Skip to content

Commit ff25e73

Browse files
authored
Merge pull request #182 from CitrineInformatics/maintain/update-object-model
Add central registry for classes; add metaclass for DictSerializable
2 parents 363500b + 8538fd4 commit ff25e73

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+223
-226
lines changed

gemd/builders/tests/test_builders.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from typing import Union
1717

1818

19-
class UnsupportedBounds(BaseBounds):
19+
class UnsupportedBounds(BaseBounds, typ="unsupported_bounds"):
2020
"""Dummy object to test Bounds type checking."""
2121

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

3434

35-
class UnsupportedAttribute(BaseAttribute):
35+
class UnsupportedAttribute(BaseAttribute, typ="unsupported_attribute"):
3636
"""Dummy object to test Attribute type checking."""
3737

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

4242

43-
class UnsupportedAttributeTemplate(AttributeTemplate):
43+
class UnsupportedAttributeTemplate(AttributeTemplate, typ="unsupported_template"):
4444
"""Dummy object to test Attribute type checking."""
4545

4646

gemd/entity/attribute/condition.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import Type
55

66

7-
class Condition(BaseAttribute):
7+
class Condition(BaseAttribute, typ="condition"):
88
"""
99
Condition of a property, process, or measurement.
1010
@@ -31,8 +31,6 @@ class Condition(BaseAttribute):
3131
3232
"""
3333

34-
typ = "condition"
35-
3634
@staticmethod
3735
def _template_type() -> Type:
3836
return ConditionTemplate

gemd/entity/attribute/parameter.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import Type
55

66

7-
class Parameter(BaseAttribute):
7+
class Parameter(BaseAttribute, typ="parameter"):
88
"""
99
Parameter of a process or measurement.
1010
@@ -32,8 +32,6 @@ class Parameter(BaseAttribute):
3232
3333
"""
3434

35-
typ = "parameter"
36-
3735
@staticmethod
3836
def _template_type() -> Type:
3937
return ParameterTemplate

gemd/entity/attribute/property.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import Type
55

66

7-
class Property(BaseAttribute):
7+
class Property(BaseAttribute, typ="property"):
88
"""
99
Property of a material, measured in a MeasurementRun or specified in a MaterialSpec.
1010
@@ -31,8 +31,6 @@ class Property(BaseAttribute):
3131
3232
"""
3333

34-
typ = "property"
35-
3634
@staticmethod
3735
def _template_type() -> Type:
3836
return PropertyTemplate

gemd/entity/attribute/property_and_conditions.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from typing import Optional, Union, Iterable, List
1010

1111

12-
class PropertyAndConditions(DictSerializable):
12+
class PropertyAndConditions(DictSerializable, typ="property_and_conditions"):
1313
"""
1414
A property and the conditions under which that property was determined.
1515
@@ -24,8 +24,6 @@ class PropertyAndConditions(DictSerializable):
2424
2525
"""
2626

27-
typ = "property_and_conditions"
28-
2927
def __init__(self,
3028
property: Property = None,
3129
conditions: Union[Iterable[Condition], Condition] = None):

gemd/entity/base_entity.py

-2
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ class BaseEntity(DictSerializable):
2424
2525
"""
2626

27-
typ = "base"
28-
2927
def __init__(self, uids: Mapping[str, str], tags: Iterable[str]):
3028
self._tags = None
3129
self.tags = tags

gemd/entity/bounds/categorical_bounds.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import Union, Set, Optional, Iterable
66

77

8-
class CategoricalBounds(BaseBounds):
8+
class CategoricalBounds(BaseBounds, typ="categorical_bounds"):
99
"""
1010
Categorical bounds, parameterized by a set of string-valued category labels.
1111
@@ -16,8 +16,6 @@ class CategoricalBounds(BaseBounds):
1616
1717
"""
1818

19-
typ = "categorical_bounds"
20-
2119
def __init__(self, categories: Optional[Iterable[str]] = None):
2220
self._categories = None
2321
self.categories = categories

gemd/entity/bounds/composition_bounds.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import Union
66

77

8-
class CompositionBounds(BaseBounds):
8+
class CompositionBounds(BaseBounds, typ="composition_bounds"):
99
"""
1010
Composition bounds, parameterized by a set of string-valued category labels.
1111
@@ -16,8 +16,6 @@ class CompositionBounds(BaseBounds):
1616
1717
"""
1818

19-
typ = "composition_bounds"
20-
2119
def __init__(self, components=None):
2220
self._components = None
2321
self.components = components

gemd/entity/bounds/integer_bounds.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import Union
55

66

7-
class IntegerBounds(BaseBounds):
7+
class IntegerBounds(BaseBounds, typ="integer_bounds"):
88
"""
99
Bounded subset of the integers, parameterized by a lower and upper bound.
1010
@@ -17,8 +17,6 @@ class IntegerBounds(BaseBounds):
1717
1818
"""
1919

20-
typ = "integer_bounds"
21-
2220
def __init__(self, lower_bound=None, upper_bound=None):
2321
self.lower_bound = lower_bound
2422
self.upper_bound = upper_bound

gemd/entity/bounds/molecular_structure_bounds.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,9 @@
88
from typing import Union
99

1010

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

14-
typ = "molecular_structure_bounds"
15-
16-
def __init__(self):
17-
pass
18-
1914
def contains(self, bounds: Union[BaseBounds, "BaseValue"]) -> bool:
2015
"""
2116
Check if another bounds or value object is contained by this bounds.

