diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml
index c9350224..a3c508a6 100644
--- a/.github/workflows/run-all-tests.yml
+++ b/.github/workflows/run-all-tests.yml
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12-dev']
+ python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v3.2.0
@@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
+ python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v3.2.0
@@ -85,7 +85,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- pypy-version: ['pypy-3.7', 'pypy-3.8', 'pypy-3.9']
+ pypy-version: ['pypy-3.7', 'pypy-3.8', 'pypy-3.9', 'pypy-3.10']
steps:
- uses: actions/checkout@v3.2.0
- name: Set up Python ${{ matrix.pypy-version }}
diff --git a/.gitignore b/.gitignore
index 45d354fa..7a8a7bd2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,10 +23,13 @@ setup.cfg
# Installer logs
pip-log.txt
-# Unit test / coverage reports
+# Unit/static test / coverage reports
.coverage
.tox
+.pytest_cache/
nosetests.xml
+.ruff_cache/
+monkeytype.sqlite3
# Translations
*.mo
@@ -51,6 +54,8 @@ venv
.mypy_cache/
.pyre/
.watchmanconfig
+.pytype/
+
# misc
.dccache
diff --git a/fastkml/enums.py b/fastkml/enums.py
index 71502458..8c1ab589 100644
--- a/fastkml/enums.py
+++ b/fastkml/enums.py
@@ -17,6 +17,7 @@
from enum import unique
+@unique
class Verbosity(Enum):
"""Enum to represent the different verbosity levels."""
diff --git a/fastkml/geometry.py b/fastkml/geometry.py
index 3e252f6f..35966899 100644
--- a/fastkml/geometry.py
+++ b/fastkml/geometry.py
@@ -931,24 +931,6 @@ def __init__(
geometry=geometry,
)
- @classmethod
- def _get_geometry(
- cls,
- *,
- ns: str,
- element: Element,
- strict: bool,
- ) -> geo.LinearRing:
- coords = cls._get_coordinates(ns=ns, element=element, strict=strict)
- try:
- return cast(geo.LinearRing, geo.LinearRing.from_coordinates(coords))
- except (IndexError, TypeError) as e:
- error = config.etree.tostring( # type: ignore[attr-defined]
- element,
- encoding="UTF-8",
- ).decode("UTF-8")
- raise KMLParseError(f"Invalid coordinates in {error}") from e
-
def etree_element(
self,
precision: Optional[int] = None,
@@ -986,13 +968,7 @@ def etree_element(
return element
@classmethod
- def _get_polygon_kwargs(
- cls,
- *,
- ns: str,
- element: Element,
- strict: bool,
- ) -> Dict[str, Any]:
+ def _get_geometry(cls, *, ns: str, element: Element, strict: bool) -> geo.Polygon:
outer_boundary = element.find(f"{ns}outerBoundaryIs")
if outer_boundary is None:
error = config.etree.tostring( # type: ignore[attr-defined]
@@ -1007,7 +983,7 @@ def _get_polygon_kwargs(
encoding="UTF-8",
).decode("UTF-8")
raise KMLParseError(f"Missing LinearRing in {error}")
- exterior = cls._get_geometry(ns=ns, element=outer_ring, strict=strict)
+ exterior = LinearRing._get_geometry(ns=ns, element=outer_ring, strict=strict)
interiors = []
for inner_boundary in element.findall(f"{ns}innerBoundaryIs"):
inner_ring = inner_boundary.find(f"{ns}LinearRing")
@@ -1018,9 +994,19 @@ def _get_polygon_kwargs(
).decode("UTF-8")
raise KMLParseError(f"Missing LinearRing in {error}")
interiors.append(
- cls._get_geometry(ns=ns, element=inner_ring, strict=strict)
+ LinearRing._get_geometry(ns=ns, element=inner_ring, strict=strict)
)
- return {"geometry": geo.Polygon.from_linear_rings(exterior, *interiors)}
+ return geo.Polygon.from_linear_rings(exterior, *interiors)
+
+ @classmethod
+ def _get_polygon_kwargs(
+ cls,
+ *,
+ ns: str,
+ element: Element,
+ strict: bool,
+ ) -> Dict[str, Any]:
+ return {"geometry": cls._get_geometry(ns=ns, element=element, strict=strict)}
@classmethod
def _get_kwargs(
@@ -1035,12 +1021,43 @@ def _get_kwargs(
return kwargs
+def create_multigeometry(
+ geometries: Sequence[AnyGeometryType],
+) -> Optional[MultiGeometryType]:
+ """Create a MultiGeometry from a sequence of geometries.
+
+ Args:
+ geometries: Sequence of geometries.
+
+ Returns:
+ MultiGeometry
+
+ """
+ geom_types = {geom.geom_type for geom in geometries}
+ if not geom_types:
+ return None
+ if len(geom_types) == 1:
+ geom_type = geom_types.pop()
+ map_to_geometries = {
+ geo.Point.__name__: geo.MultiPoint.from_points,
+ geo.LineString.__name__: geo.MultiLineString.from_linestrings,
+ geo.Polygon.__name__: geo.MultiPolygon.from_polygons,
+ }
+ for geometry_name, constructor in map_to_geometries.items():
+ if geom_type == geometry_name:
+ return constructor( # type: ignore[operator, no-any-return]
+ *geometries,
+ )
+
+ return geo.GeometryCollection(geometries)
+
+
class MultiGeometry(_Geometry):
map_to_kml = {
geo.Point: Point,
geo.LineString: LineString,
- geo.LinearRing: LinearRing,
geo.Polygon: Polygon,
+ geo.LinearRing: LinearRing,
}
multi_geometries = (
geo.MultiPoint,
@@ -1094,6 +1111,19 @@ def etree_element(
)
return element
+ @classmethod
+ def _get_geometry(
+ cls, *, ns: str, element: Element, strict: bool
+ ) -> Optional[MultiGeometryType]:
+ geometries = []
+ allowed_geometries = (cls,) + tuple(cls.map_to_kml.values())
+ for g in allowed_geometries:
+ for e in element.findall(f"{ns}{g.__name__}"):
+ geometry = g._get_geometry(ns=ns, element=e, strict=strict)
+ if geometry is not None:
+ geometries.append(geometry)
+ return create_multigeometry(geometries)
+
@classmethod
def _get_multigeometry_kwargs(
cls,
@@ -1102,16 +1132,7 @@ def _get_multigeometry_kwargs(
element: Element,
strict: bool,
) -> Dict[str, Any]:
- geometries = []
- allowed_geometries = (cls,) + tuple(cls.map_to_kml.values())
- for g in allowed_geometries:
- for e in element.findall(f"{ns}{g.__name__}"):
- geometry = g._get_geometry( # type: ignore[attr-defined]
- ns=ns, element=e, strict=strict
- )
- if geometry is not None:
- geometries.append(geometry)
- return {"geometry": geo.GeometryCollection(geometries)}
+ return {"geometry": cls._get_geometry(ns=ns, element=element, strict=strict)}
@classmethod
def _get_kwargs(
diff --git a/pyproject.toml b/pyproject.toml
index 4ec400ee..c57a1b6a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -51,3 +51,4 @@ ignore_errors = true
[tool.ruff]
[tool.ruff.extend-per-file-ignores]
"tests/oldunit_test.py" = ["E501"]
+"setup.py" = ["E501"]
diff --git a/requirements.txt b/requirements.txt
index ad28f0e3..c7b6a580 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
# Base package requirements
-pygeoif>=1.0.0
+pygeoif>=1.1.0
python-dateutil
+typing_extensions
diff --git a/setup.py b/setup.py
index c33d5194..63cb1302 100644
--- a/setup.py
+++ b/setup.py
@@ -39,6 +39,7 @@ def run_tests(self) -> None:
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
# "Development Status :: 5 - Production/Stable",
@@ -58,7 +59,7 @@ def run_tests(self) -> None:
python_requires=">=3.7",
install_requires=[
# -*- Extra requirements: -*-
- "pygeoif>=1.0.0",
+ "pygeoif>=1.1.0",
"python-dateutil",
"setuptools",
"typing_extensions",
diff --git a/tests/geometries/multigeometry_test.py b/tests/geometries/multigeometry_test.py
index 2f37cc96..02046bef 100644
--- a/tests/geometries/multigeometry_test.py
+++ b/tests/geometries/multigeometry_test.py
@@ -46,6 +46,17 @@ def test_2_points(self):
assert "MultiGeometry>" in mg.to_string()
assert "Point>" in mg.to_string()
+ def test_2_points_read(self) -> None:
+ xml = (
+ "00"
+ "1.000000,2.000000"
+ "3.000000,4.000000"
+ )
+
+ mg = MultiGeometry.class_from_string(xml, ns="")
+
+ assert mg.geometry == geo.MultiPoint([(1, 2), (3, 4)])
+
class TestMultiLineStringStdLibrary(StdLibrary):
def test_1_linestring(self):
@@ -69,6 +80,19 @@ def test_2_linestrings(self):
assert "MultiGeometry>" in mg.to_string()
assert "LineString>" in mg.to_string()
+ def test_2_linestrings_read(self) -> None:
+ xml = (
+ "00"
+ "1.000000,2.000000 3.000000,4.000000"
+ ""
+ "5.000000,6.000000 7.000000,8.000000"
+ ""
+ )
+
+ mg = MultiGeometry.class_from_string(xml, ns="")
+
+ assert mg.geometry == geo.MultiLineString([[(1, 2), (3, 4)], [(5, 6), (7, 8)]])
+
class TestMultiPolygonStdLibrary(StdLibrary):
def test_1_polygon(self):
@@ -142,6 +166,35 @@ def test_2_polygons(self):
assert "outerBoundaryIs>" in mg.to_string()
assert "innerBoundaryIs>" in mg.to_string()
+ def test_2_polygons_read(self) -> None:
+ xml = (
+ "00"
+ ""
+ "0.000000,0.000000 0.000000,1.000000 1.000000,1.000000 "
+ "1.000000,0.000000 0.000000,0.000000"
+ ""
+ ""
+ "0.100000,0.100000 0.100000,0.200000 0.200000,0.200000 "
+ "0.200000,0.100000 0.100000,0.100000"
+ ""
+ ""
+ "0.000000,0.000000 0.000000,2.000000 1.000000,1.000000 "
+ "1.000000,0.000000 0.000000,0.000000"
+ ""
+ )
+
+ mg = MultiGeometry.class_from_string(xml, ns="")
+
+ assert mg.geometry == geo.MultiPolygon(
+ [
+ (
+ ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)),
+ [((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1))],
+ ),
+ (((0.0, 0.0), (0.0, 2.0), (1.0, 1.0), (1.0, 0.0)),),
+ ],
+ )
+
class TestGeometryCollectionStdLibrary(StdLibrary):
"""Test heterogeneous geometry collections."""
@@ -202,6 +255,96 @@ def test_multi_geometries(self):
assert "Polygon>" in mg.to_string()
assert "MultiGeometry>" in mg.to_string()
+ def test_multi_geometries_read(self) -> None:
+ xml = (
+ "00"
+ "1.000000,2.000000"
+ "1.000000,2.000000 2.000000,0.000000"
+ "0.000000,0.000000 0.000000,1.000000 "
+ "1.000000,1.000000 1.000000,0.000000 0.000000,0.000000"
+ ""
+ "0.000000,0.000000 0.000000,1.000000 1.000000,1.000000 "
+ "1.000000,0.000000 0.000000,0.000000"
+ ""
+ "0.100000,0.100000 0.100000,0.900000 0.900000,0.900000 "
+ "0.900000,0.100000 0.100000,0.100000"
+ ""
+ ""
+ "0.000000,0.000000 0.000000,1.000000 1.000000,1.000000 "
+ "1.000000,0.000000 0.000000,0.000000"
+ ""
+ "0.100000,0.100000 0.100000,0.200000 0.200000,0.200000 "
+ "0.200000,0.100000 0.100000,0.100000"
+ ""
+ ""
+ "0.000000,0.000000 0.000000,2.000000 1.000000,1.000000 "
+ "1.000000,0.000000 0.000000,0.000000"
+ ""
+ "1.000000,2.000000 3.000000,4.000000"
+ "5.000000,6.000000 7.000000,8.000000"
+ ""
+ )
+
+ mg = MultiGeometry.class_from_string(xml, ns="")
+
+ assert mg.geometry == geo.GeometryCollection(
+ (
+ geo.GeometryCollection(
+ (
+ geo.Point(1.0, 2.0),
+ geo.LineString(((1.0, 2.0), (2.0, 0.0))),
+ geo.Polygon(
+ (
+ (0.0, 0.0),
+ (0.0, 1.0),
+ (1.0, 1.0),
+ (1.0, 0.0),
+ (0.0, 0.0),
+ ),
+ (
+ (
+ (0.1, 0.1),
+ (0.1, 0.9),
+ (0.9, 0.9),
+ (0.9, 0.1),
+ (0.1, 0.1),
+ ),
+ ),
+ ),
+ geo.LinearRing(
+ ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0))
+ ),
+ )
+ ),
+ geo.MultiPolygon(
+ (
+ (
+ (
+ (0.0, 0.0),
+ (0.0, 1.0),
+ (1.0, 1.0),
+ (1.0, 0.0),
+ (0.0, 0.0),
+ ),
+ (
+ (
+ (0.1, 0.1),
+ (0.1, 0.2),
+ (0.2, 0.2),
+ (0.2, 0.1),
+ (0.1, 0.1),
+ ),
+ ),
+ ),
+ (((0.0, 0.0), (0.0, 2.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)),),
+ )
+ ),
+ geo.MultiLineString(
+ (((1.0, 2.0), (3.0, 4.0)), ((5.0, 6.0), (7.0, 8.0)))
+ ),
+ )
+ )
+
class TestMultiPointLxml(Lxml, TestMultiPointStdLibrary):
"""Test with lxml."""