Skip to content

Commit 9dafd76

Browse files
authored
Merge pull request #228 from nielstron/227-tojson
Add dict/json import/export to Quantities, Units and Entities
2 parents c593f8d + 13081bb commit 9dafd76

File tree

4 files changed

+291
-4
lines changed

4 files changed

+291
-4
lines changed

.travis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ script:
4141
- coverage run -a --source=quantulum3 setup.py test -s quantulum3.tests.test_parse_ranges
4242
# Test language specific non-classifier tasks
4343
- coverage run -a --source=quantulum3 setup.py test -s quantulum3._lang.en_US.tests.extract_spellout_values
44+
# Test class methods
45+
- coverage run -a --source=quantulum3 setup.py test -s quantulum3.tests.test_classes
4446
# Test requirements.txt for classifier requirements
4547
- pip install -r requirements_classifier.txt
4648
# Lint package, now that all requirements are installed

README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,37 @@ dimensionality:
116116
Unit(name="kilometre per second", entity=Entity("speed"), uri=None)
117117
```
118118

119+
### Export/Import
120+
121+
Entities, Units and Quantities can be exported to dictionaries and JSON strings:
122+
123+
```pycon
124+
>>> quant = parser.parse('I want 2 liters of wine')
125+
>>> quant[0].to_dict()
126+
{'value': 2.0, 'unit': 'litre', "entity": "volume", 'surface': '2 liters', 'span': (7, 15), 'uncertainty': None, 'lang': 'en_US'}
127+
>>> quant[0].to_json()
128+
'{"value": 2.0, "unit": "litre", "entity": "volume", "surface": "2 liters", "span": [7, 15], "uncertainty": null, "lang": "en_US"}'
129+
```
130+
131+
By default, only the unit/entity name is included in the exported dictionary, but these can be included:
132+
133+
```pycon
134+
>>> quant = parser.parse('I want 2 liters of wine')
135+
>>> quant[0].to_dict(include_unit_dict=True, include_entity_dict=True) # same args apply to .to_json()
136+
{'value': 2.0, 'unit': {'name': 'litre', 'surfaces': ['cubic decimetre', 'cubic decimeter', 'litre', 'liter'], 'entity': {'name': 'volume', 'dimensions': [{'base': 'length', 'power': 3}], 'uri': 'Volume'}, 'uri': 'Litre', 'symbols': ['l', 'L', 'ltr', 'ℓ'], 'dimensions': [{'base': 'decimetre', 'power': 3}], 'original_dimensions': [{'base': 'litre', 'power': 1, 'surface': 'liters'}], 'currency_code': None, 'lang': 'en_US'}, 'entity': 'volume', 'surface': '2 liters', 'span': (7, 15), 'uncertainty': None, 'lang': 'en_US'}
137+
```
138+
139+
Similar export syntax applies to exporting Unit and Entity objects.
140+
141+
You can import Entity, Unit and Quantity objects from dictionaries and JSON. This requires that the object was exported with `include_unit_dict=True` and `include_entity_dict=True` (as appropriate):
142+
143+
```pycon
144+
>>> quant_dict = quant[0].to_dict(include_unit_dict=True, include_entity_dict=True)
145+
>>> quant = Quantity.from_dict(quant_dict)
146+
>>> ent_json = "{'name': 'volume', 'dimensions': [{'base': 'length', 'power': 3}], 'uri': 'Volume'}"
147+
>>> ent = Entity.from_json(ent_json)
148+
```
149+
119150
### Disambiguation
120151

121152
If the parser detects an ambiguity, a classifier based on the WikiPedia
@@ -145,7 +176,7 @@ In addition to that, the classifier is trained on the most similar words to
145176
all of the units surfaces, according to their distance in [GloVe](https://nlp.stanford.edu/projects/glove/)
146177
vector representation.
147178

148-
## Spoken version
179+
### Spoken version
149180

150181
Quantulum classes include methods to convert them to a speakable unit.
151182

@@ -156,6 +187,8 @@ ten billion gigawatts
156187
Gimme ten billion dollars now and also one terawatt and zero point five joules!
157188
```
158189

190+
191+
159192
### Manipulation
160193

161194
While quantities cannot be manipulated within this library, there are

quantulum3/classes.py

