Skip to content

Commit 6a0f92c

Browse files
authored
Merge pull request #39 from CitrineInformatics/release/0.2.0
Taurus v0.2.0 is released!
2 parents 96a17dd + 7c0b9a3 commit 6a0f92c

Some content is hidden

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

64 files changed

+978
-139
lines changed

.travis.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ install:
77
- pip install -U -r test_requirements.txt
88
- pip install --no-deps -e .
99
script:
10-
- pytest --flake8 --cov=taurus --cov-report term-missing --cov-report term:skip-covered
11-
--cov-fail-under=91 -s -r ./taurus
10+
- pytest --flake8 --cov=taurus --cov-report term-missing --cov-report term:skip-covered --cov-config=tox.ini
11+
--cov-fail-under=100 -s -r ./taurus
1212
- cd docs; make html; cd ..;
1313
- touch ./docs/_build/html/.nojekyll
1414
deploy:

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Python binding for Citrine's nextgen data concepts (codename: taurus).
33

44

5-
Provides a framework for storing information about the processes that create materials, the materials themselves, and measurements performance on those materials.
5+
Provides a framework for storing information about the processes that create materials, the materials themselves, and measurements performed on those materials.
66
Detailed documentation of the next gen format can be found in the language-agnostic documentation.
77

88
## Installation

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def run(self):
3636

3737

