Skip to content

Commit

Permalink
Merge pull request #182 from CitrineInformatics/maintain/update-objec…
Browse files Browse the repository at this point in the history
…t-model

Add central registry for classes; add metaclass for DictSerializable
  • Loading branch information
kroenlein authored Apr 12, 2023
2 parents 363500b + 8538fd4 commit ff25e73
Show file tree
Hide file tree
Showing 46 changed files with 223 additions and 226 deletions.
6 changes: 3 additions & 3 deletions gemd/builders/tests/test_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from typing import Union


class UnsupportedBounds(BaseBounds):
class UnsupportedBounds(BaseBounds, typ="unsupported_bounds"):
"""Dummy object to test Bounds type checking."""

def contains(self, bounds): # pragma: no cover
Expand All @@ -32,15 +32,15 @@ def update(self, *others: Union["BaseBounds", "BaseValue"]): # pragma: no cover
pass


class UnsupportedAttribute(BaseAttribute):
class UnsupportedAttribute(BaseAttribute, typ="unsupported_attribute"):
"""Dummy object to test Attribute type checking."""

def _template_type(self): # pragma: no cover
"""Only here to satisfy abstract method."""
return str


class UnsupportedAttributeTemplate(AttributeTemplate):
class UnsupportedAttributeTemplate(AttributeTemplate, typ="unsupported_template"):
"""Dummy object to test Attribute type checking."""


Expand Down
4 changes: 1 addition & 3 deletions gemd/entity/attribute/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import Type


class Condition(BaseAttribute):
class Condition(BaseAttribute, typ="condition"):
"""
Condition of a property, process, or measurement.
Expand All @@ -31,8 +31,6 @@ class Condition(BaseAttribute):
"""

typ = "condition"

@staticmethod
def _template_type() -> Type:
return ConditionTemplate
4 changes: 1 addition & 3 deletions gemd/entity/attribute/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import Type


class Parameter(BaseAttribute):
class Parameter(BaseAttribute, typ="parameter"):
"""
Parameter of a process or measurement.
Expand Down Expand Up @@ -32,8 +32,6 @@ class Parameter(BaseAttribute):
"""

typ = "parameter"

@staticmethod
def _template_type() -> Type:
return ParameterTemplate
4 changes: 1 addition & 3 deletions gemd/entity/attribute/property.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import Type


class Property(BaseAttribute):
class Property(BaseAttribute, typ="property"):
"""
Property of a material, measured in a MeasurementRun or specified in a MaterialSpec.
Expand All @@ -31,8 +31,6 @@ class Property(BaseAttribute):
"""

typ = "property"

@staticmethod
def _template_type() -> Type:
return PropertyTemplate
4 changes: 1 addition & 3 deletions gemd/entity/attribute/property_and_conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from typing import Optional, Union, Iterable, List


class PropertyAndConditions(DictSerializable):
class PropertyAndConditions(DictSerializable, typ="property_and_conditions"):
"""
A property and the conditions under which that property was determined.
Expand All @@ -24,8 +24,6 @@ class PropertyAndConditions(DictSerializable):
"""

typ = "property_and_conditions"

def __init__(self,
property: Property = None,
conditions: Union[Iterable[Condition], Condition] = None):
Expand Down
2 changes: 0 additions & 2 deletions gemd/entity/base_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ class BaseEntity(DictSerializable):
"""

typ = "base"

def __init__(self, uids: Mapping[str, str], tags: Iterable[str]):
self._tags = None
self.tags = tags
Expand Down
4 changes: 1 addition & 3 deletions gemd/entity/bounds/categorical_bounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing import Union, Set, Optional, Iterable


class CategoricalBounds(BaseBounds):
class CategoricalBounds(BaseBounds, typ="categorical_bounds"):
"""
Categorical bounds, parameterized by a set of string-valued category labels.
Expand All @@ -16,8 +16,6 @@ class CategoricalBounds(BaseBounds):
"""

typ = "categorical_bounds"

def __init__(self, categories: Optional[Iterable[str]] = None):
self._categories = None
self.categories = categories
Expand Down
4 changes: 1 addition & 3 deletions gemd/entity/bounds/composition_bounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing import Union


class CompositionBounds(BaseBounds):
class CompositionBounds(BaseBounds, typ="composition_bounds"):
"""
Composition bounds, parameterized by a set of string-valued category labels.
Expand All @@ -16,8 +16,6 @@ class CompositionBounds(BaseBounds):
"""

typ = "composition_bounds"

def __init__(self, components=None):
self._components = None
self.components = components
Expand Down
4 changes: 1 addition & 3 deletions gemd/entity/bounds/integer_bounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import Union


class IntegerBounds(BaseBounds):
class IntegerBounds(BaseBounds, typ="integer_bounds"):
"""
Bounded subset of the integers, parameterized by a lower and upper bound.
Expand All @@ -17,8 +17,6 @@ class IntegerBounds(BaseBounds):
"""

typ = "integer_bounds"

def __init__(self, lower_bound=None, upper_bound=None):
self.lower_bound = lower_bound
self.upper_bound = upper_bound
Expand Down
7 changes: 1 addition & 6 deletions gemd/entity/bounds/molecular_structure_bounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,9 @@
from typing import Union


class MolecularStructureBounds(BaseBounds):
class MolecularStructureBounds(BaseBounds, typ="molecular_structure_bounds"):
"""Molecular bounds, with no component or substructural restrictions (yet)."""

typ = "molecular_structure_bounds"

def __init__(self):
pass

def contains(self, bounds: Union[BaseBounds, "BaseValue"]) -> bool:
"""
Check if another bounds or value object is contained by this bounds.
Expand Down
4 changes: 1 addition & 3 deletions gemd/entity/bounds/real_bounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing import Union


