From 2cc4233a5354f2d996b46ad44b65430c0adeb23d Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 11 Nov 2023 20:13:28 +0000 Subject: [PATCH 1/3] Fix TimeStamp and TimeSpan class to use class_from_element method --- fastkml/base.py | 8 +++++- fastkml/kml.py | 18 ++++++++---- fastkml/times.py | 62 +++++++++++++++++++++++++++++++---------- fastkml/views.py | 25 +++++++++++++---- tests/times_test.py | 67 +++++++++++++++++++++++++++++++++++---------- 5 files changed, 138 insertions(+), 42 deletions(-) diff --git a/fastkml/base.py b/fastkml/base.py index 197f4a98..0181b0b7 100644 --- a/fastkml/base.py +++ b/fastkml/base.py @@ -158,7 +158,12 @@ def class_from_element( strict: bool, ) -> "_XMLObject": """Creates an XML object from an etree element.""" - kwargs = cls._get_kwargs(ns=ns, element=element, strict=strict) + kwargs = cls._get_kwargs( + ns=ns, + name_spaces=name_spaces, + element=element, + strict=strict, + ) return cls( **kwargs, ) @@ -186,6 +191,7 @@ def class_from_string( ns = cls._get_ns(ns) return cls.class_from_element( ns=ns, + name_spaces=name_spaces, strict=strict, element=cast( Element, diff --git a/fastkml/kml.py b/fastkml/kml.py index 82564b65..1f1b5a3a 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -458,14 +458,20 @@ def from_element(self, element: Element, strict: bool = False) -> None: self.snippet = _snippet timespan = element.find(f"{self.ns}TimeSpan") if timespan is not None: - s = TimeSpan(self.ns) - s.from_element(timespan) - self._timespan = s + self._timespan = TimeSpan.class_from_element( + ns=self.ns, + name_spaces=self.name_spaces, + element=timespan, + strict=strict, + ) timestamp = element.find(f"{self.ns}TimeStamp") if timestamp is not None: - s = TimeStamp(self.ns) - s.from_element(timestamp) - self._timestamp = s + self._timestamp = TimeStamp.class_from_element( + ns=self.ns, + name_spaces=self.name_spaces, + element=timestamp, + strict=strict, + ) atom_link = element.find(f"{atom.NS}link") if atom_link is not None: s = atom.Link() diff --git a/fastkml/times.py b/fastkml/times.py index 57584dbb..b71063eb 100644 --- a/fastkml/times.py +++ b/fastkml/times.py @@ -17,6 +17,8 @@ import re from datetime import date from datetime import datetime +from typing import Any +from typing import Dict from typing import Optional from typing import Union @@ -157,11 +159,12 @@ class TimeStamp(_TimePrimitive): def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, timestamp: Optional[KmlDateTime] = None, ) -> None: - super().__init__(ns=ns, id=id, target_id=target_id) + super().__init__(ns=ns, name_spaces=name_spaces, id=id, target_id=target_id) self.timestamp = timestamp def etree_element( @@ -177,11 +180,25 @@ def etree_element( when.text = str(self.timestamp) return element - def from_element(self, element: Element) -> None: - super().from_element(element) - when = element.find(f"{self.ns}when") + @classmethod + def _get_kwargs( + cls, + *, + ns: str, + name_spaces: Optional[Dict[str, str]] = None, + element: Element, + strict: bool, + ) -> Dict[str, Any]: + kwargs = super()._get_kwargs( + ns=ns, + name_spaces=name_spaces, + element=element, + strict=strict, + ) + when = element.find(f"{ns}when") if when is not None: - self.timestamp = KmlDateTime.parse(when.text) + kwargs["timestamp"] = KmlDateTime.parse(when.text) + return kwargs class TimeSpan(_TimePrimitive): @@ -192,24 +209,16 @@ class TimeSpan(_TimePrimitive): def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, begin: Optional[KmlDateTime] = None, end: Optional[KmlDateTime] = None, ) -> None: - super().__init__(ns=ns, id=id, target_id=target_id) + super().__init__(ns=ns, name_spaces=name_spaces, id=id, target_id=target_id) self.begin = begin self.end = end - def from_element(self, element: Element) -> None: - super().from_element(element) - begin = element.find(f"{self.ns}begin") - if begin is not None: - self.begin = KmlDateTime.parse(begin.text) - end = element.find(f"{self.ns}end") - if end is not None: - self.end = KmlDateTime.parse(end.text) - def etree_element( self, precision: Optional[int] = None, @@ -237,3 +246,26 @@ def etree_element( raise ValueError(msg) # TODO test if end > begin return element + + @classmethod + def _get_kwargs( + cls, + *, + ns: str, + name_spaces: Optional[Dict[str, str]] = None, + element: Element, + strict: bool, + ) -> Dict[str, Any]: + kwargs = super()._get_kwargs( + ns=ns, + name_spaces=name_spaces, + element=element, + strict=strict, + ) + begin = element.find(f"{ns}begin") + if begin is not None: + kwargs["begin"] = KmlDateTime.parse(begin.text) + end = element.find(f"{ns}end") + if end is not None: + kwargs["end"] = KmlDateTime.parse(end.text) + return kwargs diff --git a/fastkml/views.py b/fastkml/views.py index 0b09979a..71f2eda8 100644 --- a/fastkml/views.py +++ b/fastkml/views.py @@ -3,6 +3,7 @@ from typing import Optional from typing import SupportsFloat from typing import Union +from typing import cast from fastkml import config from fastkml.base import _BaseObject @@ -185,14 +186,26 @@ def from_element(self, element: Element) -> None: self.altitude_mode = AltitudeMode(altitude_mode.text) timespan = element.find(f"{self.ns}TimeSpan") if timespan is not None: - span = TimeSpan(self.ns) - span.from_element(timespan) - self._timespan = span + self._timespan = cast( + TimeSpan, + TimeSpan.class_from_element( + ns=self.ns, + name_spaces=self.name_spaces, + element=timespan, + strict=False, + ), + ) timestamp = element.find(f"{self.ns}TimeStamp") if timestamp is not None: - stamp = TimeStamp(self.ns) - stamp.from_element(timestamp) - self._timestamp = stamp + self._timestamp = cast( + TimeStamp, + TimeStamp.class_from_element( + ns=self.ns, + name_spaces=self.name_spaces, + element=timestamp, + strict=False, + ), + ) def etree_element( self, diff --git a/tests/times_test.py b/tests/times_test.py index 33081770..5fc46851 100644 --- a/tests/times_test.py +++ b/tests/times_test.py @@ -151,7 +151,13 @@ def test_parse_datetime_with_tz(self) -> None: assert dt.resolution == DateTimeResolution.datetime assert dt.dt == datetime.datetime( - 1997, 7, 16, 7, 30, 15, tzinfo=tzoffset(None, 3600), + 1997, + 7, + 16, + 7, + 30, + 15, + tzinfo=tzoffset(None, 3600), ) def test_parse_datetime_with_tz_no_colon(self) -> None: @@ -159,7 +165,13 @@ def test_parse_datetime_with_tz_no_colon(self) -> None: assert dt.resolution == DateTimeResolution.datetime assert dt.dt == datetime.datetime( - 1997, 7, 16, 7, 30, 15, tzinfo=tzoffset(None, 3600), + 1997, + 7, + 16, + 7, + 30, + 15, + tzinfo=tzoffset(None, 3600), ) def test_parse_datetime_no_tz(self) -> None: @@ -284,44 +296,55 @@ def test_feature_timespan_stamp(self) -> None: # are allowed not both pytest.raises(ValueError, f.to_string) - def test_read_timestamp(self) -> None: - ts = kml.TimeStamp(ns="") + def test_read_timestamp_year(self) -> None: doc = """ 1997 """ - ts.from_string(doc) + ts = kml.TimeStamp.class_from_string(doc, ns="") + assert ts.timestamp.resolution == DateTimeResolution.year assert ts.timestamp.dt == datetime.datetime(1997, 1, 1, 0, 0, tzinfo=tzutc()) + + def test_read_timestamp_year_month(self) -> None: doc = """ 1997-07 """ - ts.from_string(doc) + ts = kml.TimeStamp.class_from_string(doc, ns="") + assert ts.timestamp.resolution == DateTimeResolution.year_month assert ts.timestamp.dt == datetime.datetime(1997, 7, 1, 0, 0, tzinfo=tzutc()) + + def test_read_timestamp_ym_no_hyphen(self) -> None: doc = """ 199808 """ - ts.from_string(doc) + ts = kml.TimeStamp.class_from_string(doc, ns="") + assert ts.timestamp.resolution == DateTimeResolution.year_month assert ts.timestamp.dt == datetime.datetime(1998, 8, 1, 0, 0, tzinfo=tzutc()) + + def test_read_timestamp_ymd(self) -> None: doc = """ 1997-07-16 """ - ts.from_string(doc) + ts = kml.TimeStamp.class_from_string(doc, ns="") + assert ts.timestamp.resolution == DateTimeResolution.date assert ts.timestamp.dt == datetime.datetime(1997, 7, 16, 0, 0, tzinfo=tzutc()) + + def test_read_timestamp_utc(self) -> None: # dateTime (YYYY-MM-DDThh:mm:ssZ) # Here, T is the separator between the calendar and the hourly notation # of time, and Z indicates UTC. (Seconds are required.) @@ -331,25 +354,40 @@ def test_read_timestamp(self) -> None: """ - ts.from_string(doc) + ts = kml.TimeStamp.class_from_string(doc, ns="") + assert ts.timestamp.resolution == DateTimeResolution.datetime assert ts.timestamp.dt == datetime.datetime( - 1997, 7, 16, 7, 30, 15, tzinfo=tzutc(), + 1997, + 7, + 16, + 7, + 30, + 15, + tzinfo=tzutc(), ) + + def test_read_timestamp_utc_offset(self) -> None: doc = """ 1997-07-16T10:30:15+03:00 """ - ts.from_string(doc) + ts = kml.TimeStamp.class_from_string(doc, ns="") + assert ts.timestamp.resolution == DateTimeResolution.datetime assert ts.timestamp.dt == datetime.datetime( - 1997, 7, 16, 10, 30, 15, tzinfo=tzoffset(None, 10800), + 1997, + 7, + 16, + 10, + 30, + 15, + tzinfo=tzoffset(None, 10800), ) def test_read_timespan(self) -> None: - ts = kml.TimeSpan(ns="") doc = """ 1876-08-01 @@ -357,7 +395,8 @@ def test_read_timespan(self) -> None: """ - ts.from_string(doc) + ts = kml.TimeSpan.class_from_string(doc, ns="") + assert ts.begin.resolution == DateTimeResolution.date assert ts.begin.dt == datetime.datetime(1876, 8, 1, 0, 0, tzinfo=tzutc()) assert ts.end.resolution == DateTimeResolution.datetime From 19571210eb6b65016db826a27da4ce0b5cfb3fae Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 11 Nov 2023 20:14:52 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- examples/UsageExamples.py | 2 +- tests/atom_test.py | 4 +++- tests/base_test.py | 3 ++- tests/geometries/point_test.py | 3 ++- tests/gx_test.py | 44 +++++++++++++++++++++++++++++----- 5 files changed, 46 insertions(+), 10 deletions(-) diff --git a/examples/UsageExamples.py b/examples/UsageExamples.py index b55e6210..552a2d7f 100644 --- a/examples/UsageExamples.py +++ b/examples/UsageExamples.py @@ -17,7 +17,7 @@ def print_child_features(element): k = kml.KML() - with open(fname) as kml_file: # noqa: ENC001 + with open(fname) as kml_file: k.from_string(kml_file.read().encode("utf-8")) print_child_features(k) diff --git a/tests/atom_test.py b/tests/atom_test.py index d059e1c6..533448a1 100644 --- a/tests/atom_test.py +++ b/tests/atom_test.py @@ -82,7 +82,9 @@ def test_atom_person_ns(self) -> None: def test_atom_author(self) -> None: a = atom.Author( - name="Nobody", uri="http://localhost", email="cl@donotreply.com", + name="Nobody", + uri="http://localhost", + email="cl@donotreply.com", ) serialized = a.to_string() diff --git a/tests/base_test.py b/tests/base_test.py index 78eb7bba..0ab1b2fa 100644 --- a/tests/base_test.py +++ b/tests/base_test.py @@ -43,7 +43,8 @@ def test_to_str_empty_ns(self) -> None: obj.__name__ = "test" assert obj.to_string().replace(" ", "").replace( - "\n", "", + "\n", + "", ) == ''.replace(" ", "") def test_from_string(self) -> None: diff --git a/tests/geometries/point_test.py b/tests/geometries/point_test.py index dde8ce3a..e2c73c77 100644 --- a/tests/geometries/point_test.py +++ b/tests/geometries/point_test.py @@ -54,7 +54,8 @@ def test_to_string_empty_geometry(self) -> None: point = Point(geometry=geo.Point(None, None)) # type: ignore[arg-type] with pytest.raises( - KMLWriteError, match=r"Invalid dimensions in coordinates '\(\(\),\)'", + KMLWriteError, + match=r"Invalid dimensions in coordinates '\(\(\),\)'", ): point.to_string() diff --git a/tests/gx_test.py b/tests/gx_test.py index 1212b0bf..0ba0da87 100644 --- a/tests/gx_test.py +++ b/tests/gx_test.py @@ -95,14 +95,24 @@ def test_multitrack(self) -> None: track_items=[ TrackItem( when=datetime.datetime( - 2020, 1, 1, 0, 0, tzinfo=tzutc(), + 2020, + 1, + 1, + 0, + 0, + tzinfo=tzutc(), ), coord=geo.Point(0.0, 0.0), angle=None, ), TrackItem( when=datetime.datetime( - 2020, 1, 1, 0, 10, tzinfo=tzutc(), + 2020, + 1, + 1, + 0, + 10, + tzinfo=tzutc(), ), coord=geo.Point(1.0, 0.0), angle=None, @@ -119,14 +129,24 @@ def test_multitrack(self) -> None: track_items=[ TrackItem( when=datetime.datetime( - 2020, 1, 1, 0, 10, tzinfo=tzutc(), + 2020, + 1, + 1, + 0, + 10, + tzinfo=tzutc(), ), coord=geo.Point(0.0, 1.0), angle=None, ), TrackItem( when=datetime.datetime( - 2020, 1, 1, 0, 20, tzinfo=tzutc(), + 2020, + 1, + 1, + 0, + 20, + tzinfo=tzutc(), ), coord=geo.Point(1.0, 1.0), angle=None, @@ -384,14 +404,26 @@ def test_multitrack(self) -> None: track_items=[ TrackItem( when=datetime.datetime( - 2010, 5, 28, 2, 2, 55, tzinfo=tzutc(), + 2010, + 5, + 28, + 2, + 2, + 55, + tzinfo=tzutc(), ), coord=geo.Point(-122.203451, 37.374706, 141.800003), angle=Angle(heading=1.0, tilt=2.0, roll=3.0), ), TrackItem( when=datetime.datetime( - 2010, 5, 28, 2, 2, 56, tzinfo=tzutc(), + 2010, + 5, + 28, + 2, + 2, + 56, + tzinfo=tzutc(), ), coord=geo.Point(-122.203329, 37.37478, 141.199997), angle=Angle(heading=1.0, tilt=2.0, roll=3.0), From 046dd3f566073554ff476b65904833b5398b6213 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 11 Nov 2023 20:22:27 +0000 Subject: [PATCH 3/3] Fix encoding issue in KML file reading --- examples/UsageExamples.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/UsageExamples.py b/examples/UsageExamples.py index 552a2d7f..bf4a44f2 100644 --- a/examples/UsageExamples.py +++ b/examples/UsageExamples.py @@ -4,7 +4,7 @@ def print_child_features(element): - """Prints the name of every child node of the given element, recursively""" + """Prints the name of every child node of the given element, recursively.""" if not getattr(element, "features", None): return for feature in element.features(): @@ -17,7 +17,7 @@ def print_child_features(element): k = kml.KML() - with open(fname) as kml_file: + with open(fname, encoding="utf-8") as kml_file: k.from_string(kml_file.read().encode("utf-8")) print_child_features(k)