diff --git a/src/pymatgen/io/jdftx/generic_tags.py b/src/pymatgen/io/jdftx/generic_tags.py index 2d225453013..1a7bc88ae2e 100644 --- a/src/pymatgen/io/jdftx/generic_tags.py +++ b/src/pymatgen/io/jdftx/generic_tags.py @@ -100,7 +100,7 @@ def _validate_value_type( value = [self.read(tag, str(x)) for x in value] if self.can_repeat else self.read(tag, str(value)) tag, is_valid, value = self._validate_value_type(type_check, tag, value) except (TypeError, ValueError): - warning = f"Could not fix the typing for {tag} " + warning = f"Could not fix the typing for tag '{tag}'" try: warning += f"{value}!" except (ValueError, TypeError): @@ -110,7 +110,7 @@ def _validate_value_type( def _validate_repeat(self, tag: str, value: Any) -> None: if not isinstance(value, list): - raise TypeError(f"The {tag} tag can repeat but is not a list: {value}") + raise TypeError(f"The '{tag}' tag can repeat but is not a list: '{value}'") @abstractmethod def read(self, tag: str, value_str: str) -> Any: @@ -124,6 +124,39 @@ def read(self, tag: str, value_str: str) -> Any: Any: The parsed value. """ + def _general_read_validate(self, tag: str, value_str: Any) -> None: + """General validation for values to be passed to a read method.""" + try: + value = str(value_str) + except (ValueError, TypeError): + value = "(unstringable)" + if not isinstance(value_str, str): + raise TypeError(f"Value '{value}' for '{tag}' should be a string!") + + def _single_value_read_validate(self, tag: str, value: str) -> None: + """Validation for values to be passed to a read method for AbstractTag inheritors that only + read a single value.""" + self._general_read_validate(tag, value) + if len(value.split()) > 1: + raise ValueError(f"'{value}' for '{tag}' should not have a space in it!") + + def _check_unread_values(self, tag: str, unread_values: list[str]) -> None: + """Check for unread values and raise an error if any are found. Used in the read method of TagContainers.""" + if len(unread_values) > 0: + raise ValueError( + f"Something is wrong in the JDFTXInfile formatting, the following values for tag '{tag}' " + f"were not processed: {unread_values}" + ) + + def _check_nonoptional_subtags(self, tag: str, subdict: dict[str, Any], subtags: dict[str, AbstractTag]) -> None: + """Check for non-optional subtags and raise an error if any are missing. + Used in the read method of TagContainers.""" + for subtag, subtag_type in subtags.items(): + if not subtag_type.optional and subtag not in subdict: + raise ValueError( + f"The subtag '{subtag}' for tag '{tag}' is not optional but was not populated during the read!" + ) + @abstractmethod def write(self, tag: str, value: Any) -> str: """Write the tag and its value as a string. @@ -149,7 +182,7 @@ def _write(self, tag: str, value: Any, multiline_override: bool = False) -> str: if self.multiline_tag or multiline_override: tag_str += "\\\n" if self.write_value: - tag_str += f"{value} " + tag_str += f"{value}".strip() + " " return tag_str def _get_token_len(self) -> int: @@ -165,7 +198,7 @@ def get_list_representation(self, tag: str, value: Any) -> list | list[list]: Returns: list | list[list]: The value converted to a list representation. """ - raise ValueError(f"Tag object has no get_list_representation method: {tag}") + raise ValueError(f"Tag object with tag '{tag}' has no get_list_representation method") def get_dict_representation(self, tag: str, value: Any) -> dict | list[dict]: """Convert the value to a dict representation. @@ -177,7 +210,7 @@ def get_dict_representation(self, tag: str, value: Any) -> dict | list[dict]: Returns: dict | list[dict]: The value converted to a dict representation. """ - raise ValueError(f"Tag object has no get_dict_representation method: {tag}") + raise ValueError(f"Tag object with tag '{tag}' has no get_dict_representation method") @dataclass @@ -234,8 +267,7 @@ def read(self, tag: str, value: str) -> bool: Returns: bool: The parsed boolean value. """ - if len(value.split()) > 1: - raise ValueError(f"'{value}' for {tag} should not have a space in it!") + self._single_value_read_validate(tag, value) try: if not self.write_value: # accounts for exceptions where only the tagname is used, e.g. @@ -246,7 +278,7 @@ def read(self, tag: str, value: str) -> bool: self.raise_value_error(tag, value) return self._TF_options["read"][value] except (ValueError, TypeError, KeyError) as err: - raise ValueError(f"Could not set '{value}' as True/False for {tag}!") from err + raise ValueError(f"Could not set '{value}' as True/False for tag '{tag}'!") from err def write(self, tag: str, value: Any) -> str: """Write the tag and its value as a string. @@ -303,16 +335,10 @@ def read(self, tag: str, value: str) -> str: Returns: str: The parsed string value. """ - # This try except block needs to go before the value.split check - try: - value = str(value) - except (ValueError, TypeError) as err: - raise ValueError(f"Could not set (unstringable) to a str for {tag}!") from err - if len(value.split()) > 1: - raise ValueError(f"'{value}' for {tag} should not have a space in it!") + self._single_value_read_validate(tag, value) if self.options is None or value in self.options: return value - raise ValueError(f"The '{value}' string must be one of {self.options} for {tag}") + raise ValueError(f"The string value '{value}' must be one of {self.options} for tag '{tag}'") def write(self, tag: str, value: Any) -> str: """Write the tag and its value as a string. @@ -366,14 +392,11 @@ def read(self, tag: str, value: str) -> int: Returns: int: The parsed integer value. """ - if not isinstance(value, str): - raise TypeError(f"Value {value} for {tag} should be a string!") - if len(value.split()) > 1: - raise ValueError(f"'{value}' for {tag} should not have a space in it!") + self._single_value_read_validate(tag, value) try: return int(float(value)) except (ValueError, TypeError) as err: - raise ValueError(f"Could not set '{value}' to a int for {tag}!") from err + raise ValueError(f"Could not set value '{value}' to an int for tag '{tag}'!") from err def write(self, tag: str, value: Any) -> str: """Write the tag and its value as a string. @@ -429,14 +452,11 @@ def read(self, tag: str, value: str) -> float: Returns: float: The parsed float value. """ - if not isinstance(value, str): - raise TypeError(f"Value {value} for {tag} should be a string!") - if len(value.split()) > 1: - raise ValueError(f"'{value}' for {tag} should not have a space in it!") + self._single_value_read_validate(tag, value) try: value_float = float(value) except (ValueError, TypeError) as err: - raise ValueError(f"Could not set '{value}' to a float for {tag}!") from err + raise ValueError(f"Could not set value '{value}' to a float for tag '{tag}'!") from err return value_float def write(self, tag: str, value: Any) -> str: @@ -508,11 +528,8 @@ def read(self, tag: str, value: str) -> str: Returns: str: The parsed string value. """ - try: - value = str(value) - except (ValueError, TypeError) as err: - raise ValueError(f"Could not set (unstringable) to a str for {tag}!") from err - return value + self._general_read_validate(tag, value) + return str(value) def write(self, tag: str, value: Any) -> str: """Write the tag and its value as a string. @@ -557,7 +574,7 @@ def _validate_single_entry( self, value: dict | list[dict], try_auto_type_fix: bool = False ) -> tuple[list[str], list[bool], Any]: if not isinstance(value, dict): - raise TypeError(f"This tag should be a dict: {value}, which is of the type {type(value)}") + raise TypeError(f"The value '{value}' (of type {type(value)}) must be a dict for this TagContainer!") tags_checked: list[str] = [] types_checks: list[bool] = [] updated_value = deepcopy(value) @@ -625,6 +642,7 @@ def read(self, tag: str, value: str) -> dict: Returns: dict: The parsed value. """ + self._general_read_validate(tag, value) value_list = value.split() if tag == "ion": special_constraints = [x in ["HyperPlane", "Linear", "None", "Planar"] for x in value_list] @@ -647,7 +665,9 @@ def read(self, tag: str, value: str) -> dict: subtag_count = value_list.count(subtag) # Get number of times subtag appears in line if not subtag_type.can_repeat: if subtag_count > 1: - raise ValueError(f"Subtag {subtag} is not allowed to repeat repeats in {tag}'s value {value}") + raise ValueError( + f"Subtag '{subtag}' for tag '{tag}' is not allowed to repeat but repeats value {value}" + ) idx_start = value_list.index(subtag) token_len = subtag_type.get_token_len() idx_end = idx_start + token_len @@ -680,14 +700,10 @@ def read(self, tag: str, value: str) -> dict: del value_list[0] # reorder all tags to match order of __MASTER_TAG_LIST__ and do coarse-grained validation of read. + subdict = {x: tempdict[x] for x in self.subtags if x in tempdict} - for subtag, subtag_type in self.subtags.items(): - if not subtag_type.optional and subtag not in subdict: - raise ValueError(f"The {subtag} tag is not optional but was not populated during the read!") - if len(value_list) > 0: - raise ValueError( - f"Something is wrong in the JDFTXInfile formatting, some values were not processed: {value}" - ) + self._check_nonoptional_subtags(tag, subdict, self.subtags) + self._check_unread_values(tag, value_list) return subdict def write(self, tag: str, value: Any) -> str: @@ -702,7 +718,7 @@ def write(self, tag: str, value: Any) -> str: """ if not isinstance(value, dict): raise TypeError( - f"value = {value}\nThe value to the {tag} write method must be a dict since it is a TagContainer!" + f"The value '{value}' (of type {type(value)}) for tag '{tag}' must be a dict for this TagContainer!" ) final_value = "" @@ -714,9 +730,10 @@ def write(self, tag: str, value: Any) -> str: # if it is not a list, then the tag will still be printed by the else # this could be relevant if someone manually sets the tag's can_repeat value to a non-list. print_str_list = [self.subtags[subtag].write(subtag, entry) for entry in subvalue] - print_str = " ".join(print_str_list) + print_str = " ".join([v.strip() for v in print_str_list]) + " " + # print_str = " ".join(print_str_list) else: - print_str = self.subtags[subtag].write(subtag, subvalue) + print_str = self.subtags[subtag].write(subtag, subvalue).strip() + " " if self.multiline_tag: final_value += f"{indent}{print_str}\\\n" @@ -751,7 +768,7 @@ def get_token_len(self) -> int: def _make_list(self, value: dict) -> list: if not isinstance(value, dict): - raise TypeError(f"The value {value} is not a dict, so could not be converted") + raise TypeError(f"The value '{value}' is not a dict, so could not be converted") value_list = [] for subtag, subtag_value in value.items(): subtag_type = self.subtags[subtag] @@ -799,12 +816,18 @@ def get_list_representation(self, tag: str, value: Any) -> list: # cannot repeat: list of bool/str/int/float (elec-cutoff) # cannot repeat: list of lists (lattice) if self.can_repeat and not isinstance(value, list): - raise ValueError("Values for repeatable tags must be a list here") + raise ValueError( + f"Value '{value}' must be a list when passed to 'get_list_representation' since " + f"tag '{tag}' is repeatable." + ) if self.can_repeat: if all(isinstance(entry, list) for entry in value): return value # no conversion needed if any(not isinstance(entry, dict) for entry in value): - raise ValueError(f"The {tag} tag set to {value} must be a list of dict") + raise ValueError( + f"The tag '{tag}' set to value '{value}' must be a list of dicts when passed to " + "'get_list_representation' since the tag is repeatable." + ) tag_as_list = [self._make_list(entry) for entry in value] else: tag_as_list = self._make_list(value) @@ -815,11 +838,17 @@ def _check_for_mixed_nesting(tag: str, value: Any) -> None: has_nested_dict = any(isinstance(x, dict) for x in value) has_nested_list = any(isinstance(x, list) for x in value) if has_nested_dict and has_nested_list: - raise ValueError(f"{tag} with {value} cannot have nested lists/dicts mixed with bool/str/int/floats!") + raise ValueError( + f"tag '{tag}' with value '{value}' cannot have nested lists/dicts mixed with bool/str/int/floats!" + ) if has_nested_dict: - raise ValueError(f"{tag} with {value} cannot have nested dicts mixed with bool/str/int/floats!") + raise ValueError( + f"tag '{tag}' with value '{value}' cannot have nested dicts mixed with bool/str/int/floats!" + ) if has_nested_list: - raise ValueError(f"{tag} with {value} cannot have nested lists mixed with bool/str/int/floats!") + raise ValueError( + f"tag '{tag}' with value '{value}' cannot have nested lists mixed with bool/str/int/floats!" + ) def _make_str_for_dict(self, tag: str, value_list: list) -> str: """Convert the value to a string representation. @@ -848,12 +877,15 @@ def get_dict_representation(self, tag: str, value: list) -> dict | list[dict]: # convert list or list of lists representation into string the TagContainer can process back into (nested) dict if self.can_repeat and not isinstance(value, list): - raise ValueError("Values for repeatable tags must be a list here") + raise ValueError( + f"Value '{value}' must be a list when passed to 'get_dict_representation' since " + f"tag '{tag}' is repeatable." + ) if ( self.can_repeat and len({len(x) for x in value}) > 1 ): # Creates a list of every unique length of the subdicts # TODO: Populate subdicts with fewer entries with JDFTx defaults to make compatible - raise ValueError(f"The values for {tag} {value} provided in a list of lists have different lengths") + raise ValueError(f"The values '{value}' for tag '{tag}' provided in a list of lists have different lengths") value = value.tolist() if isinstance(value, np.ndarray) else value # there are 4 types of TagContainers in the list representation: @@ -959,9 +991,10 @@ def get_format_index_for_str_value(self, tag: str, value: str) -> int: return i except (ValueError, TypeError) as e: problem_log.append(f"Format {i}: {e}") - errormsg = f"No valid read format for '{tag} {value}' tag\n" - "Add option to format_options or double-check the value string and retry!\n\n" - raise ValueError(errormsg) + raise ValueError( + f"No valid read format for tag '{tag}' with value '{value}'\n" + "Add option to format_options or double-check the value string and retry!\n\n" + ) def raise_invalid_format_option_error(self, tag: str, i: int) -> None: """Raise an error for an invalid format option. @@ -973,7 +1006,7 @@ def raise_invalid_format_option_error(self, tag: str, i: int) -> None: Raises: ValueError: If the format option is invalid. """ - raise ValueError(f"{tag} option {i} is not it: validation failed") + raise ValueError(f"tag '{tag}' failed to validate for option {i}") def _determine_format_option(self, tag: str, value_any: Any, try_auto_type_fix: bool = False) -> tuple[int, Any]: """Determine the format option for the value of this tag. @@ -1006,9 +1039,10 @@ def _determine_format_option(self, tag: str, value_any: Any, try_auto_type_fix: return i, value except (ValueError, TypeError, KeyError) as e: exceptions.append(e) - err_str = f"The format for {tag} for:\n{value_any}\ncould not be determined from the available options! " - "Check your inputs and/or MASTER_TAG_LIST!" - raise ValueError(err_str) + raise ValueError( + f"The format for tag '{tag}' with value '{value_any}' could not be determined from the available options! " + "Check your inputs and/or MASTER_TAG_LIST!" + ) def get_token_len(self) -> int: """Get the token length of the tag. @@ -1043,6 +1077,7 @@ def read(self, tag: str, value_str: str) -> dict: Returns: dict: The parsed value. """ + self._general_read_validate(tag, value_str) value = value_str.split() tempdict = {} for subtag, subtag_type in self.subtags.items(): @@ -1053,13 +1088,8 @@ def read(self, tag: str, value_str: str) -> dict: tempdict[subtag] = subtag_type.read(subtag, subtag_value) del value[idx_start:idx_end] subdict = {x: tempdict[x] for x in self.subtags if x in tempdict} - for subtag, subtag_type in self.subtags.items(): - if not subtag_type.optional and subtag not in subdict: - raise ValueError(f"The {subtag} tag is not optional but was not populated during the read!") - if len(value) > 0: - raise ValueError( - f"Something is wrong in the JDFTXInfile formatting, some values were not processed: {value}" - ) + self._check_nonoptional_subtags(tag, subdict, self.subtags) + self._check_unread_values(tag, value) return subdict @@ -1083,6 +1113,7 @@ def read(self, tag: str, value_str: str) -> dict: Returns: dict: The parsed value. """ + self._general_read_validate(tag, value_str) value = value_str.split() tempdict = {} # Each subtag is a freq, which will be a BoolTagContainer @@ -1095,10 +1126,7 @@ def read(self, tag: str, value_str: str) -> dict: # reorder all tags to match order of __MASTER_TAG_LIST__ and do coarse-grained validation of read subdict = {x: tempdict[x] for x in self.subtags if x in tempdict} # There are no forced subtags for dump - if len(value) > 0: - raise ValueError( - f"Something is wrong in the JDFTXInfile formatting, some values were not processed: {value}" - ) + self._check_unread_values(tag, value) return subdict diff --git a/src/pymatgen/io/jdftx/inputs.py b/src/pymatgen/io/jdftx/inputs.py index 4649bd45614..76307890224 100644 --- a/src/pymatgen/io/jdftx/inputs.py +++ b/src/pymatgen/io/jdftx/inputs.py @@ -170,6 +170,7 @@ def get_text_list(self) -> list[str]: i, _ = tag_object._determine_format_option(tag, self_as_dict[tag]) tag_object = tag_object.format_options[i] if tag_object.can_repeat and isinstance(self_as_dict[tag], list): + # text += " ".join([tag_object.write(tag, entry) for entry in self_as_dict[tag]]) text += [tag_object.write(tag, entry) for entry in self_as_dict[tag]] else: text.append(tag_object.write(tag, self_as_dict[tag])) @@ -630,7 +631,7 @@ def append_tag(self, tag: str, value: Any) -> None: i, _ = tag_object._determine_format_option(tag, value) tag_object = tag_object.format_options[i] if not tag_object.can_repeat: - raise ValueError(f"The {tag} tag cannot be repeated and thus cannot be appended") + raise ValueError(f"The tag '{tag}' cannot be repeated and thus cannot be appended") params: dict[str, Any] = self.as_dict(skip_module_keys=True) processed_value = tag_object.read(tag, value) if isinstance(value, str) else value params = self._store_value(params, tag_object, tag, processed_value) diff --git a/tests/io/jdftx/test_generic_tags.py b/tests/io/jdftx/test_generic_tags.py index ed9bcc6d2ab..4fd3e8a9580 100644 --- a/tests/io/jdftx/test_generic_tags.py +++ b/tests/io/jdftx/test_generic_tags.py @@ -16,6 +16,22 @@ ) from pymatgen.io.jdftx.jdftxinfile_master_format import get_dump_tag_container, get_tag_object +from .shared_test_utils import assert_same_value + + +class Unstringable: + """Dummy class that cannot be converted to a string""" + + def __str__(self): + raise ValueError("Cannot convert to string") + + +class NonIterable: + """Dummy class that cannot be iterated through""" + + def __iter__(self): + raise ValueError("Cannot iterate through this object") + def test_abstract_tag(): with pytest.raises(TypeError): @@ -31,75 +47,118 @@ def test_stringify(): def test_bool_tag(): + """Expected behavior of BoolTag is tested here""" bool_tag = BoolTag(write_value=False) - with pytest.raises(ValueError, match="Tag object has no get_list_representation method: barbie"): - bool_tag.get_list_representation("barbie", "ken") - with pytest.raises(ValueError, match="Tag object has no get_dict_representation method: barbie"): - bool_tag.get_dict_representation("barbie", "ken") - with pytest.raises(ValueError, match="'non-empty-value sdfgsd' for barbie should not have a space in it!"): - bool_tag.read("barbie", "non-empty-value sdfgsd") - with pytest.raises(ValueError, match="Could not set 'non-bool-like' as True/False for barbie!"): - bool_tag.read("barbie", "non-bool-like") - bool_tag = BoolTag(write_value=True) - with pytest.raises(ValueError, match="Could not set 'not-appearing-in-read-TF-options' as True/False for barbie!"): - bool_tag.read("barbie", "not-appearing-in-read-TF-options") - with pytest.raises(TypeError): - bool_tag._validate_repeat("barbie", "ken") - - -class Unstringable: - def __str__(self): - raise ValueError("Cannot convert to string") + # Values with spaces are impossible to interpret as bools + tag = "barbie" + value = "this string has spaces" + with pytest.raises(ValueError, match=f"'{value}' for '{tag}' should not have a space in it!"): + bool_tag.read(tag, value) + # Value errors should be raised if value cannot be conveniently converted to a bool + value = "non-bool-like" + with pytest.raises(ValueError, match=f"Could not set '{value}' as True/False for tag '{tag}'!"): + bool_tag.read(tag, value) + # bool_tag = BoolTag(write_value=True) + # Only values appearing in the "_TF_options" map can be converted to bool (allows for yes/no to be interpreted + # as bools) + value = "not-appearing-in-read-TF-options" + with pytest.raises(ValueError, match=f"Could not set '{value}' as True/False for tag '{tag}'!"): + bool_tag.read(tag, value) -class NonIterable: - def __iter__(self): - raise ValueError("Cannot iterate through this object") +def test_abstract_tag_inheritor(): + """Expected behavior of methods inherited from AbstractTag are tested here + (AbstractTag cannot be directly initiated)""" + bool_tag = BoolTag(write_value=False) + tag = "barbie" + value = "ken" + # Abstract-tag inheritors should raise the following error for calling get_list_representation unless + # they have implemented the method (bool_tag does not and should not) + with pytest.raises(ValueError, match=f"Tag object with tag '{tag}' has no get_list_representation method"): + bool_tag.get_list_representation(tag, value) + # Abstract-tag inheritors should raise the following error for calling get_dict_representation unless + # they have implemented the method (bool_tag does not and should not) + with pytest.raises(ValueError, match=f"Tag object with tag '{tag}' has no get_dict_representation method"): + bool_tag.get_dict_representation(tag, value) + # "_validate_repeat" is only called if "can_repeat" is True, but must raise an error if value is not a list + with pytest.raises(TypeError): + bool_tag._validate_repeat(tag, value) def test_str_tag(): str_tag = StrTag(options=["ken"]) - with pytest.raises(ValueError, match="'ken allan' for barbie should not have a space in it!"): - str_tag.read("barbie", "ken allan") - with pytest.raises(ValueError, match=re.escape("Could not set (unstringable) to a str for barbie!")): - str_tag.read("barbie", Unstringable()) - with pytest.raises(ValueError, match=re.escape(f"The 'allan' string must be one of {['ken']} for barbie")): - str_tag.read("barbie", "allan") # Allan is not an option + # Values with spaces are rejected as spaces are occasionally used as delimiters in input files + tag = "barbie" + value = "ken, allan" + with pytest.raises(ValueError, match=f"'{value}' for '{tag}' should not have a space in it!"): + str_tag.read(tag, value) + # Values that cannot be converted to strings have a safety net error message + value = Unstringable() + print_value = "(unstringable)" + with pytest.raises(TypeError, match=re.escape(f"Value '{print_value}' for '{tag}' should be a string!")): + str_tag.read(tag, value) + # "str_tag" here was initiated with only "ken" as an option, so "allan" should raise an error + # (barbie is the tagname, not the value) + value = "allan" + with pytest.raises( + ValueError, match=re.escape(f"The string value '{value}' must be one of {str_tag.options} for tag '{tag}'") + ): + str_tag.read(tag, value) + # If both the tagname and value are written, two tokens are to be written + str_tag = StrTag(write_tagname=True, write_value=True) assert str_tag.get_token_len() == 2 - str_tag = StrTag(write_tagname=False, write_value=False) - assert str_tag.get_token_len() == 0 + # If only the tagname is written, one token is to be written str_tag = StrTag(write_tagname=True, write_value=False) assert str_tag.get_token_len() == 1 + # If only the value is written, one token is to be written str_tag = StrTag(write_tagname=False, write_value=True) assert str_tag.get_token_len() == 1 + # If neither the tagname nor the value are written, no tokens are to be written + # (this is useless, but it is a valid option) + str_tag = StrTag(write_tagname=False, write_value=False) + assert str_tag.get_token_len() == 0 def test_int_tag(): int_tag = IntTag() - with pytest.raises(ValueError, match="'ken, allan' for barbie should not have a space in it!"): - int_tag.read("barbie", "ken, allan") - with pytest.raises(TypeError): - int_tag.read("barbie", {}) - with pytest.raises(ValueError, match="Could not set 'ken' to a int for barbie!"): - int_tag.read("barbie", "ken") # (ken is not an integer) + # Values with spaces are rejected as spaces are occasionally used as delimiters in input files + value = "ken, allan" + tag = "barbie" + with pytest.raises(ValueError, match=f"'{value}' for '{tag}' should not have a space in it!"): + int_tag.read(tag, value) + # Values passed to "read" must be strings + value = {} + with pytest.raises(TypeError, match=f"Value '{value}' for '{tag}' should be a string!"): + int_tag.read(tag, value) + # Values must be able to be type-cast to an integer + value = "ken" # (ken is not an integer) + with pytest.raises(ValueError, match=f"Could not set value '{value}' to an int for tag '{tag}'!"): + int_tag.read(tag, value) def test_float_tag(): float_tag = FloatTag() - with pytest.raises(ValueError, match="'ken, allan' for barbie should not have a space in it!"): - float_tag.read("barbie", "ken, allan") + tag = "barbie" + value = "ken, allan" + with pytest.raises(ValueError, match=f"'{value}' for '{tag}' should not have a space in it!"): + float_tag.read(tag, value) with pytest.raises(TypeError): - float_tag.read("barbie", {}) - with pytest.raises(ValueError, match="Could not set 'ken' to a float for barbie!"): - float_tag.read("barbie", "ken") # (ken is not an integer) - with pytest.raises(ValueError, match="Could not set '1.2.3' to a float for barbie!"): - float_tag.read("barbie", "1.2.3") # (1.2.3 cannot be a float) + float_tag.read(tag, {}) + value = "ken" # (ken is not an number) + with pytest.raises(ValueError, match=f"Could not set value '{value}' to a float for tag '{tag}'!"): + float_tag.read(tag, value) + value = "1.2.3" # (1.2.3 cannot be a float) + with pytest.raises(ValueError, match=f"Could not set value '{value}' to a float for tag '{tag}'!"): + float_tag.read("barbie", "1.2.3") def test_initmagmomtag(): initmagmomtag = InitMagMomTag(write_tagname=True) - with pytest.raises(ValueError, match=re.escape("Could not set (unstringable) to a str for tag!")): - initmagmomtag.read("tag", Unstringable()) + tag = "tag" + value = Unstringable() + print_value = "(unstringable)" + with pytest.raises(TypeError, match=re.escape(f"Value '{print_value}' for '{tag}' should be a string!")): + initmagmomtag.read(tag, value) assert initmagmomtag.read("tag", "42") == "42" assert initmagmomtag.write("magtag", 42) == "magtag 42 " initmagmomtag = InitMagMomTag(write_tagname=False) @@ -115,203 +174,259 @@ def test_initmagmomtag(): initmagmomtag.validate_value_type("tag", Unstringable(), try_auto_type_fix=True) -def test_tagcontainer(): +def test_tagcontainer_validation(): + tag = "barbie" + repeatable_str_subtag = "ken" + non_repeatable_int_subtag = "allan" + non_intable_value = "nonintable" tagcontainer = TagContainer( - can_repeat=True, + can_repeat=False, subtags={ - "ken": StrTag(), - "allan": IntTag(can_repeat=False), + f"{repeatable_str_subtag}": StrTag(can_repeat=True), + f"{non_repeatable_int_subtag}": IntTag(can_repeat=False), }, ) - with pytest.raises(TypeError): - tagcontainer._validate_single_entry("barbie") # Not a dict - val = [{"ken": 1}, {"ken": 2, "allan": 3}] - with pytest.raises( - ValueError, - match=re.escape(f"The values for barbie {val} provided in a list of lists have different lengths"), - ): - tagcontainer.validate_value_type("barbie", val) + # issues with converting values to the correct type should only raise a warning within validate_value_type with pytest.warns(Warning): tagcontainer.validate_value_type( - "barbie", - [ - {"ken": "1"}, - {"allan": "barbie"}, # Raises a warning since barbie cannot be converted to int - ], + f"{tag}", {f"{repeatable_str_subtag}": ["1"], f"{non_repeatable_int_subtag}": f"{non_intable_value}"} ) + # _validate_single_entry for TagContainer should raise an error if the value is not a dict + with pytest.raises(TypeError): + tagcontainer._validate_single_entry("not a dict") # Not a dict + # inhomogeneous values for repeated TagContainers should raise an error + # until filling with default values is implemented + tagcontainer.can_repeat = True + value = [{"ken": 1}, {"ken": 2, "allan": 3}] with pytest.raises( - ValueError, match="Subtag allan is not allowed to repeat repeats in barbie's value allan 1 allan 2" + ValueError, + match=re.escape(f"The values '{value}' for tag '{tag}' provided in a list of lists have different lengths"), ): - tagcontainer.read("barbie", "allan 1 allan 2") - ### + tagcontainer.validate_value_type(tag, value) + + +def test_tagcontainer_mixed_nesting(): + tag = "barbie" + str_subtag = "ken" + int_subtag = "allan" tagcontainer = TagContainer( can_repeat=False, subtags={ - "ken": StrTag(), - "allan": IntTag(), + f"{str_subtag}": StrTag(), + f"{int_subtag}": IntTag(), }, ) - with pytest.warns(Warning): - tagcontainer.validate_value_type( - "barbie", - {"ken": "1", "allan": "barbie"}, # Raises a warning since barbie cannot be converted to int - ) - ### + tagcontainer_mixed_nesting_tester(tagcontainer, tag, str_subtag, int_subtag) + + +def tagcontainer_mixed_nesting_tester(tagcontainer, tag, str_subtag, int_subtag): + list_of_dicts_and_lists = [{str_subtag: "b"}, [[int_subtag, 1]]] + list_of_dicts_and_lists_err = f"tag '{tag}' with value '{list_of_dicts_and_lists}' cannot have" + " nested lists/dicts mixed with bool/str/int/floats!" + list_of_dicts_of_strs_and_ints = [{str_subtag: "b"}, {int_subtag: 1}] + list_of_dicts_of_strs_and_ints_err = f"tag '{tag}' with value '{list_of_dicts_of_strs_and_ints}' " + "cannot have nested dicts mixed with bool/str/int/floats!" + list_of_lists_of_strs_and_ints = [[str_subtag, "b"], [int_subtag, 1]] + list_of_lists_of_strs_and_ints_err = f"tag '{tag}' with value '{list_of_lists_of_strs_and_ints}' " + "cannot have nested lists mixed with bool/str/int/floats!" + for value, err_str in zip( + [list_of_dicts_and_lists, list_of_dicts_of_strs_and_ints, list_of_lists_of_strs_and_ints], + [list_of_dicts_and_lists_err, list_of_dicts_of_strs_and_ints_err, list_of_lists_of_strs_and_ints_err], + strict=False, + ): + with pytest.raises( + ValueError, + match=re.escape(err_str), + ): + tagcontainer._check_for_mixed_nesting(tag, value) + + +def test_tagcontainer_read(): + tag = "barbie" + repeatable_str_subtag = "ken" + non_repeatable_int_subtag = "allan" tagcontainer = TagContainer( - can_repeat=True, + can_repeat=False, subtags={ - "ken": StrTag(can_repeat=True), - "allan": IntTag(can_repeat=False), + f"{repeatable_str_subtag}": StrTag(can_repeat=True), + f"{non_repeatable_int_subtag}": IntTag(can_repeat=False), }, ) - assert isinstance(tagcontainer.read("barbie", "ken 1 ken 2 allan 3"), dict) - assert isinstance(tagcontainer.read("barbie", "ken 1 ken 2 allan 3")["ken"], list) - assert not isinstance(tagcontainer.read("barbie", "ken 1 ken 2 allan 3")["allan"], list) - with pytest.raises(TypeError): - tagcontainer.write("barbie", [{"ken": 1}]) - ### - v1 = tagcontainer.write("barbie", {"ken": [1, 2]}).strip().split() - v1 = [v.strip() for v in v1] - v2 = "barbie ken 1 ken 2".split() - assert len(v1) == len(v2) - for i in range(len(v1)): - assert v1[i] == v2[i] - ### + # non-repeatable subtags that repeat in the value for "read" should raise an error + value = f"{non_repeatable_int_subtag} 1 {non_repeatable_int_subtag} 2" + with pytest.raises( + ValueError, + match=f"Subtag '{non_repeatable_int_subtag}' for tag '{tag}' is not allowed to repeat " + f"but repeats value {value}", + ): + tagcontainer.read(tag, value) + # output of "read" should be a dict of subtags, with list values for repeatable subtags and single values for + # non-repeatable subtags + assert_same_value( + tagcontainer.read(f"{tag}", "ken a ken b allan 3"), + {"ken": ["a", "b"], "allan": 3}, + ) + required_subtag = "ken" + optional_subtag = "allan" tagcontainer = TagContainer( can_repeat=True, write_tagname=True, subtags={ - "ken": StrTag(optional=False, write_tagname=True, write_value=True), - "allan": IntTag(optional=True, write_tagname=True, write_value=True), + required_subtag: StrTag(optional=False, write_tagname=True, write_value=True), + optional_subtag: IntTag(optional=True, write_tagname=True, write_value=True), }, ) with pytest.raises( - ValueError, match=re.escape("The ken tag is not optional but was not populated during the read!") + ValueError, + match=re.escape( + f"The subtag '{required_subtag}' for tag '{tag}' is not optional but was not populated during the read!" + ), ): tagcontainer.read("barbie", "allan 1") + unread_values = "fgfgfgf" + value = f"ken a {unread_values}" with pytest.raises( ValueError, match=re.escape( - "Something is wrong in the JDFTXInfile formatting, some values were not processed: ken barbie fgfgfgf" + f"Something is wrong in the JDFTXInfile formatting, the following values for tag '{tag}' " + f"were not processed: {[unread_values]}" ), ): - tagcontainer.read("barbie", "ken barbie fgfgfgf") - assert tagcontainer.get_token_len() == 3 - with pytest.raises(TypeError): - tagcontainer.write("barbie", ["ken barbie"]) + tagcontainer.read(tag, value) ### tagcontainer = get_tag_object("ion") with pytest.warns(Warning): tagcontainer.read("ion", "Fe 1 1 1 1 HyperPlane") - with pytest.raises(ValueError, match="Values for repeatable tags must be a list here"): - tagcontainer.get_dict_representation("ion", "Fe 1 1 1 1") - ### + + +def test_tagcontainer_write(): + tag = "barbie" + repeatable_str_subtag = "ken" + non_repeatable_int_subtag = "allan" tagcontainer = TagContainer( - can_repeat=True, - allow_list_representation=False, + can_repeat=False, subtags={ - "ken": StrTag(), - "allan": IntTag(), + f"{repeatable_str_subtag}": StrTag(can_repeat=True), + f"{non_repeatable_int_subtag}": IntTag(can_repeat=False), }, ) - strmatch = str([{"ken": "b"}, [["allan", 1]]]) - with pytest.raises( - ValueError, - match=re.escape(f"barbie with {strmatch} cannot have nested lists/dicts mixed with bool/str/int/floats!"), - ): - tagcontainer._check_for_mixed_nesting("barbie", [{"ken": "b"}, [["allan", 1]]]) - strmatch = str([{"ken": "b"}, {"allan": 1}]) - with pytest.raises( - ValueError, - match=re.escape(f"barbie with {strmatch} cannot have nested dicts mixed with bool/str/int/floats!"), - ): - tagcontainer._check_for_mixed_nesting("barbie", [{"ken": "b"}, {"allan": 1}]) - strmatch = str([["ken", "b"], ["allan", 1]]) + assert_same_value( + tagcontainer.write(tag, {repeatable_str_subtag: ["a", "b"]}), + f"{tag} {repeatable_str_subtag} a {repeatable_str_subtag} b ", + ) + tagcontainer.subtags[repeatable_str_subtag].write_tagname = False + assert_same_value(tagcontainer.write(tag, {repeatable_str_subtag: ["a", "b"]}), f"{tag} a b ") + # Lists are reserved for repeatable tagcontainers + value = [{"ken": 1}] with pytest.raises( - ValueError, - match=re.escape(f"barbie with {strmatch} cannot have nested lists mixed with bool/str/int/floats!"), + TypeError, + match=re.escape( + f"The value '{value}' (of type {type(value)}) for tag '{tag}' must be a dict for this TagContainer!" + ), ): - tagcontainer._check_for_mixed_nesting("barbie", [["ken", "b"], ["allan", 1]]) - ### + tagcontainer.write(tag, value) + + +def test_tagcontainer_list_dict_conversion(): + top_subtag = "universe" + bottom_subtag1 = "sun" + bottom_subtag2 = "moon" tagcontainer = TagContainer( can_repeat=True, allow_list_representation=False, subtags={ - "universe": TagContainer( + top_subtag: TagContainer( allow_list_representation=True, write_tagname=True, subtags={ - "sun": BoolTag( + bottom_subtag1: BoolTag( + write_tagname=False, allow_list_representation=False, ), - "moon": BoolTag( + bottom_subtag2: BoolTag( + write_tagname=True, allow_list_representation=False, ), }, ) }, ) - subtag = "universe" - value = {"universe": "True False"} - err_str = f"The subtag {subtag} is not a dict: '{value[subtag]}', so could not be converted" - with pytest.raises(ValueError, match=re.escape(err_str)): - tagcontainer._make_list(value) - value = {"universe": {"sun": "True", "moon": "False"}} - out = tagcontainer._make_list(value) - assert isinstance(out, list) - # This is not actually what I would expect, but keeping for sake of coverage for now - out_expected = ["universe", "True", "False"] - assert len(out) == len(out_expected) - for i in range(len(out)): - assert out[i] == out_expected[i] - value = [{"universe": {"sun": "True", "moon": "False"}}] - err_str = f"The value {value} is not a dict, so could not be converted" - with pytest.raises(TypeError, match=re.escape(err_str)): + notadict = f"True {bottom_subtag2} False" + value = {top_subtag: notadict} + with pytest.raises( + ValueError, match=re.escape(f"The subtag {top_subtag} is not a dict: '{notadict}', so could not be converted") + ): tagcontainer._make_list(value) - value = {"universe": {"sun": {"True": True}, "moon": "False"}} + # Despite bottom_subtag2 have "write_tagname" set to True, it is not written in the list for _make_list + # (obviously this is dangerous and this method should be avoided) + assert_same_value( + tagcontainer._make_list({top_subtag: {bottom_subtag1: "True", bottom_subtag2: "False"}}), + [top_subtag, "True", "False"], + ) + value = {top_subtag: {bottom_subtag1: {"True": True}, bottom_subtag2: "False"}} with pytest.warns(Warning): - out = tagcontainer._make_list(value) - value = [["universe", "True", "False"]] - out = tagcontainer.get_list_representation("barbie", value) - for i in range(len(out[0])): - assert out[0][i] == value[0][i] - assert len(out) == len(value) + tagcontainer._make_list(value) + value = [[top_subtag, "True", "False"]] + assert_same_value(tagcontainer.get_list_representation("barbie", value), value) tag = "barbie" value = [["universe", "True", "False"], {"universe": {"sun": True, "moon": False}}] - err_str = f"The {tag} tag set to {value} must be a list of dict" + err_str = f"The tag '{tag}' set to value '{value}' must be a list of dicts when passed to " + "'get_list_representation' since the tag is repeatable." with pytest.raises(ValueError, match=re.escape(err_str)): tagcontainer.get_list_representation(tag, value) value = {"universe": {"sun": {"True": True}, "moon": "False"}} - err_str = "Values for repeatable tags must be a list here" + err_str = f"Value '{value}' must be a list when passed to 'get_dict_representation' since " + f"tag '{tag}' is repeatable." with pytest.raises(ValueError, match=re.escape(err_str)): tagcontainer.get_dict_representation("barbie", value) + err_str = f"Value '{value}' must be a list when passed to 'get_list_representation' since " + f"tag '{tag}' is repeatable." with pytest.raises(ValueError, match=re.escape(err_str)): tagcontainer.get_list_representation("barbie", value) def test_dumptagcontainer(): dtc = get_dump_tag_container() + tag = "dump" + unread_value = "barbie" + value = f"{unread_value} End DOS" with pytest.raises( ValueError, - match=re.escape("Something is wrong in the JDFTXInfile formatting, some values were not processed: ['barbie']"), + match=re.escape( + f"Something is wrong in the JDFTXInfile formatting, the following values for tag '{tag}' " + f"were not processed: {[unread_value]}" + ), ): - dtc.read("dump", "barbie End DOS") + dtc.read(tag, value) def test_booltagcontainer(): + tag = "barbie" + required_subtag = "ken" + optional_subtag = "allan" + not_a_subtag = "alan" btc = BoolTagContainer( subtags={ - "ken": BoolTag(optional=False, write_tagname=True, write_value=False), - "allan": BoolTag(optional=True, write_tagname=True, write_value=False), + required_subtag: BoolTag(optional=False, write_tagname=True, write_value=False), + optional_subtag: BoolTag(optional=True, write_tagname=True, write_value=False), }, ) - with pytest.raises(ValueError, match="The ken tag is not optional but was not populated during the read!"): - btc.read("barbie", "allan") with pytest.raises( ValueError, - match=re.escape("Something is wrong in the JDFTXInfile formatting, some values were not processed: ['aliah']"), + match=re.escape( + f"The subtag '{required_subtag}' for tag '{tag}' is not optional but was not populated during the read!" + ), + ): + btc.read(tag, optional_subtag) + value = f"{required_subtag} {not_a_subtag}" + with pytest.raises( + ValueError, + match=re.escape( + f"Something is wrong in the JDFTXInfile formatting, the following values for tag '{tag}' " + f"were not processed: {[not_a_subtag]}" + ), ): - btc.read("barbie", "ken aliah") + btc.read(tag, value) def test_multiformattagcontainer(): @@ -322,11 +437,11 @@ def test_multiformattagcontainer(): mftg.read(tag, value) with pytest.raises(RuntimeError): mftg.write(tag, value) - errormsg = f"No valid read format for '{tag} {value}' tag\n" + errormsg = f"No valid read format for tag '{tag}' with value '{value}'\n" "Add option to format_options or double-check the value string and retry!\n\n" with pytest.raises(ValueError, match=re.escape(errormsg)): mftg.get_format_index_for_str_value(tag, value) - err_str = f"The format for {tag} for:\n{value}\ncould not be determined from the available options!" + err_str = f"The format for tag '{tag}' with value '{value}' could not be determined from the available options! " "Check your inputs and/or MASTER_TAG_LIST!" with pytest.raises(ValueError, match=re.escape(err_str)): mftg._determine_format_option(tag, value) diff --git a/tests/io/jdftx/test_jdftxinfile.py b/tests/io/jdftx/test_jdftxinfile.py index dc6de70299a..f595490b68a 100644 --- a/tests/io/jdftx/test_jdftxinfile.py +++ b/tests/io/jdftx/test_jdftxinfile.py @@ -2,6 +2,7 @@ import os import re +from copy import deepcopy from typing import TYPE_CHECKING, Any import numpy as np @@ -63,14 +64,18 @@ def test_JDFTXInfile_known_lambda(infile_fname: str, bool_func: Callable[[JDFTXI ], ) def test_JDFTXInfile_set_values(val_key: str, val: Any): + """Test value setting for various tags""" jif = JDFTXInfile.from_file(ex_infile1_fname) jif[val_key] = val + # Test that the JDFTXInfile object is still consistent JDFTXInfile_self_consistency_tester(jif) def test_JDFTXInfile_from_dict(): + """Test the from_dict method""" jif = JDFTXInfile.from_file(ex_infile1_fname) jif_dict = jif.as_dict() + # Test that dictionary can be modified and that _from_dict will fix set values jif_dict["elec-cutoff"] = 20 jif2 = JDFTXInfile.from_dict(jif_dict) JDFTXInfile_self_consistency_tester(jif2) @@ -87,8 +92,13 @@ def test_JDFTXInfile_from_dict(): ], ) def test_JDFTXInfile_append_values(val_key: str, val: Any): + """Test the append_tag method""" jif = JDFTXInfile.from_file(ex_infile1_fname) + val_old = None if val_key not in jif else deepcopy(jif[val_key]) jif.append_tag(val_key, val) + val_new = jif[val_key] + assert val_old != val_new + # Test that the append_tag does not break the JDFTXInfile object JDFTXInfile_self_consistency_tester(jif) @@ -96,42 +106,55 @@ def test_JDFTXInfile_expected_exceptions(): jif = JDFTXInfile.from_file(ex_infile1_fname) with pytest.raises(KeyError): jif["barbie"] = "ken" - with pytest.raises(ValueError, match="The initial-state tag cannot be repeated and thus cannot be appended"): - jif.append_tag("initial-state", "$VAR") + # non-repeating tags raise value-errors when appended + tag = "initial-state" + with pytest.raises(ValueError, match=re.escape(f"The tag '{tag}' cannot be repeated and thus cannot be appended")): + jif.append_tag(tag, "$VAR") + # Phonon and Wannier tags raise value-errors at _preprocess_line with pytest.raises(ValueError, match="Phonon functionality has not been added!"): jif._preprocess_line("phonon idk") with pytest.raises(ValueError, match="Wannier functionality has not been added!"): jif._preprocess_line("wannier idk") + # Tags not in MASTER_TAG_LIST raise value-errors at _preprocess_line err_str = f"The barbie tag in {['barbie', 'ken allan']} is not in MASTER_TAG_LIST and is not a comment, " - err_str += "something is wrong with this input data!" + "something is wrong with this input data!" with pytest.raises(ValueError, match=re.escape(err_str)): jif._preprocess_line("barbie ken allan") - + # include tags raise value-errors if the file cannot be found _filename = "barbie" err_str = f"The include file {_filename} ({_filename}) does not exist!" with pytest.raises(ValueError, match=re.escape(err_str)): JDFTXInfile.from_str(f"include {_filename}\n") + # If it does exist, no error should be raised filename = ex_in_files_dir / "barbie" err_str = f"The include file {_filename} ({filename}) does not exist!" str(err_str) + # If the wrong parent_path is given for a file that does exist, error with pytest.raises(ValueError, match=re.escape(err_str)): JDFTXInfile.from_str(f"include {_filename}\n", path_parent=ex_in_files_dir) + # JDFTXInfile cannot be constructed without lattice and ion tags with pytest.raises(ValueError, match="This input file is missing required structure tags"): JDFTXInfile.from_str("dump End DOS\n") + # "barbie" here is supposed to be "list-to-dict" or "dict-to-list" with pytest.raises(ValueError, match="Conversion type barbie is not 'list-to-dict' or 'dict-to-list'"): jif._needs_conversion("barbie", ["ken"]) + # Setting tags with unfixable values immediately raises an error tag = "exchange-params" value = {"blockSize": 1, "nOuterVxx": "barbie"} err_str = str(f"The {tag} tag with value:\n{value}\ncould not be fixed!") with pytest.raises(ValueError, match=re.escape(err_str)): # Implicitly tests validate_tags jif[tag] = value + # Setting tags with unfixable values through "update" side-steps the error, but will raise it once + # "validate_tags" is inevitably called jif2 = jif.copy() jif2.update({tag: value}) with pytest.raises(ValueError, match=re.escape(err_str)): jif2.validate_tags(try_auto_type_fix=True) + # The inevitable error can be reduced to a warning if you tell it not to try to fix the values with pytest.warns(UserWarning): jif2.validate_tags(try_auto_type_fix=False) + # Setting a non-string tag raises an error within the JDFTXInfile object err_str = str(f"{1.2} is not a string!") with pytest.raises(TypeError, match=err_str): jif[1.2] = 3.4 @@ -159,18 +182,24 @@ def test_JDFTXInfile_niche_cases(): def test_JDFTXInfile_add_method(): + """Test the __add__ method""" + # No new values are being assigned in jif2, so jif + jif2 should be the same as jif + # Since the convenience of this method would be lost if the user has to pay special attention to duplicating + # repeatable values, repeatable tags are not append to each other jif = JDFTXInfile.from_file(ex_infile1_fname) jif2 = jif.copy() jif3 = jif + jif2 assert_idential_jif(jif, jif3) + # If a tag is repeated, the values must be the same since choice of value is ambiguous key = "elec-ex-corr" - val_old = jif[key] + val_old = deepcopy(jif[key]) val_new = "lda" assert val_old != val_new jif2[key] = val_new err_str = f"JDFTXInfiles have conflicting values for {key}: {val_old} != {val_new}" with pytest.raises(ValueError, match=re.escape(err_str)): jif3 = jif + jif2 + # Normal expected behavior key_add = "target-mu" val_add = 0.5 assert key_add not in jif @@ -182,6 +211,7 @@ def test_JDFTXInfile_add_method(): @pytest.mark.parametrize(("infile_fname", "knowns"), [(ex_infile1_fname, ex_infile1_knowns)]) def test_JDFTXInfile_knowns_simple(infile_fname: PathLike, knowns: dict): + """Test that known values that can be tested with assert_same_value are correct""" jif = JDFTXInfile.from_file(infile_fname) for key, val in knowns.items(): assert_same_value(jif[key], val) @@ -189,11 +219,15 @@ def test_JDFTXInfile_knowns_simple(infile_fname: PathLike, knowns: dict): @pytest.mark.parametrize("infile_fname", [ex_infile3_fname, ex_infile1_fname, ex_infile2_fname]) def test_JDFTXInfile_self_consistency(infile_fname: PathLike): + """Test that JDFTXInfile objects with different assortments of tags survive inter-conversion done within + "JDFTXInfile_self_consistency_tester""" jif = JDFTXInfile.from_file(infile_fname) JDFTXInfile_self_consistency_tester(jif) def JDFTXInfile_self_consistency_tester(jif: JDFTXInfile): + """Create an assortment of JDFTXinfile created from the same data but through different methods, and test that + they are all equivalent through "assert_idential_jif" """ dict_jif = jif.as_dict() # # Commenting out tests with jif2 due to the list representation asserted jif2 = JDFTXInfile.get_dict_representation(JDFTXInfile._from_dict(dict_jif)) @@ -212,6 +246,7 @@ def JDFTXInfile_self_consistency_tester(jif: JDFTXInfile): def test_jdftxstructure(): + """Test the JDFTXStructure object associated with the JDFTXInfile object""" jif = JDFTXInfile.from_file(ex_infile2_fname) struct = jif.to_jdftxstructure(jif) assert isinstance(struct, JDFTXStructure) @@ -220,6 +255,7 @@ def test_jdftxstructure(): assert struct.natoms == 16 with open(ex_infile2_fname) as f: lines = list.copy(list(f)) + # Test different ways of creating a JDFTXStructure object create the same object if data is the same data = "\n".join(lines) struct2 = JDFTXStructure.from_str(data) assert_equiv_jdftxstructure(struct, struct2)