From 2de6210d7f1b2b0b61a05652df12634cdcc030a0 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 13 Nov 2024 09:37:53 +0000 Subject: [PATCH 01/10] update README to include Hypothesis badge and bump version to 1.0.0rc1 --- README.rst | 6 +++++- fastkml/about.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index b52bcde9..4c8f58b2 100644 --- a/README.rst +++ b/README.rst @@ -22,12 +22,16 @@ Geometries are handled as pygeoif_ objects. Fastkml is continually tested -|test| |cov| |black| |mypy| |commit| +|test| |hypothesis| |cov| |black| |mypy| |commit| .. |test| image:: https://github.com/cleder/fastkml/actions/workflows/run-all-tests.yml/badge.svg?branch=main :target: https://github.com/cleder/fastkml/actions/workflows/run-all-tests.yml :alt: Test +.. |hypothesis| image:: https://img.shields.io/badge/hypothesis-tested-brightgreen.svg + :alt: Tested with Hypothesis + :target: https://hypothesis.readthedocs.io + .. |cov| image:: https://codecov.io/gh/cleder/fastkml/branch/main/graph/badge.svg?token=VIuhPHq0ow :target: http://codecov.io/github/cleder/fastkml?branch=main :alt: codecov.io diff --git a/fastkml/about.py b/fastkml/about.py index 778b8c85..da54aaa9 100644 --- a/fastkml/about.py +++ b/fastkml/about.py @@ -19,6 +19,6 @@ The only purpose of this module is to provide a version number for the package. """ -__version__ = "1.0.0b3" +__version__ = "1.0.0rc1" __all__ = ["__version__"] From b6e5a5203bc4224bd4c1905fb1a8ac22f3e36685 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 13 Nov 2024 18:00:02 +0000 Subject: [PATCH 02/10] additional tests --- fastkml/features.py | 36 ++------------------------ fastkml/geometry.py | 13 ---------- fastkml/overlays.py | 38 +++++++--------------------- fastkml/styles.py | 14 ---------- fastkml/utils.py | 37 +++++---------------------- fastkml/views.py | 18 ------------- tests/geometries/coordinates_test.py | 11 ++++++++ tests/geometries/linestring_test.py | 6 +++++ tests/geometries/point_test.py | 7 +++++ tests/geometries/polygon_test.py | 16 ++++++++++++ tests/validator_test.py | 16 ++++++++++++ tox.ini | 1 + 12 files changed, 75 insertions(+), 138 deletions(-) diff --git a/fastkml/features.py b/fastkml/features.py index 6a4c729b..f8d091a5 100644 --- a/fastkml/features.py +++ b/fastkml/features.py @@ -69,6 +69,8 @@ from fastkml.views import LookAt from fastkml.views import Region +__all__ = ["KmlGeometry", "NetworkLink", "Placemark", "Snippet"] + logger = logging.getLogger(__name__) KmlGeometry = Union[ @@ -303,40 +305,6 @@ def __init__( self.extended_data = extended_data self.times = times - def __repr__(self) -> str: - """ - Return a string representation of the _Feature object. - - Returns - ------- - str: String representation of the _Feature object. - - """ - return ( - f"{self.__class__.__module__}.{self.__class__.__name__}(" - f"ns={self.ns!r}, " - f"name_spaces={self.name_spaces!r}, " - f"id={self.id!r}, " - f"target_id={self.target_id!r}, " - f"name={self.name!r}, " - f"visibility={self.visibility!r}, " - f"isopen={self.isopen!r}, " - f"atom_link={self.atom_link!r}, " - f"atom_author={self.atom_author!r}, " - f"address={self.address!r}, " - f"phone_number={self.phone_number!r}, " - f"snippet={self.snippet!r}, " - f"description={self.description!r}, " - f"view={self.view!r}, " - f"times={self.times!r}, " - f"style_url={self.style_url!r}, " - f"styles={self.styles!r}, " - f"region={self.region!r}, " - f"extended_data={self.extended_data!r}, " - f"**{self._get_splat()!r}," - ")" - ) - registry.register( _Feature, diff --git a/fastkml/geometry.py b/fastkml/geometry.py index d6853b59..3d920a90 100644 --- a/fastkml/geometry.py +++ b/fastkml/geometry.py @@ -359,19 +359,6 @@ def __init__( ) self.altitude_mode = altitude_mode - def __repr__(self) -> str: - """Create a string (c)representation for _Geometry.""" - return ( - f"{self.__class__.__module__}.{self.__class__.__name__}(" - f"ns={self.ns!r}, " - f"name_spaces={self.name_spaces!r}, " - f"id={self.id!r}, " - f"target_id={self.target_id!r}, " - f"altitude_mode={self.altitude_mode}, " - f"**{self._get_splat()!r}," - ")" - ) - class Point(_Geometry): """ diff --git a/fastkml/overlays.py b/fastkml/overlays.py index 61dbe729..ab6a9551 100644 --- a/fastkml/overlays.py +++ b/fastkml/overlays.py @@ -60,6 +60,15 @@ from fastkml.views import LookAt from fastkml.views import Region +__all__ = [ + "GroundOverlay", + "ImagePyramid", + "KmlGeometry", + "LatLonBox", + "PhotoOverlay", + "ViewVolume", +] + logger = logging.getLogger(__name__) KmlGeometry = Union[ @@ -207,35 +216,6 @@ def __init__( self.color = clean_string(color) self.draw_order = draw_order - def __repr__(self) -> str: - """Create a string (c)representation for _Overlay.""" - return ( - f"{self.__class__.__module__}.{self.__class__.__name__}(" - f"ns={self.ns!r}, " - f"name_spaces={self.name_spaces!r}, " - f"id={self.id!r}, " - f"target_id={self.target_id!r}, " - f"name={self.name!r}, " - f"visibility={self.visibility!r}, " - f"isopen={self.isopen!r}, " - f"atom_link={self.atom_link!r}, " - f"atom_author={self.atom_author!r}, " - f"address={self.address!r}, " - f"phone_number={self.phone_number!r}, " - f"snippet={self.snippet!r}, " - f"description={self.description!r}, " - f"view={self.view!r}, " - f"times={self.times!r}, " - f"style_url={self.style_url!r}, " - f"styles={self.styles!r}, " - f"region={self.region!r}, " - f"extended_data={self.extended_data!r}, " - f"color={self.color!r}, " - f"draw_order={self.draw_order!r}, " - f"icon={self.icon!r}, " - ")" - ) - registry.register( _Overlay, diff --git a/fastkml/styles.py b/fastkml/styles.py index 254cb420..671a7943 100644 --- a/fastkml/styles.py +++ b/fastkml/styles.py @@ -225,20 +225,6 @@ def __init__( self.color = clean_string(color) self.color_mode = color_mode - def __repr__(self) -> str: - """Create a string (c)representation for _ColorStyle.""" - return ( - f"{self.__class__.__module__}.{self.__class__.__name__}(" - f"ns={self.ns!r}, " - f"name_spaces={self.name_spaces!r}, " - f"id={self.id!r}, " - f"target_id={self.target_id!r}, " - f"color={self.color!r}, " - f"color_mode={self.color_mode}, " - f"**{self._get_splat()!r}," - ")" - ) - registry.register( _ColorStyle, diff --git a/fastkml/utils.py b/fastkml/utils.py index 70cf5abc..87cc7d38 100644 --- a/fastkml/utils.py +++ b/fastkml/utils.py @@ -30,14 +30,14 @@ def has_attribute_values(obj: object, **kwargs: Any) -> bool: return False -def find_in( +def find_all( obj: object, *, of_type: Optional[Union[Type[object], Tuple[Type[object], ...]]] = None, **kwargs: Any, ) -> Generator[object, None, None]: """ - Find all instances of type in the attributes of an object. + Find all instances of a given type with attributes matching the kwargs. Args: ---- @@ -50,6 +50,11 @@ def find_in( An iterable of all instances of the given type in the given object. """ + if (of_type is None or isinstance(obj, of_type)) and has_attribute_values( + obj, + **kwargs, + ): + yield obj try: attrs = (attr for attr in obj.__dict__ if not attr.startswith("_")) except AttributeError: @@ -63,34 +68,6 @@ def find_in( yield from find_all(attr, of_type=of_type, **kwargs) -def find_all( - obj: object, - *, - of_type: Optional[Union[Type[object], Tuple[Type[object], ...]]] = None, - **kwargs: Any, -) -> Generator[object, None, None]: - """ - Find all instances of a given type with attributes matching the kwargs. - - Args: - ---- - obj: The object to search. - of_type: The type(s) to search for or None for any type. - **kwargs: Attributes of the object to match. - - Returns: - ------- - An iterable of all instances of the given type in the given object. - - """ - if (of_type is None or isinstance(obj, of_type)) and has_attribute_values( - obj, - **kwargs, - ): - yield obj - yield from find_in(obj, of_type=of_type, **kwargs) - - def find( obj: object, *, diff --git a/fastkml/views.py b/fastkml/views.py index 9a6b5297..16ac27fe 100644 --- a/fastkml/views.py +++ b/fastkml/views.py @@ -149,24 +149,6 @@ def __init__( self.tilt = tilt self.altitude_mode = altitude_mode - def __repr__(self) -> str: - """Create a string (c)representation for _AbstractView.""" - return ( - f"{self.__class__.__module__}.{self.__class__.__name__}(" - f"ns={self.ns!r}, " - f"name_spaces={self.name_spaces!r}, " - f"id={self.id!r}, " - f"target_id={self.target_id!r}, " - f"longitude={self.longitude!r}, " - f"latitude={self.latitude!r}, " - f"altitude={self.altitude!r}, " - f"heading={self.heading!r}, " - f"tilt={self.tilt!r}, " - f"altitude_mode={self.altitude_mode}, " - f"**{self._get_splat()!r}," - ")" - ) - registry.register( _AbstractView, diff --git a/tests/geometries/coordinates_test.py b/tests/geometries/coordinates_test.py index 0a464551..15eecebb 100644 --- a/tests/geometries/coordinates_test.py +++ b/tests/geometries/coordinates_test.py @@ -63,6 +63,17 @@ def test_coordinates_from_string_with_whitespace(self) -> None: (-123.940449937288, 49.16927524669021, 17.0), ] + def test_coordinates_from_string_parse_error_relaxed(self) -> None: + """Test the from_string method with a parse error.""" + coordinates = Coordinates.from_string( + '' + "0.a,b.000000 1.0,1.0" + "", + strict=False, + ) + + assert not coordinates.coords + class TestCoordinatesLxml(Lxml, TestCoordinates): pass diff --git a/tests/geometries/linestring_test.py b/tests/geometries/linestring_test.py index 13e60b76..44d4e059 100644 --- a/tests/geometries/linestring_test.py +++ b/tests/geometries/linestring_test.py @@ -40,6 +40,12 @@ def test_init(self) -> None: assert line_string.altitude_mode is None assert line_string.extrude is None + def test_unequal_coordinates(self) -> None: + coords = Coordinates() + lines = LineString() + + assert lines != coords + def test_geometry_error(self) -> None: """Test GeometryError.""" p = geo.LineString(((1, 2), (2, 0))) diff --git a/tests/geometries/point_test.py b/tests/geometries/point_test.py index 32fb08df..ca2c782c 100644 --- a/tests/geometries/point_test.py +++ b/tests/geometries/point_test.py @@ -151,6 +151,13 @@ def test_to_string_3d_precision_10(self) -> None: in point.to_string(precision=10) ) + def test_point_unequal(self) -> None: + """Test the __eq__ method.""" + p = geo.Point(1, 2) + point = Point(geometry=p) + + assert point != p + def test_to_string_empty_geometry(self) -> None: """Test the to_string method.""" point = Point(geometry=geo.Point(None, None)) # type: ignore[arg-type] diff --git a/tests/geometries/polygon_test.py b/tests/geometries/polygon_test.py index ed9ee2e1..4f31d72e 100644 --- a/tests/geometries/polygon_test.py +++ b/tests/geometries/polygon_test.py @@ -31,6 +31,22 @@ class TestStdLibrary(StdLibrary): """Test with the standard library.""" + def test_equal(self) -> None: + """Test equality.""" + poly = geo.Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) + + polygon1 = Polygon(ns="", geometry=poly) + polygon2 = Polygon(ns="", geometry=poly) + + assert polygon1 == polygon2 + + def test_not_equal(self) -> None: + """Test inequality.""" + polygon = Polygon(ns="") + boundary = OuterBoundaryIs(ns="") + + assert polygon != boundary + def test_exterior_only(self) -> None: """Test exterior only.""" poly = geo.Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) diff --git a/tests/validator_test.py b/tests/validator_test.py index 2d7ac834..639abce7 100644 --- a/tests/validator_test.py +++ b/tests/validator_test.py @@ -21,6 +21,7 @@ import pytest from fastkml import atom +from fastkml import config from fastkml.validator import get_schema_parser from fastkml.validator import validate from tests.base import Lxml @@ -105,3 +106,18 @@ def test_validate_invalid_element(self) -> None: with pytest.raises(AssertionError): validate(element=link.etree_element()) + + def test_get_schema_parser(self) -> None: + path = TEST_DIR / "ogc_conformance" / "data" / "atom-author-link.xsd" + assert get_schema_parser(path) + + def test_validate_empty_element(self) -> None: + element = config.etree.Element("kml") + with pytest.raises( + AssertionError, + match=( + "^Element 'kml': " + "No matching global declaration available for the validation root.$" + ), + ): + assert validate(element=element) diff --git a/tox.ini b/tox.ini index 4fb8dc1a..3456d188 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,7 @@ per-file-ignores = examples/*.py: DALL fastkml/gx.py: LIT002 fastkml/views.py: LIT002 + fastkml/utils.py: CCR001 fastkml/registry.py: E704 docs/conf.py: E402 enable-extensions=G From 8c23efebc00c2eb4c60e2a6ed1fcdec1586fcc44 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 13 Nov 2024 18:10:28 +0000 Subject: [PATCH 03/10] add xsd for tests --- .../ogc_conformance/data/atom-author-link.xsd | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tests/ogc_conformance/data/atom-author-link.xsd diff --git a/tests/ogc_conformance/data/atom-author-link.xsd b/tests/ogc_conformance/data/atom-author-link.xsd new file mode 100644 index 00000000..b3d77ade --- /dev/null +++ b/tests/ogc_conformance/data/atom-author-link.xsd @@ -0,0 +1,66 @@ + + + + + atom-author-link.xsd 2008-01-23 + There is no official atom XSD. This XSD is created based on: + http://atompub.org/2005/08/17/atom.rnc. A subset of Atom as used in the + ogckml22.xsd is defined here. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 104aa6d584c922642ec0ae55191b6d37a1319637 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 13 Nov 2024 18:56:22 +0000 Subject: [PATCH 04/10] additional tests --- fastkml/helpers.py | 19 +++++++++---------- tests/repr_eq_test.py | 19 ++----------------- tests/styles_test.py | 25 +++++++++++++++++++++++++ tests/times_test.py | 11 +++++++++++ 4 files changed, 47 insertions(+), 27 deletions(-) diff --git a/fastkml/helpers.py b/fastkml/helpers.py index 2f121b66..91fdd273 100644 --- a/fastkml/helpers.py +++ b/fastkml/helpers.py @@ -1100,16 +1100,15 @@ def datetime_subelement_kwarg( return {} node_text = node.text.strip() if node.text else "" if node_text: - try: - return {kwarg: cls.parse(node_text)} # type: ignore[attr-defined] - except ValueError as exc: - handle_error( - error=exc, - strict=strict, - element=element, - node=node, - expected="DateTime", - ) + if kdt := cls.parse(node_text): # type: ignore[attr-defined] + return {kwarg: kdt} + handle_error( + error=ValueError(f"Invalid DateTime value: {node_text}"), + strict=strict, + element=element, + node=node, + expected="DateTime", + ) return {} diff --git a/tests/repr_eq_test.py b/tests/repr_eq_test.py index 3f56a6db..8297258f 100644 --- a/tests/repr_eq_test.py +++ b/tests/repr_eq_test.py @@ -16,8 +16,6 @@ """Test the __repr__ and __eq__ methods.""" -import difflib -from textwrap import wrap from typing import Final from pygeoif.geometry import LinearRing @@ -1903,19 +1901,6 @@ class TestRepr(StdLibrary): ], ) - def diff_compare(self, a: str, b: str) -> None: - """Compare two strings and print the differences.""" - differ = difflib.Differ() - for line, d in enumerate(differ.compare(a.split(), b.split())): - if d[0] in ("+", "-"): - print(line, d) # noqa: T201 - - for i, chunk in enumerate(zip(wrap(a, 100), wrap(b, 100))): - if chunk[0] != chunk[1]: - print(i * 100) # noqa: T201 - print(chunk[0]) # noqa: T201 - print(chunk[1]) # noqa: T201 - def test_repr(self) -> None: """Test the __repr__ method.""" new_doc = eval(repr(self.clean_doc), {}, eval_locals) # noqa: S307 @@ -1929,10 +1914,10 @@ def test_str(self) -> None: def test_eq_str_round_trip(self) -> None: """Test the equality of the original and the round-tripped document.""" - new_doc = fastkml.KML.from_string(self.clean_doc.to_string(precision=15)) + new_doc = fastkml.KML.from_string(self.clean_doc.to_string()) assert str(self.clean_doc) == str(new_doc) - assert repr(new_doc) == repr(self.clean_doc) + assert new_doc == self.clean_doc # srict equality is not a given new_doc == self.clean_doc diff --git a/tests/styles_test.py b/tests/styles_test.py index 67c73be1..14ae387d 100644 --- a/tests/styles_test.py +++ b/tests/styles_test.py @@ -63,6 +63,7 @@ def test_icon_style(self) -> None: serialized = icons.to_string() + assert icons.icon_href == "http://example.com/icon.png" assert ' None: assert "" in serialized assert "" in serialized + def test_icon_style_icon_href(self) -> None: + icons = styles.IconStyle( + icon_href="http://example.com/icon.png", + ) + + assert icons.icon + assert icons.icon.href == "http://example.com/icon.png" + + def test_icon_style_icon_and_href(self) -> None: + icons = styles.IconStyle( + icon=links.Icon(href="http://example1.com/icon.png"), + icon_href="http://example2.com/icon.png", + ) + + assert icons.icon + assert icons.icon.href == "http://example1.com/icon.png" + def test_icon_style_with_hot_spot(self) -> None: icon_style = styles.IconStyle( ns="{http://www.opengis.net/kml/2.2}", @@ -131,6 +149,8 @@ def test_icon_style_read(self) -> None: assert icons.hot_spot assert icons.hot_spot.x == 0.5 assert icons.hot_spot.y == 0.7 + assert icons.hot_spot.xunits + assert icons.hot_spot.yunits assert icons.hot_spot.xunits.value == "fraction" assert icons.hot_spot.yunits.value == "insetPixels" @@ -145,6 +165,10 @@ def test_icon_style_with_hot_spot_enum_relaxed(self) -> None: "", strict=False, ) + + assert icons.hot_spot + assert icons.hot_spot.xunits + assert icons.hot_spot.yunits assert icons.hot_spot.xunits.value == "fraction" assert icons.hot_spot.yunits.value == "insetPixels" @@ -422,6 +446,7 @@ def test_style_read(self) -> None: assert style.styles[0].color_mode == ColorMode.random assert style.styles[0].scale == 1.0 assert style.styles[0].heading == 0 + assert style.styles[0].icon assert style.styles[0].icon.href == "http://example.com/icon.png" assert isinstance(style.styles[1], styles.LabelStyle) diff --git a/tests/times_test.py b/tests/times_test.py index 53306819..402aa2ef 100644 --- a/tests/times_test.py +++ b/tests/times_test.py @@ -337,6 +337,17 @@ def test_read_timestamp_ymd(self) -> None: assert ts.timestamp.resolution == DateTimeResolution.date assert ts.timestamp.dt == datetime.date(1997, 7, 16) + def test_read_timestamp_invalid(self) -> None: + doc = """ + + jan1997 + + """ + + ts = kml.TimeStamp.from_string(doc, ns="", strict=False) + + assert ts.timestamp is None + 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 From 346e676b4f0f0b70c53462d9bf8db23597ee02fc Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 13 Nov 2024 19:11:59 +0000 Subject: [PATCH 05/10] additional tests - 100% --- tests/hypothesis/common.py | 4 ++-- tests/hypothesis/multi_geometry_test.py | 4 ++-- tests/times_test.py | 11 +++++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/hypothesis/common.py b/tests/hypothesis/common.py index c76d8420..2cf23c34 100644 --- a/tests/hypothesis/common.py +++ b/tests/hypothesis/common.py @@ -81,9 +81,9 @@ def assert_repr_roundtrip(obj: _XMLObject) -> None: """Test that repr(obj) can be eval'd back to obj.""" try: assert obj == eval(repr(obj), {}, eval_locals) # noqa: S307 - except FileNotFoundError: + except FileNotFoundError: # pragma: no cover # The timezone file may not be available on all systems. - logger.exception("Failed to eval repr(obj).") + logger.exception("Failed to eval repr(obj).") # pragma: no cover def assert_str_roundtrip(obj: _XMLObject) -> None: diff --git a/tests/hypothesis/multi_geometry_test.py b/tests/hypothesis/multi_geometry_test.py index f32935c4..a9cb80ca 100644 --- a/tests/hypothesis/multi_geometry_test.py +++ b/tests/hypothesis/multi_geometry_test.py @@ -166,8 +166,8 @@ def _test_geometry_str_roundtrip_verbose( assert geometry.geometry assert type(new_g.geometry) is cls for new, orig in zip(new_g.kml_geometries, geometry.kml_geometries): - if isinstance(new, fastkml.geometry.MultiGeometry): - continue + if isinstance(new, fastkml.geometry.MultiGeometry): # pragma: no cover + continue # pragma: no cover assert not isinstance(orig, fastkml.geometry.MultiGeometry) if extrude: assert new.extrude == orig.extrude == extrude diff --git a/tests/times_test.py b/tests/times_test.py index 402aa2ef..3b52e9ff 100644 --- a/tests/times_test.py +++ b/tests/times_test.py @@ -348,6 +348,17 @@ def test_read_timestamp_invalid(self) -> None: assert ts.timestamp is None + def test_read_timestamp_empty(self) -> None: + doc = """ + + + + """ + + ts = kml.TimeStamp.from_string(doc, ns="") + + assert ts.timestamp is None + 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 From 2c17ee0fa57f5ed3c6d52176adfca534c3eb0661 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 13 Nov 2024 19:14:44 +0000 Subject: [PATCH 06/10] remove introspect.py --- introspect.py | 181 -------------------------------------------------- 1 file changed, 181 deletions(-) delete mode 100644 introspect.py diff --git a/introspect.py b/introspect.py deleted file mode 100644 index 92d8c09b..00000000 --- a/introspect.py +++ /dev/null @@ -1,181 +0,0 @@ -"""Introspection utilities.""" - -# flake8: noqa -import collections.abc -import enum -import inspect -import typing -from types import ModuleType - -from fastkml.base import _XMLObject - - -def get_init_args_with_annotations(cls: type): # type: ignore[no-untyped-def] - """Get the arguments of a class's __init__ method with type annotations.""" - subclass_init_signature = inspect.signature(cls.__init__) # type: ignore[misc] - base_init_signature = inspect.signature(cls.__bases__[-1].__init__) # type: ignore[misc] - - # Extract parameter names and type annotations - base_init_params = { - param.name: param.annotation - for param in base_init_signature.parameters.values() - } - subclass_init_params = { - param.name: param.annotation - for param in subclass_init_signature.parameters.values() - } - - # Exclude arguments of baseclass from subclass - subclass_init_params = { - k: v for k, v in subclass_init_params.items() if k not in base_init_params - } - - return subclass_init_params - - -def extract_types(annotation: typing.Type): # type: ignore[type-arg,no-untyped-def] - try: - origin = annotation.__origin__ - except AttributeError: - return annotation - if origin is typing.Union: - return tuple( - t - for t in ( - extract_types(subtype) - for subtype in annotation.__args__ - if subtype is not None - ) - if t is not type(None) - ) - elif origin is typing.Iterable or origin is collections.abc.Iterable: - iterables = extract_types(annotation.__args__[0]) - return ( - {"Iterable": iterables} - if type(iterables) is tuple - else {"Iterable": (iterables,)} - ) - elif origin is typing.Optional: - return extract_types(annotation.__args__[0]) - - -def get_type_hints(cls: typing.Type): # type: ignore[type-arg,no-untyped-def] - """Get the type hints for a class.""" - return {k: extract_types(v) for k, v in get_init_args_with_annotations(cls).items()} - - -def is_class_in_module(cls: type, module: ModuleType) -> bool: - """ - Check if a class is defined in a specific module. - - Args: - ---- - cls (type): The class to check. - module (ModuleType): The module to compare against. - - Returns: - ------- - bool: True if the class is defined in the specified module, - False if it is imported. - """ - return inspect.getmodule(cls) == module - - -def get_classes_in_module(module: ModuleType) -> typing.List[type]: - """ - Get all classes defined in a module. - - Args: - ---- - module (ModuleType): The module to check. - - Returns: - ------- - typing.List[type]: A list of classes defined in the module. - """ - return [ - cls - for name, cls in inspect.getmembers(module, inspect.isclass) - if is_class_in_module(cls, module) - ] - - -def create_registry(module: ModuleType) -> None: - """ - Create a registry of classes defined in a module. - - Args: - ---- - module (ModuleType): The module to check. - - """ - attr_cls_dict = { - bool: { - "get_kwarg": "subelement_bool_kwarg", - "set_element": "bool_subelement", - }, - str: { - "get_kwarg": "subelement_text_kwarg", - "set_element": "text_subelement", - }, - int: { - "get_kwarg": "subelement_int_kwarg", - "set_element": "int_subelement", - }, - float: { - "get_kwarg": "subelement_float_kwarg", - "set_element": "float_subelement", - }, - } - print("from fastkml.registry import registry, RegistryItem") - print() - for cls in get_classes_in_module(module): - if not issubclass(cls, _XMLObject): - continue - print(f"### {cls.__name__} ###") - hints = get_type_hints(cls) - for k, v in hints.items(): - print("registry.register(") - print(f" {cls.__name__},") - print(" RegistryItem(") - print(f" attr_name='{k}',") - print(f" node_name='${k},") - if isinstance(v[0], dict): - try: - classes = ", ".join([c.__name__ for c in v[0]["Iterable"]]) - except AttributeError: - print(f"# XXX: Error in {cls.__name__}: {k} > {v}") - print(f" classes=({classes},),") - print(" get_kwarg=xml_subelement_list_kwarg,") - print(" set_element=xml_subelement_list,") - - print(" ),") - print(")") - continue - try: - classes = ", ".join([c.__name__ for c in v]) - print(f" classes=({classes},),") - except AttributeError: - print(f"# XXX: Error in {cls.__name__}: {k} > {v}") - try: - for k, v in attr_cls_dict[v[0]].items(): - print(f" {k}={v},") - except KeyError: - if issubclass(v[0], enum.Enum): - print(" get_kwarg=subelement_enum_kwarg,") - print(" set_element=enum_subelement,") - elif issubclass(v[0], _XMLObject): - print(" get_kwarg=xml_subelement_kwarg,") - print(" set_element=xml_subelement,") - else: - print(f"# XXX Need to add support for {v[0]}") - - print(" ),") - print(")") - print() - - -if __name__ == "__main__": - from fastkml import data - - create_registry(data) From 38b92035808c8a535655cd4dad58bbd52e696f3d Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 13 Nov 2024 19:26:16 +0000 Subject: [PATCH 07/10] fix partial coverage --- tests/kml_test.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/kml_test.py b/tests/kml_test.py index bf74b9f5..c6be2344 100644 --- a/tests/kml_test.py +++ b/tests/kml_test.py @@ -232,6 +232,15 @@ def test_from_string_with_unbound_prefix_strict_no_validate(self) -> None: assert len(k.features) == 1 assert isinstance(k.features[0], features.Placemark) + def test_from_string_no_namespace(self) -> None: + doc = io.StringIO( + "", + ) + + k = kml.KML.parse(doc, ns="", strict=False) + + assert len(k.features) == 0 + class TestKmlFromString: def test_document(self) -> None: From 0a21182c0fcc1e1a30a7260c42503183122c7e9a Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 13 Nov 2024 19:40:39 +0000 Subject: [PATCH 08/10] do not add none trackitems to the multitrack --- tests/hypothesis/gx_test.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/hypothesis/gx_test.py b/tests/hypothesis/gx_test.py index dcafb9a4..e1161317 100644 --- a/tests/hypothesis/gx_test.py +++ b/tests/hypothesis/gx_test.py @@ -77,11 +77,8 @@ def test_fuzz_track_track_items( st.none(), st.sampled_from(fastkml.enums.AltitudeMode), ), - track_items=st.one_of( - st.none(), - st.lists( - track_items(), - ), + track_items=st.lists( + track_items(), ), ), ), From 7d8867c4edc054a6b1ed02c7771396b17173e0d1 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 13 Nov 2024 20:19:52 +0000 Subject: [PATCH 09/10] refactor datetime_subelement_kwarg return type for clarity --- fastkml/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastkml/helpers.py b/fastkml/helpers.py index 91fdd273..1b3d582e 100644 --- a/fastkml/helpers.py +++ b/fastkml/helpers.py @@ -1092,7 +1092,7 @@ def datetime_subelement_kwarg( kwarg: str, classes: Tuple[Type[object], ...], strict: bool, -) -> Dict[str, List["KmlDateTime"]]: +) -> Dict[str, "KmlDateTime"]: """Extract a KML datetime from a subelement of an XML element.""" cls = classes[0] node = element.find(f"{ns}{node_name}") From e8302d0f83e56263c143d2972cb696020825a525 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 13 Nov 2024 20:21:14 +0000 Subject: [PATCH 10/10] bump version to 1.0.0rc2 --- fastkml/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastkml/about.py b/fastkml/about.py index da54aaa9..578573fc 100644 --- a/fastkml/about.py +++ b/fastkml/about.py @@ -19,6 +19,6 @@ The only purpose of this module is to provide a version number for the package. """ -__version__ = "1.0.0rc1" +__version__ = "1.0.0rc2" __all__ = ["__version__"]