Lines changed: 155 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,48 @@
44
:mod:`Quantulum` classes.
55
"""
66

7+
import json
8+
from abc import ABC, abstractmethod
79
from typing import Any, Dict, List, Optional, Tuple
810

911
from . import speak
1012

1113

14+
class JSONMIxin(ABC):
15+
@abstractmethod
16+
def to_dict(self):
17+
pass # pragma: no cover
18+
19+
def to_json(self, *args, **kwargs) -> str:
20+
"""
21+
Create a JSON representation of this object.
22+
23+
:param args: Arguments to pass to the to_dict method
24+
:param kwargs: Keyword arguments to pass to the to_dict method
25+
26+
:return: JSON representation of this object
27+
"""
28+
return json.dumps(self.to_dict(*args, **kwargs))
29+
30+
@classmethod
31+
@abstractmethod
32+
def from_dict(cls, ddict: Dict):
33+
pass # pragma: no cover
34+
35+
@classmethod
36+
def from_json(cls, json_str: str):
37+
"""
38+
Create an object from a JSON string.
39+
40+
:param json_str: JSON string to convert to an object
41+
42+
:return: Object created from the JSON string
43+
"""
44+
return cls.from_dict(json.loads(json_str))
45+
46+
1247
###############################################################################
13-
class Entity(object):
48+
class Entity(JSONMIxin, object):
1449
"""
1550
Class for an entity (e.g. "volume").
1651
"""
@@ -42,9 +77,36 @@ def __ne__(self, other):
4277
def __hash__(self):
4378
return hash(repr(self))
4479

80+
def to_dict(self) -> Dict:
81+
"""
82+
Create a dictionary representation of this entity.
83+
84+
:return: Dictionary representation of this entity
85+
"""
86+
return {
87+
"name": self.name,
88+
"dimensions": self.dimensions,
89+
"uri": self.uri,
90+
}
91+
92+
@classmethod
93+
def from_dict(cls, ddict: Dict) -> "Entity":
94+
"""
95+
Create an entity from a dictionary representation.
96+
97+
:param ddict: Dictionary representation of an entity (as produced by to_dict)
98+
99+
:return: Entity created from the dictionary representation
100+
"""
101+
return cls(
102+
name=ddict["name"],
103+
dimensions=ddict["dimensions"],
104+
uri=ddict["uri"],
105+
)
106+
45107

46108
###############################################################################
47-
class Unit(object):
109+
class Unit(JSONMIxin, object):
48110
"""
49111
Class for a unit (e.g. "gallon").
50112
"""
@@ -111,11 +173,57 @@ def __ne__(self, other):
111173
def __hash__(self):
112174
return hash(repr(self))
113175

176+
def to_dict(self, include_entity_dict: bool = False) -> Dict:
177+
"""
178+
Create a dictionary representation of this unit.
179+
180+
:param include_entity: When False, just the name of the entity is included, when True the full entity is included. Default is False.
181+
182+
:return: Dictionary representation of this unit
183+
"""
184+
ddict = {
185+
"name": self.name,
186+
"surfaces": self.surfaces,
187+
"entity": self.entity.name,
188+
"uri": self.uri,
189+
"symbols": self.symbols,
190+
"dimensions": self.dimensions,
191+
"original_dimensions": self.original_dimensions,
192+
"currency_code": self.currency_code,
193+
"lang": self.lang,
194+
}
195+
196+
if include_entity_dict:
197+
ddict["entity"] = self.entity.to_dict()
198+
199+
return ddict
200+
201+
@classmethod
202+
def from_dict(cls, ddict: Dict) -> "Unit":
203+
"""
204+
Create a unit from a dictionary representation.
205+
206+
:param ddict: Dictionary representation of a unit (as produced by to_dict)
207+
208+
:return: Unit created from the dictionary representation
209+
"""
210+
return cls(
211+
name=ddict["name"],
212+
surfaces=ddict["surfaces"],
213+
entity=Entity.from_dict(ddict["entity"]),
214+
uri=ddict["uri"],
215+
symbols=ddict["symbols"],
216+
dimensions=ddict["dimensions"],
217+
original_dimensions=ddict["original_dimensions"],
218+
currency_code=ddict["currency_code"],
219+
lang=ddict["lang"],
220+
)
221+
114222

115223
###############################################################################
116224

117225

118-
class Quantity(object):
226+
class Quantity(JSONMIxin, object):
119227
"""
120228
Class for a quantity (e.g. "4.2 gallons").
121229
"""
@@ -183,3 +291,47 @@ def to_spoken(self, lang=None):
183291
:return: Speakable version of this quantity
184292
"""
185293
return speak.quantity_to_spoken(self, lang or self.lang)
294+
295+
def to_dict(
296+
self, include_unit_dict: bool = False, include_entity_dict: bool = False
297+
) -> Dict:
298+
"""
299+
Create a dictionary representation of this quantity
300+
301+
:param include_unit: When False, just the name of the unit is included, when True, the full unit is included. Defaults to False
302+
:param include_entity: When False, just the name of the entity is included, when True, the full entity is included. Defaults to False. Only used when include_unit is True.
303+
304+
:return: Dictionary representation of this quantity
305+
"""
306+
ddict = {
307+
"value": self.value,
308+
"unit": self.unit.name,
309+
"entity": self.unit.entity.name,
310+
"surface": self.surface,
311+
"span": self.span,
312+
"uncertainty": self.uncertainty,
313+
"lang": self.lang,
314+
}
315+
316+
if include_unit_dict:
317+
ddict["unit"] = self.unit.to_dict(include_entity_dict)
318+
319+
return ddict
320+
321+
@classmethod
322+
def from_dict(cls, ddict: Dict) -> "Quantity":
323+
"""
324+
Create a quantity from a dictionary representation.
325+
326+
:param ddict: Dictionary representation of a quantity (as produced by to_dict)
327+
328+
:return: Quantity created from the dictionary representation
329+
"""
330+
return cls(
331+
value=ddict["value"],
332+
unit=Unit.from_dict(ddict["unit"]),
333+
surface=ddict["surface"],
334+
span=tuple(ddict["span"]),
335+
uncertainty=ddict["uncertainty"],
336+
lang=ddict["lang"],
337+
)

