Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions gemd/entity/base_entity.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Base class for all entities."""
from typing import Optional

from gemd.entity.dict_serializable import DictSerializable
from gemd.entity.case_insensitive_dict import CaseInsensitiveDict

Expand Down Expand Up @@ -71,6 +73,38 @@ def add_uid(self, scope, uid):
"""
self.uids[scope] = uid

def to_link(self,
scope: Optional[str] = None,
*,
allow_fallback: bool = False) -> 'LinkByUID': # noqa: F821
"""
Generate a LinkByUID for this object.

Parameters
----------
scope: str, optional
scope of the uid to get
allow_fallback: bool
whether to grab another scope/id if chosen scope is missing (Default: False).

Returns
-------
LinkByUID

"""
from gemd.entity.link_by_uid import LinkByUID
if len(self.uids) == 0:
raise ValueError(f"{type(self)} {self.name} does not have any uids.")

if (scope is None) or (allow_fallback and scope not in self.uids):
scope = next(x for x in self.uids)

uid = self.uids.get(scope, None)
if uid is None:
raise ValueError(f"{type(self)} {self.name} has no uid with scope {scope}.")

return LinkByUID(scope=scope, id=uid)

# Note that this could violate transitivity -- Link(scope1) == obj == Link(scope2)
def __eq__(self, other):
from gemd.entity.link_by_uid import LinkByUID
Expand Down
2 changes: 2 additions & 0 deletions gemd/entity/link_by_uid.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ def __eq__(self, other):
from gemd.entity.base_entity import BaseEntity
if isinstance(other, BaseEntity):
return other.uids.get(self.scope) == self.id
elif isinstance(other, tuple): # Make them interchangeable in a dict
return len(other) == 2 and (self.scope, self.id) == other
else:
return super().__eq__(other)

Expand Down
17 changes: 17 additions & 0 deletions gemd/entity/tests/test_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import pytest

from gemd.entity.object.ingredient_run import IngredientRun
from gemd.entity.link_by_uid import LinkByUID


def test_id_case_sensitivitiy():
Expand All @@ -12,3 +13,19 @@ def test_id_case_sensitivitiy():
ingredient = IngredientRun(uids={'my_id': 'sample1'})
assert ingredient.uids['my_id'] == 'sample1'
assert ingredient.uids['MY_id'] == 'sample1'


def test_to_link():
"""Test that to_link behaves as expected."""
obj = IngredientRun(uids={"Scope": "UID", "Second": "option"})
assert isinstance(obj.to_link(), LinkByUID), "Returns a useful LinkByUID"
assert LinkByUID(scope="Scope", id="UID") == obj.to_link("Scope"), "Correct choice of UID"

with pytest.raises(ValueError):
IngredientRun().to_link(), "to_link on an object w/o IDs is fatal"

with pytest.raises(ValueError):
obj.to_link("Third"), "to_link with a scope that an object lacks is fatal"

assert obj.to_link(scope="Third", allow_fallback=True).scope in obj.uids, \
"... unless allow_fallback is set"
38 changes: 24 additions & 14 deletions gemd/util/impl.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Utility functions."""
import uuid
from typing import Dict, Callable, Union, Type, Tuple, List, Any
from typing import Dict, Callable, Union, Type, Tuple, List, Any, Optional
from warnings import warn

from gemd.entity.base_entity import BaseEntity
from gemd.entity.dict_serializable import DictSerializable
Expand Down Expand Up @@ -140,7 +141,7 @@ def make_index(obj: Union[List, Tuple, Dict, BaseEntity, DictSerializable]):

"""
def _make_index(_obj: BaseEntity):
return (((scope, _obj.uids[scope]), _obj) for scope in _obj.uids)
return ((LinkByUID(scope=scope, id=_obj.uids[scope]), _obj) for scope in _obj.uids)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this have any consequences besides the modification to _substitute?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently DE has built tooling that uses the make_index & tuple representation. With the eq changes to LinkByUID, we should have a drop-in replacement -- I've reached out to DE to verify. I've verified with the citrine-python test suite.

The alternative would be to remove the tuple/LinkByUID equality and have both types of objects serve as indices.

Specially formatted tuples are generally considered bad practice to my understanding.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified that the DE tooling that uses make_index continues to pass tests with these changes.


idx = {}
for uid, target in recursive_flatmap(obj, _make_index):
Expand All @@ -149,7 +150,12 @@ def _make_index(_obj: BaseEntity):
return idx


def substitute_links(obj: Any, native_uid=None):
def substitute_links(obj: Any,
scope: Optional[str] = None,
*,
native_uid: str = None,
allow_fallback: bool = True
):
"""
Recursively replace pointers to BaseEntity with LinkByUID objects.

Expand All @@ -160,19 +166,23 @@ def substitute_links(obj: Any, native_uid=None):
----------
obj: Any
target of the operation
native_uid: Optional[str]
scope: Optional[str], optional
preferred scope to use for creating LinkByUID objects (Default: None)
native_uid: str, optional
DEPRECATED; former name for scope argument
allow_fallback: bool, optional
whether to grab another scope/id if chosen scope is missing (Default: True).

"""
def make_link(entity: BaseEntity):
if len(entity.uids) == 0:
raise ValueError("No UID for {}".format(entity))
elif native_uid and native_uid in entity.uids:
return LinkByUID(native_uid, entity.uids[native_uid])
else:
return LinkByUID.from_entity(entity)

