From 3b41962634d46730b7d7c15bcc4b8d33ed0d4513 Mon Sep 17 00:00:00 2001 From: Sina Zel taat Date: Mon, 8 Jul 2024 15:29:19 +0200 Subject: [PATCH 1/4] Refactor Exposure classes and adjust tests --- blueprints/codes/eurocode/exposure_classes.py | 166 ++++++++++ .../table_4_1.py | 153 ++------- blueprints/utils/__init__.py | 1 + blueprints/utils/abc_enum_meta.py | 34 ++ .../test_table_4_1.py | 115 ------- tests/codes/eurocode/test_exposure_classes.py | 297 ++++++++++++++++++ 6 files changed, 518 insertions(+), 248 deletions(-) create mode 100644 blueprints/codes/eurocode/exposure_classes.py create mode 100644 blueprints/utils/__init__.py create mode 100644 blueprints/utils/abc_enum_meta.py create mode 100644 tests/codes/eurocode/test_exposure_classes.py diff --git a/blueprints/codes/eurocode/exposure_classes.py b/blueprints/codes/eurocode/exposure_classes.py new file mode 100644 index 00000000..3ddb29bb --- /dev/null +++ b/blueprints/codes/eurocode/exposure_classes.py @@ -0,0 +1,166 @@ +"""Module for the concrete exposure classes +according to Table 4.1 from NEN-EN 1992-1-1: Chapter 4 - Durability and cover to reinforcement. +""" + +from abc import abstractmethod +from enum import Enum +from functools import total_ordering +from typing import NamedTuple, Type, TypeVar + +from blueprints.utils.abc_enum_meta import ABCEnumMeta + +T = TypeVar("T", bound="Exposure") + + +@total_ordering +class Exposure(Enum, metaclass=ABCEnumMeta): + """Parent class for individual exposure classes. + + This class handles the ordering/comparison operations, that's why it is decorated with total_ordering (As recommended by PEP8). + On top of that, it handles a couple of methods which will be used by its subclasses. + """ + + def __eq__(self, other: object) -> bool: + """Definition of '==' operator for the comparison of the severity of the exposure classifications. + + Parameters + ---------- + self : Exposure/ subclass of Exposure + First argument for the comparison. + other : object + Second argument for the comparison. + + Raises + ------ + TypeError + If different types are being compared. + + Returns + ------- + Boolean + True if both arguments are of the same severity (In this case they will both be literally the same). + """ + if isinstance(other, self.__class__): + _self_severity = int(self.value[-1]) if self.value != "Not applicable" else 0 + _other_severity = int(other.value[-1]) if other.value != "Not applicable" else 0 + return _self_severity == _other_severity + raise TypeError("Only the same exposure class types can be compared with each other!") + + def __gt__(self, other: object) -> bool: + """Definition of '>' operator for the comparison of the severity of the exposure classifications. + + Parameters + ---------- + self : Exposure/ subclass of Exposure + First argument for the comparison. + other : object + Second argument for the comparison. + + Raises + ------ + TypeError + If different types are being compared. + + Returns + ------- + Boolean + True if the first argument is more severe than the second argument. + """ + if isinstance(other, self.__class__): + _self_severity = int(self.value[-1]) if self.value != "Not applicable" else 0 + _other_severity = int(other.value[-1]) if other.value != "Not applicable" else 0 + return _self_severity > _other_severity + raise TypeError("Only the same exposure class types can be compared with each other!") + + @classmethod + def options(cls: Type[T]) -> list[str]: + """Return all the possible options within a subclass. + + Returns + ------- + list[str] + all the possible class designations within a specific exposure class + """ + return [m._value_ for m in cls.__members__.values()] + + @staticmethod + @abstractmethod + def exposure_class_description() -> str: + """Description of subclasses to be implemented in each subclass. + + Returns + ------- + str + description of the specific exposure class + """ + + @abstractmethod + def description_of_the_environment(self) -> str: + """Description of the environment based on the instance. + + Returns + ------- + str + description of the environment based on the instance + """ + + +@total_ordering +class CarbonationBase(Exposure): + """Enum Class which indicates the classification of corrosion induced by carbonation.""" + + +@total_ordering +class ChlorideBase(Exposure): + """Enum Class which indicates the classification of corrosion induced by chlorides other than by sea water.""" + + +@total_ordering +class ChlorideSeawaterBase(Exposure): + """Enum Class which indicates the classification of corrosion induced by chlorides from sea water.""" + + +@total_ordering +class FreezeThawBase(Exposure): + """Enum Class which indicates the classification of freeze/thaw attack with or without de-icing agents.""" + + +@total_ordering +class ChemicalBase(Exposure): + """Enum Class which indicates the classification of chemical attack.""" + + +class ExposureClassesBase(NamedTuple): + """Parent class which serves as a container for the Exposure classes. + + Exposure classes related to environmental conditions in accordance with EN 206-1 + """ + + carbonation: CarbonationBase + chloride: ChlorideBase + chloride_seawater: ChlorideSeawaterBase + freeze: FreezeThawBase + chemical: ChemicalBase + + @property + def no_risk(self) -> bool: + """Check if all exposure classes are 'Not applicable'. + + This represents X0 class designation according to table 4.1 from NEN-EN 1992-1-1+C2:2011. + + Returns + ------- + bool + True if all exposure classes are 'Not applicable' + """ + return all(exposure_class.value == "Not applicable" for exposure_class in self) + + def __str__(self) -> str: + """String representation of the ExposureClasses object. + + Returns + ------- + str + String representation of the ExposureClasses object + """ + return "X0" if self.no_risk else ", ".join(enum.value for enum in self if enum.value != "Not applicable") diff --git a/blueprints/codes/eurocode/nen_en_1992_1_1_c2_2011/chapter_4_durability_and_cover/table_4_1.py b/blueprints/codes/eurocode/nen_en_1992_1_1_c2_2011/chapter_4_durability_and_cover/table_4_1.py index f1e1d774..bfd13253 100644 --- a/blueprints/codes/eurocode/nen_en_1992_1_1_c2_2011/chapter_4_durability_and_cover/table_4_1.py +++ b/blueprints/codes/eurocode/nen_en_1992_1_1_c2_2011/chapter_4_durability_and_cover/table_4_1.py @@ -2,108 +2,20 @@ according to Table 4.1 from NEN-EN 1992-1-1+C2:2011: Chapter 4 - Durability and cover to reinforcement. """ -from enum import Enum from functools import total_ordering -from typing import NamedTuple, Type, TypeVar -T = TypeVar("T", bound="Exposure") +from blueprints.codes.eurocode.exposure_classes import ( + CarbonationBase, + ChemicalBase, + ChlorideBase, + ChlorideSeawaterBase, + ExposureClassesBase, + FreezeThawBase, +) @total_ordering -class Exposure(Enum): - """Parent class for individual exposure classes. - - This class handles the ordering/comparison operations, that's why it is decorated with total_ordering (As recommended by PEP8). - On top of that, it handles a couple of methods which will be used by its subclasses. - """ - - def __eq__(self, other: object) -> bool: - """Definition of '==' operator for the comparison of the severity of the exposure classifications. - - Parameters - ---------- - self : Exposure/ subclass of Exposure - First argument for the comparison. - other : object - Second argument for the comparison. - - Raises - ------ - TypeError - If different types are being compared. - - Returns - ------- - Boolean - True if both arguments are of the same severity (In this case they will both be literally the same). - """ - if isinstance(other, self.__class__): - _self_severity = int(self.value[-1]) if self.value != "Not applicable" else 0 - _other_severity = int(other.value[-1]) if other.value != "Not applicable" else 0 - return _self_severity == _other_severity - raise TypeError("Only the same exposure class types can be compared with each other!") - - def __gt__(self, other: object) -> bool: - """Definition of '>' operator for the comparison of the severity of the exposure classifications. - - Parameters - ---------- - self : Exposure/ subclass of Exposure - First argument for the comparison. - other : object - Second argument for the comparison. - - Raises - ------ - TypeError - If different types are being compared. - - Returns - ------- - Boolean - True if the first argument is more severe than the second argument. - """ - if isinstance(other, self.__class__): - _self_severity = int(self.value[-1]) if self.value != "Not applicable" else 0 - _other_severity = int(other.value[-1]) if other.value != "Not applicable" else 0 - return _self_severity > _other_severity - raise TypeError("Only the same exposure class types can be compared with each other!") - - @classmethod - def options(cls: Type[T]) -> list[str]: - """Return all the possible options within a subclass. - - Returns - ------- - list[str] - all the possible class designations within a specific exposure class - """ - return [m._value_ for m in cls.__members__.values()] - - @staticmethod - def exposure_class_description() -> str: - """Description of subclasses to be implemented in each subclass. - - Returns - ------- - str - description of the specific exposure class - """ - raise NotImplementedError("The description method must be implemented in the subclass!") - - def description_of_the_environment(self) -> str: - """Description of the environment based on the instance. - - Returns - ------- - str - description of the environment based on the instance - """ - raise NotImplementedError("The description_of_the_environment method must be implemented in the subclass!") - - -@total_ordering -class Carbonation(Exposure): +class Carbonation(CarbonationBase): """Enum Class which indicates the classification of corrosion induced by carbonation.""" NA = "Not applicable" @@ -145,7 +57,7 @@ def description_of_the_environment(self) -> str: @total_ordering -class Chloride(Exposure): +class Chloride(ChlorideBase): """Enum Class which indicates the classification of corrosion induced by chlorides other than by sea water.""" NA = "Not applicable" @@ -184,7 +96,7 @@ def description_of_the_environment(self) -> str: @total_ordering -class ChlorideSeawater(Exposure): +class ChlorideSeawater(ChlorideSeawaterBase): """Enum Class which indicates the classification of corrosion induced by chlorides from sea water.""" NA = "Not applicable" @@ -223,7 +135,7 @@ def description_of_the_environment(self) -> str: @total_ordering -class FreezeThaw(Exposure): +class FreezeThaw(FreezeThawBase): """Enum Class which indicates the classification of freeze/thaw attack with or without de-icing agents.""" NA = "Not applicable" @@ -265,7 +177,7 @@ def description_of_the_environment(self) -> str: @total_ordering -class Chemical(Exposure): +class Chemical(ChemicalBase): """Enum Class which indicates the classification of chemical attack.""" NA = "Not applicable" @@ -303,39 +215,14 @@ def description_of_the_environment(self) -> str: return "Not applicable" -class ExposureClasses(NamedTuple): - """Implementation of tabel 4.1 from NEN-EN 1992-1-1+C2:2011. +class ExposureClasses(ExposureClassesBase): + """Implementation of table 4.1 from NEN-EN 1992-1-1+C2:2011. Exposure classes related to environmental conditions in accordance with EN 206-1 """ - carbonation: Carbonation = Carbonation("Not applicable") - chloride: Chloride = Chloride("Not applicable") - chloride_seawater: ChlorideSeawater = ChlorideSeawater("Not applicable") - freeze: FreezeThaw = FreezeThaw("Not applicable") - chemical: Chemical = Chemical("Not applicable") - - def __str__(self) -> str: - """String representation of the ExposureClasses object. - - Returns - ------- - str - String representation of the ExposureClasses object - """ - if self.no_risk: - return "X0" - return ", ".join(enum.value for enum in self if enum.value != "Not applicable") - - @property - def no_risk(self) -> bool: - """Check if all exposure classes are 'Not applicable'. - - This represents X0 class designation according to table 4.1 from NEN-EN 1992-1-1+C2:2011. - - Returns - ------- - bool - True if all exposure classes are 'Not applicable' - """ - return all(exposure_class.value == "Not applicable" for exposure_class in self) + carbonation: Carbonation + chloride: Chloride + chloride_seawater: ChlorideSeawater + freeze: FreezeThaw + chemical: Chemical diff --git a/blueprints/utils/__init__.py b/blueprints/utils/__init__.py new file mode 100644 index 00000000..48c51b66 --- /dev/null +++ b/blueprints/utils/__init__.py @@ -0,0 +1 @@ +"""Utils blueprint.""" diff --git a/blueprints/utils/abc_enum_meta.py b/blueprints/utils/abc_enum_meta.py new file mode 100644 index 00000000..c9582a59 --- /dev/null +++ b/blueprints/utils/abc_enum_meta.py @@ -0,0 +1,34 @@ +"""Custom metaclass for ABCEnum.""" + +from abc import ABCMeta +from enum import EnumMeta + + +class ABCEnumMeta(ABCMeta, EnumMeta): + """Custom metaclass for ABCEnum. + + This metaclass is a combination of ABCMeta and EnumMeta. + It is used to check if abstract methods are implemented in the class. + """ + + def __new__(cls, *args, **kwarg) -> type: + """Create a new instance of the class. + + Here we check if the class has abstract methods that are not implemented. + If so, we raise a TypeError. + """ + abstract_enum_cls = super().__new__(cls, *args, **kwarg) + # Only check abstractions if members were defined. + if abstract_enum_cls._member_map_: + try: # Handle existence of undefined abstract methods. + absmethods = list(abstract_enum_cls.__abstractmethods__) + if absmethods: + missing = ", ".join(f"{method!r}" for method in absmethods) + plural = "s" if len(absmethods) > 1 else "" + raise TypeError( + f"Can't instantiate abstract class {abstract_enum_cls.__name__!r}" + f" without an implementation for abstract method{plural} {missing}" + ) + except AttributeError: + pass + return abstract_enum_cls diff --git a/tests/codes/eurocode/nen_en_1992_1_1_c2_2011/chapter_4_durability_and_cover/test_table_4_1.py b/tests/codes/eurocode/nen_en_1992_1_1_c2_2011/chapter_4_durability_and_cover/test_table_4_1.py index 620334e6..e5947639 100644 --- a/tests/codes/eurocode/nen_en_1992_1_1_c2_2011/chapter_4_durability_and_cover/test_table_4_1.py +++ b/tests/codes/eurocode/nen_en_1992_1_1_c2_2011/chapter_4_durability_and_cover/test_table_4_1.py @@ -7,87 +7,10 @@ Chemical, Chloride, ChlorideSeawater, - Exposure, - ExposureClasses, FreezeThaw, ) -class DummyExposureSubclass(Exposure): - """Dummy Exposure subclass for testing purposes.""" - - DUMMY1 = "Dummy1" - DUMMY2 = "Dummy2" - DUMMY3 = "Dummy3" - - @staticmethod - def exposure_class_description() -> str: - """Return the description of the DummyExposureSubclass.""" - return "Dummy exposure subclass" - - -class TestExposure: - """Testing Exposure parent class.""" - - def test_equal_to_operator(self) -> None: - """Check if the == operator is working correctly.""" - assert DummyExposureSubclass.DUMMY1 == DummyExposureSubclass.DUMMY1 - assert DummyExposureSubclass.DUMMY2 == DummyExposureSubclass.DUMMY2 - assert DummyExposureSubclass.DUMMY3 == DummyExposureSubclass.DUMMY3 - - def test_not_equal_to_operator(self) -> None: - """Check if the != operator is working correctly.""" - assert DummyExposureSubclass.DUMMY1 != DummyExposureSubclass.DUMMY2 - assert DummyExposureSubclass.DUMMY2 != DummyExposureSubclass.DUMMY3 - assert DummyExposureSubclass.DUMMY3 != DummyExposureSubclass.DUMMY1 - - def test_greather_than_operator(self) -> None: - """Check if the > operator is working correctly.""" - assert DummyExposureSubclass.DUMMY3 > DummyExposureSubclass.DUMMY2 - assert DummyExposureSubclass.DUMMY2 > DummyExposureSubclass.DUMMY1 - - def test_greather_than_equal_to_operator(self) -> None: - """Check if the >= operator is working correctly.""" - # Testing greater than - assert DummyExposureSubclass.DUMMY3 >= DummyExposureSubclass.DUMMY2 - assert DummyExposureSubclass.DUMMY2 >= DummyExposureSubclass.DUMMY1 - # Testing equal to - assert DummyExposureSubclass.DUMMY3 >= DummyExposureSubclass.DUMMY3 - assert DummyExposureSubclass.DUMMY1 >= DummyExposureSubclass.DUMMY1 - - def test_lesser_than_operator(self) -> None: - """Check if the < operator is working correctly.""" - assert DummyExposureSubclass.DUMMY1 < DummyExposureSubclass.DUMMY2 - assert DummyExposureSubclass.DUMMY2 < DummyExposureSubclass.DUMMY3 - - def test_lesser_than_equal_to_operator(self) -> None: - """Check if the <= operator is working correctly.""" - # Testing lesser than - assert DummyExposureSubclass.DUMMY1 <= DummyExposureSubclass.DUMMY2 - assert DummyExposureSubclass.DUMMY2 <= DummyExposureSubclass.DUMMY3 - # Testing equal to - assert DummyExposureSubclass.DUMMY1 <= DummyExposureSubclass.DUMMY1 - assert DummyExposureSubclass.DUMMY3 <= DummyExposureSubclass.DUMMY3 - - def test_options(self) -> None: - """Check if the options method returns all the possible options within an exposure class.""" - assert DummyExposureSubclass.options() == ["Dummy1", "Dummy2", "Dummy3"] - - def test_description_not_implemented(self) -> None: - """Check if the description method raises NotImplementedError if description method is not implemented.""" - with pytest.raises(NotImplementedError): - Exposure.exposure_class_description() - - def test_description_implemented(self) -> None: - """Check if the exposure_class_description method returns the description of the subclass.""" - assert DummyExposureSubclass.exposure_class_description() == "Dummy exposure subclass" - - def test_description_of_the_environment_not_implemented(self) -> None: - """Check if the description_of_the_environment method raises NotImplementedError if it is not implemented.""" - with pytest.raises(NotImplementedError): - DummyExposureSubclass.DUMMY1.description_of_the_environment() - - class TestCarbonation: """Testing Carbonation class.""" @@ -185,44 +108,6 @@ def test_description_of_the_environment(self) -> None: assert Chemical.NA.description_of_the_environment() == "Not applicable" -class TestExposureClasses: - """Testing ExposureClasses class.""" - - def test_str(self) -> None: - """Check if the __str__ method returns the correct string representation of the exposure classes.""" - exposure_classes = ExposureClasses( - carbonation=Carbonation("XC3"), - chloride=Chloride("XD2"), - chloride_seawater=ChlorideSeawater("Not applicable"), - freeze=FreezeThaw("XF2"), - chemical=Chemical("XA2"), - ) - assert str(exposure_classes) == "XC3, XD2, XF2, XA2" - - def test_str_x0(self) -> None: - """Check if the __str__ method returns the correct string representation of the exposure classes - when all exposure classes are not applicable. - """ - exposureclasses = ExposureClasses() - assert str(exposureclasses) == "X0" - - def test_no_risk(self) -> None: - """Check if the no_risk method returns True if the exposure classes are all not applicable.""" - exposureclasses = ExposureClasses() - assert exposureclasses.no_risk is True - - def test_no_risk_false(self) -> None: - """Check if the no_risk method returns False if at least one exposure class is applicable.""" - exposureclasses = ExposureClasses( - carbonation=Carbonation("XC3"), - chloride=Chloride("XD2"), - chloride_seawater=ChlorideSeawater("Not applicable"), - freeze=FreezeThaw("XF2"), - chemical=Chemical("XA2"), - ) - assert exposureclasses.no_risk is False - - def test_comparing_different_types_raises_error() -> None: """Check if comparing different exposure class types, raises TypeError.""" with pytest.raises(TypeError): diff --git a/tests/codes/eurocode/test_exposure_classes.py b/tests/codes/eurocode/test_exposure_classes.py new file mode 100644 index 00000000..36c72c82 --- /dev/null +++ b/tests/codes/eurocode/test_exposure_classes.py @@ -0,0 +1,297 @@ +"""Tests for the Exposure classes.""" + +import pytest + +from blueprints.codes.eurocode.exposure_classes import ( + CarbonationBase, + ChemicalBase, + ChlorideBase, + ChlorideSeawaterBase, + Exposure, + ExposureClassesBase, + FreezeThawBase, +) + + +class DummyExposureSubclass(Exposure): + """Dummy Exposure subclass for testing purposes.""" + + DUMMY1 = "Dummy1" + DUMMY2 = "Dummy2" + DUMMY3 = "Dummy3" + + @staticmethod + def exposure_class_description() -> str: + """Return the description of the DummyExposureSubclass.""" + return "Dummy exposure subclass" + + def description_of_the_environment(self) -> str: + """Return the description of the environment.""" + return "Dummy environment" + + +class DummyCarbonation(CarbonationBase): + """Dummy Carbonation subclass for testing purposes.""" + + NA = "Not applicable" + XC1 = "XC1" + + @staticmethod + def exposure_class_description() -> str: + """Return the description of the DummyCarbonation.""" + return "Dummy carbonation subclass" + + def description_of_the_environment(self) -> str: + """Return the description of the environment.""" + match self: + case DummyCarbonation.XC1: + return "Dry or permanently wet" + case DummyCarbonation.NA: + return "Not applicable" + + +class DummyChloride(ChlorideBase): + """Dummy Chloride subclass for testing purposes.""" + + NA = "Not applicable" + XD1 = "XD1" + + @staticmethod + def exposure_class_description() -> str: + """Return the description of the DummyChloride.""" + return "Dummy chloride subclass" + + def description_of_the_environment(self) -> str: + """Return the description of the environment.""" + match self: + case DummyChloride.XD1: + return "Moderate humidity" + case DummyChloride.NA: + return "Not applicable" + + +class DummyChlorideSeawater(ChlorideSeawaterBase): + """Dummy ChlorideSeawater subclass for testing purposes.""" + + NA = "Not applicable" + XS1 = "XS1" + + @staticmethod + def exposure_class_description() -> str: + """Return the description of the DummyChlorideSeawater.""" + return "Dummy chloride seawater subclass" + + def description_of_the_environment(self) -> str: + """Return the description of the environment.""" + match self: + case DummyChlorideSeawater.XS1: + return "Wet, rarely dry" + case DummyChlorideSeawater.NA: + return "Not applicable" + + +class DummyFreezeThaw(FreezeThawBase): + """Dummy FreezeThaw subclass for testing purposes.""" + + NA = "Not applicable" + XF1 = "XF1" + + @staticmethod + def exposure_class_description() -> str: + """Return the description of the DummyFreezeThaw.""" + return "Dummy freeze thaw subclass" + + def description_of_the_environment(self) -> str: + """Return the description of the environment.""" + match self: + case DummyFreezeThaw.XF1: + return "Cyclic wet and dry" + case DummyFreezeThaw.NA: + return "Not applicable" + + +class DummyChemical(ChemicalBase): + """Dummy Chemical subclass for testing purposes.""" + + NA = "Not applicable" + XA1 = "XA1" + + @staticmethod + def exposure_class_description() -> str: + """Return the description of the DummyChemical.""" + return "Dummy chemical subclass" + + def description_of_the_environment(self) -> str: + """Return the description of the environment.""" + match self: + case DummyChemical.XA1: + return "Moderate humidity" + case DummyChemical.NA: + return "Not applicable" + + +class TestExposure: + """Testing Exposure parent class.""" + + def test_equal_to_operator(self) -> None: + """Check if the == operator is working correctly.""" + assert DummyExposureSubclass.DUMMY1 == DummyExposureSubclass.DUMMY1 + assert DummyExposureSubclass.DUMMY2 == DummyExposureSubclass.DUMMY2 + assert DummyExposureSubclass.DUMMY3 == DummyExposureSubclass.DUMMY3 + + def test_not_equal_to_operator(self) -> None: + """Check if the != operator is working correctly.""" + assert DummyExposureSubclass.DUMMY1 != DummyExposureSubclass.DUMMY2 + assert DummyExposureSubclass.DUMMY2 != DummyExposureSubclass.DUMMY3 + assert DummyExposureSubclass.DUMMY3 != DummyExposureSubclass.DUMMY1 + + def test_greather_than_operator(self) -> None: + """Check if the > operator is working correctly.""" + assert DummyExposureSubclass.DUMMY3 > DummyExposureSubclass.DUMMY2 + assert DummyExposureSubclass.DUMMY2 > DummyExposureSubclass.DUMMY1 + + def test_greather_than_equal_to_operator(self) -> None: + """Check if the >= operator is working correctly.""" + # Testing greater than + assert DummyExposureSubclass.DUMMY3 >= DummyExposureSubclass.DUMMY2 + assert DummyExposureSubclass.DUMMY2 >= DummyExposureSubclass.DUMMY1 + # Testing equal to + assert DummyExposureSubclass.DUMMY3 >= DummyExposureSubclass.DUMMY3 + assert DummyExposureSubclass.DUMMY1 >= DummyExposureSubclass.DUMMY1 + + def test_lesser_than_operator(self) -> None: + """Check if the < operator is working correctly.""" + assert DummyExposureSubclass.DUMMY1 < DummyExposureSubclass.DUMMY2 + assert DummyExposureSubclass.DUMMY2 < DummyExposureSubclass.DUMMY3 + + def test_lesser_than_equal_to_operator(self) -> None: + """Check if the <= operator is working correctly.""" + # Testing lesser than + assert DummyExposureSubclass.DUMMY1 <= DummyExposureSubclass.DUMMY2 + assert DummyExposureSubclass.DUMMY2 <= DummyExposureSubclass.DUMMY3 + # Testing equal to + assert DummyExposureSubclass.DUMMY1 <= DummyExposureSubclass.DUMMY1 + assert DummyExposureSubclass.DUMMY3 <= DummyExposureSubclass.DUMMY3 + + def test_options(self) -> None: + """Check if the options method returns all the possible options within an exposure class.""" + assert DummyExposureSubclass.options() == ["Dummy1", "Dummy2", "Dummy3"] + + def test_description_implemented(self) -> None: + """Check if the exposure_class_description method returns the description of the subclass.""" + assert DummyExposureSubclass.exposure_class_description() == "Dummy exposure subclass" + + def test_description_of_the_environment(self) -> None: + """Check if the description_of_the_environment method returns the description of the environment.""" + assert DummyExposureSubclass.DUMMY1.description_of_the_environment() == "Dummy environment" + + +class TestCarbonation: + """Testing CarbonationBase class.""" + + def test_initiate_subclasses(self) -> None: + """Check if initiating the CarbonationBase class raises a TypeError.""" + with pytest.raises(TypeError): + _ = CarbonationBase() # type: ignore[abstract, call-arg] + + +class TestChloride: + """Testing ChlorideBase class.""" + + def test_initiate_subclasses(self) -> None: + """Check if initiating the ChlorideBase class raises a TypeError.""" + with pytest.raises(TypeError): + _ = ChlorideBase() # type: ignore[abstract, call-arg] + + +class TestChlorideSeawater: + """Testing ChlorideSeawaterBase class.""" + + def test_initiate_subclasses(self) -> None: + """Check if initiating the ChlorideSeawaterBase class raises a TypeError.""" + with pytest.raises(TypeError): + _ = ChlorideSeawaterBase() # type: ignore[abstract, call-arg] + + +class TestFreezeThaw: + """Testing FreezeThawBase class.""" + + def test_initiate_subclasses(self) -> None: + """Check if initiating the FreezeThawBase class raises a TypeError.""" + with pytest.raises(TypeError): + _ = FreezeThawBase() # type: ignore[abstract, call-arg] + + +class TestChemical: + """Testing ChemicalBase class.""" + + def test_initiate_subclasses(self) -> None: + """Check if initiating the ChemicalBase class raises a TypeError.""" + with pytest.raises(TypeError): + _ = ChemicalBase() # type: ignore[abstract, call-arg] + + +class TestExposureClasses: + """Testing ExposureClasses class.""" + + def test_str(self) -> None: + """Check if the __str__ method returns the correct string representation of the exposure classes.""" + exposure_classes = ExposureClassesBase( + carbonation=DummyCarbonation("XC1"), + chloride=DummyChloride("XD1"), + chloride_seawater=DummyChlorideSeawater("Not applicable"), + freeze=DummyFreezeThaw("XF1"), + chemical=DummyChemical("XA1"), + ) + assert str(exposure_classes) == "XC1, XD1, XF1, XA1" + + def test_str_x0(self) -> None: + """Check if the __str__ method returns the correct string representation of the exposure classes + when all exposure classes are not applicable. + """ + exposureclasses = ExposureClassesBase( + carbonation=DummyCarbonation("Not applicable"), + chloride=DummyChloride("Not applicable"), + chloride_seawater=DummyChlorideSeawater("Not applicable"), + freeze=DummyFreezeThaw("Not applicable"), + chemical=DummyChemical("Not applicable"), + ) + assert str(exposureclasses) == "X0" + + def test_no_risk(self) -> None: + """Check if the no_risk method returns True if the exposure classes are all not applicable.""" + exposureclasses = ExposureClassesBase( + carbonation=DummyCarbonation("Not applicable"), + chloride=DummyChloride("Not applicable"), + chloride_seawater=DummyChlorideSeawater("Not applicable"), + freeze=DummyFreezeThaw("Not applicable"), + chemical=DummyChemical("Not applicable"), + ) + assert exposureclasses.no_risk is True + + def test_no_risk_false(self) -> None: + """Check if the no_risk method returns False if at least one exposure class is applicable.""" + exposureclasses = ExposureClassesBase( + carbonation=DummyCarbonation("XC1"), + chloride=DummyChloride("XD1"), + chloride_seawater=DummyChlorideSeawater("Not applicable"), + freeze=DummyFreezeThaw("XF1"), + chemical=DummyChemical("XA1"), + ) + assert exposureclasses.no_risk is False + + +def test_comparing_different_types_raises_error() -> None: + """Check if comparing different exposure class types, raises TypeError.""" + with pytest.raises(TypeError): + DummyCarbonation.XC1 > DummyChloride.NA + with pytest.raises(TypeError): + DummyChloride.NA == DummyChemical.NA + with pytest.raises(TypeError): + DummyFreezeThaw.XF1 <= DummyChlorideSeawater.XS1 + with pytest.raises(TypeError): + DummyChlorideSeawater.XS1 >= DummyChloride.XD1 + with pytest.raises(TypeError): + DummyChemical.XA1 < DummyCarbonation.XC1 + with pytest.raises(TypeError): + DummyChemical.NA != DummyFreezeThaw.XF1 From 8c0ff1837e6a53727c5291969f90f5b3641729d1 Mon Sep 17 00:00:00 2001 From: Sina Zel taat Date: Mon, 8 Jul 2024 18:28:01 +0200 Subject: [PATCH 2/4] Add test for AttributeError handling by ABCEnumMeta --- blueprints/utils/abc_enum_meta.py | 8 +- tests/codes/eurocode/test_exposure_classes.py | 72 ++++++++++------ tests/utils/__init__.py | 1 + tests/utils/conftest.py | 22 +++++ tests/utils/test_abc_enum_meta.py | 85 +++++++++++++++++++ 5 files changed, 158 insertions(+), 30 deletions(-) create mode 100644 tests/utils/__init__.py create mode 100644 tests/utils/conftest.py create mode 100644 tests/utils/test_abc_enum_meta.py diff --git a/blueprints/utils/abc_enum_meta.py b/blueprints/utils/abc_enum_meta.py index c9582a59..72e28f3c 100644 --- a/blueprints/utils/abc_enum_meta.py +++ b/blueprints/utils/abc_enum_meta.py @@ -21,10 +21,10 @@ def __new__(cls, *args, **kwarg) -> type: # Only check abstractions if members were defined. if abstract_enum_cls._member_map_: try: # Handle existence of undefined abstract methods. - absmethods = list(abstract_enum_cls.__abstractmethods__) - if absmethods: - missing = ", ".join(f"{method!r}" for method in absmethods) - plural = "s" if len(absmethods) > 1 else "" + abstract_methods = list(abstract_enum_cls.__abstractmethods__) + if abstract_methods: + missing = ", ".join(f"{method!r}" for method in abstract_methods) + plural = "s" if len(abstract_methods) > 1 else "" raise TypeError( f"Can't instantiate abstract class {abstract_enum_cls.__name__!r}" f" without an implementation for abstract method{plural} {missing}" diff --git a/tests/codes/eurocode/test_exposure_classes.py b/tests/codes/eurocode/test_exposure_classes.py index 36c72c82..989b0e0c 100644 --- a/tests/codes/eurocode/test_exposure_classes.py +++ b/tests/codes/eurocode/test_exposure_classes.py @@ -43,11 +43,7 @@ def exposure_class_description() -> str: def description_of_the_environment(self) -> str: """Return the description of the environment.""" - match self: - case DummyCarbonation.XC1: - return "Dry or permanently wet" - case DummyCarbonation.NA: - return "Not applicable" + return "Dummy environment" class DummyChloride(ChlorideBase): @@ -63,11 +59,7 @@ def exposure_class_description() -> str: def description_of_the_environment(self) -> str: """Return the description of the environment.""" - match self: - case DummyChloride.XD1: - return "Moderate humidity" - case DummyChloride.NA: - return "Not applicable" + return "Dummy environment" class DummyChlorideSeawater(ChlorideSeawaterBase): @@ -83,11 +75,7 @@ def exposure_class_description() -> str: def description_of_the_environment(self) -> str: """Return the description of the environment.""" - match self: - case DummyChlorideSeawater.XS1: - return "Wet, rarely dry" - case DummyChlorideSeawater.NA: - return "Not applicable" + return "Dummy environment" class DummyFreezeThaw(FreezeThawBase): @@ -103,11 +91,7 @@ def exposure_class_description() -> str: def description_of_the_environment(self) -> str: """Return the description of the environment.""" - match self: - case DummyFreezeThaw.XF1: - return "Cyclic wet and dry" - case DummyFreezeThaw.NA: - return "Not applicable" + return "Dummy environment" class DummyChemical(ChemicalBase): @@ -123,11 +107,7 @@ def exposure_class_description() -> str: def description_of_the_environment(self) -> str: """Return the description of the environment.""" - match self: - case DummyChemical.XA1: - return "Moderate humidity" - case DummyChemical.NA: - return "Not applicable" + return "Dummy environment" class TestExposure: @@ -177,7 +157,7 @@ def test_options(self) -> None: """Check if the options method returns all the possible options within an exposure class.""" assert DummyExposureSubclass.options() == ["Dummy1", "Dummy2", "Dummy3"] - def test_description_implemented(self) -> None: + def test_exposure_class_description_implemented(self) -> None: """Check if the exposure_class_description method returns the description of the subclass.""" assert DummyExposureSubclass.exposure_class_description() == "Dummy exposure subclass" @@ -194,6 +174,14 @@ def test_initiate_subclasses(self) -> None: with pytest.raises(TypeError): _ = CarbonationBase() # type: ignore[abstract, call-arg] + def test_exposure_class_description_implemented(self) -> None: + """Check if the exposure_class_description method returns the description of the subclass.""" + assert DummyCarbonation.exposure_class_description() == "Dummy carbonation subclass" + + def test_description_of_the_environment(self) -> None: + """Check if the description_of_the_environment method returns the description of the environment.""" + assert DummyCarbonation.XC1.description_of_the_environment() == "Dummy environment" + class TestChloride: """Testing ChlorideBase class.""" @@ -203,6 +191,14 @@ def test_initiate_subclasses(self) -> None: with pytest.raises(TypeError): _ = ChlorideBase() # type: ignore[abstract, call-arg] + def test_exposure_class_description_implemented(self) -> None: + """Check if the exposure_class_description method returns the description of the subclass.""" + assert DummyChloride.exposure_class_description() == "Dummy chloride subclass" + + def test_description_of_the_environment(self) -> None: + """Check if the description_of_the_environment method returns the description of the environment.""" + assert DummyChloride.XD1.description_of_the_environment() == "Dummy environment" + class TestChlorideSeawater: """Testing ChlorideSeawaterBase class.""" @@ -212,6 +208,14 @@ def test_initiate_subclasses(self) -> None: with pytest.raises(TypeError): _ = ChlorideSeawaterBase() # type: ignore[abstract, call-arg] + def test_exposure_class_description_implemented(self) -> None: + """Check if the exposure_class_description method returns the description of the subclass.""" + assert DummyChlorideSeawater.exposure_class_description() == "Dummy chloride seawater subclass" + + def test_description_of_the_environment(self) -> None: + """Check if the description_of_the_environment method returns the description of the environment.""" + assert DummyChlorideSeawater.XS1.description_of_the_environment() == "Dummy environment" + class TestFreezeThaw: """Testing FreezeThawBase class.""" @@ -221,6 +225,14 @@ def test_initiate_subclasses(self) -> None: with pytest.raises(TypeError): _ = FreezeThawBase() # type: ignore[abstract, call-arg] + def test_exposure_class_description_implemented(self) -> None: + """Check if the exposure_class_description method returns the description of the subclass.""" + assert DummyFreezeThaw.exposure_class_description() == "Dummy freeze thaw subclass" + + def test_description_of_the_environment(self) -> None: + """Check if the description_of_the_environment method returns the description of the environment.""" + assert DummyFreezeThaw.XF1.description_of_the_environment() == "Dummy environment" + class TestChemical: """Testing ChemicalBase class.""" @@ -230,6 +242,14 @@ def test_initiate_subclasses(self) -> None: with pytest.raises(TypeError): _ = ChemicalBase() # type: ignore[abstract, call-arg] + def test_exposure_class_description_implemented(self) -> None: + """Check if the exposure_class_description method returns the description of the subclass.""" + assert DummyChemical.exposure_class_description() == "Dummy chemical subclass" + + def test_description_of_the_environment(self) -> None: + """Check if the description_of_the_environment method returns the description of the environment.""" + assert DummyChemical.XA1.description_of_the_environment() == "Dummy environment" + class TestExposureClasses: """Testing ExposureClasses class.""" diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 00000000..292009e3 --- /dev/null +++ b/tests/utils/__init__.py @@ -0,0 +1 @@ +"""Test utils.""" diff --git a/tests/utils/conftest.py b/tests/utils/conftest.py new file mode 100644 index 00000000..c7456b73 --- /dev/null +++ b/tests/utils/conftest.py @@ -0,0 +1,22 @@ +"""Fixtures for the tests in the utils module.""" + +import pytest + +from blueprints.utils.abc_enum_meta import ABCEnumMeta + + +class AttributeErrorEnumMeta(ABCEnumMeta): + """Custom metaclass to simulate AttributeError for testing.""" + + def __new__(cls, *args, **kwargs): + # Simulate the presence of _member_map_ to bypass the initial check + abstract_enum_cls = super().__new__(cls, *args, **kwargs) + setattr(abstract_enum_cls, "_member_map_", True) + raise AttributeError("Simulated AttributeError for testing") + return abstract_enum_cls + + +@pytest.fixture() +def attribute_error_enum_meta(): + """Fixture that returns an instance of the AttributeErrorEnumMeta for testing.""" + return AttributeErrorEnumMeta diff --git a/tests/utils/test_abc_enum_meta.py b/tests/utils/test_abc_enum_meta.py new file mode 100644 index 00000000..1dfa2537 --- /dev/null +++ b/tests/utils/test_abc_enum_meta.py @@ -0,0 +1,85 @@ +"""Tests for the ABCEnumMeta class.""" + +from abc import ABCMeta, abstractmethod +from enum import Enum, EnumMeta + +import pytest + +from blueprints.utils.abc_enum_meta import ABCEnumMeta + + +class MockAbstractEnum(Enum, metaclass=ABCEnumMeta): + """Mock class for testing the ABCEnumMeta class.""" + + @abstractmethod + def test_method(self) -> int: + """Test method for the MockAbstractEnum class.""" + + +class MockConcreteEnum(MockAbstractEnum): + """Mock class for testing the ABCEnumMeta class.""" + + A = "a" + B = "b" + + def test_method(self) -> int: + """Test method for the MockConcreteEnum class.""" + return 1 + + +class TestABCEnumMeta: + """Test class for ABCEnumMeta.""" + + def test_inheritance(self) -> None: + """Test if ABCEnumMeta inherits from ABCMeta and EnumMeta.""" + assert issubclass(ABCEnumMeta, ABCMeta) + assert issubclass(ABCEnumMeta, EnumMeta) + + def test_mock_abstract_enum(self) -> None: + """Test case for MockAbstractEnum class.""" + with pytest.raises(TypeError): + _ = MockAbstractEnum("a") + + def test_mock_concrete_enum(self) -> None: + """Test case for MockConcreteEnum class.""" + assert MockConcreteEnum.A.value == "a" + assert MockConcreteEnum.B.value == "b" + assert MockConcreteEnum.B.test_method() == 1 + + def test_invalid_mock_concrete_enum(self) -> None: + """Test case for InvalidMockConcreteEnum class.""" + with pytest.raises(TypeError) as error_info: + + class InvalidMockConcreteEnum(MockAbstractEnum): + """Mock class for testing the ABCEnumMeta class.""" + + A = "a" + B = "b" + + assert str(error_info.value).endswith("without an implementation for abstract method 'test_method'") + + def test_abc_enum_meta_attribute_error(self) -> None: + """ + Test instantiation of class with ABCEnumMeta where no abstract methods attribute is present. + Expect successful instantiation due to handling AttributeError (pass if AttributeError is raised). + """ + + class NoAbstractMethodsEnum(Enum, metaclass=ABCEnumMeta): + ONE = 1 + TWO = 2 + + assert NoAbstractMethodsEnum.ONE.value == 1 + + +def test_abc_enum_meta_attribute_error_handling(attribute_error_enum_meta: ABCEnumMeta) -> None: + """ + Test instantiation of class with custom metaclass that raises AttributeError. + Ensure instantiation proceeds due to handling AttributeError. + """ + + class TestEnum(Enum, metaclass=attribute_error_enum_meta): + ONE = 1 + TWO = 2 + + # Check if the class was instantiated without raising an exception + assert TestEnum.ONE.value == 1 From a9fdc818079168a960820cc7f8344bdce67ab73d Mon Sep 17 00:00:00 2001 From: Sina Zel taat Date: Tue, 9 Jul 2024 09:40:49 +0200 Subject: [PATCH 3/4] 100% coverage on test_abc_enum_meta --- blueprints/utils/abc_enum_meta.py | 22 +++++++++------------- tests/utils/conftest.py | 22 ---------------------- tests/utils/test_abc_enum_meta.py | 26 +++++--------------------- 3 files changed, 14 insertions(+), 56 deletions(-) delete mode 100644 tests/utils/conftest.py diff --git a/blueprints/utils/abc_enum_meta.py b/blueprints/utils/abc_enum_meta.py index 72e28f3c..90900ec7 100644 --- a/blueprints/utils/abc_enum_meta.py +++ b/blueprints/utils/abc_enum_meta.py @@ -18,17 +18,13 @@ def __new__(cls, *args, **kwarg) -> type: If so, we raise a TypeError. """ abstract_enum_cls = super().__new__(cls, *args, **kwarg) - # Only check abstractions if members were defined. - if abstract_enum_cls._member_map_: - try: # Handle existence of undefined abstract methods. - abstract_methods = list(abstract_enum_cls.__abstractmethods__) - if abstract_methods: - missing = ", ".join(f"{method!r}" for method in abstract_methods) - plural = "s" if len(abstract_methods) > 1 else "" - raise TypeError( - f"Can't instantiate abstract class {abstract_enum_cls.__name__!r}" - f" without an implementation for abstract method{plural} {missing}" - ) - except AttributeError: - pass + # Enum classes have a _member_map_ attribute that is a dictionary of the enum members. + # If the class has abstract methods and the _member_map_ attribute is not empty, we check if the abstract methods are implemented. + if getattr(abstract_enum_cls, "_member_map_", False) and getattr(abstract_enum_cls, "__abstractmethods__", False): + abstract_methods = list(abstract_enum_cls.__abstractmethods__) + missing = ", ".join(f"{method!r}" for method in abstract_methods) + plural = "s" if len(abstract_methods) > 1 else "" + raise TypeError( + f"Can't instantiate abstract class {abstract_enum_cls.__name__!r}" f" without an implementation for abstract method{plural} {missing}" + ) return abstract_enum_cls diff --git a/tests/utils/conftest.py b/tests/utils/conftest.py deleted file mode 100644 index c7456b73..00000000 --- a/tests/utils/conftest.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Fixtures for the tests in the utils module.""" - -import pytest - -from blueprints.utils.abc_enum_meta import ABCEnumMeta - - -class AttributeErrorEnumMeta(ABCEnumMeta): - """Custom metaclass to simulate AttributeError for testing.""" - - def __new__(cls, *args, **kwargs): - # Simulate the presence of _member_map_ to bypass the initial check - abstract_enum_cls = super().__new__(cls, *args, **kwargs) - setattr(abstract_enum_cls, "_member_map_", True) - raise AttributeError("Simulated AttributeError for testing") - return abstract_enum_cls - - -@pytest.fixture() -def attribute_error_enum_meta(): - """Fixture that returns an instance of the AttributeErrorEnumMeta for testing.""" - return AttributeErrorEnumMeta diff --git a/tests/utils/test_abc_enum_meta.py b/tests/utils/test_abc_enum_meta.py index 1dfa2537..c822389f 100644 --- a/tests/utils/test_abc_enum_meta.py +++ b/tests/utils/test_abc_enum_meta.py @@ -35,10 +35,11 @@ def test_inheritance(self) -> None: assert issubclass(ABCEnumMeta, ABCMeta) assert issubclass(ABCEnumMeta, EnumMeta) - def test_mock_abstract_enum(self) -> None: - """Test case for MockAbstractEnum class.""" + def test_mock_abstract_enum_is_abstract(self) -> None: + """Test case for MockAbstractEnum class to check if it's an abstract class.""" + assert MockAbstractEnum.__abstractmethods__ == frozenset({"test_method"}) with pytest.raises(TypeError): - _ = MockAbstractEnum("a") + _ = MockAbstractEnum("a") # type: ignore[abstract] def test_mock_concrete_enum(self) -> None: """Test case for MockConcreteEnum class.""" @@ -59,27 +60,10 @@ class InvalidMockConcreteEnum(MockAbstractEnum): assert str(error_info.value).endswith("without an implementation for abstract method 'test_method'") def test_abc_enum_meta_attribute_error(self) -> None: - """ - Test instantiation of class with ABCEnumMeta where no abstract methods attribute is present. - Expect successful instantiation due to handling AttributeError (pass if AttributeError is raised). - """ + """Test instantiation of class with ABCEnumMeta where no abstract methods attribute is present.""" class NoAbstractMethodsEnum(Enum, metaclass=ABCEnumMeta): ONE = 1 TWO = 2 assert NoAbstractMethodsEnum.ONE.value == 1 - - -def test_abc_enum_meta_attribute_error_handling(attribute_error_enum_meta: ABCEnumMeta) -> None: - """ - Test instantiation of class with custom metaclass that raises AttributeError. - Ensure instantiation proceeds due to handling AttributeError. - """ - - class TestEnum(Enum, metaclass=attribute_error_enum_meta): - ONE = 1 - TWO = 2 - - # Check if the class was instantiated without raising an exception - assert TestEnum.ONE.value == 1 From 9b2cf30d2c6784dbd2032b080480b7258fee5c8e Mon Sep 17 00:00:00 2001 From: Sina Zel taat Date: Wed, 10 Jul 2024 11:07:50 +0200 Subject: [PATCH 4/4] Apply suggestions from code review --- .pre-commit-config.yaml | 4 ++-- .../chapter_4_durability_and_cover/test_table_4_1.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d8e63501..55789bc2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.7 + rev: v0.5.1 hooks: # Run the linter. - id: ruff @@ -26,7 +26,7 @@ repos: - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.0 + rev: v1.10.1 hooks: - id: mypy language_version: python3.11 diff --git a/tests/codes/eurocode/nen_en_1992_1_1_c2_2011/chapter_4_durability_and_cover/test_table_4_1.py b/tests/codes/eurocode/nen_en_1992_1_1_c2_2011/chapter_4_durability_and_cover/test_table_4_1.py index e5947639..353d5588 100644 --- a/tests/codes/eurocode/nen_en_1992_1_1_c2_2011/chapter_4_durability_and_cover/test_table_4_1.py +++ b/tests/codes/eurocode/nen_en_1992_1_1_c2_2011/chapter_4_durability_and_cover/test_table_4_1.py @@ -111,14 +111,14 @@ def test_description_of_the_environment(self) -> None: def test_comparing_different_types_raises_error() -> None: """Check if comparing different exposure class types, raises TypeError.""" with pytest.raises(TypeError): - Carbonation.XC2 > Chloride.XD1 + _ = Carbonation.XC2 > Chloride.XD1 with pytest.raises(TypeError): - Chloride.NA == Chemical.NA + _ = Chloride.NA == Chemical.NA with pytest.raises(TypeError): - FreezeThaw.XF1 <= ChlorideSeawater.XS2 + _ = FreezeThaw.XF1 <= ChlorideSeawater.XS2 with pytest.raises(TypeError): - ChlorideSeawater.XS3 >= Chloride.XD3 + _ = ChlorideSeawater.XS3 >= Chloride.XD3 with pytest.raises(TypeError): - Chemical.XA1 < Carbonation.XC2 + _ = Chemical.XA1 < Carbonation.XC2 with pytest.raises(TypeError): - Chemical.NA != FreezeThaw.XF1 + _ = Chemical.NA != FreezeThaw.XF1