From 00a014c9ed7c92d2944049189e5f8a94dcff192b Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 25 Mar 2024 18:00:15 +0000 Subject: [PATCH 01/24] back to develop --- docs/HISTORY.rst | 4 ++++ pygeoif/about.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.rst b/docs/HISTORY.rst index 23935c1..7203403 100644 --- a/docs/HISTORY.rst +++ b/docs/HISTORY.rst @@ -1,6 +1,10 @@ Changelog ========= +1.5.0 (unreleased) +------------------ + + 1.4.0 (2024/03/25) ------------------ diff --git a/pygeoif/about.py b/pygeoif/about.py index e274e4d..6cbc91e 100644 --- a/pygeoif/about.py +++ b/pygeoif/about.py @@ -4,4 +4,4 @@ The only purpose of this module is to provide a version number for the package. """ -__version__ = "1.4.0" +__version__ = "1.5.0" From 4cc669e8d23dc75f2d433eeff14345dc85bbfc4d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 16:35:58 +0000 Subject: [PATCH 02/24] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.28.0 → 0.28.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.28.0...0.28.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6a971e4..67b5c22 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -86,7 +86,7 @@ repos: hooks: - id: pyprojectsort - repo: https://github.com/python-jsonschema/check-jsonschema - rev: "0.28.0" + rev: "0.28.1" hooks: - id: check-github-workflows - id: check-github-actions From 4bde42697a51425bc193f25a3a878dc21397a545 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 16:38:19 +0000 Subject: [PATCH 03/24] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0) - [github.com/astral-sh/ruff-pre-commit: v0.3.4 → v0.3.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.4...v0.3.5) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 67b5c22..d5d8095 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: hooks: - id: check-useless-excludes - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: check-added-large-files - id: check-docstring-first @@ -40,7 +40,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.3.4' + rev: 'v0.3.5' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From a065942e463d98784c4d78bdfb3afd4ac6645e2b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 16:37:13 +0000 Subject: [PATCH 04/24] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 24.3.0 → 24.4.0](https://github.com/psf/black/compare/24.3.0...24.4.0) - [github.com/astral-sh/ruff-pre-commit: v0.3.5 → v0.3.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.5...v0.3.7) - [github.com/python-jsonschema/check-jsonschema: 0.28.1 → 0.28.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.28.1...0.28.2) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d5d8095..9beeb55 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,11 +36,11 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black - rev: 24.3.0 + rev: 24.4.0 hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.3.5' + rev: 'v0.3.7' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] @@ -86,7 +86,7 @@ repos: hooks: - id: pyprojectsort - repo: https://github.com/python-jsonschema/check-jsonschema - rev: "0.28.1" + rev: "0.28.2" hooks: - id: check-github-workflows - id: check-github-actions From 6eb3bf19beee1bb675d57b261df132e96d8293a6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:35:24 +0000 Subject: [PATCH 05/24] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.3.7 → v0.4.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.7...v0.4.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9beeb55..81f2011 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.3.7' + rev: 'v0.4.1' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From b8d6596751697caa09c34712dd3bda620047fa2e Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 27 Apr 2024 14:50:03 +0100 Subject: [PATCH 06/24] refactor handling of empty points --- README.rst | 6 +++--- pygeoif/functions.py | 4 ++-- pygeoif/geometry.py | 20 +++++++++----------- pyproject.toml | 2 ++ tests/test_factories.py | 14 ++++++++------ tests/test_line.py | 7 +++++++ tests/test_multipoint.py | 6 ++++-- 7 files changed, 35 insertions(+), 24 deletions(-) diff --git a/README.rst b/README.rst index c83934b..ec556d8 100644 --- a/README.rst +++ b/README.rst @@ -50,9 +50,9 @@ It was written to provide clean and python only geometries for fastkml_ :target: https://codecov.io/gh/cleder/pygeoif :alt: Codecov -.. image:: https://img.shields.io/badge/property_based_tests-hypothesis-green - :target: https://hypothesis.works - :alt: Hypothesis +.. image:: https://img.shields.io/badge/hypothesis-tested-brightgreen.svg + :alt: Tested with Hypothesis + :target: https://hypothesis.readthedocs.io .. image:: https://img.shields.io/badge/code_style-black-000000.svg :target: https://github.com/psf/ diff --git a/pygeoif/functions.py b/pygeoif/functions.py index 61a9a1c..276dd6a 100644 --- a/pygeoif/functions.py +++ b/pygeoif/functions.py @@ -68,8 +68,8 @@ def centroid(coords: LineType) -> Tuple[Point2D, float]: ans[0] += (coord[0] + next_coord[0]) * area ans[1] += (coord[1] + next_coord[1]) * area - ans[0] = (ans[0]) / (3 * signed_area) - ans[1] = (ans[1]) / (3 * signed_area) + ans[0] = ans[0] / (3 * signed_area) + ans[1] = ans[1] / (3 * signed_area) return cast(Point2D, tuple(ans)), signed_area / 2.0 diff --git a/pygeoif/geometry.py b/pygeoif/geometry.py index c500ebb..06222f6 100644 --- a/pygeoif/geometry.py +++ b/pygeoif/geometry.py @@ -248,21 +248,17 @@ def __init__(self, x: float, y: float, z: Optional[float] = None) -> None: Easting, northing, and elevation. """ + geoms = (x, y, z) if z is not None else (x, y) object.__setattr__( self, "_geoms", - cast( - PointType, - tuple( - coordinate - for coordinate in (x, y, z) - if coordinate is not None and not math.isnan(coordinate) - ), - ), + geoms, ) def __repr__(self) -> str: """Return the representation.""" + if self.is_empty: + return f"{self.geom_type}()" return f"{self.geom_type}{self._geoms}" @property @@ -270,9 +266,9 @@ def is_empty(self) -> bool: """ Return if this geometry is empty. - A Point is considered empty when it has fewer than 2 coordinates. + A Point is considered empty when it has no valid coordinates. """ - return len(self._geoms) < 2 # noqa: PLR2004 + return any(coord is None or math.isnan(coord) for coord in self._geoms) @property def x(self) -> float: @@ -733,7 +729,9 @@ def __len__(self) -> int: def __repr__(self) -> str: """Return the representation.""" - return f"{self.geom_type}({tuple(geom.coords[0] for geom in self._geoms)})" + return ( + f"{self.geom_type}({tuple(geom.coords[0] for geom in self._geoms if geom)})" + ) @property def geoms(self) -> Iterator[Point]: diff --git a/pyproject.toml b/pyproject.toml index 5ada748..17a3994 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ authors = [ ] classifiers = [ "Development Status :: 5 - Production/Stable", + "Framework :: Hypothesis", "Intended Audience :: Developers", "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", "Operating System :: OS Independent", @@ -35,6 +36,7 @@ dynamic = [ ] keywords = [ "GIS", + "Hypothesis", "Spatial", "WKT", ] diff --git a/tests/test_factories.py b/tests/test_factories.py index cf2a437..1dbafe9 100644 --- a/tests/test_factories.py +++ b/tests/test_factories.py @@ -89,7 +89,7 @@ def test_force_2d_polygon() -> None: internal = [(0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)] p = geometry.Polygon(external, [internal]) p2d = factories.force_2d(p) - assert p2d.coords[0] == (((0, 0), (0, 2), (2, 2), (2, 0), (0, 0))) + assert p2d.coords[0] == ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)) assert p2d.coords[1] == ( ((0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)), ) @@ -107,7 +107,7 @@ def test_force_2d_polygon() -> None: p = geometry.Polygon(external, [internal]) p2d = factories.force_2d(p) - assert p2d.coords[0] == (((0, 0), (0, 2), (2, 2), (2, 0), (0, 0))) + assert p2d.coords[0] == ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)) assert p2d.coords[1] == ( ((0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)), ) @@ -261,8 +261,10 @@ class TestWKT: "POINT M (1 1 80)", "LINESTRING(3 4,10 50,20 25)", "LINESTRING (30 10, 10 30, 40 40)", - "MULTIPOLYGON (((10 10, 10 20, 20 20, 20 15, 10 10))," - "((60 60, 70 70, 80 60, 60 60 )))", + ( + "MULTIPOLYGON (((10 10, 10 20, 20 20, 20 15, 10 10))," + "((60 60, 70 70, 80 60, 60 60 )))" + ), """MULTIPOLYGON (((40 40, 20 45, 45 30, 40 40)), ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20)))""", @@ -385,8 +387,8 @@ def test_multilinestring(self) -> None: ) assert isinstance(p, geometry.MultiLineString) - assert next(iter(p.geoms)).coords == (((3, 4), (10, 50), (20, 25))) - assert list(p.geoms)[1].coords == (((-5, -8), (-10, -8), (-15, -4))) + assert next(iter(p.geoms)).coords == ((3, 4), (10, 50), (20, 25)) + assert list(p.geoms)[1].coords == ((-5, -8), (-10, -8), (-15, -4)) assert ( p.wkt == "MULTILINESTRING ((3 4, 10 50, " "20 25),(-5 -8, " diff --git a/tests/test_line.py b/tests/test_line.py index 0665a43..fff6b6b 100644 --- a/tests/test_line.py +++ b/tests/test_line.py @@ -1,5 +1,6 @@ """Test LineString.""" +import math from unittest import mock import pytest @@ -20,6 +21,12 @@ def test_coords_get_3d() -> None: assert line.coords == ((0.0, 0.0, 0), (1.0, 1.0, 1)) +def test_coords_get_nan() -> None: + line = geometry.LineString([(0, math.nan, 0), (1, 1, math.nan), (2, 2, 2)]) + + assert line.coords == ((2, 2, 2),) + + def test_empty_points_omitted() -> None: line = geometry.LineString([(0, 0, 0), (None, None, None), (2, 2, 2)]) diff --git a/tests/test_multipoint.py b/tests/test_multipoint.py index 04c3705..7590775 100644 --- a/tests/test_multipoint.py +++ b/tests/test_multipoint.py @@ -1,5 +1,7 @@ """Test MultiPoint.""" +import math + import pytest from pygeoif import geometry @@ -156,9 +158,9 @@ def test_empty() -> None: def test_repr_empty() -> None: - multipoint = geometry.MultiPoint([(None, None)]) + multipoint = geometry.MultiPoint([(math.nan, math.nan)]) - assert repr(multipoint) == "MultiPoint(((),))" + assert repr(multipoint) == "MultiPoint(())" def test_empty_bounds() -> None: From a802b6c433e8a484094f5d8453a39197c6e3387a Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 27 Apr 2024 15:09:15 +0100 Subject: [PATCH 07/24] update readme and history --- README.rst | 30 ++++++++++++++++-------------- docs/HISTORY.rst | 1 + 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index ec556d8..bfa92a9 100644 --- a/README.rst +++ b/README.rst @@ -38,59 +38,61 @@ testing with Hypothesis_. It was written to provide clean and python only geometries for fastkml_ -.. image:: https://readthedocs.org/projects/pygeoif/badge/?version=latest +|doc| |test| |cov| |hypothesis| |black| |mypy| |openhub| |factor| |commit| |py| |implement| |latest| |license| |downloads| + +.. |doc| image:: https://readthedocs.org/projects/pygeoif/badge/?version=latest :target: https://pygeoif.readthedocs.io/en/latest/?badge=latest :alt: Documentation -.. image:: https://github.com/cleder/pygeoif/actions/workflows/run-all-tests.yml/badge.svg?branch=main +.. |test| image:: https://github.com/cleder/pygeoif/actions/workflows/run-all-tests.yml/badge.svg?branch=main :target: https://github.com/cleder/pygeoif/actions/workflows/run-all-tests.yml :alt: GitHub Actions -.. image:: https://codecov.io/gh/cleder/pygeoif/branch/main/graph/badge.svg?token=2EfiwBXs9X +.. |cov| image:: https://codecov.io/gh/cleder/pygeoif/branch/main/graph/badge.svg?token=2EfiwBXs9X :target: https://codecov.io/gh/cleder/pygeoif :alt: Codecov -.. image:: https://img.shields.io/badge/hypothesis-tested-brightgreen.svg +.. |hypothesis| image:: https://img.shields.io/badge/hypothesis-tested-brightgreen.svg :alt: Tested with Hypothesis :target: https://hypothesis.readthedocs.io -.. image:: https://img.shields.io/badge/code_style-black-000000.svg +.. |black| image:: https://img.shields.io/badge/code_style-black-000000.svg :target: https://github.com/psf/ :alt: Black -.. image:: https://img.shields.io/badge/type_checker-mypy-blue +.. |mypy| image:: https://img.shields.io/badge/type_checker-mypy-blue :target: http://mypy-lang.org/ :alt: Mypy -.. image:: https://www.openhub.net/p/pygeoif/widgets/project_thin_badge.gif +.. |openhub| image:: https://www.openhub.net/p/pygeoif/widgets/project_thin_badge.gif :target: https://www.openhub.net/p/pygeoif/ :alt: Openhub -.. image:: https://www.codefactor.io/repository/github/cleder/pygeoif/badge/main +.. |factor| image:: https://www.codefactor.io/repository/github/cleder/pygeoif/badge/main :target: https://www.codefactor.io/repository/github/cleder/pygeoif/overview/main :alt: CodeFactor -.. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit +.. |commit| image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit :target: https://github.com/pre-commit/pre-commit :alt: pre-commit -.. image:: https://img.shields.io/pypi/pyversions/pygeoif.svg +.. |py| image:: https://img.shields.io/pypi/pyversions/pygeoif.svg :target: https://pypi.python.org/pypi/pygeoif/ :alt: Supported Python versions -.. image:: https://img.shields.io/pypi/implementation/pygeoif.svg +.. |implement| image:: https://img.shields.io/pypi/implementation/pygeoif.svg :target: https://pypi.python.org/pypi/pygeoif/ :alt: Supported Python implementations -.. image:: https://img.shields.io/pypi/v/pygeoif.svg +.. |latest| image:: https://img.shields.io/pypi/v/pygeoif.svg :target: https://pypi.python.org/pypi/pygeoif/ :alt: Latest Version -.. image:: https://img.shields.io/pypi/l/pygeoif.svg +.. |license| image:: https://img.shields.io/pypi/l/pygeoif.svg :target: https://pypi.python.org/pypi/pygeoif/ :alt: License -.. image:: https://img.shields.io/pypi/dm/pygeoif.svg +.. |downloads| image:: https://img.shields.io/pypi/dm/pygeoif.svg :target: https://pypi.python.org/pypi/pygeoif/ :alt: Downloads diff --git a/docs/HISTORY.rst b/docs/HISTORY.rst index 7203403..2d00f7c 100644 --- a/docs/HISTORY.rst +++ b/docs/HISTORY.rst @@ -4,6 +4,7 @@ Changelog 1.5.0 (unreleased) ------------------ +- fix handling of empty geometries. 1.4.0 (2024/03/25) ------------------ From 264f79958f5686e0065c8ffcfd9431ec9d89577f Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 27 Apr 2024 15:15:20 +0100 Subject: [PATCH 08/24] update readme --- README.rst | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index bfa92a9..a4cacfe 100644 --- a/README.rst +++ b/README.rst @@ -162,14 +162,16 @@ x, y, z : float Example ~~~~~~~~ - >>> from pygeoif import Point - >>> p = Point(1.0, -1.0) - >>> print(p) - POINT (1.0 -1.0) - >>> p.y - -1.0 - >>> p.x - 1.0 +.. code-block:: pycon + + >>> from pygeoif import Point + >>> p = Point(1.0, -1.0) + >>> print(p) + POINT (1.0 -1.0) + >>> p.y + -1.0 + >>> p.x + 1.0 @@ -270,6 +272,8 @@ So it's very rarely used in the real GIS professional world. Example ~~~~~~~~ +.. code-block:: pycon + >>> from pygeoif import geometry >>> p = geometry.Point(1.0, -1.0) >>> p2 = geometry.Point(1.0, -1.0) @@ -291,6 +295,8 @@ properties : dict Example ~~~~~~~~ +.. code-block:: pycon + >>> from pygeoif import Point, Feature >>> p = Point(1.0, -1.0) >>> props = {'Name': 'Sample Point', 'Other': 'Other Data'} @@ -311,6 +317,7 @@ features: sequence Example ~~~~~~~~ +.. code-block:: pycon >>> from pygeoif import Point, Feature, FeatureCollection >>> p = Point(1.0, -1.0) @@ -332,6 +339,7 @@ shape Create a pygeoif feature from an object that provides the ``__geo_interface__`` or any GeoJSON_ compatible dictionary. +.. code-block:: pycon >>> from shapely.geometry import Point >>> from pygeoif import geometry, shape @@ -343,6 +351,7 @@ from_wkt --------- Create a geometry from its WKT representation +.. code-block:: pycon >>> from pygeoif import from_wkt >>> p = from_wkt('POINT (0 1)') @@ -389,11 +398,13 @@ pre-commit ---------- Install the ``pre-commit`` hook with:: +.. code-block:: console pip install pre-commit pre-commit install and check the code with:: +.. code-block:: console pre-commit run --all-files @@ -401,6 +412,7 @@ Testing ------- Run the unit and static tests with:: +.. code-block:: console pytest tests pytest --doctest-glob="README.rst" From d825960d8f208ffff162bcc19761665c281b81dd Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 27 Apr 2024 15:19:53 +0100 Subject: [PATCH 09/24] update readme --- README.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.rst b/README.rst index a4cacfe..ca8f3a3 100644 --- a/README.rst +++ b/README.rst @@ -106,6 +106,7 @@ You can install PyGeoIf from pypi using pip:: Example ======== +.. code-block:: pycon >>> from pygeoif import geometry >>> p = geometry.Point(1,1) @@ -161,7 +162,6 @@ x, y, z : float Example ~~~~~~~~ - .. code-block:: pycon >>> from pygeoif import Point @@ -271,7 +271,6 @@ So it's very rarely used in the real GIS professional world. Example ~~~~~~~~ - .. code-block:: pycon >>> from pygeoif import geometry From d8d318c32e204b8df68e7bcc83ae6311056713b7 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 27 Apr 2024 15:24:01 +0100 Subject: [PATCH 10/24] update readme --- README.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index ca8f3a3..85faf75 100644 --- a/README.rst +++ b/README.rst @@ -99,7 +99,8 @@ It was written to provide clean and python only geometries for fastkml_ Installation ------------ -You can install PyGeoIf from pypi using pip:: +You can install PyGeoIf from pypi using pip: +.. code-block:: console pip install pygeoif @@ -162,7 +163,7 @@ x, y, z : float Example ~~~~~~~~ -.. code-block:: pycon +.. code-block: pycon >>> from pygeoif import Point >>> p = Point(1.0, -1.0) @@ -396,13 +397,13 @@ Then install the requirements with ``pip install -e ".[dev]"``. pre-commit ---------- -Install the ``pre-commit`` hook with:: +Install the ``pre-commit`` hook with: .. code-block:: console pip install pre-commit pre-commit install -and check the code with:: +and check the code with: .. code-block:: console pre-commit run --all-files @@ -410,7 +411,7 @@ and check the code with:: Testing ------- -Run the unit and static tests with:: +Run the unit and static tests with: .. code-block:: console pytest tests From c193992edc62089aeb7d8fef005c02a3e1d05078 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 27 Apr 2024 15:28:25 +0100 Subject: [PATCH 11/24] update readme --- README.rst | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 85faf75..bb82556 100644 --- a/README.rst +++ b/README.rst @@ -99,8 +99,7 @@ It was written to provide clean and python only geometries for fastkml_ Installation ------------ -You can install PyGeoIf from pypi using pip: -.. code-block:: console +You can install PyGeoIf from pypi using pip:: pip install pygeoif @@ -397,22 +396,19 @@ Then install the requirements with ``pip install -e ".[dev]"``. pre-commit ---------- -Install the ``pre-commit`` hook with: -.. code-block:: console +Install the ``pre-commit`` hook with:: pip install pre-commit pre-commit install -and check the code with: -.. code-block:: console +and check the code with:: pre-commit run --all-files Testing ------- -Run the unit and static tests with: -.. code-block:: console +Run the unit and static tests with:: pytest tests pytest --doctest-glob="README.rst" From 08839a314a9061c6a0a2538cea96bbc7c5c7f751 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 27 Apr 2024 15:30:43 +0100 Subject: [PATCH 12/24] update readme --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index bb82556..cddaa76 100644 --- a/README.rst +++ b/README.rst @@ -338,6 +338,7 @@ shape Create a pygeoif feature from an object that provides the ``__geo_interface__`` or any GeoJSON_ compatible dictionary. + .. code-block:: pycon >>> from shapely.geometry import Point @@ -350,6 +351,7 @@ from_wkt --------- Create a geometry from its WKT representation + .. code-block:: pycon >>> from pygeoif import from_wkt From 6856844b97e8c35049ea6b88cbf975a916f5175d Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 27 Apr 2024 17:35:27 +0100 Subject: [PATCH 13/24] return an empty tuple for coordinates if point is empty --- pygeoif/geometry.py | 22 +++++++++++++++------- tests/test_line.py | 12 ++++++++++++ tests/test_point.py | 6 ++++++ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/pygeoif/geometry.py b/pygeoif/geometry.py index 06222f6..688dc86 100644 --- a/pygeoif/geometry.py +++ b/pygeoif/geometry.py @@ -289,9 +289,9 @@ def z(self) -> Optional[float]: raise DimensionError(msg) @property - def coords(self) -> Tuple[PointType]: + def coords(self) -> Union[Tuple[PointType], Tuple[()]]: """Return the geometry coordinates.""" - return (self._geoms,) + return () if self.is_empty else (self._geoms,) @property def has_z(self) -> bool: @@ -372,7 +372,10 @@ def geoms(self) -> Tuple[Point, ...]: @property def coords(self) -> LineType: """Return the geometry coordinates.""" - return cast(LineType, tuple(point.coords[0] for point in self.geoms)) + return cast( + LineType, + tuple(point.coords[0] for point in self.geoms if point.coords), + ) @property def is_empty(self) -> bool: @@ -407,7 +410,9 @@ def from_coordinates(cls, coordinates: LineType) -> "LineString": @classmethod def from_points(cls, *args: Point) -> "LineString": """Create a linestring from points.""" - return cls(cast(LineType, tuple(point.coords[0] for point in args))) + return cls( + cast(LineType, tuple(point.coords[0] for point in args if point.coords)), + ) @classmethod def _from_dict(cls, geo_interface: GeoInterface) -> "LineString": @@ -730,7 +735,8 @@ def __len__(self) -> int: def __repr__(self) -> str: """Return the representation.""" return ( - f"{self.geom_type}({tuple(geom.coords[0] for geom in self._geoms if geom)})" + f"{self.geom_type}" + f"({tuple(geom.coords[0] for geom in self._geoms if geom.coords)})" ) @property @@ -746,13 +752,15 @@ def _wkt_coords(self) -> str: def __geo_interface__(self) -> GeoInterface: """Return the geo interface.""" geo_interface = super().__geo_interface__ - geo_interface["coordinates"] = tuple(geom.coords[0] for geom in self.geoms) + geo_interface["coordinates"] = tuple( + geom.coords[0] for geom in self.geoms if geom.coords + ) return geo_interface @classmethod def from_points(cls, *args: Point, unique: bool = False) -> "MultiPoint": """Create a MultiPoint from Points.""" - return cls([point.coords[0] for point in args], unique=unique) + return cls([point.coords[0] for point in args if point.coords], unique=unique) @classmethod def _from_dict(cls, geo_interface: GeoInterface) -> "MultiPoint": diff --git a/tests/test_line.py b/tests/test_line.py index fff6b6b..1c69aab 100644 --- a/tests/test_line.py +++ b/tests/test_line.py @@ -220,3 +220,15 @@ def test_empty_bounds() -> None: line = geometry.LineString([]) assert line.bounds == () + + +def test_bounds_1_pt() -> None: + line = geometry.LineString([(0, 0)]) + + assert line.bounds == (0, 0, 0, 0) + + +def test_empty_coords() -> None: + line = geometry.LineString([]) + + assert line.coords == () diff --git a/tests/test_point.py b/tests/test_point.py index 9d91682..eacb587 100644 --- a/tests/test_point.py +++ b/tests/test_point.py @@ -82,6 +82,12 @@ def test_repr_empty() -> None: assert repr(point) == "Point()" +def test_coords_empty() -> None: + point = geometry.Point(None, None) + + assert point.coords == () + + def test_repr2d() -> None: point = geometry.Point(1, 0) From 7768e81a0a98debc0955d9037a7005a8d33b0da7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 16:37:26 +0000 Subject: [PATCH 14/24] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 24.4.0 → 24.4.2](https://github.com/psf/black/compare/24.4.0...24.4.2) - [github.com/astral-sh/ruff-pre-commit: v0.4.1 → v0.4.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.1...v0.4.2) - [github.com/pre-commit/mirrors-mypy: v1.9.0 → v1.10.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.9.0...v1.10.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 81f2011..0f07bfc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,11 +36,11 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black - rev: 24.4.0 + rev: 24.4.2 hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.4.1' + rev: 'v0.4.2' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] @@ -68,7 +68,7 @@ repos: - flake8-typing-imports - flake8-use-fstring - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.9.0 + rev: v1.10.0 hooks: - id: mypy additional_dependencies: From beae74a8c35df6ed1665a0f281acd3cfbd7247e1 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 1 May 2024 17:50:27 +0100 Subject: [PATCH 15/24] add hypothesis tests for functions --- pygeoif/factories.py | 7 +- pygeoif/functions.py | 17 +- pygeoif/geometry.py | 11 +- pygeoif/types.py | 12 +- pyproject.toml | 1 + tests/hypothesis/test_functions.py | 309 +++++++++++++++++++++++++++++ tests/test_functions.py | 3 +- tests/test_line.py | 6 + tests/test_multipoint.py | 6 + 9 files changed, 354 insertions(+), 18 deletions(-) create mode 100644 tests/hypothesis/test_functions.py diff --git a/pygeoif/factories.py b/pygeoif/factories.py index 660f722..c139434 100644 --- a/pygeoif/factories.py +++ b/pygeoif/factories.py @@ -150,15 +150,12 @@ def shape( ) raise TypeError(msg) - constructor = type_map.get(geometry["type"]) - if constructor: + if constructor := type_map.get(geometry["type"]): return constructor._from_dict( # type: ignore [attr-defined, no-any-return] geometry, ) if geometry["type"] == "GeometryCollection": - geometries = [ - shape(fi) for fi in geometry["geometries"] # type: ignore [typeddict-item] - ] + geometries = [shape(fi) for fi in geometry["geometries"]] return GeometryCollection(geometries) msg = f"[{geometry['type']} is not implemented" raise NotImplementedError(msg) diff --git a/pygeoif/functions.py b/pygeoif/functions.py index 276dd6a..0e12b2c 100644 --- a/pygeoif/functions.py +++ b/pygeoif/functions.py @@ -41,6 +41,8 @@ def signed_area(coords: LineType) -> float: Linear time algorithm: http://www.cgafaq.info/wiki/Polygon_Area. A value >= 0 indicates a counter-clockwise oriented ring. """ + if len(coords) < 3: # noqa: PLR2004 + return 0.0 xs, ys = map(list, zip(*(coord[:2] for coord in coords))) xs.append(xs[1]) # pragma: no mutate ys.append(ys[1]) # pragma: no mutate @@ -53,7 +55,6 @@ def signed_area(coords: LineType) -> float: def centroid(coords: LineType) -> Tuple[Point2D, float]: """Calculate the coordinates of the centroid and the area of a LineString.""" ans: List[float] = [0, 0] - n = len(coords) signed_area = 0.0 @@ -68,6 +69,9 @@ def centroid(coords: LineType) -> Tuple[Point2D, float]: ans[0] += (coord[0] + next_coord[0]) * area ans[1] += (coord[1] + next_coord[1]) * area + if signed_area == 0 or math.isnan(signed_area): + return ((math.nan, math.nan), signed_area) + ans[0] = ans[0] / (3 * signed_area) ans[1] = ans[1] / (3 * signed_area) @@ -167,13 +171,13 @@ def compare_geo_interface( return all( compare_geo_interface(first=g1, second=g2) # type: ignore [arg-type] for g1, g2 in zip_longest( - first["geometries"], # type: ignore [typeddict-item] + first["geometries"], second["geometries"], # type: ignore [typeddict-item] fillvalue={"type": None, "coordinates": ()}, ) ) return compare_coordinates( - coords=first["coordinates"], # type: ignore [typeddict-item] + coords=first["coordinates"], other=second["coordinates"], # type: ignore [typeddict-item] ) except KeyError: @@ -220,6 +224,8 @@ def move_coordinates( >>> move_coordinates(((0, 0), (-1, 1)), (-1, 1, 0)) ((-1, 1, 0), (-2, 2, 0)) """ + if not coordinates: + return coordinates if isinstance(coordinates[0], (int, float)): return move_coordinate(cast(PointType, coordinates), move_by) return cast( @@ -237,14 +243,13 @@ def move_geo_interface( return { "type": "GeometryCollection", "geometries": tuple( - move_geo_interface(g, move_by) - for g in interface["geometries"] # type: ignore [typeddict-item] + move_geo_interface(g, move_by) for g in interface["geometries"] ), } return { "type": interface["type"], "coordinates": move_coordinates( - interface["coordinates"], # type: ignore [typeddict-item, arg-type] + interface["coordinates"], # type: ignore [arg-type] move_by, ), } diff --git a/pygeoif/geometry.py b/pygeoif/geometry.py index 688dc86..4d8d82a 100644 --- a/pygeoif/geometry.py +++ b/pygeoif/geometry.py @@ -41,6 +41,7 @@ from pygeoif.types import Bounds from pygeoif.types import GeoCollectionInterface from pygeoif.types import GeoInterface +from pygeoif.types import GeomType from pygeoif.types import GeoType from pygeoif.types import LineType from pygeoif.types import Point2D @@ -166,7 +167,7 @@ def __geo_interface__(self) -> GeoInterface: msg = "Empty Geometry" raise AttributeError(msg) return { - "type": self.geom_type, + "type": cast(GeomType, self.geom_type), "bbox": cast(Bounds, self.bounds), "coordinates": (), } @@ -481,9 +482,9 @@ def centroid(self) -> Optional[Point]: if self.has_z: msg = "Centeroid is only implemented for 2D coordinates" raise DimensionError(msg) - try: - cent, area = centroid(self.coords) - except ZeroDivisionError: + + cent, area = centroid(self.coords) + if any(math.isnan(coord) for coord in cent): return None return ( Point(x=cent[0], y=cent[1]) @@ -625,6 +626,8 @@ def from_linear_rings(cls, shell: LinearRing, *args: LinearRing) -> "Polygon": @classmethod def _from_dict(cls, geo_interface: GeoInterface) -> "Polygon": cls._check_dict(geo_interface) + if not geo_interface["coordinates"]: + return cls(shell=(), holes=()) return cls( shell=cast(LineType, geo_interface["coordinates"][0]), holes=cast(Tuple[LineType], geo_interface["coordinates"][1:]), diff --git a/pygeoif/types.py b/pygeoif/types.py index f12ec89..c933a1b 100644 --- a/pygeoif/types.py +++ b/pygeoif/types.py @@ -57,11 +57,21 @@ ] MultiCoordinatesType = Sequence[CoordinatesType] +GeomType = Literal[ + "Point", + "LineString", + "LinearRing", + "Polygon", + "MultiPoint", + "MultiLineString", + "MultiPolygon", +] + class GeoInterface(TypedDict): """Required keys for the GeoInterface.""" - type: str + type: GeomType coordinates: Union[CoordinatesType, MultiCoordinatesType] bbox: NotRequired[Bounds] diff --git a/pyproject.toml b/pyproject.toml index 17a3994..69daa01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,6 +85,7 @@ linting = [ ] tests = [ "hypothesis", + "more_itertools", "pytest", "pytest-cov", ] diff --git a/tests/hypothesis/test_functions.py b/tests/hypothesis/test_functions.py new file mode 100644 index 0000000..6a7ce69 --- /dev/null +++ b/tests/hypothesis/test_functions.py @@ -0,0 +1,309 @@ +"""Hypothesis test cases for the `pygeoif.functions` module.""" + +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + +import math +import typing + +import more_itertools +from hypothesis import given +from hypothesis import strategies as st + +import pygeoif.functions +import pygeoif.types + + +@given( + coords=st.one_of( + st.lists( + st.tuples( + st.floats(allow_subnormal=False), + st.floats(allow_subnormal=False), + ), + ), + st.lists( + st.tuples( + st.floats(allow_subnormal=False), + st.floats(allow_subnormal=False), + st.floats(allow_subnormal=False), + ), + ), + ), +) +def test_fuzz_centroid( + coords: typing.Union[ + typing.Sequence[typing.Tuple[float, float]], + typing.Sequence[typing.Tuple[float, float, float]], + ], +) -> None: + center, area = pygeoif.functions.centroid(coords=coords) + if area == 0 or math.isnan(area): + assert math.isnan(center[0]) + assert math.isnan(center[1]) + else: + assert isinstance(center[0], float) + assert isinstance(center[1], float) + assert len(center) == 2 + + +@given( + coords=st.one_of( + st.floats(), + st.lists(st.tuples(st.floats(), st.floats())), + st.lists(st.tuples(st.floats(), st.floats(), st.floats())), + st.lists( + st.one_of( + st.lists(st.tuples(st.floats(), st.floats())), + st.lists(st.tuples(st.floats(), st.floats(), st.floats())), + ), + ), + st.lists( + st.one_of( + st.lists(st.tuples(st.floats(), st.floats())), + st.lists(st.tuples(st.floats(), st.floats(), st.floats())), + st.lists( + st.one_of( + st.lists(st.tuples(st.floats(), st.floats())), + st.lists(st.tuples(st.floats(), st.floats(), st.floats())), + ), + ), + st.tuples(st.floats(), st.floats()), + st.tuples(st.floats(), st.floats(), st.floats()), + ), + ), + st.tuples(st.floats(), st.floats()), + st.tuples(st.floats(), st.floats(), st.floats()), + ), + other=st.one_of( + st.floats(), + st.lists(st.tuples(st.floats(), st.floats())), + st.lists(st.tuples(st.floats(), st.floats(), st.floats())), + st.lists( + st.one_of( + st.lists(st.tuples(st.floats(), st.floats())), + st.lists(st.tuples(st.floats(), st.floats(), st.floats())), + ), + ), + st.lists( + st.one_of( + st.lists(st.tuples(st.floats(), st.floats())), + st.lists(st.tuples(st.floats(), st.floats(), st.floats())), + st.lists( + st.one_of( + st.lists(st.tuples(st.floats(), st.floats())), + st.lists(st.tuples(st.floats(), st.floats(), st.floats())), + ), + ), + st.tuples(st.floats(), st.floats()), + st.tuples(st.floats(), st.floats(), st.floats()), + ), + ), + st.tuples(st.floats(), st.floats()), + st.tuples(st.floats(), st.floats(), st.floats()), + ), +) +def test_fuzz_compare_coordinates( + coords: typing.Union[ + float, + typing.Tuple[float, float], + typing.Tuple[float, float, float], + typing.Sequence[typing.Tuple[float, float]], + typing.Sequence[typing.Tuple[float, float, float]], + typing.Sequence[ + typing.Union[ + typing.Sequence[typing.Tuple[float, float]], + typing.Sequence[typing.Tuple[float, float, float]], + ] + ], + typing.Sequence[ + typing.Union[ + typing.Tuple[float, float], + typing.Tuple[float, float, float], + typing.Sequence[typing.Tuple[float, float]], + typing.Sequence[typing.Tuple[float, float, float]], + typing.Sequence[ + typing.Union[ + typing.Sequence[typing.Tuple[float, float]], + typing.Sequence[typing.Tuple[float, float, float]], + ] + ], + ] + ], + ], + other: typing.Union[ + float, + typing.Tuple[float, float], + typing.Tuple[float, float, float], + typing.Sequence[typing.Tuple[float, float]], + typing.Sequence[typing.Tuple[float, float, float]], + typing.Sequence[ + typing.Union[ + typing.Sequence[typing.Tuple[float, float]], + typing.Sequence[typing.Tuple[float, float, float]], + ] + ], + typing.Sequence[ + typing.Union[ + typing.Tuple[float, float], + typing.Tuple[float, float, float], + typing.Sequence[typing.Tuple[float, float]], + typing.Sequence[typing.Tuple[float, float, float]], + typing.Sequence[ + typing.Union[ + typing.Sequence[typing.Tuple[float, float]], + typing.Sequence[typing.Tuple[float, float, float]], + ] + ], + ] + ], + ], +) -> None: + assert isinstance( + pygeoif.functions.compare_coordinates(coords=coords, other=other), + bool, + ) + + flat_coords = ( + [coords] if isinstance(coords, float) else more_itertools.collapse(coords) + ) + flat_other = [other] if isinstance(other, float) else more_itertools.collapse(other) + + if any(math.isnan(c) for c in flat_coords): + assert not pygeoif.functions.compare_coordinates(coords=coords, other=coords) + else: + assert pygeoif.functions.compare_coordinates(coords=coords, other=coords) + if any(math.isnan(c) for c in flat_other): + assert not pygeoif.functions.compare_coordinates(coords=other, other=other) + else: + assert pygeoif.functions.compare_coordinates(coords=other, other=other) + + +@given( + first=st.from_type(pygeoif.types.GeoInterface), + second=st.from_type(pygeoif.types.GeoInterface), +) +def test_fuzz_compare_geo_interface( + first: pygeoif.types.GeoInterface, + second: pygeoif.types.GeoInterface, +) -> None: + assert isinstance( + pygeoif.functions.compare_geo_interface(first=first, second=second), + bool, + ) + + +@given( + points=st.lists( + st.tuples( + st.floats(allow_nan=False, allow_infinity=False, allow_subnormal=False), + st.floats(allow_nan=False, allow_infinity=False, allow_subnormal=False), + ), + ), +) +def test_fuzz_convex_hull(points: typing.List[typing.Tuple[float, float]]) -> None: + hull = pygeoif.functions.convex_hull(points=points) + + for coord in hull: + assert coord in points + assert len(hull) <= len(points) + 1 + + +@given( + coords=st.one_of( + st.lists(st.tuples(st.floats(), st.floats())), + st.lists(st.tuples(st.floats(), st.floats(), st.floats())), + ), +) +def test_fuzz_dedupe( + coords: typing.Union[ + typing.Sequence[typing.Tuple[float, float]], + typing.Sequence[typing.Tuple[float, float, float]], + ], +) -> None: + deduped = pygeoif.functions.dedupe(coords=coords) + + assert len(deduped) <= len(coords) + for coord in deduped: + assert coord in coords + + +@given( + coordinate=st.one_of( + st.tuples(st.floats(), st.floats()), + st.tuples(st.floats(), st.floats(), st.floats()), + ), + move_by=st.one_of( + st.tuples(st.floats(), st.floats()), + st.tuples(st.floats(), st.floats(), st.floats()), + ), +) +def test_fuzz_move_coordinate( + coordinate: typing.Union[ + typing.Tuple[float, float], + typing.Tuple[float, float, float], + ], + move_by: typing.Union[ + typing.Tuple[float, float], + typing.Tuple[float, float, float], + ], +) -> None: + moved = pygeoif.functions.move_coordinate(coordinate=coordinate, move_by=move_by) + + assert len(moved) == len(move_by) + + +@given( + coordinates=st.one_of( + st.lists(st.tuples(st.floats(), st.floats())), + st.lists(st.tuples(st.floats(), st.floats(), st.floats())), + st.lists( + st.one_of( + st.lists(st.tuples(st.floats(), st.floats())), + st.lists(st.tuples(st.floats(), st.floats(), st.floats())), + ), + ), + st.tuples(st.floats(), st.floats()), + st.tuples(st.floats(), st.floats(), st.floats()), + ), + move_by=st.one_of( + st.tuples(st.floats(), st.floats()), + st.tuples(st.floats(), st.floats(), st.floats()), + ), +) +def test_fuzz_move_coordinates( + coordinates: typing.Union[ + typing.Tuple[float, float], + typing.Tuple[float, float, float], + typing.Sequence[typing.Tuple[float, float]], + typing.Sequence[typing.Tuple[float, float, float]], + typing.Sequence[ + typing.Union[ + typing.Sequence[typing.Tuple[float, float]], + typing.Sequence[typing.Tuple[float, float, float]], + ] + ], + ], + move_by: typing.Union[ + typing.Tuple[float, float], + typing.Tuple[float, float, float], + ], +) -> None: + moved = pygeoif.functions.move_coordinates(coordinates=coordinates, move_by=move_by) + + assert moved if coordinates else not moved + + +@given( + coords=st.one_of( + st.lists(st.tuples(st.floats(), st.floats())), + st.lists(st.tuples(st.floats(), st.floats(), st.floats())), + ), +) +def test_fuzz_signed_area( + coords: typing.Union[ + typing.Sequence[typing.Tuple[float, float]], + typing.Sequence[typing.Tuple[float, float, float]], + ], +) -> None: + assert isinstance(pygeoif.functions.signed_area(coords=coords), float) diff --git a/tests/test_functions.py b/tests/test_functions.py index b56f83f..13ddf64 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -72,8 +72,7 @@ def test_signed_area2() -> None: def test_centroid_line() -> None: a0 = [(0, 0), (1, 1), (0, 0)] - with pytest.raises(ZeroDivisionError): - assert centroid(a0) + assert centroid(a0) == ((math.nan, math.nan), 0) def test_signed_area_0_3d() -> None: diff --git a/tests/test_line.py b/tests/test_line.py index 1c69aab..777c90e 100644 --- a/tests/test_line.py +++ b/tests/test_line.py @@ -232,3 +232,9 @@ def test_empty_coords() -> None: line = geometry.LineString([]) assert line.coords == () + + +def test_empty_coords_nan() -> None: + line = geometry.LineString(((math.nan, math.nan),)) + + assert line.coords == () diff --git a/tests/test_multipoint.py b/tests/test_multipoint.py index 7590775..3cdde6a 100644 --- a/tests/test_multipoint.py +++ b/tests/test_multipoint.py @@ -167,3 +167,9 @@ def test_empty_bounds() -> None: multipoint = geometry.MultiPoint([(None, None)]) assert multipoint.bounds == () + + +def test_empty_geoms() -> None: + multipoint = geometry.MultiPoint([(math.nan, math.nan)]) + + assert not list(multipoint.geoms) From 17e1d72a31afe05920a77e153991bbff83626ccd Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 1 May 2024 18:16:11 +0100 Subject: [PATCH 16/24] ignore flake8 E402 in docs conf.py --- docs/conf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index f905846..f7c5f5a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,5 @@ # noqa: D100, INP001 +# flake8: noqa # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: @@ -11,6 +12,7 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + from pygeoif import about project = "pygeoif" From 9c2063c6340842c635638bed3173e625cbb450f4 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 1 May 2024 18:40:58 +0100 Subject: [PATCH 17/24] Add hypothesis profile for continuous integration (CI) testing --- .github/workflows/run-all-tests.yml | 2 +- tests/conftest.py | 7 ++++++- tests/hypothesis/test_functions.py | 20 ++++++++++++++++---- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index b1602d8..948dce2 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -44,7 +44,7 @@ jobs: python -m pip install -e ".[tests]" - name: Test with pytest including hypothesis tests run: >- - pytest tests + pytest tests --hypothesis-profile=ci test-coverage: runs-on: ubuntu-latest diff --git a/tests/conftest.py b/tests/conftest.py index 7a4dc90..b54dbc5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,9 +8,14 @@ from hypothesis import HealthCheck from hypothesis import settings -settings.register_profile("exhaustive", max_examples=10_000) +settings.register_profile( + "exhaustive", + max_examples=10_000, + suppress_health_check=[HealthCheck.too_slow], +) settings.register_profile( "coverage", max_examples=10, suppress_health_check=[HealthCheck.too_slow], ) +settings.register_profile("ci", suppress_health_check=[HealthCheck.too_slow]) diff --git a/tests/hypothesis/test_functions.py b/tests/hypothesis/test_functions.py index 6a7ce69..7e42ded 100644 --- a/tests/hypothesis/test_functions.py +++ b/tests/hypothesis/test_functions.py @@ -18,7 +18,9 @@ coords=st.one_of( st.lists( st.tuples( - st.floats(allow_subnormal=False), + st.floats( + allow_subnormal=False, + ), st.floats(allow_subnormal=False), ), ), @@ -41,7 +43,7 @@ def test_fuzz_centroid( if area == 0 or math.isnan(area): assert math.isnan(center[0]) assert math.isnan(center[1]) - else: + else: # pragma: no cover assert isinstance(center[0], float) assert isinstance(center[1], float) assert len(center) == 2 @@ -196,8 +198,18 @@ def test_fuzz_compare_geo_interface( @given( points=st.lists( st.tuples( - st.floats(allow_nan=False, allow_infinity=False, allow_subnormal=False), - st.floats(allow_nan=False, allow_infinity=False, allow_subnormal=False), + st.floats( + allow_nan=False, + allow_infinity=False, + allow_subnormal=False, + width=32, + ), + st.floats( + allow_nan=False, + allow_infinity=False, + allow_subnormal=False, + width=32, + ), ), ), ) From 35c1f9f508c67b96a222d2f88f7f6102dd749c19 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 1 May 2024 18:45:46 +0100 Subject: [PATCH 18/24] update history --- docs/HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/HISTORY.rst b/docs/HISTORY.rst index 2d00f7c..283a168 100644 --- a/docs/HISTORY.rst +++ b/docs/HISTORY.rst @@ -5,6 +5,7 @@ Changelog ------------------ - fix handling of empty geometries. +- more hypothesis tests 1.4.0 (2024/03/25) ------------------ From 38cf8a2dde4f3cc4d8e5c5e0578158105beeaf10 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 1 May 2024 19:16:42 +0100 Subject: [PATCH 19/24] assert ccw orientations for the convex hull --- tests/hypothesis/test_functions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/hypothesis/test_functions.py b/tests/hypothesis/test_functions.py index 7e42ded..579ba5b 100644 --- a/tests/hypothesis/test_functions.py +++ b/tests/hypothesis/test_functions.py @@ -219,6 +219,7 @@ def test_fuzz_convex_hull(points: typing.List[typing.Tuple[float, float]]) -> No for coord in hull: assert coord in points assert len(hull) <= len(points) + 1 + assert pygeoif.functions.signed_area(hull) >= 0 @given( From 9975d32d678a613e1fa5c43863fd4db351912e66 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 1 May 2024 19:30:40 +0100 Subject: [PATCH 20/24] add tests to construct an empty polygon from __geointerface__ --- tests/test_polygon.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_polygon.py b/tests/test_polygon.py index 8b17c1c..b2c9294 100644 --- a/tests/test_polygon.py +++ b/tests/test_polygon.py @@ -62,6 +62,18 @@ def test_from_dict_shell_only() -> None: } +def test_from_dict_empty_coordinates() -> None: + polygon = geometry.Polygon._from_dict( + { + "type": "Polygon", + "bbox": (0.0, 0.0, 1.0, 1.0), + "coordinates": (), + }, + ) + + assert polygon.is_empty + + def test_from_dict_with_holes() -> None: polygon = geometry.Polygon._from_dict( { From 5c91e5db7b241d4b0b017409edc08f02d63a35c4 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 1 May 2024 19:40:58 +0100 Subject: [PATCH 21/24] add typechecking for tests --- .github/workflows/run-all-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index 948dce2..3cb4122 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -92,7 +92,7 @@ jobs: python -m pip install -e ".[typing, complexity, linting]" - name: Typecheck run: | - mypy pygeoif + mypy pygeoif tests - name: Linting run: | flake8 pygeoif From e705ad55f470d8d807cc8b56440f1ed080f48e16 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 1 May 2024 20:00:06 +0100 Subject: [PATCH 22/24] exclude lines that may be missed in hypothesis tests --- tests/hypothesis/test_functions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/hypothesis/test_functions.py b/tests/hypothesis/test_functions.py index 579ba5b..b9f816a 100644 --- a/tests/hypothesis/test_functions.py +++ b/tests/hypothesis/test_functions.py @@ -171,13 +171,13 @@ def test_fuzz_compare_coordinates( ) flat_other = [other] if isinstance(other, float) else more_itertools.collapse(other) - if any(math.isnan(c) for c in flat_coords): + if any(math.isnan(c) for c in flat_coords): # pragma: no cover assert not pygeoif.functions.compare_coordinates(coords=coords, other=coords) - else: + else: # pragma: no cover assert pygeoif.functions.compare_coordinates(coords=coords, other=coords) - if any(math.isnan(c) for c in flat_other): + if any(math.isnan(c) for c in flat_other): # pragma: no cover assert not pygeoif.functions.compare_coordinates(coords=other, other=other) - else: + else: # pragma: no cover assert pygeoif.functions.compare_coordinates(coords=other, other=other) From 3cc76bde4f1eadc183075375bdaa7f1559d2883d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 16:36:10 +0000 Subject: [PATCH 23/24] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.4.2 → v0.4.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.2...v0.4.3) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0f07bfc..e55cf0a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.4.2' + rev: 'v0.4.3' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From e65b752da253ead053da83e7a230afb19e1fdd20 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 11 May 2024 13:27:14 +0100 Subject: [PATCH 24/24] changelog for v 1.5.0 --- docs/HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/HISTORY.rst b/docs/HISTORY.rst index 283a168..6abe02e 100644 --- a/docs/HISTORY.rst +++ b/docs/HISTORY.rst @@ -1,7 +1,7 @@ Changelog ========= -1.5.0 (unreleased) +1.5.0 (2024/05/11) ------------------ - fix handling of empty geometries.