return _substitute(obj, sub=make_link,
if native_uid is not None:
warn("The keyword argument 'native_uid' is deprecated. When selecting a default scope, "
"use the 'scope' keyword argument.", DeprecationWarning)
if scope is not None:
raise ValueError("Both 'scope' and 'native_uid' keywords passed.")
scope = native_uid

return _substitute(obj,
sub=lambda o: o.to_link(scope=scope, allow_fallback=allow_fallback),
applies=lambda o: o is not obj and isinstance_base_entity(o))


Expand All @@ -192,7 +202,7 @@ def substitute_objects(obj, index):

"""
return _substitute(obj,
sub=lambda l: index.get((l.scope.lower(), l.id), l),
sub=lambda l: index.get(l, l),
applies=lambda o: isinstance(o, LinkByUID))


Expand Down
34 changes: 29 additions & 5 deletions gemd/util/tests/test_substitute_links.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def test_substitution_without_id():
"subbed = substitute_links should fail if objects don't have uids"


def test_native_id_substitution():
def test_scope_substitution():
"""Test that the native id gets serialized, when specified."""
native_id = 'id1'
# Create measurement and material with two ids
Expand All @@ -46,12 +46,12 @@ def test_native_id_substitution():
"some_id": str(uuid4()), native_id: str(uuid4()), "an_id": str(uuid4())})

# Turn the material pointer into a LinkByUID using native_id
subbed = substitute_links(meas, native_uid=native_id)
subbed = substitute_links(meas, scope=native_id)
assert subbed.material == LinkByUID.from_entity(mat, scope=native_id)

# Put the measurement into a list and convert that into a LinkByUID using native_id
measurements_list = [meas]
subbed = substitute_links(measurements_list, native_uid=native_id)
subbed = substitute_links(measurements_list, scope=native_id)
assert subbed == [LinkByUID.from_entity(meas, scope=native_id)]


Expand All @@ -62,14 +62,38 @@ def test_object_key_substitution():
run2 = ProcessRun("Another process run", spec=spec, uids={'id': str(uuid4())})
process_dict = {spec: [run1, run2]}

subbed = substitute_links(process_dict, native_uid='auto')
subbed = substitute_links(process_dict, scope='auto')
for key, value in subbed.items():
assert key == LinkByUID.from_entity(spec, scope='auto')
assert LinkByUID.from_entity(run1, scope='auto') in value
assert LinkByUID.from_entity(run2) in value

reverse_process_dict = {run2: spec}
subbed = substitute_links(reverse_process_dict, native_uid='auto')
subbed = substitute_links(reverse_process_dict, scope='auto')
for key, value in subbed.items():
assert key == LinkByUID.from_entity(run2)
assert value == LinkByUID.from_entity(spec, scope='auto')


def test_signature():
"""Exercise various permutations of the substitute_links sig."""
spec = ProcessSpec("A process spec", uids={'my': 'spec'})

with pytest.warns(DeprecationWarning):
run1 = ProcessRun("First process run", uids={'my': 'run1'}, spec=spec)
assert isinstance(substitute_links(run1, native_uid='my').spec, LinkByUID)

run2 = ProcessRun("Second process run", uids={'my': 'run2'}, spec=spec)
assert isinstance(substitute_links(run2, scope='my').spec, LinkByUID)

run3 = ProcessRun("Third process run", uids={'my': 'run3'}, spec=spec)
assert isinstance(substitute_links(run3, 'my').spec, LinkByUID)

with pytest.raises(ValueError): # Test deprecated auto-population
run4 = ProcessRun("Fourth process run", uids={'my': 'run4'}, spec=spec)
assert isinstance(substitute_links(run4, 'other', allow_fallback=False).spec, LinkByUID)

with pytest.warns(DeprecationWarning):
with pytest.raises(ValueError): # Test deprecated auto-population
run5 = ProcessRun("Fifth process run", uids={'my': 'run4'}, spec=spec)
assert isinstance(substitute_links(run5, scope="my", native_uid="my").spec, LinkByUID)
7 changes: 6 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@


setup(name='gemd',
version='1.2.3',
version='1.3.0',
url='http://github.com/CitrineInformatics/gemd-python',
description="Python binding for Citrine's GEMD data model",
author='Citrine Informatics',
Expand All @@ -23,6 +23,11 @@
"pint>=0.10",
"deprecation>=2.0.7,<3"
],
extras_require={
"tests": [
"pytest"
]
},
classifiers=[
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
Expand Down