quantulum3/tests/test_classes.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import unittest
2+
3+
from ..classes import Entity, Quantity, Unit
4+
5+
6+
class TestClasses(unittest.TestCase):
7+
def setUp(self):
8+
self.e = Entity(name="test_entity", dimensions=list(), uri="test_uri")
9+
self.u = Unit(
10+
name="test_unit",
11+
entity=self.e,
12+
surfaces=["test_surface"],
13+
uri="test_uri",
14+
symbols=["test_symbol"],
15+
)
16+
self.q = Quantity(
17+
1.0,
18+
self.u,
19+
surface="test_surface",
20+
span=(0, 1),
21+
uncertainty=0.1,
22+
lang="en_US",
23+
)
24+
25+
def test_entity_to_dict(self):
26+
entity_dict = self.e.to_dict()
27+
self.assertIsInstance(entity_dict, dict)
28+
self.assertEqual(entity_dict["name"], self.e.name)
29+
self.assertEqual(entity_dict["dimensions"], self.e.dimensions)
30+
self.assertEqual(entity_dict["uri"], self.e.uri)
31+
32+
def test_entity_to_json(self):
33+
entity_json = self.e.to_json()
34+
self.assertIsInstance(entity_json, str)
35+
36+
def test_entity_from_dict(self):
37+
entity_dict = self.e.to_dict()
38+
entity = Entity.from_dict(entity_dict)
39+
self.assertEqual(entity, self.e)
40+
self.assertIsInstance(entity, Entity)
41+
42+
def test_entity_from_json(self):
43+
entity_json = self.e.to_json()
44+
entity = Entity.from_json(entity_json)
45+
self.assertIsInstance(entity, Entity)
46+
47+
def test_unit_to_dict(self):
48+
unit_dict = self.u.to_dict()
49+
self.assertIsInstance(unit_dict, dict)
50+
self.assertEqual(unit_dict["name"], self.u.name)
51+
self.assertEqual(unit_dict["entity"], self.u.entity.name)
52+
self.assertEqual(unit_dict["surfaces"], self.u.surfaces)
53+
self.assertEqual(unit_dict["uri"], self.u.uri)
54+
self.assertEqual(unit_dict["symbols"], self.u.symbols)
55+
self.assertEqual(unit_dict["dimensions"], self.u.dimensions)
56+
self.assertEqual(unit_dict["currency_code"], self.u.currency_code)
57+
self.assertEqual(unit_dict["original_dimensions"], self.u.original_dimensions)
58+
self.assertEqual(unit_dict["lang"], self.u.lang)
59+
60+
def test_unit_to_json(self):
61+
unit_json = self.u.to_json()
62+
self.assertIsInstance(unit_json, str)
63+
64+
def test_unit_from_dict(self):
65+
unit_dict = self.u.to_dict(include_entity_dict=True)
66+
unit = Unit.from_dict(unit_dict)
67+
self.assertEqual(unit, self.u)
68+
self.assertIsInstance(unit, Unit)
69+
70+
def test_unit_from_json(self):
71+
unit_json = self.u.to_json(include_entity_dict=True)
72+
unit = Unit.from_json(unit_json)
73+
self.assertEqual(unit, self.u)
74+
self.assertIsInstance(unit, Unit)
75+
76+
def test_quantity_to_dict(self):
77+
quantity_dict = self.q.to_dict()
78+
self.assertIsInstance(quantity_dict, dict)
79+
self.assertEqual(quantity_dict["value"], self.q.value)
80+
self.assertEqual(quantity_dict["unit"], self.u.name)
81+
self.assertEqual(quantity_dict["surface"], self.q.surface)
82+
self.assertEqual(quantity_dict["span"], self.q.span)
83+
self.assertEqual(quantity_dict["uncertainty"], self.q.uncertainty)
84+
self.assertEqual(quantity_dict["lang"], self.q.lang)
85+
86+
def test_quantity_to_json(self):
87+
quantity_json = self.q.to_json()
88+
self.assertIsInstance(quantity_json, str)
89+
90+
def test_quantity_from_dict(self):
91+
quantity_dict = self.q.to_dict(include_unit_dict=True, include_entity_dict=True)
92+
quantity = Quantity.from_dict(quantity_dict)
93+
self.assertEqual(quantity, self.q)
94+
self.assertIsInstance(quantity, Quantity)
95+
96+
def test_quantity_from_json(self):
97+
quantity_json = self.q.to_json(include_unit_dict=True, include_entity_dict=True)
98+
quantity = Quantity.from_json(quantity_json)
99+
self.assertEqual(quantity, self.q)
100+
self.assertIsInstance(quantity, Quantity)

0 commit comments

Comments
 (0)