3838
setup(name='taurus-citrine',
39-
version='0.1.0',
39+
version='0.2.0',
4040
url='http://github.com/CitrineInformatics/taurus',
4141
description='Python library for the Citrine Platform',
4242
author='Max Hutchinson',

taurus/client/json_encoder.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def thin_dumps(obj, **kwargs):
152152
153153
"""
154154
if not isinstance(obj, BaseEntity):
155-
raise ValueError("Can only dump BaseEntities, but got {}".format(type(obj)))
155+
raise TypeError("Can only dump BaseEntities, but got {}".format(type(obj)))
156156

157157
set_uuids(obj)
158158
res = deepcopy(obj)
@@ -208,7 +208,7 @@ def _loado(d, index):
208208
obj = LinkByUID.from_dict(d)
209209
return obj
210210
else:
211-
raise ValueError("Unexpected base object type: {}".format(typ))
211+
raise TypeError("Unexpected base object type: {}".format(typ))
212212

213213
if isinstance(obj, BaseEntity):
214214
for (scope, id) in obj.uids.items():

taurus/client/tests/test_json.py

+68-8
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
"""Test serialization and deserialization of taurus objects."""
2-
from taurus.client.json_encoder import dumps, loads
2+
import json
3+
import pytest
4+
5+
from taurus.client.json_encoder import dumps, loads, copy, thin_dumps
6+
from taurus.entity.dict_serializable import DictSerializable
37
from taurus.entity.case_insensitive_dict import CaseInsensitiveDict
48
from taurus.entity.attribute.condition import Condition
59
from taurus.entity.attribute.parameter import Parameter
610
from taurus.entity.link_by_uid import LinkByUID
7-
from taurus.entity.object import MeasurementRun, MaterialRun, ProcessRun
11+
from taurus.entity.object import MeasurementRun, MaterialRun, ProcessRun, MeasurementSpec
812
from taurus.entity.object.ingredient_run import IngredientRun
913
from taurus.entity.object.ingredient_spec import IngredientSpec
1014
from taurus.entity.value.nominal_real import NominalReal
1115
from taurus.entity.value.normal_real import NormalReal
12-
13-
import json
16+
from taurus.enumeration.origin import Origin
1417

1518

1619
def test_serialize():
@@ -43,10 +46,33 @@ def test_deserialize():
4346
parameter = Parameter(name="A parameter", value=NormalReal(mean=17, std=1, units=''))
4447
measurement = MeasurementRun(tags="A tag on a measurement", conditions=condition,
4548
parameters=parameter)
46-
copy = loads(dumps(measurement))
47-
assert(copy.conditions[0].value == measurement.conditions[0].value)
48-
assert(copy.parameters[0].value == measurement.parameters[0].value)
49-
assert(copy.uids["auto"] == measurement.uids["auto"])
49+
copy_meas = copy(measurement)
50+
assert(copy_meas.conditions[0].value == measurement.conditions[0].value)
51+
assert(copy_meas.parameters[0].value == measurement.parameters[0].value)
52+
assert(copy_meas.uids["auto"] == measurement.uids["auto"])
53+
54+
55+
def test_enumeration_serde():
56+
"""An enumeration should get serialized as a string."""
57+
condition = Condition(name="A condition", notes=Origin.UNKNOWN)
58+
copy_condition = copy(condition)
59+
assert copy_condition.notes == Origin.get_value(condition.notes)
60+
61+
62+
def test_thin_dumps():
63+
"""Test that thin_dumps turns pointers into links and doesn't work on non-BaseEntity."""
64+
mat = MaterialRun("The actual material")
65+
meas_spec = MeasurementSpec("measurement", uids={'my_scope': '324324'})
66+
meas = MeasurementRun("The measurement", spec=meas_spec, material=mat)
67+
68+
thin_copy = MeasurementRun.build(json.loads(thin_dumps(meas)))
69+
assert isinstance(thin_copy, MeasurementRun)
70+
assert isinstance(thin_copy.material, LinkByUID)
71+
assert isinstance(thin_copy.spec, LinkByUID)
72+
assert thin_copy.spec.id == meas_spec.uids['my_scope']
73+
74+
with pytest.raises(TypeError):
75+
thin_dumps(LinkByUID('scope', 'id'))
5076

5177

5278
def test_uid_deser():
@@ -59,6 +85,40 @@ def test_uid_deser():
5985
assert ingredient_copy.material.uids['sample id'] == material.uids['Sample ID']
6086

6187

88+
def test_dict_serialization():
89+
"""Test that a dictionary can be serialized and then deserialized as a taurus object."""
90+
process = ProcessRun("A process")
91+
mat = MaterialRun("A material", process=process)
92+
meas = MeasurementRun("A measurement", material=mat)
93+
copy = loads(dumps(meas.as_dict()))
94+
assert copy == meas
95+
96+
97+
def test_unexpected_serialization():
98+
"""Trying to serialize an unexpected class should throw a TypeError."""
99+
class DummyClass:
100+
def __init__(self, foo):
101+
self.foo = foo
102+
103+
with pytest.raises(TypeError):
104+
dumps(ProcessRun("A process", notes=DummyClass("something")))
105+
106+
107+
def test_unexpected_deserialization():
108+
"""Trying to deserialize an unexpected class should throw a TypeError."""
109+
class DummyClass(DictSerializable):
110+
typ = 'dummy_class'
111+
112+
def __init__(self, foo):
113+
self.foo = foo
114+
115+
# DummyClass can be serialized because it is a DictSerializable, but cannot be
116+
# deserialized because it is not in the _clazzes list.
117+
serialized = dumps(ProcessRun("A process", notes=DummyClass("something")))
118+
with pytest.raises(TypeError):
119+
loads(serialized)
120+
121+
62122
def test_case_insensitive_rehydration():
63123
"""
64124

taurus/demo/cake.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Bake a cake."""
22
import json
33

4-
from taurus.client.json_encoder import thin_dumps, dumps
4+
from taurus.client.json_encoder import thin_dumps
55
from taurus.entity.attribute.condition import Condition
66
from taurus.entity.attribute.parameter import Parameter
77
from taurus.entity.attribute.property import Property
@@ -27,6 +27,7 @@
2727
from taurus.entity.value.normal_real import NormalReal
2828
from taurus.enumeration.origin import Origin
2929
from taurus.util.impl import set_uuids
30+
from taurus.entity.util import complete_material_history
3031

3132

3233
def make_cake():
@@ -228,6 +229,11 @@ def make_cake():
228229

229230
if __name__ == "__main__":
230231
cake = make_cake()
232+
set_uuids(cake)
233+
234+
with open("example_taurus_material_history.json", "w") as f:
235+
context_list = complete_material_history(cake)
236+
f.write(json.dumps(context_list, indent=2))
231237

232238
with open("example_taurus_material_template.json", "w") as f:
233239
f.write(thin_dumps(cake.template, indent=2))
@@ -261,7 +267,3 @@ def make_cake():
261267

262268
with open("example_taurus_measurement_run.json", "w") as f:
263269
f.write(thin_dumps(cake.measurements[0], indent=2))
264-
265-
with open("example_taurus_material_history.json", "w") as f:
266-
context = json.loads(dumps(cake))[0]
267-
f.write(json.dumps(context, indent=2))

taurus/entity/attribute/base_attribute.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ class BaseAttribute(DictSerializable):
3131
3232
"""
3333

34-
attribute_type = None
35-
3634
def __init__(self, name=None, template=None, origin="unknown", value=None, notes=None,
3735
file_links=None):
3836
if name is None:
@@ -62,7 +60,7 @@ def value(self, value):
6260
elif isinstance(value, (BaseValue, str, bool)):
6361
self._value = value
6462
else:
65-
raise ValueError("value must be a BaseValue, string or bool")
63+
raise TypeError("value must be a BaseValue, string or bool: {}".format(value))
6664

6765
@property
6866
def template(self):
@@ -76,7 +74,8 @@ def template(self, template):
7674
elif isinstance(template, (LinkByUID, AttributeTemplate)):
7775
self._template = template
7876
else:
79-
raise ValueError("template must be a BaseAttributeTemplate or LinkByUID")
77+
raise TypeError("template must be a BaseAttributeTemplate or "
78+
"LinkByUID: {}".format(template))
8079

8180
@property
8281
def origin(self):

taurus/entity/attribute/condition.py

-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from taurus.entity.attribute.base_attribute import BaseAttribute
2-
from taurus.enumeration import AttributeType
32

43

54
class Condition(BaseAttribute):
@@ -12,4 +11,3 @@ class Condition(BaseAttribute):
1211
"""
1312

1413
typ = "condition"
15-
attribute_type = AttributeType.CONDITION

taurus/entity/attribute/metadata.py

-16
This file was deleted.

taurus/entity/attribute/parameter.py

-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from taurus.entity.attribute.base_attribute import BaseAttribute
2-
from taurus.enumeration import AttributeType
32

43

54
class Parameter(BaseAttribute):
@@ -13,4 +12,3 @@ class Parameter(BaseAttribute):
1312
"""
1413

1514
typ = "parameter"
16-
attribute_type = AttributeType.PARAMETER

taurus/entity/attribute/property.py

-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from taurus.entity.attribute.base_attribute import BaseAttribute
2-
from taurus.enumeration import AttributeType
32

43

54
class Property(BaseAttribute):
@@ -12,4 +11,3 @@ class Property(BaseAttribute):
1211
"""
1312

1413
typ = "property"
15-
attribute_type = AttributeType.PROPERTY

taurus/entity/attribute/property_and_conditions.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,4 @@ def property(self, value):
6868
if isinstance(value, Property):
6969
self._property = value
7070
else:
71-
raise ValueError("property must be a Property")
71+
raise TypeError("property must be a Property")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""Tests of the BaseAttribute class."""
2+
import pytest
3+
4+
from taurus.entity.attribute.property import Property
5+
from taurus.entity.template.process_template import ProcessTemplate
6+
from taurus.entity.value.nominal_real import NominalReal
7+
8+
9+
def test_invalid_assignment():
10+
"""Test that invalid assignments throw the appropriate errors."""
11+
with pytest.raises(ValueError):
12+
Property(value=NominalReal(10, ''))
13+
with pytest.raises(TypeError):
14+
Property(name="property", value=10)
15+
with pytest.raises(TypeError):
16+
Property(name="property", template=ProcessTemplate("wrong kind of template"))
17+
with pytest.raises(ValueError):
18+
Property(name="property", origin=None)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import pytest
2+
3+
from taurus.entity.attribute.property_and_conditions import Property, Condition, \
4+
PropertyAndConditions
5+
from taurus.entity.link_by_uid import LinkByUID
6+
from taurus.entity.template.property_template import PropertyTemplate
7+
from taurus.entity.template.condition_template import ConditionTemplate
8+
from taurus.entity.value.nominal_integer import NominalInteger
9+
from taurus.entity.value.nominal_categorical import NominalCategorical
10+
from taurus.entity.bounds.integer_bounds import IntegerBounds
11+
from taurus.entity.bounds.categorical_bounds import CategoricalBounds
12+
13+
14+
def test_fields_from_property():
15+
"""Test that several fields of the attribute are derived from the property."""
16+
prop_template = PropertyTemplate(name="cookie eating template", bounds=IntegerBounds(0, 1000))
17+
cond_template = ConditionTemplate(name="Hunger template",
18+
bounds=CategoricalBounds(["hungry", "full", "peckish"]))
19+
prop = Property(name="number of cookies eaten",
20+
template=prop_template,
21+
origin='measured',
22+
value=NominalInteger(27))
23+
cond = Condition(name="hunger level",
24+
template=cond_template,
25+
origin='specified',
26+
value=NominalCategorical("hungry"))
27+
28+
prop_and_conds = PropertyAndConditions(property=prop, conditions=[cond])
29+
assert prop_and_conds.name == prop.name
30+
assert prop_and_conds.template == prop.template
31+
assert prop_and_conds.origin == prop.origin
32+
assert prop_and_conds.value == prop.value
33+
34+
35+
def test_invalid_assignment():
36+
"""Test that invalid assignment throws a TypeError."""
37+
with pytest.raises(TypeError):
38+
PropertyAndConditions(property=LinkByUID('id', 'a15'))
39+
with pytest.raises(TypeError):
40+
PropertyAndConditions(property=Property("property"),
41+
conditions=[Condition("condition"), LinkByUID('scope', 'id')])

taurus/entity/base_entity.py

-4
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,6 @@ def __init__(self, uids, tags):
2929
self._uids = None
3030
self.uids = uids
3131

32-
def content_hash(self):
33-
"""A hash of the object's content."""
34-
return str(sorted(list(self.__dict__.items())))
35-
3632
@property
3733
def tags(self):
3834
"""Get the tags."""

taurus/entity/bounds/real_bounds.py

+7-15
Original file line numberDiff line numberDiff line change
@@ -93,19 +93,11 @@ def _convert_bounds(self, target_units):
9393
A tuple of the (lower_bound, upper_bound) in the target units.
9494
9595
"""
96-
# if neither have units, then just return the bounds
97-
if not self.default_units and not target_units:
98-
return self.lower_bound, self.upper_bound
99-
# if either have units but both don't, then we can't return anything
100-
elif not self.default_units or not target_units:
96+
try:
97+
lower_bound = units.convert_units(
98+
self.lower_bound, self.default_units, target_units)
99+
upper_bound = units.convert_units(
100+
self.upper_bound, self.default_units, target_units)
101+
return lower_bound, upper_bound
102+
except units.IncompatibleUnitsError:
101103
return None, None
102-
# if both have units, then we can try to convert
103-
else:
104-
try:
105-
lower_bound = units.convert_units(
106-
self.lower_bound, self.default_units, target_units)
107-
upper_bound = units.convert_units(
108-
self.upper_bound, self.default_units, target_units)
109-
return lower_bound, upper_bound
110-
except units.IncompatibleUnitsError:
111-
return None, None

taurus/entity/bounds/tests/test_categorical_bounds.py

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def test_contains():
2929
assert bounds.contains(CategoricalBounds(categories={"spam"}))
3030
assert not bounds.contains(CategoricalBounds(categories={"spam", "foo"}))
3131
assert not bounds.contains(RealBounds(0.0, 2.0, ''))
32+
assert not bounds.contains(None)
3233
with pytest.raises(TypeError):
3334
bounds.contains({"spam", "eggs"})
3435

taurus/entity/bounds/tests/test_composition_bounds.py

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def test_contains():
2929
assert bounds.contains(CompositionBounds(components={"spam"}))
3030
assert not bounds.contains(CompositionBounds(components={"foo"}))
3131
assert not bounds.contains(RealBounds(0.0, 2.0, ''))
32+
assert not bounds.contains(None)
3233
with pytest.raises(TypeError):
3334
bounds.contains({"spam"})
3435

taurus/entity/bounds/tests/test_integer_bounds.py

+1
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,6 @@ def test_contains():
3131
assert int_bounds.contains(IntegerBounds(0, 1))
3232
assert int_bounds.contains(IntegerBounds(1, 2))
3333
assert not int_bounds.contains(IntegerBounds(1, 3))
34+
assert not int_bounds.contains(None)
3435
with pytest.raises(TypeError):
3536
int_bounds.contains([0, 1])

0 commit comments

Comments
 (0)