Skip to content

Commit 719d4ce

Browse files
authored
src - Add Result.to_json() with examples-based tests (#101)
* Add Result.to_json() with examples-based tests Revert bad change to tox.ini * Minimal serialization docs * Fix problems identified by pre-commit hook
1 parent 290054d commit 719d4ce

File tree

4 files changed

+97
-0
lines changed

4 files changed

+97
-0
lines changed

docs/source/example.rst

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,28 @@ Line 20-21:
258258

259259
Line 22-25:
260260
The resolved nodes have been added to the result set and are available to be used again later.
261+
262+
Serialization
263+
----
264+
265+
Result objects can be converted to a dictionary, in the same format as the
266+
Overpass API ``json`` output format.
267+
268+
.. code-block:: pycon
269+
>>> import overpy, simplejson
270+
>>> api = overpy.Overpass()
271+
>>> result = api.query("[out:xml];node(50.745,7.17,50.75,7.18);out;")
272+
>>> other_result = overpy.Result.from_json(result.to_json())
273+
>>> assert other_result != result
274+
>>> assert other_result.to_json() == result.to_json()
275+
>>> assert len(result.nodes) == len(other_result.nodes)
276+
>>> assert len(result.ways) == len(other_result.ways)
277+
278+
Serializing the dictionary to JSON requires rendering Decimal values as JSON
279+
numbers, and then parsing with ``Overpass.parse_json()``. The third-party
280+
package ``simplejson`` works for this application:
281+
282+
.. code-block:: pycon
283+
>>> result_str = simplejson.dumps(result.to_json())
284+
>>> new_result = api.parse_json(result_str)
285+
>>> assert len(result.nodes) == len(new_result.nodes)

overpy/__init__.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@
3232
"visible": lambda v: v.lower() == "true"
3333
}
3434

35+
GLOBAL_ATTRIBUTE_SERIALIZERS: Dict[str, Callable] = {
36+
"timestamp": lambda dt: datetime.strftime(dt, "%Y-%m-%dT%H:%M:%SZ"),
37+
}
38+
39+
40+
def _attributes_to_json(attributes: dict):
41+
def attr_serializer(k):
42+
return GLOBAL_ATTRIBUTE_SERIALIZERS.get(k, lambda v: v)
43+
return {k: attr_serializer(k)(v) for k, v in attributes.items()}
44+
3545