class RealBounds(BaseBounds):
class RealBounds(BaseBounds, typ="real_bounds"):
"""
Bounded subset of the real numbers, parameterized by a lower and upper bound.
Expand All @@ -21,8 +21,6 @@ class RealBounds(BaseBounds):
"""

typ = "real_bounds"

def __init__(self, lower_bound=None, upper_bound=None, default_units=None):
self.lower_bound = lower_bound
self.upper_bound = upper_bound
Expand Down
56 changes: 47 additions & 9 deletions gemd/entity/dict_serializable.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,61 @@
from abc import ABC
from abc import ABC, ABCMeta
from logging import getLogger

import json
import inspect
import functools
from typing import Union, Iterable, List, Mapping, Dict, Any
from typing import TypeVar, Union, Iterable, List, Mapping, Dict, Set, Any

# There are some weird (probably resolvable) errors during object cloning if this is an
# instance variable of DictSerializable.
logger = getLogger(__name__)

DictSerializableType = TypeVar("DictSerializableType", bound="DictSerializable")

class DictSerializable(ABC):
"""A base class for objects that can be represented as a dictionary and serialized."""

typ = NotImplemented
skip = set()
class DictSerializableMeta(ABCMeta):
"""Metaclass for tracking DictSerializable type string to class mappings."""

_class: Dict[str, type] = {}

def __new__(mcs, name, bases, *args,
typ: str = None, skip: Set[str] = frozenset(),
**kwargs): # noqa: D102
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.")
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

@property
def class_mapping(cls) -> Dict[str, type]:
"""
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.
Returns
-------
Dict[str, type]
The mapping from typ string to class
"""
return cls._class.copy()


class DictSerializable(ABC, metaclass=DictSerializableMeta):
"""A base class for objects that can be represented as a dictionary and serialized."""

@classmethod
def from_dict(cls, d: Mapping[str, Any]) -> "DictSerializable":
def from_dict(cls, d: Mapping[str, Any]) -> DictSerializableType:
"""
Reconstitute the object from a dictionary.
Expand Down Expand Up @@ -111,13 +148,14 @@ def build(d: Mapping[str, Any]) -> "DictSerializable":
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}
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["DictSerializable"], "DictSerializable"]) -> str:
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.
Expand Down
4 changes: 1 addition & 3 deletions gemd/entity/file_link.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from gemd.entity.dict_serializable import DictSerializable


class FileLink(DictSerializable):
class FileLink(DictSerializable, typ="file_link"):
"""
FileLink stores a name and link to an external resource.
Expand All @@ -21,8 +21,6 @@ class FileLink(DictSerializable):
"""

typ = "file_link"

def __init__(self, filename, url):
DictSerializable.__init__(self)
self.filename = filename
Expand Down
4 changes: 1 addition & 3 deletions gemd/entity/link_by_uid.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from gemd.entity.dict_serializable import DictSerializable


class LinkByUID(DictSerializable):
class LinkByUID(DictSerializable, typ="link_by_uid"):
"""
Link object, which replaces pointers to other entities before serialization and writing.
Expand All @@ -18,8 +18,6 @@ class LinkByUID(DictSerializable):
"""

typ = "link_by_uid"

def __init__(self, scope, id):
# TODO: parse to make sure it's valid
self.scope = scope
Expand Down
6 changes: 3 additions & 3 deletions gemd/entity/object/ingredient_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
from typing import Optional, Union, Iterable, List, Mapping, Type, Any


class IngredientRun(BaseObject, HasQuantities, HasSpec, HasMaterial, HasProcess):
class IngredientRun(BaseObject,
HasQuantities, HasSpec, HasMaterial, HasProcess,
typ="ingredient_run"):
"""
An ingredient run.
Expand Down Expand Up @@ -56,8 +58,6 @@ class IngredientRun(BaseObject, HasQuantities, HasSpec, HasMaterial, HasProcess)
"""

typ = "ingredient_run"

def __init__(self,
*,
material: Union[MaterialRun, LinkByUID] = None,
Expand Down
6 changes: 3 additions & 3 deletions gemd/entity/object/ingredient_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
from typing import Optional, Union, Iterable, List, Mapping, Type


class IngredientSpec(BaseObject, HasQuantities, HasTemplate, HasMaterial, HasProcess):
class IngredientSpec(BaseObject,
HasQuantities, HasTemplate, HasMaterial, HasProcess,
typ="ingredient_spec"):
"""
An ingredient specification.
Expand Down Expand Up @@ -56,8 +58,6 @@ class IngredientSpec(BaseObject, HasQuantities, HasTemplate, HasMaterial, HasPro
"""

typ = "ingredient_spec"

def __init__(self,
name: str,
*,
Expand Down
6 changes: 1 addition & 5 deletions gemd/entity/object/material_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from typing import Optional, Union, Iterable, List, Mapping, Type, Any


class MaterialRun(BaseObject, HasSpec, HasProcess):
class MaterialRun(BaseObject, HasSpec, HasProcess, typ="material_run", skip={"_measurements"}):
"""
A material run.
Expand Down Expand Up @@ -50,10 +50,6 @@ class MaterialRun(BaseObject, HasSpec, HasProcess):
"""

typ = "material_run"

skip = {"_measurements"}

def __init__(self,
name: str,
*,
Expand Down
Loading

0 comments on commit ff25e73

Please sign in to comment.