gemd/entity/bounds/real_bounds.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import Union
66

77

8-
class RealBounds(BaseBounds):
8+
class RealBounds(BaseBounds, typ="real_bounds"):
99
"""
1010
Bounded subset of the real numbers, parameterized by a lower and upper bound.
1111
@@ -21,8 +21,6 @@ class RealBounds(BaseBounds):
2121
2222
"""
2323

24-
typ = "real_bounds"
25-
2624
def __init__(self, lower_bound=None, upper_bound=None, default_units=None):
2725
self.lower_bound = lower_bound
2826
self.upper_bound = upper_bound

gemd/entity/dict_serializable.py

+47-9
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,61 @@
1-
from abc import ABC
1+
from abc import ABC, ABCMeta
22
from logging import getLogger
33

44
import json
55
import inspect
66
import functools
7-
from typing import Union, Iterable, List, Mapping, Dict, Any
7+
from typing import TypeVar, Union, Iterable, List, Mapping, Dict, Set, Any
88

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

13+
DictSerializableType = TypeVar("DictSerializableType", bound="DictSerializable")
1314

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

17-
typ = NotImplemented
18-
skip = set()
16+
class DictSerializableMeta(ABCMeta):
17+
"""Metaclass for tracking DictSerializable type string to class mappings."""
18+
19+
_class: Dict[str, type] = {}
20+
21+
def __new__(mcs, name, bases, *args,
22+
typ: str = None, skip: Set[str] = frozenset(),
23+
**kwargs): # noqa: D102
24+
return super().__new__(mcs, name, bases, *args, **kwargs)
25+
26+
def __init__(cls, name, bases, *args, typ: str = None, skip: Set[str] = frozenset(), **kwargs):
27+
super().__init__(name, bases, *args, **kwargs)
28+
if typ is not None:
29+
if typ in cls._class and not issubclass(cls, cls._class.get(typ)):
30+
raise ValueError(f"{cls} attempted to take typ {typ} from {cls._class.get(typ)}, "
31+
f"which is not its ancestor.")
32+
cls.typ = typ
33+
cls._class[typ] = cls
34+
elif not hasattr(cls, "typ"):
35+
cls.typ = NotImplementedError
36+
cls.skip = {x for b in bases for x in getattr(b, 'skip', {})} | skip
37+
38+
@property
39+
def class_mapping(cls) -> Dict[str, type]:
40+
"""
41+
Return class typ string -> class map for DictSerializable and its descendants.
42+
43+
Note that is actually returns a copy of the internal dict to avoid accidental breakage.
44+
45+
Returns
46+
-------
47+
Dict[str, type]
48+
The mapping from typ string to class
49+
50+
"""
51+
return cls._class.copy()
52+
53+
54+
class DictSerializable(ABC, metaclass=DictSerializableMeta):
55+
"""A base class for objects that can be represented as a dictionary and serialized."""
1956

