def test_native_id_substitution(): """Test that the native id gets serialized, when specified.""" native_id = 'id1' # Create measurement and material with two ids mat = MaterialRun("A material", uids={ native_id: str(uuid4()), "an_id": str(uuid4()), "another_id": str(uuid4()) }) meas = MeasurementRun("A measurement", material=mat, uids={ "some_id": str(uuid4()), native_id: str(uuid4()), "an_id": str(uuid4()) }) # Turn the material pointer into a LinkByUID using native_id subbed = substitute_links(meas, native_uid=native_id) assert subbed.material == LinkByUID.from_entity(mat, name=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) assert subbed == [LinkByUID.from_entity(meas, name=native_id)]
def test_serialized_history(): """Test the serialization of a complete material history.""" # Create several runs and specs linked together buy_spec = LinkByUID("id", "pr723") cookie_dough_spec = MaterialSpec("cookie dough spec", process=buy_spec) buy_cookie_dough = ProcessRun("Buy cookie dough", uids={'id': '32283'}, spec=buy_spec) cookie_dough = MaterialRun("cookie dough", process=buy_cookie_dough, spec=cookie_dough_spec) bake = ProcessRun("bake cookie dough", conditions=[ Condition("oven temp", origin='measured', value=NominalReal(357, 'degF'))]) IngredientRun(material=cookie_dough, process=bake, number_fraction=NominalReal(1, '')) cookie = MaterialRun("cookie", process=bake, tags=["chocolate chip", "drop"]) MeasurementRun("taste", material=cookie, properties=[ Property("taste", value=DiscreteCategorical("scrumptious"))]) cookie_history = complete_material_history(cookie) # There are 7 entities in the serialized list: cookie dough (spec & run), buy cookie dough, # cookie dough ingredient, bake cookie dough, cookie, taste assert len(cookie_history) == 7 for entity in cookie_history: assert len(entity['uids']) > 0, "Serializing material history should assign uids." # Check that the measurement points to the material taste_dict = next(x for x in cookie_history if x.get('type') == 'measurement_run') cookie_dict = next(x for x in cookie_history if x.get('name') == 'cookie') scope = taste_dict.get('material').get('scope') assert taste_dict.get('material').get('id') == cookie_dict.get('uids').get(scope) # Check that both the material spec and the process run point to the same process spec. # Because that spec was initially a LinkByUID, this also tests the methods ability to # serialize a LinkByUID. cookie_dough_spec_dict = next(x for x in cookie_history if x.get('type') == 'material_spec') buy_cookie_dough_dict = next(x for x in cookie_history if x.get('name') == 'Buy cookie dough') assert cookie_dough_spec_dict.get('process') == buy_spec.as_dict() assert buy_cookie_dough_dict.get('spec') == buy_spec.as_dict()
def test_object_key_substitution(): """Test that client can copy a dictionary in which keys are BaseEntity objects.""" spec = ProcessSpec("A process spec", uids={ 'id': str(uuid4()), 'auto': str(uuid4()) }) run1 = ProcessRun("A process run", spec=spec, uids={ 'id': str(uuid4()), 'auto': str(uuid4()) }) run2 = ProcessRun("Another process run", spec=spec, uids={'id': str(uuid4())}) process_dict = {spec: [run1, run2]} subbed = substitute_links(process_dict, native_uid='auto') for key, value in subbed.items(): assert key == LinkByUID.from_entity(spec, name='auto') assert LinkByUID.from_entity(run1, name='auto') in value assert LinkByUID.from_entity(run2) in value reverse_process_dict = {run2: spec} subbed = substitute_links(reverse_process_dict, native_uid='auto') for key, value in subbed.items(): assert key == LinkByUID.from_entity(run2) assert value == LinkByUID.from_entity(spec, name='auto')
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)
def test_invalid_assignment(): """Test that invalid assignment throws a TypeError.""" with pytest.raises(TypeError): PropertyAndConditions(property=LinkByUID('id', 'a15')) with pytest.raises(TypeError): PropertyAndConditions( property=Property("property"), conditions=[Condition("condition"), LinkByUID('scope', 'id')])
def test_dictionary_substitution(): """substitute_objects() should substitute LinkByUIDs that occur in dict keys and values.""" proc = ProcessRun("A process", uids={'id': '123'}) mat = MaterialRun("A material", uids={'generic id': '38f8jf'}) proc_link = LinkByUID.from_entity(proc) mat_link = LinkByUID.from_entity(mat) index = {(mat_link.scope.lower(), mat_link.id): mat, (proc_link.scope.lower(), proc_link.id): proc} test_dict = {LinkByUID.from_entity(proc): LinkByUID.from_entity(mat)} substitute_objects(test_dict, index) assert test_dict[proc] == mat
def test_object_template_serde(): """Test serde of an object template.""" length_template = PropertyTemplate("Length", RealBounds(2.0, 3.5, 'cm')) sub_bounds = RealBounds(2.5, 3.0, 'cm') color_template = PropertyTemplate("Color", CategoricalBounds(["red", "green", "blue"])) # Properties are a mixture of property templates and [template, bounds], pairs block_template = MaterialTemplate("Block", properties=[[length_template, sub_bounds], color_template]) copy_template = MaterialTemplate.build(block_template.dump()) assert copy_template == block_template # Tests below exercise similar code, but for measurement and process templates pressure_template = ConditionTemplate("pressure", RealBounds(0.1, 0.11, 'MPa')) index_template = ParameterTemplate("index", IntegerBounds(2, 10)) meas_template = MeasurementTemplate("A measurement of length", properties=[length_template], conditions=[pressure_template], description="Description", parameters=[index_template], tags=["foo"]) assert MeasurementTemplate.build(meas_template.dump()) == meas_template proc_template = ProcessTemplate("Make an object", parameters=[index_template], conditions=[pressure_template], allowed_labels=["Label"], allowed_names=["first sample", "second sample"]) assert ProcessTemplate.build(proc_template.dump()) == proc_template # Check that serde still works if the template is a LinkByUID proc_template.conditions[0][0] = LinkByUID('id', pressure_template.uids['id']) assert ProcessTemplate.build(proc_template.dump()) == proc_template
def test_process_id_link(): """Test that a process run can house a LinkByUID object, and that it survives serde.""" uid = str(uuid4()) proc_link = LinkByUID(scope='id', id=uid) mat_run = MaterialRun("Another cake", process=proc_link) copy_material = loads(dumps(mat_run)) assert dumps(copy_material) == dumps(mat_run)
def _load_and_index(self, d, object_index, substitute=False): """ Load the class based on the type string and index it, if a BaseEntity. This function is used as the object hook when deserializing taurus objects :param d: dictionary to try to load into a registered class instance :param object_index: to add the object to if it is a BaseEntity :param substitute: whether to substitute LinkByUIDs when they are found in the index :return: the deserialized object, or the input dict if it wasn't recognized """ if "type" not in d: return d typ = d.pop("type") if typ in self._clazz_index: clz = self._clazz_index[typ] obj = clz.from_dict(d) elif typ == LinkByUID.typ: obj = LinkByUID.from_dict(d) if substitute and (obj.scope.lower(), obj.id) in object_index: return object_index[(obj.scope.lower(), obj.id)] return obj else: raise TypeError("Unexpected base object type: {}".format(typ)) if isinstance(obj, BaseEntity): for (scope, uid) in obj.uids.items(): object_index[(scope.lower(), uid)] = obj return obj
def test_tuple_sub(): """substitute_objects() should correctly substitute tuple values.""" proc = ProcessRun('foo', uids={'id': '123'}) proc_link = LinkByUID.from_entity(proc) index = {(proc_link.scope, proc_link.id): proc} tup = (proc_link,) subbed = substitute_objects(tup, index) assert subbed[0] == proc
def test_template_access(): """A process run's template should be equal to its spec's template.""" template = ProcessTemplate("process template", uids={'id': str(uuid4())}) spec = ProcessSpec("A spec", uids={'id': str(uuid4())}, template=template) proc = ProcessRun("A run", uids={'id': str(uuid4())}, spec=spec) assert proc.template == template proc.spec = LinkByUID.from_entity(spec) assert proc.template is None
def test_template_access(): """A material run's template should be equal to its spec's template.""" template = MaterialTemplate("material template", uids={'id': str(uuid4())}) spec = MaterialSpec("A spec", uids={'id': str(uuid4())}, template=template) mat = MaterialRun("A run", uids=['id', str(uuid4())], spec=spec) assert mat.template == template mat.spec = LinkByUID.from_entity(spec) assert mat.template is None
def test_link_by_uid(): """Test that linking works.""" root = MaterialRun(name='root', process=ProcessRun(name='root proc')) leaf = MaterialRun(name='leaf', process=ProcessRun(name='leaf proc')) IngredientRun(process=root.process, material=leaf) IngredientRun(process=root.process, material=LinkByUID.from_entity(leaf)) copy = loads(dumps(root)) assert copy.process.ingredients[0].material == copy.process.ingredients[ 1].material
def test_template_access(): """A measurement run's template should be equal to its spec's template.""" template = MeasurementTemplate("measurement template", uids={'id': str(uuid4())}) spec = MeasurementSpec("A spec", uids={'id': str(uuid4())}, template=template) meas = MeasurementRun("A run", uids={'id': str(uuid4())}, spec=spec) assert meas.template == template meas.spec = LinkByUID.from_entity(spec) assert meas.template is None
def _deserialize(self, value: dict): if 'type' in value and value['type'] == LinkByUID.typ: if 'scope' in value and 'id' in value: value.pop('type') return LinkByUID(**value) else: raise ValueError( "LinkByUID dictionary must have both scope and id fields") raise Exception( "Serializable object that is being pointed to must have a self-contained " "build() method that does not call deserialize().")
def test_thin_dumps(): """Test that thin_dumps turns pointers into links and doesn't work on non-BaseEntity.""" mat = MaterialRun("The actual material") meas_spec = MeasurementSpec("measurement", uids={'my_scope': '324324'}) meas = MeasurementRun("The measurement", spec=meas_spec, material=mat) thin_copy = MeasurementRun.build(json.loads(thin_dumps(meas))) assert isinstance(thin_copy, MeasurementRun) assert isinstance(thin_copy.material, LinkByUID) assert isinstance(thin_copy.spec, LinkByUID) assert thin_copy.spec.id == meas_spec.uids['my_scope'] with pytest.raises(TypeError): thin_dumps(LinkByUID('scope', 'id'))
def _loado(d, index): if "type" not in d: return d typ = d.pop("type") if typ in _clazz_index: clz = _clazz_index[typ] obj = clz.from_dict(d) elif typ == LinkByUID.typ: obj = LinkByUID.from_dict(d) return obj else: raise TypeError("Unexpected base object type: {}".format(typ)) if isinstance(obj, BaseEntity): for (scope, id) in obj.uids.items(): index[(scope.lower(), id)] = obj return obj
def test_thin_dumps(): """Test that thin_dumps turns pointers into links.""" mat = MaterialRun("The actual material") meas_spec = MeasurementSpec("measurement", uids={'my_scope': '324324'}) meas = MeasurementRun("The measurement", spec=meas_spec, material=mat) thin_copy = MeasurementRun.build(json.loads(TaurusJson().thin_dumps(meas))) assert isinstance(thin_copy, MeasurementRun) assert isinstance(thin_copy.material, LinkByUID) assert isinstance(thin_copy.spec, LinkByUID) assert thin_copy.spec.id == meas_spec.uids['my_scope'] # Check that LinkByUID objects are correctly converted their JSON equivalent expected_json = '{"id": "my_id", "scope": "scope", "type": "link_by_uid"}' assert TaurusJson().thin_dumps(LinkByUID('scope', 'my_id')) == expected_json # Check that objects lacking .uid attributes will raise an exception when dumped with pytest.raises(TypeError): TaurusJson().thin_dumps({{'key': 'value'}})
def _recursive_substitute(obj, native_uid=None, seen=None): if seen is None: seen = set({}) if obj.__hash__ is not None: if obj in seen: return else: seen.add(obj) if isinstance(obj, (list, tuple)): for i, x in enumerate(obj): if isinstance(x, BaseEntity): if len(x.uids) == 0: raise ValueError("No UID for {}".format(x)) elif native_uid and native_uid in x.uids: obj[i] = LinkByUID(native_uid, x.uids[native_uid]) else: uid_to_use = next(iter(x.uids.items())) obj[i] = LinkByUID(uid_to_use[0], uid_to_use[1]) else: _recursive_substitute(x, native_uid, seen) elif isinstance(obj, dict): for k, x in obj.items(): if isinstance(x, BaseEntity): if len(x.uids) == 0: raise ValueError("No UID for {}".format(x)) elif native_uid and native_uid in x.uids: obj[k] = LinkByUID(native_uid, x.uids[native_uid]) else: uid_to_use = next(iter(x.uids.items())) obj[k] = LinkByUID(uid_to_use[0], uid_to_use[1]) else: _recursive_substitute(x, native_uid, seen) for k, x in obj.items(): if isinstance(k, BaseEntity): if len(k.uids) == 0: raise ValueError("No UID for {}".format(k)) elif native_uid and native_uid in k.uids: obj[LinkByUID(native_uid, k.uids[native_uid])] = x del obj[k] else: uid_to_use = next(iter(k.uids.items())) obj[LinkByUID(uid_to_use[0], uid_to_use[1])] = x del obj[k] else: _recursive_substitute(k, native_uid, seen) elif isinstance(obj, DictSerializable): for k, x in obj.__dict__.items(): if isinstance(obj, BaseEntity) and k in obj.skip: continue if isinstance(x, BaseEntity): if len(x.uids) == 0: raise ValueError("No UID for {}".format(x)) elif native_uid and native_uid in x.uids: obj.__dict__[k] = LinkByUID(native_uid, x.uids[native_uid]) else: uid_to_use = next(iter(x.uids.items())) obj.__dict__[k] = LinkByUID(uid_to_use[0], uid_to_use[1]) else: _recursive_substitute(x, native_uid, seen) return
def test_material_id_link(): """Check that a measurement can be linked to a material that is a LinkByUID.""" mat = LinkByUID('id', str(uuid4())) meas = MeasurementRun(material=mat) assert meas.material == mat assert loads(dumps(meas)) == meas