diff --git a/gemd/__init__.py b/gemd/__init__.py index b8ae0b5a..3a1a4051 100644 --- a/gemd/__init__.py +++ b/gemd/__init__.py @@ -1,30 +1,82 @@ """Data concepts library.""" + from .__version__ import __version__ # noqa: F401 -from .entity import Condition, Parameter, Property, PropertyAndConditions, \ - CategoricalBounds, CompositionBounds, IntegerBounds, \ - MolecularStructureBounds, RealBounds, \ - MaterialRun, MeasurementRun, ProcessRun, IngredientRun, \ - MaterialSpec, MeasurementSpec, ProcessSpec, IngredientSpec, \ - PerformedSource, \ - PropertyTemplate, ConditionTemplate, ParameterTemplate, \ - MaterialTemplate, MeasurementTemplate, ProcessTemplate, \ - NominalReal, NormalReal, UniformReal, NominalInteger, \ - UniformInteger, DiscreteCategorical, NominalCategorical, \ - EmpiricalFormula, NominalComposition, InChI, Smiles, \ - LinkByUID, \ - FileLink # noqa: F401 +from .entity import ( + CategoricalBounds, + CompositionBounds, + Condition, + ConditionTemplate, + DiscreteCategorical, + EmpiricalFormula, + FileLink, # noqa: F401 + InChI, + IngredientRun, + IngredientSpec, + IntegerBounds, + LinkByUID, + MaterialRun, + MaterialSpec, + MaterialTemplate, + MeasurementRun, + MeasurementSpec, + MeasurementTemplate, + MolecularStructureBounds, + NominalCategorical, + NominalComposition, + NominalInteger, + NominalReal, + NormalReal, + Parameter, + ParameterTemplate, + PerformedSource, + ProcessRun, + ProcessSpec, + ProcessTemplate, + Property, + PropertyAndConditions, + PropertyTemplate, + RealBounds, + Smiles, + UniformInteger, + UniformReal, +) -__all__ = ["Condition", "Parameter", "Property", "PropertyAndConditions", - "CategoricalBounds", "CompositionBounds", "IntegerBounds", - "MolecularStructureBounds", "RealBounds", - "MaterialRun", "MeasurementRun", "ProcessRun", "IngredientRun", - "MaterialSpec", "MeasurementSpec", "ProcessSpec", "IngredientSpec", - "PerformedSource", - "PropertyTemplate", "ConditionTemplate", "ParameterTemplate", - "MaterialTemplate", "MeasurementTemplate", "ProcessTemplate", - "NominalReal", "NormalReal", "UniformReal", "NominalInteger", - "UniformInteger", "DiscreteCategorical", "NominalCategorical", - "EmpiricalFormula", "NominalComposition", "InChI", "Smiles", - "LinkByUID", - "FileLink" - ] +__all__ = [ + "Condition", + "Parameter", + "Property", + "PropertyAndConditions", + "CategoricalBounds", + "CompositionBounds", + "IntegerBounds", + "MolecularStructureBounds", + "RealBounds", + "MaterialRun", + "MeasurementRun", + "ProcessRun", + "IngredientRun", + "MaterialSpec", + "MeasurementSpec", + "ProcessSpec", + "IngredientSpec", + "PerformedSource", + "PropertyTemplate", + "ConditionTemplate", + "ParameterTemplate", + "MaterialTemplate", + "MeasurementTemplate", + "ProcessTemplate", + "NominalReal", + "NormalReal", + "UniformReal", + "NominalInteger", + "UniformInteger", + "DiscreteCategorical", + "NominalCategorical", + "EmpiricalFormula", + "NominalComposition", + "InChI", + "Smiles", + "LinkByUID", + "FileLink", +] diff --git a/gemd/__version__.py b/gemd/__version__.py index 62fa04d7..f1e49f68 100644 --- a/gemd/__version__.py +++ b/gemd/__version__.py @@ -1 +1 @@ -__version__ = "2.2.4" +__version__ = "2.2.5" diff --git a/gemd/builders/__init__.py b/gemd/builders/__init__.py index 9b21387f..9cba7c00 100644 --- a/gemd/builders/__init__.py +++ b/gemd/builders/__init__.py @@ -2,5 +2,10 @@ from .impl import make_node, add_edge, add_measurement, add_attribute, make_attribute, make_value __all__ = [ - "make_node", "add_edge", "add_measurement", "add_attribute", "make_attribute", "make_value" + "make_node", + "add_edge", + "add_measurement", + "add_attribute", + "make_attribute", + "make_value", ] diff --git a/gemd/builders/impl.py b/gemd/builders/impl.py index 395755d9..b55420a6 100644 --- a/gemd/builders/impl.py +++ b/gemd/builders/impl.py @@ -1,35 +1,66 @@ # Tools to help build GEMD objects -from gemd.entity.object import ProcessSpec, ProcessRun, MaterialSpec, IngredientSpec, \ - MaterialRun, IngredientRun, MeasurementSpec, MeasurementRun +from typing import List, Union + +from gemd.entity.attribute import Condition, Parameter, Property, PropertyAndConditions +from gemd.entity.attribute.base_attribute import BaseAttribute +from gemd.entity.bounds import ( + CategoricalBounds, + CompositionBounds, + IntegerBounds, + MolecularStructureBounds, + RealBounds, +) +from gemd.entity.bounds.base_bounds import BaseBounds +from gemd.entity.object import ( + IngredientRun, + IngredientSpec, + MaterialRun, + MaterialSpec, + MeasurementRun, + MeasurementSpec, + ProcessRun, + ProcessSpec, +) from gemd.entity.object.has_conditions import HasConditions from gemd.entity.object.has_parameters import HasParameters from gemd.entity.object.has_properties import HasProperties -from gemd.entity.attribute import Property, PropertyAndConditions, Condition, Parameter -from gemd.entity.attribute.base_attribute import BaseAttribute -from gemd.entity.template import PropertyTemplate, ParameterTemplate, ConditionTemplate, \ - MaterialTemplate, MeasurementTemplate, ProcessTemplate -from gemd.entity.bounds import RealBounds, IntegerBounds, CategoricalBounds, \ - CompositionBounds, MolecularStructureBounds -from gemd.entity.bounds.base_bounds import BaseBounds -from gemd.entity.value import NominalReal, NominalInteger, NominalCategorical, \ - EmpiricalFormula, InChI, Smiles -from gemd.entity.value.continuous_value import ContinuousValue +from gemd.entity.template import ( + ConditionTemplate, + MaterialTemplate, + MeasurementTemplate, + ParameterTemplate, + ProcessTemplate, + PropertyTemplate, +) +from gemd.entity.value import ( + EmpiricalFormula, + InChI, + NominalCategorical, + NominalInteger, + NominalReal, + Smiles, +) from gemd.entity.value.base_value import BaseValue - -from typing import Union, List +from gemd.entity.value.continuous_value import ContinuousValue __all__ = [ - "make_node", "add_edge", "add_measurement", "add_attribute", "make_attribute", "make_value" + "make_node", + "add_edge", + "add_measurement", + "add_attribute", + "make_attribute", + "make_value", ] -def make_node(name: str, - *, - process_name: str = None, - process_template: ProcessTemplate = None, - material_template: MaterialTemplate = None) -> MaterialRun: - """ - Generate a material-process spec-run quadruple. +def make_node( + name: str, + *, + process_name: str = None, + process_template: ProcessTemplate = None, + material_template: MaterialTemplate = None, +) -> MaterialRun: + """Generate a material-process spec-run quadruple. Parameters ---------- @@ -46,7 +77,7 @@ def make_node(name: str, :class:`~gemd.entity.template.material_template.MaterialTemplate` for the quadruple. Returns - -------- + ------- ~gemd.entity.object.material_run.MaterialRun A :class:`~gemd.entity.object.material_run.MaterialRun` with linked processes, specs and templates @@ -58,42 +89,28 @@ def make_node(name: str, else: process_name = process_template.name - my_process_spec = ProcessSpec( - name=process_name, - template=process_template - ) + my_process_spec = ProcessSpec(name=process_name, template=process_template) - my_process_run = ProcessRun( - name=process_name, - spec=my_process_spec - ) + my_process_run = ProcessRun(name=process_name, spec=my_process_spec) - my_mat_spec = MaterialSpec( - name=name, - process=my_process_spec, - template=material_template - ) + my_mat_spec = MaterialSpec(name=name, process=my_process_spec, template=material_template) - my_mat_run = MaterialRun( - name=name, - process=my_process_run, - spec=my_mat_spec - ) + my_mat_run = MaterialRun(name=name, process=my_process_run, spec=my_mat_spec) return my_mat_run -def add_edge(input_material: MaterialRun, - output_material: MaterialRun, - *, - name: str = None, - mass_fraction: Union[float, ContinuousValue] = None, - number_fraction: Union[float, ContinuousValue] = None, - volume_fraction: Union[float, ContinuousValue] = None, - absolute_quantity: Union[int, float, ContinuousValue] = None, - absolute_units: str = None, - ) -> IngredientRun: - """ - Connect two material-process spec-run quadruples with ingredients. +def add_edge( + input_material: MaterialRun, + output_material: MaterialRun, + *, + name: str = None, + mass_fraction: Union[float, ContinuousValue] = None, + number_fraction: Union[float, ContinuousValue] = None, + volume_fraction: Union[float, ContinuousValue] = None, + absolute_quantity: Union[int, float, ContinuousValue] = None, + absolute_units: str = None, +) -> IngredientRun: + """Connect two material-process spec-run quadruples with ingredients. Parameters ---------- @@ -132,45 +149,43 @@ def add_edge(input_material: MaterialRun, The absolute units. Required if absolute_quantity is provided as a float Returns - -------- + ------- ~gemd.entity.object.ingredient_run.IngredientRun A :class:`~gemd.entity.object.ingredient_run.IngredientRun` with linked processes, specs and materials """ output_spec = output_material.spec - if not isinstance(output_spec, MaterialSpec) \ - or output_spec.process is None \ - or output_material.process is None: - raise ValueError("Output Material must be a MaterialRun with connected " - "Specs and Processes.") + spec_linked = isinstance(output_spec, MaterialSpec) and output_spec.process is not None + if not spec_linked or output_material.process is None: + raise ValueError( + "Output Material must be a MaterialRun with connected Specs and Processes." + ) if input_material.spec is None: raise ValueError("Input Material must be a MaterialRun with connected Spec.") if name is None: name = input_material.name - my_ingredient_spec = IngredientSpec(name=name, - process=output_spec.process, - material=input_material.spec - ) - my_ingredient_run = IngredientRun(spec=my_ingredient_spec, - process=output_material.process, - material=input_material - ) + my_ingredient_spec = IngredientSpec( + name=name, process=output_spec.process, material=input_material.spec + ) + my_ingredient_run = IngredientRun( + spec=my_ingredient_spec, process=output_material.process, material=input_material + ) if mass_fraction is not None: if isinstance(mass_fraction, float): - mass_fraction = NominalReal(nominal=mass_fraction, units='') + mass_fraction = NominalReal(nominal=mass_fraction, units="") my_ingredient_run.mass_fraction = mass_fraction if number_fraction is not None: if isinstance(number_fraction, float): - number_fraction = NominalReal(nominal=number_fraction, units='') + number_fraction = NominalReal(nominal=number_fraction, units="") my_ingredient_run.number_fraction = number_fraction if volume_fraction is not None: if isinstance(volume_fraction, float): - volume_fraction = NominalReal(nominal=volume_fraction, units='') + volume_fraction = NominalReal(nominal=volume_fraction, units="") my_ingredient_run.volume_fraction = volume_fraction if absolute_quantity is not None: @@ -182,20 +197,21 @@ def add_edge(input_material: MaterialRun, my_ingredient_run.absolute_quantity = absolute_quantity if absolute_units is not None: - raise ValueError("Absolute Units are only used if " - "Absolute Quantity is given as is a float.") + raise ValueError( + "Absolute Units are only used if Absolute Quantity is given as is a float." + ) return my_ingredient_run -def add_measurement(material: MaterialRun, - *, - name: str = None, - template: MeasurementTemplate = None, - attributes: List[BaseAttribute] = None, - ) -> MeasurementRun: - """ - Add a measurement run-spec set to a :class:`~gemd.entity.object.material_run.MaterialRun`. +def add_measurement( + material: MaterialRun, + *, + name: str = None, + template: MeasurementTemplate = None, + attributes: List[BaseAttribute] = None, +) -> MeasurementRun: + """Add a measurement run-spec set to a :class:`~gemd.entity.object.material_run.MaterialRun`. Parameters ---------- @@ -218,7 +234,7 @@ def add_measurement(material: MaterialRun, and/or :class:`Properties `. Returns - -------- + ------- ~gemd.entity.object.measurement_run.MeasurementRun A :class:`~gemd.entity.object.measurement_run.MeasurementRun` with linked material, spec and template @@ -247,12 +263,12 @@ def add_measurement(material: MaterialRun, return my_measurement_run -def add_attribute(target: Union[HasProperties, HasConditions, HasParameters], - template: Union[PropertyTemplate, ConditionTemplate, ParameterTemplate], - value: Union[BaseValue, str, float, int] - ) -> Union[Property, Condition, Parameter]: - """ - Generate an attribute, and then add it attribute to a GEMD object. +def add_attribute( + target: Union[HasProperties, HasConditions, HasParameters], + template: Union[PropertyTemplate, ConditionTemplate, ParameterTemplate], + value: Union[BaseValue, str, float, int], +) -> Union[Property, Condition, Parameter]: + """Generate an attribute, and then add it attribute to a GEMD object. Parameters ---------- @@ -265,7 +281,7 @@ def add_attribute(target: Union[HasProperties, HasConditions, HasParameters], attempt to generate an appropriate :class:`BaseValue` subclass given a str, float or int. Returns - -------- + ------- BaseAttribute The generated attribute @@ -278,8 +294,9 @@ def add_attribute(target: Union[HasProperties, HasConditions, HasParameters], target.properties.append(PropertyAndConditions(property=attribute)) elif attr_class is Condition: if len(target.properties) == 0: - raise ValueError("Cannot add a condition to a MaterialSpec " - "before it has at least one property.") + raise ValueError( + "Cannot add a condition to a MaterialSpec before it has at least one property." + ) target.properties[-1].conditions.append(attribute) else: raise ValueError(f"Attribute {attr_class} is incompatible with target {type(target)}.") @@ -296,11 +313,11 @@ def add_attribute(target: Union[HasProperties, HasConditions, HasParameters], return attribute -def make_attribute(template: Union[PropertyTemplate, ConditionTemplate, ParameterTemplate], - value: Union[BaseValue, str, float, int] - ) -> Union[Property, Condition, Parameter]: - """ - Generate an Attribute and the contained Value. +def make_attribute( + template: Union[PropertyTemplate, ConditionTemplate, ParameterTemplate], + value: Union[BaseValue, str, float, int], +) -> Union[Property, Condition, Parameter]: + """Generate an Attribute and the contained Value. Parameters ---------- @@ -311,7 +328,7 @@ def make_attribute(template: Union[PropertyTemplate, ConditionTemplate, Paramete attempt to generate an appropriate Value given a str, float or int. Returns - -------- + ------- BaseAttribute The generated attribute @@ -333,10 +350,8 @@ def make_attribute(template: Union[PropertyTemplate, ConditionTemplate, Paramete return attribute -def make_value(value: Union[str, float, int], - bounds: BaseBounds) -> BaseValue: - """ - Generate a Value object based upon a number or string and a particular bounds. +def make_value(value: Union[str, float, int], bounds: BaseBounds) -> BaseValue: + """Generate a Value object based upon a number or string and a particular bounds. Parameters ---------- @@ -346,7 +361,7 @@ def make_value(value: Union[str, float, int], The bounds type to determine which value type we want to coerce the value into Returns - -------- + ------- BaseValue The generated value diff --git a/gemd/demo/cake.py b/gemd/demo/cake.py index 794744fd..fe4b0cbd 100644 --- a/gemd/demo/cake.py +++ b/gemd/demo/cake.py @@ -1,43 +1,73 @@ """Bake a cake.""" + +import random from importlib.resources import files from io import BytesIO -import random from gemd.entity.attribute import Condition, Parameter, Property, PropertyAndConditions from gemd.entity.base_entity import BaseEntity -from gemd.entity.bounds import IntegerBounds, RealBounds, CategoricalBounds, CompositionBounds, \ - MolecularStructureBounds -from gemd.entity.object import ProcessSpec, ProcessRun, MaterialSpec, MaterialRun, \ - MeasurementSpec, MeasurementRun, IngredientSpec, IngredientRun -from gemd.entity.template import ProcessTemplate, MaterialTemplate, MeasurementTemplate, \ - PropertyTemplate, ParameterTemplate, ConditionTemplate -from gemd.entity.value import NominalInteger, UniformInteger, \ - NominalReal, NormalReal, UniformReal, \ - NominalCategorical, DiscreteCategorical, \ - NominalComposition, EmpiricalFormula, \ - Smiles, InChI -from gemd.enumeration.origin import Origin - -from gemd.entity.util import complete_material_history, make_instance +from gemd.entity.bounds import ( + CategoricalBounds, + CompositionBounds, + IntegerBounds, + MolecularStructureBounds, + RealBounds, +) from gemd.entity.file_link import FileLink +from gemd.entity.object import ( + IngredientRun, + IngredientSpec, + MaterialRun, + MaterialSpec, + MeasurementRun, + MeasurementSpec, + ProcessRun, + ProcessSpec, +) from gemd.entity.source.performed_source import PerformedSource +from gemd.entity.template import ( + ConditionTemplate, + MaterialTemplate, + MeasurementTemplate, + ParameterTemplate, + ProcessTemplate, + PropertyTemplate, +) +from gemd.entity.util import complete_material_history, make_instance +from gemd.entity.value import ( + DiscreteCategorical, + EmpiricalFormula, + InChI, + NominalCategorical, + NominalComposition, + NominalInteger, + NominalReal, + NormalReal, + Smiles, + UniformInteger, + UniformReal, +) +from gemd.enumeration.origin import Origin from gemd.json import GEMDJson - from gemd.util.impl import recursive_foreach __all__ = [ - "change_scope", "get_demo_scope", "get_template_scope", "import_toothpick_picture", - "make_cake_templates", "make_cake_spec", "make_cake" + "change_scope", + "get_demo_scope", + "get_template_scope", + "import_toothpick_picture", + "make_cake_templates", + "make_cake_spec", + "make_cake", ] # For now, module constant, though likely this should get promoted to a package level -DEMO_SCOPE = 'citrine-demo' -TEMPLATE_SCOPE = DEMO_SCOPE + '-template' +DEMO_SCOPE = "citrine-demo" +TEMPLATE_SCOPE = DEMO_SCOPE + "-template" def change_scope(data, *, templates=None): - """ - Change scope(s) of internal uids. + """Change scope(s) of internal uids. Parameters ---------- @@ -51,7 +81,7 @@ def change_scope(data, *, templates=None): global DEMO_SCOPE, TEMPLATE_SCOPE DEMO_SCOPE = data if templates is None: - TEMPLATE_SCOPE = DEMO_SCOPE + '-template' + TEMPLATE_SCOPE = DEMO_SCOPE + "-template" else: TEMPLATE_SCOPE = templates @@ -79,164 +109,159 @@ def make_cake_templates(): tmpl["Mixer speed setting"] = ParameterTemplate( name="Mixer speed setting", description="What speed setting to use on the mixer", - bounds=IntegerBounds(0, 10) + bounds=IntegerBounds(0, 10), ) - tmpl['Cooking time'] = ConditionTemplate( + tmpl["Cooking time"] = ConditionTemplate( name="Cooking time", description="The time elapsed during a cooking process", - bounds=RealBounds(0, 7 * 24.0, "hr") + bounds=RealBounds(0, 7 * 24.0, "hr"), ) tmpl["Oven temperature setting"] = ParameterTemplate( name="Oven temperature setting", description="Where the knob points", - bounds=RealBounds(0, 2000.0, "K") + bounds=RealBounds(0, 2000.0, "K"), ) tmpl["Oven temperature"] = ConditionTemplate( name="Oven temperature", description="Actual temperature measured by the thermocouple", - bounds=RealBounds(0, 2000.0, "K") + bounds=RealBounds(0, 2000.0, "K"), ) tmpl["Toothpick test"] = PropertyTemplate( name="Toothpick test", description="Results of inserting a toothpick to check doneness", - bounds=CategoricalBounds(["wet", "crumbs", "completely clean"]) + bounds=CategoricalBounds(["wet", "crumbs", "completely clean"]), ) tmpl["Color"] = PropertyTemplate( name="Baked color", description="Visual observation of the color of a baked good", - bounds=CategoricalBounds(["Pale", "Golden brown", "Deep brown", "Black"]) + bounds=CategoricalBounds(["Pale", "Golden brown", "Deep brown", "Black"]), ) tmpl["Tastiness"] = PropertyTemplate( name="Tastiness", description="Yumminess on a fairly arbitrary scale", - bounds=IntegerBounds(lower_bound=1, upper_bound=10) + bounds=IntegerBounds(lower_bound=1, upper_bound=10), ) tmpl["Nutritional Information"] = PropertyTemplate( name="Nutritional Information", description="FDA Nutrition Facts, mass basis. Please be attentive to g vs. mg. " - "`other-carbohydrate` and `other-fat` are the total values minus the " - "broken-out quantities. Other is the difference between the total and the " - "serving size.", + "`other-carbohydrate` and `other-fat` are the total values minus the " + "broken-out quantities. Other is the difference between the total and the " + "serving size.", bounds=CompositionBounds( components=[ - 'other', - 'saturated-fat', - 'trans-fat', - 'other-fat', - 'cholesterol', - 'sodium', - 'dietary-fiber', - 'sugars', - 'other-carbohydrate', - 'protein', - 'vitamin-d', - 'calcium', - 'iron', - 'potassium' + "other", + "saturated-fat", + "trans-fat", + "other-fat", + "cholesterol", + "sodium", + "dietary-fiber", + "sugars", + "other-carbohydrate", + "protein", + "vitamin-d", + "calcium", + "iron", + "potassium", ] - ) + ), ) tmpl["Sample Mass"] = ConditionTemplate( name="Sample Mass", description="Sample size in mass units, to go along with FDA Nutrition Facts", - bounds=RealBounds(1.e-3, 1.e4, "g") + bounds=RealBounds(1.0e-3, 1.0e4, "g"), ) tmpl["Expected Sample Mass"] = ParameterTemplate( name="Expected Sample Mass", description="Specified sample size in mass units, to go along with FDA Nutrition Facts", - bounds=RealBounds(1.e-3, 1.e4, "g") + bounds=RealBounds(1.0e-3, 1.0e4, "g"), ) tmpl["Chemical Formula"] = PropertyTemplate( name="Chemical Formula", description="The chemical formula of a material", - bounds=CompositionBounds(components=EmpiricalFormula.all_elements()) + bounds=CompositionBounds(components=EmpiricalFormula.all_elements()), ) tmpl["Molecular Structure"] = PropertyTemplate( name="Molecular Structure", description="The molecular structure of the material", - bounds=MolecularStructureBounds() + bounds=MolecularStructureBounds(), ) # Objects tmpl["Procuring"] = ProcessTemplate( name="Procuring", description="Buyin' stuff", - allowed_names=[] # Takes no ingredients by definition + allowed_names=[], # Takes no ingredients by definition ) tmpl["Baking"] = ProcessTemplate( name="Baking", - description='Using heat to promote chemical reactions in a material', - allowed_names=['batter'], - allowed_labels=['precursor'], + description="Using heat to promote chemical reactions in a material", + allowed_names=["batter"], + allowed_labels=["precursor"], conditions=[(tmpl["Oven temperature"], RealBounds(0, 700, "degF"))], - parameters=[(tmpl["Oven temperature setting"], RealBounds(100, 550, "degF"))] + parameters=[(tmpl["Oven temperature setting"], RealBounds(100, 550, "degF"))], ) tmpl["Icing"] = ProcessTemplate( name="Icing", - description='Applying a coating to a substrate', - allowed_labels=['coating', 'substrate'] + description="Applying a coating to a substrate", + allowed_labels=["coating", "substrate"], ) tmpl["Mixing"] = ProcessTemplate( name="Mixing", - description='Physically combining ingredients', - allowed_labels=['wet', 'dry', 'leavening', 'seasoning', - 'sweetener', 'shortening', 'flavoring'], - parameters=[tmpl["Mixer speed setting"]] + description="Physically combining ingredients", + allowed_labels=[ + "wet", + "dry", + "leavening", + "seasoning", + "sweetener", + "shortening", + "flavoring", + ], + parameters=[tmpl["Mixer speed setting"]], ) tmpl["Generic Material"] = MaterialTemplate(name="Generic") tmpl["Nutritional Material"] = MaterialTemplate( name="Nutritional Material", description="A material with FDA Nutrition Facts attached", - properties=[ - tmpl["Nutritional Information"] - ] + properties=[tmpl["Nutritional Information"]], ) tmpl["Formulaic Material"] = MaterialTemplate( name="Formulaic Material", description="A material with chemical characterization", - properties=[ - tmpl["Chemical Formula"], - tmpl["Molecular Structure"] - ] + properties=[tmpl["Chemical Formula"], tmpl["Molecular Structure"]], ) tmpl["Baked Good"] = MaterialTemplate( - name="Baked Good", - properties=[tmpl["Toothpick test"], tmpl["Color"]] - ) - tmpl["Dessert"] = MaterialTemplate( - name="Dessert", - properties=[tmpl["Tastiness"]] + name="Baked Good", properties=[tmpl["Toothpick test"], tmpl["Color"]] ) + tmpl["Dessert"] = MaterialTemplate(name="Dessert", properties=[tmpl["Tastiness"]]) tmpl["Doneness"] = MeasurementTemplate( name="Doneness test", description="An ensemble of tests to determine the doneness of a baked good", - properties=[tmpl["Toothpick test"], tmpl["Color"]] - ) - tmpl["Taste test"] = MeasurementTemplate( - name="Taste test", - properties=[tmpl["Tastiness"]] + properties=[tmpl["Toothpick test"], tmpl["Color"]], ) + tmpl["Taste test"] = MeasurementTemplate(name="Taste test", properties=[tmpl["Tastiness"]]) tmpl["Nutritional Analysis"] = MeasurementTemplate( name="Nutritional Analysis", properties=[tmpl["Nutritional Information"]], conditions=[tmpl["Sample Mass"]], - parameters=[tmpl["Expected Sample Mass"]] + parameters=[tmpl["Expected Sample Mass"]], ) tmpl["Elemental Analysis"] = MeasurementTemplate( name="Elemental Analysis", properties=[tmpl["Chemical Formula"]], conditions=[tmpl["Sample Mass"]], - parameters=[tmpl["Expected Sample Mass"]] + parameters=[tmpl["Expected Sample Mass"]], ) for key in tmpl: - tmpl[key].add_uid(TEMPLATE_SCOPE, key.lower().replace(' ', '-')) + tmpl[key].add_uid(TEMPLATE_SCOPE, key.lower().replace(" ", "-")) return tmpl @@ -254,27 +279,27 @@ def _make_ingredient(*, material, process, **kwargs): tags=list(material.tags), material=material, process=process, - uids={DEMO_SCOPE: "{}--{}".format(material.uids[DEMO_SCOPE], - process.uids[DEMO_SCOPE] - )}, - **kwargs + uids={DEMO_SCOPE: f"{material.uids[DEMO_SCOPE]}--{process.uids[DEMO_SCOPE]}"}, + **kwargs, ) - def _make_material(*, material_name, template, process_tmpl_name, process_kwargs, - **material_kwargs): + def _make_material( + *, material_name, template, process_tmpl_name, process_kwargs, **material_kwargs + ): """Convenience method to reuse material name in creating a material's arguments.""" - process_name = "{} {}".format(process_tmpl_name, material_name) + process_name = f"{process_tmpl_name} {material_name}" return MaterialSpec( name=material_name, - uids={DEMO_SCOPE: material_name.lower().replace(' ', '-')}, + uids={DEMO_SCOPE: material_name.lower().replace(" ", "-")}, template=template, process=ProcessSpec( name=process_name, - uids={DEMO_SCOPE: process_name.lower().replace(' ', '-')}, + uids={DEMO_SCOPE: process_name.lower().replace(" ", "-")}, template=tmpl[process_tmpl_name], - **process_kwargs + **process_kwargs, ), - **material_kwargs) + **material_kwargs, + ) ############################################################################################### # Objects @@ -282,27 +307,26 @@ def _make_material(*, material_name, template, process_tmpl_name, process_kwargs material_name="Cake", process_tmpl_name="Icing", process_kwargs={ - "tags": ['spreading'], - "notes": 'The act of covering a baked output with frosting' + "tags": ["spreading"], + "notes": "The act of covering a baked output with frosting", }, template=tmpl["Dessert"], properties=[ - PropertyAndConditions(Property(name="Tastiness", - value=NominalInteger(5), - template=tmpl["Tastiness"], - origin="specified" - )) + PropertyAndConditions( + Property( + name="Tastiness", + value=NominalInteger(5), + template=tmpl["Tastiness"], + origin="specified", + ) + ) ], file_links=FileLink( filename="Becky's Butter Cake", - url='https://www.landolakes.com/recipe/16730/becky-s-butter-cake/' + url="https://www.landolakes.com/recipe/16730/becky-s-butter-cake/", ), - tags=[ - 'cake::butter cake', - 'dessert::baked::cake', - 'iced::chocolate' - ], - notes='Butter cake recipe reminiscent of the 1-2-3-4 cake that Grandma may have baked.' + tags=["cake::butter cake", "dessert::baked::cake", "iced::chocolate"], + notes="Butter cake recipe reminiscent of the 1-2-3-4 cake that Grandma may have baked.", ) ######################## @@ -310,52 +334,51 @@ def _make_material(*, material_name, template, process_tmpl_name, process_kwargs material_name="Frosting", process_tmpl_name="Mixing", process_kwargs={ - "tags": [ - 'mixing' - ], + "tags": ["mixing"], "parameters": [ - Parameter(name='Mixer speed setting', - template=tmpl['Mixer speed setting'], - origin='specified', - value=NominalInteger(2)) + Parameter( + name="Mixer speed setting", + template=tmpl["Mixer speed setting"], + origin="specified", + value=NominalInteger(2), + ) ], - "notes": 'Combining ingredients to make a sweet frosting' + "notes": "Combining ingredients to make a sweet frosting", }, template=tmpl["Dessert"], - tags=[ - 'frosting::chocolate', - 'topping::chocolate' - ], - notes='Chocolate frosting' + tags=["frosting::chocolate", "topping::chocolate"], + notes="Chocolate frosting", ) _make_ingredient( material=frosting, - notes='Seems like a lot of frosting', - labels=['coating'], + notes="Seems like a lot of frosting", + labels=["coating"], process=cake_obj.process, - absolute_quantity=NominalReal(nominal=0.751, units='kg') + absolute_quantity=NominalReal(nominal=0.751, units="kg"), ) baked_cake = _make_material( material_name="Baked Cake", process_tmpl_name="Baking", process_kwargs={ - "tags": [ - 'oven::baking' - ], + "tags": ["oven::baking"], "conditions": [ - Condition(name='Cooking time', - template=tmpl['Cooking time'], - origin=Origin.SPECIFIED, - value=NormalReal(mean=50, std=5, units='min')) + Condition( + name="Cooking time", + template=tmpl["Cooking time"], + origin=Origin.SPECIFIED, + value=NormalReal(mean=50, std=5, units="min"), + ) ], "parameters": [ - Parameter(name='Oven temperature setting', - template=tmpl['Oven temperature setting'], - origin="specified", - value=NominalReal(nominal=350, units='degF')) + Parameter( + name="Oven temperature setting", + template=tmpl["Oven temperature setting"], + origin="specified", + value=NominalReal(nominal=350, units="degF"), + ) ], - "notes": 'Using heat to convert batter into a solid matrix' + "notes": "Using heat to convert batter into a solid matrix", }, template=tmpl["Baked Good"], properties=[ @@ -363,7 +386,7 @@ def _make_material(*, material_name, template, process_tmpl_name, process_kwargs property=Property( name="Toothpick test", value=NominalCategorical("completely clean"), - template=tmpl["Toothpick test"] + template=tmpl["Toothpick test"], ) ), PropertyAndConditions( @@ -371,109 +394,82 @@ def _make_material(*, material_name, template, process_tmpl_name, process_kwargs name="Color", value=NominalCategorical("Golden brown"), template=tmpl["Color"], - origin="specified" + origin="specified", ) - ) - ], - tags=[ - 'substrate' + ), ], - notes='The cakey part of the cake' - ) - _make_ingredient( - material=baked_cake, - labels=['substrate'], - process=cake_obj.process + tags=["substrate"], + notes="The cakey part of the cake", ) + _make_ingredient(material=baked_cake, labels=["substrate"], process=cake_obj.process) ######################## batter = _make_material( material_name="Batter", process_tmpl_name="Mixing", process_kwargs={ - "tags": [ - 'mixing' - ], + "tags": ["mixing"], "parameters": [ - Parameter(name='Mixer speed setting', - template=tmpl['Mixer speed setting'], - origin='specified', - value=NominalInteger(2)) + Parameter( + name="Mixer speed setting", + template=tmpl["Mixer speed setting"], + origin="specified", + value=NominalInteger(2), + ) ], - "notes": 'Combining ingredients to make a baking feedstock' + "notes": "Combining ingredients to make a baking feedstock", }, template=tmpl["Generic Material"], - tags=[ - 'mixture' - ], - notes='The fluid that converts to cake with heat' - ) - _make_ingredient( - material=batter, - labels=['precursor'], - process=baked_cake.process + tags=["mixture"], + notes="The fluid that converts to cake with heat", ) + _make_ingredient(material=batter, labels=["precursor"], process=baked_cake.process) ######################## wetmix = _make_material( material_name="Wet Ingredients", process_tmpl_name="Mixing", process_kwargs={ - "tags": [ - 'mixing' - ], + "tags": ["mixing"], "parameters": [ - Parameter(name='Mixer speed setting', - template=tmpl['Mixer speed setting'], - origin='specified', - value=NominalInteger(2)) + Parameter( + name="Mixer speed setting", + template=tmpl["Mixer speed setting"], + origin="specified", + value=NominalInteger(2), + ) ], - "notes": 'Combining wet ingredients to make a baking feedstock' + "notes": "Combining wet ingredients to make a baking feedstock", }, template=tmpl["Generic Material"], - tags=[ - "mixture" - ], - notes='The wet fraction of a batter' - ) - _make_ingredient( - material=wetmix, - labels=['wet'], - process=batter.process + tags=["mixture"], + notes="The wet fraction of a batter", ) + _make_ingredient(material=wetmix, labels=["wet"], process=batter.process) drymix = _make_material( material_name="Dry Ingredients", process_tmpl_name="Mixing", process_kwargs={ - "tags": [ - 'mixing' - ], - "notes": 'Combining dry ingredients to make a baking feedstock' + "tags": ["mixing"], + "notes": "Combining dry ingredients to make a baking feedstock", }, template=tmpl["Generic Material"], - tags=[ - "mixture" - ], - notes='The dry fraction of a batter' + tags=["mixture"], + notes="The dry fraction of a batter", ) _make_ingredient( material=drymix, - labels=['dry'], + labels=["dry"], process=batter.process, - absolute_quantity=NominalReal(nominal=3.052, units='cups') + absolute_quantity=NominalReal(nominal=3.052, units="cups"), ) ######################## flour = _make_material( material_name="Flour", process_tmpl_name="Procuring", - process_kwargs={ - "tags": [ - 'purchase::dry-goods' - ], - "notes": 'Purchasing all purpose flour' - }, + process_kwargs={"tags": ["purchase::dry-goods"], "notes": "Purchasing all purpose flour"}, template=tmpl["Nutritional Material"], properties=[ PropertyAndConditions( @@ -485,284 +481,207 @@ def _make_material(*, material_name, template, process_tmpl_name, process_kwargs "sugars": 1, "other-carbohydrate": 20, "protein": 4, - "other": 4 + "other": 4, } ), template=tmpl["Nutritional Information"], - origin="specified" + origin="specified", ), conditions=Condition( name="Serving Size", - value=NominalReal(30, 'g'), + value=NominalReal(30, "g"), template=tmpl["Sample Mass"], - origin="specified" - ) + origin="specified", + ), ) ], - tags=[ - 'raw material', - 'flour', - 'dry-goods' - ], - notes='All-purpose flour' + tags=["raw material", "flour", "dry-goods"], + notes="All-purpose flour", ) _make_ingredient( material=flour, - labels=['dry'], + labels=["dry"], process=drymix.process, - volume_fraction=NominalReal(nominal=0.9829, units='') # 3 cups + volume_fraction=NominalReal(nominal=0.9829, units=""), # 3 cups ) baking_powder = _make_material( material_name="Baking Powder", process_tmpl_name="Procuring", - process_kwargs={ - "tags": [ - 'purchase::dry-goods' - ], - "notes": 'Purchasing baking powder' - }, + process_kwargs={"tags": ["purchase::dry-goods"], "notes": "Purchasing baking powder"}, template=tmpl["Generic Material"], - tags=[ - 'raw material', - 'leavening', - 'dry-goods' - ], - notes='Leavening agent for cake' + tags=["raw material", "leavening", "dry-goods"], + notes="Leavening agent for cake", ) _make_ingredient( material=baking_powder, - labels=['leavening', 'dry'], + labels=["leavening", "dry"], process=drymix.process, - volume_fraction=NominalReal(nominal=0.0137, units='') # 2 teaspoons + volume_fraction=NominalReal(nominal=0.0137, units=""), # 2 teaspoons ) salt = _make_material( material_name="Salt", process_tmpl_name="Procuring", - process_kwargs={ - "tags": [ - 'purchase::dry-goods' - ], - "notes": 'Purchasing salt' - }, + process_kwargs={"tags": ["purchase::dry-goods"], "notes": "Purchasing salt"}, template=tmpl["Formulaic Material"], - tags=[ - 'raw material', - 'seasoning', - 'dry-goods' - ], - notes='Plain old NaCl', + tags=["raw material", "seasoning", "dry-goods"], + notes="Plain old NaCl", properties=[ - PropertyAndConditions(Property(name='Formula', value=EmpiricalFormula("NaCl"))) - ] + PropertyAndConditions(Property(name="Formula", value=EmpiricalFormula("NaCl"))) + ], ) _make_ingredient( material=salt, - labels=['dry', 'seasoning'], + labels=["dry", "seasoning"], process=drymix.process, - volume_fraction=NominalReal(nominal=0.0034, units='') # 1/2 teaspoon + volume_fraction=NominalReal(nominal=0.0034, units=""), # 1/2 teaspoon ) sugar = _make_material( material_name="Sugar", process_tmpl_name="Procuring", - process_kwargs={ - "tags": [ - 'purchase::dry-goods' - ], - "notes": 'Purchasing granulated sugar' - }, + process_kwargs={"tags": ["purchase::dry-goods"], "notes": "Purchasing granulated sugar"}, template=tmpl["Formulaic Material"], - tags=[ - 'raw material', - 'sweetener', - 'dry-goods' - ], - notes='Sugar', + tags=["raw material", "sweetener", "dry-goods"], + notes="Sugar", properties=[ PropertyAndConditions(Property(name="Formula", value=EmpiricalFormula("C12H22O11"))), PropertyAndConditions( - Property(name='SMILES', - value=Smiles("C(C1C(C(C(C(O1)OC2(C(C(C(O2)CO)O)O)CO)O)O)O)O"), - template=tmpl["Molecular Structure"] - ) - ) - ] + Property( + name="SMILES", + value=Smiles("C(C1C(C(C(C(O1)OC2(C(C(C(O2)CO)O)O)CO)O)O)O)O"), + template=tmpl["Molecular Structure"], + ) + ), + ], ) _make_ingredient( material=sugar, - labels=['wet', 'sweetener'], + labels=["wet", "sweetener"], process=wetmix.process, - absolute_quantity=NominalReal(nominal=2, units='cups') + absolute_quantity=NominalReal(nominal=2, units="cups"), ) butter = _make_material( material_name="Butter", process_tmpl_name="Procuring", - process_kwargs={ - "tags": [ - 'purchase::produce' - ], - "notes": 'Purchasing butter' - }, + process_kwargs={"tags": ["purchase::produce"], "notes": "Purchasing butter"}, template=tmpl["Generic Material"], - tags=[ - 'raw material', - 'produce', - 'shortening', - 'dairy' - ], - notes='Shortening for making rich, buttery baked goods' + tags=["raw material", "produce", "shortening", "dairy"], + notes="Shortening for making rich, buttery baked goods", ) _make_ingredient( material=butter, - labels=['wet', 'shortening'], + labels=["wet", "shortening"], process=wetmix.process, - absolute_quantity=NominalReal(nominal=1, units='cups') + absolute_quantity=NominalReal(nominal=1, units="cups"), ) _make_ingredient( material=butter, - labels=['shortening'], + labels=["shortening"], process=frosting.process, - mass_fraction=NominalReal(nominal=0.1434, units='') # 1/2 c @ 0.911 g/cc + mass_fraction=NominalReal(nominal=0.1434, units=""), # 1/2 c @ 0.911 g/cc ) eggs = _make_material( material_name="Eggs", process_tmpl_name="Procuring", - process_kwargs={ - "tags": [ - 'purchase::produce' - ], - "notes": 'Purchasing eggs' - }, + process_kwargs={"tags": ["purchase::produce"], "notes": "Purchasing eggs"}, template=tmpl["Generic Material"], tags=[ - 'raw material', - 'produce', + "raw material", + "produce", ], - notes='A custard waiting to happen' + notes="A custard waiting to happen", ) _make_ingredient( material=eggs, - labels=['wet'], + labels=["wet"], process=wetmix.process, - absolute_quantity=NominalReal(nominal=4, units='count') + absolute_quantity=NominalReal(nominal=4, units="count"), ) vanilla = _make_material( material_name="Vanilla", process_tmpl_name="Procuring", - process_kwargs={ - "tags": [ - 'purchase::solution' - ], - "notes": 'Purchasing vanilla' - }, + process_kwargs={"tags": ["purchase::solution"], "notes": "Purchasing vanilla"}, template=tmpl["Generic Material"], - tags=[ - 'raw material', - 'seasoning' - ], - notes='Vanilla Extract is mostly alcohol but the most important component ' - 'is vanillin (see attached structure)', + tags=["raw material", "seasoning"], + notes="Vanilla Extract is mostly alcohol but the most important component " + "is vanillin (see attached structure)", properties=[ PropertyAndConditions( - Property(name='Component Structure', - value=InChI("InChI=1S/C8H8O3/c1-11-8-4-6(5-9)2-3-7(8)10/h2-5,10H,1H3"), - template=tmpl["Molecular Structure"] - ) + Property( + name="Component Structure", + value=InChI("InChI=1S/C8H8O3/c1-11-8-4-6(5-9)2-3-7(8)10/h2-5,10H,1H3"), + template=tmpl["Molecular Structure"], + ) ) - ] + ], ) _make_ingredient( material=vanilla, - labels=['wet', 'flavoring'], + labels=["wet", "flavoring"], process=wetmix.process, - absolute_quantity=NominalReal(nominal=2, units='teaspoons') + absolute_quantity=NominalReal(nominal=2, units="teaspoons"), ) _make_ingredient( material=vanilla, - labels=['flavoring'], + labels=["flavoring"], process=frosting.process, - mass_fraction=NominalReal(nominal=0.0231, units='') # 2 tsp @ 0.879 g/cc + mass_fraction=NominalReal(nominal=0.0231, units=""), # 2 tsp @ 0.879 g/cc ) milk = _make_material( material_name="Milk", process_tmpl_name="Procuring", - process_kwargs={ - "tags": [ - 'purchase::produce' - ], - "notes": 'Purchasing milk' - }, + process_kwargs={"tags": ["purchase::produce"], "notes": "Purchasing milk"}, template=tmpl["Generic Material"], - tags=[ - 'raw material', - 'produce', - 'dairy' - ], - notes='' + tags=["raw material", "produce", "dairy"], + notes="", ) _make_ingredient( material=milk, - labels=['wet'], + labels=["wet"], process=batter.process, - absolute_quantity=NominalReal(nominal=1, units='cup') + absolute_quantity=NominalReal(nominal=1, units="cup"), ) _make_ingredient( material=milk, labels=[], process=frosting.process, - mass_fraction=NominalReal(nominal=0.0816, units='') # 1/4 c @ 1.037 g/cc + mass_fraction=NominalReal(nominal=0.0816, units=""), # 1/4 c @ 1.037 g/cc ) chocolate = _make_material( material_name="Chocolate", process_tmpl_name="Procuring", - process_kwargs={ - "tags": [ - 'purchase::dry-goods' - ], - "notes": 'Purchasing chocolate' - }, + process_kwargs={"tags": ["purchase::dry-goods"], "notes": "Purchasing chocolate"}, template=tmpl["Generic Material"], - tags=[ - 'raw material' - ], - notes='' + tags=["raw material"], + notes="", ) _make_ingredient( material=chocolate, - labels=['flavoring'], + labels=["flavoring"], process=frosting.process, - mass_fraction=NominalReal(nominal=0.1132, units='') # 3 oz. + mass_fraction=NominalReal(nominal=0.1132, units=""), # 3 oz. ) powder_sugar = _make_material( material_name="Powdered Sugar", process_tmpl_name="Procuring", - process_kwargs={ - "tags": [ - 'purchase::dry-goods' - ], - "notes": 'Purchasing powdered sugar' - }, + process_kwargs={"tags": ["purchase::dry-goods"], "notes": "Purchasing powdered sugar"}, template=tmpl["Generic Material"], - tags=[ - 'raw material', - 'sweetener', - 'dry-goods' - ], - notes='Granulated sugar mixed with corn starch' + tags=["raw material", "sweetener", "dry-goods"], + notes="Granulated sugar mixed with corn starch", ) _make_ingredient( material=powder_sugar, - labels=['flavoring'], + labels=["flavoring"], process=frosting.process, - mass_fraction=NominalReal(nominal=0.6387, units='') # 4 c @ 30 g/ 0.25 cups + mass_fraction=NominalReal(nominal=0.6387, units=""), # 4 c @ 30 g/ 0.25 cups ) return cake_obj @@ -770,8 +689,8 @@ def _make_material(*, material_name, template, process_tmpl_name, process_kwargs def make_cake(seed=None, tmpl=None, cake_spec=None, toothpick_img=None): """Define all objects that go into making a demo cake.""" - import struct import hashlib + import struct if seed is not None: random.seed(seed) @@ -792,65 +711,68 @@ def make_cake(seed=None, tmpl=None, cake_spec=None, toothpick_img=None): ###################################################################### # Objects cake_obj = make_instance(cake_spec) - operators = ['gwash', 'jadams', 'thomasj', 'jmadison', 'jmonroe'] - producers = ['Fresh Farm', 'Sunnydale', 'Greenbrook'] - drygoods = ['Acme', 'A1', 'Reliable', "Big Box"] - cake_obj.process.source = PerformedSource(performed_by=random.choice(operators), - performed_date='2015-03-14') + operators = ["gwash", "jadams", "thomasj", "jmadison", "jmonroe"] + producers = ["Fresh Farm", "Sunnydale", "Greenbrook"] + drygoods = ["Acme", "A1", "Reliable", "Big Box"] + cake_obj.process.source = PerformedSource( + performed_by=random.choice(operators), performed_date="2015-03-14" + ) def _randomize_object(item: BaseEntity): # Add in the randomized particular values if not isinstance(item, (MaterialRun, ProcessRun, IngredientRun)): return - item.add_uid(DEMO_SCOPE, '{}-{}'.format(item.spec.uids[DEMO_SCOPE], run_key)) + item.add_uid(DEMO_SCOPE, f"{item.spec.uids[DEMO_SCOPE]}-{run_key}") if item.spec.tags is not None: item.tags = list(item.spec.tags) if item.spec.notes: # Neither None nor empty string - item.notes = 'The spec says "{}"'.format(item.spec.notes) + item.notes = f'The spec says "{item.spec.notes}"' if isinstance(item, MaterialRun): - if 'raw material' in item.tags: - if 'produce' in item.tags: + if "raw material" in item.tags: + if "produce" in item.tags: supplier = random.choice(producers) else: supplier = random.choice(drygoods) - item.name = "{} {}".format(supplier, item.spec.name) + item.name = f"{supplier} {item.spec.name}" if isinstance(item, ProcessRun): if item.template.name == "Procuring": - item.source = PerformedSource(performed_by='hamilton', - performed_date='2015-02-17') - item.name = "{} {}".format(item.template.name, item.output_material.name) + item.source = PerformedSource(performed_by="hamilton", performed_date="2015-02-17") + item.name = f"{item.template.name} {item.output_material.name}" else: item.source = cake_obj.process.source if isinstance(item, IngredientRun): fuzz = 0.95 + 0.1 * random.random() if item.spec.absolute_quantity is not None: - item.absolute_quantity = \ - NormalReal(mean=fuzz * item.spec.absolute_quantity.nominal, - std=0.05 * item.spec.absolute_quantity.nominal, - units=item.spec.absolute_quantity.units) + item.absolute_quantity = NormalReal( + mean=fuzz * item.spec.absolute_quantity.nominal, + std=0.05 * item.spec.absolute_quantity.nominal, + units=item.spec.absolute_quantity.units, + ) if item.spec.volume_fraction is not None: # The only element here is dry mix, and it's almost entirely flour - item.volume_fraction = \ - NormalReal(mean=0.01 * (fuzz - 0.5) + item.spec.volume_fraction.nominal, - std=0.005, - units=item.spec.volume_fraction.units) + item.volume_fraction = NormalReal( + mean=0.01 * (fuzz - 0.5) + item.spec.volume_fraction.nominal, + std=0.005, + units=item.spec.volume_fraction.units, + ) if item.spec.mass_fraction is not None: - item.mass_fraction = \ - UniformReal(lower_bound=(fuzz - 0.05) * item.spec.mass_fraction.nominal, - upper_bound=(fuzz + 0.05) * item.spec.mass_fraction.nominal, - units=item.spec.mass_fraction.units) + item.mass_fraction = UniformReal( + lower_bound=(fuzz - 0.05) * item.spec.mass_fraction.nominal, + upper_bound=(fuzz + 0.05) * item.spec.mass_fraction.nominal, + units=item.spec.mass_fraction.units, + ) if item.spec.number_fraction is not None: - item.number_fraction = \ - NormalReal(mean=fuzz * item.spec.number_fraction.nominal, - std=0.05 * item.spec.number_fraction.nominal, - units=item.spec.number_fraction.units) + item.number_fraction = NormalReal( + mean=fuzz * item.spec.number_fraction.nominal, + std=0.05 * item.spec.number_fraction.nominal, + units=item.spec.number_fraction.units, + ) + recursive_foreach(cake_obj, _randomize_object) - frosting = \ - next(x.material for x in cake_obj.process.ingredients if 'rosting' in x.name) - baked = \ - next(x.material for x in cake_obj.process.ingredients if 'aked' in x.name) + frosting = next(x.material for x in cake_obj.process.ingredients if "rosting" in x.name) + baked = next(x.material for x in cake_obj.process.ingredients if "aked" in x.name) def _find_name(name, material): """Recursively search for the right material.""" @@ -862,195 +784,232 @@ def _find_name(name, material): return result return - flour = _find_name('Flour', cake_obj) - salt = _find_name('Salt', cake_obj) - sugar = _find_name('Sugar', cake_obj) + flour = _find_name("Flour", cake_obj) + salt = _find_name("Salt", cake_obj) + sugar = _find_name("Sugar", cake_obj) # Add measurements - cake_taste = MeasurementRun(name='Final Taste', material=cake_obj) - cake_appearance = MeasurementRun(name='Final Appearance', material=cake_obj) - frosting_taste = MeasurementRun(name='Frosting Taste', material=frosting) - frosting_sweetness = MeasurementRun(name='Frosting Sweetness', material=frosting) - baked_doneness = MeasurementRun(name='Baking doneness', material=baked) - flour_content = MeasurementRun(name='Flour nutritional analysis', material=flour) - salt_content = MeasurementRun(name='Salt elemental analysis', material=salt) - sugar_content = MeasurementRun(name='Sugar elemental analysis', material=sugar) + cake_taste = MeasurementRun(name="Final Taste", material=cake_obj) + cake_appearance = MeasurementRun(name="Final Appearance", material=cake_obj) + frosting_taste = MeasurementRun(name="Frosting Taste", material=frosting) + frosting_sweetness = MeasurementRun(name="Frosting Sweetness", material=frosting) + baked_doneness = MeasurementRun(name="Baking doneness", material=baked) + flour_content = MeasurementRun(name="Flour nutritional analysis", material=flour) + salt_content = MeasurementRun(name="Salt elemental analysis", material=salt) + sugar_content = MeasurementRun(name="Sugar elemental analysis", material=sugar) if toothpick_img is not None: baked_doneness.file_links.append(toothpick_img) # and spec out the measurements - cake_taste.spec = MeasurementSpec(name='Taste', template=tmpl['Taste test']) - cake_appearance.spec = MeasurementSpec(name='Appearance') + cake_taste.spec = MeasurementSpec(name="Taste", template=tmpl["Taste test"]) + cake_appearance.spec = MeasurementSpec(name="Appearance") frosting_taste.spec = cake_taste.spec # Taste - frosting_sweetness.spec = MeasurementSpec(name='Sweetness') - baked_doneness.spec = MeasurementSpec(name='Doneness', template=tmpl["Doneness"]) - flour_content.spec = MeasurementSpec(name='Nutritional analysis', - template=tmpl["Nutritional Analysis"]) - salt_content.spec = MeasurementSpec(name='Elemental analysis', - template=tmpl["Elemental Analysis"] - ) + frosting_sweetness.spec = MeasurementSpec(name="Sweetness") + baked_doneness.spec = MeasurementSpec(name="Doneness", template=tmpl["Doneness"]) + flour_content.spec = MeasurementSpec( + name="Nutritional analysis", template=tmpl["Nutritional Analysis"] + ) + salt_content.spec = MeasurementSpec( + name="Elemental analysis", template=tmpl["Elemental Analysis"] + ) sugar_content.spec = salt_content.spec # Note that while specs are regenerated each make_cake invocation, they are all identical - for msr in (cake_taste, cake_appearance, frosting_taste, frosting_sweetness, - baked_doneness, flour_content, salt_content, sugar_content): + for msr in ( + cake_taste, + cake_appearance, + frosting_taste, + frosting_sweetness, + baked_doneness, + flour_content, + salt_content, + sugar_content, + ): msr.spec.add_uid(DEMO_SCOPE, msr.spec.name.lower()) - msr.add_uid(DEMO_SCOPE, '{}--{}-{}'.format(msr.spec.uids[DEMO_SCOPE], - msr.material.spec.uids[DEMO_SCOPE], - run_key - )) + msr.add_uid( + DEMO_SCOPE, + f"{msr.spec.uids[DEMO_SCOPE]}--{msr.material.spec.uids[DEMO_SCOPE]}-{run_key}", + ) ###################################################################### # Let's add some attributes - baked.process.conditions.append(Condition(name='Cooking time', - template=tmpl['Cooking time'], - origin=Origin.MEASURED, - value=NominalReal(nominal=48, units='min'))) - baked.process.conditions.append(Condition(name='Oven temperature', - origin="measured", - value=NominalReal(nominal=362, units='degF'))) - - cake_taste.properties.append(Property(name='Tastiness', - origin=Origin.MEASURED, - template=tmpl['Tastiness'], - value=UniformInteger(4, 5))) - cake_appearance.properties.append(Property(name='Visual Appeal', - origin=Origin.MEASURED, - value=NominalInteger(nominal=5))) - frosting_taste.properties.append(Property(name='Tastiness', - origin=Origin.MEASURED, - template=tmpl['Tastiness'], - value=NominalInteger(nominal=4))) - frosting_sweetness.properties.append(Property(name='Sweetness (Sucrose-basis)', - origin=Origin.MEASURED, - value=NominalReal(nominal=1.7, units=''))) - - baked_doneness.properties.append(Property( - name='Toothpick test', - origin="measured", - template=tmpl["Toothpick test"], - value=NominalCategorical("crumbs") - )) - baked_doneness.properties.append(Property( - name='Color', - origin="measured", - template=tmpl["Color"], - value=DiscreteCategorical({ - "Pale": 0.05, - "Golden brown": 0.65, - "Deep brown": 0.3 - }) - )) - - flour_content.properties.append(Property( - name='Nutritional Information', - value=NominalComposition( - { - "dietary-fiber": 1 * (0.99 + 0.02 * random.random()), - "sugars": 1 * (0.99 + 0.02 * random.random()), - "other-carbohydrate": 20 * (0.99 + 0.02 * random.random()), - "protein": 4 * (0.99 + 0.02 * random.random()), - "other": 4 * (0.99 + 0.02 * random.random()) - } - ), - template=tmpl["Nutritional Information"], - origin="measured" - )) - flour_content.conditions.append(Condition( - name='Sample Mass', - value=NormalReal( - mean=99 + 2 * random.random(), - std=1.5, - units='mg' - ), - template=tmpl["Sample Mass"], - origin="measured" - )) - flour_content.parameters.append(Parameter( - name='Expected Sample Mass', - value=NominalReal(nominal=0.1, units='g'), - template=tmpl["Expected Sample Mass"], - origin="specified" - )) - flour_content.spec.conditions.append(Condition( - name='Sample Mass', - value=NominalReal( - nominal=100, - units='mg' - ), - template=tmpl["Sample Mass"], - origin="specified" - )) - flour_content.spec.parameters.append(Parameter( - name='Expected Sample Mass', - value=NominalReal(nominal=0.1, units='g'), - template=tmpl["Expected Sample Mass"], - origin="specified" - )) - - salt_content.properties.append(Property( - name="Composition", - value=EmpiricalFormula(formula="NaClCa0.006Si0.006O0.018K0.000015I0.000015"), - template=tmpl["Chemical Formula"], - origin="measured" - )) - salt_content.conditions.append(Condition( - name='Sample Mass', - value=NormalReal( - mean=99 + 2 * random.random(), - std=1.5, - units='mg' - ), - template=tmpl["Sample Mass"], - origin="measured" - )) - salt_content.parameters.append(Parameter( - name='Expected Sample Mass', - value=NominalReal(nominal=0.1, units='g'), - template=tmpl["Expected Sample Mass"], - origin="specified" - )) - salt_content.spec.conditions.append(Condition( - name='Sample Mass', - value=NominalReal( - nominal=100, - units='mg' - ), - template=tmpl["Sample Mass"], - origin="specified" - )) - - sugar_content.properties.append(Property( - name="Composition", - value=EmpiricalFormula(formula='C11.996H21.995O10.997S0.00015'), - template=tmpl["Chemical Formula"], - origin="measured" - )) - sugar_content.conditions.append(Condition( - name='Sample Mass', - value=NormalReal( - mean=99 + 2 * random.random(), - std=1.5, - units='mg' - ), - template=tmpl["Sample Mass"], - origin="measured" - )) - sugar_content.spec.parameters.append(Parameter( - name='Expected Sample Mass', - value=NominalReal(nominal=0.1, units='g'), - template=tmpl["Expected Sample Mass"], - origin="specified" - )) + baked.process.conditions.append( + Condition( + name="Cooking time", + template=tmpl["Cooking time"], + origin=Origin.MEASURED, + value=NominalReal(nominal=48, units="min"), + ) + ) + baked.process.conditions.append( + Condition( + name="Oven temperature", + origin="measured", + value=NominalReal(nominal=362, units="degF"), + ) + ) + + cake_taste.properties.append( + Property( + name="Tastiness", + origin=Origin.MEASURED, + template=tmpl["Tastiness"], + value=UniformInteger(4, 5), + ) + ) + cake_appearance.properties.append( + Property(name="Visual Appeal", origin=Origin.MEASURED, value=NominalInteger(nominal=5)) + ) + frosting_taste.properties.append( + Property( + name="Tastiness", + origin=Origin.MEASURED, + template=tmpl["Tastiness"], + value=NominalInteger(nominal=4), + ) + ) + frosting_sweetness.properties.append( + Property( + name="Sweetness (Sucrose-basis)", + origin=Origin.MEASURED, + value=NominalReal(nominal=1.7, units=""), + ) + ) + + baked_doneness.properties.append( + Property( + name="Toothpick test", + origin="measured", + template=tmpl["Toothpick test"], + value=NominalCategorical("crumbs"), + ) + ) + baked_doneness.properties.append( + Property( + name="Color", + origin="measured", + template=tmpl["Color"], + value=DiscreteCategorical({"Pale": 0.05, "Golden brown": 0.65, "Deep brown": 0.3}), + ) + ) + + flour_content.properties.append( + Property( + name="Nutritional Information", + value=NominalComposition( + { + "dietary-fiber": 1 * (0.99 + 0.02 * random.random()), + "sugars": 1 * (0.99 + 0.02 * random.random()), + "other-carbohydrate": 20 * (0.99 + 0.02 * random.random()), + "protein": 4 * (0.99 + 0.02 * random.random()), + "other": 4 * (0.99 + 0.02 * random.random()), + } + ), + template=tmpl["Nutritional Information"], + origin="measured", + ) + ) + flour_content.conditions.append( + Condition( + name="Sample Mass", + value=NormalReal(mean=99 + 2 * random.random(), std=1.5, units="mg"), + template=tmpl["Sample Mass"], + origin="measured", + ) + ) + flour_content.parameters.append( + Parameter( + name="Expected Sample Mass", + value=NominalReal(nominal=0.1, units="g"), + template=tmpl["Expected Sample Mass"], + origin="specified", + ) + ) + flour_content.spec.conditions.append( + Condition( + name="Sample Mass", + value=NominalReal(nominal=100, units="mg"), + template=tmpl["Sample Mass"], + origin="specified", + ) + ) + flour_content.spec.parameters.append( + Parameter( + name="Expected Sample Mass", + value=NominalReal(nominal=0.1, units="g"), + template=tmpl["Expected Sample Mass"], + origin="specified", + ) + ) + + salt_content.properties.append( + Property( + name="Composition", + value=EmpiricalFormula(formula="NaClCa0.006Si0.006O0.018K0.000015I0.000015"), + template=tmpl["Chemical Formula"], + origin="measured", + ) + ) + salt_content.conditions.append( + Condition( + name="Sample Mass", + value=NormalReal(mean=99 + 2 * random.random(), std=1.5, units="mg"), + template=tmpl["Sample Mass"], + origin="measured", + ) + ) + salt_content.parameters.append( + Parameter( + name="Expected Sample Mass", + value=NominalReal(nominal=0.1, units="g"), + template=tmpl["Expected Sample Mass"], + origin="specified", + ) + ) + salt_content.spec.conditions.append( + Condition( + name="Sample Mass", + value=NominalReal(nominal=100, units="mg"), + template=tmpl["Sample Mass"], + origin="specified", + ) + ) + + sugar_content.properties.append( + Property( + name="Composition", + value=EmpiricalFormula(formula="C11.996H21.995O10.997S0.00015"), + template=tmpl["Chemical Formula"], + origin="measured", + ) + ) + sugar_content.conditions.append( + Condition( + name="Sample Mass", + value=NormalReal(mean=99 + 2 * random.random(), std=1.5, units="mg"), + template=tmpl["Sample Mass"], + origin="measured", + ) + ) + sugar_content.spec.parameters.append( + Parameter( + name="Expected Sample Mass", + value=NominalReal(nominal=0.1, units="g"), + template=tmpl["Expected Sample Mass"], + origin="specified", + ) + ) cake_obj.notes = cake_obj.notes + "; Très délicieux! 😀" - cake_obj.file_links = [FileLink( - filename="Photo", - url='https://storcpdkenticomedia.blob.core.windows.net/media/' - 'recipemanagementsystem/media/recipe-media-files/recipes/retail/x17/' - '16730-beckys-butter-cake-600x600.jpg?ext=.jpg' - )] + cake_obj.file_links = [ + FileLink( + filename="Photo", + url="https://storcpdkenticomedia.blob.core.windows.net/media/" + "recipemanagementsystem/media/recipe-media-files/recipes/retail/x17/" + "16730-beckys-butter-cake-600x600.jpg?ext=.jpg", + ) + ] return cake_obj @@ -1070,7 +1029,8 @@ def _find_name(name, material): with open("example_gemd_process_template.json", "w") as f: f.write( - encoder.thin_dumps(cake.process.ingredients[0].material.process.template, indent=2)) + encoder.thin_dumps(cake.process.ingredients[0].material.process.template, indent=2) + ) with open("example_gemd_measurement_template.json", "w") as f: f.write(encoder.thin_dumps(cake.measurements[0].template, indent=2)) diff --git a/gemd/demo/material_run_example.py b/gemd/demo/material_run_example.py index 072e540d..d70bb7f0 100644 --- a/gemd/demo/material_run_example.py +++ b/gemd/demo/material_run_example.py @@ -1,4 +1,5 @@ """An example ingest of a material run.""" + from gemd import units from gemd.entity.attribute.condition import Condition from gemd.entity.attribute.parameter import Parameter @@ -16,25 +17,24 @@ known_properties = { "density": PropertyTemplate( name="density", - bounds=RealBounds(lower_bound=0.0, upper_bound=1000.0, default_units='g / cm^3') + bounds=RealBounds(lower_bound=0.0, upper_bound=1000.0, default_units="g / cm^3"), ), "kinematic viscosity": PropertyTemplate( name="kinematic viscosity", - bounds=RealBounds(lower_bound=0.0, upper_bound=10.0**40, default_units="m^2 / s") - ) + bounds=RealBounds(lower_bound=0.0, upper_bound=10.0**40, default_units="m^2 / s"), + ), } known_conditions = { "temperature": ConditionTemplate( name="temperature", - bounds=RealBounds(lower_bound=0.0, upper_bound=1000.0, default_units='K') + bounds=RealBounds(lower_bound=0.0, upper_bound=1000.0, default_units="K"), ) } known_parameters = { "knob_2_setting": ParameterTemplate( - name="knob_2_setting", - bounds=CategoricalBounds(categories={"low", "medium", "high"}) + name="knob_2_setting", bounds=CategoricalBounds(categories={"low", "medium", "high"}) ) } @@ -52,8 +52,8 @@ def _parse_value(val): try: unit = units.parse_units(toks[-1]) except (ValueError, units.UndefinedUnitError): - print("Couldn't find {}".format(toks[-1])) - unit = '' + print(f"Couldn't find {toks[-1]}") + unit = "" if std >= 0: return NormalReal(mean=mean, std=std, units=unit) @@ -61,16 +61,16 @@ def _parse_value(val): return NominalReal(mean, units=unit) # if it is just a number wrap it in a nominal value elif isinstance(val, (float, int)): - return NominalReal(val, '') + return NominalReal(val, "") # if it is a single string, it's either a single number of a category elif isinstance(val, str): try: num = float(val) - return NominalReal(num, '') + return NominalReal(num, "") except ValueError: return DiscreteCategorical(val) else: - raise ValueError("Couldn't parse {}".format(val)) + raise ValueError(f"Couldn't parse {val}") def ingest_material_run(data, material_spec=None, process_run=None): @@ -79,7 +79,7 @@ def ingest_material_run(data, material_spec=None, process_run=None): return [ingest_material_run(x, material_spec) for x in data] if not isinstance(data, dict): - raise ValueError("This ingester operates on dict, but got {}".format(type(data))) + raise ValueError(f"This ingester operates on dict, but got {type(data)}") material = MaterialRun("Material Run") @@ -96,25 +96,19 @@ def ingest_material_run(data, material_spec=None, process_run=None): for name in set(known_properties.keys()).intersection(experiment.keys()): prop = Property( - name=name, - template=known_properties[name], - value=_parse_value(experiment[name]) + name=name, template=known_properties[name], value=_parse_value(experiment[name]) ) measurement.properties.append(prop) for name in set(known_conditions.keys()).intersection(experiment.keys()): cond = Condition( - name=name, - template=known_conditions[name], - value=_parse_value(experiment[name]) + name=name, template=known_conditions[name], value=_parse_value(experiment[name]) ) measurement.conditions.append(cond) for name in set(known_parameters.keys()).intersection(experiment.keys()): param = Parameter( - name=name, - template=known_parameters[name], - value=_parse_value(experiment[name]) + name=name, template=known_parameters[name], value=_parse_value(experiment[name]) ) measurement.parameters.append(param) diff --git a/gemd/demo/measurement_example.py b/gemd/demo/measurement_example.py index 3e3010ab..9755630e 100644 --- a/gemd/demo/measurement_example.py +++ b/gemd/demo/measurement_example.py @@ -1,4 +1,5 @@ """Demonstrate attaching measurements to a material.""" + import random import string @@ -28,16 +29,14 @@ def make_demo_measurements(num_measurements, extra_tags=frozenset()): """Make a measurement object.""" return [ make_flexural_test_measurement( - my_id=__random_my_id(), - deflection=random.random(), - extra_tags=extra_tags - ) for _ in range(num_measurements) + my_id=__random_my_id(), deflection=random.random(), extra_tags=extra_tags + ) + for _ in range(num_measurements) ] def make_flexural_test_measurement(my_id, deflection, extra_tags=frozenset()): - """ - Compute the stree, strain, and modulus. + """Compute the stree, strain, and modulus. According to https://en.wikipedia.org/wiki/Three-point_flexural_test """ @@ -53,23 +52,23 @@ def make_flexural_test_measurement(my_id, deflection, extra_tags=frozenset()): Property( name="flexural stress", value=NormalReal(stress, std=(0.01 * stress), units="MPa"), - origin=Origin.MEASURED + origin=Origin.MEASURED, ), Property( name="flexural strain", value=NormalReal(strain, std=(0.01 * strain), units=""), - origin=Origin.MEASURED + origin=Origin.MEASURED, ), Property( name="flexural modulus", value=NormalReal(modulus, std=(0.01 * modulus), units="MPa"), - origin=Origin.MEASURED + origin=Origin.MEASURED, ), Property( name="deflection", value=NominalReal(deflection, units="mm"), - origin=Origin.MEASURED - ) - ] + origin=Origin.MEASURED, + ), + ], ) return measurement diff --git a/gemd/demo/strehlow_and_cook.py b/gemd/demo/strehlow_and_cook.py index fe8f662e..aa8f5293 100644 --- a/gemd/demo/strehlow_and_cook.py +++ b/gemd/demo/strehlow_and_cook.py @@ -1,59 +1,56 @@ """Demo representing Strehlow & Cook bandgap data with data concepts.""" -from gemd.entity.util import make_instance - -from gemd.entity.object.process_spec import ProcessSpec -from gemd.entity.object.material_spec import MaterialSpec -from gemd.entity.object.measurement_spec import MeasurementSpec -from gemd.entity.template.process_template import ProcessTemplate -from gemd.entity.template.material_template import MaterialTemplate -from gemd.entity.template.measurement_template import MeasurementTemplate +from typing import Iterable +from gemd.entity.attribute.condition import Condition from gemd.entity.attribute.property import Property -from gemd.entity.template.property_template import PropertyTemplate from gemd.entity.attribute.property_and_conditions import PropertyAndConditions -from gemd.entity.attribute.condition import Condition -from gemd.entity.template.condition_template import ConditionTemplate - from gemd.entity.bounds.categorical_bounds import CategoricalBounds -from gemd.entity.value.nominal_categorical import NominalCategorical - from gemd.entity.bounds.composition_bounds import CompositionBounds +from gemd.entity.bounds.real_bounds import RealBounds +from gemd.entity.object.material_spec import MaterialSpec +from gemd.entity.object.measurement_spec import MeasurementSpec +from gemd.entity.object.process_spec import ProcessSpec +from gemd.entity.template.condition_template import ConditionTemplate +from gemd.entity.template.material_template import MaterialTemplate +from gemd.entity.template.measurement_template import MeasurementTemplate +from gemd.entity.template.process_template import ProcessTemplate +from gemd.entity.template.property_template import PropertyTemplate +from gemd.entity.util import make_instance from gemd.entity.value.empirical_formula import EmpiricalFormula - -from gemd.entity.value.normal_real import NormalReal +from gemd.entity.value.nominal_categorical import NominalCategorical from gemd.entity.value.nominal_real import NominalReal +from gemd.entity.value.normal_real import NormalReal from gemd.entity.value.uniform_real import UniformReal -from gemd.entity.bounds.real_bounds import RealBounds - from gemd.enumeration.origin import Origin - from gemd.units import convert_units -from typing import Iterable - # For now, module constant, though likely this should get promoted to a package level -DEMO_TEMPLATE_SCOPE = 'citrine-demo-sac-template' +DEMO_TEMPLATE_SCOPE = "citrine-demo-sac-template" FULL_TABLE = "strehlow_and_cook.pif" SMALL_TABLE = "strehlow_and_cook_small.pif" __all__ = [ - "import_table", "minimal_subset", "make_templates", "make_strehlow_objects", - "make_strehlow_table", "make_display_table" + "import_table", + "minimal_subset", + "make_templates", + "make_strehlow_objects", + "make_strehlow_table", + "make_display_table", ] def import_table(filename=SMALL_TABLE): """Return the deserialized JSON table.""" - from importlib.resources import files import json + from importlib.resources import files - return json.loads(files("gemd.demo").joinpath(filename).read_text(encoding='utf-8')) + return json.loads(files("gemd.demo").joinpath(filename).read_text(encoding="utf-8")) def _fingerprint(row): """Generate a string-based fingerprint to characterize row diversity.""" - return ''.join(map(lambda x: str(type(x)), row)) + return "".join(map(lambda x: str(type(x)), row)) def minimal_subset(table): @@ -72,13 +69,15 @@ def minimal_subset(table): def formula_latex(old): """Transform a formula into one with LaTeX markup.""" import re + return re.sub(r"(?<=[A-Za-z])([\d.]+)(?=[A-Za-z]|$)", r"$_{\1}$", formula_clean(old)) def formula_clean(old): """Transform a formula into a cleaner version.""" import re - return re.sub(r"(?<=[A-Za-z])1(?=[A-Za-z]|$)", '', old) + + return re.sub(r"(?<=[A-Za-z])1(?=[A-Za-z]|$)", "", old) def make_templates(template_scope: str = DEMO_TEMPLATE_SCOPE): @@ -87,107 +86,163 @@ def make_templates(template_scope: str = DEMO_TEMPLATE_SCOPE): # Attribute Templates attribute_feed = { - "Formula": [PropertyTemplate, - CompositionBounds(components=EmpiricalFormula.all_elements())], - "Crystallinity": [ConditionTemplate, - CategoricalBounds( - ['Amorphous', 'Polycrystalline', 'Single crystalline'] - )], - "Color": [PropertyTemplate, - CategoricalBounds( - ['Amber', 'Black', 'Blue', 'Bluish', 'Bronze', 'Brown', 'Brown-Black', - 'Copper-Red', 'Dark Brown', 'Dark Gray', 'Dark Green', 'Dark Red', 'Gray', - 'Light Gray', 'Ocher', 'Orange', 'Orange-Red', 'Pale Yellow', 'Red', - 'Red-Yellow', 'Violet', 'White', 'Yellow', 'Yellow-Orange', 'Yellow-White'] - )], - "Band gap": [PropertyTemplate, - RealBounds(lower_bound=0.001, upper_bound=100, default_units='eV')], - "Temperature": [ConditionTemplate, - RealBounds(lower_bound=1, upper_bound=1000, default_units='K')], - "Temperature derivative of band gap": [PropertyTemplate, - RealBounds(lower_bound=-0.01, upper_bound=0.01, - default_units='eV/K')], - "Lasing": [PropertyTemplate, - CategoricalBounds(['True', 'False'])], - "Cathodoluminescence": [PropertyTemplate, - CategoricalBounds(['True', 'False'])], - "Mechanical luminescence": [PropertyTemplate, - CategoricalBounds(['True', 'False'])], - "Photoluminescence": [PropertyTemplate, - CategoricalBounds(['True', 'False'])], - "Electroluminescence": [PropertyTemplate, - CategoricalBounds(['True', 'False'])], - "Thermoluminescence": [PropertyTemplate, - CategoricalBounds(['True', 'False'])], - "Morphology": [ConditionTemplate, - CategoricalBounds(['Thin film', 'Bulk'])], - "Electric field polarization": [ConditionTemplate, - CategoricalBounds(['Parallel to A axis', - 'Parallel to B axis', - 'Parallel to C axis', - 'Perpendicular to B axis', - 'Perpendicular to C axis'])], - "Phase": [ConditionTemplate, - CategoricalBounds(['A', 'B', 'B1', 'B2', 'Fused quartz', 'Natural diamond', - 'Rutile', 'Sapphire', 'Synthetic quartz'])], - "Crystal system": [ConditionTemplate, - CategoricalBounds(['Cubic', 'Hexagonal', 'Orthorhombic', 'Tetragonal', - 'Trigonal'])], - "Transition": [ConditionTemplate, - CategoricalBounds(['Direct', 'Excitonic', 'Indirect'])], - "Bands": [ConditionTemplate, - CategoricalBounds(['G1 to X1', 'G15 to G1', 'G15 to X1', 'G25 to G1', - 'G25 to G12', 'G25 to G15', 'G6 to G8', 'G8 to G6+', - 'L6+ to L6-'])] + "Formula": [ + PropertyTemplate, + CompositionBounds(components=EmpiricalFormula.all_elements()), + ], + "Crystallinity": [ + ConditionTemplate, + CategoricalBounds(["Amorphous", "Polycrystalline", "Single crystalline"]), + ], + "Color": [ + PropertyTemplate, + CategoricalBounds( + [ + "Amber", + "Black", + "Blue", + "Bluish", + "Bronze", + "Brown", + "Brown-Black", + "Copper-Red", + "Dark Brown", + "Dark Gray", + "Dark Green", + "Dark Red", + "Gray", + "Light Gray", + "Ocher", + "Orange", + "Orange-Red", + "Pale Yellow", + "Red", + "Red-Yellow", + "Violet", + "White", + "Yellow", + "Yellow-Orange", + "Yellow-White", + ] + ), + ], + "Band gap": [ + PropertyTemplate, + RealBounds(lower_bound=0.001, upper_bound=100, default_units="eV"), + ], + "Temperature": [ + ConditionTemplate, + RealBounds(lower_bound=1, upper_bound=1000, default_units="K"), + ], + "Temperature derivative of band gap": [ + PropertyTemplate, + RealBounds(lower_bound=-0.01, upper_bound=0.01, default_units="eV/K"), + ], + "Lasing": [PropertyTemplate, CategoricalBounds(["True", "False"])], + "Cathodoluminescence": [PropertyTemplate, CategoricalBounds(["True", "False"])], + "Mechanical luminescence": [PropertyTemplate, CategoricalBounds(["True", "False"])], + "Photoluminescence": [PropertyTemplate, CategoricalBounds(["True", "False"])], + "Electroluminescence": [PropertyTemplate, CategoricalBounds(["True", "False"])], + "Thermoluminescence": [PropertyTemplate, CategoricalBounds(["True", "False"])], + "Morphology": [ConditionTemplate, CategoricalBounds(["Thin film", "Bulk"])], + "Electric field polarization": [ + ConditionTemplate, + CategoricalBounds( + [ + "Parallel to A axis", + "Parallel to B axis", + "Parallel to C axis", + "Perpendicular to B axis", + "Perpendicular to C axis", + ] + ), + ], + "Phase": [ + ConditionTemplate, + CategoricalBounds( + [ + "A", + "B", + "B1", + "B2", + "Fused quartz", + "Natural diamond", + "Rutile", + "Sapphire", + "Synthetic quartz", + ] + ), + ], + "Crystal system": [ + ConditionTemplate, + CategoricalBounds(["Cubic", "Hexagonal", "Orthorhombic", "Tetragonal", "Trigonal"]), + ], + "Transition": [ConditionTemplate, CategoricalBounds(["Direct", "Excitonic", "Indirect"])], + "Bands": [ + ConditionTemplate, + CategoricalBounds( + [ + "G1 to X1", + "G15 to G1", + "G15 to X1", + "G25 to G1", + "G25 to G12", + "G25 to G15", + "G6 to G8", + "G8 to G6+", + "L6+ to L6-", + ] + ), + ], } - for (name, (typ, bounds)) in attribute_feed.items(): + for name, (typ, bounds) in attribute_feed.items(): assert name not in tmpl - tmpl[name] = typ(name=name, - bounds=bounds, - uids={template_scope: name}, - tags=['citrine::demo::template::attribute'] - ) + tmpl[name] = typ( + name=name, + bounds=bounds, + uids={template_scope: name}, + tags=["citrine::demo::template::attribute"], + ) # Object Templates object_feed = { - "Sample preparation": [ - ProcessTemplate, - dict() - ], - "Chemical": [ - MaterialTemplate, - {"properties": [tmpl["Formula"]]} - ], + "Sample preparation": [ProcessTemplate, dict()], + "Chemical": [MaterialTemplate, {"properties": [tmpl["Formula"]]}], "Band gap measurement": [ MeasurementTemplate, - {"properties": [tmpl["Band gap"], - tmpl["Temperature derivative of band gap"], - tmpl["Color"], - tmpl["Lasing"], - tmpl["Cathodoluminescence"], - tmpl["Mechanical luminescence"], - tmpl["Photoluminescence"], - tmpl["Electroluminescence"], - tmpl["Thermoluminescence"] - ], - "conditions": [tmpl["Temperature"], - tmpl["Crystallinity"], - tmpl["Morphology"], - tmpl["Electric field polarization"], - tmpl["Phase"], - tmpl["Crystal system"], - tmpl["Transition"], - tmpl["Bands"] - ] - } + { + "properties": [ + tmpl["Band gap"], + tmpl["Temperature derivative of band gap"], + tmpl["Color"], + tmpl["Lasing"], + tmpl["Cathodoluminescence"], + tmpl["Mechanical luminescence"], + tmpl["Photoluminescence"], + tmpl["Electroluminescence"], + tmpl["Thermoluminescence"], + ], + "conditions": [ + tmpl["Temperature"], + tmpl["Crystallinity"], + tmpl["Morphology"], + tmpl["Electric field polarization"], + tmpl["Phase"], + tmpl["Crystal system"], + tmpl["Transition"], + tmpl["Bands"], + ], + }, ], } - for (name, (typ, kw_args)) in object_feed.items(): + for name, (typ, kw_args) in object_feed.items(): assert name not in tmpl - tmpl[name] = typ(name=name, - uids={template_scope: name}, - tags=['citrine::demo::template::object'], - **kw_args) + tmpl[name] = typ( + name=name, + uids={template_scope: name}, + tags=["citrine::demo::template::object"], + **kw_args, + ) return tmpl @@ -200,54 +255,51 @@ def make_strehlow_objects(table: Iterable = None, template_scope: str = DEMO_TEM table = import_table() # Specs - msr_spec = MeasurementSpec(name='Band gap', - template=tmpl["Band gap measurement"] - ) + msr_spec = MeasurementSpec(name="Band gap", template=tmpl["Band gap measurement"]) def real_mapper(prop): """Mapping methods for RealBounds.""" - if 'uncertainty' in prop['scalars'][0]: - if prop['units'] == 'eV': # Arbitrarily convert to attojoules - mean = convert_units(value=float(prop['scalars'][0]['value']), - starting_unit=prop['units'], - final_unit='aJ' - ) - std = convert_units(value=float(prop['scalars'][0]['value']), - starting_unit=prop['units'], - final_unit='aJ' - ) - val = NormalReal(mean=mean, - units='aJ', - std=std - ) + if "uncertainty" in prop["scalars"][0]: + if prop["units"] == "eV": # Arbitrarily convert to attojoules + mean = convert_units( + value=float(prop["scalars"][0]["value"]), + starting_unit=prop["units"], + final_unit="aJ", + ) + std = convert_units( + value=float(prop["scalars"][0]["value"]), + starting_unit=prop["units"], + final_unit="aJ", + ) + val = NormalReal(mean=mean, units="aJ", std=std) else: - val = NormalReal(mean=float(prop['scalars'][0]['value']), - units=prop['units'], - std=float(prop['scalars'][0]['uncertainty']) - ) + val = NormalReal( + mean=float(prop["scalars"][0]["value"]), + units=prop["units"], + std=float(prop["scalars"][0]["uncertainty"]), + ) else: - val = NominalReal(nominal=float(prop['scalars'][0]['value']), - units=prop['units'] - ) + val = NominalReal(nominal=float(prop["scalars"][0]["value"]), units=prop["units"]) return val content_map = { RealBounds: real_mapper, - CategoricalBounds: lambda prop: NominalCategorical(category=prop['scalars'][0]['value']), - type(None): lambda bnd: 'Label' + CategoricalBounds: lambda prop: NominalCategorical(category=prop["scalars"][0]["value"]), + type(None): lambda bnd: "Label", } datapoints = [] compounds = dict() for row in table: - formula = formula_clean(row['chemicalFormula']) + formula = formula_clean(row["chemicalFormula"]) if formula not in compounds: compounds[formula] = MaterialSpec( name=formula_latex(formula), template=tmpl["Chemical"], - process=ProcessSpec(name="Sample preparation", - template=tmpl["Sample preparation"] - )) + process=ProcessSpec( + name="Sample preparation", template=tmpl["Sample preparation"] + ), + ) spec = compounds[formula] run = make_instance(spec) datapoints.append(run) @@ -255,67 +307,67 @@ def real_mapper(prop): if not spec.properties: spec.properties.append( PropertyAndConditions( - property=Property(name=spec.template.properties[0][0].name, - value=EmpiricalFormula(formula=formula), - template=spec.template.properties[0][0]) - )) + property=Property( + name=spec.template.properties[0][0].name, + value=EmpiricalFormula(formula=formula), + template=spec.template.properties[0][0], + ) + ) + ) msr = make_instance(msr_spec) msr.material = run # 2 categories in the PIF need to be split to avoid repeat Attribute Templates in a Run - name_map = { - 'Phase': 'Crystal system', - 'Transition': 'Bands' - } - origin_map = { - 'EXPERIMENTAL': Origin.MEASURED, - 'COMPUTATIONAL': Origin.COMPUTED - } + name_map = {"Phase": "Crystal system", "Transition": "Bands"} + origin_map = {"EXPERIMENTAL": Origin.MEASURED, "COMPUTATIONAL": Origin.COMPUTED} seen = set() # Some conditions come in from multiple properties on the same object - for prop in row['properties']: - origin = origin_map.get(prop.get('dataType', None), Origin.UNKNOWN) - if 'method' in prop: - method = 'Method: ' + prop['method']['name'] + for prop in row["properties"]: + origin = origin_map.get(prop.get("dataType", None), Origin.UNKNOWN) + if "method" in prop: + method = "Method: " + prop["method"]["name"] else: - method = 'Method: unreported' - for attr in [prop] + prop.get('conditions', []): - if attr['name'] in seen: + method = "Method: unreported" + for attr in [prop] + prop.get("conditions", []): + if attr["name"] in seen: # Early return if it's a repeat continue - seen.add(attr['name']) + seen.add(attr["name"]) - template = tmpl[attr['name']] + template = tmpl[attr["name"]] # Figure out if we need to split this column - if attr['name'] in name_map: - value = attr['scalars'][0]['value'] + if attr["name"] in name_map: + value = attr["scalars"][0]["value"] if value not in template.bounds.categories: - template = tmpl[name_map[attr['name']]] + template = tmpl[name_map[attr["name"]]] # Move into GEMD structure if isinstance(template, PropertyTemplate): msr.properties.append( - Property(name=template.name, - template=template, - value=content_map[type(template.bounds)](attr), - origin=origin, - notes=method - )) + Property( + name=template.name, + template=template, + value=content_map[type(template.bounds)](attr), + origin=origin, + notes=method, + ) + ) elif isinstance(template, ConditionTemplate): msr.conditions.append( - Condition(name=template.name, - template=template, - value=content_map[type(template.bounds)](attr), - origin=origin, - notes=method - )) + Condition( + name=template.name, + template=template, + value=content_map[type(template.bounds)](attr), + origin=origin, + notes=method, + ) + ) return datapoints def make_strehlow_table(compounds): - """ - Headers and content for the output of make_strehlow_objects. + """Headers and content for the output of make_strehlow_objects. Note that this is supposed to be mimicking the transformation of a set of Material Histories into a Training Table, and as such we are missing the column definition component of the query @@ -333,44 +385,54 @@ def make_strehlow_table(compounds): properties = compounds[0].measurements[0].spec.template.properties conditions = compounds[0].measurements[0].spec.template.conditions parameters = compounds[0].measurements[0].spec.template.parameters - for attr in (properties + conditions + parameters): + for attr in properties + conditions + parameters: tmpl[attr[0].name] = attr[0] # Consider how to specify relevant data pathing here - output = {'headers': [], 'content': []} + output = {"headers": [], "content": []} # "Chemical" is supposed to be the unifying characterization of all the root elements of the # Material Histories, but that can't be the spec name because the spec is Compound specific -- # that's where the chemical is defined - output['headers'].append( - {'name': [chem_mat_tmpl.name, - "Display name" # It would be good to derive this from the structure somehow - ], - 'primitive': True - } + output["headers"].append( + { + "name": [ + chem_mat_tmpl.name, + "Display name", # It would be good to derive this from the structure somehow + ], + "primitive": True, + } ) - output['headers'].append( - {'name': [chem_mat_tmpl.name, - chem_tmpl.name - ], - 'primitive': False, - 'bounds': CompositionBounds() - } + output["headers"].append( + { + "name": [chem_mat_tmpl.name, chem_tmpl.name], + "primitive": False, + "bounds": CompositionBounds(), + } ) - terms = ["Band gap", "Temperature derivative of band gap", "Temperature", "Color", - "Lasing", "Cathodoluminescence", "Mechanical luminescence", "Photoluminescence", - "Electroluminescence", "Thermoluminescence", "Transition", "Bands", - "Electric field polarization", "Crystallinity", "Morphology", "Phase", - 'Crystal system'] + terms = [ + "Band gap", + "Temperature derivative of band gap", + "Temperature", + "Color", + "Lasing", + "Cathodoluminescence", + "Mechanical luminescence", + "Photoluminescence", + "Electroluminescence", + "Thermoluminescence", + "Transition", + "Bands", + "Electric field polarization", + "Crystallinity", + "Morphology", + "Phase", + "Crystal system", + ] for term in terms: - output['headers'].append( - {'name': [chem_mat_tmpl.name, - term - ], - 'primitive': False, - 'bounds': tmpl[term].bounds - } + output["headers"].append( + {"name": [chem_mat_tmpl.name, term], "primitive": False, "bounds": tmpl[term].bounds} ) for comp in compounds: @@ -382,21 +444,24 @@ def make_strehlow_table(compounds): row.append(None) for term in terms: - x = list(filter(lambda y: y.name == term, - comp.measurements[0].properties + comp.measurements[0].conditions)) + x = list( + filter( + lambda y: y.name == term, + comp.measurements[0].properties + comp.measurements[0].conditions, + ) + ) if x: row.append(x[0].value) else: row.append(None) - output['content'].append(row) + output["content"].append(row) return output def make_display_table(structured): - """ - Generate a Display Table from a passed Structured Table. + """Generate a Display Table from a passed Structured Table. This routine takes in a prototyped Structured Table and returns a prototyped Display Table (CSV of scalar values) based upon some standard assumptions about how it should be displayed. @@ -407,22 +472,22 @@ def make_display_table(structured): """ table = [[]] header_map = { - RealBounds: lambda bnd: 'Mean({})'.format(bnd.default_units), - CategoricalBounds: lambda bnd: 'Category', - CompositionBounds: lambda bnd: 'Formula', - type(None): lambda bnd: 'Label' + RealBounds: lambda bnd: f"Mean({bnd.default_units})", + CategoricalBounds: lambda bnd: "Category", + CompositionBounds: lambda bnd: "Formula", + type(None): lambda bnd: "Label", } - for column in structured['headers']: - bounds = column.get('bounds', None) - table[0].append('~'.join(column['name'] + [header_map[type(bounds)](bounds)])) + for column in structured["headers"]: + bounds = column.get("bounds", None) + table[0].append("~".join(column["name"] + [header_map[type(bounds)](bounds)])) - i_bandgap = list(filter(lambda i: 'Band gap' in table[0][i], range(len(table[0])))) + i_bandgap = list(filter(lambda i: "Band gap" in table[0][i], range(len(table[0])))) assert i_bandgap, "Band gap was not found" i_bandgap = i_bandgap[0] - column = structured['headers'][i_bandgap] + column = structured["headers"][i_bandgap] table[0].insert( i_bandgap + 1, - '~'.join(column['name'] + ['Std Deviation({})'.format(column['bounds'].default_units)]) + "~".join(column["name"] + [f"Std Deviation({column['bounds'].default_units})"]), ) content_map = { @@ -432,15 +497,15 @@ def make_display_table(structured): NominalCategorical: lambda x: x.category, EmpiricalFormula: lambda x: x.formula, str: lambda x: x, - type(None): lambda x: '' + type(None): lambda x: "", } uncert_map = { - NominalReal: lambda x: '', + NominalReal: lambda x: "", NormalReal: lambda x: x.std, UniformReal: lambda x: 0.29 * (x.upper_bound - x.lower_bound), - type(None): lambda x: '' + type(None): lambda x: "", } - for row in structured['content']: + for row in structured["content"]: table.append([]) for element in row: table[-1].append(content_map[type(element)](element)) @@ -459,11 +524,12 @@ def make_display_table(structured): Takes one optional argument - the name to use for the template scope. Defaults to DEMO_TEMPLATE_SCOPE if none provided. """ - import os.path import json as json_builtin - import gemd.json as gemd_json + import os.path import sys + import gemd.json as gemd_json + args = sys.argv[1:] if len(args) >= 1: template_scope = args[0] @@ -472,12 +538,12 @@ def make_display_table(structured): imported_table = import_table(FULL_TABLE) full_compounds = make_strehlow_objects(imported_table, template_scope) full_table = make_strehlow_table(full_compounds) - small_table = minimal_subset(full_table['content']) + small_table = minimal_subset(full_table["content"]) todo = set(_fingerprint(x) for x in small_table) - print('Total number of prototypes: {}'.format(len(small_table))) + print(f"Total number of prototypes: {len(small_table)}") reduced_list = [] - for (raw, clean) in zip(imported_table, full_table['content']): + for raw, clean in zip(imported_table, full_table["content"]): fp = _fingerprint(clean) if fp in todo: reduced_list.append(raw) @@ -485,7 +551,7 @@ def make_display_table(structured): if not todo: break - with open(os.path.join(os.path.dirname(__file__), SMALL_TABLE), 'w') as f: + with open(os.path.join(os.path.dirname(__file__), SMALL_TABLE), "w") as f: json_builtin.dump(reduced_list, f, indent=2) print("\n\nJSON -- Training table") @@ -494,4 +560,4 @@ def make_display_table(structured): print("\n\nCSV -- Display table") display = make_display_table(full_table) for row_ in display: - print(','.join(map(lambda x: str(x), row_))) + print(",".join(map(lambda x: str(x), row_))) diff --git a/gemd/demo/table_example.py b/gemd/demo/table_example.py index 48947cd1..1bc5d16c 100644 --- a/gemd/demo/table_example.py +++ b/gemd/demo/table_example.py @@ -1,4 +1,5 @@ """Ingest a table.""" + from gemd.entity.attribute.condition import Condition from gemd.entity.attribute.property import Property from gemd.entity.object import MeasurementRun @@ -14,12 +15,14 @@ def ingest_table(material_run, table): exp = MeasurementRun("Material Run") for prop_name in known_properties: if prop_name in row: - exp.properties.append(Property(name=prop_name, - value=NominalReal(row[prop_name], ''))) + exp.properties.append( + Property(name=prop_name, value=NominalReal(row[prop_name], "")) + ) for cond_name in known_conditions: if cond_name in row: - exp.conditions.append(Condition(name=cond_name, - value=NominalReal(row[cond_name], ''))) + exp.conditions.append( + Condition(name=cond_name, value=NominalReal(row[cond_name], "")) + ) exp.material = material_run return material_run diff --git a/gemd/entity/__init__.py b/gemd/entity/__init__.py index b082d266..bbbf55e4 100644 --- a/gemd/entity/__init__.py +++ b/gemd/entity/__init__.py @@ -1,29 +1,83 @@ # flake8: noqa from .attribute import Condition, Parameter, Property, PropertyAndConditions -from .bounds import CategoricalBounds, CompositionBounds, IntegerBounds, \ - MolecularStructureBounds, RealBounds -from .object import MaterialRun, MeasurementRun, ProcessRun, IngredientRun, \ - MaterialSpec, MeasurementSpec, ProcessSpec, IngredientSpec +from .bounds import ( + CategoricalBounds, + CompositionBounds, + IntegerBounds, + MolecularStructureBounds, + RealBounds, +) +from .object import ( + MaterialRun, + MeasurementRun, + ProcessRun, + IngredientRun, + MaterialSpec, + MeasurementSpec, + ProcessSpec, + IngredientSpec, +) from .source import PerformedSource -from .template import PropertyTemplate, ConditionTemplate, ParameterTemplate, \ - MaterialTemplate, MeasurementTemplate, ProcessTemplate -from .value import NominalReal, NormalReal, UniformReal, NominalInteger, \ - UniformInteger, DiscreteCategorical, NominalCategorical, \ - EmpiricalFormula, NominalComposition, InChI, Smiles +from .template import ( + PropertyTemplate, + ConditionTemplate, + ParameterTemplate, + MaterialTemplate, + MeasurementTemplate, + ProcessTemplate, +) +from .value import ( + NominalReal, + NormalReal, + UniformReal, + NominalInteger, + UniformInteger, + DiscreteCategorical, + NominalCategorical, + EmpiricalFormula, + NominalComposition, + InChI, + Smiles, +) from .link_by_uid import LinkByUID from .file_link import FileLink -__all__ = ["Condition", "Parameter", "Property", "PropertyAndConditions", - "CategoricalBounds", "CompositionBounds", "IntegerBounds", - "MolecularStructureBounds", "RealBounds", - "MaterialRun", "MeasurementRun", "ProcessRun", "IngredientRun", - "MaterialSpec", "MeasurementSpec", "ProcessSpec", "IngredientSpec", - "PerformedSource", - "PropertyTemplate", "ConditionTemplate", "ParameterTemplate", - "MaterialTemplate", "MeasurementTemplate", "ProcessTemplate", - "NominalReal", "NormalReal", "UniformReal", "NominalInteger", - "UniformInteger", "DiscreteCategorical", "NominalCategorical", - "EmpiricalFormula", "NominalComposition", "InChI", "Smiles", - "LinkByUID", - "FileLink" - ] +__all__ = [ + "Condition", + "Parameter", + "Property", + "PropertyAndConditions", + "CategoricalBounds", + "CompositionBounds", + "IntegerBounds", + "MolecularStructureBounds", + "RealBounds", + "MaterialRun", + "MeasurementRun", + "ProcessRun", + "IngredientRun", + "MaterialSpec", + "MeasurementSpec", + "ProcessSpec", + "IngredientSpec", + "PerformedSource", + "PropertyTemplate", + "ConditionTemplate", + "ParameterTemplate", + "MaterialTemplate", + "MeasurementTemplate", + "ProcessTemplate", + "NominalReal", + "NormalReal", + "UniformReal", + "NominalInteger", + "UniformInteger", + "DiscreteCategorical", + "NominalCategorical", + "EmpiricalFormula", + "NominalComposition", + "InChI", + "Smiles", + "LinkByUID", + "FileLink", +] diff --git a/gemd/entity/attribute/__init__.py b/gemd/entity/attribute/__init__.py index 6a0d8d3a..8c088381 100644 --- a/gemd/entity/attribute/__init__.py +++ b/gemd/entity/attribute/__init__.py @@ -1,4 +1,5 @@ """Attribute objects.""" + # flake8: noqa from .condition import Condition from .parameter import Parameter diff --git a/gemd/entity/attribute/base_attribute.py b/gemd/entity/attribute/base_attribute.py index c38aa026..0b43652d 100644 --- a/gemd/entity/attribute/base_attribute.py +++ b/gemd/entity/attribute/base_attribute.py @@ -1,19 +1,18 @@ +from abc import abstractmethod +from typing import Iterable, List, Optional, Type, Union + +from gemd.entity.bounds_validation import WarningLevel, get_validation_level from gemd.entity.dict_serializable import DictSerializable, logger +from gemd.entity.file_link import FileLink +from gemd.entity.link_by_uid import LinkByUID +from gemd.entity.setters import validate_list from gemd.entity.template.attribute_template import AttributeTemplate from gemd.entity.value.base_value import BaseValue from gemd.enumeration.origin import Origin -from gemd.entity.setters import validate_list -from gemd.entity.file_link import FileLink -from gemd.entity.link_by_uid import LinkByUID -from gemd.entity.bounds_validation import get_validation_level, WarningLevel - -from typing import Optional, Union, Iterable, List, Type -from abc import abstractmethod class BaseAttribute(DictSerializable): - """ - Base class for all attributes, which include property, condition, parameter, and metadata. + """Base class for all attributes, which include property, condition, parameter, and metadata. Parameters ---------- @@ -34,14 +33,16 @@ class BaseAttribute(DictSerializable): """ - def __init__(self, - name: str, - *, - template: Union[AttributeTemplate, LinkByUID, None] = None, - origin: Union[Origin, str] = Origin.UNKNOWN, - value: BaseValue = None, - notes: str = None, - file_links: Optional[Union[Iterable[FileLink], FileLink]] = None): + def __init__( + self, + name: str, + *, + template: Union[AttributeTemplate, LinkByUID, None] = None, + origin: Union[Origin, str] = Origin.UNKNOWN, + value: BaseValue = None, + notes: str = None, + file_links: Optional[Union[Iterable[FileLink], FileLink]] = None, + ): self.name = name self.notes = notes @@ -98,8 +99,7 @@ def template(self, template: Optional[Union[AttributeTemplate, LinkByUID]]): self._check(template, self.value) self._template = template else: - raise TypeError("template must be a BaseAttributeTemplate or " - "LinkByUID: {}".format(template)) + raise TypeError(f"template must be a BaseAttributeTemplate or LinkByUID: {template}") @staticmethod @abstractmethod diff --git a/gemd/entity/attribute/condition.py b/gemd/entity/attribute/condition.py index aff09882..b714373d 100644 --- a/gemd/entity/attribute/condition.py +++ b/gemd/entity/attribute/condition.py @@ -1,14 +1,13 @@ +from typing import Type + from gemd.entity.attribute.base_attribute import BaseAttribute from gemd.entity.template import ConditionTemplate -from typing import Type - __all__ = ["Condition"] class Condition(BaseAttribute, typ="condition"): - """ - Condition of a property, process, or measurement. + """Condition of a property, process, or measurement. Conditions are environmental variables (typically measured) that may affect a process or measurement: e.g., temperature, pressure. diff --git a/gemd/entity/attribute/parameter.py b/gemd/entity/attribute/parameter.py index d8bf136c..fea5dd61 100644 --- a/gemd/entity/attribute/parameter.py +++ b/gemd/entity/attribute/parameter.py @@ -1,14 +1,13 @@ +from typing import Type + from gemd.entity.attribute.base_attribute import BaseAttribute from gemd.entity.template import ParameterTemplate -from typing import Type - __all__ = ["Parameter"] class Parameter(BaseAttribute, typ="parameter"): - """ - Parameter of a process or measurement. + """Parameter of a process or measurement. Parameters are the non-environmental variables (typically specified and controlled) that may affect a process or measurement: e.g. oven dial temperature for a kiln firing, magnification diff --git a/gemd/entity/attribute/property.py b/gemd/entity/attribute/property.py index bdc0277c..fab30a5f 100644 --- a/gemd/entity/attribute/property.py +++ b/gemd/entity/attribute/property.py @@ -1,14 +1,13 @@ +from typing import Type + from gemd.entity.attribute.base_attribute import BaseAttribute from gemd.entity.template import PropertyTemplate -from typing import Type - __all__ = ["Property"] class Property(BaseAttribute, typ="property"): - """ - Property of a material, measured in a MeasurementRun or specified in a MaterialSpec. + """Property of a material, measured in a MeasurementRun or specified in a MaterialSpec. Properties are characteristics of a material that could be measured, e.g. chemical composition, density, yield strength. diff --git a/gemd/entity/attribute/property_and_conditions.py b/gemd/entity/attribute/property_and_conditions.py index 040f77da..3fb96b95 100644 --- a/gemd/entity/attribute/property_and_conditions.py +++ b/gemd/entity/attribute/property_and_conditions.py @@ -1,19 +1,18 @@ +from typing import Iterable, List, Optional, Union + from gemd.entity.attribute.condition import Condition from gemd.entity.attribute.property import Property -from gemd.entity.template.property_template import PropertyTemplate -from gemd.entity.value.base_value import BaseValue from gemd.entity.dict_serializable import DictSerializable from gemd.entity.link_by_uid import LinkByUID from gemd.entity.setters import validate_list - -from typing import Optional, Union, Iterable, List +from gemd.entity.template.property_template import PropertyTemplate +from gemd.entity.value.base_value import BaseValue __all__ = ["PropertyAndConditions"] class PropertyAndConditions(DictSerializable, typ="property_and_conditions"): - """ - A property and the conditions under which that property was determined. + """A property and the conditions under which that property was determined. This attribute is only relevant for material specs. @@ -26,9 +25,9 @@ class PropertyAndConditions(DictSerializable, typ="property_and_conditions"): """ - def __init__(self, - property: Property = None, - conditions: Union[Iterable[Condition], Condition] = None): + def __init__( + self, property: Property = None, conditions: Union[Iterable[Condition], Condition] = None + ): self._property = None self.property = property self._conditions = None diff --git a/gemd/entity/base_entity.py b/gemd/entity/base_entity.py index 7b70d2cb..870aafbe 100644 --- a/gemd/entity/base_entity.py +++ b/gemd/entity/base_entity.py @@ -1,9 +1,10 @@ """Base class for all entities.""" -from typing import TypeVar, Optional, Union, Iterable, List, Set, FrozenSet, MutableMapping, Dict +from typing import Dict, FrozenSet, Iterable, List, MutableMapping, Optional, Set, TypeVar, Union + +from gemd.entity.case_insensitive_dict import CaseInsensitiveDict from gemd.entity.dict_serializable import DictSerializable from gemd.entity.has_dependencies import HasDependencies -from gemd.entity.case_insensitive_dict import CaseInsensitiveDict from gemd.entity.setters import validate_list __all__ = ["BaseEntity"] @@ -11,6 +12,11 @@ LinkByUIDType = TypeVar("LinkByUIDType", bound="LinkByUID") # noqa: F821 +def _is_non_str_iterable(value) -> bool: + """Check whether a value is an iterable that is not a string.""" + return isinstance(value, Iterable) and not isinstance(value, str) + + class BaseEntity(DictSerializable): """Base class for any entity, which includes objects and templates.""" @@ -38,8 +44,7 @@ def tags(self, tags: Iterable[str]): @property def uids(self) -> Dict[str, str]: - """ - A collection of unique IDs. + """A collection of unique IDs. Requirements for and the value of unique IDs are discussed `here LinkByUIDType: - """ - Generate a ~gemd.entity.link_by_uid.LinkByUID for this object. + def to_link( + self, scope: Optional[str] = None, *, allow_fallback: bool = False + ) -> LinkByUIDType: + """Generate a ~gemd.entity.link_by_uid.LinkByUID for this object. Parameters ---------- @@ -93,6 +94,7 @@ def to_link(self, """ from gemd.entity.link_by_uid import LinkByUID + if len(self.uids) == 0: raise ValueError(f"{type(self)} {self.name} does not have any uids.") @@ -111,20 +113,17 @@ def all_dependencies(self) -> Set[Union[BaseEntityType, LinkByUIDType]]: queue = [type(self)] while queue: cls = queue.pop() - if issubclass(cls, HasDependencies) and \ - "_local_dependencies" not in cls.__abstractmethods__: - result |= cls._local_dependencies(self) - queue.extend(cls.__bases__) + if issubclass(cls, HasDependencies): + if "_local_dependencies" not in cls.__abstractmethods__: + result |= cls._local_dependencies(self) + queue.extend(cls.__bases__) return result @staticmethod - def _cached_equals(this: "BaseEntity", - that: "BaseEntity", - *, - cache: Dict[FrozenSet, Optional[bool]] = None - ) -> Optional[bool]: - """ - Compute and stash whether two Base Entities are equal in a recursive sense. + def _cached_equals( + this: "BaseEntity", that: "BaseEntity", *, cache: Dict[FrozenSet, Optional[bool]] = None + ) -> Optional[bool]: + """Compute and stash whether two Base Entities are equal in a recursive sense. The cache uses ternary logic to communicate state. True or False indicate a completed evaluation. If the cache contains None, this indicates that we have not yet completed @@ -158,8 +157,7 @@ def _cached_equals(this: "BaseEntity", if BaseEntity._cached_equals(this_value, that_value, cache=cache) is False: cache[cache_key] = False # Mark as failed return False - elif isinstance(this_value, Iterable) and isinstance(that_value, Iterable) \ - and not isinstance(this_value, str) and not isinstance(that_value, str): + elif _is_non_str_iterable(this_value) and _is_non_str_iterable(that_value): # Necessary to maintain context for recursive parts of the structure this_list = list(this_value) that_list = list(that_value) diff --git a/gemd/entity/bounds/__init__.py b/gemd/entity/bounds/__init__.py index 1efd8ab8..b6d7188a 100644 --- a/gemd/entity/bounds/__init__.py +++ b/gemd/entity/bounds/__init__.py @@ -1,4 +1,5 @@ """Bounds on a value.""" + # flake8: noqa from .categorical_bounds import CategoricalBounds from .composition_bounds import CompositionBounds @@ -6,5 +7,10 @@ from .molecular_structure_bounds import MolecularStructureBounds from .real_bounds import RealBounds -__all__ = ["RealBounds", "IntegerBounds", "CategoricalBounds", "CompositionBounds", - "MolecularStructureBounds"] +__all__ = [ + "RealBounds", + "IntegerBounds", + "CategoricalBounds", + "CompositionBounds", + "MolecularStructureBounds", +] diff --git a/gemd/entity/bounds/base_bounds.py b/gemd/entity/bounds/base_bounds.py index 93281264..f236b659 100644 --- a/gemd/entity/bounds/base_bounds.py +++ b/gemd/entity/bounds/base_bounds.py @@ -1,4 +1,5 @@ """Base class for all bounds.""" + from abc import abstractmethod from typing import TypeVar, Union @@ -14,8 +15,7 @@ class BaseBounds(DictSerializable): @abstractmethod def contains(self, bounds: Union[BaseBoundsType, BaseValueType]): - """ - Check if another bounds is contained within this bounds. + """Check if another bounds is contained within this bounds. Parameters ---------- @@ -38,12 +38,11 @@ def contains(self, bounds: Union[BaseBoundsType, BaseValueType]): bounds = bounds._to_bounds() if isinstance(bounds, BaseBounds): return True - raise TypeError('{} is not a Bounds object'.format(bounds)) + raise TypeError(f"{bounds} is not a Bounds object") @abstractmethod def union(self, *others: Union[BaseBoundsType, BaseValueType]) -> BaseBoundsType: - """ - Return the union of this bounds and other bounds. + """Return the union of this bounds and other bounds. The others list must also be the same class (e.g., categorical, real...). @@ -63,8 +62,7 @@ def union(self, *others: Union[BaseBoundsType, BaseValueType]) -> BaseBoundsType @abstractmethod def update(self, *others: Union[BaseBoundsType, BaseValueType]): - """ - Update this bounds to include other bounds. + """Update this bounds to include other bounds. The others list must also be the same class (e.g., categorical, real...). diff --git a/gemd/entity/bounds/categorical_bounds.py b/gemd/entity/bounds/categorical_bounds.py index 20be8acf..ada26d82 100644 --- a/gemd/entity/bounds/categorical_bounds.py +++ b/gemd/entity/bounds/categorical_bounds.py @@ -1,4 +1,4 @@ -from typing import TypeVar, Any, Union, Set, Optional, Iterable, Dict +from typing import Any, Dict, Iterable, Optional, Set, TypeVar, Union from gemd.entity.bounds.base_bounds import BaseBounds from gemd.entity.util import array_like @@ -10,8 +10,7 @@ class CategoricalBounds(BaseBounds, typ="categorical_bounds"): - """ - Categorical bounds, parameterized by a set of string-valued category labels. + """Categorical bounds, parameterized by a set of string-valued category labels. Parameters ---------- @@ -44,8 +43,7 @@ def categories(self, categories: Optional[Iterable[str]]): raise ValueError("All the categories must be strings") def contains(self, bounds: Union[BaseBounds, BaseValueType]) -> bool: - """ - Check if another bounds object or value object is contained by this bounds. + """Check if another bounds object or value object is contained by this bounds. The other object must also be Categorical and its allowed categories must be a subset of this bounds's allowed categories. @@ -75,11 +73,10 @@ def contains(self, bounds: Union[BaseBounds, BaseValueType]) -> bool: return bounds.categories.issubset(self.categories) - def union(self, - *others: Union[CategoricalBoundsType, CategoricalValueType] - ) -> CategoricalBoundsType: - """ - Return the union of this bounds and other bounds. + def union( + self, *others: Union[CategoricalBoundsType, CategoricalValueType] + ) -> CategoricalBoundsType: + """Return the union of this bounds and other bounds. The others list must also be Categorical Bounds or Values. @@ -99,11 +96,14 @@ def union(self, from gemd.entity.value.categorical_value import CategoricalValue if any(not isinstance(x, (CategoricalBounds, CategoricalValue)) for x in others): - misses = {type(x).__name__ - for x in others - if not isinstance(x, (CategoricalBounds, CategoricalValue))} - raise TypeError(f"union requires consistent typing; " - f"expected categorical, found {misses}") + misses = { + type(x).__name__ + for x in others + if not isinstance(x, (CategoricalBounds, CategoricalValue)) + } + raise TypeError( + f"union requires consistent typing; expected categorical, found {misses}" + ) result = self.categories.copy() for bounds in others: if isinstance(bounds, CategoricalValue): @@ -112,8 +112,7 @@ def union(self, return CategoricalBounds(result) def update(self, *others: Union[CategoricalBoundsType, CategoricalValueType]): - """ - Update this bounds to include other bounds. + """Update this bounds to include other bounds. The others list must also be Categorical Bounds or Values. @@ -128,8 +127,7 @@ def update(self, *others: Union[CategoricalBoundsType, CategoricalValueType]): self.categories = self.union(*others).categories def as_dict(self) -> Dict[str, Any]: - """ - Convert bounds to a dictionary. + """Convert bounds to a dictionary. Returns ------- diff --git a/gemd/entity/bounds/composition_bounds.py b/gemd/entity/bounds/composition_bounds.py index 3b2ff8fd..70601277 100644 --- a/gemd/entity/bounds/composition_bounds.py +++ b/gemd/entity/bounds/composition_bounds.py @@ -1,9 +1,10 @@ """Bounds a composition to have a specified set of components.""" + +from typing import Iterable, Set, TypeVar, Union + from gemd.entity.bounds.base_bounds import BaseBounds from gemd.entity.util import array_like -from typing import TypeVar, Union, Set, Iterable - __all__ = ["CompositionBounds"] CompositionBoundsType = TypeVar("CompositionBoundsType", bound="CompositionBounds") BaseValueType = TypeVar("BaseValueType", bound="BaseValue") # noqa: F821 @@ -11,8 +12,7 @@ class CompositionBounds(BaseBounds, typ="composition_bounds"): - """ - Composition bounds, parameterized by a set of string-valued category labels. + """Composition bounds, parameterized by a set of string-valued category labels. Parameters ---------- @@ -40,14 +40,13 @@ def components(self, value: Iterable[str]): elif isinstance(value, set): self._components = value else: - raise ValueError("Components must be a list, tuple, or set: {}".format(value)) + raise ValueError(f"Components must be a list, tuple, or set: {value}") if not all(isinstance(x, str) for x in self.components): raise ValueError("All the components must be strings") def contains(self, bounds: Union[BaseBounds, BaseValueType]) -> bool: - """ - Check if another bounds or value object is contained by this bounds. + """Check if another bounds or value object is contained by this bounds. The other object must also be a Composition and its components must be a subset of this bounds's set of allowed components. @@ -76,11 +75,10 @@ def contains(self, bounds: Union[BaseBounds, BaseValueType]) -> bool: return bounds.components.issubset(self.components) - def union(self, - *others: Union[CompositionBoundsType, CompositionValueType] - ) -> CompositionBoundsType: - """ - Return the union of this bounds and other bounds. + def union( + self, *others: Union[CompositionBoundsType, CompositionValueType] + ) -> CompositionBoundsType: + """Return the union of this bounds and other bounds. The others list must also be Composition Bounds or Values. @@ -100,11 +98,14 @@ def union(self, from gemd.entity.value.composition_value import CompositionValue if any(not isinstance(x, (CompositionBounds, CompositionValue)) for x in others): - misses = {type(x).__name__ - for x in others - if not isinstance(x, (CompositionBounds, CompositionValue))} - raise TypeError(f"union requires consistent typing; " - f"expected composition, found {misses}") + misses = { + type(x).__name__ + for x in others + if not isinstance(x, (CompositionBounds, CompositionValue)) + } + raise TypeError( + f"union requires consistent typing; expected composition, found {misses}" + ) result = self.components.copy() for bounds in others: if isinstance(bounds, CompositionValue): @@ -113,8 +114,7 @@ def union(self, return CompositionBounds(result) def update(self, *others: Union[CompositionBoundsType, CompositionValueType]): - """ - Update this bounds to include other bounds. + """Update this bounds to include other bounds. The others list must also be Composition Bounds or Values. @@ -129,8 +129,7 @@ def update(self, *others: Union[CompositionBoundsType, CompositionValueType]): self.components = self.union(*others).components def as_dict(self): - """ - Convert bounds to a dictionary. + """Convert bounds to a dictionary. Returns ------- diff --git a/gemd/entity/bounds/integer_bounds.py b/gemd/entity/bounds/integer_bounds.py index bd27b991..5c7acd2d 100644 --- a/gemd/entity/bounds/integer_bounds.py +++ b/gemd/entity/bounds/integer_bounds.py @@ -1,4 +1,5 @@ """Bounds an integer to be between two values.""" + from math import isfinite from typing import TypeVar, Union @@ -11,8 +12,7 @@ class IntegerBounds(BaseBounds, typ="integer_bounds"): - """ - Bounded subset of the integers, parameterized by a lower and upper bound. + """Bounded subset of the integers, parameterized by a lower and upper bound. Parameters ---------- @@ -20,6 +20,7 @@ class IntegerBounds(BaseBounds, typ="integer_bounds"): The lower endpoint (inclusive) of the permitted range. upper_bound: int The upper endpoint (inclusive) of the permitted range. + """ def __init__(self, lower_bound: int, upper_bound: int): @@ -40,8 +41,10 @@ def lower_bound(self, value: int): if value is None or not isfinite(value) or int(value) != float(value): raise ValueError(f"Lower bound must be given, integer and finite: {value}") if self.upper_bound is not None and value > self.upper_bound: - raise ValueError(f"Upper bound ({self.upper_bound}) must be " - f"greater than or equal to lower bound ({value})") + raise ValueError( + f"Upper bound ({self.upper_bound}) must be " + f"greater than or equal to lower bound ({value})" + ) self._lower_bound = int(value) @property @@ -55,13 +58,14 @@ def upper_bound(self, value: int): if value is None or not isfinite(value) or int(value) != float(value): raise ValueError(f"Upper bound must be given, integer and finite: {value}") if self.lower_bound is not None and value < self.lower_bound: - raise ValueError(f"Upper bound ({value}) must be " - f"greater than or equal to lower bound ({self.lower_bound})") + raise ValueError( + f"Upper bound ({value}) must be " + f"greater than or equal to lower bound ({self.lower_bound})" + ) self._upper_bound = int(value) def contains(self, bounds: Union[BaseBounds, BaseValueType]) -> bool: - """ - Check if another bounds or value object is a subset of this range. + """Check if another bounds or value object is a subset of this range. The other object must also be an Integer and its lower and upper bound must *both* be within the range of this bounds object. @@ -90,11 +94,8 @@ def contains(self, bounds: Union[BaseBounds, BaseValueType]) -> bool: return bounds.lower_bound >= self.lower_bound and bounds.upper_bound <= self.upper_bound - def union(self, - *others: Union[IntegerBoundsType, IntegerValueType] - ) -> IntegerBoundsType: - """ - Return the union of this bounds and other bounds. + def union(self, *others: Union[IntegerBoundsType, IntegerValueType]) -> IntegerBoundsType: + """Return the union of this bounds and other bounds. The others list must also be Integer Bounds or Values. @@ -114,9 +115,11 @@ def union(self, from gemd.entity.value.integer_value import IntegerValue if any(not isinstance(x, (IntegerBounds, IntegerValue)) for x in others): - misses = {type(x).__name__ - for x in others - if not isinstance(x, (IntegerBounds, IntegerValue))} + misses = { + type(x).__name__ + for x in others + if not isinstance(x, (IntegerBounds, IntegerValue)) + } raise TypeError(f"union requires consistent typing; expected integer, found {misses}") lower = self.lower_bound upper = self.upper_bound @@ -130,8 +133,7 @@ def union(self, return IntegerBounds(lower_bound=lower, upper_bound=upper) def update(self, *others: Union[IntegerBoundsType, IntegerValueType]): - """ - Update this bounds to include other bounds. + """Update this bounds to include other bounds. The others list must also be Integer Bounds or Values. diff --git a/gemd/entity/bounds/molecular_structure_bounds.py b/gemd/entity/bounds/molecular_structure_bounds.py index 4586531e..7b5b2e9a 100644 --- a/gemd/entity/bounds/molecular_structure_bounds.py +++ b/gemd/entity/bounds/molecular_structure_bounds.py @@ -1,12 +1,12 @@ -""" -Bounds a molecular structure to be a valid representation. +"""Bounds a molecular structure to be a valid representation. In the future, this may include substructural restrictions. """ -from gemd.entity.bounds.base_bounds import BaseBounds from typing import TypeVar, Union +from gemd.entity.bounds.base_bounds import BaseBounds + __all__ = ["MolecularStructureBounds"] MolecularBoundsType = TypeVar("MolecularBoundsType", bound="MolecularStructureBounds") BaseValueType = TypeVar("BaseValueType", bound="BaseValue") # noqa: F821 @@ -17,8 +17,7 @@ class MolecularStructureBounds(BaseBounds, typ="molecular_structure_bounds"): """Molecular bounds, with no component or substructural restrictions (yet).""" def contains(self, bounds: Union[BaseBounds, BaseValueType]) -> bool: - """ - Check if another bounds or value object is contained by this bounds. + """Check if another bounds or value object is contained by this bounds. The other object must also be or type Molecular. There are no other conditions at this time. @@ -47,11 +46,10 @@ def contains(self, bounds: Union[BaseBounds, BaseValueType]) -> bool: return True - def union(self, - *others: Union[MolecularBoundsType, MolecularValueType] - ) -> MolecularBoundsType: - """ - Return the union of this bounds and other bounds. + def union( + self, *others: Union[MolecularBoundsType, MolecularValueType] + ) -> MolecularBoundsType: + """Return the union of this bounds and other bounds. The others list must also be Molecular Structure Bounds or Values. @@ -71,16 +69,18 @@ def union(self, from gemd.entity.value.molecular_value import MolecularValue if any(not isinstance(x, (MolecularStructureBounds, MolecularValue)) for x in others): - misses = {type(x).__name__ - for x in others - if not isinstance(x, (MolecularStructureBounds, MolecularValue))} - raise TypeError(f"union requires consistent typing; " - f"expected molecular structure, found {misses}") + misses = { + type(x).__name__ + for x in others + if not isinstance(x, (MolecularStructureBounds, MolecularValue)) + } + raise TypeError( + f"union requires consistent typing; expected molecular structure, found {misses}" + ) return MolecularStructureBounds() def update(self, *others: Union[MolecularBoundsType, MolecularValueType]): - """ - Update this bounds to include other bounds. + """Update this bounds to include other bounds. The others list must also be Molecular Structure Bounds or Values. @@ -95,8 +95,7 @@ def update(self, *others: Union[MolecularBoundsType, MolecularValueType]): pass # This is a no-op for Molecular structure def as_dict(self): - """ - Convert bounds to a dictionary. + """Convert bounds to a dictionary. Returns ------- diff --git a/gemd/entity/bounds/real_bounds.py b/gemd/entity/bounds/real_bounds.py index 041d0159..e3dfdd75 100644 --- a/gemd/entity/bounds/real_bounds.py +++ b/gemd/entity/bounds/real_bounds.py @@ -1,9 +1,10 @@ """Bound a real number to be between two values.""" + from math import isfinite from typing import TypeVar, Union -from gemd.entity.bounds.base_bounds import BaseBounds import gemd.units as units +from gemd.entity.bounds.base_bounds import BaseBounds __all__ = ["RealBounds"] RealBoundsType = TypeVar("RealBoundsType", bound="RealBounds") @@ -12,8 +13,7 @@ class RealBounds(BaseBounds, typ="real_bounds"): - """ - Bounded subset of the real numbers, parameterized by a lower and upper bound. + """Bounded subset of the real numbers, parameterized by a lower and upper bound. Parameters ---------- @@ -21,6 +21,7 @@ class RealBounds(BaseBounds, typ="real_bounds"): The lower endpoint (inclusive) of the permitted range. upper_bound: float The upper endpoint (inclusive) of the permitted range. + """ def __init__(self, lower_bound: float, upper_bound: float, default_units: str): @@ -43,8 +44,10 @@ def lower_bound(self, value: float): if value is None or not isfinite(value): raise ValueError(f"Lower bound must be given and finite: {value}") if self.upper_bound is not None and value > self.upper_bound: - raise ValueError(f"Upper bound ({self.upper_bound}) must be " - f"greater than or equal to lower bound ({value})") + raise ValueError( + f"Upper bound ({self.upper_bound}) must be " + f"greater than or equal to lower bound ({value})" + ) self._lower_bound = float(value) @property @@ -58,14 +61,15 @@ def upper_bound(self, value: float): if value is None or not isfinite(value): raise ValueError(f"Upper bound must be given and finite: {value}") if self.lower_bound is not None and value < self.lower_bound: - raise ValueError(f"Upper bound ({value}) must be " - f"greater than or equal to lower bound ({self.lower_bound})") + raise ValueError( + f"Upper bound ({value}) must be " + f"greater than or equal to lower bound ({self.lower_bound})" + ) self._upper_bound = float(value) @property def default_units(self) -> str: - """ - A string describing the units. + """A string describing the units. Units must be present and parseable by Pint. An empty string can be used for the units of a dimensionless quantity. @@ -76,13 +80,13 @@ def default_units(self) -> str: def default_units(self, default_units: str): """Set the string describing the units.""" if default_units is None: - raise ValueError("Real bounds must have units. " - "Use an empty string for a dimensionless quantity.") + raise ValueError( + "Real bounds must have units. Use an empty string for a dimensionless quantity." + ) self._default_units = units.parse_units(default_units, return_unit=False) def contains(self, bounds: Union[BaseBounds, BaseValueType]) -> bool: - """ - Check if another bounds or value object is a subset of this range. + """Check if another bounds or value object is a subset of this range. The other object must also be Real and its lower and upper bound must *both* be within the range of this bounds object. Values that are unbounded @@ -117,11 +121,8 @@ def contains(self, bounds: Union[BaseBounds, BaseValueType]) -> bool: return bounds.lower_bound >= lower and bounds.upper_bound <= upper - def union(self, - *others: Union[RealBoundsType, ContinuousValueType] - ) -> RealBoundsType: - """ - Return the union of this bounds and other bounds. + def union(self, *others: Union[RealBoundsType, ContinuousValueType]) -> RealBoundsType: + """Return the union of this bounds and other bounds. The others list must also be Real Bounds or Values. @@ -141,9 +142,11 @@ def union(self, from gemd.entity.value.continuous_value import ContinuousValue if any(not isinstance(x, (RealBounds, ContinuousValue)) for x in others): - misses = {type(x).__name__ - for x in others - if not isinstance(x, (RealBounds, ContinuousValue))} + misses = { + type(x).__name__ + for x in others + if not isinstance(x, (RealBounds, ContinuousValue)) + } raise TypeError(f"union requires consistent typing; expected real, found {misses}") lower = self.lower_bound upper = self.upper_bound @@ -161,8 +164,7 @@ def union(self, return RealBounds(lower_bound=lower, upper_bound=upper, default_units=unit_) def update(self, *others: Union[RealBoundsType, ContinuousValueType]): - """ - Update this bounds to include other bounds. + """Update this bounds to include other bounds. The others list must also be Real Bounds or Values. @@ -180,8 +182,7 @@ def update(self, *others: Union[RealBoundsType, ContinuousValueType]): self.default_units = result.default_units def _convert_bounds(self, target_units): - """ - Convert the bounds to the target unit system, or None if not possible. + """Convert the bounds to the target unit system, or None if not possible. Parameters ---------- @@ -195,10 +196,8 @@ def _convert_bounds(self, target_units): """ try: - lower_bound = units.convert_units( - self.lower_bound, self.default_units, target_units) - upper_bound = units.convert_units( - self.upper_bound, self.default_units, target_units) + lower_bound = units.convert_units(self.lower_bound, self.default_units, target_units) + upper_bound = units.convert_units(self.upper_bound, self.default_units, target_units) return lower_bound, upper_bound except units.IncompatibleUnitsError: return None, None diff --git a/gemd/entity/bounds_validation.py b/gemd/entity/bounds_validation.py index 3b8c039b..fab80c97 100644 --- a/gemd/entity/bounds_validation.py +++ b/gemd/entity/bounds_validation.py @@ -1,12 +1,11 @@ -from enum import IntEnum from contextlib import contextmanager +from enum import IntEnum __all__ = ["WarningLevel", "get_validation_level", "set_validation_level", "validation_level"] class WarningLevel(IntEnum): - """ - Control the behavior for warnings/errors around template validations. + """Control the behavior for warnings/errors around template validations. IGNORE: Do not check if values are consistent with bounds. WARNING: Accept bad values and issue a warning saying as much. diff --git a/gemd/entity/case_insensitive_dict.py b/gemd/entity/case_insensitive_dict.py index 6aac2b3d..5a919e79 100644 --- a/gemd/entity/case_insensitive_dict.py +++ b/gemd/entity/case_insensitive_dict.py @@ -1,4 +1,4 @@ -from typing import Tuple, Sequence, Any, Mapping, Optional +from typing import Any, Mapping, Optional, Sequence, Tuple __all__ = ["CaseInsensitiveDict"] @@ -6,8 +6,7 @@ class CaseInsensitiveDict(dict): - """ - A dictionary in which the keys are case-insensitive. + """A dictionary in which the keys are case-insensitive. It is initialized the same way as a typical dict, but the values can be accessed without regard to key case. The value associated with key "Key" can also be accessed with "key" @@ -34,8 +33,7 @@ def __getitem__(self, key: str) -> Any: return super().__getitem__(self.lowercase_dict[key.lower()]) def get(self, key: str, default: Any = None) -> Any: - """ - Get the value for a given case-insensitive key. + """Get the value for a given case-insensitive key. Parameters ---------- @@ -75,8 +73,7 @@ def clear(self) -> None: self.lowercase_dict.clear() def pop(self, key: str, default=_RaiseKeyError) -> Any: - """ - Remove and return the value for a given key from the dictionary. + """Remove and return the value for a given key from the dictionary. If key is in the dictionary, remove it and return its value, else return default. If default is not given and key is not in the dictionary, a KeyError is raised. @@ -107,8 +104,7 @@ def pop(self, key: str, default=_RaiseKeyError) -> Any: return val def popitem(self) -> Tuple: - """ - Remove and return a (key, value) pair from the dictionary. + """Remove and return a (key, value) pair from the dictionary. popitem() is useful to destructively iterate over a dictionary, as often used in set algorithms. If the dictionary is empty, calling popitem() raises a @@ -127,9 +123,8 @@ def popitem(self) -> Tuple: del self.lowercase_dict[result[0].lower()] return result - def copy(self) -> 'CaseInsensitiveDict': - """ - Return a shallow copy of the dictionary. + def copy(self) -> "CaseInsensitiveDict": + """Return a shallow copy of the dictionary. Returns ------- @@ -140,8 +135,7 @@ def copy(self) -> 'CaseInsensitiveDict': return CaseInsensitiveDict(super().copy()) def update(self, mapping: Optional[Mapping[str, Any]] = None, **kwargs) -> None: - """ - Update the dictionary with the key/value pairs from other, overwriting existing keys. + """Update the dictionary with the key/value pairs from other, overwriting existing keys. update() accepts either another dictionary object or an iterable of key/value pairs (as tuples or other iterables of length two). If keyword @@ -167,8 +161,8 @@ def update(self, mapping: Optional[Mapping[str, Any]] = None, **kwargs) -> None: prev = self.lowercase_dict[key.lower()] if prev != key: raise ValueError( - "Key '{}' already exists in dict with different case: " - "'{}'".format(key, prev)) + f"Key '{key}' already exists in dict with different case: '{prev}'" + ) if no_mapping: super().update(**kwargs) else: @@ -177,8 +171,7 @@ def update(self, mapping: Optional[Mapping[str, Any]] = None, **kwargs) -> None: self._register_key(key) def _register_key(self, key: str) -> None: - """ - Register a key to the dictionary. + """Register a key to the dictionary. Check to make sure it doesn't already exist in a different case. @@ -190,6 +183,5 @@ def _register_key(self, key: str) -> None: """ prev = self.lowercase_dict.get(key.lower()) if prev is not None and prev != key: - raise ValueError( - "Key '{}' already exists in dict with different case: '{}'".format(key, prev)) + raise ValueError(f"Key '{key}' already exists in dict with different case: '{prev}'") self.lowercase_dict[key.lower()] = key diff --git a/gemd/entity/dict_serializable.py b/gemd/entity/dict_serializable.py index 777ed1a2..7ed208b1 100644 --- a/gemd/entity/dict_serializable.py +++ b/gemd/entity/dict_serializable.py @@ -1,9 +1,8 @@ +import functools +import inspect from abc import ABC, ABCMeta from logging import getLogger - -import inspect -import functools -from typing import TypeVar, Union, Iterable, List, Mapping, Dict, Set, Any +from typing import Any, Dict, Iterable, List, Mapping, Set, TypeVar, Union __all__ = ["DictSerializable"] @@ -19,27 +18,34 @@ class DictSerializableMeta(ABCMeta): _class: Dict[str, type] = {} - def __new__(mcs, name, bases, *args, # noqa: D102 - typ: str = None, skip: Set[str] = frozenset(), - **kwargs): + def __new__( + mcs, + name, + bases, + *args, # noqa: D102 + typ: str = None, + skip: Set[str] = frozenset(), + **kwargs, + ): return super().__new__(mcs, name, bases, *args, **kwargs) def __init__(cls, name, bases, *args, typ: str = None, skip: Set[str] = frozenset(), **kwargs): super().__init__(name, bases, *args, **kwargs) if typ is not None: if typ in cls._class and not issubclass(cls, cls._class.get(typ)): - raise ValueError(f"{cls} attempted to take typ {typ} from {cls._class.get(typ)}, " - f"which is not its ancestor.") + raise ValueError( + f"{cls} attempted to take typ {typ} from {cls._class.get(typ)}, " + f"which is not its ancestor." + ) cls.typ = typ cls._class[typ] = cls elif not hasattr(cls, "typ"): cls.typ = NotImplementedError - cls.skip = {x for b in bases for x in getattr(b, 'skip', {})} | skip + cls.skip = {x for b in bases for x in getattr(b, "skip", {})} | skip @property def class_mapping(cls) -> Dict[str, type]: - """ - Return class typ string -> class map for DictSerializable and its descendants. + """Return class typ string -> class map for DictSerializable and its descendants. Note that is actually returns a copy of the internal dict to avoid accidental breakage. @@ -57,8 +63,7 @@ class DictSerializable(ABC, metaclass=DictSerializableMeta): @classmethod def from_dict(cls, d: Mapping[str, Any]) -> DictSerializableType: - """ - Reconstitute the object from a dictionary. + """Reconstitute the object from a dictionary. Parameters ---------- @@ -76,9 +81,8 @@ def from_dict(cls, d: Mapping[str, Any]) -> DictSerializableType: for name, arg in d.items(): if name in expected_arg_names: kwargs[name] = arg - elif name != 'type': - logger.warning('Ignoring unexpected keyword argument in {}: {}'.format( - cls.__name__, name)) + elif name != "type": + logger.warning(f"Ignoring unexpected keyword argument in {cls.__name__}: {name}") # noinspection PyArgumentList # DictSerializable's constructor is not intended for use, # but all of its children will use from_dict like this. @@ -93,8 +97,7 @@ def _init_sig(cls) -> List[str]: return expected_arg_names def as_dict(self) -> Dict[str, Any]: - """ - Convert the object to a dictionary. + """Convert the object to a dictionary. Returns ------- @@ -102,14 +105,13 @@ def as_dict(self) -> Dict[str, Any]: A dictionary representation of the object, where the keys are its fields. """ - keys = {x.lstrip('_') for x in vars(self) if x not in self.skip} + keys = {x.lstrip("_") for x in vars(self) if x not in self.skip} attributes = {k: self.__getattribute__(k) for k in keys} attributes["type"] = self.typ return attributes def dump(self) -> Dict[str, Any]: - """ - Convert the object to a JSON dictionary, so that every entry is serialized. + """Convert the object to a JSON dictionary, so that every entry is serialized. Uses the json encoder client, so objects with uids are converted to LinkByUID dictionaries. @@ -119,16 +121,16 @@ def dump(self) -> Dict[str, Any]: A string representation of the object as a dictionary. """ - from gemd.json import GEMDJson import json + from gemd.json import GEMDJson + encoder = GEMDJson() return json.loads(encoder.raw_dumps(self)) @staticmethod def build(d: Mapping[str, Any]) -> DictSerializableType: - """ - Build an object from a JSON dictionary. + """Build an object from a JSON dictionary. This differs from `from_dict` in that the values themselves may *also* be dictionaries corresponding to serialized DictSerializable objects. @@ -145,22 +147,23 @@ def build(d: Mapping[str, Any]) -> DictSerializableType: """ from gemd.json import GEMDJson + encoder = GEMDJson() return encoder.raw_loads(encoder.raw_dumps(d)) def __repr__(self) -> str: object_dict = self.as_dict() # as_dict() skips over keys in `skip`, but they should be in the representation. - skipped_keys = {x.lstrip('_') for x in self.skip} + skipped_keys = {x.lstrip("_") for x in self.skip} for key in skipped_keys: skipped_field = getattr(self, key, None) object_dict[key] = self._name_repr(skipped_field) return str(object_dict) - def _name_repr(self, - entity: Union[Iterable[DictSerializableType], DictSerializableType]) -> str: - """ - A representation of an object or a list of objects that uses the name and type. + def _name_repr( + self, entity: Union[Iterable[DictSerializableType], DictSerializableType] + ) -> str: + """A representation of an object or a list of objects that uses the name and type. This is used to represent soft-linked objects without inundating the user with repetitive information. @@ -182,7 +185,7 @@ def _name_repr(self, elif entity is None: return None else: - name = getattr(entity, 'name', '') + name = getattr(entity, "name", "") return f"<{type(entity).__name__} '{name}'>" def _dict_for_compare(self) -> Dict[str, Any]: diff --git a/gemd/entity/file_link.py b/gemd/entity/file_link.py index 24e971af..7ff7d0a2 100644 --- a/gemd/entity/file_link.py +++ b/gemd/entity/file_link.py @@ -1,12 +1,12 @@ """Represents a link to an external file.""" + from gemd.entity.dict_serializable import DictSerializable __all__ = ["FileLink"] class FileLink(DictSerializable, typ="file_link"): - """ - FileLink stores a name and link to an external resource. + """FileLink stores a name and link to an external resource. More information can be found in the `data model documentation \ diff --git a/gemd/entity/has_dependencies.py b/gemd/entity/has_dependencies.py index ce57b0ee..d911c0a1 100644 --- a/gemd/entity/has_dependencies.py +++ b/gemd/entity/has_dependencies.py @@ -1,6 +1,7 @@ """For entities that have dependencies.""" + from abc import ABC, abstractmethod -from typing import TypeVar, Union, Set +from typing import Set, TypeVar, Union __all__ = ["HasDependencies"] BaseEntityType = TypeVar("BaseEntityType", bound="BaseEntity") # noqa: F821 diff --git a/gemd/entity/link_by_uid.py b/gemd/entity/link_by_uid.py index 0ca54a10..6cc8f229 100644 --- a/gemd/entity/link_by_uid.py +++ b/gemd/entity/link_by_uid.py @@ -1,6 +1,7 @@ """A unique id that stands in for a data object.""" -from typing import TypeVar + import uuid +from typing import TypeVar from gemd.entity.dict_serializable import DictSerializable @@ -9,8 +10,7 @@ class LinkByUID(DictSerializable, typ="link_by_uid"): - """ - Link object, which replaces pointers to other entities before serialization and writing. + """Link object, which replaces pointers to other entities before serialization and writing. Parameters ---------- @@ -31,8 +31,7 @@ def __repr__(self): @classmethod def from_entity(cls, entity: BaseEntityType, *, scope=None): - """ - Create LinkByUID from in-memory object. + """Create LinkByUID from in-memory object. - If there exists an id with scope (default 'auto'), the LinkByUID object will be built with that scope. diff --git a/gemd/entity/object/__init__.py b/gemd/entity/object/__init__.py index 5b07645f..d60da56e 100644 --- a/gemd/entity/object/__init__.py +++ b/gemd/entity/object/__init__.py @@ -1,4 +1,5 @@ """Run and Spec Objects""" + # flake8: noqa from .material_run import MaterialRun from .measurement_run import MeasurementRun @@ -9,5 +10,13 @@ from .ingredient_run import IngredientRun from .ingredient_spec import IngredientSpec -__all__ = ["ProcessSpec", "MaterialSpec", "IngredientSpec", "MeasurementSpec", - "ProcessRun", "MaterialRun", "IngredientRun", "MeasurementRun"] +__all__ = [ + "ProcessSpec", + "MaterialSpec", + "IngredientSpec", + "MeasurementSpec", + "ProcessRun", + "MaterialRun", + "IngredientRun", + "MeasurementRun", +] diff --git a/gemd/entity/object/base_object.py b/gemd/entity/object/base_object.py index 6078e83c..4d7a433d 100644 --- a/gemd/entity/object/base_object.py +++ b/gemd/entity/object/base_object.py @@ -1,17 +1,15 @@ import functools +from typing import Iterable, List, Mapping, Optional, Union from gemd.entity.base_entity import BaseEntity from gemd.entity.file_link import FileLink from gemd.entity.setters import validate_list, validate_str -from typing import Optional, Union, Iterable, List, Mapping - __all__ = ["BaseObject"] class BaseObject(BaseEntity): - """ - Base class for objects. + """Base class for objects. This includes {Material, Process, Measurement, Ingredient} {Run, Spec} @@ -34,13 +32,15 @@ class BaseObject(BaseEntity): """ - def __init__(self, - name: str, - *, - uids: Mapping[str, str] = None, - tags: Iterable[str] = None, - notes: str = None, - file_links: Optional[Union[Iterable[FileLink], FileLink]] = None): + def __init__( + self, + name: str, + *, + uids: Mapping[str, str] = None, + tags: Iterable[str] = None, + notes: str = None, + file_links: Optional[Union[Iterable[FileLink], FileLink]] = None, + ): BaseEntity.__init__(self, uids, tags) self.notes = notes self._name = None @@ -53,8 +53,7 @@ def __init__(self, @classmethod @functools.lru_cache(maxsize=1024) def _attribute_has_setter(cls, name: str) -> bool: - """ - Internal method to identify if an attribute has a setter method. + """Internal method to identify if an attribute has a setter method. Necessary because IngredientRun clobbers the name setter. """ diff --git a/gemd/entity/object/has_conditions.py b/gemd/entity/object/has_conditions.py index 8be1bb14..61e0a7db 100644 --- a/gemd/entity/object/has_conditions.py +++ b/gemd/entity/object/has_conditions.py @@ -1,14 +1,15 @@ """For entities that have conditions.""" + +from abc import ABC +from typing import Iterable, List, Set, Union + +from gemd.entity.attribute.condition import Condition from gemd.entity.base_entity import BaseEntity from gemd.entity.has_dependencies import HasDependencies from gemd.entity.link_by_uid import LinkByUID from gemd.entity.object.has_template_check_generator import HasTemplateCheckGenerator -from gemd.entity.template.has_condition_templates import HasConditionTemplates -from gemd.entity.attribute.condition import Condition from gemd.entity.setters import validate_list - -from typing import Union, Iterable, List, Set -from abc import ABC +from gemd.entity.template.has_condition_templates import HasConditionTemplates __all__ = ["HasConditions"] diff --git a/gemd/entity/object/has_material.py b/gemd/entity/object/has_material.py index dd8c326a..4f5484bb 100644 --- a/gemd/entity/object/has_material.py +++ b/gemd/entity/object/has_material.py @@ -1,12 +1,13 @@ """For entities that have specs.""" + +from abc import ABC, abstractmethod +from typing import Set, Union + from gemd.entity.base_entity import BaseEntity from gemd.entity.has_dependencies import HasDependencies from gemd.entity.link_by_uid import LinkByUID from gemd.entity.object.base_object import BaseObject -from abc import ABC, abstractmethod -from typing import Union, Set - __all__ = ["HasMaterial"] diff --git a/gemd/entity/object/has_parameters.py b/gemd/entity/object/has_parameters.py index 3343c470..196ddd03 100644 --- a/gemd/entity/object/has_parameters.py +++ b/gemd/entity/object/has_parameters.py @@ -1,14 +1,15 @@ """For entities that have parameters.""" + +from abc import ABC +from typing import Iterable, List, Set, Union + +from gemd.entity.attribute.parameter import Parameter from gemd.entity.base_entity import BaseEntity from gemd.entity.has_dependencies import HasDependencies from gemd.entity.link_by_uid import LinkByUID from gemd.entity.object.has_template_check_generator import HasTemplateCheckGenerator -from gemd.entity.template.has_parameter_templates import HasParameterTemplates -from gemd.entity.attribute.parameter import Parameter from gemd.entity.setters import validate_list - -from typing import Union, Iterable, List, Set -from abc import ABC +from gemd.entity.template.has_parameter_templates import HasParameterTemplates __all__ = ["HasParameters"] diff --git a/gemd/entity/object/has_process.py b/gemd/entity/object/has_process.py index 9f686e68..5fdbbfaa 100644 --- a/gemd/entity/object/has_process.py +++ b/gemd/entity/object/has_process.py @@ -1,12 +1,13 @@ """For entities that have specs.""" + +from abc import abstractmethod +from typing import Set, Union + from gemd.entity.base_entity import BaseEntity from gemd.entity.has_dependencies import HasDependencies from gemd.entity.link_by_uid import LinkByUID from gemd.entity.object.base_object import BaseObject -from abc import abstractmethod -from typing import Union, Set - __all__ = ["HasProcess"] diff --git a/gemd/entity/object/has_properties.py b/gemd/entity/object/has_properties.py index 28341af0..3c44dfd9 100644 --- a/gemd/entity/object/has_properties.py +++ b/gemd/entity/object/has_properties.py @@ -1,14 +1,15 @@ """For entities that have properties.""" + +from abc import ABC +from typing import Iterable, List, Set, Union + +from gemd.entity.attribute.property import Property from gemd.entity.base_entity import BaseEntity from gemd.entity.has_dependencies import HasDependencies from gemd.entity.link_by_uid import LinkByUID from gemd.entity.object.has_template_check_generator import HasTemplateCheckGenerator -from gemd.entity.template.has_property_templates import HasPropertyTemplates -from gemd.entity.attribute.property import Property from gemd.entity.setters import validate_list - -from typing import Union, Iterable, List, Set -from abc import ABC +from gemd.entity.template.has_property_templates import HasPropertyTemplates __all__ = ["HasProperties"] diff --git a/gemd/entity/object/has_quantities.py b/gemd/entity/object/has_quantities.py index 5c366eb3..c06c2ba9 100644 --- a/gemd/entity/object/has_quantities.py +++ b/gemd/entity/object/has_quantities.py @@ -1,11 +1,12 @@ """For entities that hve quantities.""" + from sys import float_info from gemd.entity.bounds.real_bounds import RealBounds -from gemd.entity.value.continuous_value import ContinuousValue -from gemd.entity.value.base_value import BaseValue -from gemd.entity.bounds_validation import get_validation_level, WarningLevel +from gemd.entity.bounds_validation import WarningLevel, get_validation_level from gemd.entity.dict_serializable import logger +from gemd.entity.value.base_value import BaseValue +from gemd.entity.value.continuous_value import ContinuousValue __all__ = ["HasQuantities"] @@ -13,11 +14,14 @@ class HasQuantities(object): """Mixin-trait that includes the mass, volume, number fraction, and absolute quantity.""" - def __init__(self, *, - mass_fraction: ContinuousValue = None, - volume_fraction: ContinuousValue = None, - number_fraction: ContinuousValue = None, - absolute_quantity: ContinuousValue = None): + def __init__( + self, + *, + mass_fraction: ContinuousValue = None, + volume_fraction: ContinuousValue = None, + number_fraction: ContinuousValue = None, + absolute_quantity: ContinuousValue = None, + ): self._mass_fraction = None self.mass_fraction = mass_fraction @@ -33,7 +37,7 @@ def __init__(self, *, @staticmethod def _check(value: BaseValue): - fraction_bounds = RealBounds(lower_bound=0.0, upper_bound=1.0, default_units='') + fraction_bounds = RealBounds(lower_bound=0.0, upper_bound=1.0, default_units="") level = get_validation_level() accept = level == WarningLevel.IGNORE or fraction_bounds.contains(value) if not accept: @@ -101,14 +105,10 @@ def absolute_quantity(self, absolute_quantity: ContinuousValue): raise TypeError("absolute_quantity was not given as a continuous value") else: max_bounds = RealBounds( - lower_bound=0.0, - upper_bound=float_info.max, - default_units=absolute_quantity.units + lower_bound=0.0, upper_bound=float_info.max, default_units=absolute_quantity.units ) dimensionless = RealBounds( - lower_bound=0.0, - upper_bound=float_info.max, - default_units='' + lower_bound=0.0, upper_bound=float_info.max, default_units="" ) level = get_validation_level() if level != WarningLevel.IGNORE: diff --git a/gemd/entity/object/has_source.py b/gemd/entity/object/has_source.py index f14dcc5c..666e557e 100644 --- a/gemd/entity/object/has_source.py +++ b/gemd/entity/object/has_source.py @@ -1,4 +1,5 @@ """For entities that have parameters.""" + from gemd.entity.source.performed_source import PerformedSource __all__ = ["HasSource"] diff --git a/gemd/entity/object/has_spec.py b/gemd/entity/object/has_spec.py index 7772cde3..76c73896 100644 --- a/gemd/entity/object/has_spec.py +++ b/gemd/entity/object/has_spec.py @@ -1,13 +1,14 @@ """For entities that have specs.""" + +from abc import abstractmethod +from typing import Optional, Set, Type, Union + from gemd.entity.base_entity import BaseEntity from gemd.entity.has_dependencies import HasDependencies from gemd.entity.link_by_uid import LinkByUID from gemd.entity.object.has_template import HasTemplate from gemd.entity.template.base_template import BaseTemplate -from abc import abstractmethod -from typing import Optional, Union, Set, Type - __all__ = ["HasSpec"] @@ -31,8 +32,9 @@ def spec(self, spec: Union[HasTemplate, LinkByUID]): elif isinstance(spec, (self._spec_type(), LinkByUID)): self._spec = spec else: - raise TypeError(f"Template must be a {self._spec_type()} or LinkByUID, " - f"not {type(spec)}") + raise TypeError( + f"Template must be a {self._spec_type()} or LinkByUID, not {type(spec)}" + ) @staticmethod @abstractmethod diff --git a/gemd/entity/object/has_template.py b/gemd/entity/object/has_template.py index c38662e1..8a732546 100644 --- a/gemd/entity/object/has_template.py +++ b/gemd/entity/object/has_template.py @@ -1,12 +1,13 @@ """For entities that have templates.""" + +from abc import abstractmethod +from typing import Optional, Set, Type, Union + from gemd.entity.base_entity import BaseEntity from gemd.entity.has_dependencies import HasDependencies from gemd.entity.link_by_uid import LinkByUID from gemd.entity.template.base_template import BaseTemplate -from abc import abstractmethod -from typing import Optional, Union, Set, Type - __all__ = ["HasTemplate"] @@ -35,8 +36,9 @@ def template(self, template: Optional[Union[BaseTemplate, LinkByUID]]): elif isinstance(template, (self._template_type(), LinkByUID)): self._template = template else: - raise TypeError(f"Template must be a {self._template_type()} or LinkByUID, " - f"not {type(template)}") + raise TypeError( + f"Template must be a {self._template_type()} or LinkByUID, not {type(template)}" + ) def _local_dependencies(self) -> Set[Union[BaseEntity, LinkByUID]]: """Return a set of all immediate dependencies (no recursion).""" diff --git a/gemd/entity/object/has_template_check_generator.py b/gemd/entity/object/has_template_check_generator.py index 23b936ba..bea344fe 100644 --- a/gemd/entity/object/has_template_check_generator.py +++ b/gemd/entity/object/has_template_check_generator.py @@ -1,16 +1,17 @@ """For entities that have specs.""" -from gemd.entity.template.base_template import BaseTemplate -from gemd.entity.link_by_uid import LinkByUID -from gemd.entity.bounds_validation import get_validation_level, WarningLevel -from gemd.entity.dict_serializable import logger from abc import ABC, abstractmethod -from inspect import getmodule, getmembers, isclass, signature -from typing import Union, Callable, TypeVar +from inspect import getmembers, getmodule, isclass, signature +from typing import Callable, TypeVar, Union + +from gemd.entity.bounds_validation import WarningLevel, get_validation_level +from gemd.entity.dict_serializable import logger +from gemd.entity.link_by_uid import LinkByUID +from gemd.entity.template.base_template import BaseTemplate __all__ = ["HasTemplateCheckGenerator"] -T = TypeVar('T') +T = TypeVar("T") class HasTemplateCheckGenerator(ABC): @@ -21,11 +22,10 @@ class HasTemplateCheckGenerator(ABC): def template(self) -> Union[BaseTemplate, LinkByUID]: """Get the object template associated with this object.""" - def _generate_template_check(self, - validate: Callable[["HasTemplateCheckGenerator", T], bool] - ) -> Callable[[T], None]: - """ - Generate a closure for the object and the validation routine. + def _generate_template_check( + self, validate: Callable[["HasTemplateCheckGenerator", T], bool] + ) -> Callable[[T], None]: + """Generate a closure for the object and the validation routine. This method generates a function that takes a single attribute as input and checks it against the relevant templates and restricted bounds of the object template associated @@ -77,9 +77,8 @@ def _generate_template_check(self, def template_check(x: attr): """Given an attribute, check it against this object's template.""" level = get_validation_level() - reject = level != WarningLevel.IGNORE \ - and isinstance(self.template, cls) \ - and not validate(self.template, x) + checkable = level != WarningLevel.IGNORE and isinstance(self.template, cls) + reject = checkable and not validate(self.template, x) if reject: message = f"Value {x.value} is inconsistent with template {self.template.name}" diff --git a/gemd/entity/object/ingredient_run.py b/gemd/entity/object/ingredient_run.py index 4be4a690..f7f068af 100644 --- a/gemd/entity/object/ingredient_run.py +++ b/gemd/entity/object/ingredient_run.py @@ -1,27 +1,26 @@ -from gemd.entity.object.ingredient_spec import IngredientSpec -from gemd.entity.object.material_run import MaterialRun -from gemd.entity.object.process_run import ProcessRun +from typing import Any, Iterable, List, Mapping, Optional, Type, Union + +from gemd.entity.dict_serializable import DictSerializable +from gemd.entity.file_link import FileLink +from gemd.entity.link_by_uid import LinkByUID from gemd.entity.object.base_object import BaseObject from gemd.entity.object.has_material import HasMaterial from gemd.entity.object.has_process import HasProcess from gemd.entity.object.has_quantities import HasQuantities from gemd.entity.object.has_spec import HasSpec -from gemd.entity.value.continuous_value import ContinuousValue -from gemd.entity.dict_serializable import DictSerializable -from gemd.entity.file_link import FileLink -from gemd.entity.link_by_uid import LinkByUID +from gemd.entity.object.ingredient_spec import IngredientSpec +from gemd.entity.object.material_run import MaterialRun +from gemd.entity.object.process_run import ProcessRun from gemd.entity.setters import validate_list - -from typing import Optional, Union, Iterable, List, Mapping, Type, Any +from gemd.entity.value.continuous_value import ContinuousValue __all__ = ["IngredientRun"] -class IngredientRun(BaseObject, - HasQuantities, HasSpec, HasMaterial, HasProcess, - typ="ingredient_run"): - """ - An ingredient run. +class IngredientRun( + BaseObject, HasQuantities, HasSpec, HasMaterial, HasProcess, typ="ingredient_run" +): + """An ingredient run. Ingredients annotate a material with information about its usage in a process. @@ -56,27 +55,34 @@ class IngredientRun(BaseObject, """ - def __init__(self, - *, - material: Union[MaterialRun, LinkByUID] = None, - process: Union[ProcessRun, LinkByUID] = None, - mass_fraction: ContinuousValue = None, - volume_fraction: ContinuousValue = None, - number_fraction: ContinuousValue = None, - absolute_quantity: ContinuousValue = None, - spec: Union[IngredientSpec, LinkByUID] = None, - uids: Mapping[str, str] = None, - tags: Iterable[str] = None, - notes: str = None, - file_links: Optional[Union[Iterable[FileLink], FileLink]] = None): - BaseObject.__init__(self, name=None, uids=uids, tags=tags, - notes=notes, file_links=file_links) + def __init__( + self, + *, + material: Union[MaterialRun, LinkByUID] = None, + process: Union[ProcessRun, LinkByUID] = None, + mass_fraction: ContinuousValue = None, + volume_fraction: ContinuousValue = None, + number_fraction: ContinuousValue = None, + absolute_quantity: ContinuousValue = None, + spec: Union[IngredientSpec, LinkByUID] = None, + uids: Mapping[str, str] = None, + tags: Iterable[str] = None, + notes: str = None, + file_links: Optional[Union[Iterable[FileLink], FileLink]] = None, + ): + BaseObject.__init__( + self, name=None, uids=uids, tags=tags, notes=notes, file_links=file_links + ) self._labels = None HasSpec.__init__(self, spec) # this will overwrite name/labels if/when they are set - HasQuantities.__init__(self, mass_fraction=mass_fraction, volume_fraction=volume_fraction, - number_fraction=number_fraction, absolute_quantity=absolute_quantity - ) + HasQuantities.__init__( + self, + mass_fraction=mass_fraction, + volume_fraction=volume_fraction, + number_fraction=number_fraction, + absolute_quantity=absolute_quantity, + ) self._material = None self._process = None @@ -87,6 +93,7 @@ def __init__(self, def name(self) -> str: """Get name.""" from gemd.entity.object.ingredient_spec import IngredientSpec + if isinstance(self.spec, IngredientSpec): return self.spec.name else: @@ -96,6 +103,7 @@ def name(self) -> str: def labels(self) -> List[str]: """Get labels.""" from gemd.entity.object.ingredient_spec import IngredientSpec + if isinstance(self.spec, IngredientSpec): return self.spec.labels else: @@ -113,8 +121,9 @@ def material(self, material: Union[MaterialRun, LinkByUID]): elif isinstance(material, (MaterialRun, LinkByUID)): self._material = material else: - raise TypeError("IngredientRun.material must be a MaterialRun or " - "LinkByUID: {}".format(material)) + raise TypeError( + f"IngredientRun.material must be a MaterialRun or LinkByUID: {material}" + ) @property def process(self) -> Union[ProcessRun, LinkByUID]: @@ -132,8 +141,7 @@ def process(self, process: Union[ProcessRun, LinkByUID]): if isinstance(process, ProcessRun): process.ingredients.append(self) else: - raise TypeError("IngredientRun.process must be a ProcessRun or " - "LinkByUID: {}".format(process)) + raise TypeError(f"IngredientRun.process must be a ProcessRun or LinkByUID: {process}") @staticmethod def _spec_type() -> Type: @@ -156,8 +164,7 @@ def spec(self, spec: Union[IngredientSpec, LinkByUID]): @classmethod def from_dict(cls, d: Mapping[str, Any]) -> DictSerializable: - """ - Overloaded method from DictSerializable to intercept `name` and `labels` fields. + """Overloaded method from DictSerializable to intercept `name` and `labels` fields. Parameters ---------- diff --git a/gemd/entity/object/ingredient_spec.py b/gemd/entity/object/ingredient_spec.py index 7a2f8d41..2dc0138f 100644 --- a/gemd/entity/object/ingredient_spec.py +++ b/gemd/entity/object/ingredient_spec.py @@ -1,25 +1,24 @@ -from gemd.entity.object.material_spec import MaterialSpec -from gemd.entity.object.process_spec import ProcessSpec +from typing import Iterable, List, Mapping, Optional, Type, Union + +from gemd.entity.file_link import FileLink +from gemd.entity.link_by_uid import LinkByUID from gemd.entity.object.base_object import BaseObject from gemd.entity.object.has_material import HasMaterial from gemd.entity.object.has_process import HasProcess from gemd.entity.object.has_quantities import HasQuantities from gemd.entity.object.has_template import HasTemplate -from gemd.entity.value.continuous_value import ContinuousValue -from gemd.entity.file_link import FileLink -from gemd.entity.link_by_uid import LinkByUID +from gemd.entity.object.material_spec import MaterialSpec +from gemd.entity.object.process_spec import ProcessSpec from gemd.entity.setters import validate_list - -from typing import Optional, Union, Iterable, List, Mapping, Type +from gemd.entity.value.continuous_value import ContinuousValue __all__ = ["IngredientSpec"] -class IngredientSpec(BaseObject, - HasQuantities, HasTemplate, HasMaterial, HasProcess, - typ="ingredient_spec"): - """ - An ingredient specification. +class IngredientSpec( + BaseObject, HasQuantities, HasTemplate, HasMaterial, HasProcess, typ="ingredient_spec" +): + """An ingredient specification. Ingredients annotate a material with information about its usage in a process. @@ -56,26 +55,33 @@ class IngredientSpec(BaseObject, """ - def __init__(self, - name: str, - *, - material: Union[MaterialSpec, LinkByUID] = None, - process: Union[ProcessSpec, LinkByUID] = None, - labels: Iterable[str] = None, - mass_fraction: ContinuousValue = None, - volume_fraction: ContinuousValue = None, - number_fraction: ContinuousValue = None, - absolute_quantity: ContinuousValue = None, - uids: Mapping[str, str] = None, - tags: Iterable[str] = None, - notes: str = None, - file_links: Optional[Union[Iterable[FileLink], FileLink]] = None): - - BaseObject.__init__(self, name=name, - uids=uids, tags=tags, notes=notes, file_links=file_links) - HasQuantities.__init__(self, mass_fraction=mass_fraction, volume_fraction=volume_fraction, - number_fraction=number_fraction, absolute_quantity=absolute_quantity - ) + def __init__( + self, + name: str, + *, + material: Union[MaterialSpec, LinkByUID] = None, + process: Union[ProcessSpec, LinkByUID] = None, + labels: Iterable[str] = None, + mass_fraction: ContinuousValue = None, + volume_fraction: ContinuousValue = None, + number_fraction: ContinuousValue = None, + absolute_quantity: ContinuousValue = None, + uids: Mapping[str, str] = None, + tags: Iterable[str] = None, + notes: str = None, + file_links: Optional[Union[Iterable[FileLink], FileLink]] = None, + ): + + BaseObject.__init__( + self, name=name, uids=uids, tags=tags, notes=notes, file_links=file_links + ) + HasQuantities.__init__( + self, + mass_fraction=mass_fraction, + volume_fraction=volume_fraction, + number_fraction=number_fraction, + absolute_quantity=absolute_quantity, + ) self._material = None self._process = None @@ -126,8 +132,9 @@ def process(self, process: Union[ProcessSpec, LinkByUID]): if isinstance(process, ProcessSpec): process.ingredients.append(self) else: - raise TypeError("IngredientSpec.process must be a ProcessSpec or " - "LinkByUID: {}".format(process)) + raise TypeError( + f"IngredientSpec.process must be a ProcessSpec or LinkByUID: {process}" + ) @property def template(self): diff --git a/gemd/entity/object/material_run.py b/gemd/entity/object/material_run.py index a99fd703..44698286 100644 --- a/gemd/entity/object/material_run.py +++ b/gemd/entity/object/material_run.py @@ -1,22 +1,21 @@ -from gemd.entity.object.material_spec import MaterialSpec -from gemd.entity.object.process_run import ProcessRun +from typing import Any, Iterable, List, Mapping, Optional, Type, TypeVar, Union + +from gemd.entity.file_link import FileLink +from gemd.entity.link_by_uid import LinkByUID from gemd.entity.object.base_object import BaseObject from gemd.entity.object.has_process import HasProcess from gemd.entity.object.has_spec import HasSpec -from gemd.enumeration import SampleType -from gemd.entity.file_link import FileLink -from gemd.entity.link_by_uid import LinkByUID +from gemd.entity.object.material_spec import MaterialSpec +from gemd.entity.object.process_run import ProcessRun from gemd.entity.setters import validate_list - -from typing import TypeVar, Optional, Union, Iterable, List, Mapping, Type, Any +from gemd.enumeration import SampleType __all__ = ["MaterialRun"] MeasurementRunType = TypeVar("MeasurementRunType", bound="MeasurementRun") # noqa: F821 class MaterialRun(BaseObject, HasSpec, HasProcess, typ="material_run", skip={"_measurements"}): - """ - A material run. + """A material run. This includes a link to the originating process and soft links to measurements. @@ -46,19 +45,23 @@ class MaterialRun(BaseObject, HasSpec, HasProcess, typ="material_run", skip={"_m """ - def __init__(self, - name: str, - *, - spec: Union[MaterialSpec, LinkByUID] = None, - process: Union[ProcessRun, LinkByUID] = None, - sample_type: Union[SampleType, str] = "unknown", - uids: Mapping[str, str] = None, - tags: Iterable[str] = None, - notes: str = None, - file_links: Optional[Union[Iterable[FileLink], FileLink]] = None): + def __init__( + self, + name: str, + *, + spec: Union[MaterialSpec, LinkByUID] = None, + process: Union[ProcessRun, LinkByUID] = None, + sample_type: Union[SampleType, str] = "unknown", + uids: Mapping[str, str] = None, + tags: Iterable[str] = None, + notes: str = None, + file_links: Optional[Union[Iterable[FileLink], FileLink]] = None, + ): from gemd.entity.object.measurement_run import MeasurementRun - BaseObject.__init__(self, name=name, uids=uids, tags=tags, notes=notes, - file_links=file_links) + + BaseObject.__init__( + self, name=name, uids=uids, tags=tags, notes=notes, file_links=file_links + ) HasSpec.__init__(self, spec=spec) self._process = None self._measurements = validate_list(None, [MeasurementRun, LinkByUID]) @@ -84,7 +87,7 @@ def process(self, process: Union[ProcessRun, LinkByUID]): process._output_material = self self._process = process else: - raise TypeError("process must be a ProcessRun or LinkByUID: {}".format(process)) + raise TypeError(f"process must be a ProcessRun or LinkByUID: {process}") @property def measurements(self) -> List[MeasurementRunType]: @@ -113,5 +116,5 @@ def _spec_type() -> Type: def _dict_for_compare(self) -> Mapping[str, Any]: """Support for recursive equals.""" base = super()._dict_for_compare() - base['measurements'] = self.measurements + base["measurements"] = self.measurements return base diff --git a/gemd/entity/object/material_spec.py b/gemd/entity/object/material_spec.py index 3410028e..f0d4ba90 100644 --- a/gemd/entity/object/material_spec.py +++ b/gemd/entity/object/material_spec.py @@ -1,24 +1,22 @@ +from typing import Iterable, List, Mapping, Optional, Set, Type, Union + from gemd.entity.attribute.property_and_conditions import PropertyAndConditions -from gemd.entity.object.process_spec import ProcessSpec -from gemd.entity.object.base_object import BaseEntity -from gemd.entity.object.base_object import BaseObject +from gemd.entity.file_link import FileLink +from gemd.entity.link_by_uid import LinkByUID +from gemd.entity.object.base_object import BaseEntity, BaseObject from gemd.entity.object.has_process import HasProcess from gemd.entity.object.has_properties import HasProperties from gemd.entity.object.has_template import HasTemplate +from gemd.entity.object.process_spec import ProcessSpec +from gemd.entity.setters import validate_list from gemd.entity.template.has_property_templates import HasPropertyTemplates from gemd.entity.template.material_template import MaterialTemplate -from gemd.entity.file_link import FileLink -from gemd.entity.link_by_uid import LinkByUID -from gemd.entity.setters import validate_list - -from typing import Optional, Union, Iterable, List, Set, Mapping, Type __all__ = ["MaterialSpec"] class MaterialSpec(BaseObject, HasTemplate, HasProcess, HasProperties, typ="material_spec"): - """ - A material specification. + """A material specification. This includes a link to the originating process and specified properties with conditions. @@ -49,18 +47,21 @@ class MaterialSpec(BaseObject, HasTemplate, HasProcess, HasProperties, typ="mate """ - def __init__(self, - name: str, - *, - template: Optional[Union[MaterialTemplate, LinkByUID]] = None, - process: Union[ProcessSpec, LinkByUID] = None, - properties: Union[Iterable[PropertyAndConditions], PropertyAndConditions] = None, - uids: Mapping[str, str] = None, - tags: Iterable[str] = None, - notes: str = None, - file_links: Optional[Union[Iterable[FileLink], FileLink]] = None): - BaseObject.__init__(self, name=name, uids=uids, tags=tags, notes=notes, - file_links=file_links) + def __init__( + self, + name: str, + *, + template: Optional[Union[MaterialTemplate, LinkByUID]] = None, + process: Union[ProcessSpec, LinkByUID] = None, + properties: Union[Iterable[PropertyAndConditions], PropertyAndConditions] = None, + uids: Mapping[str, str] = None, + tags: Iterable[str] = None, + notes: str = None, + file_links: Optional[Union[Iterable[FileLink], FileLink]] = None, + ): + BaseObject.__init__( + self, name=name, uids=uids, tags=tags, notes=notes, file_links=file_links + ) HasTemplate.__init__(self, template) self._properties = None self.properties = properties @@ -73,8 +74,9 @@ def properties(self) -> List[PropertyAndConditions]: return self._properties @properties.setter - def properties(self, - properties: Union[Iterable[PropertyAndConditions], PropertyAndConditions]): + def properties( + self, properties: Union[Iterable[PropertyAndConditions], PropertyAndConditions] + ): """Set the list of property-and-conditions.""" checker = self._generate_template_check(HasPropertyTemplates.validate_property) self._properties = validate_list(properties, PropertyAndConditions, trigger=checker) @@ -86,15 +88,15 @@ def process(self) -> Union[ProcessSpec, LinkByUID]: @process.setter def process(self, process: Union[ProcessSpec, LinkByUID]): - """ - Link to the ProcessSpec that creates this MaterialSpec. + """Link to the ProcessSpec that creates this MaterialSpec. If the input, process, is not an instance of ProcessSpec, raise an error. Otherwise, make a bidirectional link: this MaterialSpec is linked to process, and process has its output_material field linked to this MaterialSpec """ - from gemd.entity.object.process_spec import ProcessSpec from gemd.entity.link_by_uid import LinkByUID + from gemd.entity.object.process_spec import ProcessSpec + if self.process is not None and isinstance(self.process, ProcessSpec): self.process._output_material = None if process is None: @@ -105,8 +107,10 @@ def process(self, process: Union[ProcessSpec, LinkByUID]): process._output_material = self self._process = process else: - raise TypeError(f"process must be an instance of ProcessSpec or LinkByUID; " - f"instead received type {type(process)}: {process}") + raise TypeError( + f"process must be an instance of ProcessSpec or LinkByUID; " + f"instead received type {type(process)}: {process}" + ) @staticmethod def _template_type() -> Type: diff --git a/gemd/entity/object/measurement_run.py b/gemd/entity/object/measurement_run.py index 60e42302..9e5a22f1 100644 --- a/gemd/entity/object/measurement_run.py +++ b/gemd/entity/object/measurement_run.py @@ -1,28 +1,35 @@ -from gemd.entity.object.measurement_spec import MeasurementSpec -from gemd.entity.object.material_run import MaterialRun -from gemd.entity.object.base_object import BaseObject -from gemd.entity.object.has_material import HasMaterial -from gemd.entity.object.has_spec import HasSpec -from gemd.entity.object.has_conditions import HasConditions -from gemd.entity.object.has_properties import HasProperties -from gemd.entity.object.has_parameters import HasParameters -from gemd.entity.object.has_source import HasSource +from typing import Iterable, Mapping, Optional, Type, Union + from gemd.entity.attribute.condition import Condition from gemd.entity.attribute.parameter import Parameter from gemd.entity.attribute.property import Property -from gemd.entity.source.performed_source import PerformedSource from gemd.entity.file_link import FileLink from gemd.entity.link_by_uid import LinkByUID - -from typing import Optional, Union, Iterable, Mapping, Type +from gemd.entity.object.base_object import BaseObject +from gemd.entity.object.has_conditions import HasConditions +from gemd.entity.object.has_material import HasMaterial +from gemd.entity.object.has_parameters import HasParameters +from gemd.entity.object.has_properties import HasProperties +from gemd.entity.object.has_source import HasSource +from gemd.entity.object.has_spec import HasSpec +from gemd.entity.object.material_run import MaterialRun +from gemd.entity.object.measurement_spec import MeasurementSpec +from gemd.entity.source.performed_source import PerformedSource __all__ = ["MeasurementRun"] -class MeasurementRun(BaseObject, HasMaterial, HasSpec, HasConditions, HasProperties, - HasParameters, HasSource, typ="measurement_run"): - """ - A measurement run. +class MeasurementRun( + BaseObject, + HasMaterial, + HasSpec, + HasConditions, + HasProperties, + HasParameters, + HasSource, + typ="measurement_run", +): + """A measurement run. This contains a link to the material the measurement is performed on, as well as links to any properties, conditions, and parameters. @@ -60,21 +67,24 @@ class MeasurementRun(BaseObject, HasMaterial, HasSpec, HasConditions, HasPropert """ - def __init__(self, - name: str, - *, - spec: Union[MeasurementSpec, LinkByUID] = None, - material: Union[MaterialRun, LinkByUID] = None, - properties: Union[Property, Iterable[Property]] = None, - conditions: Union[Condition, Iterable[Condition]] = None, - parameters: Union[Parameter, Iterable[Parameter]] = None, - uids: Mapping[str, str] = None, - tags: Iterable[str] = None, - notes: str = None, - file_links: Optional[Union[Iterable[FileLink], FileLink]] = None, - source: PerformedSource = None): - BaseObject.__init__(self, name=name, uids=uids, tags=tags, notes=notes, - file_links=file_links) + def __init__( + self, + name: str, + *, + spec: Union[MeasurementSpec, LinkByUID] = None, + material: Union[MaterialRun, LinkByUID] = None, + properties: Union[Property, Iterable[Property]] = None, + conditions: Union[Condition, Iterable[Condition]] = None, + parameters: Union[Parameter, Iterable[Parameter]] = None, + uids: Mapping[str, str] = None, + tags: Iterable[str] = None, + notes: str = None, + file_links: Optional[Union[Iterable[FileLink], FileLink]] = None, + source: PerformedSource = None, + ): + BaseObject.__init__( + self, name=name, uids=uids, tags=tags, notes=notes, file_links=file_links + ) HasSpec.__init__(self, spec=spec) HasProperties.__init__(self, properties) HasConditions.__init__(self, conditions) @@ -100,7 +110,7 @@ def material(self, value: Union[MaterialRun, LinkByUID]): if isinstance(value, MaterialRun): value.measurements.append(self) else: - raise TypeError("material must be a MaterialRun or LinkByUID: {}".format(value)) + raise TypeError(f"material must be a MaterialRun or LinkByUID: {value}") @staticmethod def _spec_type() -> Type: diff --git a/gemd/entity/object/measurement_spec.py b/gemd/entity/object/measurement_spec.py index 6997002d..65552d00 100644 --- a/gemd/entity/object/measurement_spec.py +++ b/gemd/entity/object/measurement_spec.py @@ -1,23 +1,22 @@ -from gemd.entity.object.base_object import BaseObject -from gemd.entity.object.has_parameters import HasParameters -from gemd.entity.object.has_conditions import HasConditions -from gemd.entity.object.has_template import HasTemplate -from gemd.entity.template.measurement_template import MeasurementTemplate +from typing import Iterable, Mapping, Optional, Type, Union + from gemd.entity.attribute.condition import Condition from gemd.entity.attribute.parameter import Parameter from gemd.entity.file_link import FileLink from gemd.entity.link_by_uid import LinkByUID - -from typing import Optional, Union, Iterable, Mapping, Type +from gemd.entity.object.base_object import BaseObject +from gemd.entity.object.has_conditions import HasConditions +from gemd.entity.object.has_parameters import HasParameters +from gemd.entity.object.has_template import HasTemplate +from gemd.entity.template.measurement_template import MeasurementTemplate __all__ = ["MeasurementSpec"] -class MeasurementSpec(BaseObject, - HasTemplate, HasParameters, HasConditions, - typ="measurement_spec"): - """ - A measurement specification. +class MeasurementSpec( + BaseObject, HasTemplate, HasParameters, HasConditions, typ="measurement_spec" +): + """A measurement specification. This includes links to the conditions and parameters under which the measurement is expected to be performed. @@ -48,18 +47,21 @@ class MeasurementSpec(BaseObject, """ - def __init__(self, - name: str, - *, - template: Optional[Union[MeasurementTemplate, LinkByUID]] = None, - conditions: Union[Condition, Iterable[Condition]] = None, - parameters: Union[Parameter, Iterable[Parameter]] = None, - uids: Mapping[str, str] = None, - tags: Iterable[str] = None, - notes: str = None, - file_links: Optional[Union[Iterable[FileLink], FileLink]] = None): - BaseObject.__init__(self, name=name, uids=uids, tags=tags, notes=notes, - file_links=file_links) + def __init__( + self, + name: str, + *, + template: Optional[Union[MeasurementTemplate, LinkByUID]] = None, + conditions: Union[Condition, Iterable[Condition]] = None, + parameters: Union[Parameter, Iterable[Parameter]] = None, + uids: Mapping[str, str] = None, + tags: Iterable[str] = None, + notes: str = None, + file_links: Optional[Union[Iterable[FileLink], FileLink]] = None, + ): + BaseObject.__init__( + self, name=name, uids=uids, tags=tags, notes=notes, file_links=file_links + ) HasTemplate.__init__(self, template=template) HasParameters.__init__(self, parameters=parameters) HasConditions.__init__(self, conditions=conditions) diff --git a/gemd/entity/object/process_run.py b/gemd/entity/object/process_run.py index f1c73289..99a59852 100644 --- a/gemd/entity/object/process_run.py +++ b/gemd/entity/object/process_run.py @@ -1,28 +1,33 @@ -from gemd.entity.object.process_spec import ProcessSpec -from gemd.entity.object.base_object import BaseObject -from gemd.entity.object.has_spec import HasSpec -from gemd.entity.object.has_conditions import HasConditions -from gemd.entity.object.has_parameters import HasParameters -from gemd.entity.object.has_source import HasSource +from typing import Any, Dict, Iterable, List, Mapping, Optional, Type, TypeVar, Union + from gemd.entity.attribute.condition import Condition from gemd.entity.attribute.parameter import Parameter -from gemd.entity.source.performed_source import PerformedSource from gemd.entity.file_link import FileLink from gemd.entity.link_by_uid import LinkByUID +from gemd.entity.object.base_object import BaseObject +from gemd.entity.object.has_conditions import HasConditions +from gemd.entity.object.has_parameters import HasParameters +from gemd.entity.object.has_source import HasSource +from gemd.entity.object.has_spec import HasSpec +from gemd.entity.object.process_spec import ProcessSpec from gemd.entity.setters import validate_list - -from typing import TypeVar, Optional, Union, Iterable, List, Mapping, Dict, Type, Any +from gemd.entity.source.performed_source import PerformedSource __all__ = ["ProcessRun"] MaterialRunType = TypeVar("MaterialRunType", bound="MaterialRun") # noqa: F821 IngredientRunType = TypeVar("IngredientRunType", bound="IngredientRun") # noqa: F821 -class ProcessRun(BaseObject, - HasSpec, HasConditions, HasParameters, HasSource, - typ="process_run", skip={"_output_material", "_ingredients"}): - """ - A process run. +class ProcessRun( + BaseObject, + HasSpec, + HasConditions, + HasParameters, + HasSource, + typ="process_run", + skip={"_output_material", "_ingredients"}, +): + """A process run. Processes transform zero or more input materials into exactly one output material. This includes links to conditions and parameters under which the process was performed, @@ -55,21 +60,24 @@ class ProcessRun(BaseObject, """ - def __init__(self, - name: str, - *, - spec: Union[ProcessSpec, LinkByUID] = None, - conditions: Union[Condition, Iterable[Condition]] = None, - parameters: Union[Parameter, Iterable[Parameter]] = None, - uids: Mapping[str, str] = None, - tags: Iterable[str] = None, - notes: str = None, - file_links: Optional[Union[Iterable[FileLink], FileLink]] = None, - source: PerformedSource = None): + def __init__( + self, + name: str, + *, + spec: Union[ProcessSpec, LinkByUID] = None, + conditions: Union[Condition, Iterable[Condition]] = None, + parameters: Union[Parameter, Iterable[Parameter]] = None, + uids: Mapping[str, str] = None, + tags: Iterable[str] = None, + notes: str = None, + file_links: Optional[Union[Iterable[FileLink], FileLink]] = None, + source: PerformedSource = None, + ): from gemd.entity.object.ingredient_run import IngredientRun - BaseObject.__init__(self, name=name, uids=uids, tags=tags, notes=notes, - file_links=file_links) + BaseObject.__init__( + self, name=name, uids=uids, tags=tags, notes=notes, file_links=file_links + ) HasSpec.__init__(self, spec=spec) HasConditions.__init__(self, conditions) HasParameters.__init__(self, parameters) @@ -106,5 +114,5 @@ def _spec_type() -> Type: def _dict_for_compare(self) -> Dict[str, Any]: """Support for recursive equals.""" base = super()._dict_for_compare() - base['ingredients'] = self.ingredients + base["ingredients"] = self.ingredients return base diff --git a/gemd/entity/object/process_spec.py b/gemd/entity/object/process_spec.py index 40bcb185..b07c117d 100644 --- a/gemd/entity/object/process_spec.py +++ b/gemd/entity/object/process_spec.py @@ -1,26 +1,30 @@ -from gemd.entity.object.base_object import BaseObject -from gemd.entity.object.has_parameters import HasParameters -from gemd.entity.object.has_conditions import HasConditions -from gemd.entity.object.has_template import HasTemplate -from gemd.entity.template.process_template import ProcessTemplate +from typing import Any, Dict, Iterable, List, Mapping, Optional, Type, TypeVar, Union + from gemd.entity.attribute.condition import Condition from gemd.entity.attribute.parameter import Parameter from gemd.entity.file_link import FileLink from gemd.entity.link_by_uid import LinkByUID +from gemd.entity.object.base_object import BaseObject +from gemd.entity.object.has_conditions import HasConditions +from gemd.entity.object.has_parameters import HasParameters +from gemd.entity.object.has_template import HasTemplate from gemd.entity.setters import validate_list - -from typing import TypeVar, Optional, Union, Iterable, List, Mapping, Dict, Type, Any +from gemd.entity.template.process_template import ProcessTemplate __all__ = ["ProcessSpec"] IngredientSpecType = TypeVar("IngredientSpecType", bound="IngredientSpec") # noqa: F821 MaterialSpecType = TypeVar("MaterialSpecType", bound="MaterialSpec") # noqa: F821 -class ProcessSpec(BaseObject, - HasTemplate, HasParameters, HasConditions, - typ="process_spec", skip={"_output_material", "_ingredients"}): - """ - A process specification. +class ProcessSpec( + BaseObject, + HasTemplate, + HasParameters, + HasConditions, + typ="process_spec", + skip={"_output_material", "_ingredients"}, +): + """A process specification. Processes transform zero or more input materials into exactly one output material. This includes links to the parameters and conditions under which the process is expected @@ -51,21 +55,24 @@ class ProcessSpec(BaseObject, """ - def __init__(self, - name: str, - *, - template: Optional[Union[ProcessTemplate, LinkByUID]] = None, - conditions: Union[Condition, Iterable[Condition]] = None, - parameters: Union[Parameter, Iterable[Parameter]] = None, - uids: Mapping[str, str] = None, - tags: Iterable[str] = None, - notes: str = None, - file_links: Optional[Union[Iterable[FileLink], FileLink]] = None): - from gemd.entity.object.ingredient_spec import IngredientSpec + def __init__( + self, + name: str, + *, + template: Optional[Union[ProcessTemplate, LinkByUID]] = None, + conditions: Union[Condition, Iterable[Condition]] = None, + parameters: Union[Parameter, Iterable[Parameter]] = None, + uids: Mapping[str, str] = None, + tags: Iterable[str] = None, + notes: str = None, + file_links: Optional[Union[Iterable[FileLink], FileLink]] = None, + ): from gemd.entity.link_by_uid import LinkByUID + from gemd.entity.object.ingredient_spec import IngredientSpec - BaseObject.__init__(self, name=name, uids=uids, tags=tags, notes=notes, - file_links=file_links) + BaseObject.__init__( + self, name=name, uids=uids, tags=tags, notes=notes, file_links=file_links + ) HasTemplate.__init__(self, template=template) HasParameters.__init__(self, parameters=parameters) HasConditions.__init__(self, conditions=conditions) @@ -104,5 +111,5 @@ def output_material(self) -> Optional[MaterialSpecType]: def _dict_for_compare(self) -> Dict[str, Any]: """Support for recursive equals.""" base = super()._dict_for_compare() - base['ingredients'] = self.ingredients + base["ingredients"] = self.ingredients return base diff --git a/gemd/entity/setters.py b/gemd/entity/setters.py index 6a86e64d..bac6515d 100644 --- a/gemd/entity/setters.py +++ b/gemd/entity/setters.py @@ -1,19 +1,21 @@ """Methods for setting and validating.""" -from gemd.entity.valid_list import ValidList -from typing import Union, Iterable, Optional, Callable, Type, TypeVar +from typing import Callable, Iterable, Optional, Type, TypeVar, Union + +from gemd.entity.valid_list import ValidList __all__ = ["validate_list", "validate_str"] -T = TypeVar('T') +T = TypeVar("T") -def validate_list(obj: Optional[Union[Iterable[T], T]], - typ: Union[Iterable[Type], Type], - *, - trigger: Callable[[T], Optional[T]] = None) -> ValidList: - """ - Attempts to return obj as a list, each element of which has type typ. +def validate_list( + obj: Optional[Union[Iterable[T], T]], + typ: Union[Iterable[Type], Type], + *, + trigger: Callable[[T], Optional[T]] = None, +) -> ValidList: + """Attempts to return obj as a list, each element of which has type typ. Parameters ---------- @@ -40,8 +42,7 @@ def validate_list(obj: Optional[Union[Iterable[T], T]], def validate_str(obj) -> str: - """ - Check that obj is a string and then convert it to unicode. + """Check that obj is a string and then convert it to unicode. Parameters ---------- @@ -54,11 +55,11 @@ def validate_str(obj) -> str: `obj` as a string. Raises - ------- + ------ ValueError If `obj` is not a string. """ if not isinstance(obj, str): - raise TypeError("Expected a string but got {} instead".format(type(obj))) + raise TypeError(f"Expected a string but got {type(obj)} instead") return obj diff --git a/gemd/entity/source/performed_source.py b/gemd/entity/source/performed_source.py index 3a9823e8..0046e11c 100644 --- a/gemd/entity/source/performed_source.py +++ b/gemd/entity/source/performed_source.py @@ -4,8 +4,7 @@ class PerformedSource(DictSerializable, typ="performed_source"): - """ - Information about an activity that was performed. + """Information about an activity that was performed. Parameters ---------- diff --git a/gemd/entity/template/__init__.py b/gemd/entity/template/__init__.py index 8d751223..fdfc8515 100644 --- a/gemd/entity/template/__init__.py +++ b/gemd/entity/template/__init__.py @@ -1,4 +1,5 @@ """Attribute and Object Templates""" + # flake8: noqa from .property_template import PropertyTemplate from .condition_template import ConditionTemplate @@ -7,5 +8,11 @@ from .measurement_template import MeasurementTemplate from .process_template import ProcessTemplate -__all__ = ["PropertyTemplate", "ConditionTemplate", "ParameterTemplate", - "ProcessTemplate", "MaterialTemplate", "MeasurementTemplate"] +__all__ = [ + "PropertyTemplate", + "ConditionTemplate", + "ParameterTemplate", + "ProcessTemplate", + "MaterialTemplate", + "MeasurementTemplate", +] diff --git a/gemd/entity/template/attribute_template.py b/gemd/entity/template/attribute_template.py index c74ccd43..3f8b7842 100644 --- a/gemd/entity/template/attribute_template.py +++ b/gemd/entity/template/attribute_template.py @@ -1,4 +1,5 @@ """Attribute templates.""" + from gemd.entity.base_entity import BaseEntity from gemd.entity.bounds.base_bounds import BaseBounds @@ -6,8 +7,7 @@ class AttributeTemplate(BaseEntity): - """ - An attribute template, which can be a property, parameter, or condition template. + """An attribute template, which can be a property, parameter, or condition template. Parameters ---------- diff --git a/gemd/entity/template/base_template.py b/gemd/entity/template/base_template.py index 7620e43e..c6e9fdb3 100644 --- a/gemd/entity/template/base_template.py +++ b/gemd/entity/template/base_template.py @@ -1,17 +1,17 @@ """Base template.""" + +from typing import Iterable, Mapping, Union + from gemd.entity.base_entity import BaseEntity from gemd.entity.bounds.base_bounds import BaseBounds from gemd.entity.link_by_uid import LinkByUID from gemd.entity.template.attribute_template import AttributeTemplate -from typing import Union, Iterable, Mapping - __all__ = ["BaseTemplate"] class BaseTemplate(BaseEntity): - """ - Base class for all object templates. + """Base class for all object templates. Parameters ---------- @@ -30,32 +30,33 @@ class BaseTemplate(BaseEntity): """ - def __init__(self, - name: str, - *, - description: str = None, - uids: Mapping[str, str] = None, - tags: Iterable[str] = None): + def __init__( + self, + name: str, + *, + description: str = None, + uids: Mapping[str, str] = None, + tags: Iterable[str] = None, + ): BaseEntity.__init__(self, uids, tags) self.name = name self.description = description @staticmethod - def _homogenize_ranges(template_or_tuple: Union[AttributeTemplate, - LinkByUID, - Iterable[Union[AttributeTemplate, - BaseBounds]]]): - """ - Take either a template or pair and turn it into a (template, bounds) pair. + def _homogenize_ranges( + template_or_tuple: Union[ + AttributeTemplate, LinkByUID, Iterable[Union[AttributeTemplate, BaseBounds]] + ], + ): + """Take either a template or pair and turn it into a (template, bounds) pair. If no bounds are provided, use the attribute template's default bounds. Parameters ---------- - template_or_tuple: AttributeTemplate OR a list or - tuple [AttributeTemplate or LinkByUID, BaseBounds] - An attribute template, optionally with another Bounds object that is more - restrictive than the attribute template's default bounds. + template_or_tuple: AttributeTemplate or [AttributeTemplate or LinkByUID, BaseBounds] + An attribute template, optionally with another Bounds object that is more + restrictive than the attribute template's default bounds. Returns ------- @@ -71,8 +72,9 @@ def _homogenize_ranges(template_or_tuple: Union[AttributeTemplate, # check that the bounds is consistent with that of the template elif isinstance(template_or_tuple, (tuple, list)): first, second = template_or_tuple - if isinstance(first, (LinkByUID, AttributeTemplate)) and \ - (isinstance(second, BaseBounds) or second is None): + if isinstance(first, (LinkByUID, AttributeTemplate)) and ( + isinstance(second, BaseBounds) or second is None + ): if isinstance(first, AttributeTemplate) and isinstance(second, BaseBounds): if not first.bounds.contains(second): raise ValueError("Range and template are inconsistent") diff --git a/gemd/entity/template/has_condition_templates.py b/gemd/entity/template/has_condition_templates.py index 6714b10e..da7ae7e7 100644 --- a/gemd/entity/template/has_condition_templates.py +++ b/gemd/entity/template/has_condition_templates.py @@ -1,12 +1,13 @@ """For entities that have a condition template.""" + +from typing import Iterable, List, Optional, Set, Tuple, TypeVar, Union + +from gemd.entity.bounds.base_bounds import BaseBounds from gemd.entity.has_dependencies import HasDependencies from gemd.entity.link_by_uid import LinkByUID from gemd.entity.setters import validate_list from gemd.entity.template.base_template import BaseTemplate from gemd.entity.template.condition_template import ConditionTemplate -from gemd.entity.bounds.base_bounds import BaseBounds - -from typing import TypeVar, Optional, Union, Iterable, List, Tuple, Set __all__ = ["HasConditionTemplates"] BaseEntityType = TypeVar("BaseEntityType", bound="BaseEntity") # noqa: F821 @@ -14,8 +15,7 @@ class HasConditionTemplates(HasDependencies): - """ - Mixin-trait for entities that include condition templates. + """Mixin-trait for entities that include condition templates. Parameters ---------- @@ -25,16 +25,21 @@ class HasConditionTemplates(HasDependencies): """ - def __init__(self, conditions: Iterable[Union[Union[ConditionTemplate, LinkByUID], - Tuple[Union[ConditionTemplate, LinkByUID], - Optional[BaseBounds]]]]): + def __init__( + self, + conditions: Iterable[ + Union[ + Union[ConditionTemplate, LinkByUID], + Tuple[Union[ConditionTemplate, LinkByUID], Optional[BaseBounds]], + ] + ], + ): self._conditions = None self.conditions = conditions @property def conditions(self) -> List[Union[ConditionTemplate, LinkByUID]]: - """ - Get the list of condition template/bounds tuples. + """Get the list of condition template/bounds tuples. Returns ------- @@ -45,11 +50,16 @@ def conditions(self) -> List[Union[ConditionTemplate, LinkByUID]]: return self._conditions @conditions.setter - def conditions(self, conditions: Iterable[Union[Union[ConditionTemplate, LinkByUID], - Tuple[Union[ConditionTemplate, LinkByUID], - Optional[BaseBounds]]]]): - """ - Set the list of condition templates. + def conditions( + self, + conditions: Iterable[ + Union[ + Union[ConditionTemplate, LinkByUID], + Tuple[Union[ConditionTemplate, LinkByUID], Optional[BaseBounds]], + ] + ], + ): + """Set the list of condition templates. Parameters ---------- @@ -66,19 +76,22 @@ def conditions(self, conditions: Iterable[Union[Union[ConditionTemplate, LinkByU if isinstance(conditions, Iterable): if any(isinstance(x, BaseBounds) for x in conditions): conditions = [conditions] # It's a template/bounds tuple (probably) - self._conditions = validate_list(conditions, - (ConditionTemplate, LinkByUID, list, tuple), - trigger=BaseTemplate._homogenize_ranges - ) + self._conditions = validate_list( + conditions, + (ConditionTemplate, LinkByUID, list, tuple), + trigger=BaseTemplate._homogenize_ranges, + ) def validate_condition(self, condition: ConditionType) -> bool: """Check if the condition is consistent w/ this template.""" if condition.template is not None: - attr, bnd = next((x for x in self.conditions if condition.template == x[0]), - (None, None)) + attr, bnd = next( + (x for x in self.conditions if condition.template == x[0]), (None, None) + ) else: - attr, bnd = next((x for x in self.conditions if condition.name == x[0].name), - (None, None)) + attr, bnd = next( + (x for x in self.conditions if condition.name == x[0].name), (None, None) + ) if bnd is not None: return bnd.contains(condition.value) diff --git a/gemd/entity/template/has_parameter_templates.py b/gemd/entity/template/has_parameter_templates.py index 423c3eba..ea7b0a74 100644 --- a/gemd/entity/template/has_parameter_templates.py +++ b/gemd/entity/template/has_parameter_templates.py @@ -1,12 +1,13 @@ """For entities that have a parameter template.""" + +from typing import Iterable, List, Optional, Set, Tuple, TypeVar, Union + +from gemd.entity.bounds.base_bounds import BaseBounds from gemd.entity.has_dependencies import HasDependencies from gemd.entity.link_by_uid import LinkByUID from gemd.entity.setters import validate_list from gemd.entity.template.base_template import BaseTemplate from gemd.entity.template.parameter_template import ParameterTemplate -from gemd.entity.bounds.base_bounds import BaseBounds - -from typing import TypeVar, Optional, Union, Iterable, List, Tuple, Set __all__ = ["HasParameterTemplates"] ParameterType = TypeVar("ParameterType", bound="Parameter") # noqa: F821 @@ -14,8 +15,7 @@ class HasParameterTemplates(HasDependencies): - """ - Mixin-trait for entities that include parameter templates. + """Mixin-trait for entities that include parameter templates. Parameters ---------- @@ -25,16 +25,21 @@ class HasParameterTemplates(HasDependencies): """ - def __init__(self, parameters: Iterable[Union[Union[ParameterTemplate, LinkByUID], - Tuple[Union[ParameterTemplate, LinkByUID], - Optional[BaseBounds]]]]): + def __init__( + self, + parameters: Iterable[ + Union[ + Union[ParameterTemplate, LinkByUID], + Tuple[Union[ParameterTemplate, LinkByUID], Optional[BaseBounds]], + ] + ], + ): self._parameters = None self.parameters = parameters @property def parameters(self) -> List[Union[ParameterTemplate, LinkByUID]]: - """ - Get the list of parameter template/bounds tuples. + """Get the list of parameter template/bounds tuples. Returns ------- @@ -45,11 +50,16 @@ def parameters(self) -> List[Union[ParameterTemplate, LinkByUID]]: return self._parameters @parameters.setter - def parameters(self, parameters: Iterable[Union[Union[ParameterTemplate, LinkByUID], - Tuple[Union[ParameterTemplate, LinkByUID], - Optional[BaseBounds]]]]): - """ - Set the list of parameter templates. + def parameters( + self, + parameters: Iterable[ + Union[ + Union[ParameterTemplate, LinkByUID], + Tuple[Union[ParameterTemplate, LinkByUID], Optional[BaseBounds]], + ] + ], + ): + """Set the list of parameter templates. Parameters ---------- @@ -66,19 +76,22 @@ def parameters(self, parameters: Iterable[Union[Union[ParameterTemplate, LinkByU if isinstance(parameters, Iterable): if any(isinstance(x, BaseBounds) for x in parameters): parameters = [parameters] # It's a template/bounds tuple (probably) - self._parameters = validate_list(parameters, - (ParameterTemplate, LinkByUID, list, tuple), - trigger=BaseTemplate._homogenize_ranges - ) + self._parameters = validate_list( + parameters, + (ParameterTemplate, LinkByUID, list, tuple), + trigger=BaseTemplate._homogenize_ranges, + ) def validate_parameter(self, parameter: ParameterType) -> bool: """Check if the parameter is consistent w/ this template.""" if parameter.template is not None: - attr, bnd = next((x for x in self.parameters if parameter.template == x[0]), - (None, None)) + attr, bnd = next( + (x for x in self.parameters if parameter.template == x[0]), (None, None) + ) else: - attr, bnd = next((x for x in self.parameters if parameter.name == x[0].name), - (None, None)) + attr, bnd = next( + (x for x in self.parameters if parameter.name == x[0].name), (None, None) + ) if bnd is not None: return bnd.contains(parameter.value) diff --git a/gemd/entity/template/has_property_templates.py b/gemd/entity/template/has_property_templates.py index 0495fd2a..29d6acf1 100644 --- a/gemd/entity/template/has_property_templates.py +++ b/gemd/entity/template/has_property_templates.py @@ -1,23 +1,25 @@ """For entities that have a property template.""" + +from typing import Iterable, List, Optional, Set, Tuple, TypeVar, Union + +from gemd.entity.bounds.base_bounds import BaseBounds from gemd.entity.has_dependencies import HasDependencies from gemd.entity.link_by_uid import LinkByUID from gemd.entity.setters import validate_list from gemd.entity.template.base_template import BaseTemplate from gemd.entity.template.property_template import PropertyTemplate -from gemd.entity.bounds.base_bounds import BaseBounds - -from typing import TypeVar, Optional, Union, Iterable, List, Set, Tuple __all__ = ["HasPropertyTemplates"] BaseEntityType = TypeVar("BaseEntityType", bound="BaseEntity") # noqa: F821 PropertyType = TypeVar("PropertyType", bound="Property") # noqa: F821 -PropertyAndConditionsType = TypeVar("PropertyAndConditionsType", - bound="PropertyAndConditions") # noqa: F821 +PropertyAndConditionsType = TypeVar( + "PropertyAndConditionsType", + bound="PropertyAndConditions", # noqa: F821 +) class HasPropertyTemplates(HasDependencies): - """ - Mixin-trait for entities that include property templates. + """Mixin-trait for entities that include property templates. Parameters ---------- @@ -27,17 +29,21 @@ class HasPropertyTemplates(HasDependencies): """ - def __init__(self, properties: Iterable[Union[Union[PropertyTemplate, LinkByUID], - Tuple[Union[PropertyTemplate, LinkByUID], - Optional[BaseBounds]]]]): + def __init__( + self, + properties: Iterable[ + Union[ + Union[PropertyTemplate, LinkByUID], + Tuple[Union[PropertyTemplate, LinkByUID], Optional[BaseBounds]], + ] + ], + ): self._properties = None self.properties = properties @property - def properties(self) -> List[Tuple[Union[PropertyTemplate, LinkByUID], - Optional[BaseBounds]]]: - """ - Get the list of property template/bounds tuples. + def properties(self) -> List[Tuple[Union[PropertyTemplate, LinkByUID], Optional[BaseBounds]]]: + """Get the list of property template/bounds tuples. Returns ------- @@ -48,11 +54,16 @@ def properties(self) -> List[Tuple[Union[PropertyTemplate, LinkByUID], return self._properties @properties.setter - def properties(self, properties: Iterable[Union[Union[PropertyTemplate, LinkByUID], - Tuple[Union[PropertyTemplate, LinkByUID], - Optional[BaseBounds]]]]): - """ - Set the list of property templates. + def properties( + self, + properties: Iterable[ + Union[ + Union[PropertyTemplate, LinkByUID], + Tuple[Union[PropertyTemplate, LinkByUID], Optional[BaseBounds]], + ] + ], + ): + """Set the list of property templates. Parameters ---------- @@ -64,23 +75,23 @@ def properties(self, properties: Iterable[Union[Union[PropertyTemplate, LinkByUI if isinstance(properties, Iterable): if any(isinstance(x, BaseBounds) for x in properties): properties = [properties] # It's a template/bounds tuple (probably) - self._properties = validate_list(properties, - (PropertyTemplate, LinkByUID, list, tuple), - trigger=BaseTemplate._homogenize_ranges - ) + self._properties = validate_list( + properties, + (PropertyTemplate, LinkByUID, list, tuple), + trigger=BaseTemplate._homogenize_ranges, + ) def validate_property(self, prop: Union[PropertyType, PropertyAndConditionsType]) -> bool: """Check if the property is consistent w/ this template.""" from gemd.entity.attribute import PropertyAndConditions + if isinstance(prop, PropertyAndConditions): prop = prop.property if prop.template is not None: - attr, bnd = next((x for x in self.properties if prop.template == x[0]), - (None, None)) + attr, bnd = next((x for x in self.properties if prop.template == x[0]), (None, None)) else: - attr, bnd = next((x for x in self.properties if prop.name == x[0].name), - (None, None)) + attr, bnd = next((x for x in self.properties if prop.name == x[0].name), (None, None)) if bnd is not None: return bnd.contains(prop.value) diff --git a/gemd/entity/template/material_template.py b/gemd/entity/template/material_template.py index bf5e140a..fbbb9e97 100644 --- a/gemd/entity/template/material_template.py +++ b/gemd/entity/template/material_template.py @@ -1,4 +1,5 @@ """A material template.""" + from gemd.entity.template.base_template import BaseTemplate from gemd.entity.template.has_property_templates import HasPropertyTemplates @@ -6,8 +7,7 @@ class MaterialTemplate(BaseTemplate, HasPropertyTemplates, typ="material_template"): - """ - A material template. + """A material template. Material templates are collections of property templates that constrain the values of a material's property attributes, and provide a common structure for describing similar @@ -36,10 +36,6 @@ class MaterialTemplate(BaseTemplate, HasPropertyTemplates, typ="material_templat """ - def __init__(self, name, *, description=None, - properties=None, - uids=None, tags=None): - BaseTemplate.__init__(self, name=name, description=description, - uids=uids, tags=tags - ) + def __init__(self, name, *, description=None, properties=None, uids=None, tags=None): + BaseTemplate.__init__(self, name=name, description=description, uids=uids, tags=tags) HasPropertyTemplates.__init__(self, properties) diff --git a/gemd/entity/template/measurement_template.py b/gemd/entity/template/measurement_template.py index 23104c61..328c440b 100644 --- a/gemd/entity/template/measurement_template.py +++ b/gemd/entity/template/measurement_template.py @@ -1,4 +1,5 @@ """A measurement template.""" + from gemd.entity.template.base_template import BaseTemplate from gemd.entity.template.has_condition_templates import HasConditionTemplates from gemd.entity.template.has_parameter_templates import HasParameterTemplates @@ -7,11 +8,14 @@ __all__ = ["MeasurementTemplate"] -class MeasurementTemplate(BaseTemplate, - HasPropertyTemplates, HasConditionTemplates, HasParameterTemplates, - typ="measurement_template"): - """ - A measurement template. +class MeasurementTemplate( + BaseTemplate, + HasPropertyTemplates, + HasConditionTemplates, + HasParameterTemplates, + typ="measurement_template", +): + """A measurement template. Measurement templates are collections of condition, parameter and property templates that constrain the values of a measurement's condition, parameter and property attributes, and @@ -52,11 +56,18 @@ class MeasurementTemplate(BaseTemplate, """ - def __init__(self, name, *, description=None, - properties=None, conditions=None, parameters=None, - uids=None, tags=None): - BaseTemplate.__init__(self, name=name, description=description, - uids=uids, tags=tags) + def __init__( + self, + name, + *, + description=None, + properties=None, + conditions=None, + parameters=None, + uids=None, + tags=None, + ): + BaseTemplate.__init__(self, name=name, description=description, uids=uids, tags=tags) HasPropertyTemplates.__init__(self, properties) HasConditionTemplates.__init__(self, conditions) HasParameterTemplates.__init__(self, parameters) diff --git a/gemd/entity/template/process_template.py b/gemd/entity/template/process_template.py index 7325e6fa..b31ba523 100644 --- a/gemd/entity/template/process_template.py +++ b/gemd/entity/template/process_template.py @@ -1,4 +1,5 @@ """A process template.""" + from gemd.entity.setters import validate_list from gemd.entity.template.base_template import BaseTemplate from gemd.entity.template.has_condition_templates import HasConditionTemplates @@ -7,11 +8,10 @@ __all__ = ["ProcessTemplate"] -class ProcessTemplate(BaseTemplate, - HasConditionTemplates, HasParameterTemplates, - typ="process_template"): - """ - A process template. +class ProcessTemplate( + BaseTemplate, HasConditionTemplates, HasParameterTemplates, typ="process_template" +): + """A process template. Process templates are collections of condition and parameter templates that constrain the values of a measurement's condition and parameter attributes, and provide a common structure @@ -50,12 +50,19 @@ class ProcessTemplate(BaseTemplate, """ - def __init__(self, name, *, description=None, - conditions=None, parameters=None, - allowed_names=None, allowed_labels=None, - uids=None, tags=None): - BaseTemplate.__init__(self, name=name, description=description, - uids=uids, tags=tags) + def __init__( + self, + name, + *, + description=None, + conditions=None, + parameters=None, + allowed_names=None, + allowed_labels=None, + uids=None, + tags=None, + ): + BaseTemplate.__init__(self, name=name, description=description, uids=uids, tags=tags) HasConditionTemplates.__init__(self, conditions) HasParameterTemplates.__init__(self, parameters) diff --git a/gemd/entity/util.py b/gemd/entity/util.py index 035a3e4c..a9ced921 100644 --- a/gemd/entity/util.py +++ b/gemd/entity/util.py @@ -1,17 +1,17 @@ """Utility methods.""" -from gemd.util import recursive_foreach -from typing import List, Dict, Any +from typing import Any, Dict, List + +from gemd.util import recursive_foreach __all__ = ["make_instance", "array_like", "complete_material_history"] def make_instance(base_spec): - """ - Create a set of Run objects that mimic the connectivity of the passed Spec object. + """Create a set of Run objects that mimic the connectivity of the passed Spec object. Parameters - --------- + ---------- base_spec: BaseObject A spec instance that may point to other specs. @@ -24,41 +24,32 @@ def make_instance(base_spec): seen = dict() def crawler(spec): - from gemd.entity.object.measurement_spec import MeasurementSpec - from gemd.entity.object.measurement_run import MeasurementRun - from gemd.entity.object.material_spec import MaterialSpec - from gemd.entity.object.material_run import MaterialRun - from gemd.entity.object.ingredient_spec import IngredientSpec from gemd.entity.object.ingredient_run import IngredientRun - from gemd.entity.object.process_spec import ProcessSpec + from gemd.entity.object.ingredient_spec import IngredientSpec + from gemd.entity.object.material_run import MaterialRun + from gemd.entity.object.material_spec import MaterialSpec + from gemd.entity.object.measurement_run import MeasurementRun + from gemd.entity.object.measurement_spec import MeasurementSpec from gemd.entity.object.process_run import ProcessRun + from gemd.entity.object.process_spec import ProcessSpec if id(spec) in seen: return seen[id(spec)] if isinstance(spec, MeasurementSpec): - seen[id(spec)] = MeasurementRun( - name=spec.name, - spec=spec - ) + seen[id(spec)] = MeasurementRun(name=spec.name, spec=spec) elif isinstance(spec, MaterialSpec): - seen[id(spec)] = MaterialRun( - name=spec.name, - spec=spec - ) + seen[id(spec)] = MaterialRun(name=spec.name, spec=spec) seen[id(spec)].process = crawler(spec.process) if spec.process else None elif isinstance(spec, IngredientSpec): seen[id(spec)] = IngredientRun(spec=spec) seen[id(spec)].material = crawler(spec.material) if spec.material else None elif isinstance(spec, ProcessSpec): - seen[id(spec)] = ProcessRun( - name=spec.name, - spec=spec - ) + seen[id(spec)] = ProcessRun(name=spec.name, spec=spec) for x in spec.ingredients: crawler(x).process = seen[id(spec)] else: - raise TypeError('Passed object is not a spec-like object({})'.format(type(spec))) + raise TypeError(f"Passed object is not a spec-like object({type(spec)})") # Should we assume that the same MaterialSpec in different parts of the tree # yields the same MaterialRun? @@ -72,8 +63,7 @@ def crawler(spec): def array_like(): - """ - Figure out what kinds of list-like things we should be supporting for list type-checks. + """Figure out what kinds of list-like things we should be supporting for list type-checks. Returns ------- @@ -86,11 +76,17 @@ def array_like(): return _array_like try: import numpy as np + try: import pandas as pd - _array_like = (list, tuple, np.ndarray, - pd.core.base.PandasObject, - pd.api.extensions.ExtensionArray) + + _array_like = ( + list, + tuple, + np.ndarray, + pd.core.base.PandasObject, + pd.api.extensions.ExtensionArray, + ) except ImportError: # pragma: no cover _array_like = (list, tuple, np.ndarray) # pragma: no cover except ImportError: # pragma: no cover @@ -100,16 +96,16 @@ def array_like(): def complete_material_history(mat) -> List[Dict[str, Any]]: - """ - Get a list of every single object in the material history, all as dictionaries. + """Get a list of every single object in the material history, all as dictionaries. This is useful for testing, if we want the context list that can be used to rehydrate an entire material history. Parameters - --------- + ---------- mat: ~gemd.entity.object.material_run.MaterialRun root material run + Returns ------- list @@ -117,9 +113,10 @@ def complete_material_history(mat) -> List[Dict[str, Any]]: all links substituted. """ - from gemd.entity.base_entity import BaseEntity import json as json_builtin + import gemd.json as gemd_json + from gemd.entity.base_entity import BaseEntity from gemd.util.impl import substitute_links result = [] diff --git a/gemd/entity/valid_list.py b/gemd/entity/valid_list.py index 52647823..cec3e8a0 100644 --- a/gemd/entity/valid_list.py +++ b/gemd/entity/valid_list.py @@ -1,14 +1,14 @@ """A list that can validate its contents.""" -from typing import Optional, Union, Iterable, Callable, Type, TypeVar + +from typing import Callable, Iterable, Optional, Type, TypeVar, Union __all__ = ["ValidList"] -T = TypeVar('T') +T = TypeVar("T") class ValidList(list): - """ - A list-like class that verifies that its content conforms to specified types. + """A list-like class that verifies that its content conforms to specified types. Parameters ---------- @@ -26,22 +26,24 @@ class ValidList(list): _content_type = tuple([]) - def __init__(self, - _list: Iterable, - content_type: Optional[Union[Iterable[Type], Type]] = None, - trigger: Callable[[T], Optional[T]] = None): + def __init__( + self, + _list: Iterable, + content_type: Optional[Union[Iterable[Type], Type]] = None, + trigger: Callable[[T], Optional[T]] = None, + ): if content_type is None: content_type = tuple() if isinstance(content_type, dict): - raise TypeError('A dict is not an acceptable container for content filters') + raise TypeError("A dict is not an acceptable container for content filters") elif isinstance(content_type, Iterable): self._content_type = tuple(content_type) else: self._content_type = tuple([content_type]) for elem in self._content_type: if not isinstance(elem, type): - raise TypeError('Content filters must be types') + raise TypeError("Content filters must be types") for value in _list: self._validate(value) @@ -49,7 +51,7 @@ def __init__(self, cache = list(_list) if trigger is not None: if not callable(trigger): - raise TypeError('Triggers must be callable') + raise TypeError("Triggers must be callable") self._trigger = trigger for i, value in enumerate(_list): result = self._trigger(value) @@ -59,8 +61,7 @@ def __init__(self, list.__init__(self, cache) def _validate(self, value): - """ - Validate a value against the allowed types. + """Validate a value against the allowed types. Parameters ---------- @@ -78,12 +79,10 @@ def _validate(self, value): """ if not isinstance(value, self._content_type): - raise TypeError( - 'Value is not of an accepted type: {} =/= {}'.format(value, self._content_type)) + raise TypeError(f"Value is not of an accepted type: {value} =/= {self._content_type}") def __setitem__(self, index, value): - """ - Called to implement assignment to self[index]. + """Called to implement assignment to self[index]. Validates that `value` is one of the allowed types. @@ -108,8 +107,7 @@ def __setitem__(self, index, value): super().__setitem__(index, value) def append(self, value): - """ - Add an item to the end of the list; equivalent to a[len(a):] = [x]. + """Add an item to the end of the list; equivalent to a[len(a):] = [x]. Validates that `value` is one of the allowed types. @@ -132,8 +130,7 @@ def append(self, value): super().append(value) def extend(self, list_): - """ - Extend the list by appending all the items in the given list; equivalent to a[len(a):] = L. + """Extend the list by appending all the items in the given list; same as a[len(a):] = L. Validates that `value` is one of the allowed types. @@ -152,7 +149,7 @@ def extend(self, list_): for value in list_: self._validate(value) else: - raise TypeError("'{}' object is not iterable".format(type(list_))) + raise TypeError(f"'{type(list_)}' object is not iterable") cache = list(list_) # So that we don't edit a passed reference if self._trigger is not None: @@ -164,8 +161,7 @@ def extend(self, list_): super().extend(cache) def insert(self, i, value): - """ - Insert a value at a given position, if it is one of the allowed types. + """Insert a value at a given position, if it is one of the allowed types. a.insert(0, x) inserts at the front of the list, and a.insert(len(a), x) is equivalent to a.append(x). diff --git a/gemd/entity/value/__init__.py b/gemd/entity/value/__init__.py index 4fb20a3b..703f967d 100644 --- a/gemd/entity/value/__init__.py +++ b/gemd/entity/value/__init__.py @@ -1,4 +1,5 @@ """Value objects""" + # flake8: noqa from .nominal_real import NominalReal from .normal_real import NormalReal @@ -12,9 +13,16 @@ from .inchi_value import InChI from .smiles_value import Smiles -__all__ = ["NominalReal", "NormalReal", "UniformReal", - "NominalInteger", "UniformInteger", - "NominalCategorical", "DiscreteCategorical", - "NominalComposition", "EmpiricalFormula", - "InChI", "Smiles" - ] +__all__ = [ + "NominalReal", + "NormalReal", + "UniformReal", + "NominalInteger", + "UniformInteger", + "NominalCategorical", + "DiscreteCategorical", + "NominalComposition", + "EmpiricalFormula", + "InChI", + "Smiles", +] diff --git a/gemd/entity/value/base_value.py b/gemd/entity/value/base_value.py index b02c25f3..18d96ffc 100644 --- a/gemd/entity/value/base_value.py +++ b/gemd/entity/value/base_value.py @@ -1,15 +1,15 @@ """Base class for all values.""" -from gemd.entity.dict_serializable import DictSerializable -from gemd.entity.bounds.base_bounds import BaseBounds from abc import abstractmethod +from gemd.entity.bounds.base_bounds import BaseBounds +from gemd.entity.dict_serializable import DictSerializable + __all__ = ["BaseValue"] class BaseValue(DictSerializable): - """ - Base class for all values. + """Base class for all values. "Value" is a generic term for the information contained in an :class:`attribute `. @@ -17,8 +17,7 @@ class BaseValue(DictSerializable): @abstractmethod def _to_bounds(self) -> BaseBounds: - """ - Return the smallest bounds object that is consistent with the Value. + """Return the smallest bounds object that is consistent with the Value. Returns ------- diff --git a/gemd/entity/value/categorical_value.py b/gemd/entity/value/categorical_value.py index ddee8992..b74ed8e5 100644 --- a/gemd/entity/value/categorical_value.py +++ b/gemd/entity/value/categorical_value.py @@ -1,23 +1,22 @@ """Base class for categorical values.""" -from gemd.entity.value.base_value import BaseValue -from gemd.entity.bounds import CategoricalBounds from abc import abstractmethod +from gemd.entity.bounds import CategoricalBounds +from gemd.entity.value.base_value import BaseValue + __all__ = ["CategoricalValue"] class CategoricalValue(BaseValue): - """ - Base class for categorical values, which are distributions over valid category names. + """Base class for categorical values, which are distributions over valid category names. All category names must be in unicode. """ @abstractmethod def _to_bounds(self) -> CategoricalBounds: - """ - Return the smallest bounds object that is consistent with the Value. + """Return the smallest bounds object that is consistent with the Value. Returns ------- diff --git a/gemd/entity/value/composition_value.py b/gemd/entity/value/composition_value.py index 511528ca..381c5df8 100644 --- a/gemd/entity/value/composition_value.py +++ b/gemd/entity/value/composition_value.py @@ -1,9 +1,10 @@ """Composition of a material.""" -from gemd.entity.value.base_value import BaseValue -from gemd.entity.bounds import CompositionBounds from abc import abstractmethod +from gemd.entity.bounds import CompositionBounds +from gemd.entity.value.base_value import BaseValue + __all__ = ["CompositionValue"] @@ -12,8 +13,7 @@ class CompositionValue(BaseValue): @abstractmethod def _to_bounds(self) -> CompositionBounds: - """ - Return the smallest bounds object that is consistent with the Value. + """Return the smallest bounds object that is consistent with the Value. Returns ------- diff --git a/gemd/entity/value/continuous_value.py b/gemd/entity/value/continuous_value.py index 9bae50af..edd92dc8 100644 --- a/gemd/entity/value/continuous_value.py +++ b/gemd/entity/value/continuous_value.py @@ -1,16 +1,16 @@ """Base class for all continuous values.""" -from gemd.entity.value.base_value import BaseValue -from gemd.units import parse_units -from gemd.entity.bounds import RealBounds from abc import abstractmethod +from gemd.entity.bounds import RealBounds +from gemd.entity.value.base_value import BaseValue +from gemd.units import parse_units + __all__ = ["ContinuousValue"] class ContinuousValue(BaseValue): - """ - A base class for values that correspond to a distribution over the real numbers. + """A base class for values that correspond to a distribution over the real numbers. Parameters ---------- @@ -37,14 +37,15 @@ def units(self) -> str: @units.setter def units(self, units: str): if units is None: - raise ValueError("Continuous values must have units. " - "Use an empty string for a dimensionless quantity.") + raise ValueError( + "Continuous values must have units. " + "Use an empty string for a dimensionless quantity." + ) self._units = parse_units(units) @abstractmethod def _to_bounds(self) -> RealBounds: - """ - Return the smallest bounds object that is consistent with the Value. + """Return the smallest bounds object that is consistent with the Value. Returns ------- diff --git a/gemd/entity/value/discrete_categorical.py b/gemd/entity/value/discrete_categorical.py index 6d3c1c1b..dce0a46b 100644 --- a/gemd/entity/value/discrete_categorical.py +++ b/gemd/entity/value/discrete_categorical.py @@ -1,16 +1,16 @@ """Discrete distribution across several categories.""" -from typing import Optional, Union, Mapping +from typing import Mapping, Optional, Union + +from gemd.entity.bounds import CategoricalBounds from gemd.entity.setters import validate_str from gemd.entity.value.categorical_value import CategoricalValue -from gemd.entity.bounds import CategoricalBounds __all__ = ["DiscreteCategorical"] class DiscreteCategorical(CategoricalValue, typ="discrete_categorical"): - """ - Distribution over a discrete set of categories. + """Distribution over a discrete set of categories. Parameters ---------- @@ -49,8 +49,7 @@ def probabilities(self, probabilities: Optional[Union[str, Mapping[str, float]]] raise TypeError("probabilities must be dict or single value") def _to_bounds(self) -> CategoricalBounds: - """ - Return the smallest bounds object that is consistent with the Value. + """Return the smallest bounds object that is consistent with the Value. Returns ------- diff --git a/gemd/entity/value/empirical_formula.py b/gemd/entity/value/empirical_formula.py index fbfd9958..111656c5 100644 --- a/gemd/entity/value/empirical_formula.py +++ b/gemd/entity/value/empirical_formula.py @@ -1,8 +1,9 @@ """An empirical chemical formula.""" + import re -from gemd.entity.value.composition_value import CompositionValue from gemd.entity.bounds import CompositionBounds +from gemd.entity.value.composition_value import CompositionValue __all__ = ["EmpiricalFormula"] @@ -23,8 +24,7 @@ class EmpiricalFormula(CompositionValue, typ="empirical_formula"): - """ - An empirical chemical formula where only the relative stoichiometries matter. + """An empirical chemical formula where only the relative stoichiometries matter. Parameters ---------- @@ -46,7 +46,8 @@ def formula(self) -> str: @staticmethod def _elements(value: str): import re - return set(re.findall('[A-Z][a-z]*', value)) + + return set(re.findall("[A-Z][a-z]*", value)) @formula.setter def formula(self, value: str): @@ -55,15 +56,13 @@ def formula(self, value: str): elif isinstance(value, str): if not EmpiricalFormula._elements(value).issubset(_all_elements): unknown = sorted(EmpiricalFormula._elements(value).difference(_all_elements)) - raise ValueError('Formula {} contains unknown elements: {}' - .format(value, ' '.join(unknown))) + raise ValueError(f"Formula {value} contains unknown elements: {' '.join(unknown)}") self._formula = value else: - raise TypeError("Formula must be given as a string; got {}".format(type(value))) + raise TypeError(f"Formula must be given as a string; got {type(value)}") def _to_bounds(self) -> CompositionBounds: - """ - Return the smallest bounds object that is consistent with the Value. + """Return the smallest bounds object that is consistent with the Value. Returns ------- diff --git a/gemd/entity/value/inchi_value.py b/gemd/entity/value/inchi_value.py index 4e7ba263..75d041cc 100644 --- a/gemd/entity/value/inchi_value.py +++ b/gemd/entity/value/inchi_value.py @@ -1,13 +1,13 @@ """An empirical chemical formula.""" -from gemd.entity.value.molecular_value import MolecularValue + from gemd.entity.bounds import MolecularStructureBounds +from gemd.entity.value.molecular_value import MolecularValue __all__ = ["InChI"] class InChI(MolecularValue, typ="inchi"): - """ - A molecular structure encoded according to the IUPAC International Chemical Identifier (InChI). + """A molecular structure in IUPAC International Chemical Identifier (InChI) format. Parameters ---------- @@ -31,19 +31,18 @@ def inchi(self, value: str): if value is None: self._inchi = None elif isinstance(value, str): - if value.lower().startswith('1s/'): - value = value.replace(value[:2], 'InChI=1S') - elif not value.lower().startswith('inchi'): + if value.lower().startswith("1s/"): + value = value.replace(value[:2], "InChI=1S") + elif not value.lower().startswith("inchi"): value = f"InChI=1S/{value}" - elif not value.startswith('InChI'): - value = value.replace(value[:5], 'InChI') + elif not value.startswith("InChI"): + value = value.replace(value[:5], "InChI") self._inchi = value else: - raise TypeError("InChI must be given as a string; got {}".format(type(value))) + raise TypeError(f"InChI must be given as a string; got {type(value)}") def _to_bounds(self) -> MolecularStructureBounds: - """ - Return the smallest bounds object that is consistent with the Value. + """Return the smallest bounds object that is consistent with the Value. Returns ------- diff --git a/gemd/entity/value/integer_value.py b/gemd/entity/value/integer_value.py index 43283b56..519881f8 100644 --- a/gemd/entity/value/integer_value.py +++ b/gemd/entity/value/integer_value.py @@ -1,9 +1,10 @@ """Base class for integer values.""" -from gemd.entity.value.base_value import BaseValue -from gemd.entity.bounds import IntegerBounds from abc import abstractmethod +from gemd.entity.bounds import IntegerBounds +from gemd.entity.value.base_value import BaseValue + __all__ = ["IntegerValue"] @@ -12,8 +13,7 @@ class IntegerValue(BaseValue): @abstractmethod def _to_bounds(self) -> IntegerBounds: - """ - Return the smallest bounds object that is consistent with the Value. + """Return the smallest bounds object that is consistent with the Value. Returns ------- diff --git a/gemd/entity/value/molecular_value.py b/gemd/entity/value/molecular_value.py index 3318cea7..940e75c8 100644 --- a/gemd/entity/value/molecular_value.py +++ b/gemd/entity/value/molecular_value.py @@ -1,9 +1,10 @@ """Composition of a material.""" -from gemd.entity.value.base_value import BaseValue -from gemd.entity.bounds import MolecularStructureBounds from abc import abstractmethod +from gemd.entity.bounds import MolecularStructureBounds +from gemd.entity.value.base_value import BaseValue + __all__ = ["MolecularValue"] @@ -12,8 +13,7 @@ class MolecularValue(BaseValue): @abstractmethod def _to_bounds(self) -> MolecularStructureBounds: - """ - Return the smallest bounds object that is consistent with the Value. + """Return the smallest bounds object that is consistent with the Value. Returns ------- diff --git a/gemd/entity/value/nominal_categorical.py b/gemd/entity/value/nominal_categorical.py index 5f738619..2c4526ac 100644 --- a/gemd/entity/value/nominal_categorical.py +++ b/gemd/entity/value/nominal_categorical.py @@ -1,14 +1,14 @@ """A value that nominally is equal to a single category.""" + +from gemd.entity.bounds import CategoricalBounds from gemd.entity.setters import validate_str from gemd.entity.value.categorical_value import CategoricalValue -from gemd.entity.bounds import CategoricalBounds __all__ = ["NominalCategorical"] class NominalCategorical(CategoricalValue, typ="nominal_categorical"): - """ - A nominal category that the value is believed to have. It may not be exact. + """A nominal category that the value is believed to have. It may not be exact. Parameters ---------- @@ -34,8 +34,7 @@ def category(self, category: str): self._category = validate_str(category) def _to_bounds(self) -> CategoricalBounds: - """ - Return the smallest bounds object that is consistent with the Value. + """Return the smallest bounds object that is consistent with the Value. Returns ------- diff --git a/gemd/entity/value/nominal_composition.py b/gemd/entity/value/nominal_composition.py index ca3f891a..94111e8f 100644 --- a/gemd/entity/value/nominal_composition.py +++ b/gemd/entity/value/nominal_composition.py @@ -1,13 +1,13 @@ """A nominal composition value.""" -from gemd.entity.value.composition_value import CompositionValue + from gemd.entity.bounds import CompositionBounds +from gemd.entity.value.composition_value import CompositionValue __all__ = ["NominalComposition"] class NominalComposition(CompositionValue, typ="nominal_composition"): - """ - Nominal composition, represented as a map from the component names to the quantities. + """Nominal composition, represented as a map from the component names to the quantities. The quantities do not express an uncertainty but also do not imply that there is absolute certainty to their values. @@ -45,8 +45,7 @@ def quantities(self, quantities: dict): raise TypeError("quantities must be dict or List of two-item lists or None") def _to_bounds(self) -> CompositionBounds: - """ - Return the smallest bounds object that is consistent with the Value. + """Return the smallest bounds object that is consistent with the Value. Returns ------- diff --git a/gemd/entity/value/nominal_integer.py b/gemd/entity/value/nominal_integer.py index 5be33d5c..2adf02a8 100644 --- a/gemd/entity/value/nominal_integer.py +++ b/gemd/entity/value/nominal_integer.py @@ -1,13 +1,13 @@ """A nominal integer value.""" -from gemd.entity.value.integer_value import IntegerValue + from gemd.entity.bounds import IntegerBounds +from gemd.entity.value.integer_value import IntegerValue __all__ = ["NominalInteger"] class NominalInteger(IntegerValue, typ="nominal_integer"): - """ - Nominal integer, which does not specify an uncertainty but is not assumed to be exact. + """Nominal integer, which does not specify an uncertainty but is not assumed to be exact. Parameters ---------- @@ -30,13 +30,12 @@ def nominal(self, nominal: int) -> None: """A proscribed integer value without uncertainty.""" # This check/cast is necessary to handle JSON serialization behavior under 3.6 if not isinstance(nominal, (int, float)) or int(nominal) != nominal: - raise TypeError("nominal must be an int; got an {}({})".format(type(nominal), nominal)) + raise TypeError(f"nominal must be an int; got an {type(nominal)}({nominal})") self._nominal = int(nominal) def _to_bounds(self) -> IntegerBounds: - """ - Return the smallest bounds object that is consistent with the Value. + """Return the smallest bounds object that is consistent with the Value. Returns ------- diff --git a/gemd/entity/value/nominal_real.py b/gemd/entity/value/nominal_real.py index 3a79028d..874afcdb 100644 --- a/gemd/entity/value/nominal_real.py +++ b/gemd/entity/value/nominal_real.py @@ -1,13 +1,13 @@ """A nominal real value.""" -from gemd.entity.value.continuous_value import ContinuousValue + from gemd.entity.bounds import RealBounds +from gemd.entity.value.continuous_value import ContinuousValue __all__ = ["NominalReal"] class NominalReal(ContinuousValue, typ="nominal_real"): - """ - Nominal real, which does not specify an uncertainty but is not to be assumed exact. + """Nominal real, which does not specify an uncertainty but is not to be assumed exact. Parameters ---------- @@ -21,13 +21,11 @@ class NominalReal(ContinuousValue, typ="nominal_real"): def __init__(self, nominal=None, units=None): ContinuousValue.__init__(self, units) - assert isinstance(nominal, (int, float)), \ - "nominal value must be an int or float" + assert isinstance(nominal, (int, float)), "nominal value must be an int or float" self.nominal = float(nominal) def _to_bounds(self) -> RealBounds: - """ - Return the smallest bounds object that is consistent with the Value. + """Return the smallest bounds object that is consistent with the Value. Returns ------- @@ -36,6 +34,6 @@ def _to_bounds(self) -> RealBounds: :class:`~gemd.entity.bounds.real_bounds.RealBounds`. """ - return RealBounds(lower_bound=self.nominal, - upper_bound=self.nominal, - default_units=self.units) + return RealBounds( + lower_bound=self.nominal, upper_bound=self.nominal, default_units=self.units + ) diff --git a/gemd/entity/value/normal_real.py b/gemd/entity/value/normal_real.py index 4ce1908d..75874e00 100644 --- a/gemd/entity/value/normal_real.py +++ b/gemd/entity/value/normal_real.py @@ -1,13 +1,13 @@ """A normally distributed real value.""" -from gemd.entity.value.continuous_value import ContinuousValue + from gemd.entity.bounds import RealBounds +from gemd.entity.value.continuous_value import ContinuousValue __all__ = ["NormalReal"] class NormalReal(ContinuousValue, typ="normal_real"): - """ - Normal distribution over real numbers, parameterized by a mean and standard deviation. + """Normal distribution over real numbers, parameterized by a mean and standard deviation. Parameters ---------- @@ -27,8 +27,7 @@ def __init__(self, mean=None, std=None, units=None): self.std = std def _to_bounds(self) -> RealBounds: - """ - Return the smallest bounds object that is consistent with the Value. + """Return the smallest bounds object that is consistent with the Value. Returns ------- @@ -37,6 +36,4 @@ def _to_bounds(self) -> RealBounds: :class:`~gemd.entity.bounds.real_bounds.RealBounds`. """ - return RealBounds(lower_bound=self.mean, - upper_bound=self.mean, - default_units=self.units) + return RealBounds(lower_bound=self.mean, upper_bound=self.mean, default_units=self.units) diff --git a/gemd/entity/value/smiles_value.py b/gemd/entity/value/smiles_value.py index cfeb2b53..99249926 100644 --- a/gemd/entity/value/smiles_value.py +++ b/gemd/entity/value/smiles_value.py @@ -1,13 +1,13 @@ """An empirical chemical formula.""" -from gemd.entity.value.molecular_value import MolecularValue + from gemd.entity.bounds import MolecularStructureBounds +from gemd.entity.value.molecular_value import MolecularValue __all__ = ["Smiles"] class Smiles(MolecularValue, typ="smiles"): - """ - A molecular structure encoded according to SMILES. + """A molecular structure encoded according to SMILES. Parameters ---------- @@ -33,11 +33,10 @@ def smiles(self, value: str): elif isinstance(value, str): self._smiles = value else: - raise TypeError("SMILES must be given as a string; got {}".format(type(value))) + raise TypeError(f"SMILES must be given as a string; got {type(value)}") def _to_bounds(self) -> MolecularStructureBounds: - """ - Return the smallest bounds object that is consistent with the Value. + """Return the smallest bounds object that is consistent with the Value. Returns ------- diff --git a/gemd/entity/value/uniform_integer.py b/gemd/entity/value/uniform_integer.py index 40cdb938..add53dbd 100644 --- a/gemd/entity/value/uniform_integer.py +++ b/gemd/entity/value/uniform_integer.py @@ -1,13 +1,13 @@ """A uniformly distributed integer value.""" -from gemd.entity.value.integer_value import IntegerValue + from gemd.entity.bounds import IntegerBounds +from gemd.entity.value.integer_value import IntegerValue __all__ = ["UniformInteger"] class UniformInteger(IntegerValue, typ="uniform_integer"): - """ - Uniform integer distribution, with inclusive lower and upper bounds. + """Uniform integer distribution, with inclusive lower and upper bounds. Parameters ---------- @@ -35,13 +35,12 @@ def lower_bound(self, lower_bound: int) -> None: """The lower bound of a uniform distribution.""" # This check/cast is necessary to handle JSON serialization behavior under 3.6 if not isinstance(lower_bound, (int, float)) or int(lower_bound) != lower_bound: - raise TypeError( - "lower_bound must be an int; got {}({})".format(type(lower_bound), lower_bound)) + raise TypeError(f"lower_bound must be an int; got {type(lower_bound)}({lower_bound})") if self._upper_bound is not None: if lower_bound > self.upper_bound: raise ValueError( - "lower_bound ({}) must be <= upper_bound ({})".format(lower_bound, - self.upper_bound)) + f"lower_bound ({lower_bound}) must be <= upper_bound ({self.upper_bound})" + ) self._lower_bound = int(lower_bound) @property @@ -54,17 +53,15 @@ def upper_bound(self, upper_bound: int) -> None: """The upper bound of a uniform distribution.""" # This check/cast is necessary to handle JSON serialization behavior under 3.6 if not isinstance(upper_bound, (int, float)) or int(upper_bound) != upper_bound: - raise TypeError( - "upper_bound must be an int; got {}({})".format(type(upper_bound), upper_bound)) + raise TypeError(f"upper_bound must be an int; got {type(upper_bound)}({upper_bound})") if self.lower_bound > upper_bound: raise ValueError( - "upper_bound ({}) must be >= lower_bound ({})".format(upper_bound, - self.lower_bound)) + f"upper_bound ({upper_bound}) must be >= lower_bound ({self.lower_bound})" + ) self._upper_bound = int(upper_bound) def _to_bounds(self) -> IntegerBounds: - """ - Return the smallest bounds object that is consistent with the Value. + """Return the smallest bounds object that is consistent with the Value. Returns ------- diff --git a/gemd/entity/value/uniform_real.py b/gemd/entity/value/uniform_real.py index 757c68d0..b882bdeb 100644 --- a/gemd/entity/value/uniform_real.py +++ b/gemd/entity/value/uniform_real.py @@ -1,13 +1,13 @@ """A uniformly distributed real value.""" -from gemd.entity.value.continuous_value import ContinuousValue + from gemd.entity.bounds import RealBounds +from gemd.entity.value.continuous_value import ContinuousValue __all__ = ["UniformReal"] class UniformReal(ContinuousValue, typ="uniform_real"): - """ - Uniform continuous distribution, with inclusive lower and upper bounds. + """Uniform continuous distribution, with inclusive lower and upper bounds. Note ---- @@ -31,12 +31,10 @@ def __init__(self, lower_bound=None, upper_bound=None, units=None): ContinuousValue.__init__(self, units) self.lower_bound = lower_bound self.upper_bound = upper_bound - assert lower_bound <= upper_bound, \ - "the lower bound must be <= the upper bound" + assert lower_bound <= upper_bound, "the lower bound must be <= the upper bound" def _to_bounds(self) -> RealBounds: - """ - Return the smallest bounds object that is consistent with the Value. + """Return the smallest bounds object that is consistent with the Value. Returns ------- @@ -45,6 +43,6 @@ def _to_bounds(self) -> RealBounds: :class:`~gemd.entity.bounds.real_bounds.RealBounds`. """ - return RealBounds(lower_bound=self.lower_bound, - upper_bound=self.upper_bound, - default_units=self.units) + return RealBounds( + lower_bound=self.lower_bound, upper_bound=self.upper_bound, default_units=self.units + ) diff --git a/gemd/enumeration/base_enumeration.py b/gemd/enumeration/base_enumeration.py index f751cad2..224bfd14 100644 --- a/gemd/enumeration/base_enumeration.py +++ b/gemd/enumeration/base_enumeration.py @@ -1,9 +1,11 @@ """Base class for all enumerations.""" -from deprecation import deprecated + from enum import Enum -from typing import Optional, Type, Callable +from typing import Callable, Optional, Type from warnings import warn +from deprecation import deprecated + __all__ = ["BaseEnumeration"] @@ -29,9 +31,9 @@ class BaseEnumeration(str, Enum): def __new__(cls, value: str, *args): """Overloaded to allow for synonyms.""" if any(not isinstance(x, str) for x in (value,) + args): - raise ValueError("All values of enum {} must be strings".format(cls)) + raise ValueError(f"All values of enum {cls} must be strings") if cls.from_str(value, exception=False) is not None: - raise ValueError("Duplicates not allowed in enumerated set of values {}".format(cls)) + raise ValueError(f"Duplicates not allowed in enumerated set of values {cls}") obj = str.__new__(cls, value) obj._value_ = value obj.synonyms = frozenset(args) @@ -40,8 +42,7 @@ def __new__(cls, value: str, *args): @classmethod def from_str(cls, val: str, *, exception: bool = False) -> Optional["BaseEnumeration"]: - """ - Given a string value, return the Enumeration object that matches. + """Given a string value, return the Enumeration object that matches. Parameters ---------- @@ -78,13 +79,10 @@ def _missing_(cls, value: object) -> Optional["BaseEnumeration"]: return None -def migrated_enum(*, - old_value: str, - new_value: str, - deprecated_in: str, - removed_in: str) -> Callable[[Type], Type]: - """ - Decorator for registering an enumerated value as migrated to a new symbol. +def migrated_enum( + *, old_value: str, new_value: str, deprecated_in: str, removed_in: str +) -> Callable[[Type], Type]: + """Decorator for registering an enumerated value as migrated to a new symbol. Parameters ---------- @@ -99,6 +97,7 @@ def migrated_enum(*, The version of the library the old enumerated value will be removed in. """ + def decorator(cls) -> Type: print("Sear") @@ -111,7 +110,7 @@ def __getitem__(cls, name): f"{old_value} is deprecated as of {deprecated_in} " f"and will be removed in {removed_in}. " f"{old_value} has been renamed to {cls(new_value).name}.", - DeprecationWarning + DeprecationWarning, ) return cls(new_value) else: @@ -122,10 +121,11 @@ def accessor(self): return cls(new_value) accessor.__name__ = old_value # So deprecated knows the correct target name - deprecator = deprecated(deprecated_in=deprecated_in, - removed_in=removed_in, - details=f"{old_value} has been renamed to {cls(new_value).name}.", - ) + deprecator = deprecated( + deprecated_in=deprecated_in, + removed_in=removed_in, + details=f"{old_value} has been renamed to {cls(new_value).name}.", + ) # Add the property to the metaclass, and then update cls' meta setattr(MixinMeta, old_value, property(deprecator(accessor))) diff --git a/gemd/enumeration/origin.py b/gemd/enumeration/origin.py index c6817275..af19ec08 100644 --- a/gemd/enumeration/origin.py +++ b/gemd/enumeration/origin.py @@ -1,4 +1,5 @@ """All possible origins of an attribute.""" + from gemd.enumeration.base_enumeration import BaseEnumeration __all__ = ["Origin"] diff --git a/gemd/enumeration/sample_type.py b/gemd/enumeration/sample_type.py index b8193f0d..a477c748 100644 --- a/gemd/enumeration/sample_type.py +++ b/gemd/enumeration/sample_type.py @@ -1,4 +1,5 @@ """All possible types of samples.""" + from gemd.enumeration.base_enumeration import BaseEnumeration __all__ = ["SampleType"] diff --git a/gemd/json/__init__.py b/gemd/json/__init__.py index 608536ec..6ac15d43 100644 --- a/gemd/json/__init__.py +++ b/gemd/json/__init__.py @@ -19,17 +19,13 @@ from .gemd_encoder import GEMDEncoder # noqa: F401 from .gemd_json import GEMDJson -__all__ = [ - "GEMDEncoder", "GEMDJson", - "loads", "dumps", "load", "dump" -] +__all__ = ["GEMDEncoder", "GEMDJson", "loads", "dumps", "load", "dump"] __default = GEMDJson() def loads(json_str, **kwargs): - """ - Deserialize a json-formatted string into a gemd object. + """Deserialize a json-formatted string into a gemd object. Parameters ---------- @@ -49,8 +45,7 @@ def loads(json_str, **kwargs): def dumps(obj, **kwargs): - """ - Serialize a gemd object, or container of them, into a json-formatting string. + """Serialize a gemd object, or container of them, into a json-formatting string. Parameters ---------- @@ -69,8 +64,7 @@ def dumps(obj, **kwargs): def load(fp, **kwargs): - """ - Load serialized string representation of an object from a file. + """Load serialized string representation of an object from a file. Parameters ---------- @@ -89,8 +83,7 @@ def load(fp, **kwargs): def dump(obj, fp, **kwargs): - """ - Dump an object to a file, as a serialized string. + """Dump an object to a file, as a serialized string. Parameters ---------- diff --git a/gemd/json/gemd_json.py b/gemd/json/gemd_json.py index 1e2e0d60..acbb01d2 100644 --- a/gemd/json/gemd_json.py +++ b/gemd/json/gemd_json.py @@ -1,18 +1,17 @@ import json -from typing import Dict, Any, Type +from typing import Any, Dict, Type -from gemd.entity.dict_serializable import DictSerializable from gemd.entity.base_entity import BaseEntity +from gemd.entity.dict_serializable import DictSerializable from gemd.entity.link_by_uid import LinkByUID from gemd.json import GEMDEncoder -from gemd.util import flatten, substitute_links, set_uuids +from gemd.util import flatten, set_uuids, substitute_links __all__ = ["GEMDJson"] class GEMDJson(object): - """ - Class that provides json load/dump functionality that is compatible with gemd objects. + """Class that provides json load/dump functionality that is compatible with gemd objects. The serialization and deserialization strategy implemented by this class is described in :ref:`Serialization In Depth` @@ -20,7 +19,7 @@ class GEMDJson(object): scope: defines the scope to use for autogenerated UUIDs for objects without uids """ - def __init__(self, scope: str = 'auto'): + def __init__(self, scope: str = "auto"): self._scope = scope self._clazz_index = dict() @@ -30,8 +29,7 @@ def scope(self) -> str: return self._scope def dumps(self, obj, **kwargs) -> str: - """ - Serialize a gemd object, or container of them, into a json-formatting string. + """Serialize a gemd object, or container of them, into a json-formatting string. Parameters ---------- @@ -55,8 +53,7 @@ def dumps(self, obj, **kwargs) -> str: return json.dumps(res, cls=GEMDEncoder, sort_keys=True, **kwargs) def loads(self, json_str: str, **kwargs): - """ - Deserialize a json-formatted string into a gemd object. + """Deserialize a json-formatted string into a gemd object. Parameters ---------- @@ -79,17 +76,16 @@ def loads(self, json_str: str, **kwargs): clazz_index.update(self._clazz_index) raw = json.loads( json_str, - object_hook=lambda x: self._load_and_index(x, - index, - clazz_index=clazz_index, - substitute=True), - **kwargs) + object_hook=lambda x: self._load_and_index( + x, index, clazz_index=clazz_index, substitute=True + ), + **kwargs, + ) # the return value is in the 2nd position. return raw["object"] def load(self, fp, **kwargs): - """ - Load serialized string representation of an object from a file. + """Load serialized string representation of an object from a file. Parameters ---------- @@ -107,8 +103,7 @@ def load(self, fp, **kwargs): return self.loads(fp.read(), **kwargs) def dump(self, obj, fp, **kwargs): - """ - Dump an object to a file, as a serialized string. + """Dump an object to a file, as a serialized string. Parameters ---------- @@ -128,8 +123,7 @@ def dump(self, obj, fp, **kwargs): return def copy(self, obj): - """ - Copy an object by dumping and then loading it. + """Copy an object by dumping and then loading it. Parameters ---------- @@ -145,8 +139,7 @@ def copy(self, obj): return self.loads(self.dumps(obj)) def raw_dumps(self, obj, **kwargs): - """ - Serialize the object as-is, which could be as a nested object. + """Serialize the object as-is, which could be as a nested object. Parameters ---------- @@ -164,8 +157,7 @@ def raw_dumps(self, obj, **kwargs): return json.dumps(obj, cls=GEMDEncoder, sort_keys=True, **kwargs) def thin_dumps(self, obj, **kwargs): - """ - Serialize a "thin" version of an object in which pointers are replaced by links. + """Serialize a "thin" version of an object in which pointers are replaced by links. Parameters ---------- @@ -185,8 +177,7 @@ def thin_dumps(self, obj, **kwargs): return json.dumps(res, cls=GEMDEncoder, sort_keys=True, **kwargs) def raw_loads(self, json_str, **kwargs): - """ - Deserialize a json-formatted string with no context into a gemd object as-is. + """Deserialize a json-formatted string with no context into a gemd object as-is. Parameters ---------- @@ -209,16 +200,17 @@ def raw_loads(self, json_str, **kwargs): return json.loads( json_str, object_hook=lambda x: self._load_and_index(x, index, clazz_index=clazz_index), - **kwargs) + **kwargs, + ) @staticmethod def _load_and_index( - d: Dict[str, Any], - object_index: Dict[str, DictSerializable], - clazz_index: Dict[str, Type], - substitute: bool = False) -> DictSerializable: - """ - Load the class based on the type string and index it, if a BaseEntity. + d: Dict[str, Any], + object_index: Dict[str, DictSerializable], + clazz_index: Dict[str, Type], + substitute: bool = False, + ) -> DictSerializable: + """Load the class based on the type string and index it, if a BaseEntity. This function is used as the object hook when deserializing gemd objects @@ -228,6 +220,8 @@ def _load_and_index( dictionary to try to load into a registered class instance object_index: dict to add the object to if it is a BaseEntity + clazz_index: dict + maps each type string to the class registered for it substitute: bool whether to substitute LinkByUIDs when they are found in the index @@ -242,13 +236,13 @@ def _load_and_index( typ = d.pop("type") if typ not in clazz_index: - raise TypeError("Unexpected base object type: {}".format(typ)) + raise TypeError(f"Unexpected base object type: {typ}") clz = clazz_index[typ] obj = clz.from_dict(d) if isinstance(obj, BaseEntity): # Add it to the object index - for (scope, uid) in obj.uids.items(): + for scope, uid in obj.uids.items(): object_index[(scope.lower(), uid)] = obj if substitute and issubclass(clz, LinkByUID): # sub it if possible diff --git a/gemd/units/__init__.py b/gemd/units/__init__.py index 5b8ce3c8..fc4bd89b 100644 --- a/gemd/units/__init__.py +++ b/gemd/units/__init__.py @@ -1,8 +1,20 @@ # flake8: noqa -from .impl import parse_units, convert_units, get_base_units, change_definitions_file, \ - UndefinedUnitError, IncompatibleUnitsError, DefinitionSyntaxError +from .impl import ( + parse_units, + convert_units, + get_base_units, + change_definitions_file, + UndefinedUnitError, + IncompatibleUnitsError, + DefinitionSyntaxError, +) __all__ = [ - "parse_units", "convert_units", "get_base_units", "change_definitions_file", - "UndefinedUnitError", "IncompatibleUnitsError", "DefinitionSyntaxError" + "parse_units", + "convert_units", + "get_base_units", + "change_definitions_file", + "UndefinedUnitError", + "IncompatibleUnitsError", + "DefinitionSyntaxError", ] diff --git a/gemd/units/impl.py b/gemd/units/impl.py index 0815d9cc..3bcd789c 100644 --- a/gemd/units/impl.py +++ b/gemd/units/impl.py @@ -1,32 +1,42 @@ """Implementation of units.""" -from deprecation import deprecated + import functools -from importlib.resources import files import os -from pathlib import Path import re +from importlib.resources import files +from pathlib import Path from tempfile import TemporaryDirectory -from typing import Union, List, Tuple, Generator, Any +from typing import Any, Generator, List, Tuple, Union + +from deprecation import deprecated + try: from typing import TypeAlias # Python 3.10+ except ImportError: # pragma nocover from typing_extensions import TypeAlias # Python 3.9 +from tokenize import ERRORTOKEN, NAME, NUMBER, OP, TokenInfo + from pint import UnitRegistry, register_unit_format -from pint.pint_eval import tokenizer -from tokenize import NAME, NUMBER, OP, ERRORTOKEN, TokenInfo +from pint.errors import DefinitionSyntaxError, UndefinedUnitError + # alias the error that is thrown when units are incompatible # this helps to isolate the dependence on pint from pint.errors import DimensionalityError as IncompatibleUnitsError -from pint.errors import UndefinedUnitError, DefinitionSyntaxError +from pint.pint_eval import tokenizer from pint.registry import GenericUnitRegistry # Store directories so they don't get auto-cleaned until exit _TEMP_DIRECTORY = TemporaryDirectory() __all__ = [ - "parse_units", "convert_units", "get_base_units", "change_definitions_file", - "UndefinedUnitError", "IncompatibleUnitsError", "DefinitionSyntaxError" + "parse_units", + "convert_units", + "get_base_units", + "change_definitions_file", + "UndefinedUnitError", + "IncompatibleUnitsError", + "DefinitionSyntaxError", ] @@ -48,21 +58,22 @@ def _deploy_default_files() -> Tuple[Path, Path]: def _scientific_notation_preprocessor(input_string: str) -> str: """Preprocessor that converts x * 10 ** y format to xEy.""" + def _as_scientific(matchobj: re.Match) -> str: return f"{matchobj.group(1) or '1'}e{matchobj.group(2)}" - number = r'\b(?:(\d+\.?\d*|\.\d+)\s*\*\s*)?10\s*(?:\*{2}|\^)\s*\+?(-?\d+\b)' + number = r"\b(?:(\d+\.?\d*|\.\d+)\s*\*\s*)?10\s*(?:\*{2}|\^)\s*\+?(-?\d+\b)" return re.sub(number, _as_scientific, input_string) def _scaling_find_blocks(token_stream: Generator[TokenInfo, Any, None]) -> List[List[TokenInfo]]: - """ - Supporting routine for _scaling_preprocessor; tokenizer stream -> blocks. + """Supporting routine for _scaling_preprocessor; tokenizer stream -> blocks. Takes a stream of tokens, and breaks it into a lists of tokens that represent multiplicative subunits of the original expression. """ + def _handle_operator(token_, exponent_context_, operator_stack_, result_): if token_.string not in _ALLOWED_OPERATORS: raise UndefinedUnitError(f"Unrecognized operator: {token_.string}") @@ -75,11 +86,11 @@ def _handle_operator(token_, exponent_context_, operator_stack_, result_): result_.append([]) # Manage the operator stack - if token_.string == '(': + if token_.string == "(": operator_stack_.append(token_) - elif token_.string == ')': + elif token_.string == ")": while operator_stack_: # don't worry about enforcing balance - if operator_stack_.pop().string == '(': + if operator_stack_.pop().string == "(": break # We found token's friend elif token_.string in {"**", "^"}: # A spare to pop so next loop is in exponent context @@ -117,11 +128,9 @@ def _do_nothing(token_, exponent_context_, operator_stack_, result_): def _scaling_identify_factors( - input_string: str, - blocks: List[List[TokenInfo]] + input_string: str, blocks: List[List[TokenInfo]] ) -> List[Tuple[str, str, str]]: - """ - Supporting routine for _scaling_preprocessor; blocks -> scaling terms. + """Supporting routine for _scaling_preprocessor; blocks -> scaling terms. Takes the input_string and the blocks output by _scaling_find_blocks and returns a tuple of the substrings that contain scaling factors, the scaling @@ -144,11 +153,15 @@ def _scaling_identify_factors( if i_name is not None and i_name < position: raise ValueError(f"Scaling factor ({value}) follows unit in {input_string}") if float(value) != 1.0 and float(value) != 0.0: # Don't create definitions for 0 or 1 - block_string = input_string[block[0].start[1]:block[-1].end[1]] + block_start = block[0].start[1] + block_end = block[-1].end[1] + block_string = input_string[block_start:block_end] if i_name is None: unit_string = None else: - unit_string = input_string[block[position + 1].start[1]:block[i_name].end[1]] + unit_start = block[position + 1].start[1] + unit_end = block[i_name].end[1] + unit_string = input_string[unit_start:unit_end] todo.append((block_string, value, unit_string)) elif len(numbers) > 1: raise ValueError( @@ -159,8 +172,7 @@ def _scaling_identify_factors( def _scaling_store_and_mangle(input_string: str, todo: List[Tuple[str, str, str]]) -> str: - """ - Supporting routine for _scaling_preprocessor; scaling terms -> updated input_string. + """Supporting routine for _scaling_preprocessor; scaling terms -> updated input_string. Takes the terms to be updated, and actually updates the input_string as well as creating an entry for each in the registry. @@ -207,13 +219,13 @@ def _scaling_preprocessor(input_string: str) -> str: def _unmangle_scaling(input_string: str) -> str: """Convert mangled scaling values into a pint-compatible expression.""" - number_re = r'\b_(_)?(\d+)(_\d+)?([eE]_?\d+)?(_(?=[a-zA-Z]))?' + number_re = r"\b_(_)?(\d+)(_\d+)?([eE]_?\d+)?(_(?=[a-zA-Z]))?" while match := re.search(number_re, input_string): - replacement = '' if match.group(1) is None else '-' + replacement = "" if match.group(1) is None else "-" replacement += match.group(2) - replacement += '' if match.group(3) is None else match.group(3).replace('_', '.') - replacement += '' if match.group(4) is None else match.group(4).replace('_', '-') - replacement += '' if match.group(5) is None else match.group(5).replace('_', ' ') + replacement += "" if match.group(3) is None else match.group(3).replace("_", ".") + replacement += "" if match.group(4) is None else match.group(4).replace("_", "-") + replacement += "" if match.group(5) is None else match.group(5).replace("_", " ") input_string = input_string.replace(match.group(0), replacement) return input_string @@ -221,6 +233,7 @@ def _unmangle_scaling(input_string: str) -> str: # Standard approach to creating a custom registry class: # https://pint.readthedocs.io/en/0.23/advanced/custom-registry-class.html + class _ScaleFactorUnit(UnitRegistry.Unit): """Child class of Units for generating units w/ clean scaling factors.""" @@ -247,8 +260,7 @@ class _ScaleFactorRegistry(GenericUnitRegistry[_ScaleFactorQuantity, _ScaleFacto @functools.lru_cache(maxsize=1024 * 1024) def convert_units(value: float, starting_unit: str, final_unit: str) -> float: - """ - Convert the value from the starting_unit to the final_unit. + """Convert the value from the starting_unit to the final_unit. Parameters ---------- @@ -279,7 +291,7 @@ def convert_units(value: float, starting_unit: str, final_unit: str) -> float: units1=resolved_value.units, dim1=_REGISTRY.get_dimensionality(resolved_final_unit), units2=final_unit, - dim2=_REGISTRY.get_dimensionality(resolved_final_unit) + dim2=_REGISTRY.get_dimensionality(resolved_final_unit), ) return resolved_value.to(resolved_final_unit).magnitude @@ -287,17 +299,18 @@ def convert_units(value: float, starting_unit: str, final_unit: str) -> float: @register_unit_format("clean") @deprecated(deprecated_in="2.1.0", removed_in="3.0.0", details="Scaling factor clean-up ") def _format_clean(unit, registry, **options): - """ - DEPRECATED Formatter that turns scaling-factor-units into numbers again. + """DEPRECATED Formatter that turns scaling-factor-units into numbers again. Responsibility for this piece of clean-up has been shifted to a custom class. """ try: # Informal route changed in 0.22 from pint.formatting import _FORMATTERS + formatter = _FORMATTERS["D"] # pragma: no cover except ImportError: # pragma: no cover from pint import Unit + formatter_obj = registry.formatter._formatters["D"] def _surrogate_formatter(unit, registry, **options): @@ -313,12 +326,10 @@ def _surrogate_formatter(unit, registry, **options): @functools.lru_cache(maxsize=1024) -def parse_units(units: Union[str, UnitRegistry.Unit, None], - *, - return_unit: bool = False - ) -> Union[str, UnitRegistry.Unit, None]: - """ - Parse a string or Unit into a standard string representation of the unit. +def parse_units( + units: Union[str, UnitRegistry.Unit, None], *, return_unit: bool = False +) -> Union[str, UnitRegistry.Unit, None]: + """Parse a string or Unit into a standard string representation of the unit. Parameters ---------- @@ -353,8 +364,7 @@ def parse_units(units: Union[str, UnitRegistry.Unit, None], @functools.lru_cache(maxsize=1024) def get_base_units(units: Union[str, UnitRegistry.Unit]) -> Tuple[UnitRegistry.Unit, float, float]: - """ - Get the base units and conversion factors for the given unit. + """Get the base units and conversion factors for the given unit. Parameters ---------- @@ -376,8 +386,7 @@ def get_base_units(units: Union[str, UnitRegistry.Unit]) -> Tuple[UnitRegistry.U def change_definitions_file(filename: str = None): - """ - Change which file is used for units definition. + """Change which file is used for units definition. Parameters ---------- @@ -399,12 +408,11 @@ def change_definitions_file(filename: str = None): os.chdir(target.parent) # Need to re-verify path because of some slippiness around tmp on macOS updated = (Path.cwd() / target.name).resolve(strict=True) - _REGISTRY = _ScaleFactorRegistry(filename=updated, - preprocessors=[_scientific_notation_preprocessor, - _scaling_preprocessor - ], - autoconvert_offset_to_baseunit=True - ) + _REGISTRY = _ScaleFactorRegistry( + filename=updated, + preprocessors=[_scientific_notation_preprocessor, _scaling_preprocessor], + autoconvert_offset_to_baseunit=True, + ) finally: os.chdir(current_dir) diff --git a/gemd/util/__init__.py b/gemd/util/__init__.py index d0105f36..20c0904d 100644 --- a/gemd/util/__init__.py +++ b/gemd/util/__init__.py @@ -1,8 +1,24 @@ # flake8: noqa -from .impl import set_uuids, cached_isinstance, make_index, substitute_links, \ - substitute_objects, flatten, recursive_foreach, recursive_flatmap, \ - writable_sort_order +from .impl import ( + set_uuids, + cached_isinstance, + make_index, + substitute_links, + substitute_objects, + flatten, + recursive_foreach, + recursive_flatmap, + writable_sort_order, +) -__all__ = ["set_uuids", "cached_isinstance", "make_index", "substitute_links", - "substitute_objects", "flatten", "recursive_foreach", "recursive_flatmap", - "writable_sort_order"] +__all__ = [ + "set_uuids", + "cached_isinstance", + "make_index", + "substitute_links", + "substitute_objects", + "flatten", + "recursive_foreach", + "recursive_flatmap", + "writable_sort_order", +] diff --git a/gemd/util/impl.py b/gemd/util/impl.py index 3e808a8c..590f2359 100644 --- a/gemd/util/impl.py +++ b/gemd/util/impl.py @@ -1,8 +1,21 @@ """Utility functions.""" -import uuid + import functools -from typing import Optional, Union, Type, Iterable, MutableSequence, List, Tuple, Mapping, \ - Callable, Any, Reversible, ByteString +import uuid +from typing import ( + Any, + ByteString, + Callable, + Iterable, + List, + Mapping, + MutableSequence, + Optional, + Reversible, + Tuple, + Type, + Union, +) from gemd.entity.base_entity import BaseEntity from gemd.entity.dict_serializable import DictSerializable @@ -10,8 +23,7 @@ def set_uuids(obj, scope): - """ - Recursively assign a uuid to every BaseEntity that doesn't already contain a uuid. + """Recursively assign a uuid to every BaseEntity that doesn't already contain a uuid. This ensures that all of the pointers in the object can be replaced with LinkByUID objects @@ -27,24 +39,23 @@ def set_uuids(obj, scope): None """ + def func(base_obj): if len(base_obj.uids) == 0: base_obj.add_uid(scope, str(uuid.uuid4())) return + recursive_foreach(obj, func) return -def cached_isinstance( - obj: object, - class_or_tuple: Union[Type, Tuple[Type]]) -> bool: - """ - Emulate isinstance builtin to take advantage of functools caching. +def cached_isinstance(obj: object, class_or_tuple: Union[Type, Tuple[Type]]) -> bool: + """Emulate isinstance builtin to take advantage of functools caching. Parameters ---------- obj: object - + The object to check. class_or_tuple: Type or Tuple[Type] A single type, a tuple of types (potentially nested) @@ -63,11 +74,8 @@ def cached_isinstance( @functools.lru_cache(maxsize=1024) -def _cached_issubclass( - cls: Type, - class_or_tuple: Union[Type, Tuple[Type]]) -> bool: - """ - Emulate issubclass builtin to take advantage of functools caching. +def _cached_issubclass(cls: Type, class_or_tuple: Union[Type, Tuple[Type]]) -> bool: + """Emulate issubclass builtin to take advantage of functools caching. Parameters ---------- @@ -85,12 +93,13 @@ def _cached_issubclass( return issubclass(cls, class_or_tuple) -def _substitute(thing: Any, - sub: Callable[[object], object], - applies: Callable[[object], bool], - visited: Mapping[object, object] = None) -> object: - """ - Generic recursive substitute function. +def _substitute( + thing: Any, + sub: Callable[[object], object], + applies: Callable[[object], bool], + visited: Mapping[object, object] = None, +) -> object: + """Generic recursive substitute function. Generates a new instance of thing by traversing its contents recursively, substituting values for which the sub function applies. @@ -103,6 +112,8 @@ def _substitute(thing: Any, Function which provides substitute for value; should not have side effects. applies: Callable[[object], bool] Function which defines the domain for the sub function to be invoked. + visited: Mapping[object, object], optional + Maps each object already substituted to its replacement. """ if visited is None: @@ -122,11 +133,15 @@ def _substitute(thing: Any, elif cached_isinstance(replacement, Tuple): new = tuple(_substitute(x, sub, applies, visited) for x in replacement) elif cached_isinstance(replacement, Mapping): - new = {_substitute(k, sub, applies, visited): _substitute(v, sub, applies, visited) - for k, v in replacement.items()} + new = { + _substitute(k, sub, applies, visited): _substitute(v, sub, applies, visited) + for k, v in replacement.items() + } elif cached_isinstance(replacement, DictSerializable): - new_attrs = {_substitute(k, sub, applies, visited): _substitute(v, sub, applies, visited) - for k, v in replacement.as_dict().items()} + new_attrs = { + _substitute(k, sub, applies, visited): _substitute(v, sub, applies, visited) + for k, v in replacement.as_dict().items() + } new = replacement.build(new_attrs) else: new = replacement @@ -137,12 +152,13 @@ def _substitute(thing: Any, return new -def _substitute_inplace(thing: Any, - sub: Callable[[object], object], - applies: Callable[[object], bool], - visited: Mapping[object, object] = None) -> object: - """ - Generic recursive in-place substitute function. +def _substitute_inplace( + thing: Any, + sub: Callable[[object], object], + applies: Callable[[object], bool], + visited: Mapping[object, object] = None, +) -> object: + """Generic recursive in-place substitute function. Iteratively crawls the passed structure, substituting elements with sub(element) when applies(element) is true and the element is mutable. @@ -155,8 +171,11 @@ def _substitute_inplace(thing: Any, Function which provides substitute for value; should not have side effects. applies: Callable[[object], bool] Function which defines the domain for the sub function to be invoked. + visited: Mapping[object, object], optional + Maps each object already substituted to its replacement. """ + def _key(obj): if cached_isinstance(obj, (float, int, str)): return None @@ -209,8 +228,7 @@ def _key(obj): @functools.lru_cache(maxsize=1024) def _setter_by_attribute(clazz: type, attribute: str) -> Callable: - """ - Internal method to get the setter method for an attribute. + """Internal method to get the setter method for an attribute. Note that if the attribute in question is a @property (read-only attribute), it assumes that the correct choice is just setting the field name with a @@ -229,6 +247,7 @@ def _setter_by_attribute(clazz: type, attribute: str) -> Callable: The attribute's setter method, callable w/ setter(object, value). """ + def _emulator(inner_name: str) -> Callable: return lambda self, value: setattr(self, inner_name, value) @@ -244,8 +263,7 @@ def _emulator(inner_name: str) -> Callable: def make_index(obj: Union[Iterable, BaseEntity, DictSerializable]): - """ - Generates an index that can be used for the substitute_objects method. + """Generates an index that can be used for the substitute_objects method. This method builds a dictionary of GEMD objects found by recursively crawling the passed object, indexed by all scope:id tuples found in any of the objects. The passed object can @@ -257,6 +275,7 @@ def make_index(obj: Union[Iterable, BaseEntity, DictSerializable]): target container (dict, list, ...) from which to create an index of GEMD objects """ + def _make_index(_obj: BaseEntity): return ((LinkByUID(scope=scope, id=_obj.uids[scope]), _obj) for scope in _obj.uids) @@ -267,14 +286,10 @@ def _make_index(_obj: BaseEntity): return idx -def substitute_links(obj: Any, - scope: Optional[str] = None, - *, - allow_fallback: bool = True, - inplace: bool = False - ): - """ - Recursively replace pointers to BaseEntity with LinkByUID objects. +def substitute_links( + obj: Any, scope: Optional[str] = None, *, allow_fallback: bool = True, inplace: bool = False +): + """Recursively replace pointers to BaseEntity with LinkByUID objects. This prepares the object to be serialized or written to the API. It is the inverse of substitute_objects. @@ -296,17 +311,15 @@ def substitute_links(obj: Any, else: method = _substitute - return method(obj, - sub=lambda o: o.to_link(scope=scope, allow_fallback=allow_fallback), - applies=lambda o: o is not obj and cached_isinstance(o, BaseEntity)) + return method( + obj, + sub=lambda o: o.to_link(scope=scope, allow_fallback=allow_fallback), + applies=lambda o: o is not obj and cached_isinstance(o, BaseEntity), + ) -def substitute_objects(obj, - index, - *, - inplace: bool = False): - """ - Recursively replace LinkByUID objects with pointers to the objects with that UID in the index. +def substitute_objects(obj, index, *, inplace: bool = False): + """Recursively replace each LinkByUID with the indexed object that carries that UID. This prepares the object to be used after being deserialized. It is the inverse of substitute_links. @@ -326,14 +339,15 @@ def substitute_objects(obj, else: method = _substitute - return method(obj, - sub=lambda link: index.get(link, link), - applies=lambda o: cached_isinstance(o, LinkByUID)) + return method( + obj, + sub=lambda link: index.get(link, link), + applies=lambda o: cached_isinstance(o, LinkByUID), + ) def flatten(obj, scope=None) -> List[BaseEntity]: - """ - Flatten a BaseEntity (or array of them) into a list of objects connected by LinkByUID objects. + """Flatten a BaseEntity (or array of them) into objects connected by LinkByUID. This is a composite operation the amounts to: - Making sure at least one uid is set in each BaseEntity in scope @@ -388,12 +402,13 @@ def _flatten(base_obj: BaseEntity): return sorted([substitute_links(x) for x in res], key=lambda x: writable_sort_order(x)) -def recursive_foreach(obj: Union[Iterable, DictSerializable], - func: Callable[[BaseEntity], None], - *, - apply_first=False): - """ - Apply a function recursively to each BaseEntity object. +def recursive_foreach( + obj: Union[Iterable, DictSerializable], + func: Callable[[BaseEntity], None], + *, + apply_first=False, +): + """Apply a function recursively to each BaseEntity object. Only :class:`BaseEntity` objects will have the function applied, but the recursion will walk through all objects. For example, BaseEntity -> list -> BaseEntity will have func applied @@ -433,8 +448,7 @@ def recursive_foreach(obj: Union[Iterable, DictSerializable], elif cached_isinstance(this, DictSerializable): for k, x in this.__dict__.items(): queue.append(x) - elif cached_isinstance(this, Iterable) \ - and not cached_isinstance(this, (str, ByteString)): + elif cached_isinstance(this, Iterable) and not cached_isinstance(this, (str, ByteString)): for x in this: queue.append(x) @@ -444,12 +458,13 @@ def recursive_foreach(obj: Union[Iterable, DictSerializable], return -def recursive_flatmap(obj: Union[Iterable, DictSerializable], - func: Callable[[BaseEntity], Iterable], - *, - unidirectional=True) -> List: - """ - Recursively apply and accumulate a list-valued function to BaseEntity members. +def recursive_flatmap( + obj: Union[Iterable, DictSerializable], + func: Callable[[BaseEntity], Iterable], + *, + unidirectional=True, +) -> List: + """Recursively apply and accumulate a list-valued function to BaseEntity members. Only :class:`BaseEntity` objects will have the function applied, but the recursion will walk through all objects. For example, BaseEntity -> list -> BaseEntity will have func applied @@ -465,7 +480,7 @@ def recursive_flatmap(obj: Union[Iterable, DictSerializable], only recurse through the writeable direction of bidirectional links Returns - -------- + ------- List[Any] a list of accumulated return values @@ -496,8 +511,7 @@ def recursive_flatmap(obj: Union[Iterable, DictSerializable], queue.append(x) elif cached_isinstance(this, Reversible): queue.extend(reversed(this)) # Preserve order of the list/tuple - elif cached_isinstance(this, Iterable) \ - and not cached_isinstance(this, (str, ByteString)): + elif cached_isinstance(this, Iterable) and not cached_isinstance(this, (str, ByteString)): queue.extend(this) # No control over order return res @@ -505,17 +519,31 @@ def recursive_flatmap(obj: Union[Iterable, DictSerializable], def writable_sort_order(key: Union[BaseEntity, str]) -> int: """Sort order for flattening such that the objects can be read back and re-nested.""" - from gemd.entity.object import MeasurementSpec, ProcessSpec, MaterialSpec, IngredientSpec, \ - MeasurementRun, IngredientRun, MaterialRun, ProcessRun - from gemd.entity.template import ConditionTemplate, MaterialTemplate, MeasurementTemplate, \ - ParameterTemplate, ProcessTemplate, PropertyTemplate + from gemd.entity.object import ( + IngredientRun, + IngredientSpec, + MaterialRun, + MaterialSpec, + MeasurementRun, + MeasurementSpec, + ProcessRun, + ProcessSpec, + ) + from gemd.entity.template import ( + ConditionTemplate, + MaterialTemplate, + MeasurementTemplate, + ParameterTemplate, + ProcessTemplate, + PropertyTemplate, + ) if cached_isinstance(key, BaseEntity): typ = key.typ elif cached_isinstance(key, str): typ = key else: - raise ValueError("Can ony sort BaseEntities and type strings, not {}".format(key)) + raise ValueError(f"Can ony sort BaseEntities and type strings, not {key}") if typ in [ConditionTemplate.typ, ParameterTemplate.typ, PropertyTemplate.typ]: return 0 @@ -530,4 +558,4 @@ def writable_sort_order(key: Union[BaseEntity, str]) -> int: if typ in [IngredientRun.typ, MeasurementRun.typ]: return 5 - raise ValueError("Unrecognized type string: {}".format(typ)) + raise ValueError(f"Unrecognized type string: {typ}") diff --git a/pyproject.toml b/pyproject.toml index 458a4fa9..61007f27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ exclude = ["docs", "tests"] [dependency-groups] dev = [ + "ruff==0.16.6", "flake8==7.0.0", "flake8-docstrings==1.7.0", "numpy>=1.24.4; python_version<'3.10'", @@ -71,3 +72,34 @@ testpaths = [ omit = [ "gemd/demo/*", ] + + +[tool.ruff] +line-length = 99 +target-version = "py39" + +[tool.ruff.lint] +# Mirror the flake8 configuration in tox.ini so both linters agree. +preview = true +explicit-preview-rules = true +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "D", # pydocstyle (flake8-docstrings) + "I", # isort + "E231", # missing whitespace after ',' (preview-only in ruff) + "E275", # missing whitespace after keyword (preview-only in ruff) +] +# D100: Docstring at top of module is often redundant +# D104: Docstring in package is often redundant +# D105: Magic methods (e.g. __str__) are self explanatory +# D107: __init__ is self explanatory +# D203: Blank line before class docstrings; conflicts with D211 +# D213: A multiline summary begins on the first line; conflicts with D212 +# D301: backslash is used in making docstrings for sphinx to parse +# D401: Imperative mood requirement basically gets in the way +ignore = ["D100", "D104", "D105", "D107", "D203", "D213", "D301", "D401"] + +[tool.ruff.lint.pycodestyle] +max-doc-length = 119 diff --git a/tests/builders/test_builders.py b/tests/builders/test_builders.py index 663beebd..10a6b469 100644 --- a/tests/builders/test_builders.py +++ b/tests/builders/test_builders.py @@ -1,20 +1,32 @@ -from gemd.builders import make_node, add_edge, add_measurement, add_attribute, \ - make_attribute +from typing import Union + +import pytest + +from gemd.builders import add_attribute, add_edge, add_measurement, make_attribute, make_node from gemd.entity.attribute.base_attribute import BaseAttribute +from gemd.entity.bounds import ( + CategoricalBounds, + CompositionBounds, + IntegerBounds, + MolecularStructureBounds, + RealBounds, +) +from gemd.entity.bounds.base_bounds import BaseBounds +from gemd.entity.link_by_uid import LinkByUID from gemd.entity.object import MaterialRun -from gemd.entity.template import ProcessTemplate, MaterialTemplate, MeasurementTemplate, \ - PropertyTemplate, ConditionTemplate, ParameterTemplate +from gemd.entity.template import ( + ConditionTemplate, + MaterialTemplate, + MeasurementTemplate, + ParameterTemplate, + ProcessTemplate, + PropertyTemplate, +) from gemd.entity.template.attribute_template import AttributeTemplate -from gemd.entity.bounds import RealBounds, IntegerBounds, CategoricalBounds, \ - CompositionBounds, MolecularStructureBounds -from gemd.entity.bounds.base_bounds import BaseBounds from gemd.entity.value import EmpiricalFormula, NominalReal -from gemd.entity.link_by_uid import LinkByUID +from gemd.entity.value.base_value import BaseValue from gemd.units import parse_units -import pytest -from typing import Union - class UnsupportedBounds(BaseBounds, typ="unsupported_bounds"): """Dummy object to test Bounds type checking.""" @@ -53,8 +65,9 @@ def test_build(): prop_tmpl = PropertyTemplate(name="Property", bounds=RealBounds(0, 10, "m")) cond_tmpl = ConditionTemplate(name="Condition", bounds=CategoricalBounds(["a", "b", "c"])) - param_tmpl = ParameterTemplate(name="Parameter", - bounds=CompositionBounds(EmpiricalFormula.all_elements())) + param_tmpl = ParameterTemplate( + name="Parameter", bounds=CompositionBounds(EmpiricalFormula.all_elements()) + ) mol_tmpl = PropertyTemplate(name="Molecule", bounds=MolecularStructureBounds()) int_tmpl = ConditionTemplate(name="Integer", bounds=IntegerBounds(0, 10)) bad_tmpl = PropertyTemplate(name="Bad", bounds=UnsupportedBounds()) @@ -64,9 +77,7 @@ def test_build(): assert root.process.template == mix_tmpl, "Object didn't link correctly." one = make_node("One", material_template=raw_tmpl, process_template=procure_tmpl) add_edge(output_material=root, input_material=one) - add_edge(output_material=root, - input_material=make_node("Two") - ) + add_edge(output_material=root, input_material=make_node("Two")) assert len(root.process.ingredients) == 2, "Ingredient count didn't line up." # Attribute tests @@ -81,8 +92,9 @@ def test_build(): assert one.spec.properties[0].property.value.nominal == 1, "Wrong value on property." add_attribute(one.spec, cond_tmpl, "b") assert len(one.spec.properties[0].conditions) == 1, "Wrong location on condition." - assert one.spec.properties[0].conditions[0].template == cond_tmpl, \ + assert one.spec.properties[0].conditions[0].template == cond_tmpl, ( "Wrong linking on condition." + ) assert one.spec.properties[0].conditions[0].value.category == "b", "Wrong value on condition." with pytest.raises(ValueError): add_attribute(one.spec, param_tmpl, "H2O") # Mat Specs don't support parameters @@ -121,63 +133,75 @@ def test_build(): def test_quantities(): """Exercise the expressions on quantity in add_edge.""" - ing_one = add_edge(make_node("Input"), - make_node("Output"), - mass_fraction=0.1, - number_fraction=0.2, - volume_fraction=0.3, - absolute_quantity=0.4, - absolute_units='kg') + ing_one = add_edge( + make_node("Input"), + make_node("Output"), + mass_fraction=0.1, + number_fraction=0.2, + volume_fraction=0.3, + absolute_quantity=0.4, + absolute_units="kg", + ) assert ing_one.mass_fraction.nominal == 0.1, "Mass fraction got set." assert ing_one.number_fraction.nominal == 0.2, "Number fraction got set." assert ing_one.volume_fraction.nominal == 0.3, "Volume fraction got set." assert ing_one.absolute_quantity.nominal == 0.4, "Absolute quantity got set." - assert ing_one.absolute_quantity.units == parse_units('kg'), "Absolute units got set." + assert ing_one.absolute_quantity.units == parse_units("kg"), "Absolute units got set." - ing_two = add_edge(make_node("Input"), - make_node("Output"), - mass_fraction=NominalReal(0.5, ''), - number_fraction=NominalReal(0.6, ''), - volume_fraction=NominalReal(0.7, ''), - absolute_quantity=NominalReal(0.8, 'liters')) + ing_two = add_edge( + make_node("Input"), + make_node("Output"), + mass_fraction=NominalReal(0.5, ""), + number_fraction=NominalReal(0.6, ""), + volume_fraction=NominalReal(0.7, ""), + absolute_quantity=NominalReal(0.8, "liters"), + ) assert ing_two.mass_fraction.nominal == 0.5, "Mass fraction got set." assert ing_two.number_fraction.nominal == 0.6, "Number fraction got set." assert ing_two.volume_fraction.nominal == 0.7, "Volume fraction got set." assert ing_two.absolute_quantity.nominal == 0.8, "Absolute quantity got set." - assert ing_two.absolute_quantity.units == parse_units('liters'), "Absolute units got set." + assert ing_two.absolute_quantity.units == parse_units("liters"), "Absolute units got set." with pytest.raises(ValueError): add_edge(make_node("Input"), make_node("Output"), absolute_quantity=0.4) with pytest.raises(ValueError): - add_edge(make_node("Input"), make_node("Output"), - absolute_quantity=NominalReal(0.8, 'liters'), absolute_units='liters') + add_edge( + make_node("Input"), + make_node("Output"), + absolute_quantity=NominalReal(0.8, "liters"), + absolute_units="liters", + ) def test_attributes(): """Exercise permutations of attributes, bounds and values.""" prop_tmpl = PropertyTemplate(name="Property", bounds=RealBounds(0, 10, "m")) cond_tmpl = ConditionTemplate(name="Condition", bounds=CategoricalBounds(["a", "b", "c"])) - param_tmpl = ParameterTemplate(name="Parameter", - bounds=CompositionBounds(EmpiricalFormula.all_elements())) + param_tmpl = ParameterTemplate( + name="Parameter", bounds=CompositionBounds(EmpiricalFormula.all_elements()) + ) mol_tmpl = PropertyTemplate(name="Molecule", bounds=MolecularStructureBounds()) int_tmpl = ConditionTemplate(name="Integer", bounds=IntegerBounds(0, 10)) - msr = add_measurement(make_node('Material'), - name='Measurement', - attributes=[make_attribute(prop_tmpl, 5), - make_attribute(cond_tmpl, 'a'), - make_attribute(param_tmpl, 'SiC'), - make_attribute(mol_tmpl, 'InChI=1S/CSi/c1-2'), - make_attribute(mol_tmpl, '[C-]#[Si+]'), - make_attribute(int_tmpl, 5) - ]) + msr = add_measurement( + make_node("Material"), + name="Measurement", + attributes=[ + make_attribute(prop_tmpl, 5), + make_attribute(cond_tmpl, "a"), + make_attribute(param_tmpl, "SiC"), + make_attribute(mol_tmpl, "InChI=1S/CSi/c1-2"), + make_attribute(mol_tmpl, "[C-]#[Si+]"), + make_attribute(int_tmpl, 5), + ], + ) assert msr.properties[0].value.nominal == 5 - assert msr.conditions[0].value.category == 'a' - assert msr.parameters[0].value.formula == 'SiC' - assert msr.properties[1].value.inchi == 'InChI=1S/CSi/c1-2' - assert msr.properties[2].value.smiles == '[C-]#[Si+]' + assert msr.conditions[0].value.category == "a" + assert msr.parameters[0].value.formula == "SiC" + assert msr.properties[1].value.inchi == "InChI=1S/CSi/c1-2" + assert msr.properties[2].value.smiles == "[C-]#[Si+]" assert msr.conditions[1].value.nominal == 5 @@ -190,9 +214,11 @@ def test_exceptions(): add_edge(make_node("Input"), MaterialRun("Output", spec=LinkByUID("Bad", "ID"))) with pytest.raises(ValueError): - add_measurement(make_node('Material'), - name='Measurement', - attributes=[UnsupportedAttribute("Spider-man")]) + add_measurement( + make_node("Material"), + name="Measurement", + attributes=[UnsupportedAttribute("Spider-man")], + ) with pytest.raises(ValueError): make_attribute(UnsupportedAttributeTemplate, 5) diff --git a/tests/demo/test_cake.py b/tests/demo/test_cake.py index 9e31f9e4..cf7a68d9 100644 --- a/tests/demo/test_cake.py +++ b/tests/demo/test_cake.py @@ -1,17 +1,24 @@ """Test cake demo.""" -from gemd.entity.object.material_spec import MaterialSpec + +from gemd.demo.cake import ( + change_scope, + get_demo_scope, + get_template_scope, + import_toothpick_picture, + make_cake, + make_cake_spec, + make_cake_templates, +) +from gemd.entity.file_link import FileLink +from gemd.entity.object.ingredient_run import IngredientRun +from gemd.entity.object.ingredient_spec import IngredientSpec from gemd.entity.object.material_run import MaterialRun +from gemd.entity.object.material_spec import MaterialSpec +from gemd.entity.object.measurement_run import MeasurementRun +from gemd.entity.object.measurement_spec import MeasurementSpec from gemd.entity.object.process_run import ProcessRun from gemd.entity.object.process_spec import ProcessSpec -from gemd.entity.object.measurement_spec import MeasurementSpec -from gemd.entity.object.measurement_run import MeasurementRun -from gemd.entity.object.ingredient_spec import IngredientSpec -from gemd.entity.object.ingredient_run import IngredientRun -from gemd.entity.file_link import FileLink - from gemd.json import dumps, loads -from gemd.demo.cake import make_cake_templates, make_cake_spec, make_cake, \ - import_toothpick_picture, change_scope, get_demo_scope, get_template_scope from gemd.util import recursive_foreach @@ -35,10 +42,11 @@ def increment(dummy): def _check_ids(obj): nonlocal uid_seen for scope in obj.uids: - lbl = '{}::{}'.format(scope, obj.uids[scope].lower()) + lbl = f"{scope}::{obj.uids[scope].lower()}" if lbl in uid_seen: - assert uid_seen[lbl] == id(obj), "'{}' seen twice".format(lbl) + assert uid_seen[lbl] == id(obj), f"'{lbl}' seen twice" uid_seen[lbl] = id(obj) + recursive_foreach(cake, _check_ids) # Check that all recursive and square links are structured correctly @@ -64,6 +72,7 @@ def _check_crosslinks(obj): elif isinstance(obj, IngredientRun): assert obj in obj.process.ingredients assert obj.spec.material == obj.material.spec + recursive_foreach(cake, _check_crosslinks) @@ -80,7 +89,7 @@ def test_cake_sigs(): assert dumps(templates) == tmpl_snap assert dumps(specs) == spec_snap - filelink = FileLink(filename='The name of the file', url='www.file.gov') + filelink = FileLink(filename="The name of the file", url="www.file.gov") cake2 = make_cake(seed=27, cake_spec=specs, tmpl=templates, toothpick_img=filelink) assert filelink.filename not in dumps(cake1) @@ -98,27 +107,27 @@ def test_scope(): default_cake = make_cake() default_scope = next(iter(default_cake.uids)) - change_scope('second-scope') + change_scope("second-scope") second_cake = make_cake() - change_scope(data='third-scope', templates='also-a-scope') - assert get_demo_scope() == 'third-scope' - assert get_template_scope() == 'also-a-scope' + change_scope(data="third-scope", templates="also-a-scope") + assert get_demo_scope() == "third-scope" + assert get_template_scope() == "also-a-scope" third_cake = make_cake() - assert 'second-scope' not in default_cake.uids - assert 'third-scope' not in default_cake.uids + assert "second-scope" not in default_cake.uids + assert "third-scope" not in default_cake.uids assert default_scope not in second_cake.uids - assert 'second-scope' in second_cake.uids - assert 'third-scope' not in second_cake.uids + assert "second-scope" in second_cake.uids + assert "third-scope" not in second_cake.uids assert default_scope not in third_cake.uids - assert 'second-scope' not in third_cake.uids - assert 'third-scope' in third_cake.uids + assert "second-scope" not in third_cake.uids + assert "third-scope" in third_cake.uids - assert any('template' in x for x in default_cake.spec.template.uids) - assert not any('template' in x for x in third_cake.spec.template.uids) + assert any("template" in x for x in default_cake.spec.template.uids) + assert not any("template" in x for x in third_cake.spec.template.uids) def test_recursive_equals(): @@ -127,5 +136,5 @@ def test_recursive_equals(): copy = loads(dumps(cake)) assert cake == copy - copy.process.ingredients[0].material.process.ingredients[0].material.tags.append('Hi') + copy.process.ingredients[0].material.process.ingredients[0].material.tags.append("Hi") assert cake != copy diff --git a/tests/demo/test_material_run_example.py b/tests/demo/test_material_run_example.py index 5cb99e29..7d783d57 100644 --- a/tests/demo/test_material_run_example.py +++ b/tests/demo/test_material_run_example.py @@ -1,44 +1,42 @@ """Test the ingestion of a material run.""" -from gemd.json import dump, load -from gemd.demo.material_run_example import ingest_material_run + import tempfile +from gemd.demo.material_run_example import ingest_material_run +from gemd.json import dump, load + # Example data (that could have been loaded from a json file) example = { - "sample_id": '37e6b61f-b55c-43f1-a14d-534fc29c86f8', + "sample_id": "37e6b61f-b55c-43f1-a14d-534fc29c86f8", "tags": ["example", "demo", "json"], "experiments": [ { "knob_2_setting": "low", "temperature": "300 degF", "density": "1.0 +- 0.5 g/cm^3", - "tags": "warm up" + "tags": "warm up", }, { "knob_2_setting": "low", "temperature": "302 degF", "density": "1.04 +- 0.1 g/cm^3", - "tags": ["high quality", "hutch"] - }, - { - "knob_2_setting": "medium", - "density": "0.9 +- 0.4 g/cm^3", - "tags": ["oops"] + "tags": ["high quality", "hutch"], }, + {"knob_2_setting": "medium", "density": "0.9 +- 0.4 g/cm^3", "tags": ["oops"]}, { "knob_2_setting": "medium", "temperature": "456 degF", "density": "0.87 +- 0.1 g/cm^3", - "tags": ["high quality", "hutch"] + "tags": ["high quality", "hutch"], }, { "knob_2_setting": "high", "temperature": "624 degF", "density": "0.80 +- 0.12 g/cm^3", "kinematic viscosity": "0.1 m^2/s", - "tags": ["hutch", "viscous"] - } - ] + "tags": ["hutch", "viscous"], + }, + ], } diff --git a/tests/demo/test_measurement_run_example.py b/tests/demo/test_measurement_run_example.py index 6482dc25..70fba486 100644 --- a/tests/demo/test_measurement_run_example.py +++ b/tests/demo/test_measurement_run_example.py @@ -1,6 +1,7 @@ """Test measurement demo.""" -from gemd.json import dumps, load + from gemd.demo.measurement_example import make_demo_measurements +from gemd.json import dumps, load def test_measurement_example(tmp_path): diff --git a/tests/demo/test_sac.py b/tests/demo/test_sac.py index 446e1096..dc790567 100644 --- a/tests/demo/test_sac.py +++ b/tests/demo/test_sac.py @@ -1,9 +1,15 @@ """Test Strehlow & Cook demo.""" -from gemd.demo.strehlow_and_cook import make_strehlow_table, make_strehlow_objects, \ - minimal_subset, import_table -import gemd.json as gemd_json + import json as json_builtin +import gemd.json as gemd_json +from gemd.demo.strehlow_and_cook import ( + import_table, + make_strehlow_objects, + make_strehlow_table, + minimal_subset, +) + def test_sac(): """Make S&C table and assert that it can be serialized.""" @@ -21,7 +27,7 @@ def test_sac(): assert (comp1.name == comp2.name) == (comp1.spec.uids == comp2.spec.uids) # Look at each different combination of Value types in a S&C record - smaller = minimal_subset(sac_tbl['content']) + smaller = minimal_subset(sac_tbl["content"]) # Make sure that the diversity of value types isn't lost, e.g. something is being None'd assert len(smaller) == 162 diff --git a/tests/demo/test_table_example.py b/tests/demo/test_table_example.py index a24a0d38..67ec109c 100644 --- a/tests/demo/test_table_example.py +++ b/tests/demo/test_table_example.py @@ -1,10 +1,10 @@ """Test an example table.""" + import pandas as pd -from gemd.json import load, dump -from gemd.entity.object import MaterialRun from gemd.demo.table_example import ingest_table - +from gemd.entity.object import MaterialRun +from gemd.json import dump, load data = [ {"vapor pressure": 2.0, "temperature": 300}, diff --git a/tests/entity/attribute/test_base_attribute.py b/tests/entity/attribute/test_base_attribute.py index d22df747..67c30d87 100644 --- a/tests/entity/attribute/test_base_attribute.py +++ b/tests/entity/attribute/test_base_attribute.py @@ -1,18 +1,19 @@ """Tests of the BaseAttribute class.""" + import pytest from gemd.entity.attribute.property import Property +from gemd.entity.bounds.real_bounds import RealBounds +from gemd.entity.bounds_validation import WarningLevel, validation_level from gemd.entity.template.process_template import ProcessTemplate from gemd.entity.template.property_template import PropertyTemplate from gemd.entity.value.nominal_real import NominalReal -from gemd.entity.bounds.real_bounds import RealBounds -from gemd.entity.bounds_validation import validation_level, WarningLevel def test_invalid_assignment(caplog): """Test that invalid assignments throw the appropriate errors.""" with pytest.raises(TypeError): - Property(value=NominalReal(10, '')) + Property(value=NominalReal(10, "")) with pytest.raises(TypeError): Property(name="property", value=10) with pytest.raises(TypeError): @@ -20,14 +21,13 @@ def test_invalid_assignment(caplog): with pytest.raises(ValueError): Property(name="property", origin=None) - valid_prop = Property(name="property", - value=NominalReal(10, ''), - template=PropertyTemplate("template", - bounds=RealBounds(0, 100, '') - ) - ) + valid_prop = Property( + name="property", + value=NominalReal(10, ""), + template=PropertyTemplate("template", bounds=RealBounds(0, 100, "")), + ) good_val = valid_prop.value - bad_val = NominalReal(-10.0, '') + bad_val = NominalReal(-10.0, "") assert len(caplog.records) == 0, "Warning caught before logging tests were reached." with validation_level(WarningLevel.IGNORE): valid_prop.value = bad_val @@ -48,7 +48,5 @@ def test_invalid_assignment(caplog): with validation_level(WarningLevel.FATAL): with pytest.raises(ValueError): - valid_prop.template = PropertyTemplate("template", - bounds=RealBounds(0, 1, '') - ) + valid_prop.template = PropertyTemplate("template", bounds=RealBounds(0, 1, "")) assert valid_prop.value == good_val, "FATAL didn't allow the bad value to be set." diff --git a/tests/entity/attribute/test_imports.py b/tests/entity/attribute/test_imports.py index f89f8d3d..eeebddd7 100644 --- a/tests/entity/attribute/test_imports.py +++ b/tests/entity/attribute/test_imports.py @@ -8,7 +8,7 @@ class ImportTestObj: @property def test_property(self): """A property to validate decorator functionality.""" - Property(name='Trial') # noqa: F405 + Property(name="Trial") # noqa: F405 return True diff --git a/tests/entity/attribute/test_property_and_conditions.py b/tests/entity/attribute/test_property_and_conditions.py index 5824f497..9c7168c4 100644 --- a/tests/entity/attribute/test_property_and_conditions.py +++ b/tests/entity/attribute/test_property_and_conditions.py @@ -1,29 +1,37 @@ import pytest -from gemd.entity.attribute.property_and_conditions import Property, Condition, \ - PropertyAndConditions +from gemd.entity.attribute.property_and_conditions import ( + Condition, + Property, + PropertyAndConditions, +) +from gemd.entity.bounds.categorical_bounds import CategoricalBounds +from gemd.entity.bounds.integer_bounds import IntegerBounds from gemd.entity.link_by_uid import LinkByUID -from gemd.entity.template.property_template import PropertyTemplate from gemd.entity.template.condition_template import ConditionTemplate -from gemd.entity.value.nominal_integer import NominalInteger +from gemd.entity.template.property_template import PropertyTemplate from gemd.entity.value.nominal_categorical import NominalCategorical -from gemd.entity.bounds.integer_bounds import IntegerBounds -from gemd.entity.bounds.categorical_bounds import CategoricalBounds +from gemd.entity.value.nominal_integer import NominalInteger def test_fields_from_property(): """Test that several fields of the attribute are derived from the property.""" prop_template = PropertyTemplate(name="cookie eating template", bounds=IntegerBounds(0, 1000)) - cond_template = ConditionTemplate(name="Hunger template", - bounds=CategoricalBounds(["hungry", "full", "peckish"])) - prop = Property(name="number of cookies eaten", - template=prop_template, - origin='measured', - value=NominalInteger(27)) - cond = Condition(name="hunger level", - template=cond_template, - origin='specified', - value=NominalCategorical("hungry")) + cond_template = ConditionTemplate( + name="Hunger template", bounds=CategoricalBounds(["hungry", "full", "peckish"]) + ) + prop = Property( + name="number of cookies eaten", + template=prop_template, + origin="measured", + value=NominalInteger(27), + ) + cond = Condition( + name="hunger level", + template=cond_template, + origin="specified", + value=NominalCategorical("hungry"), + ) prop_and_conds = PropertyAndConditions(property=prop, conditions=[cond]) assert prop_and_conds.name == prop.name @@ -35,7 +43,9 @@ def test_fields_from_property(): def test_invalid_assignment(): """Test that invalid assignment throws a TypeError.""" with pytest.raises(TypeError): - PropertyAndConditions(property=LinkByUID('id', 'a15')) + PropertyAndConditions(property=LinkByUID("id", "a15")) with pytest.raises(TypeError): - PropertyAndConditions(property=Property("property"), - conditions=[Condition("condition"), LinkByUID('scope', 'id')]) + PropertyAndConditions( + property=Property("property"), + conditions=[Condition("condition"), LinkByUID("scope", "id")], + ) diff --git a/tests/entity/bounds/test_categorical_bounds.py b/tests/entity/bounds/test_categorical_bounds.py index 24c5987f..10f08520 100644 --- a/tests/entity/bounds/test_categorical_bounds.py +++ b/tests/entity/bounds/test_categorical_bounds.py @@ -1,11 +1,12 @@ """Test of CategoricalBounds.""" + import pytest -from gemd.json import dumps, loads from gemd.entity.bounds.categorical_bounds import CategoricalBounds from gemd.entity.bounds.real_bounds import RealBounds from gemd.entity.util import array_like from gemd.entity.value.nominal_categorical import NominalCategorical +from gemd.json import dumps, loads def test_categories(): @@ -29,7 +30,7 @@ def test_contains(): bounds = CategoricalBounds(categories={"spam", "eggs"}) assert bounds.contains(CategoricalBounds(categories={"spam"})) assert not bounds.contains(CategoricalBounds(categories={"spam", "foo"})) - assert not bounds.contains(RealBounds(0.0, 2.0, '')) + assert not bounds.contains(RealBounds(0.0, 2.0, "")) assert not bounds.contains(None) with pytest.raises(TypeError): bounds.contains({"spam", "eggs"}) @@ -66,12 +67,14 @@ def test_numpy(): if len(array_like()) > 2: # Test numpy import numpy as np + np_bounds = CategoricalBounds(np.array(["spam", "eggs"], dtype=object)) np_copy = loads(dumps(np_bounds)) assert np_copy == np_bounds if len(array_like()) > 3: # Test pandas import pandas as pd + pd_bounds = CategoricalBounds(pd.Series(["spam", "eggs"])) pd_copy = loads(dumps(pd_bounds)) assert pd_copy == pd_bounds diff --git a/tests/entity/bounds/test_composition_bounds.py b/tests/entity/bounds/test_composition_bounds.py index 0f137e71..84b7e05a 100644 --- a/tests/entity/bounds/test_composition_bounds.py +++ b/tests/entity/bounds/test_composition_bounds.py @@ -1,12 +1,13 @@ """Test CompositionBounds.""" + import pytest -from gemd.json import dumps, loads from gemd.entity.bounds.composition_bounds import CompositionBounds from gemd.entity.bounds.real_bounds import RealBounds from gemd.entity.util import array_like from gemd.entity.value.empirical_formula import EmpiricalFormula from gemd.entity.value.nominal_composition import NominalComposition +from gemd.json import dumps, loads def test_components(): @@ -30,7 +31,7 @@ def test_contains(): bounds = CompositionBounds(components={"spam", "eggs"}) assert bounds.contains(CompositionBounds(components={"spam"})) assert not bounds.contains(CompositionBounds(components={"foo"})) - assert not bounds.contains(RealBounds(0.0, 2.0, '')) + assert not bounds.contains(RealBounds(0.0, 2.0, "")) assert not bounds.contains(None) with pytest.raises(TypeError): bounds.contains({"spam"}) diff --git a/tests/entity/bounds/test_integer_bounds.py b/tests/entity/bounds/test_integer_bounds.py index 5f4e6f24..cf40cd49 100644 --- a/tests/entity/bounds/test_integer_bounds.py +++ b/tests/entity/bounds/test_integer_bounds.py @@ -1,4 +1,5 @@ """Test IntegerBounds.""" + import pytest from gemd.entity.bounds.integer_bounds import IntegerBounds @@ -49,7 +50,7 @@ def test_incompatible_types(): """Make sure that incompatible types aren't contained or validated.""" int_bounds = IntegerBounds(0, 1) - assert not int_bounds.contains(RealBounds(0.0, 1.0, '')) + assert not int_bounds.contains(RealBounds(0.0, 1.0, "")) def test_contains(): diff --git a/tests/entity/bounds/test_molecular_structure_bounds.py b/tests/entity/bounds/test_molecular_structure_bounds.py index 6a3fe34c..c48a08ad 100644 --- a/tests/entity/bounds/test_molecular_structure_bounds.py +++ b/tests/entity/bounds/test_molecular_structure_bounds.py @@ -1,25 +1,25 @@ """Test of CategoricalBounds.""" + import pytest -from gemd.json import dumps, loads from gemd.entity.bounds.molecular_structure_bounds import MolecularStructureBounds from gemd.entity.bounds.real_bounds import RealBounds - -from gemd.entity.value import Smiles, NominalInteger +from gemd.entity.value import NominalInteger, Smiles +from gemd.json import dumps, loads def test_contains(): """Test basic contains logic.""" bounds = MolecularStructureBounds() assert bounds.contains(MolecularStructureBounds()) - assert not bounds.contains(RealBounds(0.0, 2.0, '')) + assert not bounds.contains(RealBounds(0.0, 2.0, "")) assert not bounds.contains(None) with pytest.raises(TypeError): - bounds.contains('c1(C=O)cc(OC)c(O)cc1') + bounds.contains("c1(C=O)cc(OC)c(O)cc1") with pytest.raises(TypeError): - bounds.contains('InChI=1/C8H8O3/c1-11-8-4-6(5-9)2-3-7(8)10/h2-5,10H,1H3') + bounds.contains("InChI=1/C8H8O3/c1-11-8-4-6(5-9)2-3-7(8)10/h2-5,10H,1H3") - assert bounds.contains(Smiles('c1(C=O)cc(OC)c(O)cc1')) + assert bounds.contains(Smiles("c1(C=O)cc(OC)c(O)cc1")) assert not bounds.contains(NominalInteger(5)) diff --git a/tests/entity/bounds/test_real_bounds.py b/tests/entity/bounds/test_real_bounds.py index cd34ebea..12319954 100644 --- a/tests/entity/bounds/test_real_bounds.py +++ b/tests/entity/bounds/test_real_bounds.py @@ -1,4 +1,5 @@ """Test RealBounds.""" + import pytest from gemd.entity.bounds.integer_bounds import IntegerBounds @@ -13,16 +14,16 @@ def test_contains(): dim2 = RealBounds(lower_bound=33, upper_bound=200, default_units="degF") assert dim.contains(dim2) - assert dim.contains(NominalReal(5, 'degC')) - assert not dim.contains(NominalReal(5, 'K')) + assert dim.contains(NominalReal(5, "degC")) + assert not dim.contains(NominalReal(5, "K")) def test_union(): """Test basic union & update logic.""" - bounds = RealBounds(lower_bound=1, upper_bound=5, default_units='mm') - low = RealBounds(lower_bound=1, upper_bound=5, default_units='um') - high = NominalReal(1, 'cm') - bad = NominalReal(1, 'kg') + bounds = RealBounds(lower_bound=1, upper_bound=5, default_units="mm") + low = RealBounds(lower_bound=1, upper_bound=5, default_units="um") + high = NominalReal(1, "cm") + bad = NominalReal(1, "kg") assert bounds.union(low).contains(low), "Bounds didn't get low value" assert bounds.union(high).contains(high), "Bounds didn't get high value" assert bounds.union(low, high).contains(bounds), "Bounds didn't keep old values" @@ -50,7 +51,7 @@ def test_contains_incompatible_units(): """Make sure contains returns false when the units don't match.""" dim = RealBounds(lower_bound=0, upper_bound=100, default_units="m") dim2 = RealBounds(lower_bound=0, upper_bound=100, default_units="kJ") - dim3 = RealBounds(lower_bound=0, upper_bound=100, default_units='') + dim3 = RealBounds(lower_bound=0, upper_bound=100, default_units="") assert not dim.contains(dim2) assert not dim.contains(dim3) @@ -64,7 +65,7 @@ def test_constructor_error(): RealBounds(lower_bound=0, upper_bound=float("inf"), default_units="meter") with pytest.raises(ValueError): - RealBounds(lower_bound=None, upper_bound=10, default_units='') + RealBounds(lower_bound=None, upper_bound=10, default_units="") with pytest.raises(ValueError): RealBounds(lower_bound=0, upper_bound=100, default_units=None) @@ -87,4 +88,4 @@ def test_type_mismatch(): assert not bounds.contains(IntegerBounds(0, 1)) assert not bounds.contains(None) with pytest.raises(TypeError): - bounds.contains([.33, .66]) + bounds.contains([0.33, 0.66]) diff --git a/tests/entity/object/test_ingredient_run.py b/tests/entity/object/test_ingredient_run.py index 1f534d73..86d872d1 100644 --- a/tests/entity/object/test_ingredient_run.py +++ b/tests/entity/object/test_ingredient_run.py @@ -1,9 +1,10 @@ """Tests of the ingredient run object.""" + import pytest +from gemd.entity.bounds.real_bounds import RealBounds from gemd.entity.object.ingredient_run import IngredientRun from gemd.entity.object.process_run import ProcessRun -from gemd.entity.bounds.real_bounds import RealBounds def test_ingredient_reassignment(): @@ -30,7 +31,7 @@ def test_ingredient_reassignment(): def test_invalid_assignment(): """Invalid assignments to `process` or `material` throw a TypeError.""" with pytest.raises(TypeError): - IngredientRun(material=RealBounds(0, 5.0, '')) + IngredientRun(material=RealBounds(0, 5.0, "")) with pytest.raises(TypeError): IngredientRun(process="process") with pytest.raises(TypeError): @@ -41,38 +42,37 @@ def test_invalid_assignment(): def test_name_persistence(): """Verify that a serialized IngredientRun doesn't lose its name.""" - from gemd.entity.object import IngredientSpec from gemd.entity.link_by_uid import LinkByUID + from gemd.entity.object import IngredientSpec from gemd.json import GEMDJson je = GEMDJson() - ms_link = LinkByUID(scope='local', id='mat_spec') - mr_link = LinkByUID(scope='local', id='mat_run') - ps_link = LinkByUID(scope='local', id='pro_spec') - pr_link = LinkByUID(scope='local', id='pro_run') - spec = IngredientSpec(name='Ingred', labels=['some', 'words'], - process=ps_link, material=ms_link) - run = IngredientRun(spec=spec, - process=pr_link, material=mr_link) + ms_link = LinkByUID(scope="local", id="mat_spec") + mr_link = LinkByUID(scope="local", id="mat_run") + ps_link = LinkByUID(scope="local", id="pro_spec") + pr_link = LinkByUID(scope="local", id="pro_run") + spec = IngredientSpec( + name="Ingred", labels=["some", "words"], process=ps_link, material=ms_link + ) + run = IngredientRun(spec=spec, process=pr_link, material=mr_link) assert run.name == spec.name assert run.labels == spec.labels # Try changing them and make sure they change - spec.name = 'Frank' - spec.labels = ['other', 'words'] + spec.name = "Frank" + spec.labels = ["other", "words"] assert run.name == spec.name assert run.labels == spec.labels - run.spec = LinkByUID(scope='local', id='ing_spec') + run.spec = LinkByUID(scope="local", id="ing_spec") # Name and labels are now stashed but not stored assert run == je.copy(run) assert run.name == spec.name assert run.labels == spec.labels # Test that serialization doesn't get confused after a deser and set - spec_too = IngredientSpec(name='Jorge', labels=[], - process=ps_link, material=ms_link) + spec_too = IngredientSpec(name="Jorge", labels=[], process=ps_link, material=ms_link) run.spec = spec_too assert run == je.copy(run) assert run.name == spec_too.name @@ -81,8 +81,8 @@ def test_name_persistence(): def test_implicit_fields(): """These test that users can't directly set names and labels.""" - name = 'name' - labels = ['label', 'also'] + name = "name" + labels = ["label", "also"] with pytest.raises(TypeError): IngredientRun(name=name) with pytest.raises(TypeError): diff --git a/tests/entity/object/test_ingredient_spec.py b/tests/entity/object/test_ingredient_spec.py index 6388720b..b9a19168 100644 --- a/tests/entity/object/test_ingredient_spec.py +++ b/tests/entity/object/test_ingredient_spec.py @@ -1,15 +1,16 @@ """Tests of the ingredient spec object.""" + import pytest +from gemd.entity.bounds_validation import WarningLevel, validation_level from gemd.entity.object.ingredient_spec import IngredientSpec from gemd.entity.object.process_spec import ProcessSpec +from gemd.entity.value.empirical_formula import EmpiricalFormula +from gemd.entity.value.nominal_categorical import NominalCategorical +from gemd.entity.value.nominal_integer import NominalInteger from gemd.entity.value.nominal_real import NominalReal -from gemd.entity.value.uniform_real import UniformReal from gemd.entity.value.normal_real import NormalReal -from gemd.entity.value.nominal_integer import NominalInteger -from gemd.entity.value.nominal_categorical import NominalCategorical -from gemd.entity.value.empirical_formula import EmpiricalFormula -from gemd.entity.bounds_validation import validation_level, WarningLevel +from gemd.entity.value.uniform_real import UniformReal def test_ingredient_reassignment(): @@ -33,27 +34,19 @@ def test_ingredient_reassignment(): assert set(frying.ingredients) == {oil, potatoes} -VALID_FRACTIONS = [ - NominalReal(1.0, ''), - UniformReal(0.5, 0.6, ''), - NormalReal(0.2, 0.3, '') -] +VALID_FRACTIONS = [NominalReal(1.0, ""), UniformReal(0.5, 0.6, ""), NormalReal(0.2, 0.3, "")] -INVALID_FRACTIONS = [ - NominalReal(1.0, 'm'), - UniformReal(0.7, 1.1, ''), - NormalReal(-0.2, 0.3, '') -] +INVALID_FRACTIONS = [NominalReal(1.0, "m"), UniformReal(0.7, 1.1, ""), NormalReal(-0.2, 0.3, "")] VALID_QUANTITIES = [ - NominalReal(14.0, 'g'), - UniformReal(0.5, 0.6, 'mol'), - NormalReal(0.3, 0.6, 'cc') + NominalReal(14.0, "g"), + UniformReal(0.5, 0.6, "mol"), + NormalReal(0.3, 0.6, "cc"), ] INVALID_QUANTITIES = [ - NominalReal(14.0, ''), - UniformReal(-0.1, 0.3, 'mol'), + NominalReal(14.0, ""), + UniformReal(-0.1, 0.3, "mol"), ] INVALID_TYPES = [ @@ -61,15 +54,13 @@ def test_ingredient_reassignment(): NominalInteger(5), EmpiricalFormula("CH4"), 0.33, - "0.5" + "0.5", ] @pytest.mark.parametrize("valid_fraction", VALID_FRACTIONS) def test_valid_fractions(valid_fraction, caplog): - """ - Check that all fractional quantities must be continuous values. - """ + """Check that all fractional quantities must be continuous values.""" with validation_level(WarningLevel.WARNING): ingred = IngredientSpec(name="name", mass_fraction=valid_fraction) assert ingred.mass_fraction == valid_fraction @@ -83,9 +74,7 @@ def test_valid_fractions(valid_fraction, caplog): @pytest.mark.parametrize("valid_quantity", VALID_QUANTITIES) def test_valid_quantities(valid_quantity, caplog): - """ - Check that all quantities must be continuous values. - """ + """Check that all quantities must be continuous values.""" with validation_level(WarningLevel.WARNING): ingred = IngredientSpec(name="name", absolute_quantity=valid_quantity) assert ingred.absolute_quantity == valid_quantity @@ -97,15 +86,15 @@ def test_valid_quantities(valid_quantity, caplog): @pytest.mark.parametrize("invalid_fraction", INVALID_FRACTIONS) def test_invalid_fractions(invalid_fraction, caplog): - """ - Verify that when validation is requested, limits are enforced for fractions. - """ + """Verify that when validation is requested, limits are enforced for fractions.""" with validation_level(WarningLevel.IGNORE): IngredientSpec(name="name", mass_fraction=invalid_fraction) assert len(caplog.records) == 0, f"Warned on invalid values with IGNORE: {invalid_fraction}" with validation_level(WarningLevel.WARNING): IngredientSpec(name="name", mass_fraction=invalid_fraction) - assert len(caplog.records) == 1, f"Didn't warn on invalid values with IGNORE: {invalid_fraction}" + assert len(caplog.records) == 1, ( + f"Didn't warn on invalid values with IGNORE: {invalid_fraction}" + ) with validation_level(WarningLevel.FATAL): with pytest.raises(ValueError): IngredientSpec(name="name", mass_fraction=invalid_fraction) @@ -113,15 +102,15 @@ def test_invalid_fractions(invalid_fraction, caplog): @pytest.mark.parametrize("invalid_quantity", INVALID_QUANTITIES) def test_invalid_quantities(invalid_quantity, caplog): - """ - Verify that when validation is requested, limits are enforced for fractions. - """ + """Verify that when validation is requested, limits are enforced for fractions.""" with validation_level(WarningLevel.IGNORE): IngredientSpec(name="name", absolute_quantity=invalid_quantity) assert len(caplog.records) == 0, f"Warned on invalid values with IGNORE: {invalid_quantity}" with validation_level(WarningLevel.WARNING): IngredientSpec(name="name", absolute_quantity=invalid_quantity) - assert len(caplog.records) == 1, f"Didn't warn on invalid values with IGNORE: {invalid_quantity}" + assert len(caplog.records) == 1, ( + f"Didn't warn on invalid values with IGNORE: {invalid_quantity}" + ) with validation_level(WarningLevel.FATAL): with pytest.raises(ValueError): IngredientSpec(name="name", absolute_quantity=invalid_quantity) @@ -143,7 +132,7 @@ def test_invalid_types(invalid_type): def test_invalid_assignment(): """Invalid assignments to `process` or `material` throw a TypeError.""" with pytest.raises(TypeError): - IngredientSpec(name="name", material=NominalReal(3, '')) + IngredientSpec(name="name", material=NominalReal(3, "")) with pytest.raises(TypeError): IngredientSpec(name="name", process="process") with pytest.raises(TypeError): @@ -152,9 +141,11 @@ def test_invalid_assignment(): def test_bad_has_template(): """Make sure the non-implementation of HasTemplate behaves properly.""" - assert isinstance(None, IngredientSpec(name="name")._template_type()), \ + assert isinstance(None, IngredientSpec(name="name")._template_type()), ( "Ingredients didn't have NoneType templates" - assert IngredientSpec(name="name").template is None, \ + ) + assert IngredientSpec(name="name").template is None, ( "An ingredient didn't have a null template." + ) with pytest.raises(AttributeError): # Note an AttributeError, not a TypeError IngredientSpec(name="name").template = 1 diff --git a/tests/entity/object/test_material_run.py b/tests/entity/object/test_material_run.py index c15e948e..9ce25aff 100644 --- a/tests/entity/object/test_material_run.py +++ b/tests/entity/object/test_material_run.py @@ -1,40 +1,41 @@ """Test of the material run object.""" -import pytest + import json as json_builtin -from uuid import uuid4 from copy import deepcopy +from uuid import uuid4 + +import pytest import gemd.json as gemd_json -from gemd.entity.attribute import PropertyAndConditions, Property -from gemd.entity.object import MaterialRun, ProcessSpec, ProcessRun, MaterialSpec, MeasurementRun +from gemd.entity.attribute import Property, PropertyAndConditions +from gemd.entity.link_by_uid import LinkByUID +from gemd.entity.object import MaterialRun, MaterialSpec, MeasurementRun, ProcessRun, ProcessSpec from gemd.entity.template import MaterialTemplate from gemd.entity.value import NominalReal -from gemd.entity.link_by_uid import LinkByUID from gemd.util import flatten def test_material_run(): - """ - Test the ability to create a MaterialRun that is linked to a MaterialSpec. + """Test the ability to create a MaterialRun that is linked to a MaterialSpec. Make sure all enumerated values are respected, and check consistency after serializing and deserializing. """ # Define a property, and make sure that an inappropriate value for origin throws ValueError with pytest.raises(ValueError): - prop = Property(name="A property", origin="bad origin", value=NominalReal(17, units='')) + prop = Property(name="A property", origin="bad origin", value=NominalReal(17, units="")) # Create a MaterialSpec with a property - prop = Property(name="A property", origin="specified", value=NominalReal(17, units='')) + prop = Property(name="A property", origin="specified", value=NominalReal(17, units="")) mat_spec = MaterialSpec( name="a specification for a material", properties=PropertyAndConditions(prop), - notes="Funny lookin'" + notes="Funny lookin'", ) # Make sure that when property is serialized, origin (an enumeration) is serialized as a string copy_prop = json_builtin.loads(gemd_json.dumps(mat_spec)) - copy_origin = copy_prop["context"][0]["properties"][0]['property']['origin'] + copy_origin = copy_prop["context"][0]["properties"][0]["property"]["origin"] assert isinstance(copy_origin, str) # Create a MaterialRun, and make sure an inappropriate value for sample_type throws ValueError @@ -44,13 +45,14 @@ def test_material_run(): # ensure that serialization does not change the MaterialRun copy = gemd_json.loads(gemd_json.dumps(mat)) - assert gemd_json.dumps(copy) == gemd_json.dumps(mat), \ + assert gemd_json.dumps(copy) == gemd_json.dumps(mat), ( "Material run is modified by serialization or deserialization" + ) def test_process_run(): """Test that a process run can house a material, and that it survives serde.""" - process_run = ProcessRun("Bake a cake", uids={'My_ID': str(17)}) + process_run = ProcessRun("Bake a cake", uids={"My_ID": str(17)}) material_run = MaterialRun("A cake", process=process_run) # Check that a bidirectional link is established @@ -60,14 +62,14 @@ def test_process_run(): copy_material = gemd_json.loads(gemd_json.dumps(material_run)) assert gemd_json.dumps(copy_material) == gemd_json.dumps(material_run) - assert 'output_material' in repr(process_run) - assert 'process' in repr(material_run) + assert "output_material" in repr(process_run) + assert "process" in repr(material_run) def test_process_id_link(): """Test that a process run can house a LinkByUID object, and that it survives serde.""" uid = str(uuid4()) - proc_link = LinkByUID(scope='id', id=uid) + proc_link = LinkByUID(scope="id", id=uid) mat_run = MaterialRun("Another cake", process=proc_link) copy_material = gemd_json.loads(gemd_json.dumps(mat_run)) assert gemd_json.dumps(copy_material) == gemd_json.dumps(mat_run) @@ -102,9 +104,9 @@ def test_invalid_assignment(): def test_template_access(): """A material run's template should be equal to its spec's template.""" - template = MaterialTemplate("material template", uids={'id': str(uuid4())}) - spec = MaterialSpec("A spec", uids={'id': str(uuid4())}, template=template) - mat = MaterialRun("A run", uids={'id': str(uuid4())}, spec=spec) + template = MaterialTemplate("material template", uids={"id": str(uuid4())}) + spec = MaterialSpec("A spec", uids={"id": str(uuid4())}, template=template) + mat = MaterialRun("A run", uids={"id": str(uuid4())}, spec=spec) assert mat.template == template mat.spec = LinkByUID.from_entity(spec) @@ -113,22 +115,28 @@ def test_template_access(): def test_build(): """Test that build recreates the material.""" - spec = MaterialSpec("A spec", - properties=PropertyAndConditions( - property=Property("a property", value=NominalReal(3, ''))), - tags=["a tag"]) + spec = MaterialSpec( + "A spec", + properties=PropertyAndConditions( + property=Property("a property", value=NominalReal(3, "")) + ), + tags=["a tag"], + ) mat = MaterialRun(name="a material", spec=spec) mat_dict = mat.as_dict() - mat_dict['spec'] = mat.spec.as_dict() + mat_dict["spec"] = mat.spec.as_dict() assert MaterialRun.build(mat_dict) == mat def test_equality(): """Test that equality check works as expected.""" - spec = MaterialSpec("A spec", - properties=PropertyAndConditions( - property=Property("a property", value=NominalReal(3, ''))), - tags=["a tag"]) + spec = MaterialSpec( + "A spec", + properties=PropertyAndConditions( + property=Property("a property", value=NominalReal(3, "")) + ), + tags=["a tag"], + ) mat1 = MaterialRun("A material", spec=spec) mat2 = MaterialRun("A material", spec=spec, tags=["A tag"]) assert mat1 == deepcopy(mat1) @@ -142,10 +150,10 @@ def test_equality(): mat4 = deepcopy(mat3) assert mat4 == mat3, "Copy somehow failed" - mat4.measurements[0].tags.append('A tag') + mat4.measurements[0].tags.append("A tag") assert mat4 != mat3 - mat5 = next(x for x in flatten(mat4, 'test-scope') if isinstance(x, MaterialRun)) + mat5 = next(x for x in flatten(mat4, "test-scope") if isinstance(x, MaterialRun)) assert mat5 == mat4, "Flattening removes measurement references, but that's okay" diff --git a/tests/entity/object/test_material_spec.py b/tests/entity/object/test_material_spec.py index 8df92d1f..37391c11 100644 --- a/tests/entity/object/test_material_spec.py +++ b/tests/entity/object/test_material_spec.py @@ -1,12 +1,13 @@ """Tests of the material spec object.""" + import pytest -from gemd.entity.attribute import PropertyAndConditions, Property, Condition +from gemd.entity.attribute import Condition, Property, PropertyAndConditions from gemd.entity.bounds import IntegerBounds -from gemd.entity.object import ProcessSpec, MaterialSpec -from gemd.entity.template import MaterialTemplate, PropertyTemplate, ConditionTemplate +from gemd.entity.bounds_validation import WarningLevel, validation_level +from gemd.entity.object import MaterialSpec, ProcessSpec +from gemd.entity.template import ConditionTemplate, MaterialTemplate, PropertyTemplate from gemd.entity.value import NominalInteger -from gemd.entity.bounds_validation import validation_level, WarningLevel def test_process_reassignment(): @@ -42,15 +43,15 @@ def test_mat_spec_properties(caplog): mat_spec = MaterialSpec("Material Spec", template=mat_tmpl) good_prop = PropertyAndConditions( property=Property("Name", value=NominalInteger(1), template=prop_tmpl), - conditions=[Condition("Name", value=NominalInteger(1), template=cond_tmpl)] + conditions=[Condition("Name", value=NominalInteger(1), template=cond_tmpl)], ) bad_prop = PropertyAndConditions( property=Property("Name", value=NominalInteger(2), template=prop_tmpl), - conditions=[Condition("Name", value=NominalInteger(1), template=cond_tmpl)] + conditions=[Condition("Name", value=NominalInteger(1), template=cond_tmpl)], ) bad_cond = PropertyAndConditions( # This will pass since we don't have a condition constraint property=Property("Name", value=NominalInteger(1), template=prop_tmpl), - conditions=[Condition("Name", value=NominalInteger(2), template=cond_tmpl)] + conditions=[Condition("Name", value=NominalInteger(2), template=cond_tmpl)], ) with validation_level(WarningLevel.IGNORE): mat_spec.properties.append(good_prop) @@ -79,13 +80,16 @@ def test_dependencies(): cond = ConditionTemplate(name="name", bounds=IntegerBounds(0, 1)) template = MaterialTemplate("measurement template") - spec = MaterialSpec("A spec", template=template, - properties=[PropertyAndConditions( - property=Property("name", template=prop, value=NominalInteger(1)), - conditions=[ - Condition("name", template=cond, value=NominalInteger(1)) - ] - )]) + spec = MaterialSpec( + "A spec", + template=template, + properties=[ + PropertyAndConditions( + property=Property("name", template=prop, value=NominalInteger(1)), + conditions=[Condition("name", template=cond, value=NominalInteger(1))], + ) + ], + ) assert template in spec.all_dependencies() assert cond in spec.all_dependencies() diff --git a/tests/entity/object/test_measurement_run.py b/tests/entity/object/test_measurement_run.py index 835f5f01..15743697 100644 --- a/tests/entity/object/test_measurement_run.py +++ b/tests/entity/object/test_measurement_run.py @@ -1,44 +1,48 @@ """Tests of the measurement run object.""" -import pytest + from uuid import uuid4 -from gemd.json import dumps, loads -from gemd.entity.bounds import IntegerBounds -from gemd.entity.object import MeasurementRun, MaterialRun -from gemd.entity.object.measurement_spec import MeasurementSpec +import pytest + from gemd.entity.attribute import Condition, Parameter, Property -from gemd.entity.source.performed_source import PerformedSource -from gemd.entity.template import MeasurementTemplate, PropertyTemplate, ParameterTemplate, \ - ConditionTemplate -from gemd.entity.value import NominalReal, NominalInteger +from gemd.entity.bounds import IntegerBounds, RealBounds +from gemd.entity.bounds_validation import WarningLevel, validation_level from gemd.entity.file_link import FileLink from gemd.entity.link_by_uid import LinkByUID -from gemd.entity.bounds import RealBounds -from gemd.entity.bounds_validation import validation_level, WarningLevel +from gemd.entity.object import MaterialRun, MeasurementRun +from gemd.entity.object.measurement_spec import MeasurementSpec +from gemd.entity.source.performed_source import PerformedSource +from gemd.entity.template import ( + ConditionTemplate, + MeasurementTemplate, + ParameterTemplate, + PropertyTemplate, +) +from gemd.entity.value import NominalInteger, NominalReal +from gemd.json import dumps, loads from gemd.util.impl import substitute_links def test_measurement_spec(): """Test the measurement spec/run connection survives ser/de.""" - condition = Condition(name="Temp condition", value=NominalReal(nominal=298, units='kelvin')) + condition = Condition(name="Temp condition", value=NominalReal(nominal=298, units="kelvin")) parameter = Parameter(name="Important parameter") spec = MeasurementSpec( - name="Precise way to do a measurement", - parameters=parameter, - conditions=condition + name="Precise way to do a measurement", parameters=parameter, conditions=condition ) # Create a measurement run from this measurement spec measurement = MeasurementRun("The Measurement", conditions=condition, spec=spec) copy = loads(dumps(measurement)) - assert dumps(copy.spec) == dumps(measurement.spec), \ + assert dumps(copy.spec) == dumps(measurement.spec), ( "Measurement spec should be preserved if measurement run is serialized" + ) def test_material_soft_link(): """Test that a measurement run can link to a material run, and that it survives serde.""" - dye = MaterialRun("rhodamine", file_links=FileLink(filename='a.csv', url='/a/path')) + dye = MaterialRun("rhodamine", file_links=FileLink(filename="a.csv", url="/a/path")) assert dye.measurements == [], "default value of .measurements should be an empty list" # The .measurements member should not be settable @@ -47,8 +51,8 @@ def test_material_soft_link(): absorbance = MeasurementRun( name="Absorbance", - uids={'id': str(uuid4())}, - properties=[Property(name='Abs at 500 nm', value=NominalReal(0.1, ''))] + uids={"id": str(uuid4())}, + properties=[Property(name="Abs at 500 nm", value=NominalReal(0.1, ""))], ) assert absorbance.material is None, "Measurements should have None as the material by default" absorbance.material = dye @@ -57,31 +61,34 @@ def test_material_soft_link(): fluorescence = MeasurementRun( name="Fluorescence", - uids={'id': str(uuid4())}, - properties=[Property(name='PL counts at 550 nm', value=NominalReal(30000, ''))], - material=dye + uids={"id": str(uuid4())}, + properties=[Property(name="PL counts at 550 nm", value=NominalReal(30000, ""))], + material=dye, ) assert fluorescence.material == dye, "Material not set correctly for measurement" - assert dye.measurements == [absorbance, fluorescence], \ + assert dye.measurements == [absorbance, fluorescence], ( "Soft-link from material to measurements not created" + ) - assert loads(dumps(absorbance)) == absorbance, \ + assert loads(dumps(absorbance)) == absorbance, ( "Measurement should remain unchanged when serialized" - assert loads(dumps(fluorescence)) == fluorescence, \ + ) + assert loads(dumps(fluorescence)) == fluorescence, ( "Measurement should remain unchanged when serialized" + ) - assert 'measurements' in repr(dye) - assert 'material' in repr(fluorescence) - assert 'material' in repr(absorbance) + assert "measurements" in repr(dye) + assert "material" in repr(fluorescence) + assert "material" in repr(absorbance) subbed = substitute_links(dye) - assert 'measurements' in repr(subbed) + assert "measurements" in repr(subbed) def test_material_id_link(): """Check that a measurement can be linked to a material that is a LinkByUID.""" - mat = LinkByUID('id', str(uuid4())) + mat = LinkByUID("id", str(uuid4())) meas = MeasurementRun("name", material=mat) assert meas.material == mat assert loads(dumps(meas)) == meas @@ -120,7 +127,7 @@ def test_measurement_reassignment(): def test_invalid_assignment(): """Invalid assignments to `material` or `spec` throw a TypeError.""" with pytest.raises(TypeError): - MeasurementRun("name", spec=Condition("value of pi", value=NominalReal(3.14159, ''))) + MeasurementRun("name", spec=Condition("value of pi", value=NominalReal(3.14159, ""))) with pytest.raises(TypeError): MeasurementRun("name", material=FileLink("filename", "url")) with pytest.raises(TypeError): @@ -160,9 +167,9 @@ def test_template_validations(caplog): def test_template_access(): """A measurement run's template should be equal to its spec's template.""" - template = MeasurementTemplate("measurement template", uids={'id': str(uuid4())}) - spec = MeasurementSpec("A spec", uids={'id': str(uuid4())}, template=template) - meas = MeasurementRun("A run", uids={'id': str(uuid4())}, spec=spec) + template = MeasurementTemplate("measurement template", uids={"id": str(uuid4())}) + spec = MeasurementSpec("A spec", uids={"id": str(uuid4())}, template=template) + meas = MeasurementRun("A run", uids={"id": str(uuid4())}, spec=spec) assert meas.template == template meas.spec = LinkByUID.from_entity(spec) @@ -175,23 +182,19 @@ def test_dependencies(): cond = ConditionTemplate(name="name", bounds=IntegerBounds(0, 1)) param = ParameterTemplate(name="name", bounds=IntegerBounds(0, 1)) - template = MeasurementTemplate("measurement template", - parameters=[param], - conditions=[cond], - properties=[prop]) + template = MeasurementTemplate( + "measurement template", parameters=[param], conditions=[cond], properties=[prop] + ) spec = MeasurementSpec("A spec", template=template) mat = MaterialRun(name="mr") - meas = MeasurementRun("A run", spec=spec, material=mat, - properties=[ - Property(prop.name, template=prop, value=NominalInteger(1)) - ], - conditions=[ - Condition(cond.name, template=cond, value=NominalInteger(1)) - ], - parameters=[ - Parameter(param.name, template=param, value=NominalInteger(1)) - ] - ) + meas = MeasurementRun( + "A run", + spec=spec, + material=mat, + properties=[Property(prop.name, template=prop, value=NominalInteger(1))], + conditions=[Condition(cond.name, template=cond, value=NominalInteger(1))], + parameters=[Parameter(param.name, template=param, value=NominalInteger(1))], + ) assert template not in meas.all_dependencies() assert spec in meas.all_dependencies() diff --git a/tests/entity/object/test_process_run.py b/tests/entity/object/test_process_run.py index 969b954f..45a1218d 100644 --- a/tests/entity/object/test_process_run.py +++ b/tests/entity/object/test_process_run.py @@ -1,13 +1,15 @@ """Tests of the process run object.""" -import pytest -from uuid import uuid4 + from copy import deepcopy +from uuid import uuid4 + +import pytest -from gemd.json import dumps, loads from gemd.entity.attribute import Condition -from gemd.entity.object import ProcessRun, ProcessSpec, IngredientRun, MaterialRun -from gemd.entity.template import ProcessTemplate from gemd.entity.link_by_uid import LinkByUID +from gemd.entity.object import IngredientRun, MaterialRun, ProcessRun, ProcessSpec +from gemd.entity.template import ProcessTemplate +from gemd.json import dumps, loads from gemd.util import flatten @@ -22,23 +24,24 @@ def test_process_spec(): process = ProcessRun("Run", conditions=condition2, spec=spec) copy_process = loads(dumps(process)) - assert dumps(copy_process.spec) == dumps(spec), \ + assert dumps(copy_process.spec) == dumps(spec), ( "Process spec should be preserved through serialization" + ) def test_ingredient_run(): """Tests that a process can house an ingredient, and that pairing survives serialization.""" # Create a ProcessSpec proc_run = ProcessRun(name="a process spec", tags=["tag1", "tag2"]) - ingred_run = IngredientRun(material=MaterialRun(name='Raw'), process=proc_run) + ingred_run = IngredientRun(material=MaterialRun(name="Raw"), process=proc_run) # Make copies of both specs proc_run_copy = loads(dumps(proc_run)) assert proc_run_copy == proc_run, "Full structure wasn't preserved across serialization" - assert 'process' in repr(ingred_run) - assert 'ingredients' in repr(proc_run) + assert "process" in repr(ingred_run) + assert "ingredients" in repr(proc_run) def test_invalid_assignment(): @@ -51,9 +54,9 @@ def test_invalid_assignment(): def test_template_access(): """A process run's template should be equal to its spec's template.""" - template = ProcessTemplate("process template", uids={'id': str(uuid4())}) - spec = ProcessSpec("A spec", uids={'id': str(uuid4())}, template=template) - proc = ProcessRun("A run", uids={'id': str(uuid4())}, spec=spec) + template = ProcessTemplate("process template", uids={"id": str(uuid4())}) + spec = ProcessSpec("A spec", uids={"id": str(uuid4())}, template=template) + proc = ProcessRun("A run", uids={"id": str(uuid4())}, spec=spec) assert proc.template == template proc.spec = LinkByUID.from_entity(spec) @@ -72,8 +75,8 @@ def test_equality(): run3 = deepcopy(run2) assert run3 == run2, "Copy somehow failed" - run3.ingredients[0].tags.append('A tag') + run3.ingredients[0].tags.append("A tag") assert run3 != run2 - run4 = next(x for x in flatten(run3, 'test-scope') if isinstance(x, ProcessRun)) + run4 = next(x for x in flatten(run3, "test-scope") if isinstance(x, ProcessRun)) assert run4 == run3, "Flattening removes measurement references, but that's okay" diff --git a/tests/entity/object/test_process_spec.py b/tests/entity/object/test_process_spec.py index 9df51c63..d6196f2e 100644 --- a/tests/entity/object/test_process_spec.py +++ b/tests/entity/object/test_process_spec.py @@ -1,11 +1,13 @@ """Tests of the process spec object.""" -import pytest + from copy import deepcopy -from gemd.json import dumps, loads -from gemd.entity.attribute import PropertyAndConditions, Property -from gemd.entity.object import ProcessSpec, MaterialSpec, IngredientSpec +import pytest + +from gemd.entity.attribute import Property, PropertyAndConditions +from gemd.entity.object import IngredientSpec, MaterialSpec, ProcessSpec from gemd.entity.value import DiscreteCategorical +from gemd.json import dumps, loads from gemd.util import flatten @@ -16,12 +18,12 @@ def test_material_spec(): # Create MaterialSpec without a ProcessSpec prop = Property( - name="The material is a solid", - value=DiscreteCategorical(probabilities="solid") + name="The material is a solid", value=DiscreteCategorical(probabilities="solid") ) mat_spec = MaterialSpec(name="a material spec", properties=PropertyAndConditions(prop)) - assert mat_spec.process is None, \ + assert mat_spec.process is None, ( "MaterialSpec should be initialized with no ProcessSpec, by default" + ) # Assign a ProcessSpec to mat_spec, first ensuring that the type is enforced with pytest.raises(TypeError): @@ -29,28 +31,32 @@ def test_material_spec(): mat_spec.process = proc_spec # Assert circular links - assert dumps(proc_spec.output_material.process) == dumps(proc_spec), \ + assert dumps(proc_spec.output_material.process) == dumps(proc_spec), ( "ProcessSpec should link to MaterialSpec that links back to itself" + ) - assert dumps(mat_spec.process.output_material) == dumps(mat_spec), \ + assert dumps(mat_spec.process.output_material) == dumps(mat_spec), ( "MaterialSpec should link to ProcessSpec that links back to itself" + ) # Make copies of both specs mat_spec_copy = loads(dumps(mat_spec)) proc_spec_copy = loads(dumps(proc_spec)) - assert proc_spec_copy.output_material == mat_spec, \ + assert proc_spec_copy.output_material == mat_spec, ( "Serialization should preserve link from ProcessSpec to MaterialSpec" + ) - assert mat_spec_copy.process == proc_spec, \ + assert mat_spec_copy.process == proc_spec, ( "Serialization should preserve link from MaterialSpec to ProcessSpec" + ) def test_ingredient_spec(): """Tests that a process can house an ingredient, and that pairing survives serialization.""" # Create a ProcessSpec proc_spec = ProcessSpec(name="a process spec", tags=["tag1", "tag2"]) - IngredientSpec(name='Input', material=MaterialSpec(name='Raw'), process=proc_spec) + IngredientSpec(name="Input", material=MaterialSpec(name="Raw"), process=proc_spec) # Make copies of both specs proc_spec_copy = loads(dumps(proc_spec)) @@ -77,10 +83,10 @@ def test_equality(): spec4 = deepcopy(spec3) assert spec4 == spec3, "Copy somehow failed" - spec4.ingredients[0].tags.append('A tag') + spec4.ingredients[0].tags.append("A tag") assert spec4 != spec3 - spec5 = next(x for x in flatten(spec4, 'test-scope') if isinstance(x, ProcessSpec)) + spec5 = next(x for x in flatten(spec4, "test-scope") if isinstance(x, ProcessSpec)) assert spec5 == spec4, "Flattening removes measurement references, but that's okay" diff --git a/tests/entity/template/test_base_attribute_template.py b/tests/entity/template/test_base_attribute_template.py index 6febab78..deb63995 100644 --- a/tests/entity/template/test_base_attribute_template.py +++ b/tests/entity/template/test_base_attribute_template.py @@ -1,13 +1,14 @@ """Test of the base attribute template.""" + import pytest from gemd.entity.bounds.categorical_bounds import CategoricalBounds from gemd.entity.bounds.real_bounds import RealBounds -from gemd.entity.value.uniform_real import UniformReal from gemd.entity.template.attribute_template import AttributeTemplate -from gemd.entity.template.property_template import PropertyTemplate from gemd.entity.template.condition_template import ConditionTemplate from gemd.entity.template.parameter_template import ParameterTemplate +from gemd.entity.template.property_template import PropertyTemplate +from gemd.entity.value.uniform_real import UniformReal from gemd.json import dumps, loads @@ -31,7 +32,7 @@ def test_invalid_bounds(): with pytest.raises(ValueError): SampleAttributeTemplate(name="name") # Must have a bounds with pytest.raises(TypeError): - SampleAttributeTemplate(name="name", bounds=UniformReal(0, 1, '')) + SampleAttributeTemplate(name="name", bounds=UniformReal(0, 1, "")) def test_json(): @@ -44,9 +45,9 @@ def test_json(): def test_dependencies(): """Test that dependency lists make sense.""" targets = [ - PropertyTemplate(name="name", bounds=RealBounds(0, 1, '')), - ConditionTemplate(name="name", bounds=RealBounds(0, 1, '')), - ParameterTemplate(name="name", bounds=RealBounds(0, 1, '')), + PropertyTemplate(name="name", bounds=RealBounds(0, 1, "")), + ConditionTemplate(name="name", bounds=RealBounds(0, 1, "")), + ParameterTemplate(name="name", bounds=RealBounds(0, 1, "")), ] for target in targets: assert len(target.all_dependencies()) == 0, f"{type(target)} had dependencies" diff --git a/tests/entity/template/test_measurement_template.py b/tests/entity/template/test_measurement_template.py index db765d92..cce55b29 100644 --- a/tests/entity/template/test_measurement_template.py +++ b/tests/entity/template/test_measurement_template.py @@ -1,22 +1,27 @@ import pytest +from gemd.entity.attribute import Condition, Parameter, Property, PropertyAndConditions from gemd.entity.bounds import IntegerBounds -from gemd.entity.value import NominalInteger -from gemd.entity.attribute import Condition, Property, Parameter, PropertyAndConditions -from gemd.entity.template import MeasurementTemplate, PropertyTemplate, ConditionTemplate, \ - ParameterTemplate -from gemd.entity.template.attribute_template import AttributeTemplate from gemd.entity.bounds.base_bounds import BaseBounds +from gemd.entity.template import ( + ConditionTemplate, + MeasurementTemplate, + ParameterTemplate, + PropertyTemplate, +) +from gemd.entity.template.attribute_template import AttributeTemplate from gemd.entity.valid_list import ValidList +from gemd.entity.value import NominalInteger def test_mixins(): """Measurement templates have all 3 mixin traits.""" - obj = MeasurementTemplate("Name", - properties=[PropertyTemplate("Name", bounds=IntegerBounds(0, 1))], - conditions=[ConditionTemplate("Name", bounds=IntegerBounds(0, 1))], - parameters=[ParameterTemplate("Name", bounds=IntegerBounds(0, 1))], - ) + obj = MeasurementTemplate( + "Name", + properties=[PropertyTemplate("Name", bounds=IntegerBounds(0, 1))], + conditions=[ConditionTemplate("Name", bounds=IntegerBounds(0, 1))], + parameters=[ParameterTemplate("Name", bounds=IntegerBounds(0, 1))], + ) with pytest.raises(TypeError): obj.properties.append(ConditionTemplate("3", bounds=IntegerBounds(0, 5))) with pytest.raises(TypeError): @@ -33,19 +38,20 @@ def test_mixins(): obj.parameters.append(PropertyTemplate("3", bounds=IntegerBounds(0, 5))) with pytest.raises(TypeError): # You passed a `scalar` to extend - obj.properties.extend((PropertyTemplate("3", bounds=IntegerBounds(0, 5)), - IntegerBounds(1, 3))) + obj.properties.extend( + (PropertyTemplate("3", bounds=IntegerBounds(0, 5)), IntegerBounds(1, 3)) + ) with pytest.raises(ValueError): # You passed a `scalar` to extend - obj.properties = (PropertyTemplate("3", bounds=IntegerBounds(1, 3)), - IntegerBounds(0, 5)) - - obj.properties.append((PropertyTemplate("2", bounds=IntegerBounds(0, 5)), - IntegerBounds(1, 3))) - obj.properties.extend([PropertyTemplate("3", bounds=IntegerBounds(0, 5)), - (PropertyTemplate("4", bounds=IntegerBounds(0, 5)), - IntegerBounds(1, 3)) - ]) + obj.properties = (PropertyTemplate("3", bounds=IntegerBounds(1, 3)), IntegerBounds(0, 5)) + + obj.properties.append((PropertyTemplate("2", bounds=IntegerBounds(0, 5)), IntegerBounds(1, 3))) + obj.properties.extend( + [ + PropertyTemplate("3", bounds=IntegerBounds(0, 5)), + (PropertyTemplate("4", bounds=IntegerBounds(0, 5)), IntegerBounds(1, 3)), + ] + ) obj.conditions.insert(1, ConditionTemplate("2", bounds=IntegerBounds(0, 1))) obj.parameters[0] = ParameterTemplate("Name", bounds=IntegerBounds(0, 1)) @@ -58,17 +64,12 @@ def test_mixins(): if y[1] is not None: assert isinstance(y[1], BaseBounds) - second = MeasurementTemplate("Name", - properties=[PropertyTemplate("Name", bounds=IntegerBounds(0, 1)), - IntegerBounds(0, 1) - ], - conditions=[ConditionTemplate("Name", bounds=IntegerBounds(0, 1)), - IntegerBounds(0, 1) - ], - parameters=[ParameterTemplate("Name", bounds=IntegerBounds(0, 1)), - IntegerBounds(0, 1) - ], - ) + second = MeasurementTemplate( + "Name", + properties=[PropertyTemplate("Name", bounds=IntegerBounds(0, 1)), IntegerBounds(0, 1)], + conditions=[ConditionTemplate("Name", bounds=IntegerBounds(0, 1)), IntegerBounds(0, 1)], + parameters=[ParameterTemplate("Name", bounds=IntegerBounds(0, 1)), IntegerBounds(0, 1)], + ) assert len(second.properties) == 1 assert len(second.conditions) == 1 assert len(second.parameters) == 1 @@ -76,63 +77,69 @@ def test_mixins(): good_val = NominalInteger(1) bad_val = NominalInteger(2) - assert second.validate_condition(Condition("Other name", - value=good_val, - template=second.conditions[0][0])), \ - "Condition with template and good value didn't validate." - assert not second.validate_condition(Condition("Other name", - value=bad_val, - template=second.conditions[0][0])), \ - "Condition with template and bad value DID validate." - assert second.validate_parameter(Parameter("Other name", - value=good_val, - template=second.parameters[0][0])), \ - "Parameter with template and good value didn't validate." - assert not second.validate_parameter(Parameter("Other name", - value=bad_val, - template=second.parameters[0][0])), \ - "Parameter with template and bad value DID validate." - assert second.validate_property(Property("Other name", - value=good_val, - template=second.properties[0][0])), \ - "Property with template and good value didn't validate." - assert not second.validate_property(Property("Other name", - value=bad_val, - template=second.properties[0][0])), \ - "Property with template and bad value DID validate." - - assert second.validate_condition(Condition("Name", value=good_val)), \ + assert second.validate_condition( + Condition("Other name", value=good_val, template=second.conditions[0][0]) + ), "Condition with template and good value didn't validate." + assert not second.validate_condition( + Condition("Other name", value=bad_val, template=second.conditions[0][0]) + ), "Condition with template and bad value DID validate." + assert second.validate_parameter( + Parameter("Other name", value=good_val, template=second.parameters[0][0]) + ), "Parameter with template and good value didn't validate." + assert not second.validate_parameter( + Parameter("Other name", value=bad_val, template=second.parameters[0][0]) + ), "Parameter with template and bad value DID validate." + assert second.validate_property( + Property("Other name", value=good_val, template=second.properties[0][0]) + ), "Property with template and good value didn't validate." + assert not second.validate_property( + Property("Other name", value=bad_val, template=second.properties[0][0]) + ), "Property with template and bad value DID validate." + + assert second.validate_condition(Condition("Name", value=good_val)), ( "Condition without template and good value didn't validate." - assert not second.validate_condition(Condition("Name", value=bad_val)), \ + ) + assert not second.validate_condition(Condition("Name", value=bad_val)), ( "Condition without template and bad value DID validate." - assert second.validate_parameter(Parameter("Name", value=good_val)), \ + ) + assert second.validate_parameter(Parameter("Name", value=good_val)), ( "Parameter without template and good value didn't validate." - assert not second.validate_parameter(Parameter("Name", value=bad_val)), \ + ) + assert not second.validate_parameter(Parameter("Name", value=bad_val)), ( "Parameter without template and bad value DID validate." - assert second.validate_property(Property("Name", value=good_val)), \ + ) + assert second.validate_property(Property("Name", value=good_val)), ( "Property without template and good value didn't validate." - assert not second.validate_property(Property("Name", value=bad_val)), \ + ) + assert not second.validate_property(Property("Name", value=bad_val)), ( "Property without template and bad value DID validate." + ) - assert second.validate_condition(Condition("Other name", value=bad_val)), \ + assert second.validate_condition(Condition("Other name", value=bad_val)), ( "Unmatched condition and bad value didn't validate." - assert second.validate_parameter(Parameter("Other name", value=bad_val)), \ + ) + assert second.validate_parameter(Parameter("Other name", value=bad_val)), ( "Unmatched parameter and bad value didn't validate." - assert second.validate_property(Property("Other name", value=bad_val)), \ + ) + assert second.validate_property(Property("Other name", value=bad_val)), ( "Unmatched property and bad value didn't validate." + ) second.conditions[0][1] = None second.parameters[0][1] = None second.properties[0][1] = None - assert second.validate_condition(Condition("Name", value=good_val)), \ + assert second.validate_condition(Condition("Name", value=good_val)), ( "Condition and good value with passthrough didn't validate." - assert second.validate_parameter(Parameter("Name", value=good_val)), \ + ) + assert second.validate_parameter(Parameter("Name", value=good_val)), ( "Parameter and good value with passthrough didn't validate." - assert second.validate_property(Property("Name", value=good_val)), \ + ) + assert second.validate_property(Property("Name", value=good_val)), ( "Property and good value with passthrough didn't validate." + ) assert second.validate_property( - PropertyAndConditions(property=Property("Name", value=good_val))), \ - "PropertyAndConditions didn't fall back to Property." + PropertyAndConditions(property=Property("Name", value=good_val)) + ), "PropertyAndConditions didn't fall back to Property." def test_links_as_templates(): @@ -140,11 +147,12 @@ def test_links_as_templates(): prop_tmpl = PropertyTemplate("Name", uids={"scope": "prop"}, bounds=IntegerBounds(1, 5)) cond_tmpl = ConditionTemplate("Name", uids={"scope": "cond"}, bounds=IntegerBounds(1, 5)) param_tmpl = ParameterTemplate("Name", uids={"scope": "param"}, bounds=IntegerBounds(1, 5)) - no_bounds = MeasurementTemplate("Name", - properties=[prop_tmpl], - conditions=[cond_tmpl], - parameters=[param_tmpl], - ) + no_bounds = MeasurementTemplate( + "Name", + properties=[prop_tmpl], + conditions=[cond_tmpl], + parameters=[param_tmpl], + ) just_right = NominalInteger(2) middling = NominalInteger(4) @@ -160,43 +168,45 @@ def test_links_as_templates(): for scenario in scenarios: name, attr, validate, tmpl = scenario - assert validate(no_bounds, - attr("Other name", template=tmpl.to_link(), value=middling)), \ + assert validate(no_bounds, attr("Other name", template=tmpl.to_link(), value=middling)), ( f"{name} didn't validate with {name}.template as LinkByUID." - assert not validate(no_bounds, - attr("Other name", template=tmpl.to_link(), value=too_high)), \ - f"{name} DID validate with {name}.template as LinkByUID and bad value." - - with_bounds = MeasurementTemplate("Name", - properties=[(prop_tmpl.to_link(), IntegerBounds(1, 3))], - conditions=[(cond_tmpl.to_link(), IntegerBounds(1, 3))], - parameters=[(param_tmpl.to_link(), IntegerBounds(1, 3))], - ) + ) + assert not validate( + no_bounds, attr("Other name", template=tmpl.to_link(), value=too_high) + ), f"{name} DID validate with {name}.template as LinkByUID and bad value." + + with_bounds = MeasurementTemplate( + "Name", + properties=[(prop_tmpl.to_link(), IntegerBounds(1, 3))], + conditions=[(cond_tmpl.to_link(), IntegerBounds(1, 3))], + parameters=[(param_tmpl.to_link(), IntegerBounds(1, 3))], + ) # Check that Attributes are checked against the bounds when the attributes links for scenario in scenarios: name, attr, validate, tmpl = scenario - assert validate(with_bounds, - attr("Other name", template=tmpl.to_link(), value=just_right)), \ - f"{name} didn't validate with {name}.template as LinkByUID and bounds." - assert not validate(with_bounds, - attr("Other name", template=tmpl.to_link(), value=middling)), \ - f"{name} DID validate with {name}.template as LinkByUID, bad value, and bounds." + assert validate( + with_bounds, attr("Other name", template=tmpl.to_link(), value=just_right) + ), f"{name} didn't validate with {name}.template as LinkByUID and bounds." + assert not validate( + with_bounds, attr("Other name", template=tmpl.to_link(), value=middling) + ), f"{name} DID validate with {name}.template as LinkByUID, bad value, and bounds." - with_links = MeasurementTemplate("Name", - properties=[(prop_tmpl.to_link())], - conditions=[(cond_tmpl.to_link())], - parameters=[(param_tmpl.to_link())], - ) + with_links = MeasurementTemplate( + "Name", + properties=[(prop_tmpl.to_link())], + conditions=[(cond_tmpl.to_link())], + parameters=[(param_tmpl.to_link())], + ) # Check that tests pass when there's no way to test for scenario in scenarios: name, attr, validate, tmpl = scenario - assert validate(with_links, - attr("Other name", template=tmpl.to_link(), value=too_high)), \ + assert validate(with_links, attr("Other name", template=tmpl.to_link(), value=too_high)), ( f"{name} didn't validate with LinkByUID for everything." + ) def test_dependencies(): @@ -205,10 +215,9 @@ def test_dependencies(): cond = ConditionTemplate(name="name", bounds=IntegerBounds(0, 1)) param = ParameterTemplate(name="name", bounds=IntegerBounds(0, 1)) - msr_template = MeasurementTemplate("a process template", - conditions=[cond], - properties=[prop], - parameters=[param]) + msr_template = MeasurementTemplate( + "a process template", conditions=[cond], properties=[prop], parameters=[param] + ) assert prop in msr_template.all_dependencies() assert cond in msr_template.all_dependencies() assert param in msr_template.all_dependencies() diff --git a/tests/entity/template/test_process_template.py b/tests/entity/template/test_process_template.py index fd2b81d8..e3f8d399 100644 --- a/tests/entity/template/test_process_template.py +++ b/tests/entity/template/test_process_template.py @@ -1,26 +1,26 @@ """Tests of the ProcessTemplate object.""" + import pytest from gemd.entity.bounds import IntegerBounds +from gemd.entity.bounds.real_bounds import RealBounds from gemd.entity.link_by_uid import LinkByUID -from gemd.entity.template.process_template import ProcessTemplate from gemd.entity.template.condition_template import ConditionTemplate -from gemd.entity.bounds.real_bounds import RealBounds +from gemd.entity.template.process_template import ProcessTemplate from gemd.json import dumps, loads def test_bounds_mismatch(): """Test that a mismatch between the attribute and given bounds throws a ValueError.""" - attribute_bounds = RealBounds(0, 100, '') - object_bounds = RealBounds(200, 300, '') + attribute_bounds = RealBounds(0, 100, "") + object_bounds = RealBounds(200, 300, "") cond_template = ConditionTemplate("a condition", bounds=attribute_bounds) with pytest.raises(ValueError): ProcessTemplate("a process template", conditions=[[cond_template, object_bounds]]) def test_allowed_names(): - """ - Test that allowed_names can be assigned. + """Test that allowed_names can be assigned. Presently allowed_names is not used for any validation, but this test can be expanded if it is used in the future. @@ -32,8 +32,7 @@ def test_allowed_names(): def test_allowed_labels(): - """ - Test that allowed_labels can be assigned. + """Test that allowed_labels can be assigned. Presently allowed_labels is not used for any validation, but this test can be expanded if it is used in the future. @@ -46,12 +45,15 @@ def test_allowed_labels(): def test_passthrough_bounds(): """Test that unspecified Bounds are accepted and set to None.""" - template = ProcessTemplate('foo', conditions=[ - (LinkByUID('1', '2'), None), - [LinkByUID('3', '4'), None], - LinkByUID('5', '6'), - ConditionTemplate('foo', bounds=IntegerBounds(0, 10)), - ]) + template = ProcessTemplate( + "foo", + conditions=[ + (LinkByUID("1", "2"), None), + [LinkByUID("3", "4"), None], + LinkByUID("5", "6"), + ConditionTemplate("foo", bounds=IntegerBounds(0, 10)), + ], + ) assert len(template.conditions) == 4 for _, bounds in template.conditions: assert bounds is None @@ -59,26 +61,28 @@ def test_passthrough_bounds(): assert len(copied.conditions) == 4 for _, bounds in copied.conditions: assert bounds is None - from_dict = ProcessTemplate.build({ - 'type': 'process_template', - 'name': 'foo', - 'conditions': [ - [ - { - 'scope': 'foo', - 'id': 'bar', - 'type': 'link_by_uid', - }, - None, - ] - ], - }) + from_dict = ProcessTemplate.build( + { + "type": "process_template", + "name": "foo", + "conditions": [ + [ + { + "scope": "foo", + "id": "bar", + "type": "link_by_uid", + }, + None, + ] + ], + } + ) assert len(from_dict.conditions) == 1 def test_dependencies(): """Test that dependency lists make sense.""" - attribute_bounds = RealBounds(0, 100, '') + attribute_bounds = RealBounds(0, 100, "") cond_template = ConditionTemplate("a condition", bounds=attribute_bounds) proc_template = ProcessTemplate("a process template", conditions=[cond_template]) assert cond_template in proc_template.all_dependencies() diff --git a/tests/entity/test_bounds_validation.py b/tests/entity/test_bounds_validation.py index 28f2665f..4ac52468 100644 --- a/tests/entity/test_bounds_validation.py +++ b/tests/entity/test_bounds_validation.py @@ -1,5 +1,9 @@ -from gemd.entity.bounds_validation import WarningLevel, set_validation_level, \ - get_validation_level, validation_level +from gemd.entity.bounds_validation import ( + WarningLevel, + get_validation_level, + set_validation_level, + validation_level, +) def test_bounds_validation(): diff --git a/tests/entity/test_case_insensitive_dict.py b/tests/entity/test_case_insensitive_dict.py index e7151e92..d06850aa 100644 --- a/tests/entity/test_case_insensitive_dict.py +++ b/tests/entity/test_case_insensitive_dict.py @@ -1,82 +1,83 @@ """Tests of the case-insensitive dictionary class.""" + import pytest from gemd.entity.case_insensitive_dict import CaseInsensitiveDict from gemd.entity.object.process_run import ProcessRun -from gemd.json import loads, dumps +from gemd.json import dumps, loads def test_case_sensitivity(): """Test some basic setting and getting operations.""" # If two keys are the same up to case, the dictionary is invalid. - bad_data = {'A': 1, 'a': 2} + bad_data = {"A": 1, "a": 2} with pytest.raises(ValueError): CaseInsensitiveDict(**bad_data) - data = {'key1': 'value1', 'key2': 2} + data = {"key1": "value1", "key2": 2} data_dict = CaseInsensitiveDict(**data) - data_dict['kEY3'] = "three" # A new key-value pair can be added. - data_dict['key2'] = 22 # An existing can be overridden by the exact same key. + data_dict["kEY3"] = "three" # A new key-value pair can be added. + data_dict["key2"] = 22 # An existing can be overridden by the exact same key. # A value can be accessed by the key, no matter what the case is. - assert data_dict['key2'] == 22 - assert data_dict['KEY2'] == 22 - assert data_dict.get('KEY2') == 22 + assert data_dict["key2"] == 22 + assert data_dict["KEY2"] == 22 + assert data_dict.get("KEY2") == 22 # A failed get with a default value is not fatal with pytest.raises(KeyError): - data_dict['KEY4'] == 4 - assert data_dict.get('Key4', 4) == 4 + data_dict["KEY4"] == 4 + assert data_dict.get("Key4", 4) == 4 # Check that the keys maintain their original case - assert set(data_dict.keys()) == {'key1', 'key2', 'kEY3'} + assert set(data_dict.keys()) == {"key1", "key2", "kEY3"} # A value cannot be overridden by a key that is similar but has different case. # If the user has defined an id with scope 'my_id' they shouldn't be setting 'My_ID'. with pytest.raises(ValueError): - data_dict['KEY2'] = 222 + data_dict["KEY2"] = 222 # A bad assignment attempt should not alter the dictionary - assert data_dict['KEY2'] == 22 + assert data_dict["KEY2"] == 22 def test_serde(): """Test that an object with a case-insensitive dict can be serialized properly.""" - process = ProcessRun("A process", uids={'Foo': str(17)}) + process = ProcessRun("A process", uids={"Foo": str(17)}) process_copy = loads(dumps(process)) assert process == process_copy - assert process_copy.uids['foo'] == process_copy.uids['Foo'] + assert process_copy.uids["foo"] == process_copy.uids["Foo"] def test_contains(): """Test checking whether or not a case insensitive dict contains a key.""" - data = {'Key': 'value'} + data = {"Key": "value"} data_dict = CaseInsensitiveDict(**data) - for k in ('key', 'Key', 'KEY'): + for k in ("key", "Key", "KEY"): assert k in data_dict - assert 'not_a_key' not in data_dict + assert "not_a_key" not in data_dict def test_all_dict_methods(): """Tests checking consistency of all standard dictionary methods.""" # __init__ - data = {'K' + x: 'V' + x for x in ('1', '2', '3', '4', '5')} + data = {"K" + x: "V" + x for x in ("1", "2", "3", "4", "5")} ci_dict = CaseInsensitiveDict(**data) assert sorted(list(ci_dict)) == sorted(list(data)) assert len(ci_dict) == len(data) # __getitem__ - assert ci_dict['K1'] == data['K1'] + assert ci_dict["K1"] == data["K1"] # __setitem__ - ci_dict['K6'] = 'V6' - assert ci_dict['K6'] == 'V6' + ci_dict["K6"] = "V6" + assert ci_dict["K6"] == "V6" # __delitem__ - del ci_dict['K6'] - assert 'K6' not in ci_dict + del ci_dict["K6"] + assert "K6" not in ci_dict # iter(d) ci_iter = iter(ci_dict) @@ -92,16 +93,16 @@ def test_all_dict_methods(): # copy dup = ci_dict.copy() - assert type(dup) == type(ci_dict) + assert type(dup) is type(ci_dict) # fromkeys key_copy = CaseInsensitiveDict.fromkeys(dup) assert set(dup) == set(key_copy) - assert type(dup) == type(key_copy) + assert type(dup) is type(key_copy) # get - assert ci_dict.get('K1') == 'v1' - assert ci_dict.get('K6', None) is None + assert ci_dict.get("K1") == "v1" + assert ci_dict.get("K6", None) is None # items for k, v in ci_dict.items(): @@ -112,11 +113,11 @@ def test_all_dict_methods(): assert k not in data # because the cases are all wrong # pop - assert ci_dict.pop('k1') == 'v1' - assert 'K1' not in ci_dict + assert ci_dict.pop("k1") == "v1" + assert "K1" not in ci_dict with pytest.raises(KeyError): - ci_dict.pop('k1') - assert ci_dict.pop('k1', None) is None + ci_dict.pop("k1") + assert ci_dict.pop("k1", None) is None # popitem pop_k, pop_v = ci_dict.popitem() @@ -127,12 +128,12 @@ def test_all_dict_methods(): assert ci_dict.setdefault(pop_k.upper(), pop_v.lower()) == pop_v.upper() # update - ci_dict.update({pop_k.upper(): pop_v.lower(), 'K6': 'v6'}) - ci_dict.update(K6='V6') + ci_dict.update({pop_k.upper(): pop_v.lower(), "K6": "v6"}) + ci_dict.update(K6="V6") with pytest.raises(ValueError): - ci_dict.update(k6='v6') + ci_dict.update(k6="v6") with pytest.raises(ValueError): ci_dict.update({k.lower(): v for k, v in ci_dict.items()}) # values - assert 'V6' in ci_dict.values() + assert "V6" in ci_dict.values() diff --git a/tests/entity/test_entity.py b/tests/entity/test_entity.py index f5a4465c..83d527a2 100644 --- a/tests/entity/test_entity.py +++ b/tests/entity/test_entity.py @@ -1,30 +1,39 @@ """General tests of entities.""" -from abc import ABC + import inspect -import pytest +from abc import ABC from typing import Generic, TypeVar -from gemd import ProcessSpec, IngredientSpec, MaterialSpec, IngredientRun, \ - LinkByUID, ConditionTemplate, MolecularStructureBounds -from gemd.entity.dict_serializable import DictSerializable +import pytest + +from gemd import ( + ConditionTemplate, + IngredientRun, + IngredientSpec, + LinkByUID, + MaterialSpec, + MolecularStructureBounds, + ProcessSpec, +) from gemd.entity.base_entity import BaseEntity +from gemd.entity.dict_serializable import DictSerializable def test_id_case_sensitivity(): """Test that uids are case-insensitive.""" with pytest.raises(ValueError): - IngredientRun(uids={'my_id': 'sample1', 'My_ID': 'sample2'}) + IngredientRun(uids={"my_id": "sample1", "My_ID": "sample2"}) - ingredient = IngredientRun(uids={'my_id': 'sample1'}) - assert ingredient.uids['my_id'] == 'sample1' - assert ingredient.uids['MY_id'] == 'sample1' + ingredient = IngredientRun(uids={"my_id": "sample1"}) + assert ingredient.uids["my_id"] == "sample1" + assert ingredient.uids["MY_id"] == "sample1" def test_id_iterables(): """Test that the uids setter is very forgiving.""" - assert IngredientRun(uids={'my_id': 'sample1'}).uids['my_id'] == 'sample1' - assert IngredientRun(uids=['my_id', 'sample1']).uids['my_id'] == 'sample1' - assert IngredientRun(uids=('my_id', 'sample1')).uids['my_id'] == 'sample1' + assert IngredientRun(uids={"my_id": "sample1"}).uids["my_id"] == "sample1" + assert IngredientRun(uids=["my_id", "sample1"]).uids["my_id"] == "sample1" + assert IngredientRun(uids=("my_id", "sample1")).uids["my_id"] == "sample1" def test_to_link(): @@ -39,8 +48,9 @@ def test_to_link(): with pytest.raises(ValueError): obj.to_link("Third"), "to_link with a scope that an object lacks is fatal" - assert obj.to_link(scope="Third", allow_fallback=True).scope in obj.uids, \ + assert obj.to_link(scope="Third", allow_fallback=True).scope in obj.uids, ( "... unless allow_fallback is set" + ) def test_equality(): @@ -123,9 +133,9 @@ class ChildEntity(BaseEntity, typ=child_typ, skip={child_skip}): def test_mro(): - """This test mimics a citrine-python class inheritance structure.""" - SerializableType = TypeVar('SerializableType', bound='Serializable') - ResourceType = TypeVar('ResourceType', bound='Resource') + """Mimic a citrine-python class inheritance structure.""" + SerializableType = TypeVar("SerializableType", bound="Serializable") + ResourceType = TypeVar("ResourceType", bound="Resource") class Serializable(Generic[SerializableType]): pass @@ -133,13 +143,11 @@ class Serializable(Generic[SerializableType]): class Resource(Serializable[ResourceType]): pass - class DataConcepts(DictSerializable, Serializable['DataConcepts'], ABC): + class DataConcepts(DictSerializable, Serializable["DataConcepts"], ABC): pass class TestConditionTemplate( - DataConcepts, - Resource['TestConditionTemplate'], - ConditionTemplate + DataConcepts, Resource["TestConditionTemplate"], ConditionTemplate ): pass @@ -148,8 +156,8 @@ class TestConditionTemplate( def test_derived_collision(): """Test that an exception is thrown when multiple classes claim the same typ.""" - # One parent - class Parent(DictSerializable, typ="mine"): + + class Parent(DictSerializable, typ="mine"): # One parent pass # First kid is fine @@ -160,5 +168,6 @@ class ElderChild(Parent, typ="mine"): assert DictSerializable.class_mapping["mine"] is ElderChild with pytest.raises(ValueError, match="mine"): + class SecondChild(Parent, typ="mine"): pass diff --git a/tests/entity/test_link_by_uid.py b/tests/entity/test_link_by_uid.py index 6f355520..618c00c7 100644 --- a/tests/entity/test_link_by_uid.py +++ b/tests/entity/test_link_by_uid.py @@ -1,17 +1,16 @@ """General tests of LinkByUID dynamics.""" -import pytest -from gemd.json import dumps, loads -from gemd.entity.object import MaterialRun, ProcessRun, IngredientRun from gemd.entity.link_by_uid import LinkByUID +from gemd.entity.object import IngredientRun, MaterialRun, ProcessRun +from gemd.json import dumps, loads def test_link_by_uid(): """Test that linking works.""" - root = MaterialRun(name='root', process=ProcessRun(name='root proc')) - leaf = MaterialRun(name='leaf', process=ProcessRun(name='leaf proc')) + root = MaterialRun(name="root", process=ProcessRun(name="root proc")) + leaf = MaterialRun(name="leaf", process=ProcessRun(name="leaf proc")) IngredientRun(process=root.process, material=leaf) - IngredientRun(process=root.process, material=LinkByUID.from_entity(leaf, scope='id')) + IngredientRun(process=root.process, material=LinkByUID.from_entity(leaf, scope="id")) # Paranoid assertions about equality's symmetry since it's implemented in 2 places assert root.process.ingredients[0].material == root.process.ingredients[1].material @@ -27,14 +26,14 @@ def test_link_by_uid(): def test_from_entity(): """Test permutations of LinkByUID.from_entity arguments.""" - run = MaterialRun(name='leaf', process=ProcessRun(name='leaf proc')) - assert LinkByUID.from_entity(run).scope == 'auto' - assert LinkByUID.from_entity(run, scope='missing').scope == 'auto' + run = MaterialRun(name="leaf", process=ProcessRun(name="leaf proc")) + assert LinkByUID.from_entity(run).scope == "auto" + assert LinkByUID.from_entity(run, scope="missing").scope == "auto" assert len(run.uids) == 1 - run.uids['foo'] = 'bar' - link1 = LinkByUID.from_entity(run, scope='foo') - assert (link1.scope, link1.id) == ('foo', 'bar') + run.uids["foo"] = "bar" + link1 = LinkByUID.from_entity(run, scope="foo") + assert (link1.scope, link1.id) == ("foo", "bar") def test_equality(): diff --git a/tests/entity/test_util.py b/tests/entity/test_util.py index 19c70739..8807bdac 100644 --- a/tests/entity/test_util.py +++ b/tests/entity/test_util.py @@ -1,20 +1,21 @@ """Tests of entity utils.""" + import pytest -from gemd.entity.util import make_instance, complete_material_history +from gemd.entity.attribute.condition import Condition from gemd.entity.attribute.property import Property from gemd.entity.link_by_uid import LinkByUID -from gemd.entity.object.ingredient_spec import IngredientSpec from gemd.entity.object.ingredient_run import IngredientRun -from gemd.entity.object.material_spec import MaterialSpec +from gemd.entity.object.ingredient_spec import IngredientSpec from gemd.entity.object.material_run import MaterialRun -from gemd.entity.object.measurement_spec import MeasurementSpec +from gemd.entity.object.material_spec import MaterialSpec from gemd.entity.object.measurement_run import MeasurementRun -from gemd.entity.object.process_spec import ProcessSpec +from gemd.entity.object.measurement_spec import MeasurementSpec from gemd.entity.object.process_run import ProcessRun +from gemd.entity.object.process_spec import ProcessSpec +from gemd.entity.util import complete_material_history, make_instance from gemd.entity.value.discrete_categorical import DiscreteCategorical from gemd.entity.value.nominal_real import NominalReal -from gemd.entity.attribute.condition import Condition from gemd.entity.value.uniform_real import UniformReal @@ -23,10 +24,10 @@ def test_make_instance(): msr_spec = MeasurementSpec("Measurement") assert isinstance(make_instance(msr_spec), MeasurementRun) - mat_spec = MaterialSpec(name='Mat name') - mat_spec.process = ProcessSpec(name='Pro name') - IngredientSpec(name='Ing label', process=mat_spec.process) - mat_spec.process.ingredients[0].material = MaterialSpec(name='Baby mat name') + mat_spec = MaterialSpec(name="Mat name") + mat_spec.process = ProcessSpec(name="Pro name") + IngredientSpec(name="Ing label", process=mat_spec.process) + mat_spec.process.ingredients[0].material = MaterialSpec(name="Baby mat name") mat_run = make_instance(mat_spec) assert isinstance(mat_run, MaterialRun) @@ -45,41 +46,45 @@ def test_serialized_history(): # Create several runs and specs linked together buy_spec = LinkByUID("id", "pr723") cookie_dough_spec = MaterialSpec("cookie dough spec", process=buy_spec) - buy_cookie_dough = ProcessRun("Buy cookie dough", uids={'id': '32283'}, spec=buy_spec) + buy_cookie_dough = ProcessRun("Buy cookie dough", uids={"id": "32283"}, spec=buy_spec) cookie_dough = MaterialRun("cookie dough", process=buy_cookie_dough, spec=cookie_dough_spec) - bake = ProcessRun("bake cookie dough", conditions=[ - Condition("oven temp", origin='measured', value=NominalReal(357, 'degF'))]) - IngredientRun(material=cookie_dough, - process=bake, number_fraction=NominalReal(1, '')) + bake = ProcessRun( + "bake cookie dough", + conditions=[Condition("oven temp", origin="measured", value=NominalReal(357, "degF"))], + ) + IngredientRun(material=cookie_dough, process=bake, number_fraction=NominalReal(1, "")) cookie = MaterialRun("cookie", process=bake, tags=["chocolate chip", "drop"]) - MeasurementRun("taste", material=cookie, properties=[ - Property("taste", value=DiscreteCategorical("scrumptious"))]) + MeasurementRun( + "taste", + material=cookie, + properties=[Property("taste", value=DiscreteCategorical("scrumptious"))], + ) cookie_history = complete_material_history(cookie) # There are 7 entities in the serialized list: cookie dough (spec & run), buy cookie dough, # cookie dough ingredient, bake cookie dough, cookie, taste assert len(cookie_history) == 7 for entity in cookie_history: - assert len(entity['uids']) > 0, "Serializing material history should assign uids." + assert len(entity["uids"]) > 0, "Serializing material history should assign uids." # Check that the measurement points to the material - taste_dict = next(x for x in cookie_history if x.get('type') == 'measurement_run') - cookie_dict = next(x for x in cookie_history if x.get('name') == 'cookie') - scope = taste_dict.get('material').get('scope') - assert taste_dict.get('material').get('id') == cookie_dict.get('uids').get(scope) + taste_dict = next(x for x in cookie_history if x.get("type") == "measurement_run") + cookie_dict = next(x for x in cookie_history if x.get("name") == "cookie") + scope = taste_dict.get("material").get("scope") + assert taste_dict.get("material").get("id") == cookie_dict.get("uids").get(scope) # Check that both the material spec and the process run point to the same process spec. # Because that spec was initially a LinkByUID, this also tests the method's ability to # serialize a LinkByUID. - cookie_dough_spec_dict = next(x for x in cookie_history if x.get('type') == 'material_spec') - buy_cookie_dough_dict = next(x for x in cookie_history if x.get('name') == 'Buy cookie dough') - assert cookie_dough_spec_dict.get('process') == buy_spec.as_dict() - assert buy_cookie_dough_dict.get('spec') == buy_spec.as_dict() + cookie_dough_spec_dict = next(x for x in cookie_history if x.get("type") == "material_spec") + buy_cookie_dough_dict = next(x for x in cookie_history if x.get("name") == "Buy cookie dough") + assert cookie_dough_spec_dict.get("process") == buy_spec.as_dict() + assert buy_cookie_dough_dict.get("spec") == buy_spec.as_dict() def test_invalid_instance(): """Calling make_instance on a non-spec should throw a TypeError.""" - not_specs = [MeasurementRun("meas"), Condition("cond"), UniformReal(0, 1, ''), 'foo', 10] + not_specs = [MeasurementRun("meas"), Condition("cond"), UniformReal(0, 1, ""), "foo", 10] for not_spec in not_specs: with pytest.raises(TypeError): make_instance(not_spec) diff --git a/tests/entity/test_valid_list.py b/tests/entity/test_valid_list.py index 3cd633b3..8525a0b4 100644 --- a/tests/entity/test_valid_list.py +++ b/tests/entity/test_valid_list.py @@ -1,18 +1,19 @@ """Tests of the ValidList class.""" + import pytest -from gemd.entity.valid_list import ValidList from gemd.entity.setters import validate_list +from gemd.entity.valid_list import ValidList def test_access_data(): """Test that valid data can be inserted into the list and that invalid data throws an error.""" - lo_strings = ValidList(['z'], str) - lo_strings[0] = 'a' - lo_strings.append('b') - lo_strings.extend(tuple(['d', 'e', 'f'])) - lo_strings.insert(2, 'c') - assert lo_strings == ['a', 'b', 'c', 'd', 'e', 'f'] + lo_strings = ValidList(["z"], str) + lo_strings[0] = "a" + lo_strings.append("b") + lo_strings.extend(tuple(["d", "e", "f"])) + lo_strings.insert(2, "c") + assert lo_strings == ["a", "b", "c", "d", "e", "f"] with pytest.raises(TypeError): lo_strings[0] = 1 @@ -25,7 +26,7 @@ def test_access_data(): with pytest.raises(TypeError): lo_strings.insert(2, 1) - lo_both = ValidList(_list=tuple([1, 'a']), content_type=[int, str]) + lo_both = ValidList(_list=tuple([1, "a"]), content_type=[int, str]) with pytest.raises(TypeError): lo_both[0] = 1.1 @@ -34,7 +35,7 @@ def test_iterables(): """Test that Iterables are handled correctly.""" assert validate_list("Qwerty", str) == ["Qwerty"] assert validate_list(["Qwerty"], str) == ["Qwerty"] - assert validate_list(("Qwerty", ), str) == ["Qwerty"] + assert validate_list(("Qwerty",), str) == ["Qwerty"] assert validate_list(["Q", "w"], str) == ["Q", "w"] @@ -63,9 +64,7 @@ def dummy(val): def test_transform(): """Test that transformations do what we expect in changing data.""" first = [1, 2] - vlst = ValidList(first, - content_type=int, - trigger=lambda x: x + 1) + vlst = ValidList(first, content_type=int, trigger=lambda x: x + 1) vlst.append(3) vlst.extend([4]) vlst.insert(2, 5) @@ -80,7 +79,7 @@ def test_transform(): def test_invalid_content(): """Test that an invalid content_type throws a TypeError.""" with pytest.raises(TypeError): - ValidList(['z'], content_type={'types': [str]}) + ValidList(["z"], content_type={"types": [str]}) with pytest.raises(TypeError): ValidList(_list=tuple([1, 1]), content_type=1) with pytest.raises(TypeError): diff --git a/tests/entity/value/test_discrete_categorical.py b/tests/entity/value/test_discrete_categorical.py index e122558b..ff0f5790 100644 --- a/tests/entity/value/test_discrete_categorical.py +++ b/tests/entity/value/test_discrete_categorical.py @@ -1,8 +1,9 @@ """Tests of the DiscreteCategorical class.""" + import pytest -from gemd.entity.value.discrete_categorical import DiscreteCategorical from gemd.entity.bounds import CategoricalBounds +from gemd.entity.value.discrete_categorical import DiscreteCategorical def test_probabilities_setter(): diff --git a/tests/entity/value/test_empirical_formula.py b/tests/entity/value/test_empirical_formula.py index 30ec26f8..0e72e781 100644 --- a/tests/entity/value/test_empirical_formula.py +++ b/tests/entity/value/test_empirical_formula.py @@ -1,15 +1,16 @@ """Test parsing and serde of empirical chemical formulae.""" + import pytest -from gemd.json import dumps, loads -from gemd.entity.value.empirical_formula import EmpiricalFormula from gemd.entity.bounds import CompositionBounds +from gemd.entity.value.empirical_formula import EmpiricalFormula +from gemd.json import dumps, loads def test_all_elements(): """Check that list of all elements exists and has some select examples.""" for el in ["H", "He", "C", "Si", "Mg", "Al", "Co", "Ce"]: - assert el in EmpiricalFormula.all_elements(), "Couldn't find {} in all_elements".format(el) + assert el in EmpiricalFormula.all_elements(), f"Couldn't find {el} in all_elements" assert len(EmpiricalFormula.all_elements()) == 120, "Expected 120 elements" @@ -17,7 +18,7 @@ def test_json(): """Check that we can json ser/de round-robin.""" empirical = EmpiricalFormula("Al94.5Si5.5") copy = loads(dumps(empirical)) - assert(copy == empirical) + assert copy == empirical def test_formula_setter(): @@ -40,5 +41,5 @@ def test_invalid_formula(): def test_contains(): """Test that bounds know if a Value is contained within it.""" bounds = CompositionBounds({"C", "H", "O", "N"}) - assert bounds.contains(EmpiricalFormula('C2H5OH')._to_bounds()) - assert not bounds.contains(EmpiricalFormula('NaCl')._to_bounds()) + assert bounds.contains(EmpiricalFormula("C2H5OH")._to_bounds()) + assert not bounds.contains(EmpiricalFormula("NaCl")._to_bounds()) diff --git a/tests/entity/value/test_inchi.py b/tests/entity/value/test_inchi.py index f77a6bef..240cbc97 100644 --- a/tests/entity/value/test_inchi.py +++ b/tests/entity/value/test_inchi.py @@ -1,16 +1,17 @@ """Test parsing and serde of an InChI molecular structure.""" + import pytest -from gemd.json import dumps, loads -from gemd.entity.value.inchi_value import InChI from gemd.entity.bounds import MolecularStructureBounds +from gemd.entity.value.inchi_value import InChI +from gemd.json import dumps, loads def test_json(): """Check that we can json ser/de round-robin.""" inchi = InChI("InChI=1/C8H8O3/c1-11-8-4-6(5-9)2-3-7(8)10/h2-5,10H,1H3") copy = loads(dumps(inchi)) - assert(copy == inchi) + assert copy == inchi def test_inchi_setter(): @@ -28,8 +29,7 @@ def test_inchi_setter(): def test_invalid_inchi(): - """ - Check that an invalid InChI throws a TypeError. + """Check that an invalid InChI throws a TypeError. Note that real checking requires an external package. """ diff --git a/tests/entity/value/test_nomial_integer.py b/tests/entity/value/test_nomial_integer.py index aab143ed..073a3df5 100644 --- a/tests/entity/value/test_nomial_integer.py +++ b/tests/entity/value/test_nomial_integer.py @@ -1,8 +1,9 @@ """Tests of the UniformInteger class.""" + import pytest -from gemd.entity.value.nominal_integer import NominalInteger from gemd.entity.bounds import IntegerBounds +from gemd.entity.value.nominal_integer import NominalInteger def test_bounds_are_integers(): diff --git a/tests/entity/value/test_nominal_categorical.py b/tests/entity/value/test_nominal_categorical.py index 9ad16ff4..2919f632 100644 --- a/tests/entity/value/test_nominal_categorical.py +++ b/tests/entity/value/test_nominal_categorical.py @@ -1,6 +1,7 @@ """Tests of the NominalCategorical class.""" -from gemd.entity.value.nominal_categorical import NominalCategorical + from gemd.entity.bounds import CategoricalBounds +from gemd.entity.value.nominal_categorical import NominalCategorical def test_category_setter(): diff --git a/tests/entity/value/test_nominal_composition.py b/tests/entity/value/test_nominal_composition.py index 26139e6f..0c1d30bd 100644 --- a/tests/entity/value/test_nominal_composition.py +++ b/tests/entity/value/test_nominal_composition.py @@ -1,8 +1,9 @@ """Tests of the NominalComposition class.""" + import pytest -from gemd.entity.value.nominal_composition import NominalComposition from gemd.entity.bounds import CompositionBounds +from gemd.entity.value.nominal_composition import NominalComposition def test_quantities_are_dict(): diff --git a/tests/entity/value/test_nominal_real.py b/tests/entity/value/test_nominal_real.py index fdd0c69a..e6a76efa 100644 --- a/tests/entity/value/test_nominal_real.py +++ b/tests/entity/value/test_nominal_real.py @@ -1,10 +1,11 @@ """Tests of the NominalReal class.""" -from gemd.entity.value.nominal_real import NominalReal + from gemd.entity.bounds import RealBounds +from gemd.entity.value.nominal_real import NominalReal def test_contains(): """Test that bounds know if a Value is contained within it.""" - bounds = RealBounds(1, 3, 'm') - assert bounds.contains(NominalReal(200, 'cm')._to_bounds()) - assert not bounds.contains(NominalReal(5, 'm')._to_bounds()) + bounds = RealBounds(1, 3, "m") + assert bounds.contains(NominalReal(200, "cm")._to_bounds()) + assert not bounds.contains(NominalReal(5, "m")._to_bounds()) diff --git a/tests/entity/value/test_normal_real.py b/tests/entity/value/test_normal_real.py index fd591f34..19726082 100644 --- a/tests/entity/value/test_normal_real.py +++ b/tests/entity/value/test_normal_real.py @@ -1,10 +1,11 @@ """Tests of the NormalReal class.""" -from gemd.entity.value.normal_real import NormalReal + from gemd.entity.bounds import RealBounds +from gemd.entity.value.normal_real import NormalReal def test_contains(): """Test that bounds know if a Value is contained within it.""" - bounds = RealBounds(1, 3, 'm') - assert bounds.contains(NormalReal(300, 10, 'cm')._to_bounds()) - assert not bounds.contains(NormalReal(5, 0.1, 'm')._to_bounds()) + bounds = RealBounds(1, 3, "m") + assert bounds.contains(NormalReal(300, 10, "cm")._to_bounds()) + assert not bounds.contains(NormalReal(5, 0.1, "m")._to_bounds()) diff --git a/tests/entity/value/test_smiles.py b/tests/entity/value/test_smiles.py index fc45c932..64f059f4 100644 --- a/tests/entity/value/test_smiles.py +++ b/tests/entity/value/test_smiles.py @@ -1,16 +1,17 @@ """Test parsing and serde of empirical chemical formulae.""" + import pytest -from gemd.json import dumps, loads -from gemd.entity.value.smiles_value import Smiles from gemd.entity.bounds import MolecularStructureBounds +from gemd.entity.value.smiles_value import Smiles +from gemd.json import dumps, loads def test_json(): """Check that we can json ser/de round-robin.""" smiles = Smiles("c1(C=O)cc(OC)c(O)cc1") copy = loads(dumps(smiles)) - assert(copy == smiles) + assert copy == smiles def test_smiles_setter(): @@ -23,8 +24,7 @@ def test_smiles_setter(): def test_invalid_smiles(): - """ - Check that an invalid SMILES throws a TypeError. + """Check that an invalid SMILES throws a TypeError. Note that real checking requires an external package. """ diff --git a/tests/entity/value/test_uniform_integer.py b/tests/entity/value/test_uniform_integer.py index 42d151bb..475d8e67 100644 --- a/tests/entity/value/test_uniform_integer.py +++ b/tests/entity/value/test_uniform_integer.py @@ -1,8 +1,9 @@ """Tests of the UniformInteger class.""" + import pytest -from gemd.entity.value.uniform_integer import UniformInteger from gemd.entity.bounds import IntegerBounds +from gemd.entity.value.uniform_integer import UniformInteger def test_bounds_order(): diff --git a/tests/entity/value/test_uniform_real.py b/tests/entity/value/test_uniform_real.py index f0b277db..1aa396b6 100644 --- a/tests/entity/value/test_uniform_real.py +++ b/tests/entity/value/test_uniform_real.py @@ -1,23 +1,24 @@ """Tests of the UniformReal class.""" + import pytest -from gemd.entity.value.uniform_real import UniformReal from gemd.entity.bounds import RealBounds +from gemd.entity.value.uniform_real import UniformReal def test_bounds_order(): """Lower bound must be <= upper bound.""" - UniformReal(4.4, 8.8, 'm') - UniformReal(100.0, 100.0, 'm') + UniformReal(4.4, 8.8, "m") + UniformReal(100.0, 100.0, "m") with pytest.raises(AssertionError): - UniformReal(23.2, 18.9, 'm') + UniformReal(23.2, 18.9, "m") def test_equality(): """Test that equality checks both bounds and units.""" - value1 = UniformReal(0, 1, '') - value2 = UniformReal(0, 1, 'cm') - value3 = UniformReal(0, 2, '') + value1 = UniformReal(0, 1, "") + value2 = UniformReal(0, 1, "cm") + value3 = UniformReal(0, 2, "") assert value1 == value1 assert value1 != value2 @@ -27,7 +28,7 @@ def test_equality(): def test_contains(): """Test that bounds know if a Value is contained within it.""" - bounds = RealBounds(1, 3, 'm') - assert bounds.contains(UniformReal(100, 200, 'cm')._to_bounds()) - assert not bounds.contains(UniformReal(3, 5, 'm')._to_bounds()) - assert not bounds.contains(UniformReal(1, 3, '')._to_bounds()) + bounds = RealBounds(1, 3, "m") + assert bounds.contains(UniformReal(100, 200, "cm")._to_bounds()) + assert not bounds.contains(UniformReal(3, 5, "m")._to_bounds()) + assert not bounds.contains(UniformReal(1, 3, "")._to_bounds()) diff --git a/tests/entity/value/test_units.py b/tests/entity/value/test_units.py index 60ffdbfb..4f8873cb 100644 --- a/tests/entity/value/test_units.py +++ b/tests/entity/value/test_units.py @@ -1,4 +1,5 @@ """Test that units behave correctly.""" + import pytest from gemd.entity.value.nominal_real import NominalReal diff --git a/tests/enumeration/test_enumeration.py b/tests/enumeration/test_enumeration.py index fbed117a..130d846c 100644 --- a/tests/enumeration/test_enumeration.py +++ b/tests/enumeration/test_enumeration.py @@ -1,11 +1,13 @@ """Tests of the enumeration class.""" -import pytest + import warnings +import pytest + from gemd.entity.attribute.property import Property from gemd.enumeration import Origin from gemd.enumeration.base_enumeration import BaseEnumeration, migrated_enum -from gemd.json import loads, dumps +from gemd.json import dumps, loads def test_json_serde(): @@ -21,12 +23,14 @@ def test_json_serde(): def test_restrictions(): """Test that restrictions apply to enumerations--all values must be unique strings.""" with pytest.raises(ValueError): + class BadClass1(BaseEnumeration): RED = "red" BLUE = "blue" MAROON = "red" with pytest.raises(ValueError): + class BadClass2(BaseEnumeration): FIRST = "one" SECOND = 2 @@ -53,13 +57,14 @@ class TestEnum(BaseEnumeration): for key in TestEnum.TWO.synonyms: assert key != TestEnum.TWO, f"Synonym {key} was equal?" assert TestEnum.from_str(key) == TestEnum.TWO, f"from_str didn't resolve {key}" - assert ( - TestEnum.from_str(key.upper()) == TestEnum.TWO - ), f"from_str didn't resolve {key.upper()}" + assert TestEnum.from_str(key.upper()) == TestEnum.TWO, ( + f"from_str didn't resolve {key.upper()}" + ) def test_missing(): """Test that enumeration is resolved via multiple paths.""" + class TestEnum(BaseEnumeration): ONE = "One", "1" TWO = "Two", "2" @@ -78,6 +83,7 @@ class TestEnum(BaseEnumeration): def test_migrated(): """Verify that migration functions as expected.""" + @migrated_enum(old_value="UNO", new_value="ONE", deprecated_in="1.9.9", removed_in="2.0.0") class TestEnum(BaseEnumeration): ONE = "One", "1" diff --git a/tests/json/test_json.py b/tests/json/test_json.py index 69f360bf..0a921992 100644 --- a/tests/json/test_json.py +++ b/tests/json/test_json.py @@ -1,21 +1,27 @@ """Test serialization and deserialization of gemd objects.""" + import json as json_builtin from copy import deepcopy from uuid import uuid4 import pytest -from gemd.json import GEMDJson import gemd.json as gemd_json +from gemd.entity.attribute.condition import Condition +from gemd.entity.attribute.parameter import Parameter from gemd.entity.attribute.property import Property from gemd.entity.bounds.real_bounds import RealBounds from gemd.entity.case_insensitive_dict import CaseInsensitiveDict -from gemd.entity.attribute.condition import Condition -from gemd.entity.attribute.parameter import Parameter from gemd.entity.dict_serializable import DictSerializable from gemd.entity.link_by_uid import LinkByUID -from gemd.entity.object import MeasurementRun, MaterialRun, ProcessRun -from gemd.entity.object import MeasurementSpec, MaterialSpec, ProcessSpec +from gemd.entity.object import ( + MaterialRun, + MaterialSpec, + MeasurementRun, + MeasurementSpec, + ProcessRun, + ProcessSpec, +) from gemd.entity.object.ingredient_run import IngredientRun from gemd.entity.object.ingredient_spec import IngredientSpec from gemd.entity.template.property_template import PropertyTemplate @@ -23,45 +29,52 @@ from gemd.entity.value.nominal_real import NominalReal from gemd.entity.value.normal_real import NormalReal from gemd.enumeration.origin import Origin -from gemd.util import substitute_objects, substitute_links +from gemd.json import GEMDJson +from gemd.util import substitute_links, substitute_objects def test_serialize(): """Serializing a nested object should be identical to individually serializing each piece.""" - condition = Condition(name="A condition", value=NominalReal(7, '')) - parameter = Parameter(name="A parameter", value=NormalReal(mean=17, std=1, units='')) + condition = Condition(name="A condition", value=NominalReal(7, "")) + parameter = Parameter(name="A parameter", value=NormalReal(mean=17, std=1, units="")) input_material = MaterialRun("name", tags="input") process = ProcessRun("name", tags="A tag on a process run") ingredient = IngredientRun(material=input_material, process=process) material = MaterialRun("name", tags=["A tag on a material"], process=process) - measurement = MeasurementRun("name", tags="A tag on a measurement", conditions=condition, - parameters=parameter, material=material) + measurement = MeasurementRun( + "name", + tags="A tag on a measurement", + conditions=condition, + parameters=parameter, + material=material, + ) # serialize the root of the tree native_object = json_builtin.loads(gemd_json.dumps(measurement)) # ingredients don't get serialized on the process - assert(len(native_object["context"]) == 5) - assert(native_object["object"]["type"] == LinkByUID.typ) + assert len(native_object["context"]) == 5 + assert native_object["object"]["type"] == LinkByUID.typ # serialize all the nodes - native_batch = json_builtin.loads(gemd_json.dumps([material, process, measurement, ingredient])) - assert(len(native_batch["context"]) == 5) - assert(len(native_batch["object"]) == 4) - assert(all(x["type"] == LinkByUID.typ for x in native_batch["object"])) + native_batch = json_builtin.loads( + gemd_json.dumps([material, process, measurement, ingredient]) + ) + assert len(native_batch["context"]) == 5 + assert len(native_batch["object"]) == 4 + assert all(x["type"] == LinkByUID.typ for x in native_batch["object"]) def test_deserialize(): """Round-trip serde should leave the object unchanged.""" - condition = Condition(name="A condition", value=NominalReal(7, '')) - parameter = Parameter(name="A parameter", value=NormalReal(mean=17, std=1, units='')) - measurement = MeasurementRun("name", - tags="A tag on a measurement", - conditions=condition, - parameters=parameter) + condition = Condition(name="A condition", value=NominalReal(7, "")) + parameter = Parameter(name="A parameter", value=NormalReal(mean=17, std=1, units="")) + measurement = MeasurementRun( + "name", tags="A tag on a measurement", conditions=condition, parameters=parameter + ) copy_meas = GEMDJson().copy(measurement) - assert(copy_meas.conditions[0].value == measurement.conditions[0].value) - assert(copy_meas.parameters[0].value == measurement.parameters[0].value) - assert(copy_meas.uids["auto"] == measurement.uids["auto"]) + assert copy_meas.conditions[0].value == measurement.conditions[0].value + assert copy_meas.parameters[0].value == measurement.parameters[0].value + assert copy_meas.uids["auto"] == measurement.uids["auto"] def test_uuid_serde(): @@ -96,7 +109,7 @@ def test_scope_control(): material.uids = {} # Verify the default scope is there - custom_json = GEMDJson(scope='custom') + custom_json = GEMDJson(scope="custom") custom_text = custom_json.dumps(material) assert "auto" not in custom_text assert "custom" in custom_text @@ -104,9 +117,11 @@ def test_scope_control(): def test_deserialize_extra_fields(): """Extra JSON fields should be ignored in deserialization.""" - json_data = '{"context": [],' \ - ' "object": {"nominal": 5, "type": "nominal_integer", "extra garbage": "foo"}}' - assert(gemd_json.loads(json_data) == NominalInteger(nominal=5)) + json_data = ( + '{"context": [],' + ' "object": {"nominal": 5, "type": "nominal_integer", "extra garbage": "foo"}}' + ) + assert gemd_json.loads(json_data) == NominalInteger(nominal=5) def test_enumeration_serde(): @@ -119,13 +134,8 @@ def test_enumeration_serde(): def test_attribute_serde(): """An attribute with a link to an attribute template should be copy-able.""" - prop_tmpl = PropertyTemplate(name='prop_tmpl', - bounds=RealBounds(0, 2, 'm') - ) - prop = Property(name='prop', - template=prop_tmpl, - value=NominalReal(1, 'm') - ) + prop_tmpl = PropertyTemplate(name="prop_tmpl", bounds=RealBounds(0, 2, "m")) + prop = Property(name="prop", template=prop_tmpl, value=NominalReal(1, "m")) meas_spec = MeasurementSpec("a spec") meas = MeasurementRun("a measurement", spec=meas_spec, properties=[prop]) assert gemd_json.loads(gemd_json.dumps(prop)) == prop @@ -136,36 +146,37 @@ def test_attribute_serde(): def test_thin_dumps(): """Test that thin_dumps turns pointers into links.""" mat = MaterialRun("The actual material") - meas_spec = MeasurementSpec("measurement", uids={'my_scope': '324324'}) + meas_spec = MeasurementSpec("measurement", uids={"my_scope": "324324"}) meas = MeasurementRun("The measurement", spec=meas_spec, material=mat) thin_copy = MeasurementRun.build(json_builtin.loads(GEMDJson().thin_dumps(meas))) assert isinstance(thin_copy, MeasurementRun) assert isinstance(thin_copy.material, LinkByUID) assert isinstance(thin_copy.spec, LinkByUID) - assert thin_copy.spec.id == meas_spec.uids['my_scope'] + assert thin_copy.spec.id == meas_spec.uids["my_scope"] # Check that LinkByUID objects are correctly converted their JSON equivalent expected_json = '{"id": "my_id", "scope": "scope", "type": "link_by_uid"}' - assert GEMDJson().thin_dumps(LinkByUID('scope', 'my_id')) == expected_json + assert GEMDJson().thin_dumps(LinkByUID("scope", "my_id")) == expected_json # Check that objects lacking .uid attributes will raise an exception when dumped with pytest.raises(TypeError): - GEMDJson().thin_dumps({{'key': 'value'}}) + GEMDJson().thin_dumps({{"key": "value"}}) def test_uid_deser(): """Test that uids continue to be a CaseInsensitiveDict after deserialization.""" - material = MaterialRun("Input material", tags="input", uids={'Sample ID': '500-B'}) + material = MaterialRun("Input material", tags="input", uids={"Sample ID": "500-B"}) ingredient = IngredientRun(material=material) ingredient_copy = gemd_json.loads(gemd_json.dumps(ingredient)) assert isinstance(ingredient_copy.uids, CaseInsensitiveDict) assert ingredient_copy.material == material - assert ingredient_copy.material.uids['sample id'] == material.uids['Sample ID'] + assert ingredient_copy.material.uids["sample id"] == material.uids["Sample ID"] def test_unexpected_serialization(): """Trying to serialize an unexpected class should throw a TypeError.""" + class DummyClass: def __init__(self, foo): self.foo = foo @@ -184,6 +195,7 @@ def test_unexpected_deserialization(): def test_register_classes_override(): """Test that register_classes overrides existing entries in the class index.""" + class MyProcessSpec(ProcessSpec): pass @@ -191,16 +203,18 @@ class MyProcessSpec(ProcessSpec): custom = GEMDJson() obj = ProcessSpec(name="foo") - assert not isinstance(normal.copy(obj), MyProcessSpec),\ + assert not isinstance(normal.copy(obj), MyProcessSpec), ( "Class registration bled across GEMDJson() objects" + ) - assert isinstance(custom.copy(obj), ProcessSpec),\ + assert isinstance(custom.copy(obj), ProcessSpec), ( "Custom GEMDJson didn't deserialize as MyProcessSpec" + ) def test_pure_substitutions(): """Make sure substitute methods don't mutate inputs.""" - json_str = ''' + json_str = """ [ [ { @@ -228,10 +242,12 @@ def test_pure_substitutions(): } } ] - ''' + """ index = {} clazz_index = DictSerializable.class_mapping - original = json_builtin.loads(json_str, object_hook=lambda x: GEMDJson()._load_and_index(x, index, clazz_index)) + original = json_builtin.loads( + json_str, object_hook=lambda x: GEMDJson()._load_and_index(x, index, clazz_index) + ) frozen = deepcopy(original) loaded = substitute_objects(original, index) assert original == frozen @@ -244,9 +260,7 @@ def test_pure_substitutions(): def test_case_insensitive_rehydration(): - """ - - Test that loads() can connect id scopes with different cases. + """Test that loads() can connect id scopes with different cases. This situation should not occur in gemd on its own, but faraday returns LinkOrElse objects with the default scope "ID", whereas citrine-python assigns ids with the scope "id". @@ -255,7 +269,7 @@ def test_case_insensitive_rehydration(): # A simple json string that could be loaded, representing an ingredient linked to a material. # The material link has "scope": "ID", whereas the material in the context list, which is # to be loaded, has uid with scope "id". - json_str = ''' + json_str = """ { "context": [ { @@ -283,7 +297,7 @@ def test_case_insensitive_rehydration(): } } } - ''' + """ loaded_ingredient = gemd_json.loads(json_str) # The ingredient's material will either be a MaterialRun (pass) or a LinkByUID (fail) assert isinstance(loaded_ingredient.material, MaterialRun) @@ -294,10 +308,10 @@ def test_many_ingredients(): proc = ProcessRun("foo", spec=ProcessSpec("sfoo")) expected = [] for i in range(10): - mat = MaterialRun(name=str(i), spec=MaterialSpec("s{}".format(i))) - i_spec = IngredientSpec(name="i{}".format(i), material=mat.spec, process=proc.spec) + mat = MaterialRun(name=str(i), spec=MaterialSpec(f"s{i}")) + i_spec = IngredientSpec(name=f"i{i}", material=mat.spec, process=proc.spec) IngredientRun(process=proc, material=mat, spec=i_spec) - expected.append("i{}".format(i)) + expected.append(f"i{i}") reloaded = gemd_json.loads(gemd_json.dumps(proc)) assert len(list(reloaded.ingredients)) == 10 @@ -306,13 +320,12 @@ def test_many_ingredients(): def test_deeply_nested_rehydration(): - """ - Tests that loads fully replaces links with objects. + """Tests that loads fully replaces links with objects. In particular, this test makes sure that loads is robust to objects being referenced by LinkByUid before they are "declared" in the JSON array. """ - json_str = ''' + json_str = """ { "context": [ { @@ -644,7 +657,7 @@ def test_deeply_nested_rehydration(): "id": "f0f41fb9-32dc-4903-aaf4-f369de71530f" } } - ''' + """ material_history = gemd_json.loads(json_str) assert isinstance(material_history.process.ingredients[1].spec, IngredientSpec) assert isinstance(material_history.measurements[0], MeasurementRun) diff --git a/tests/test_examples.py b/tests/test_examples.py index 9f9c1ee7..bb403246 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1,13 +1,21 @@ """Test of a complicated set of interlocking data objects.""" + import json as json_builtin -import gemd.json as gemd_json +import gemd.json as gemd_json from gemd.entity.attribute.condition import Condition from gemd.entity.attribute.parameter import Parameter from gemd.entity.attribute.property import Property from gemd.entity.bounds.real_bounds import RealBounds -from gemd.entity.object import MeasurementRun, MaterialRun, ProcessRun, ProcessSpec,\ - MeasurementSpec, MaterialSpec, IngredientRun +from gemd.entity.object import ( + IngredientRun, + MaterialRun, + MaterialSpec, + MeasurementRun, + MeasurementSpec, + ProcessRun, + ProcessSpec, +) from gemd.entity.template.condition_template import ConditionTemplate from gemd.entity.template.material_template import MaterialTemplate from gemd.entity.template.measurement_template import MeasurementTemplate @@ -20,24 +28,22 @@ from gemd.entity.value.uniform_real import UniformReal density_template = PropertyTemplate( - name="Density", - bounds=RealBounds(lower_bound=0, upper_bound=1.0e9, default_units='') + name="Density", bounds=RealBounds(lower_bound=0, upper_bound=1.0e9, default_units="") ) firing_temperature_template = ConditionTemplate( name="Firing Temperature", - bounds=RealBounds(lower_bound=0, upper_bound=1.0e9, default_units='degC') + bounds=RealBounds(lower_bound=0, upper_bound=1.0e9, default_units="degC"), ) measurement_template = MeasurementTemplate("Density Measurement", properties=density_template) firing_template = ProcessTemplate( name="Firing in a kiln", - conditions=(firing_temperature_template, RealBounds(lower_bound=500, upper_bound=1000, - default_units='degC')) -) -material_template = MaterialTemplate( - name="Some ceramic thing", - properties=density_template + conditions=( + firing_temperature_template, + RealBounds(lower_bound=500, upper_bound=1000, default_units="degC"), + ), ) +material_template = MaterialTemplate(name="Some ceramic thing", properties=density_template) def make_data_island(density, bulk_modulus, firing_temperature, binders, powders, tag=None): @@ -50,21 +56,16 @@ def make_data_island(density, bulk_modulus, firing_temperature, binders, powders all_input_materials = {k.spec.name: v for k, v in binder_runs.items() | powder_runs.items()} mixing_composition = Condition( - name="composition", - value=NominalComposition(all_input_materials) - ) - mixing_process = ProcessRun( - name="Mixing", - tags=["mixing"], - conditions=[mixing_composition] + name="composition", value=NominalComposition(all_input_materials) ) + mixing_process = ProcessRun(name="Mixing", tags=["mixing"], conditions=[mixing_composition]) binder_ingredients = [] for run in binder_runs: binder_ingredients.append( IngredientRun( material=run, process=mixing_process, - mass_fraction=NominalReal(binders[run.spec.name], ''), + mass_fraction=NominalReal(binders[run.spec.name], ""), ) ) @@ -74,7 +75,7 @@ def make_data_island(density, bulk_modulus, firing_temperature, binders, powders IngredientRun( material=run, process=mixing_process, - mass_fraction=NominalReal(powders[run.spec.name], ''), + mass_fraction=NominalReal(powders[run.spec.name], ""), ) ) @@ -82,51 +83,45 @@ def make_data_island(density, bulk_modulus, firing_temperature, binders, powders measured_firing_temperature = Condition( name="Firing Temperature", - value=UniformReal(firing_temperature - 0.5, firing_temperature + 0.5, 'degC'), - template=firing_temperature_template + value=UniformReal(firing_temperature - 0.5, firing_temperature + 0.5, "degC"), + template=firing_temperature_template, ) - specified_firing_setting = Parameter( - name="Firing setting", - value=DiscreteCategorical("hot") - ) + specified_firing_setting = Parameter(name="Firing setting", value=DiscreteCategorical("hot")) firing_spec = ProcessSpec("Firing", template=firing_template) firing_process = ProcessRun( name=firing_spec.name, conditions=[measured_firing_temperature], parameters=[specified_firing_setting], - spec=firing_spec + spec=firing_spec, ) IngredientRun( material=green_sample, process=firing_process, - mass_fraction=NormalReal(1.0, 0.0, ''), - volume_fraction=NormalReal(1.0, 0.0, ''), - number_fraction=NormalReal(1.0, 0.0, '') + mass_fraction=NormalReal(1.0, 0.0, ""), + volume_fraction=NormalReal(1.0, 0.0, ""), + number_fraction=NormalReal(1.0, 0.0, ""), ) measured_density = Property( - name="Density", - value=NominalReal(density, ''), - template=density_template + name="Density", value=NominalReal(density, ""), template=density_template ) measured_modulus = Property( - name="Bulk modulus", - value=NormalReal(bulk_modulus, bulk_modulus / 100.0, '') + name="Bulk modulus", value=NormalReal(bulk_modulus, bulk_modulus / 100.0, "") ) - measurement_spec = MeasurementSpec("Mechanical Properties", - template=measurement_template) + measurement_spec = MeasurementSpec("Mechanical Properties", template=measurement_template) measurement = MeasurementRun( measurement_spec.name, properties=[measured_density, measured_modulus], - spec=measurement_spec + spec=measurement_spec, ) tags = [tag] if tag else [] material_spec = MaterialSpec("Coupon", template=material_template) - material_run = MaterialRun(material_spec.name, process=firing_process, - tags=tags, spec=material_spec) + material_run = MaterialRun( + material_spec.name, process=firing_process, tags=tags, spec=material_spec + ) measurement.material = material_run return material_run @@ -136,7 +131,7 @@ def test_access_data(): binders = { "Polyethylene Glycol 100M": 0.02, "Sodium lignosulfonate": 0.004, - "Polyvinyl Acetate": 0.0001 + "Polyvinyl Acetate": 0.0001, } powders = {"Al2O3": 0.96} island = make_data_island( @@ -145,16 +140,16 @@ def test_access_data(): firing_temperature=750.0, binders=binders, powders=powders, - tag="Me" + tag="Me", ) # read the density value - assert(island.measurements[0].properties[0].value == NominalReal(1.0, '')) + assert island.measurements[0].properties[0].value == NominalReal(1.0, "") # read the bulk modulus value - assert(island.measurements[0].properties[1].value == NormalReal(300.0, 3.0, '')) + assert island.measurements[0].properties[1].value == NormalReal(300.0, 3.0, "") # read the firing temperature - assert(island.process.conditions[0].value == UniformReal(749.5, 750.5, 'degC')) - assert(island.process.parameters[0].value == DiscreteCategorical({"hot": 1.0})) + assert island.process.conditions[0].value == UniformReal(749.5, 750.5, "degC") + assert island.process.parameters[0].value == DiscreteCategorical({"hot": 1.0}) # read the quantity of alumina quantities = island.process.ingredients[0].material.process.conditions[0].value.quantities @@ -162,4 +157,4 @@ def test_access_data(): # check that the serialization results in the correct number of objects in the preface # (note that neither measurements nor ingredients are serialized) - assert(len(json_builtin.loads(gemd_json.dumps(island))["context"]) == 26) + assert len(json_builtin.loads(gemd_json.dumps(island))["context"]) == 26 diff --git a/tests/units/test_parser.py b/tests/units/test_parser.py index ffcdeac0..f3bd4be4 100644 --- a/tests/units/test_parser.py +++ b/tests/units/test_parser.py @@ -1,29 +1,54 @@ +import re from contextlib import contextmanager -from deprecation import DeprecatedWarning from importlib.resources import files -import re -from pint import UnitRegistry + import pytest +from deprecation import DeprecatedWarning +from pint import UnitRegistry -from gemd.units import parse_units, convert_units, get_base_units, change_definitions_file, \ - UndefinedUnitError, DefinitionSyntaxError, IncompatibleUnitsError +from gemd.units import ( + DefinitionSyntaxError, + IncompatibleUnitsError, + UndefinedUnitError, + change_definitions_file, + convert_units, + get_base_units, + parse_units, +) @pytest.mark.parametrize("return_unit", [True, False]) def test_parse_expected(return_unit): """Test that we can parse the units that we expect to be able to.""" # Pint's parse_units actually gets this wrong - assert parse_units("m^-1 * newton / meter", return_unit=return_unit) == \ - parse_units("N / m^2", return_unit=return_unit) + assert parse_units("m^-1 * newton / meter", return_unit=return_unit) == parse_units( + "N / m^2", return_unit=return_unit + ) expected = [ - "degC", "degF", "K", - "g", "kg", "mg", "ton", - "L", "mL", - "inch", "ft", "mm", "um", - "second", "ms", "hour", "minute", "ns", - "g/cm^3", "g/mL", "kg/cm^3", - "", "1", + "degC", + "degF", + "K", + "g", + "kg", + "mg", + "ton", + "L", + "mL", + "inch", + "ft", + "mm", + "um", + "second", + "ms", + "hour", + "minute", + "ns", + "g/cm^3", + "g/mL", + "kg/cm^3", + "", + "1", "amu", # A line that was edited "Seconds", # Added support for some title-case units "delta_Celsius / hour", # Added to make sure pint version is right (>0.10) @@ -39,17 +64,18 @@ def test_parse_expected(return_unit): for unit in expected: parsed = parse_units(unit, return_unit=return_unit) assert parsed == parse_units(parsed, return_unit=return_unit) - assert parse_units("") == 'dimensionless' + assert parse_units("") == "dimensionless" # Scaling factors bind tightly to trailing units scaling = [ ("g / 2.5 cm", "g / (2.5 cm)"), ("g / 2.5cm", "g / (2.5 cm)"), ("g / 25.mm", "g / (25. mm)"), - ("g / 2.5 * cm", "g cm / 2.5") + ("g / 2.5 * cm", "g cm / 2.5"), ] for left, right in scaling: - assert parse_units(left, return_unit=return_unit) == \ - parse_units(right, return_unit=return_unit) + assert parse_units(left, return_unit=return_unit) == parse_units( + right, return_unit=return_unit + ) def test_parse_unexpected(): @@ -144,20 +170,21 @@ def test_conversion(): assert convert_units(convert_units(1, source, dest), dest, source) == 1 # Verify that convert_units respects scaling factors - assert -1e-8 < convert_units(100, 'g / 100 mL', 'g/cc') - 1 < 1e-8 + assert -1e-8 < convert_units(100, "g / 100 mL", "g/cc") - 1 < 1e-8 assert -1e-8 < convert_units(1, "g / 2.5 cm", "g / 25 mm") - 1 < 1e-8 # Verify that convert_units throws exceptions with pytest.raises(IncompatibleUnitsError): - convert_units(1, 'mL', 'g') + convert_units(1, "mL", "g") with pytest.raises(IncompatibleUnitsError): # https://pint.readthedocs.io/en/0.23/user/angular_frequency.html - convert_units(1, 'Hz', 'rpm') + convert_units(1, "Hz", "rpm") def test_get_base_units(): """Test that base units & conversions make sense.""" from gemd.units.impl import _REGISTRY + assert get_base_units("degC") == (_REGISTRY("kelvin"), 1, 273.15) assert get_base_units("degC") == get_base_units(_REGISTRY("degC")) assert get_base_units("km") == (_REGISTRY("meter"), 1000, 0) @@ -175,42 +202,42 @@ def _change_units(filename): def test_file_change(tmpdir): """Test that swapping units files works.""" - assert convert_units(1, 'm', 'cm') == 100 + assert convert_units(1, "m", "cm") == 100 with pytest.raises(UndefinedUnitError): - assert convert_units(1, 'usd', 'USD') == 1 + assert convert_units(1, "usd", "USD") == 1 test_file = tmpdir / "test_units.txt" test_file.write_binary(files("tests.units").joinpath("test_units.txt").read_bytes()) with _change_units(filename=test_file): with pytest.raises(UndefinedUnitError): - assert convert_units(1, 'm', 'cm') == 100 - assert convert_units(1, 'usd', 'USD') == 1 - assert convert_units(1, 'm', 'cm') == 100 # And verify we're back to normal + assert convert_units(1, "m", "cm") == 100 + assert convert_units(1, "usd", "USD") == 1 + assert convert_units(1, "m", "cm") == 100 # And verify we're back to normal with pytest.raises(UndefinedUnitError): - parse_units('mol : mol') # Ensure the preprocessor is still there + parse_units("mol : mol") # Ensure the preprocessor is still there def test_punctuation(): """Test that punctuation parses reasonably.""" - assert parse_units('mol.') == parse_units('moles') - assert parse_units('N.m') == parse_units('N * m') + assert parse_units("mol.") == parse_units("moles") + assert parse_units("N.m") == parse_units("N * m") with pytest.raises(UndefinedUnitError): - parse_units('mol : mol') + parse_units("mol : mol") def test_exponents(): """SPT-874 fractional exponents were being treated as zero.""" megapascals = parse_units("MPa") - sqrt_megapascals = parse_units('MPa^0.5') + sqrt_megapascals = parse_units("MPa^0.5") assert megapascals in sqrt_megapascals assert sqrt_megapascals == parse_units(f"{megapascals} / {sqrt_megapascals}") - assert parse_units('MPa^1.5') == parse_units(f"{megapascals} * {sqrt_megapascals}") + assert parse_units("MPa^1.5") == parse_units(f"{megapascals} * {sqrt_megapascals}") def test__scientific_notation_preprocessor(): """Verify that numbers are converted into scientific notation.""" assert "1e2 kilogram" in parse_units("F* 10 ** 2 kg") - assert "1e2 kg" in f'{parse_units("F* 10 ** 2 kg", return_unit=True):~}' + assert "1e2 kg" in f"{parse_units('F* 10 ** 2 kg', return_unit=True):~}" assert "1e-5" in parse_units("F* mm*10**-5") assert "1e" not in parse_units("F* kg * 10 cm") assert "-3.07e2" in parse_units("F* -3.07 * 10 ** 2") @@ -225,9 +252,11 @@ def test_deprecation(): assert megapascals == parse_units(stringified, return_unit=False) from pint import Quantity + with pytest.warns(DeprecatedWarning): assert f"{Quantity('5 MPa'):clean}" == f"5 {stringified}" from pint import Unit + with pytest.warns(DeprecatedWarning): assert f"{Unit('MPa'):clean}" == stringified diff --git a/tests/util/test_cached_isinstance.py b/tests/util/test_cached_isinstance.py index fffdf042..faaeeab8 100644 --- a/tests/util/test_cached_isinstance.py +++ b/tests/util/test_cached_isinstance.py @@ -1,6 +1,6 @@ -from gemd.util import cached_isinstance +from typing import Iterable, List -from typing import List, Iterable +from gemd.util import cached_isinstance def test_cached_isinstance(): diff --git a/tests/util/test_flatten.py b/tests/util/test_flatten.py index a1ed9bfc..8a00db8c 100644 --- a/tests/util/test_flatten.py +++ b/tests/util/test_flatten.py @@ -1,25 +1,30 @@ +import pytest + +from gemd.entity.attribute.condition import Condition from gemd.entity.bounds.categorical_bounds import CategoricalBounds -from gemd.entity.object import ProcessSpec, MaterialSpec, IngredientSpec, ProcessRun, \ - MaterialRun, IngredientRun +from gemd.entity.object import ( + IngredientRun, + IngredientSpec, + MaterialRun, + MaterialSpec, + ProcessRun, + ProcessSpec, +) from gemd.entity.template.condition_template import ConditionTemplate from gemd.entity.template.process_template import ProcessTemplate -from gemd.entity.attribute.condition import Condition from gemd.entity.value.nominal_categorical import NominalCategorical from gemd.util import flatten, recursive_flatmap -import pytest - def test_flatten_bounds(): """Test that flatten works when the objects contain other objects.""" bounds = CategoricalBounds(categories=["foo", "bar"]) template = ProcessTemplate( - "spam", - conditions=[(ConditionTemplate(name="eggs", bounds=bounds), bounds)] + "spam", conditions=[(ConditionTemplate(name="eggs", bounds=bounds), bounds)] ) spec = ProcessSpec(name="spec", template=template) - flat = flatten(spec, 'test-scope') + flat = flatten(spec, "test-scope") # 3 objects: 1 Process Template, 1 Condition Template and 1 Process Spec assert len(flat) == 3, "Expected 3 flattened objects" @@ -36,16 +41,16 @@ def test_flatten_empty_history(): transform_run = ProcessRun(name="transformed", spec=transform) ingredient_run = IngredientRun(material=input_run, process=transform_run, spec=ingredient) - assert len(flatten(procured, 'test-scope')) == 2 - assert 'test-scope' in procured.uids - assert len(flatten(input, 'test-scope')) == 2 - assert len(flatten(ingredient, 'test-scope')) == 4 - assert len(flatten(transform, 'test-scope')) == 4 + assert len(flatten(procured, "test-scope")) == 2 + assert "test-scope" in procured.uids + assert len(flatten(input, "test-scope")) == 2 + assert len(flatten(ingredient, "test-scope")) == 4 + assert len(flatten(transform, "test-scope")) == 4 - assert len(flatten(procured_run, 'test-scope')) == 4 - assert len(flatten(input_run, 'test-scope')) == 4 - assert len(flatten(ingredient_run, 'test-scope')) == 8 - assert len(flatten(transform_run, 'test-scope')) == 8 + assert len(flatten(procured_run, "test-scope")) == 4 + assert len(flatten(input_run, "test-scope")) == 4 + assert len(flatten(ingredient_run, "test-scope")) == 8 + assert len(flatten(transform_run, "test-scope")) == 8 def test_default_scope(): @@ -55,12 +60,12 @@ def test_default_scope(): with pytest.raises(ValueError): flatten(ps_one) - pr_one = ProcessRun(name="one", uids={'my': 'outer'}, spec=ps_one) + pr_one = ProcessRun(name="one", uids={"my": "outer"}, spec=ps_one) with pytest.raises(ValueError): flatten(pr_one) - ps_one.uids['my'] = 'id' + ps_one.uids["my"] = "id" assert len(flatten(pr_one)) == 2 two = ProcessRun(name="two", spec=ProcessSpec(name="two")) @@ -79,16 +84,13 @@ def test_flatmap_unidirectional_ordering(): def test_repeated_objects(): """Test that objects aren't double counted.""" - ct = ConditionTemplate(name="color", - bounds=CategoricalBounds(categories=["black", "white"])) + ct = ConditionTemplate(name="color", bounds=CategoricalBounds(categories=["black", "white"])) pt = ProcessTemplate(name="painting", conditions=[ct]) - ps = ProcessSpec(name='painting', - template=pt, - conditions=Condition(name='Paint color', - value=NominalCategorical("black"), - template=ct - ) - ) + ps = ProcessSpec( + name="painting", + template=pt, + conditions=Condition(name="Paint color", value=NominalCategorical("black"), template=ct), + ) assert len(recursive_flatmap(ps, lambda x: [x])) == 3 diff --git a/tests/util/test_foreach.py b/tests/util/test_foreach.py index 6334fa98..4172804d 100644 --- a/tests/util/test_foreach.py +++ b/tests/util/test_foreach.py @@ -1,6 +1,6 @@ from gemd.entity.attribute.property import Property from gemd.entity.bounds.real_bounds import RealBounds -from gemd.entity.object import ProcessRun, MaterialRun, IngredientRun, MeasurementRun +from gemd.entity.object import IngredientRun, MaterialRun, MeasurementRun, ProcessRun from gemd.entity.template.property_template import PropertyTemplate from gemd.entity.value.nominal_real import NominalReal from gemd.util.impl import recursive_foreach @@ -21,12 +21,14 @@ def test_recursive_foreach(): types = [] recursive_foreach(output, lambda x: types.append(x.typ)) - expected = ["ingredient_run", - "material_run", "material_run", - "process_run", - "measurement_run", - "property_template" - ] + expected = [ + "ingredient_run", + "material_run", + "material_run", + "process_run", + "measurement_run", + "property_template", + ] assert sorted(types) == sorted(expected) diff --git a/tests/util/test_make_index.py b/tests/util/test_make_index.py index 08df086d..8c0d0d3a 100644 --- a/tests/util/test_make_index.py +++ b/tests/util/test_make_index.py @@ -1,5 +1,5 @@ -from gemd.entity.object import ProcessSpec, ProcessRun, MaterialSpec, MaterialRun from gemd.entity.link_by_uid import LinkByUID +from gemd.entity.object import MaterialRun, MaterialSpec, ProcessRun, ProcessSpec from gemd.util.impl import make_index, substitute_objects @@ -9,9 +9,7 @@ def test_make_index(): pr1 = ProcessRun( name="world", spec=LinkByUID(scope="test_scope", id="test_value"), - uids={"test_scope": "another_test_value", - "other_test": "also_valid" - }, + uids={"test_scope": "another_test_value", "other_test": "also_valid"}, ) ms1 = MaterialSpec( name="material", diff --git a/tests/util/test_substitute_links.py b/tests/util/test_substitute_links.py index 6033ff15..f990f1e2 100644 --- a/tests/util/test_substitute_links.py +++ b/tests/util/test_substitute_links.py @@ -1,14 +1,15 @@ -""" -Test the subbed = substitute_links method. +"""Test the subbed = substitute_links method. Focuses in particular on the edge cases that the client doesn't test. """ -import pytest + from uuid import uuid4 -from gemd.util.impl import substitute_links +import pytest + from gemd.entity.link_by_uid import LinkByUID -from gemd.entity.object import MeasurementRun, MaterialRun, ProcessRun, ProcessSpec +from gemd.entity.object import MaterialRun, MeasurementRun, ProcessRun, ProcessSpec +from gemd.util.impl import substitute_links def test_substitution_without_id(): @@ -19,31 +20,44 @@ def test_substitution_without_id(): substitute_links(meas), "subbed = substitute_links should fail if objects don't have uids" with pytest.raises(ValueError): - substitute_links([meas, mat]), \ - "subbed = substitute_links should fail if objects don't have uids" + ( + substitute_links([meas, mat]), + "subbed = substitute_links should fail if objects don't have uids", + ) with pytest.raises(ValueError): - substitute_links(meas.as_dict()), \ - "subbed = substitute_links should fail if objects don't have uids" + ( + substitute_links(meas.as_dict()), + "subbed = substitute_links should fail if objects don't have uids", + ) # Create a dictionary in which either the key or value is missing a uid - meas.add_uid('id', str(uuid4())) + meas.add_uid("id", str(uuid4())) with pytest.raises(ValueError): - substitute_links({mat: meas}), \ - "subbed = substitute_links should fail if objects don't have uids" + ( + substitute_links({mat: meas}), + "subbed = substitute_links should fail if objects don't have uids", + ) with pytest.raises(ValueError): - substitute_links({meas: mat}), \ - "subbed = substitute_links should fail if objects don't have uids" + ( + substitute_links({meas: mat}), + "subbed = substitute_links should fail if objects don't have uids", + ) def test_scope_substitution(): """Test that the native id gets serialized, when specified.""" - native_id = 'id1' + native_id = "id1" # Create measurement and material with two ids - mat = MaterialRun("A material", uids={ - native_id: str(uuid4()), "an_id": str(uuid4()), "another_id": str(uuid4())}) - meas = MeasurementRun("A measurement", material=mat, uids={ - "some_id": str(uuid4()), native_id: str(uuid4()), "an_id": str(uuid4())}) + mat = MaterialRun( + "A material", + uids={native_id: str(uuid4()), "an_id": str(uuid4()), "another_id": str(uuid4())}, + ) + meas = MeasurementRun( + "A measurement", + material=mat, + uids={"some_id": str(uuid4()), native_id: str(uuid4()), "an_id": str(uuid4())}, + ) # Turn the material pointer into a LinkByUID using native_id subbed = substitute_links(meas, scope=native_id) @@ -57,44 +71,44 @@ def test_scope_substitution(): def test_object_key_substitution(): """Test that client can copy a dictionary in which keys are BaseEntity objects.""" - spec = ProcessSpec("A process spec", uids={'id': str(uuid4()), 'auto': str(uuid4())}) - run1 = ProcessRun("A process run", spec=spec, uids={'id': str(uuid4()), 'auto': str(uuid4())}) - run2 = ProcessRun("Another process run", spec=spec, uids={'id': str(uuid4())}) + spec = ProcessSpec("A process spec", uids={"id": str(uuid4()), "auto": str(uuid4())}) + run1 = ProcessRun("A process run", spec=spec, uids={"id": str(uuid4()), "auto": str(uuid4())}) + run2 = ProcessRun("Another process run", spec=spec, uids={"id": str(uuid4())}) process_dict = {spec: [run1, run2]} - subbed = substitute_links(process_dict, scope='auto') + subbed = substitute_links(process_dict, scope="auto") for key, value in subbed.items(): - assert key == LinkByUID.from_entity(spec, scope='auto') - assert LinkByUID.from_entity(run1, scope='auto') in value + assert key == LinkByUID.from_entity(spec, scope="auto") + assert LinkByUID.from_entity(run1, scope="auto") in value assert LinkByUID.from_entity(run2) in value reverse_process_dict = {run2: spec} - subbed = substitute_links(reverse_process_dict, scope='auto') + subbed = substitute_links(reverse_process_dict, scope="auto") for key, value in subbed.items(): assert key == LinkByUID.from_entity(run2) - assert value == LinkByUID.from_entity(spec, scope='auto') + assert value == LinkByUID.from_entity(spec, scope="auto") def test_signature(): """Exercise various permutations of the substitute_links sig.""" - spec = ProcessSpec("A process spec", uids={'my': 'spec'}) + spec = ProcessSpec("A process spec", uids={"my": "spec"}) - run1 = ProcessRun("First process run", uids={'my': 'run1'}, spec=spec) - assert isinstance(substitute_links(run1, scope='my').spec, LinkByUID) + run1 = ProcessRun("First process run", uids={"my": "run1"}, spec=spec) + assert isinstance(substitute_links(run1, scope="my").spec, LinkByUID) - run2 = ProcessRun("Second process run", uids={'my': 'run2'}, spec=spec) - assert isinstance(substitute_links(run2, 'my').spec, LinkByUID) + run2 = ProcessRun("Second process run", uids={"my": "run2"}, spec=spec) + assert isinstance(substitute_links(run2, "my").spec, LinkByUID) with pytest.raises(ValueError): - run3 = ProcessRun("Third process run", uids={'my': 'run3'}, spec=spec) - assert isinstance(substitute_links(run3, 'other', allow_fallback=False).spec, LinkByUID) + run3 = ProcessRun("Third process run", uids={"my": "run3"}, spec=spec) + assert isinstance(substitute_links(run3, "other", allow_fallback=False).spec, LinkByUID) def test_inplace_v_not(): """Test that client can copy a dictionary in which keys are BaseEntity objects.""" - spec = ProcessSpec("A process spec", uids={'id': str(uuid4()), 'auto': str(uuid4())}) - run1 = ProcessRun("A process run", spec=spec, uids={'id': str(uuid4()), 'auto': str(uuid4())}) - run2 = ProcessRun("Another process run", spec=spec, uids={'id': str(uuid4())}) + spec = ProcessSpec("A process spec", uids={"id": str(uuid4()), "auto": str(uuid4())}) + run1 = ProcessRun("A process run", spec=spec, uids={"id": str(uuid4()), "auto": str(uuid4())}) + run2 = ProcessRun("Another process run", spec=spec, uids={"id": str(uuid4())}) process_dict = {spec: [run1, run2]} subbed = substitute_links(process_dict) diff --git a/tests/util/test_substitute_objects.py b/tests/util/test_substitute_objects.py index 61bfeaac..2c58c31b 100644 --- a/tests/util/test_substitute_objects.py +++ b/tests/util/test_substitute_objects.py @@ -1,23 +1,32 @@ -from gemd.util import substitute_objects, recursive_foreach, flatten, make_index, recursive_flatmap -from gemd.util.impl import _substitute, _substitute_inplace -from gemd.entity.object import MaterialSpec, MaterialRun, ProcessSpec, ProcessRun, IngredientRun, \ - IngredientSpec, MeasurementSpec -from gemd.entity.template import ProcessTemplate, ParameterTemplate, MeasurementTemplate -from gemd.entity.value.normal_real import NormalReal from gemd.entity.attribute.parameter import Parameter -from gemd.entity.link_by_uid import LinkByUID from gemd.entity.bounds.real_bounds import RealBounds +from gemd.entity.link_by_uid import LinkByUID +from gemd.entity.object import ( + IngredientRun, + IngredientSpec, + MaterialRun, + MaterialSpec, + MeasurementSpec, + ProcessRun, + ProcessSpec, +) +from gemd.entity.template import MeasurementTemplate, ParameterTemplate, ProcessTemplate +from gemd.entity.value.normal_real import NormalReal +from gemd.util import flatten, make_index, recursive_flatmap, recursive_foreach, substitute_objects +from gemd.util.impl import _substitute, _substitute_inplace def test_dictionary_substitution(): """substitute_objects() should substitute LinkByUIDs that occur in dict keys and values.""" - proc = ProcessRun("A process", uids={'id': '123'}) - mat = MaterialRun("A material", uids={'generic id': '38f8jf'}) + proc = ProcessRun("A process", uids={"id": "123"}) + mat = MaterialRun("A material", uids={"generic id": "38f8jf"}) proc_link = LinkByUID.from_entity(proc) mat_link = LinkByUID.from_entity(mat) - index = {(mat_link.scope.lower(), mat_link.id): mat, - (proc_link.scope.lower(), proc_link.id): proc} + index = { + (mat_link.scope.lower(), mat_link.id): mat, + (proc_link.scope.lower(), proc_link.id): proc, + } test_dict = {LinkByUID.from_entity(proc): LinkByUID.from_entity(mat)} subbed = substitute_objects(test_dict, index) @@ -28,7 +37,7 @@ def test_dictionary_substitution(): def test_tuple_sub(): """substitute_objects() should correctly substitute tuple values.""" - proc = ProcessRun('foo', uids={'id': '123'}) + proc = ProcessRun("foo", uids={"id": "123"}) proc_link = LinkByUID.from_entity(proc) index = {(proc_link.scope, proc_link.id): proc} tup = (proc_link,) @@ -45,9 +54,9 @@ def func(base_ent): base_ent.tags.extend([new_tag]) return - param_template = ParameterTemplate("a param template", bounds=RealBounds(0, 100, '')) + param_template = ParameterTemplate("a param template", bounds=RealBounds(0, 100, "")) meas_template = MeasurementTemplate("Measurement template", parameters=[param_template]) - parameter = Parameter(name="A parameter", value=NormalReal(mean=17, std=1, units='')) + parameter = Parameter(name="A parameter", value=NormalReal(mean=17, std=1, units="")) measurement = MeasurementSpec(name="name", parameters=parameter, template=meas_template) test_dict = {"foo": measurement} recursive_foreach(test_dict, func, apply_first=True) @@ -58,10 +67,10 @@ def func(base_ent): def test_substitute_equivalence(): """PLA-6423: verify that substitutions match up.""" - spec = ProcessSpec(name="old spec", uids={'scope': 'spec'}) - run = ProcessRun(name="old run", - uids={'scope': 'run'}, - spec=LinkByUID(id='spec', scope="scope")) + spec = ProcessSpec(name="old spec", uids={"scope": "spec"}) + run = ProcessRun( + name="old run", uids={"scope": "run"}, spec=LinkByUID(id="spec", scope="scope") + ) # make a dictionary from ids to objects, to be used in substitute_objects gem_index = make_index([run, spec]) @@ -71,39 +80,28 @@ def test_substitute_equivalence(): def test_complex_substitutions(): """Make sure accounting works for realistic objects.""" - root = MaterialRun("root", - process=ProcessRun("root", spec=ProcessSpec("root")), - spec=MaterialSpec("root") - ) + root = MaterialRun( + "root", process=ProcessRun("root", spec=ProcessSpec("root")), spec=MaterialSpec("root") + ) root.spec.process = root.process.spec - input = MaterialRun("input", - process=ProcessRun("input", spec=ProcessSpec("input")), - spec=MaterialSpec("input") - ) + input = MaterialRun( + "input", process=ProcessRun("input", spec=ProcessSpec("input")), spec=MaterialSpec("input") + ) input.spec.process = input.process.spec - IngredientRun(process=root.process, - material=input, - spec=IngredientSpec("ingredient", - process=root.process.spec, - material=input.spec - ) - ) + IngredientRun( + process=root.process, + material=input, + spec=IngredientSpec("ingredient", process=root.process.spec, material=input.spec), + ) param = ParameterTemplate("Param", bounds=RealBounds(-1, 1, "m")) - root.process.spec.template = ProcessTemplate("Proc", - parameters=[param] - ) - root.process.parameters.append(Parameter("Param", - value=NormalReal(0, 1, 'm'), - template=param)) + root.process.spec.template = ProcessTemplate("Proc", parameters=[param]) + root.process.parameters.append(Parameter("Param", value=NormalReal(0, 1, "m"), template=param)) links = flatten(root, scope="test-scope") index = make_index(links) rebuild = substitute_objects(links, index, inplace=True) rebuilt_root = next(x for x in rebuild if x.name == root.name and x.typ == root.typ) - all_objs = recursive_flatmap(rebuilt_root, - func=lambda x: [x], - unidirectional=False - ) + all_objs = recursive_flatmap(rebuilt_root, func=lambda x: [x], unidirectional=False) unique = [x for i, x in enumerate(all_objs) if i == all_objs.index(x)] assert not any(isinstance(x, LinkByUID) for x in unique), "All are objects" assert len(links) == len(unique), "Objects are missing" @@ -117,16 +115,14 @@ def test_sub_inplace_lists(): [ [1, 2, 3], ], - lst_one + lst_one, ] - lol_dup = _substitute(lol_main, - applies=lambda x: isinstance(x, int), - sub=lambda x: x + 1) + lol_dup = _substitute(lol_main, applies=lambda x: isinstance(x, int), sub=lambda x: x + 1) assert lol_dup != lol_main - lol_mod = _substitute_inplace(lol_main, - applies=lambda x: isinstance(x, int), - sub=lambda x: x + 1) + lol_mod = _substitute_inplace( + lol_main, applies=lambda x: isinstance(x, int), sub=lambda x: x + 1 + ) assert lol_mod == lol_main assert lol_mod == lol_dup @@ -135,19 +131,15 @@ def test_sub_inplace_tuples(): """Verify consistency for nested tuples.""" lot_main = [ # Base object must mutable to make sense for inplace (1, 2, 3), - ( - (1, 2, 3), - ), - (1, 2, 3) + ((1, 2, 3),), + (1, 2, 3), ] - lot_dup = _substitute(lot_main, - applies=lambda x: isinstance(x, int), - sub=lambda x: x + 1) + lot_dup = _substitute(lot_main, applies=lambda x: isinstance(x, int), sub=lambda x: x + 1) assert lot_dup != lot_main - lot_mod = _substitute_inplace(lot_main, - applies=lambda x: isinstance(x, int), - sub=lambda x: x + 1) + lot_mod = _substitute_inplace( + lot_main, applies=lambda x: isinstance(x, int), sub=lambda x: x + 1 + ) assert lot_mod == lot_main assert lot_mod == lot_dup @@ -159,14 +151,12 @@ def test_sub_inplace_dicts(): "sub": {1: 1, 2: 2, 3: 3}, 3: 3, } - dod_dup = _substitute(dod_main, - applies=lambda x: isinstance(x, int), - sub=lambda x: x + 1) + dod_dup = _substitute(dod_main, applies=lambda x: isinstance(x, int), sub=lambda x: x + 1) assert dod_dup != dod_main - dod_mod = _substitute_inplace(dod_main, - applies=lambda x: isinstance(x, int), - sub=lambda x: x + 1) + dod_mod = _substitute_inplace( + dod_main, applies=lambda x: isinstance(x, int), sub=lambda x: x + 1 + ) assert dod_mod == dod_main assert dod_mod == dod_dup @@ -176,8 +166,6 @@ def test_sub_inplace_objects(): run = IngredientRun(spec=IngredientSpec("string"), notes="note") run.spec = None - _substitute_inplace(run, - applies=lambda x: isinstance(x, str), - sub=lambda x: f"{x}s") + _substitute_inplace(run, applies=lambda x: isinstance(x, str), sub=lambda x: f"{x}s") assert run.name == "strings" assert run.notes == "notes" diff --git a/tests/util/test_writeable_order.py b/tests/util/test_writeable_order.py index af9e9fbe..674fd740 100644 --- a/tests/util/test_writeable_order.py +++ b/tests/util/test_writeable_order.py @@ -1,6 +1,6 @@ import pytest -from gemd.entity.object import ProcessRun, MaterialRun +from gemd.entity.object import MaterialRun, ProcessRun from gemd.entity.value.nominal_integer import NominalInteger from gemd.util import writable_sort_order