2057
@classmethod
21-
def from_dict(cls, d: Mapping[str, Any]) -> "DictSerializable":
58+
def from_dict(cls, d: Mapping[str, Any]) -> DictSerializableType:
2259
"""
2360
Reconstitute the object from a dictionary.
2461
@@ -111,13 +148,14 @@ def build(d: Mapping[str, Any]) -> "DictSerializable":
111148
def __repr__(self) -> str:
112149
object_dict = self.as_dict()
113150
# as_dict() skips over keys in `skip`, but they should be in the representation.
114-
skipped_keys = {x.lstrip('_') for x in vars(self) if x in self.skip}
151+
skipped_keys = {x.lstrip('_') for x in self.skip}
115152
for key in skipped_keys:
116153
skipped_field = getattr(self, key, None)
117154
object_dict[key] = self._name_repr(skipped_field)
118155
return str(object_dict)
119156

120-
def _name_repr(self, entity: Union[Iterable["DictSerializable"], "DictSerializable"]) -> str:
157+
def _name_repr(self,
158+
entity: Union[Iterable[DictSerializableType], DictSerializableType]) -> str:
121159
"""
122160
A representation of an object or a list of objects that uses the name and type.
123161

gemd/entity/file_link.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from gemd.entity.dict_serializable import DictSerializable
33

44

5-
class FileLink(DictSerializable):
5+
class FileLink(DictSerializable, typ="file_link"):
66
"""
77
FileLink stores a name and link to an external resource.
88
@@ -21,8 +21,6 @@ class FileLink(DictSerializable):
2121
2222
"""
2323

24-
typ = "file_link"
25-
2624
def __init__(self, filename, url):
2725
DictSerializable.__init__(self)
2826
self.filename = filename

gemd/entity/link_by_uid.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from gemd.entity.dict_serializable import DictSerializable
66

77

8-
class LinkByUID(DictSerializable):
8+
class LinkByUID(DictSerializable, typ="link_by_uid"):
99
"""
1010
Link object, which replaces pointers to other entities before serialization and writing.
1111
@@ -18,8 +18,6 @@ class LinkByUID(DictSerializable):
1818
1919
"""
2020

21-
typ = "link_by_uid"
22-
2321
def __init__(self, scope, id):
2422
# TODO: parse to make sure it's valid
2523
self.scope = scope

gemd/entity/object/ingredient_run.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
from typing import Optional, Union, Iterable, List, Mapping, Type, Any
1616

1717

18-
class IngredientRun(BaseObject, HasQuantities, HasSpec, HasMaterial, HasProcess):
18+
class IngredientRun(BaseObject,
19+
HasQuantities, HasSpec, HasMaterial, HasProcess,
20+
typ="ingredient_run"):
1921
"""
2022
An ingredient run.
2123
@@ -56,8 +58,6 @@ class IngredientRun(BaseObject, HasQuantities, HasSpec, HasMaterial, HasProcess)
5658
5759
"""
5860

59-
typ = "ingredient_run"
60-
6161
def __init__(self,
6262
*,
6363
material: Union[MaterialRun, LinkByUID] = None,

gemd/entity/object/ingredient_spec.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
from typing import Optional, Union, Iterable, List, Mapping, Type
1414

1515

16-
class IngredientSpec(BaseObject, HasQuantities, HasTemplate, HasMaterial, HasProcess):
16+
class IngredientSpec(BaseObject,
17+
HasQuantities, HasTemplate, HasMaterial, HasProcess,
18+
typ="ingredient_spec"):
1719
"""
1820
An ingredient specification.
1921
@@ -56,8 +58,6 @@ class IngredientSpec(BaseObject, HasQuantities, HasTemplate, HasMaterial, HasPro
5658
5759
"""
5860

59-
typ = "ingredient_spec"
60-
6161
def __init__(self,
6262
name: str,
6363
*,

gemd/entity/object/material_run.py

+1-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from typing import Optional, Union, Iterable, List, Mapping, Type, Any
1212

1313

14-
class MaterialRun(BaseObject, HasSpec, HasProcess):
14+
class MaterialRun(BaseObject, HasSpec, HasProcess, typ="material_run", skip={"_measurements"}):
1515
"""
1616
A material run.
1717
@@ -50,10 +50,6 @@ class MaterialRun(BaseObject, HasSpec, HasProcess):
5050
5151
"""
5252

53-
typ = "material_run"
54-
55-
skip = {"_measurements"}
56-
5753
def __init__(self,
5854
name: str,
5955
*,

0 commit comments

Comments
 (0)