3646
def is_valid_type(
3747
element: Union["Area", "Node", "Relation", "Way"],
@@ -334,6 +344,18 @@ def from_json(cls, data: dict, api: Optional[Overpass] = None) -> "Result":
334344

335345
return result
336346

347+
def to_json(self) -> dict:
348+
def elements_to_json():
349+
for elem_cls in [Node, Way, Relation, Area]:
350+
for element in self.get_elements(elem_cls):
351+
yield element.to_json()
352+
353+
return {
354+
"version": 0.6,
355+
"generator": "Overpy Serializer",
356+
"elements": list(elements_to_json())
357+
}
358+
337359
@classmethod
338360
def from_xml(
339361
cls,
@@ -620,6 +642,11 @@ def from_json(cls: Type[ElementTypeVar], data: dict, result: Optional[Result] =
620642
"""
621643
raise NotImplementedError
622644

645+
def to_json(self) -> dict:
646+
d = {"type": self._type_value, "id": self.id, "tags": self.tags}
647+
d.update(_attributes_to_json(self.attributes))
648+
return d
649+
623650
@classmethod
624651
def from_xml(
625652
cls: Type[ElementTypeVar],
@@ -782,6 +809,12 @@ def from_json(cls, data: dict, result: Optional[Result] = None) -> "Node":
782809

783810
return cls(node_id=node_id, lat=lat, lon=lon, tags=tags, attributes=attributes, result=result)
784811

812+
def to_json(self) -> dict:
813+
d = super().to_json()
814+
d["lat"] = self.lat
815+
d["lon"] = self.lon
816+
return d
817+
785818
@classmethod
786819
def from_xml(cls, child: xml.etree.ElementTree.Element, result: Optional[Result] = None) -> "Node":
787820
"""
@@ -965,6 +998,13 @@ def from_json(cls, data: dict, result: Optional[Result] = None) -> "Way":
965998
way_id=way_id
966999
)
9671000

1001+
def to_json(self) -> dict:
1002+
d = super().to_json()
1003+
if self.center_lat is not None and self.center_lon is not None:
1004+
d["center"] = {"lat": self.center_lat, "lon": self.center_lon}
1005+
d["nodes"] = self._node_ids
1006+
return d
1007+
9681008
@classmethod
9691009
def from_xml(cls, child: xml.etree.ElementTree.Element, result: Optional[Result] = None) -> "Way":
9701010
"""
@@ -1104,6 +1144,14 @@ def from_json(cls, data: dict, result: Optional[Result] = None) -> "Relation":
11041144
result=result
11051145
)
11061146

1147+
def to_json(self) -> dict:
1148+
d = super().to_json()
1149+
if self.center_lat is not None and self.center_lon is not None:
1150+
d["center"] = {"lat": self.center_lat, "lon": self.center_lon}
1151+
1152+
d["members"] = [member.to_json() for member in self.members]
1153+
return d
1154+
11071155
@classmethod
11081156
def from_xml(cls, child: xml.etree.ElementTree.Element, result: Optional[Result] = None) -> "Relation":
11091157
"""
@@ -1244,6 +1292,13 @@ def from_json(cls, data: dict, result: Optional[Result] = None) -> "RelationMemb
12441292
result=result
12451293
)
12461294

1295+
def to_json(self):
1296+
d = {"type": self._type_value, "ref": self.ref, "role": self.role}
1297+
if self.geometry is not None:
1298+
d["geometry"] = [{"lat": v.lat, "lon": v.lon} for v in self.geometry]
1299+
d.update(_attributes_to_json(self.attributes))
1300+
return d
1301+
12471302
@classmethod
12481303
def from_xml(
12491304
cls,

tests/test_json.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,78 @@
11
import pytest
2+
import simplejson
23

34
import overpy
45

56
from tests import read_file
67
from tests.base_class import BaseTestAreas, BaseTestNodes, BaseTestRelation, BaseTestWay
78

89

10+
def reparse(api: overpy.Overpass, r: overpy.Result):
11+
# we need `simplejson` because core `json` can't serialize Decimals in the way
12+
# that we would like without enormous hacks
13+
return api.parse_json(simplejson.dumps(r.to_json()))
14+
15+
916
class TestAreas(BaseTestAreas):
1017
def test_area01(self):
1118
api = overpy.Overpass()
1219
result = api.parse_json(read_file("json/area-01.json"))
1320
self._test_area01(result)
21+
self._test_area01(reparse(api, result))
1422

1523

1624
class TestNodes(BaseTestNodes):
1725
def test_node01(self):
1826
api = overpy.Overpass()
1927
result = api.parse_json(read_file("json/node-01.json"))
2028
self._test_node01(result)
29+
self._test_node01(reparse(api, result))
2130

2231

2332
class TestRelation(BaseTestRelation):
2433
def test_relation01(self):
2534
api = overpy.Overpass()
2635
result = api.parse_json(read_file("json/relation-01.json"))
2736
self._test_relation01(result)
37+
self._test_relation01(reparse(api, result))
2838

2939
def test_relation02(self):
3040
api = overpy.Overpass()
3141
result = api.parse_json(read_file("json/relation-02.json"))
3242
self._test_relation02(result)
43+
self._test_relation02(reparse(api, result))
3344

3445
def test_relation03(self):
3546
api = overpy.Overpass()
3647
result = api.parse_json(read_file("json/relation-03.json"))
3748
self._test_relation03(result)
49+
self._test_relation03(reparse(api, result))
3850

3951
def test_relation04(self):
4052
api = overpy.Overpass()
4153
result = api.parse_json(read_file("json/relation-04.json"))
4254
self._test_relation04(result)
55+
self._test_relation04(reparse(api, result))
4356

4457

4558
class TestWay(BaseTestWay):
4659
def test_way01(self):
4760
api = overpy.Overpass()
4861
result = api.parse_json(read_file("json/way-01.json"))
4962
self._test_way01(result)
63+
self._test_way01(reparse(api, result))
5064

5165
def test_way02(self):
5266
api = overpy.Overpass()
5367
result = api.parse_json(read_file("json/way-02.json"))
5468
self._test_way02(result)
69+
self._test_way02(reparse(api, result))
5570

5671
def test_way03(self):
5772
api = overpy.Overpass()
5873
result = api.parse_json(read_file("json/way-03.json"))
5974
self._test_way03(result)
75+
self._test_way03(reparse(api, result))
6076

6177
def test_way04(self):
6278
api = overpy.Overpass()

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ envlist = py37,py38,py39,py310,py311,pypy39
77
[testenv]
88
deps =
99
pytest
10+
simplejson
1011
pytest-cov
1112
commands = pytest --cov overpy --cov-report=term-missing -v tests/
1213

0 commit comments

Comments
 (0)