diff --git a/gemd/entity/attribute/base_attribute.py b/gemd/entity/attribute/base_attribute.py index a097924e..63a3101d 100644 --- a/gemd/entity/attribute/base_attribute.py +++ b/gemd/entity/attribute/base_attribute.py @@ -6,6 +6,9 @@ from gemd.entity.file_link import FileLink from gemd.entity.link_by_uid import LinkByUID +from typing import Optional, Union, Iterable, List, Type +from abc import abstractmethod + class BaseAttribute(DictSerializable): """ @@ -31,8 +34,14 @@ class BaseAttribute(DictSerializable): """ - def __init__(self, name, *, template=None, origin="unknown", value=None, notes=None, - file_links=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 @@ -47,12 +56,12 @@ def __init__(self, name, *, template=None, origin="unknown", value=None, notes=N self.file_links = file_links @property - def value(self): + def value(self) -> BaseValue: """Get value.""" return self._value @value.setter - def value(self, value): + def value(self, value: BaseValue): if value is None: self._value = None elif isinstance(value, (BaseValue, str, bool)): @@ -61,36 +70,41 @@ def value(self, value): raise TypeError("value must be a BaseValue, string or bool: {}".format(value)) @property - def template(self): + def template(self) -> Optional[Union[AttributeTemplate, LinkByUID]]: """Get template.""" return self._template @template.setter - def template(self, template): + def template(self, template: Optional[Union[AttributeTemplate, LinkByUID]]): if template is None: self._template = None - elif isinstance(template, (LinkByUID, AttributeTemplate)): + elif isinstance(template, (self._template_type(), LinkByUID)): self._template = template else: raise TypeError("template must be a BaseAttributeTemplate or " "LinkByUID: {}".format(template)) + @staticmethod + @abstractmethod + def _template_type() -> Type: + """Get the expected type of template for this object (property of child).""" + @property - def origin(self): + def origin(self) -> str: """Get origin.""" return self._origin @origin.setter - def origin(self, origin): + def origin(self, origin: Union[Origin, str]): if origin is None: raise ValueError("origin must be specified (but may be `unknown`)") self._origin = Origin.get_value(origin) @property - def file_links(self): + def file_links(self) -> List[FileLink]: """Get file links.""" return self._file_links @file_links.setter - def file_links(self, file_links): + def file_links(self, file_links: Optional[Union[Iterable[FileLink], FileLink]]): self._file_links = validate_list(file_links, FileLink) diff --git a/gemd/entity/attribute/condition.py b/gemd/entity/attribute/condition.py index 6ab4d540..87dada89 100644 --- a/gemd/entity/attribute/condition.py +++ b/gemd/entity/attribute/condition.py @@ -1,4 +1,7 @@ from gemd.entity.attribute.base_attribute import BaseAttribute +from gemd.entity.template import ConditionTemplate + +from typing import Type class Condition(BaseAttribute): @@ -29,3 +32,7 @@ class Condition(BaseAttribute): """ typ = "condition" + + @staticmethod + def _template_type() -> Type: + return ConditionTemplate diff --git a/gemd/entity/attribute/parameter.py b/gemd/entity/attribute/parameter.py index 57b56738..8c741141 100644 --- a/gemd/entity/attribute/parameter.py +++ b/gemd/entity/attribute/parameter.py @@ -1,4 +1,7 @@ from gemd.entity.attribute.base_attribute import BaseAttribute +from gemd.entity.template import ParameterTemplate + +from typing import Type class Parameter(BaseAttribute): @@ -30,3 +33,7 @@ class Parameter(BaseAttribute): """ typ = "parameter" + + @staticmethod + def _template_type() -> Type: + return ParameterTemplate diff --git a/gemd/entity/attribute/property.py b/gemd/entity/attribute/property.py index 3beefbba..75e98ee8 100644 --- a/gemd/entity/attribute/property.py +++ b/gemd/entity/attribute/property.py @@ -1,4 +1,7 @@ from gemd.entity.attribute.base_attribute import BaseAttribute +from gemd.entity.template import PropertyTemplate + +from typing import Type class Property(BaseAttribute): @@ -29,3 +32,7 @@ class Property(BaseAttribute): """ typ = "property" + + @staticmethod + def _template_type() -> Type: + return PropertyTemplate diff --git a/gemd/entity/attribute/property_and_conditions.py b/gemd/entity/attribute/property_and_conditions.py index 94696266..46548497 100644 --- a/gemd/entity/attribute/property_and_conditions.py +++ b/gemd/entity/attribute/property_and_conditions.py @@ -1,8 +1,13 @@ 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 + class PropertyAndConditions(DictSerializable): """ @@ -21,50 +26,50 @@ class PropertyAndConditions(DictSerializable): typ = "property_and_conditions" - def __init__(self, property=None, conditions=None): + def __init__(self, property: Property = None, conditions: Iterable[Condition] = None): self._property = None self.property = property self._conditions = None self.conditions = conditions @property - def conditions(self): + def conditions(self) -> List[Condition]: """Get conditions.""" return self._conditions @conditions.setter - def conditions(self, conditions): + def conditions(self, conditions: Iterable[Condition]): self._conditions = validate_list(conditions, Condition) # Horrible hacks to make templates work in the short term @property - def name(self): + def name(self) -> str: """Get name of attribute (use name of property).""" return self.property.name @property - def template(self): + def template(self) -> Optional[Union[PropertyTemplate, LinkByUID]]: """Get template of attribute (use template of property).""" return self.property.template @property - def origin(self): + def origin(self) -> str: """Get origin of attribute (use origin of property).""" return self.property.origin @property - def value(self): + def value(self) -> BaseValue: """Get value of attribute (use value of property).""" return self.property.value # NOTE: this definition must go last, or else it overrides the property decorator @property - def property(self): + def property(self) -> Property: """Get property.""" return self._property @property.setter - def property(self, value): + def property(self, value: Property): if isinstance(value, Property): self._property = value else: diff --git a/gemd/entity/base_entity.py b/gemd/entity/base_entity.py index 17a330dc..f036c565 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 Optional, Dict, FrozenSet -from collections.abc import Collection +from typing import Optional, Union, Iterable, List, Set, FrozenSet, Mapping, Dict 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 class BaseEntity(DictSerializable): @@ -25,7 +26,7 @@ class BaseEntity(DictSerializable): typ = "base" - def __init__(self, uids, tags): + def __init__(self, uids: Mapping[str, str], tags: Iterable[str]): self._tags = None self.tags = tags @@ -33,34 +34,29 @@ def __init__(self, uids, tags): self.uids = uids @property - def tags(self): + def tags(self) -> List[str]: """Get the tags.""" return self._tags @tags.setter - def tags(self, tags): - if tags is None: - self._tags = [] - elif isinstance(tags, list): - self._tags = tags - else: - self._tags = [tags] + def tags(self, tags: Iterable[str]): + self._tags = validate_list(tags, str) @property - def uids(self): + def uids(self) -> Mapping[str, str]: """Get the uids.""" return self._uids @uids.setter - def uids(self, uids): + def uids(self, uids: Mapping[str, str]): if uids is None: self._uids = CaseInsensitiveDict() - elif isinstance(uids, dict): + elif isinstance(uids, Mapping): self._uids = CaseInsensitiveDict(**uids) else: self._uids = CaseInsensitiveDict(**{uids[0]: uids[1]}) - def add_uid(self, scope, uid): + def add_uid(self, scope: str, uid: str): """ Add a uid. @@ -106,6 +102,18 @@ def to_link(self, return LinkByUID(scope=scope, id=uid) + def all_dependencies(self) -> Set[Union["BaseEntity", "LinkByUID"]]: + """Return a set of all immediate dependencies (no recursion).""" + result = set() + 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__) + return result + @staticmethod def _cached_equals(this: 'BaseEntity', that: 'BaseEntity', @@ -146,7 +154,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, Collection) and isinstance(that_value, Collection) \ + elif isinstance(this_value, Iterable) and isinstance(that_value, Iterable) \ and not isinstance(this_value, str) and not isinstance(that_value, str): # Necessary to maintain context for recursive parts of the structure this_list = list(this_value) diff --git a/gemd/entity/dict_serializable.py b/gemd/entity/dict_serializable.py index 99d32485..dc8daeef 100644 --- a/gemd/entity/dict_serializable.py +++ b/gemd/entity/dict_serializable.py @@ -4,6 +4,7 @@ import json import inspect import functools +from typing import Union, Iterable, List, Mapping, Dict, Any # There are some weird (probably resolvable) errors during object cloning if this is an # instance variable of DictSerializable. @@ -17,7 +18,7 @@ class DictSerializable(ABC): skip = set() @classmethod - def from_dict(cls, d): + def from_dict(cls, d: Mapping[str, Any]) -> "DictSerializable": """ Reconstitute the object from a dictionary. @@ -47,13 +48,13 @@ def from_dict(cls, d): @classmethod @functools.lru_cache(maxsize=None) - def _init_sig(cls): + def _init_sig(cls) -> List[str]: """Internal method for generating the argument names for the class init method.""" expected_arg_names = inspect.getfullargspec(cls.__init__).args expected_arg_names += inspect.getfullargspec(cls.__init__).kwonlyargs return expected_arg_names - def as_dict(self): + def as_dict(self) -> Dict[str, Any]: """ Convert the object to a dictionary. @@ -68,7 +69,7 @@ def as_dict(self): attributes["type"] = self.typ return attributes - def dump(self): + def dump(self) -> str: """ Convert the object to a JSON dictionary, so that every entry is serialized. @@ -85,7 +86,7 @@ def dump(self): return json.loads(encoder.raw_dumps(self)) @staticmethod - def build(d): + def build(d: Mapping[str, Any]) -> "DictSerializable": """ Build an object from a JSON dictionary. @@ -107,7 +108,7 @@ def build(d): encoder = GEMDJson() return encoder.raw_loads(encoder.raw_dumps(d)) - def __repr__(self): + 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 vars(self) if x in self.skip} @@ -116,7 +117,7 @@ def __repr__(self): object_dict[key] = self._name_repr(skipped_field) return str(object_dict) - def _name_repr(self, entity): + def _name_repr(self, entity: Union[Iterable["DictSerializable"], "DictSerializable"]) -> str: """ A representation of an object or a list of objects that uses the name and type. @@ -135,15 +136,15 @@ def _name_repr(self, entity): A representation of `entity` using its name. """ - if isinstance(entity, (list, tuple)): + if isinstance(entity, Iterable): return [self._name_repr(item) for item in entity] elif entity is None: return None else: name = getattr(entity, 'name', '') - return "<{} '{}'>".format(type(entity).__name__, name) + return f"<{type(entity).__name__} '{name}'>" - def _dict_for_compare(self): + def _dict_for_compare(self) -> Dict[str, Any]: """Which fields & values are relevant to an equality test.""" return self.as_dict() diff --git a/gemd/entity/has_dependencies.py b/gemd/entity/has_dependencies.py new file mode 100644 index 00000000..14ab166b --- /dev/null +++ b/gemd/entity/has_dependencies.py @@ -0,0 +1,11 @@ +"""For entities that have dependencies.""" +from abc import ABC, abstractmethod +from typing import Union, Set + + +class HasDependencies(ABC): + """Mix-in trait for objects that reference other objects.""" + + @abstractmethod + def _local_dependencies(self) -> Set[Union["BaseEntity", "LinkByUID"]]: + """All dependencies (objects) that this class introduces.""" diff --git a/gemd/entity/object/base_object.py b/gemd/entity/object/base_object.py index 39f7f72b..76f6536e 100644 --- a/gemd/entity/object/base_object.py +++ b/gemd/entity/object/base_object.py @@ -4,6 +4,8 @@ from gemd.entity.file_link import FileLink from gemd.entity.setters import validate_list, validate_str +from typing import Optional, Union, Iterable, List, Mapping + class BaseObject(BaseEntity): """ @@ -30,7 +32,13 @@ class BaseObject(BaseEntity): """ - def __init__(self, name, *, uids=None, tags=None, notes=None, file_links=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 @@ -52,19 +60,19 @@ def _attribute_has_setter(cls, name: str) -> bool: return prop is None or prop.fset is not None @property - def name(self): + def name(self) -> str: """Get name.""" return self._name @name.setter - def name(self, name): + def name(self, name: str): self._name = validate_str(name) @property - def file_links(self): + def file_links(self) -> List[FileLink]: """Get file links.""" return self._file_links @file_links.setter - def file_links(self, file_links): + def file_links(self, file_links: Union[Iterable[FileLink], FileLink]): self._file_links = validate_list(file_links, FileLink) diff --git a/gemd/entity/object/has_conditions.py b/gemd/entity/object/has_conditions.py index ccf7efc9..cdec53b9 100644 --- a/gemd/entity/object/has_conditions.py +++ b/gemd/entity/object/has_conditions.py @@ -1,9 +1,12 @@ """For entities that have conditions.""" +from gemd.entity.has_dependencies import HasDependencies from gemd.entity.attribute.condition import Condition from gemd.entity.setters import validate_list +from typing import Union, Iterable, List, Set -class HasConditions(object): + +class HasConditions(HasDependencies): """Mixin-trait for entities that include conditions. Parameters @@ -13,15 +16,20 @@ class HasConditions(object): """ - def __init__(self, conditions): + def __init__(self, conditions: Iterable[Condition]): self._conditions = None self.conditions = conditions @property - def conditions(self): + def conditions(self) -> List[Condition]: """Get a list of the conditions.""" return self._conditions @conditions.setter - def conditions(self, conditions): + def conditions(self, conditions: Iterable[Condition]): + """Set the list of conditions.""" self._conditions = validate_list(conditions, Condition) + + def _local_dependencies(self) -> Set[Union["BaseEntity", "LinkByUID"]]: + """Return a set of all immediate dependencies (no recursion).""" + return {cond.template for cond in self.conditions if cond.template is not None} diff --git a/gemd/entity/object/has_material.py b/gemd/entity/object/has_material.py new file mode 100644 index 00000000..f4450c28 --- /dev/null +++ b/gemd/entity/object/has_material.py @@ -0,0 +1,25 @@ +"""For entities that have specs.""" +from gemd.entity.has_dependencies import HasDependencies +from gemd.entity.object.base_object import BaseObject +from gemd.entity.link_by_uid import LinkByUID + +from abc import abstractmethod +from typing import Union, Set + + +class HasMaterial(HasDependencies): + """Mix-in trait for objects that can be assigned materials.""" + + @property + @abstractmethod + def material(self) -> Union[BaseObject, LinkByUID]: + """Get the material.""" + + @material.setter + @abstractmethod + def material(self, spec: Union[BaseObject, LinkByUID]): + """Set the material.""" + + def _local_dependencies(self) -> Set[Union["BaseEntity", "LinkByUID"]]: + """Return a set of all immediate dependencies (no recursion).""" + return {self.material} if self.material is not None else set() diff --git a/gemd/entity/object/has_parameters.py b/gemd/entity/object/has_parameters.py index 0c52b583..4251d69a 100644 --- a/gemd/entity/object/has_parameters.py +++ b/gemd/entity/object/has_parameters.py @@ -1,9 +1,12 @@ """For entities that have parameters.""" +from gemd.entity.has_dependencies import HasDependencies from gemd.entity.attribute.parameter import Parameter from gemd.entity.setters import validate_list +from typing import Union, Iterable, List, Set -class HasParameters(object): + +class HasParameters(HasDependencies): """Mixin-trait for entities that include parameters. Parameters @@ -13,15 +16,20 @@ class HasParameters(object): """ - def __init__(self, parameters): + def __init__(self, parameters: Iterable[Parameter]): self._parameters = None self.parameters = parameters @property - def parameters(self): + def parameters(self) -> List[Parameter]: """Get the list of parameters.""" return self._parameters @parameters.setter - def parameters(self, parameters): + def parameters(self, parameters: Iterable[Parameter]): + """Set the list of parameters.""" self._parameters = validate_list(parameters, Parameter) + + def _local_dependencies(self) -> Set[Union["BaseEntity", "LinkByUID"]]: + """Return a set of all immediate dependencies (no recursion).""" + return {param.template for param in self.parameters if param.template is not None} diff --git a/gemd/entity/object/has_process.py b/gemd/entity/object/has_process.py new file mode 100644 index 00000000..5f029483 --- /dev/null +++ b/gemd/entity/object/has_process.py @@ -0,0 +1,25 @@ +"""For entities that have specs.""" +from gemd.entity.has_dependencies import HasDependencies +from gemd.entity.object.base_object import BaseObject +from gemd.entity.link_by_uid import LinkByUID + +from abc import abstractmethod +from typing import Union, Set + + +class HasProcess(HasDependencies): + """Mix-in trait for objects that can be assigned materials.""" + + @property + @abstractmethod + def process(self) -> Union[BaseObject, LinkByUID]: + """Get the process.""" + + @process.setter + @abstractmethod + def process(self, process: Union[BaseObject, LinkByUID]): + """Set the process.""" + + def _local_dependencies(self) -> Set[Union["BaseEntity", "LinkByUID"]]: + """Return a set of all immediate dependencies (no recursion).""" + return {self.process} if self.process is not None else set() diff --git a/gemd/entity/object/has_properties.py b/gemd/entity/object/has_properties.py index 397de18b..581bae54 100644 --- a/gemd/entity/object/has_properties.py +++ b/gemd/entity/object/has_properties.py @@ -1,9 +1,12 @@ """For entities that have properties.""" +from gemd.entity.has_dependencies import HasDependencies from gemd.entity.attribute.property import Property from gemd.entity.setters import validate_list +from typing import Union, Iterable, List, Set -class HasProperties(object): + +class HasProperties(HasDependencies): """Mixin-trait for entities that include properties. Parameters @@ -13,15 +16,20 @@ class HasProperties(object): """ - def __init__(self, properties): + def __init__(self, properties: Iterable[Property]): self._properties = None self.properties = properties @property - def properties(self): + def properties(self) -> List[Property]: """Get a list of the properties.""" return self._properties @properties.setter - def properties(self, properties): + def properties(self, properties: Iterable[Property]): + """Set the list of properties.""" self._properties = validate_list(properties, Property) + + def _local_dependencies(self) -> Set[Union["BaseEntity", "LinkByUID"]]: + """Return a set of all immediate dependencies (no recursion).""" + return {prop.template for prop in self.properties if prop.template is not None} diff --git a/gemd/entity/object/has_quantities.py b/gemd/entity/object/has_quantities.py index 034f2370..c2724195 100644 --- a/gemd/entity/object/has_quantities.py +++ b/gemd/entity/object/has_quantities.py @@ -26,8 +26,10 @@ class HasQuantities(object): """ def __init__(self, *, - mass_fraction=None, volume_fraction=None, number_fraction=None, - absolute_quantity=None): + 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 @@ -42,12 +44,12 @@ def __init__(self, *, self.absolute_quantity = absolute_quantity @property - def mass_fraction(self): + def mass_fraction(self) -> ContinuousValue: """Get mass fraction.""" return self._mass_fraction @mass_fraction.setter - def mass_fraction(self, mass_fraction): + def mass_fraction(self, mass_fraction: ContinuousValue): if mass_fraction is None: self._mass_fraction = None elif not isinstance(mass_fraction, ContinuousValue): @@ -56,12 +58,12 @@ def mass_fraction(self, mass_fraction): self._mass_fraction = mass_fraction @property - def volume_fraction(self): + def volume_fraction(self) -> ContinuousValue: """Get volume fraction.""" return self._volume_fraction @volume_fraction.setter - def volume_fraction(self, volume_fraction): + def volume_fraction(self, volume_fraction: ContinuousValue): if volume_fraction is None: self._volume_fraction = None elif not isinstance(volume_fraction, ContinuousValue): @@ -70,12 +72,12 @@ def volume_fraction(self, volume_fraction): self._volume_fraction = volume_fraction @property - def number_fraction(self): + def number_fraction(self) -> ContinuousValue: """Get number fraction.""" return self._number_fraction @number_fraction.setter - def number_fraction(self, number_fraction): + def number_fraction(self, number_fraction: ContinuousValue): if number_fraction is None: self._number_fraction = None elif not isinstance(number_fraction, ContinuousValue): @@ -84,12 +86,12 @@ def number_fraction(self, number_fraction): self._number_fraction = number_fraction @property - def absolute_quantity(self): + def absolute_quantity(self) -> ContinuousValue: """Get absolute quantity.""" return self._absolute_quantity @absolute_quantity.setter - def absolute_quantity(self, absolute_quantity): + def absolute_quantity(self, absolute_quantity: ContinuousValue): if absolute_quantity is None: self._absolute_quantity = None elif isinstance(absolute_quantity, ContinuousValue): diff --git a/gemd/entity/object/has_source.py b/gemd/entity/object/has_source.py index 99b134a5..a4b04bb1 100644 --- a/gemd/entity/object/has_source.py +++ b/gemd/entity/object/has_source.py @@ -13,20 +13,20 @@ class HasSource(object): """ - def __init__(self, source): + def __init__(self, source: PerformedSource): self._source = None self.source = source @property - def source(self): + def source(self) -> PerformedSource: """Get the list of parameters.""" return self._source @source.setter - def source(self, value): + def source(self, value: PerformedSource): if value is None: self._source = None elif isinstance(value, PerformedSource): self._source = value else: - raise TypeError("Source must be a PerformedSource; was {}".format(type(value))) + raise TypeError(f"Source must be a PerformedSource; was {type(value)}") diff --git a/gemd/entity/object/has_spec.py b/gemd/entity/object/has_spec.py new file mode 100644 index 00000000..6e50cc53 --- /dev/null +++ b/gemd/entity/object/has_spec.py @@ -0,0 +1,56 @@ +"""For entities that have specs.""" +from gemd.entity.has_dependencies import HasDependencies +from gemd.entity.object.has_template import HasTemplate +from gemd.entity.template.base_template import BaseTemplate +from gemd.entity.link_by_uid import LinkByUID + +from abc import abstractmethod +from typing import Optional, Union, Set, Type + + +class HasSpec(HasDependencies): + """Mix-in trait for objects that can be assigned specs. + + Parameters + ---------- + spec: :class:`Has_Template ` + A spec, which expresses the anticipated or aspirational behavior of this object. + + """ + + def __init__(self, spec: Union[HasTemplate, LinkByUID] = None): + self._spec = None + self.spec = spec + + @property + def spec(self) -> Union[HasTemplate, LinkByUID]: + """Get the spec.""" + return self._spec + + @spec.setter + def spec(self, spec: Union[HasTemplate, LinkByUID]): + """Set the spec.""" + if spec is None: + self._spec = None + 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)}") + + @staticmethod + @abstractmethod + def _spec_type() -> Type: + """Child must report implementation details.""" + + @property + def template(self) -> Optional[Union[BaseTemplate, LinkByUID]]: + """Get the template associated with the spec.""" + if isinstance(self.spec, HasTemplate): + return self.spec.template + else: + return None + + def _local_dependencies(self) -> Set[Union["BaseEntity", "LinkByUID"]]: + """Return a set of all immediate dependencies (no recursion).""" + return {self.spec} if self.spec is not None else set() diff --git a/gemd/entity/object/has_template.py b/gemd/entity/object/has_template.py index eec23d96..f69d684f 100644 --- a/gemd/entity/object/has_template.py +++ b/gemd/entity/object/has_template.py @@ -1,9 +1,13 @@ """For entities that have templates.""" +from gemd.entity.has_dependencies import HasDependencies from gemd.entity.template.base_template import BaseTemplate from gemd.entity.link_by_uid import LinkByUID +from abc import abstractmethod +from typing import Optional, Union, Set, Type -class HasTemplate(object): + +class HasTemplate(HasDependencies): """Mix-in trait for objects that can be assigned templates. Parameters @@ -13,20 +17,31 @@ class HasTemplate(object): """ - def __init__(self, template=None): + def __init__(self, template: Optional[Union[BaseTemplate, LinkByUID]] = None): self._template = None self.template = template + @staticmethod + @abstractmethod + def _template_type() -> Type: + """Child must report implementation details.""" + @property - def template(self): + def template(self) -> Optional[Union[BaseTemplate, LinkByUID]]: """Get the template.""" return self._template @template.setter - def template(self, template): + def template(self, template: Optional[Union[BaseTemplate, LinkByUID]]): + """Set the template.""" if template is None: self._template = None - elif isinstance(template, (BaseTemplate, LinkByUID)): + elif isinstance(template, (self._template_type(), LinkByUID)): self._template = template else: - raise TypeError("Template must be a template or LinkByUID: {}".format(template)) + raise TypeError(f"Template must be a {self._template_type()} or LinkByUID, " + f"not {type(template)}") + + def _local_dependencies(self) -> Set[Union["BaseEntity", "LinkByUID"]]: + """Return a set of all immediate dependencies (no recursion).""" + return {self.template} if self.template is not None else set() diff --git a/gemd/entity/object/ingredient_run.py b/gemd/entity/object/ingredient_run.py index e85ec864..5e514954 100644 --- a/gemd/entity/object/ingredient_run.py +++ b/gemd/entity/object/ingredient_run.py @@ -1,9 +1,21 @@ +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.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.setters import validate_list +from typing import Optional, Union, Iterable, List, Mapping, Type, Any -class IngredientRun(BaseObject, HasQuantities): + +class IngredientRun(BaseObject, HasQuantities, HasSpec, HasMaterial, HasProcess): """ An ingredient run. @@ -46,26 +58,35 @@ class IngredientRun(BaseObject, HasQuantities): typ = "ingredient_run" - def __init__(self, *, material=None, process=None, mass_fraction=None, - volume_fraction=None, number_fraction=None, absolute_quantity=None, - spec=None, uids=None, tags=None, notes=None, file_links=None): + 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 ) self._material = None self._process = None - self._spec = None - self._labels = None - # this will overwrite name/labels if/when they are set - self.spec = spec self.material = material self.process = process @property - def name(self): + def name(self) -> str: """Get name.""" from gemd.entity.object.ingredient_spec import IngredientSpec if isinstance(self.spec, IngredientSpec): @@ -74,7 +95,7 @@ def name(self): return super().name @property - def labels(self): + def labels(self) -> List[str]: """Get labels.""" from gemd.entity.object.ingredient_spec import IngredientSpec if isinstance(self.spec, IngredientSpec): @@ -83,14 +104,12 @@ def labels(self): return self._labels @property - def material(self): + def material(self) -> Union[MaterialRun, LinkByUID]: """Get the material.""" return self._material @material.setter - def material(self, material): - from gemd.entity.object import MaterialRun - from gemd.entity.link_by_uid import LinkByUID + def material(self, material: Union[MaterialRun, LinkByUID]): if material is None: self._material = None elif isinstance(material, (MaterialRun, LinkByUID)): @@ -100,14 +119,12 @@ def material(self, material): "LinkByUID: {}".format(material)) @property - def process(self): + def process(self) -> Union[ProcessRun, LinkByUID]: """Get the material.""" return self._process @process.setter - def process(self, process): - from gemd.entity.object import ProcessRun - from gemd.entity.link_by_uid import LinkByUID + def process(self, process: Union[ProcessRun, LinkByUID]): if isinstance(self._process, ProcessRun): # This could throw an exception if it's not in the list, but then something else broke self._process.ingredients.remove(self) @@ -120,29 +137,27 @@ def process(self, process): raise TypeError("IngredientRun.process must be a ProcessRun or " "LinkByUID: {}".format(process)) + @staticmethod + def _spec_type() -> Type: + """Required method to satisfy HasTemplates mix-in.""" + return IngredientSpec + @property - def spec(self): - """Get the ingredient spec.""" - return self._spec + def spec(self) -> Union[IngredientSpec, LinkByUID]: + """Get the spec.""" + return super().spec @spec.setter - def spec(self, spec): - from gemd.entity.object.ingredient_spec import IngredientSpec - from gemd.entity.link_by_uid import LinkByUID - - if isinstance(self._spec, IngredientSpec): # Store values if you had them + def spec(self, spec: Union[IngredientSpec, LinkByUID]): + """Set the spec.""" + if isinstance(self.spec, IngredientSpec): # Store values if you had them self._name = self.spec.name self._labels = validate_list(self.spec.labels, str) - - if spec is None: - self._spec = None - elif isinstance(spec, (IngredientSpec, LinkByUID)): - self._spec = spec - else: - raise TypeError("spec must be a IngredientSpec or LinkByUID: {}".format(spec)) + # Note that the super() mechanism does not work properly for overloaded setters + getattr(HasSpec, "spec").fset(self, spec) @classmethod - def from_dict(cls, d): + def from_dict(cls, d: Mapping[str, Any]) -> DictSerializable: """ Overloaded method from DictSerializable to intercept `name` and `labels` fields. diff --git a/gemd/entity/object/ingredient_spec.py b/gemd/entity/object/ingredient_spec.py index d70bbfae..95721548 100644 --- a/gemd/entity/object/ingredient_spec.py +++ b/gemd/entity/object/ingredient_spec.py @@ -1,9 +1,19 @@ +from gemd.entity.object.material_spec import MaterialSpec +from gemd.entity.object.process_spec import ProcessSpec 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.setters import validate_list +from typing import Optional, Union, Iterable, List, Mapping, Type -class IngredientSpec(BaseObject, HasQuantities): + +class IngredientSpec(BaseObject, HasQuantities, HasTemplate, HasMaterial, HasProcess): """ An ingredient specification. @@ -48,10 +58,20 @@ class IngredientSpec(BaseObject, HasQuantities): typ = "ingredient_spec" - def __init__(self, name, *, material=None, process=None, labels=None, - mass_fraction=None, volume_fraction=None, number_fraction=None, - absolute_quantity=None, - uids=None, tags=None, notes=None, file_links=None): + 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) @@ -68,23 +88,22 @@ def __init__(self, name, *, material=None, process=None, labels=None, self.process = process @property - def labels(self): + def labels(self) -> List[str]: """Get labels.""" return self._labels @labels.setter - def labels(self, labels): + def labels(self, labels: Iterable[str]): self._labels = validate_list(labels, str) @property - def material(self): + def material(self) -> Union[MaterialSpec, LinkByUID]: """Get the material spec.""" return self._material @material.setter - def material(self, material): - from gemd.entity.object.material_spec import MaterialSpec - from gemd.entity.link_by_uid import LinkByUID + def material(self, material: Union[MaterialSpec, LinkByUID]): + """Set the material spec.""" if material is None: self._material = None elif isinstance(material, (MaterialSpec, LinkByUID)): @@ -93,14 +112,13 @@ def material(self, material): raise TypeError("IngredientSpec.material must be a MaterialSpec or LinkByUID") @property - def process(self): - """Get the material.""" + def process(self) -> Union[ProcessSpec, LinkByUID]: + """Get the process.""" return self._process @process.setter - def process(self, process): - from gemd.entity.object import ProcessSpec - from gemd.entity.link_by_uid import LinkByUID + def process(self, process: Union[ProcessSpec, LinkByUID]): + """Set the process.""" if isinstance(self._process, ProcessSpec): # This could throw an exception if it's not in the list, but then something else broke self._process.ingredients.remove(self) @@ -112,3 +130,18 @@ def process(self, process): else: raise TypeError("IngredientSpec.process must be a ProcessSpec or " "LinkByUID: {}".format(process)) + + @property + def template(self): + """Ingredients do not have templates, so this method always returns None.""" + return None + + @template.setter + def template(self, template): + """Ingredients do not have templates, so this method always raises an exception.""" + raise AttributeError("Ingredients do not support a template.") + + @staticmethod + def _template_type() -> Type: + """Required method to satisfy HasTemplates mix-in.""" + return type(None) diff --git a/gemd/entity/object/material_run.py b/gemd/entity/object/material_run.py index 86f52fbb..04417fc5 100644 --- a/gemd/entity/object/material_run.py +++ b/gemd/entity/object/material_run.py @@ -1,9 +1,17 @@ +from gemd.entity.object.material_spec import MaterialSpec +from gemd.entity.object.process_run import ProcessRun 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.setters import validate_list +from typing import Optional, Union, Iterable, List, Mapping, Type, Any -class MaterialRun(BaseObject): + +class MaterialRun(BaseObject, HasSpec, HasProcess): """ A material run. @@ -46,31 +54,34 @@ class MaterialRun(BaseObject): skip = {"_measurements"} - def __init__(self, name, *, spec=None, process=None, sample_type="unknown", - uids=None, tags=None, notes=None, file_links=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 - from gemd.entity.link_by_uid import LinkByUID - 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]) self._sample_type = None - self._spec = None - self.spec = spec self.process = process self.sample_type = sample_type @property - def process(self): + def process(self) -> Union[ProcessRun, LinkByUID]: """Get the originating process run.""" return self._process @process.setter - def process(self, process): - from gemd.entity.object.process_run import ProcessRun - from gemd.entity.link_by_uid import LinkByUID + def process(self, process: Union[ProcessRun, LinkByUID]): if self.process is not None and isinstance(self.process, ProcessRun): self.process._output_material = None if process is None: @@ -84,45 +95,25 @@ def process(self, process): raise TypeError("process must be a ProcessRun or LinkByUID: {}".format(process)) @property - def measurements(self): - """Get a list of measurement runs.""" + def measurements(self) -> List["MeasurementRun"]: + """Get a read-only list of the measurement runs.""" return self._measurements @property - def sample_type(self): + def sample_type(self) -> str: """Get the sample type.""" return self._sample_type @sample_type.setter - def sample_type(self, sample_type): + def sample_type(self, sample_type: Union[SampleType, str]): self._sample_type = SampleType.get_value(sample_type) - @property - def spec(self): - """Get the material spec.""" - return self._spec - - @spec.setter - def spec(self, spec): - from gemd.entity.object.material_spec import MaterialSpec - from gemd.entity.link_by_uid import LinkByUID - if spec is None: - self._spec = None - elif isinstance(spec, (MaterialSpec, LinkByUID)): - self._spec = spec - else: - raise TypeError("spec must be a MaterialSpec or LinkByUID: {}".format(spec)) - - @property - def template(self): - """Get the template of the spec, if applicable.""" - from gemd.entity.object.material_spec import MaterialSpec - if isinstance(self.spec, MaterialSpec): - return self.spec.template - else: - return None + @staticmethod + def _spec_type() -> Type: + """Required method to satisfy HasTemplates mix-in.""" + return MaterialSpec - def _dict_for_compare(self): + def _dict_for_compare(self) -> Mapping[str, Any]: """Support for recursive equals.""" base = super()._dict_for_compare() base['measurements'] = self.measurements diff --git a/gemd/entity/object/material_spec.py b/gemd/entity/object/material_spec.py index 7006861b..7d92a43a 100644 --- a/gemd/entity/object/material_spec.py +++ b/gemd/entity/object/material_spec.py @@ -1,10 +1,17 @@ from gemd.entity.attribute.property_and_conditions import PropertyAndConditions +from gemd.entity.object.process_spec import ProcessSpec from gemd.entity.object.base_object import BaseObject +from gemd.entity.object.has_process import HasProcess from gemd.entity.object.has_template import HasTemplate +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 -class MaterialSpec(BaseObject, HasTemplate): + +class MaterialSpec(BaseObject, HasTemplate, HasProcess): """ A material specification. @@ -40,9 +47,16 @@ class MaterialSpec(BaseObject, HasTemplate): typ = "material_spec" - def __init__(self, name, *, template=None, - properties=None, process=None, uids=None, tags=None, - notes=None, file_links=None): + def __init__(self, + name: str, + *, + template: Optional[Union[MaterialTemplate, LinkByUID]] = None, + process: Union[ProcessSpec, LinkByUID] = None, + properties: Iterable[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) self._properties = None @@ -52,21 +66,21 @@ def __init__(self, name, *, template=None, HasTemplate.__init__(self, template) @property - def properties(self): + def properties(self) -> List[PropertyAndConditions]: """Get the list of property-and-conditions.""" return self._properties @properties.setter - def properties(self, properties): + def properties(self, properties: Iterable[PropertyAndConditions]): self._properties = validate_list(properties, PropertyAndConditions) @property - def process(self): + def process(self) -> Union[ProcessSpec, LinkByUID]: """Get the originating process spec.""" return self._process @process.setter - def process(self, process): + def process(self, process: Union[ProcessSpec, LinkByUID]): """ Link to the ProcessSpec that creates this MaterialSpec. @@ -86,5 +100,21 @@ def process(self, process): process._output_material = self self._process = process else: - raise TypeError("process must be an instance of ProcessSpec or LinkByUID; " - "instead received type {}: {}".format(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: + """Communicate expected template type to parent class.""" + return MaterialTemplate + + def _local_dependencies(self) -> Set[Union["BaseEntity", "LinkByUID"]]: + """Return a set of all immediate dependencies (no recursion).""" + result = set() + for attr in self.properties: + if attr.property.template is not None: + result.add(attr.property.template) + for condition in attr.conditions: + if condition.template is not None: + result.add(condition.template) + return result diff --git a/gemd/entity/object/measurement_run.py b/gemd/entity/object/measurement_run.py index 1b40fc8a..e2ffba7e 100644 --- a/gemd/entity/object/measurement_run.py +++ b/gemd/entity/object/measurement_run.py @@ -1,11 +1,24 @@ +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 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 -class MeasurementRun(BaseObject, HasConditions, HasProperties, HasParameters, HasSource): + +class MeasurementRun(BaseObject, HasMaterial, HasSpec, HasConditions, HasProperties, + HasParameters, HasSource): """ A measurement run. @@ -48,30 +61,37 @@ class MeasurementRun(BaseObject, HasConditions, HasProperties, HasParameters, Ha typ = "measurement_run" - def __init__(self, name, *, spec=None, material=None, - properties=None, conditions=None, parameters=None, - uids=None, tags=None, notes=None, file_links=None, source=None): + def __init__(self, + name: str, + *, + spec: Union[MeasurementSpec, LinkByUID] = None, + material: Union[MaterialRun, LinkByUID] = None, + properties: Iterable[Property] = None, + conditions: Iterable[Condition] = None, + parameters: 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) HasParameters.__init__(self, parameters) HasSource.__init__(self, source) - self._spec = None - self.spec = spec self._material = None self.material = material @property - def material(self): + def material(self) -> Union[MaterialRun, LinkByUID]: """Get the material.""" return self._material @material.setter - def material(self, value): - from gemd.entity.object import MaterialRun - from gemd.entity.link_by_uid import LinkByUID + def material(self, value: Union[MaterialRun, LinkByUID]): if isinstance(self._material, MaterialRun): # This could throw an exception if it's not in the list, but then something else broke self._material.measurements.remove(self) @@ -83,27 +103,7 @@ def material(self, value): else: raise TypeError("material must be a MaterialRun or LinkByUID: {}".format(value)) - @property - def spec(self): - """Get the measurement spec.""" - return self._spec - - @spec.setter - def spec(self, spec): - from gemd.entity.object.measurement_spec import MeasurementSpec - from gemd.entity.link_by_uid import LinkByUID - if spec is None: - self._spec = None - elif isinstance(spec, (MeasurementSpec, LinkByUID)): - self._spec = spec - else: - raise TypeError("spec must be a MeasurementSpec or LinkByUID: {}".format(spec)) - - @property - def template(self): - """Get the template of the spec, if applicable.""" - from gemd.entity.object.measurement_spec import MeasurementSpec - if isinstance(self.spec, MeasurementSpec): - return self.spec.template - else: - return None + @staticmethod + def _spec_type() -> Type: + """Required method to satisfy HasTemplates mix-in.""" + return MeasurementSpec diff --git a/gemd/entity/object/measurement_spec.py b/gemd/entity/object/measurement_spec.py index 7926838d..586f7666 100644 --- a/gemd/entity/object/measurement_spec.py +++ b/gemd/entity/object/measurement_spec.py @@ -2,6 +2,13 @@ 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 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 class MeasurementSpec(BaseObject, HasParameters, HasConditions, HasTemplate): @@ -40,11 +47,23 @@ class MeasurementSpec(BaseObject, HasParameters, HasConditions, HasTemplate): typ = "measurement_spec" - def __init__(self, name, *, template=None, - parameters=None, conditions=None, - uids=None, tags=None, notes=None, file_links=None): + def __init__(self, + name: str, + *, + template: Optional[Union[MeasurementTemplate, LinkByUID]] = None, + conditions: Iterable[Condition] = None, + parameters: 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) HasParameters.__init__(self, parameters=parameters) HasConditions.__init__(self, conditions=conditions) HasTemplate.__init__(self, template=template) + + @staticmethod + def _template_type() -> Type: + """Communicate expected template type to parent class.""" + return MeasurementTemplate diff --git a/gemd/entity/object/process_run.py b/gemd/entity/object/process_run.py index e03aa56e..f7900b49 100644 --- a/gemd/entity/object/process_run.py +++ b/gemd/entity/object/process_run.py @@ -1,11 +1,20 @@ +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 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.setters import validate_list +from typing import Optional, Union, Iterable, List, Mapping, Dict, Type, Any -class ProcessRun(BaseObject, HasConditions, HasParameters, HasSource): + +class ProcessRun(BaseObject, HasSpec, HasConditions, HasParameters, HasSource): """ A process run. @@ -55,59 +64,45 @@ class ProcessRun(BaseObject, HasConditions, HasParameters, HasSource): skip = {"_output_material", "_ingredients"} - def __init__(self, name, *, spec=None, - conditions=None, parameters=None, - uids=None, tags=None, notes=None, file_links=None, source=None): + def __init__(self, + name: str, + *, + spec: Union[ProcessSpec, LinkByUID] = None, + conditions: Iterable[Condition] = None, + parameters: 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 - from gemd.entity.link_by_uid import LinkByUID 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) HasSource.__init__(self, source) - self._spec = None - self.spec = spec - self._output_material = None self._ingredients = validate_list(None, [IngredientRun, LinkByUID]) + self._output_material = None @property - def output_material(self): + def output_material(self) -> Optional["MaterialRun"]: """Get the output material run.""" return self._output_material @property - def ingredients(self): + def ingredients(self) -> List["IngredientRun"]: """Get the input ingredient runs.""" return self._ingredients - @property - def spec(self): - """Get the process spec.""" - return self._spec - - @spec.setter - def spec(self, spec): - from gemd.entity.object.process_spec import ProcessSpec - from gemd.entity.link_by_uid import LinkByUID - if spec is None: - self._spec = None - elif isinstance(spec, (ProcessSpec, LinkByUID)): - self._spec = spec - else: - raise TypeError("spec must be a ProcessSpec or LinkByUID: {}".format(spec)) + @staticmethod + def _spec_type() -> Type: + """Required method to satisfy HasTemplates mix-in.""" + return ProcessSpec - @property - def template(self): - """Get the template of the spec, if applicable.""" - from gemd.entity.object.process_spec import ProcessSpec - if isinstance(self.spec, ProcessSpec): - return self.spec.template - else: - return None - - def _dict_for_compare(self): + def _dict_for_compare(self) -> Dict[str, Any]: """Support for recursive equals.""" base = super()._dict_for_compare() base['ingredients'] = self.ingredients diff --git a/gemd/entity/object/process_spec.py b/gemd/entity/object/process_spec.py index 9a18002b..f6f1cb1e 100644 --- a/gemd/entity/object/process_spec.py +++ b/gemd/entity/object/process_spec.py @@ -2,8 +2,15 @@ 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 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.setters import validate_list +from typing import Optional, Union, Iterable, List, Mapping, Dict, Type, Any + class ProcessSpec(BaseObject, HasParameters, HasConditions, HasTemplate): """ @@ -54,9 +61,16 @@ class ProcessSpec(BaseObject, HasParameters, HasConditions, HasTemplate): skip = {"_output_material", "_ingredients"} - def __init__(self, name, *, template=None, - parameters=None, conditions=None, - uids=None, tags=None, notes=None, file_links=None): + def __init__(self, + name: str, + *, + template: Optional[Union[ProcessTemplate, LinkByUID]] = None, + conditions: Iterable[Condition] = None, + parameters: 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 from gemd.entity.link_by_uid import LinkByUID @@ -72,17 +86,22 @@ def __init__(self, name, *, template=None, self._output_material = None self._ingredients = validate_list(None, [IngredientSpec, LinkByUID]) + @staticmethod + def _template_type() -> Type: + """Communicate expected template type to parent class.""" + return ProcessTemplate + @property - def ingredients(self): + def ingredients(self) -> List["IngredientSpec"]: """Get the list of input ingredient specs.""" return self._ingredients @property - def output_material(self): + def output_material(self) -> Optional["MaterialSpec"]: # noqa: F821 """Get the output material spec.""" return self._output_material - def _dict_for_compare(self): + def _dict_for_compare(self) -> Dict[str, Any]: """Support for recursive equals.""" base = super()._dict_for_compare() base['ingredients'] = self.ingredients diff --git a/gemd/entity/object/tests/test_ingredient_spec.py b/gemd/entity/object/tests/test_ingredient_spec.py index 4dd4e75a..2984e830 100644 --- a/gemd/entity/object/tests/test_ingredient_spec.py +++ b/gemd/entity/object/tests/test_ingredient_spec.py @@ -86,3 +86,13 @@ def test_invalid_assignment(): IngredientSpec(name="name", process="process") with pytest.raises(TypeError): IngredientSpec() # Name is required + + +def test_bad_has_template(): + """Make sure the non-implementation of HasTemplate behaves properly.""" + assert isinstance(None, IngredientSpec(name="name")._template_type()), \ + "Ingredients didn't have NoneType templates" + 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/gemd/entity/object/tests/test_material_run.py b/gemd/entity/object/tests/test_material_run.py index 21fa9bcf..5b4e46aa 100644 --- a/gemd/entity/object/tests/test_material_run.py +++ b/gemd/entity/object/tests/test_material_run.py @@ -6,7 +6,7 @@ from gemd.json import loads, dumps from gemd.entity.attribute import PropertyAndConditions, Property -from gemd.entity.object import MaterialRun, ProcessRun, MaterialSpec, MeasurementRun +from gemd.entity.object import MaterialRun, ProcessSpec, ProcessRun, MaterialSpec, MeasurementRun from gemd.entity.template import MaterialTemplate from gemd.entity.value import NominalReal from gemd.entity.link_by_uid import LinkByUID @@ -147,3 +147,15 @@ def test_equality(): 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" + + +def test_dependencies(): + """Test that dependency lists make sense.""" + ps = ProcessSpec(name="ps") + pr = ProcessRun(name="pr", spec=ps) + ms = MaterialSpec(name="ms", process=ps) + mr = MaterialRun(name="mr", spec=ms, process=pr) + + assert ps not in mr.all_dependencies() + assert pr in mr.all_dependencies() + assert ms in mr.all_dependencies() diff --git a/gemd/entity/object/tests/test_material_spec.py b/gemd/entity/object/tests/test_material_spec.py index b3c7b75b..84ebdc3c 100644 --- a/gemd/entity/object/tests/test_material_spec.py +++ b/gemd/entity/object/tests/test_material_spec.py @@ -1,8 +1,12 @@ """Tests of the material spec object.""" import pytest +from gemd.entity.attribute import PropertyAndConditions, Property, Condition +from gemd.entity.bounds import IntegerBounds from gemd.entity.object.process_spec import ProcessSpec from gemd.entity.object.material_spec import MaterialSpec +from gemd.entity.template import MaterialTemplate, PropertyTemplate, ConditionTemplate +from gemd.entity.value import NominalInteger def test_process_reassignment(): @@ -28,3 +32,22 @@ def test_invalid_assignment(): MaterialSpec("name", template=MaterialSpec("another spec")) with pytest.raises(TypeError): MaterialSpec() # Name is required + + +def test_dependencies(): + """Test that dependency lists make sense.""" + prop = PropertyTemplate(name="name", bounds=IntegerBounds(0, 1)) + 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)) + ] + )]) + + assert template in spec.all_dependencies() + assert cond in spec.all_dependencies() + assert prop in spec.all_dependencies() diff --git a/gemd/entity/object/tests/test_measurement_run.py b/gemd/entity/object/tests/test_measurement_run.py index df66cf04..56e30da4 100644 --- a/gemd/entity/object/tests/test_measurement_run.py +++ b/gemd/entity/object/tests/test_measurement_run.py @@ -3,14 +3,16 @@ 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 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.template.measurement_template import MeasurementTemplate -from gemd.entity.value.nominal_real import NominalReal +from gemd.entity.template import MeasurementTemplate, PropertyTemplate, ParameterTemplate, \ + ConditionTemplate +from gemd.entity.value import NominalReal, NominalInteger from gemd.entity.file_link import FileLink from gemd.entity.link_by_uid import LinkByUID from gemd.util.impl import substitute_links @@ -134,3 +136,35 @@ def test_template_access(): meas.spec = LinkByUID.from_entity(spec) assert meas.template is None + + +def test_dependencies(): + """Test that dependency lists make sense.""" + prop = PropertyTemplate(name="name", bounds=IntegerBounds(0, 1)) + 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]) + 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)) + ] + ) + + assert template not in meas.all_dependencies() + assert spec in meas.all_dependencies() + assert mat in meas.all_dependencies() + assert prop in meas.all_dependencies() + assert cond in meas.all_dependencies() + assert param in meas.all_dependencies() diff --git a/gemd/entity/setters.py b/gemd/entity/setters.py index 5905e2e4..0213179b 100644 --- a/gemd/entity/setters.py +++ b/gemd/entity/setters.py @@ -1,8 +1,10 @@ """Methods for setting and validating.""" from gemd.entity.valid_list import ValidList +from typing import Iterable -def validate_list(obj, typ, *, trigger=None): + +def validate_list(obj, typ, *, trigger=None) -> ValidList: """ Attempts to return obj as a list, each element of which has type typ. @@ -24,13 +26,13 @@ def validate_list(obj, typ, *, trigger=None): """ if obj is None: return ValidList([], typ, trigger) - elif isinstance(obj, (list, tuple)): + elif isinstance(obj, Iterable): return ValidList(obj, typ, trigger) else: return ValidList([obj], typ, trigger) -def validate_str(obj): +def validate_str(obj) -> str: """ Check that obj is a string and then convert it to unicode. @@ -52,9 +54,4 @@ def validate_str(obj): """ if not isinstance(obj, str): raise TypeError("Expected a string but got {} instead".format(type(obj))) - - # If python 2 and the string isn't already unicode, turn it into unicode""" - try: - return obj.decode("utf-8") - except AttributeError: - return obj + return obj diff --git a/gemd/entity/template/attribute_template.py b/gemd/entity/template/attribute_template.py index d38b75a9..6af0f5ae 100644 --- a/gemd/entity/template/attribute_template.py +++ b/gemd/entity/template/attribute_template.py @@ -49,3 +49,8 @@ def bounds(self, bounds): if not isinstance(bounds, BaseBounds): raise TypeError("Bounds must be an instance of BaseBounds: {}".format(bounds)) self._bounds = bounds + + def all_dependencies(self): + """Return a set of all immediate dependencies (no recursion).""" + # Attribute Templates never depend on other objects. + return set() diff --git a/gemd/entity/template/base_template.py b/gemd/entity/template/base_template.py index 89187077..cac6eaf9 100644 --- a/gemd/entity/template/base_template.py +++ b/gemd/entity/template/base_template.py @@ -4,6 +4,8 @@ from gemd.entity.link_by_uid import LinkByUID from gemd.entity.template.attribute_template import AttributeTemplate +from typing import Union, Iterable, Mapping + class BaseTemplate(BaseEntity): """ @@ -26,13 +28,21 @@ class BaseTemplate(BaseEntity): """ - def __init__(self, name, *, description=None, uids=None, tags=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): + 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. diff --git a/gemd/entity/template/has_condition_templates.py b/gemd/entity/template/has_condition_templates.py index 41e1eb55..3bc56eb3 100644 --- a/gemd/entity/template/has_condition_templates.py +++ b/gemd/entity/template/has_condition_templates.py @@ -1,13 +1,15 @@ """For entities that have a condition template.""" +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 Iterable +from typing import Optional, Union, Iterable, List, Tuple, Set -class HasConditionTemplates(object): + +class HasConditionTemplates(HasDependencies): """ Mixin-trait for entities that include condition templates. @@ -19,12 +21,14 @@ class HasConditionTemplates(object): """ - def __init__(self, conditions): + 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): + def conditions(self) -> List[Union[ConditionTemplate, LinkByUID]]: """ Get the list of condition template/bounds tuples. @@ -37,7 +41,9 @@ def conditions(self): return self._conditions @conditions.setter - def conditions(self, conditions): + def conditions(self, conditions: Iterable[Union[Union[ConditionTemplate, LinkByUID], + Tuple[Union[ConditionTemplate, LinkByUID], + Optional[BaseBounds]]]]): """ Set the list of condition templates. @@ -60,3 +66,7 @@ def conditions(self, conditions): (ConditionTemplate, LinkByUID, list, tuple), trigger=BaseTemplate._homogenize_ranges ) + + def _local_dependencies(self) -> Set[Union["BaseEntity", "LinkByUID"]]: + """Return a set of all immediate dependencies (no recursion).""" + return {attr[0] for attr in self.conditions} diff --git a/gemd/entity/template/has_parameter_templates.py b/gemd/entity/template/has_parameter_templates.py index 4859e24c..f4e5b143 100644 --- a/gemd/entity/template/has_parameter_templates.py +++ b/gemd/entity/template/has_parameter_templates.py @@ -1,13 +1,15 @@ """For entities that have a parameter template.""" +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 Iterable +from typing import Optional, Union, Iterable, List, Tuple, Set -class HasParameterTemplates(object): + +class HasParameterTemplates(HasDependencies): """ Mixin-trait for entities that include parameter templates. @@ -19,12 +21,14 @@ class HasParameterTemplates(object): """ - def __init__(self, parameters): + 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): + def parameters(self) -> List[Union[ParameterTemplate, LinkByUID]]: """ Get the list of parameter template/bounds tuples. @@ -37,7 +41,9 @@ def parameters(self): return self._parameters @parameters.setter - def parameters(self, parameters): + def parameters(self, parameters: Iterable[Union[Union[ParameterTemplate, LinkByUID], + Tuple[Union[ParameterTemplate, LinkByUID], + Optional[BaseBounds]]]]): """ Set the list of parameter templates. @@ -60,3 +66,7 @@ def parameters(self, parameters): (ParameterTemplate, LinkByUID, list, tuple), trigger=BaseTemplate._homogenize_ranges ) + + def _local_dependencies(self) -> Set[Union["BaseEntity", "LinkByUID"]]: + """Return a set of all immediate dependencies (no recursion).""" + return {attr[0] for attr in self.parameters} diff --git a/gemd/entity/template/has_property_templates.py b/gemd/entity/template/has_property_templates.py index c8e2a3f8..28e20328 100644 --- a/gemd/entity/template/has_property_templates.py +++ b/gemd/entity/template/has_property_templates.py @@ -1,13 +1,15 @@ """For entities that have a property template.""" +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 Iterable +from typing import Optional, Union, Iterable, List, Set, Tuple -class HasPropertyTemplates(object): + +class HasPropertyTemplates(HasDependencies): """ Mixin-trait for entities that include property templates. @@ -19,12 +21,15 @@ class HasPropertyTemplates(object): """ - def __init__(self, properties): + 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): + def properties(self) -> List[Tuple[Union[PropertyTemplate, LinkByUID], + Optional[BaseBounds]]]: """ Get the list of property template/bounds tuples. @@ -37,7 +42,9 @@ def properties(self): return self._properties @properties.setter - def properties(self, properties): + def properties(self, properties: Iterable[Union[Union[PropertyTemplate, LinkByUID], + Tuple[Union[PropertyTemplate, LinkByUID], + Optional[BaseBounds]]]]): """ Set the list of parameter templates. @@ -47,11 +54,6 @@ def properties(self, properties): A list of tuples containing this entity's property templates as well as any restrictions on those templates' bounds. - Returns - ------- - List[(PropertyTemplate, bounds)] - List of this entity's property template/bounds pairs - """ if isinstance(properties, Iterable): if any(isinstance(x, BaseBounds) for x in properties): @@ -60,3 +62,7 @@ def properties(self, properties): (PropertyTemplate, LinkByUID, list, tuple), trigger=BaseTemplate._homogenize_ranges ) + + def _local_dependencies(self) -> Set[Union["BaseEntity", "LinkByUID"]]: + """Return a set of all immediate dependencies (no recursion).""" + return {attr[0] for attr in self.properties} diff --git a/gemd/entity/template/tests/test_base_attribute_template.py b/gemd/entity/template/tests/test_base_attribute_template.py index cae639d7..79ed5a73 100644 --- a/gemd/entity/template/tests/test_base_attribute_template.py +++ b/gemd/entity/template/tests/test_base_attribute_template.py @@ -6,6 +6,8 @@ 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.json import dumps, loads @@ -39,3 +41,14 @@ def test_json(): template = PropertyTemplate(name="foo", bounds=RealBounds(0, 1, "")) copy = loads(dumps(template)) assert copy == template + + +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, '')), + ] + for target in targets: + assert len(target.all_dependencies()) == 0, f"{type(target)} had dependencies" diff --git a/gemd/entity/template/tests/test_measurement_template.py b/gemd/entity/template/tests/test_measurement_template.py index 0463dd52..0b95b96f 100644 --- a/gemd/entity/template/tests/test_measurement_template.py +++ b/gemd/entity/template/tests/test_measurement_template.py @@ -70,3 +70,18 @@ def test_mixins(): assert len(second.properties) == 1 assert len(second.conditions) == 1 assert len(second.parameters) == 1 + + +def test_dependencies(): + """Test that dependency lists make sense.""" + prop = PropertyTemplate(name="name", bounds=IntegerBounds(0, 1)) + 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]) + assert prop in msr_template.all_dependencies() + assert cond in msr_template.all_dependencies() + assert param in msr_template.all_dependencies() diff --git a/gemd/entity/template/tests/test_process_template.py b/gemd/entity/template/tests/test_process_template.py index 2e409171..fd2b81d8 100644 --- a/gemd/entity/template/tests/test_process_template.py +++ b/gemd/entity/template/tests/test_process_template.py @@ -74,3 +74,11 @@ def test_passthrough_bounds(): ], }) assert len(from_dict.conditions) == 1 + + +def test_dependencies(): + """Test that dependency lists make sense.""" + 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/gemd/util/impl.py b/gemd/util/impl.py index 583da5ce..c52f8bb6 100644 --- a/gemd/util/impl.py +++ b/gemd/util/impl.py @@ -1,14 +1,14 @@ """Utility functions.""" import uuid import functools -from typing import Dict, Callable, Union, Type, Tuple, List, Any, Optional +from typing import Optional, Union, Type, Iterable, MutableSequence, List, Tuple, Mapping, \ + Callable, Any, Reversible, ByteString from warnings import warn from gemd.entity.base_entity import BaseEntity from gemd.entity.dict_serializable import DictSerializable from gemd.entity.link_by_uid import LinkByUID -from collections.abc import Reversible, Iterable, ByteString from toolz import concatv @@ -87,7 +87,7 @@ def _cached_issubclass( def _substitute(thing: Any, sub: Callable[[object], object], applies: Callable[[object], bool], - visited: Dict[object, object] = None) -> object: + visited: Mapping[object, object] = None) -> object: """ Generic recursive substitute function. @@ -116,11 +116,11 @@ def _substitute(thing: Any, else: replacement = thing - if _cached_isinstance(replacement, list): + if _cached_isinstance(replacement, MutableSequence): new = [_substitute(x, sub, applies, visited) for x in replacement] - elif _cached_isinstance(replacement, tuple): + elif _cached_isinstance(replacement, Tuple): new = tuple(_substitute(x, sub, applies, visited) for x in replacement) - elif _cached_isinstance(replacement, dict): + elif _cached_isinstance(replacement, Mapping): new = {_substitute(k, sub, applies, visited): _substitute(v, sub, applies, visited) for k, v in replacement.items()} elif _cached_isinstance(replacement, DictSerializable): @@ -139,7 +139,7 @@ def _substitute(thing: Any, def _substitute_inplace(thing: Any, sub: Callable[[object], object], applies: Callable[[object], bool], - visited: Dict[object, object] = None) -> object: + visited: Mapping[object, object] = None) -> object: """ Generic recursive in-place substitute function. @@ -177,13 +177,13 @@ def _key(obj): if orig_key is not None: visited[orig_key] = thing # Store before we start recursing - if _cached_isinstance(thing, list): # Change list in place + if _cached_isinstance(thing, MutableSequence): # Change list in place for i, x in enumerate(thing): thing[i] = _substitute_inplace(x, sub, applies, visited) - elif _cached_isinstance(thing, tuple): # Tuples are immutable; regenerate + elif _cached_isinstance(thing, Tuple): # Tuples are immutable; regenerate thing = tuple(_substitute_inplace(x, sub, applies, visited) for x in thing) visited[orig_key] = thing # We mutated it - elif _cached_isinstance(thing, dict): # Change dict in place, both keys & values + elif _cached_isinstance(thing, Mapping): # Change dict in place, both keys & values remove = set() # Store todos because can't mutate a dict in a loop update = dict() for k, v in thing.items(): @@ -242,7 +242,7 @@ def _emulator(inner_name: str) -> Callable: return setter -def make_index(obj: Union[List, Tuple, Dict, BaseEntity, DictSerializable]): +def make_index(obj: Union[Iterable, BaseEntity, DictSerializable]): """ Generates an index that can be used for the substitute_objects method. @@ -252,7 +252,7 @@ def make_index(obj: Union[List, Tuple, Dict, BaseEntity, DictSerializable]): Parameters ---------- - obj: Union[List, Tuple, Dict, BaseEntity, DictSerializable] + obj: Union[Iterable, Mapping, BaseEntity, DictSerializable] target container (dict, list, ..) from which to create an index of GEMD objects """ @@ -340,7 +340,7 @@ def substitute_objects(obj, applies=lambda o: _cached_isinstance(o, LinkByUID)) -def flatten(obj, scope=None): +def flatten(obj, scope=None) -> List[BaseEntity]: """ Flatten a BaseEntity (or array of them) into a list of objects connected by LinkByUID objects. @@ -397,7 +397,7 @@ 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[List, Tuple, Dict, BaseEntity, DictSerializable], +def recursive_foreach(obj: Union[Iterable, BaseEntity, DictSerializable], func: Callable[[BaseEntity], None], *, apply_first=False): @@ -410,7 +410,7 @@ def recursive_foreach(obj: Union[List, Tuple, Dict, BaseEntity, DictSerializable Parameters ---------- - obj: Union[List, Tuple, Dict, BaseEntity, DictSerializable] + obj: Union[Iterable, Mapping, BaseEntity, DictSerializable] target of the operation func: Callable[[BaseEntity], None] to apply to each contained BaseEntity @@ -436,7 +436,7 @@ def recursive_foreach(obj: Union[List, Tuple, Dict, BaseEntity, DictSerializable if apply_first and _cached_isinstance(this, BaseEntity): func(this) - if _cached_isinstance(this, dict): + if _cached_isinstance(this, Mapping): for x in concatv(this.keys(), this.values()): queue.append(x) elif _cached_isinstance(this, DictSerializable): @@ -453,18 +453,18 @@ def recursive_foreach(obj: Union[List, Tuple, Dict, BaseEntity, DictSerializable return -def recursive_flatmap(obj: Union[List, Tuple, Dict, BaseEntity, DictSerializable], - func: Callable[[BaseEntity], Union[List, Tuple]], +def recursive_flatmap(obj: Union[Iterable, BaseEntity, DictSerializable], + func: Callable[[BaseEntity], Iterable], *, - unidirectional=True): + unidirectional=True) -> List: """ Recursively apply and accumulate a list-valued function to BaseEntity members. Parameters ---------- - obj: Union[List, Tuple, Dict, BaseEntity, DictSerializable] + obj: Union[Iterable, Mapping, BaseEntity, DictSerializable] target of the operation - func: Callable[[BaseEntity], Union[List[Any], Tuple[Any]]] + func: Callable[[BaseEntity], Iterable] function to apply; must be list-valued unidirectional: bool only recurse through the writeable direction of bidirectional links @@ -491,7 +491,7 @@ def recursive_flatmap(obj: Union[List, Tuple, Dict, BaseEntity, DictSerializable if _cached_isinstance(this, BaseEntity): res.extend(func(this)) - if _cached_isinstance(this, dict): + if _cached_isinstance(this, Mapping): queue.extend(concatv(this.keys(), this.values())) elif _cached_isinstance(this, DictSerializable): for k, x in sorted(this.__dict__.items()): diff --git a/setup.py b/setup.py index 6cfbe478..7e21fe28 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup(name='gemd', - version='1.5.0', + version='1.6.0', url='http://github.com/CitrineInformatics/gemd-python', description="Python binding for Citrine's GEMD data model", author='Citrine Informatics',