From 7a95372a63cd12a6b2bcfb0f11c44466dae33415 Mon Sep 17 00:00:00 2001 From: Evan Heidtmann Date: Thu, 21 Oct 2021 16:11:09 -0700 Subject: [PATCH 1/3] Add Result.to_json() with examples-based tests Revert bad change to tox.ini --- overpy/__init__.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++ tests/test_json.py | 20 +++++++++++++++++ tox.ini | 1 + 3 files changed, 76 insertions(+) diff --git a/overpy/__init__.py b/overpy/__init__.py index a85bb01..c2ab3f4 100644 --- a/overpy/__init__.py +++ b/overpy/__init__.py @@ -32,6 +32,16 @@ "visible": lambda v: v.lower() == "true" } +GLOBAL_ATTRIBUTE_SERIALIZERS: Dict[str, Callable] = { + "timestamp": lambda dt: datetime.strftime(dt, "%Y-%m-%dT%H:%M:%SZ"), +} + + +def _attributes_to_json(attributes: dict): + def attr_serializer(k): + return GLOBAL_ATTRIBUTE_SERIALIZERS.get(k, lambda v: v) + return {k: attr_serializer(k)(v) for k, v in attributes.items()} + def is_valid_type( element: Union["Area", "Node", "Relation", "Way"], @@ -366,6 +376,18 @@ def from_json(cls, data: dict, api: Optional[Overpass] = None) -> "Result": return result + def to_json(self) -> dict: + def elements_to_json(): + for elem_cls in [Node, Way, Relation, Area]: + for element in self.get_elements(elem_cls): + yield element.to_json() + + return { + "version": 0.6, + "generator": "Overpy Serializer", + "elements": list(elements_to_json()) + } + @classmethod def from_xml( cls, @@ -664,6 +686,11 @@ def from_json(cls: Type[ElementTypeVar], data: dict, result: Optional[Result] = """ raise NotImplementedError + def to_json(self) -> dict: + d = {"type": self._type_value, "id": self.id, "tags": self.tags} + d.update(_attributes_to_json(self.attributes)) + return d + @classmethod def from_xml( cls: Type[ElementTypeVar], @@ -826,6 +853,12 @@ def from_json(cls, data: dict, result: Optional[Result] = None) -> "Node": return cls(node_id=node_id, lat=lat, lon=lon, tags=tags, attributes=attributes, result=result) + def to_json(self) -> dict: + d = super().to_json() + d["lat"] = self.lat + d["lon"] = self.lon + return d + @classmethod def from_xml(cls, child: xml.etree.ElementTree.Element, result: Optional[Result] = None) -> "Node": """ @@ -1012,6 +1045,13 @@ def from_json(cls, data: dict, result: Optional[Result] = None) -> "Way": way_id=way_id ) + def to_json(self) -> dict: + d = super().to_json() + if self.center_lat is not None and self.center_lon is not None: + d["center"] = {"lat": self.center_lat, "lon": self.center_lon} + d["nodes"] = self._node_ids + return d + @classmethod def from_xml(cls, child: xml.etree.ElementTree.Element, result: Optional[Result] = None) -> "Way": """ @@ -1151,6 +1191,14 @@ def from_json(cls, data: dict, result: Optional[Result] = None) -> "Relation": result=result ) + def to_json(self) -> dict: + d = super().to_json() + if self.center_lat is not None and self.center_lon is not None: + d["center"] = {"lat": self.center_lat, "lon": self.center_lon} + + d["members"] = [member.to_json() for member in self.members] + return d + @classmethod def from_xml(cls, child: xml.etree.ElementTree.Element, result: Optional[Result] = None) -> "Relation": """ @@ -1291,6 +1339,13 @@ def from_json(cls, data: dict, result: Optional[Result] = None) -> "RelationMemb result=result ) + def to_json(self): + d = {"type": self._type_value, "ref": self.ref, "role": self.role} + if self.geometry is not None: + d["geometry"] = [{"lat": v.lat, "lon": v.lon} for v in self.geometry] + d.update(_attributes_to_json(self.attributes)) + return d + @classmethod def from_xml( cls, diff --git a/tests/test_json.py b/tests/test_json.py index 9a96d0e..d82eb9a 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -1,4 +1,8 @@ +from decimal import Decimal +from typing import Iterable, Mapping import pytest +import simplejson +import json import overpy @@ -6,11 +10,17 @@ from tests.base_class import BaseTestAreas, BaseTestNodes, BaseTestRelation, BaseTestWay +def reparse(api: overpy.Overpass, r: overpy.Result): + # we need `simplejson` because core `json` can't serialize Decimals in the way + # that we would like without enormous hacks + return api.parse_json(simplejson.dumps(r.to_json())) + class TestAreas(BaseTestAreas): def test_area01(self): api = overpy.Overpass() result = api.parse_json(read_file("json/area-01.json")) self._test_area01(result) + self._test_area01(reparse(api, result)) class TestNodes(BaseTestNodes): @@ -18,6 +28,7 @@ def test_node01(self): api = overpy.Overpass() result = api.parse_json(read_file("json/node-01.json")) self._test_node01(result) + self._test_node01(reparse(api, result)) class TestRelation(BaseTestRelation): @@ -25,21 +36,27 @@ def test_relation01(self): api = overpy.Overpass() result = api.parse_json(read_file("json/relation-01.json")) self._test_relation01(result) + self._test_relation01(reparse(api, result)) def test_relation02(self): api = overpy.Overpass() result = api.parse_json(read_file("json/relation-02.json")) self._test_relation02(result) + self._test_relation02(reparse(api, result)) + def test_relation03(self): api = overpy.Overpass() result = api.parse_json(read_file("json/relation-03.json")) self._test_relation03(result) + self._test_relation03(reparse(api, result)) + def test_relation04(self): api = overpy.Overpass() result = api.parse_json(read_file("json/relation-04.json")) self._test_relation04(result) + self._test_relation04(reparse(api, result)) class TestWay(BaseTestWay): @@ -47,16 +64,19 @@ def test_way01(self): api = overpy.Overpass() result = api.parse_json(read_file("json/way-01.json")) self._test_way01(result) + self._test_way01(reparse(api, result)) def test_way02(self): api = overpy.Overpass() result = api.parse_json(read_file("json/way-02.json")) self._test_way02(result) + self._test_way02(reparse(api, result)) def test_way03(self): api = overpy.Overpass() result = api.parse_json(read_file("json/way-03.json")) self._test_way03(result) + self._test_way03(reparse(api, result)) def test_way04(self): api = overpy.Overpass() diff --git a/tox.ini b/tox.ini index d4360ac..c7114fb 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,7 @@ envlist = py36,py37,py38,py39,pypy3 [testenv] deps = pytest + simplejson pytest-cov commands = pytest --cov overpy --cov-report=term-missing -v tests/ From 54499fc137421fef02a7ebf5f75c3b27fe91781f Mon Sep 17 00:00:00 2001 From: Evan Heidtmann Date: Thu, 21 Oct 2021 16:49:56 -0700 Subject: [PATCH 2/3] Minimal serialization docs --- docs/source/example.rst | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/source/example.rst b/docs/source/example.rst index f749227..90d0ab3 100644 --- a/docs/source/example.rst +++ b/docs/source/example.rst @@ -258,3 +258,28 @@ Line 20-21: Line 22-25: The resolved nodes have been added to the result set and are available to be used again later. + +Serialization +---- + +Result objects can be converted to a dictionary, in the same format +as the Overpass API `json` output format. + +.. code-block:: pycon + >>> import overpy, simplejson + >>> api = overpy.Overpass() + >>> result = api.query("[out:xml];node(50.745,7.17,50.75,7.18);out;") + >>> other_result = overpy.Result.from_json(result.to_json()) + >>> assert other_result != result + >>> assert other_result.to_json() == result.to_json() + >>> assert len(result.nodes) == len(other_result.nodes) + >>> assert len(result.ways) == len(other_result.ways) + +Serializing the dictionary to JSON requires rendering Decimal values as JSON +numbers, and then parsing with `Overpass.parse_json()`. The third-party package +`simplejson` works for this application: + +.. code-block:: pycon + >>> result_str = simplejson.dumps(result.to_json()) + >>> new_result = api.parse_json(result_str) + >>> assert len(result.nodes) == len(new_result.nodes) \ No newline at end of file From 73d65c306b37dbce8c1ac0e383a70e0968bc036c Mon Sep 17 00:00:00 2001 From: Evan Heidtmann Date: Fri, 29 Oct 2021 08:42:49 -0700 Subject: [PATCH 3/3] Fix problems identified by pre-commit hook --- docs/source/example.rst | 10 +++++----- tests/test_json.py | 6 +----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/source/example.rst b/docs/source/example.rst index 90d0ab3..d7664e3 100644 --- a/docs/source/example.rst +++ b/docs/source/example.rst @@ -262,8 +262,8 @@ Line 22-25: Serialization ---- -Result objects can be converted to a dictionary, in the same format -as the Overpass API `json` output format. +Result objects can be converted to a dictionary, in the same format as the +Overpass API ``json`` output format. .. code-block:: pycon >>> import overpy, simplejson @@ -276,10 +276,10 @@ as the Overpass API `json` output format. >>> assert len(result.ways) == len(other_result.ways) Serializing the dictionary to JSON requires rendering Decimal values as JSON -numbers, and then parsing with `Overpass.parse_json()`. The third-party package -`simplejson` works for this application: +numbers, and then parsing with ``Overpass.parse_json()``. The third-party +package ``simplejson`` works for this application: .. code-block:: pycon >>> result_str = simplejson.dumps(result.to_json()) >>> new_result = api.parse_json(result_str) - >>> assert len(result.nodes) == len(new_result.nodes) \ No newline at end of file + >>> assert len(result.nodes) == len(new_result.nodes) diff --git a/tests/test_json.py b/tests/test_json.py index d82eb9a..3dbdb0e 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -1,8 +1,5 @@ -from decimal import Decimal -from typing import Iterable, Mapping import pytest import simplejson -import json import overpy @@ -15,6 +12,7 @@ def reparse(api: overpy.Overpass, r: overpy.Result): # that we would like without enormous hacks return api.parse_json(simplejson.dumps(r.to_json())) + class TestAreas(BaseTestAreas): def test_area01(self): api = overpy.Overpass() @@ -44,14 +42,12 @@ def test_relation02(self): self._test_relation02(result) self._test_relation02(reparse(api, result)) - def test_relation03(self): api = overpy.Overpass() result = api.parse_json(read_file("json/relation-03.json")) self._test_relation03(result) self._test_relation03(reparse(api, result)) - def test_relation04(self): api = overpy.Overpass() result = api.parse_json(read_file("json/relation-04.json"))