From dc6c70016f36a166daa5b05893ed014e7b683636 Mon Sep 17 00:00:00 2001 From: Ben Shaver Date: Sun, 5 Nov 2023 18:33:04 -0500 Subject: [PATCH 01/55] test: initial commit adding hypothesis property testing library Adding the Hypothesis cache dir to .gitignore Grammar fixes in geometry.py Hypothesis dep in pyproject.toml without version peg Initial strategies in conftest.py. This is the only file name I could use that didn't cause an issue with the name-tests-test pre-commit hook. 'strategies.py' would be better because strictly speaking these aren't fixtures. An example test for encode/decode invariance. --- .gitignore | 1 + pygeoif/geometry.py | 6 +++--- pyproject.toml | 1 + tests/conftest.py | 52 +++++++++++++++++++++++++++++++++++++++++++++ tests/test_point.py | 9 ++++++++ 5 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 tests/conftest.py diff --git a/.gitignore b/.gitignore index 97717c04..87d5c97f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ venv/ __pycache__/ *.stderr* docs/_* +.hypothesis/ diff --git a/pygeoif/geometry.py b/pygeoif/geometry.py index 58ed051a..fc86441a 100644 --- a/pygeoif/geometry.py +++ b/pygeoif/geometry.py @@ -117,7 +117,7 @@ def convex_hull(self) -> Optional[Union["Point", "LineString", "Polygon"]]: Returns a representation of the smallest convex Polygon containing all the points in the object unless the number of points in the object - is less than three. + is fewer than three. For two points, the convex hull collapses to a LineString; for 1, to a Point. """ @@ -271,7 +271,7 @@ def is_empty(self) -> bool: """ Return if this geometry is empty. - A Point is considered empty when it has less than 2 coordinates. + A Point is considered empty when it has fewer than 2 coordinates. """ return len(self._geoms) < 2 # noqa: PLR2004 @@ -382,7 +382,7 @@ def is_empty(self) -> bool: """ Return if this geometry is empty. - A Linestring is considered empty when it has less than 2 points. + A Linestring is considered empty when it has fewer than 2 points. """ return len(self._geoms) < 2 # noqa: PLR2004 diff --git a/pyproject.toml b/pyproject.toml index 78a35d36..fe2d5327 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,6 +79,7 @@ linting = [ "yamllint", ] tests = [ + "hypothesis", "pytest", "pytest-cov", ] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..a6365803 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,52 @@ +"""Data-generating strategies for property-based testing.""" +import hypothesis.strategies as st + +# Incomplete list of allowed spatial reference systems to generate data +# - EPSG 4326 +# - EPSG 3857 +# ...? + + +# EPSG:4326 primitives +latitudes = st.floats( + min_value=-90.0, + max_value=90.0, + allow_nan=False, + allow_infinity=False, +) +longitudes = st.floats( + min_value=-180.0, + max_value=180.0, + allow_nan=False, + allow_infinity=False, +) +elevations = st.floats(allow_nan=False, allow_infinity=False) + + +# Point2D +@st.composite +def points_2d(draw, srs="EPSG:4326"): + if srs == "EPSG:4326": + return draw(st.tuples(latitudes, longitudes)) + raise NotImplementedError + + +# Point3D +@st.composite +def points_3d(draw, srs="EPSG:4326"): + if srs == "EPSG:4326": + return draw(st.tuples(latitudes, longitudes, elevations)) + raise NotImplementedError + + +# PointType +@st.composite +def points(draw, srs="EPSG:4326"): + if srs == "EPSG:4326": + return draw(st.one_of(points_2d(), points_3d())) + raise NotImplementedError + + +# LineType + +# Geometries diff --git a/tests/test_point.py b/tests/test_point.py index 37ad8803..c728feb9 100644 --- a/tests/test_point.py +++ b/tests/test_point.py @@ -3,9 +3,11 @@ from unittest import mock import pytest +from hypothesis import given from pygeoif import geometry from pygeoif.exceptions import DimensionError +from tests.conftest import points def test_empty() -> None: @@ -250,3 +252,10 @@ def test_hash_empty() -> None: point = geometry.Point(None, None) assert hash(point) == hash(()) + + +@given(points("EPSG:4326")) +def test_repr_eval_hypothesis_epsg_4326(point) -> None: + point = geometry.Point(*point) + + assert eval(repr(point), {}, {"Point": geometry.Point}) == point From a7b9230c5f49477a0f8a0f72cab3baf6b86a6f6d Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 1 Feb 2024 09:42:24 +0000 Subject: [PATCH 02/55] update codecov --- .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 4cf1a2cd..2b091f68 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -30,7 +30,7 @@ jobs: with: fail_ci_if_error: true verbose: true - token: ${{ env.CODECOV_TOKEN }} + token: ${{ secrets.CODECOV_TOKEN }} static-tests: runs-on: ubuntu-latest From e95aa6c607edce54e7e3527404061b83b58fe8a6 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 5 Feb 2024 16:15:14 +0000 Subject: [PATCH 03/55] Update changelog --- docs/HISTORY.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.rst b/docs/HISTORY.rst index 0f0331ed..fbe9fe10 100644 --- a/docs/HISTORY.rst +++ b/docs/HISTORY.rst @@ -1,9 +1,12 @@ Changelog ========= -1.3.0 (unreleased) +1.3.0 (2024/02/05) ------------------ +- add Python 3.13 to supported versions +- remove ``maybe_valid`` methods +- GeoType and GeoCollectionType protocols to use a property instead of an attribute 1.2.0 (2023/11/27) ------------------ From 02961aee5a7a8b68bf3d387caf60624e548859e1 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 5 Feb 2024 16:32:56 +0000 Subject: [PATCH 04/55] Add version 1.4.0 to changelog --- docs/HISTORY.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/HISTORY.rst b/docs/HISTORY.rst index fbe9fe10..d3e2cc87 100644 --- a/docs/HISTORY.rst +++ b/docs/HISTORY.rst @@ -1,6 +1,11 @@ Changelog ========= +1.4.0 (unreleased) +------------------ + + + 1.3.0 (2024/02/05) ------------------ From a5c65a0bab7402a8278b2e8cd63a27a7cd4c6d7e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:35:39 +0000 Subject: [PATCH 05/55] [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.1.14 → v0.2.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.14...v0.2.0) - [github.com/python-jsonschema/check-jsonschema: 0.27.3 → 0.27.4](https://github.com/python-jsonschema/check-jsonschema/compare/0.27.3...0.27.4) --- .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 a93bf37f..8361d474 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.1.14' + rev: 'v0.2.0' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] @@ -84,7 +84,7 @@ repos: hooks: - id: pyprojectsort - repo: https://github.com/python-jsonschema/check-jsonschema - rev: "0.27.3" + rev: "0.27.4" hooks: - id: check-github-workflows - id: check-github-actions From 5453cde18bda62fa310fb382fd574a4b86531a34 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:38:57 +0000 Subject: [PATCH 06/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 9feba681..f905846f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,7 +11,7 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -from pygeoif import about # noqa: E402 +from pygeoif import about project = "pygeoif" copyright = "2023, Christian Ledermann" # noqa: A001 From e37232ff57069eac8ea87785427963bd8d8314a5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 16:37:22 +0000 Subject: [PATCH 07/55] [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.2.0 → v0.2.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.0...v0.2.1) - [github.com/python-jsonschema/check-jsonschema: 0.27.4 → 0.28.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.27.4...0.28.0) --- .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 8361d474..f4129c2f 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.2.0' + rev: 'v0.2.1' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] @@ -84,7 +84,7 @@ repos: hooks: - id: pyprojectsort - repo: https://github.com/python-jsonschema/check-jsonschema - rev: "0.27.4" + rev: "0.28.0" hooks: - id: check-github-workflows - id: check-github-actions From 4c36af1ddd80d5cdf05c4781cfb6473bc1e6c5e3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 16:39:54 +0000 Subject: [PATCH 08/55] [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.1.1 → 24.2.0](https://github.com/psf/black/compare/24.1.1...24.2.0) - [github.com/astral-sh/ruff-pre-commit: v0.2.1 → v0.2.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.1...v0.2.2) --- .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 f4129c2f..37abb7f7 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.1.1 + rev: 24.2.0 hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.2.1' + rev: 'v0.2.2' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 64aebe920b781fe278d73f6cecfc70462d9eb407 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 19:20:48 +0000 Subject: [PATCH 09/55] [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.2.2 → v0.3.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.2...v0.3.2) - [github.com/pre-commit/mirrors-mypy: v1.8.0 → v1.9.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.8.0...v1.9.0) --- .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 37abb7f7..611a9f31 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.2.2' + rev: 'v0.3.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.8.0 + rev: v1.9.0 hooks: - id: mypy # - repo: https://github.com/Lucas-C/pre-commit-hooks-markup From 4a04e03c2fa2cb14150bc59d977feea340f33813 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 16:37:01 +0000 Subject: [PATCH 10/55] [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.2.0 → 24.3.0](https://github.com/psf/black/compare/24.2.0...24.3.0) - [github.com/astral-sh/ruff-pre-commit: v0.3.2 → v0.3.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.2...v0.3.3) --- .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 611a9f31..51bab723 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.2.0 + rev: 24.3.0 hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.3.2' + rev: 'v0.3.3' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From fc90ba385c8a01d0aa469f45b6f8956b185568db Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 16:36:04 +0000 Subject: [PATCH 11/55] [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: 23.10.1 → 23.11.0](https://github.com/psf/black/compare/23.10.1...23.11.0) - [github.com/astral-sh/ruff-pre-commit: v0.1.4 → v0.1.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.4...v0.1.5) - [github.com/pre-commit/mirrors-mypy: v1.6.1 → v1.7.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.6.1...v1.7.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 f6c65955..e3d96856 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,11 +41,11 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black - rev: 23.10.1 + rev: 23.11.0 hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.1.4' + rev: 'v0.1.5' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] @@ -73,7 +73,7 @@ repos: - flake8-typing-imports - flake8-use-fstring - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.6.1 + rev: v1.7.0 hooks: - id: mypy # - repo: https://github.com/Lucas-C/pre-commit-hooks-markup From 6852d9d7145b91bfb4474c6848a8375812cc009a Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 13 Nov 2023 17:45:09 +0000 Subject: [PATCH 12/55] update type annotations for mypy 1.7 --- pygeoif/functions.py | 9 +++------ pygeoif/geometry.py | 4 +--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/pygeoif/functions.py b/pygeoif/functions.py index d9f5190d..19cd7b75 100644 --- a/pygeoif/functions.py +++ b/pygeoif/functions.py @@ -44,12 +44,9 @@ def signed_area(coords: LineType) -> float: 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 - return ( - sum( - xs[i] * (ys[i + 1] - ys[i - 1]) # type: ignore [operator] - for i in range(1, len(coords)) - ) - / 2.0 + return cast( + float, + sum(xs[i] * (ys[i + 1] - ys[i - 1]) for i in range(1, len(coords))) / 2.0, ) diff --git a/pygeoif/geometry.py b/pygeoif/geometry.py index fc86441a..3d6b0396 100644 --- a/pygeoif/geometry.py +++ b/pygeoif/geometry.py @@ -955,9 +955,7 @@ def __init__(self, polygons: Sequence[PolygonType], unique: bool = False) -> Non tuple( Polygon( shell=polygon[0], - holes=polygon[1] # type: ignore [misc] - if len(polygon) == 2 # noqa: PLR2004 - else None, + holes=polygon[1] if len(polygon) == 2 else None, # noqa: PLR2004 ) for polygon in polygons ), From 22fd86a670192ea17eefe9e66b35dc943a5f8b18 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:34:39 +0000 Subject: [PATCH 13/55] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/hakancelikdev/unimport: 1.0.0 → 1.1.0](https://github.com/hakancelikdev/unimport/compare/1.0.0...1.1.0) - [github.com/astral-sh/ruff-pre-commit: v0.1.5 → v0.1.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.5...v0.1.6) --- .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 e3d96856..e822e498 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: hooks: - id: absolufy-imports - repo: https://github.com/hakancelikdev/unimport - rev: 1.0.0 + rev: 1.1.0 hooks: - id: unimport args: [--remove, --include-star-import, --ignore-init, --gitignore] @@ -45,7 +45,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.1.5' + rev: 'v0.1.6' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 25ee709abd7e8b78a5982868c674e3cf07381e44 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 26 Nov 2023 13:38:08 +0000 Subject: [PATCH 14/55] Remove hash method. it cannot be guaranteed that objects which compare equal have the same hash value --- .pre-commit-config.yaml | 5 ----- docs/HISTORY.rst | 2 +- pygeoif/geometry.py | 7 ------ tests/test_geometrycollection.py | 36 ------------------------------ tests/test_line.py | 6 ----- tests/test_linear_ring.py | 6 ----- tests/test_multiline.py | 8 ------- tests/test_multipoint.py | 6 ----- tests/test_multipolygon.py | 38 -------------------------------- tests/test_point.py | 6 ----- tests/test_polygon.py | 6 ----- 11 files changed, 1 insertion(+), 125 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e822e498..b5a3bbcd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,11 +31,6 @@ repos: rev: v0.3.1 hooks: - id: absolufy-imports - - repo: https://github.com/hakancelikdev/unimport - rev: 1.1.0 - hooks: - - id: unimport - args: [--remove, --include-star-import, --ignore-init, --gitignore] - repo: https://github.com/pycqa/isort rev: 5.12.0 hooks: diff --git a/docs/HISTORY.rst b/docs/HISTORY.rst index 3cec4bf8..2ccdf5fd 100644 --- a/docs/HISTORY.rst +++ b/docs/HISTORY.rst @@ -5,7 +5,7 @@ Changelog ------------------ - remove Python 3.7 support - - Geometries are now immutable and hashable + - Geometries are now immutable (but not hashable) - add ``force_2d`` and ``force_3d`` factories [Alex Svetkin] 1.1.1 (2023/10/27) diff --git a/pygeoif/geometry.py b/pygeoif/geometry.py index 3d6b0396..fec6fcd2 100644 --- a/pygeoif/geometry.py +++ b/pygeoif/geometry.py @@ -67,9 +67,6 @@ def __delattr__(self, *args: Any) -> NoReturn: # noqa: ANN401 msg, ) - def __hash__(self) -> int: - return hash(self._geoms) - def __str__(self) -> str: return self.wkt @@ -1095,10 +1092,6 @@ def __eq__(self, other: object) -> bool: second=other.__geo_interface__, # type: ignore [attr-defined] ) - def __hash__(self) -> int: - """Return the hash of the collection.""" - return hash(self.wkt) - def __len__(self) -> int: """ Length of the collection. diff --git a/tests/test_geometrycollection.py b/tests/test_geometrycollection.py index 59065667..51b016cc 100644 --- a/tests/test_geometrycollection.py +++ b/tests/test_geometrycollection.py @@ -425,39 +425,3 @@ def test_nested_geometry_collection_repr_eval() -> None: ).__geo_interface__ == gc.__geo_interface__ ) - - -def test_nested_geometry_collection_hash() -> None: - multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) - gc1 = geometry.GeometryCollection([geometry.Point(0, 0), multipoint]) - line1 = geometry.LineString([(0, 0), (3, 1)]) - gc2 = geometry.GeometryCollection([gc1, line1]) - poly1 = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) - e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] - i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] - poly2 = geometry.Polygon(e, [i]) - p0 = geometry.Point(0, 0) - p1 = geometry.Point(-1, -1) - ring = geometry.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)]) - line = geometry.LineString([(0, 0), (1, 1)]) - gc = geometry.GeometryCollection([gc2, poly1, poly2, p0, p1, ring, line]) - - assert hash(gc) == hash( - geometry.GeometryCollection( - [ - gc2, - poly1, - poly2, - p0, - p1, - ring, - line, - ], - ), - ) - - -def test_hash_empty() -> None: - gc = geometry.GeometryCollection([]) - - assert hash(gc) == hash(geometry.GeometryCollection([])) diff --git a/tests/test_line.py b/tests/test_line.py index 2d6fd5bf..b0b4df8a 100644 --- a/tests/test_line.py +++ b/tests/test_line.py @@ -19,12 +19,6 @@ def test_coords_get_3d() -> None: assert line.coords == ((0.0, 0.0, 0), (1.0, 1.0, 1)) -def test_hash() -> None: - line = geometry.LineString([(0, 0, 0), (1, 1, 1)]) - - assert hash(line) == hash(geometry.LineString([(0, 0, 0), (1, 1, 1)])) - - def test_set_geoms_raises() -> None: line = geometry.LineString([(0, 0), (1, 0)]) # pragma: no mutate diff --git a/tests/test_linear_ring.py b/tests/test_linear_ring.py index 3a67a879..8e4d3c9b 100644 --- a/tests/test_linear_ring.py +++ b/tests/test_linear_ring.py @@ -240,9 +240,3 @@ def test_empty_bounds() -> None: ring = geometry.LinearRing([]) assert ring.bounds == () - - -def test_hash() -> None: - ring = geometry.LinearRing([(0, 0), (4, 0), (4, 2), (0, 2)]) - - assert hash(ring) == hash(((0, 0), (4, 0), (4, 2), (0, 2), (0, 0))) diff --git a/tests/test_multiline.py b/tests/test_multiline.py index 0aadecae..40d14280 100644 --- a/tests/test_multiline.py +++ b/tests/test_multiline.py @@ -200,11 +200,3 @@ def test_empty_bounds() -> None: lines = geometry.MultiLineString([]) assert lines.bounds == () - - -def test_hash() -> None: - lines = geometry.MultiLineString(([(0, 0), (1, 1)], [[0.0, 0.0], [1.0, 2.0]])) - - assert hash(lines) == hash( - geometry.MultiLineString(([(0, 0), (1, 1)], [[0.0, 0.0], [1.0, 2.0]])), - ) diff --git a/tests/test_multipoint.py b/tests/test_multipoint.py index 9b64bef3..693bad36 100644 --- a/tests/test_multipoint.py +++ b/tests/test_multipoint.py @@ -148,12 +148,6 @@ def test_from_points_unique() -> None: ) -def test_hash() -> None: - multipoint = geometry.MultiPoint([(0, 0), (1, 0), (2, 2)]) - - assert hash(multipoint) == hash(geometry.MultiPoint([(0, 0), (1, 0), (2, 2)])) - - def test_empty() -> None: multipoint = geometry.MultiPoint([(1, None)]) diff --git a/tests/test_multipolygon.py b/tests/test_multipolygon.py index 4ae637ba..967b8fee 100644 --- a/tests/test_multipolygon.py +++ b/tests/test_multipolygon.py @@ -284,41 +284,3 @@ def test_empty_bounds() -> None: polys = geometry.MultiPolygon([]) assert polys.bounds == () - - -def test_hash() -> None: - polys = geometry.MultiPolygon( - ( - ( - ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)), - ( - ((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)), - ((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)), - ), - ), - (((0, 0, 0), (1, 1, 0), (1, 0, 0), (0, 0, 0)),), - ( - ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)), - (((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)),), - ), - ), - ) - - assert hash(polys) == hash( - geometry.MultiPolygon( - ( - ( - ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)), - ( - ((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)), - ((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)), - ), - ), - (((0, 0, 0), (1, 1, 0), (1, 0, 0), (0, 0, 0)),), - ( - ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)), - (((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)),), - ), - ), - ), - ) diff --git a/tests/test_point.py b/tests/test_point.py index c728feb9..f1343026 100644 --- a/tests/test_point.py +++ b/tests/test_point.py @@ -59,12 +59,6 @@ def test_xy() -> None: assert point.y == 0 -def test_hash() -> None: - point = geometry.Point(1.0, 2.0, 3.0) - - assert hash(point) == hash((1.0, 2.0, 3.0)) - - def test_xy_raises_error_accessing_z() -> None: point = geometry.Point(1, 0) diff --git a/tests/test_polygon.py b/tests/test_polygon.py index 07c8b056..4ef462d3 100644 --- a/tests/test_polygon.py +++ b/tests/test_polygon.py @@ -20,12 +20,6 @@ def test_coords_with_holes() -> None: ) -def test_hash() -> None: - polygon = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) - - assert hash(polygon) == hash(geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)])) - - def test_geo_interface_shell_only() -> None: polygon = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) From 13ec2cd5bd366c97e45a6178033499e0380dadd6 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 26 Nov 2023 14:27:14 +0000 Subject: [PATCH 15/55] Add coverage configuration to pyproject.toml --- pyproject.toml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index fe2d5327..bc9eb82e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,6 +96,15 @@ Changelog = "https://github.com/cleder/pygeoif/blob/develop/docs/HISTORY.rst" Documentation = "https://pygeoif.readthedocs.io/" Homepage = "https://github.com/cleder/pygeoif/" +[tool.coverage.paths] +source = [ + "pygeoif", + "tests", +] + +[tool.coverage.run] +branch = true + [tool.flake8] max_line_length = 88 From 9bab53c2bc082d23f140552e2a928b2040ae246b Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 26 Nov 2023 14:36:40 +0000 Subject: [PATCH 16/55] Add test for empty points omission in LineString --- tests/test_line.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_line.py b/tests/test_line.py index b0b4df8a..6cc60f40 100644 --- a/tests/test_line.py +++ b/tests/test_line.py @@ -19,6 +19,12 @@ def test_coords_get_3d() -> None: assert line.coords == ((0.0, 0.0, 0), (1.0, 1.0, 1)) +def test_empty_points_omitted() -> None: + line = geometry.LineString([(0, 0, 0), (None, None, None), (2, 2, 2)]) + + assert line.coords == ((0, 0, 0), (2, 2, 2)) + + def test_set_geoms_raises() -> None: line = geometry.LineString([(0, 0), (1, 0)]) # pragma: no mutate From 295263f5a1f33cc507e74c0c37d1b9ad5702e3c5 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 27 Nov 2023 13:07:53 +0000 Subject: [PATCH 17/55] Add slotscheck hook, include py.typed --- .pre-commit-config.yaml | 4 ++++ pyproject.toml | 1 + 2 files changed, 5 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b5a3bbcd..b3b6eaf5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,6 +31,10 @@ repos: rev: v0.3.1 hooks: - id: absolufy-imports + - repo: https://github.com/ariebovenberg/slotscheck + rev: v0.17.1 + hooks: + - id: slotscheck - repo: https://github.com/pycqa/isort rev: 5.12.0 hooks: diff --git a/pyproject.toml b/pyproject.toml index bc9eb82e..f16ae7e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -262,4 +262,5 @@ exclude = [ ] include = [ "pygeoif*", + "pygeoif/py.typed", ] From dd761ee7541fcecf0ab0637d7f70f396b431539b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 16:34:39 +0000 Subject: [PATCH 18/55] [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/mirrors-mypy: v1.7.0 → v1.7.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.7.0...v1.7.1) - [github.com/python-jsonschema/check-jsonschema: 0.27.1 → 0.27.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.27.1...0.27.2) --- .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 b3b6eaf5..7d5a794e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -72,7 +72,7 @@ repos: - flake8-typing-imports - flake8-use-fstring - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.0 + rev: v1.7.1 hooks: - id: mypy # - repo: https://github.com/Lucas-C/pre-commit-hooks-markup @@ -88,7 +88,7 @@ repos: hooks: - id: pyprojectsort - repo: https://github.com/python-jsonschema/check-jsonschema - rev: "0.27.1" + rev: "0.27.2" hooks: - id: check-github-workflows - id: check-github-actions From 5dba7716fbd498c72080d0f981eb8ff6fb35d031 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 27 Nov 2023 17:14:58 +0000 Subject: [PATCH 19/55] Remove slotscheck hook from pre-commit-config.yaml --- .pre-commit-config.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7d5a794e..258362e7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,10 +31,6 @@ repos: rev: v0.3.1 hooks: - id: absolufy-imports - - repo: https://github.com/ariebovenberg/slotscheck - rev: v0.17.1 - hooks: - - id: slotscheck - repo: https://github.com/pycqa/isort rev: 5.12.0 hooks: From 0e39e327489ab70461073a7e83f3ffe3083163e9 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 27 Nov 2023 17:25:59 +0000 Subject: [PATCH 20/55] Release notes 1.2.0 --- docs/HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/HISTORY.rst b/docs/HISTORY.rst index 2ccdf5fd..6e5c9718 100644 --- a/docs/HISTORY.rst +++ b/docs/HISTORY.rst @@ -1,7 +1,7 @@ Changelog ========= -1.2.0 (unreleased) +1.2.0 (2023/11/27) ------------------ - remove Python 3.7 support From f1767878c76fa6e43c3d555cb3b47874c9f1a253 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 27 Nov 2023 18:20:45 +0000 Subject: [PATCH 21/55] Update version number to 1.3.0 --- 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 6e5c9718..0f0331ed 100644 --- a/docs/HISTORY.rst +++ b/docs/HISTORY.rst @@ -1,6 +1,10 @@ Changelog ========= +1.3.0 (unreleased) +------------------ + + 1.2.0 (2023/11/27) ------------------ diff --git a/pygeoif/about.py b/pygeoif/about.py index 10c86489..9145bc88 100644 --- a/pygeoif/about.py +++ b/pygeoif/about.py @@ -3,4 +3,4 @@ The only purpose of this module is to provide a version number for the package. """ -__version__ = "1.2.0" +__version__ = "1.3.0" From 61e7299ce58f3fb8aef397fb31b5216d2848ad10 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:23:15 +0000 Subject: [PATCH 22/55] Bump actions/setup-python from 4 to 5 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/run-all-tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index 75891c36..4cf1a2cd 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -41,7 +41,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -71,7 +71,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.pypy-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.pypy-version }} - name: Install dependencies @@ -90,7 +90,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python 3.12 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.12 - name: Install pypa/build From 46a7a903f9f972f2c635d15bec861ae1d73e3ec8 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 7 Dec 2023 09:25:36 +0000 Subject: [PATCH 23/55] Add GraalPy workflow for running tests --- .github/workflows/run-all-tests.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index 4cf1a2cd..472a5b4c 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -82,6 +82,25 @@ jobs: run: | pytest tests + graalpy: + runs-on: ubuntu-latest + strategy: + matrix: + graalpy-version: ['graalpy-22.3'] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.graalpy-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.graalpy-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -e ".[tests]" + - name: Test with pytest + run: | + pytest tests + publish: if: "github.event_name == 'push' && github.repository == 'cleder/pygeoif'" needs: [cpython, static-tests, pypy] From 4a4e94d92f342a2a8e60f3124cd3695716ce51e8 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 7 Dec 2023 09:29:26 +0000 Subject: [PATCH 24/55] Remove graalpy test job --- .github/workflows/run-all-tests.yml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index 472a5b4c..4cf1a2cd 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -82,25 +82,6 @@ jobs: run: | pytest tests - graalpy: - runs-on: ubuntu-latest - strategy: - matrix: - graalpy-version: ['graalpy-22.3'] - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.graalpy-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.graalpy-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install -e ".[tests]" - - name: Test with pytest - run: | - pytest tests - publish: if: "github.event_name == 'push' && github.repository == 'cleder/pygeoif'" needs: [cpython, static-tests, pypy] From d00aaad03af04d9efe3d38e6228fa2e6ff79b0b9 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 7 Dec 2023 09:30:41 +0000 Subject: [PATCH 25/55] Add GraalPy workflow for running tests --- .github/workflows/run-all-tests.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index 4cf1a2cd..1ac83615 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -82,6 +82,25 @@ jobs: run: | pytest tests + graalpy: + runs-on: ubuntu-latest + strategy: + matrix: + graalpy-version: ['graalpy-22.3'] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.graalpy-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.graalpy-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install pytest + - name: Test with pytest + run: | + pytest tests + publish: if: "github.event_name == 'push' && github.repository == 'cleder/pygeoif'" needs: [cpython, static-tests, pypy] From 12604b7bb8b12d8519222933c9b756af57c43882 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 7 Dec 2023 09:33:13 +0000 Subject: [PATCH 26/55] Add typing_extensions to pytest dependencies --- .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 1ac83615..b0a01d25 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -96,7 +96,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install pytest + python -m pip install pytest typing_extensions - name: Test with pytest run: | pytest tests From d6d2eec71948b8fed47c4892172914a85786c498 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 7 Dec 2023 09:35:52 +0000 Subject: [PATCH 27/55] Remove graalpy workflow --- .github/workflows/run-all-tests.yml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index b0a01d25..4cf1a2cd 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -82,25 +82,6 @@ jobs: run: | pytest tests - graalpy: - runs-on: ubuntu-latest - strategy: - matrix: - graalpy-version: ['graalpy-22.3'] - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.graalpy-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.graalpy-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install pytest typing_extensions - - name: Test with pytest - run: | - pytest tests - publish: if: "github.event_name == 'push' && github.repository == 'cleder/pygeoif'" needs: [cpython, static-tests, pypy] From 35eebf374734ef83a97134f95437e93a1cb220b2 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 9 Dec 2023 12:25:50 +0000 Subject: [PATCH 28/55] Add Codium AI PR Agent workflow --- .github/dependabot.yml | 2 -- .github/workflows/pr_agent.yml | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/pr_agent.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 10902aec..335e8c0a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,5 +1,3 @@ -# Set update schedule for GitHub Actions - version: 2 updates: - package-ecosystem: "github-actions" diff --git a/.github/workflows/pr_agent.yml b/.github/workflows/pr_agent.yml new file mode 100644 index 00000000..74742d7b --- /dev/null +++ b/.github/workflows/pr_agent.yml @@ -0,0 +1,15 @@ +on: [pull_request, issue_comment] +jobs: + pr_agent_job: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + contents: write + name: Run pr agent on every pull request, respond to user comments + steps: + - name: PR Agent action step + uses: Codium-ai/pr-agent@main + env: + OPENAI_KEY: ${{ secrets.OPENAI_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 608aea0dfee77aec65ee547bb9761c9247de3074 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 9 Dec 2023 19:26:29 +0000 Subject: [PATCH 29/55] Refactor __geo_interface__ to use property --- pygeoif/types.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pygeoif/types.py b/pygeoif/types.py index e9de16f6..9738b64f 100644 --- a/pygeoif/types.py +++ b/pygeoif/types.py @@ -87,13 +87,17 @@ class GeoFeatureCollectionInterface(TypedDict): class GeoType(Protocol): """Any compatible type that implements the __geo_interface__.""" - __geo_interface__: GeoInterface + @property + def __geo_interface__(self) -> GeoInterface: + """Return the GeoInterface.""" class GeoCollectionType(Protocol): """Any compatible type that implements the __geo_interface__.""" - __geo_interface__: GeoCollectionInterface + @property + def __geo_interface__(self) -> GeoCollectionInterface: + """Return the GeoInterface.""" __all__ = [ From 9b7e2313aa4403253b78b248ce0a13abf0aab938 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 9 Dec 2023 21:11:29 +0000 Subject: [PATCH 30/55] Add badges and update classifiers --- README.rst | 12 ++++++++++++ pyproject.toml | 1 + 2 files changed, 13 insertions(+) diff --git a/README.rst b/README.rst index 64873415..bd03e620 100644 --- a/README.rst +++ b/README.rst @@ -76,6 +76,18 @@ It was written to provide clean and python only geometries for fastkml_ :target: https://pypi.python.org/pypi/pygeoif/ :alt: Supported Python implementations +.. 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 + :target: https://pypi.python.org/pypi/pygeoif/ + :alt: License + +.. image:: https://img.shields.io/pypi/dm/pygeoif.svg + :target: https://pypi.python.org/pypi/pygeoif/ + :alt: Downloads + Installation ------------ diff --git a/pyproject.toml b/pyproject.toml index f16ae7e2..85cd5d9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Scientific/Engineering :: GIS", + "Typing :: Typed", ] dependencies = [ "typing_extensions", From 3b52ddaf4ce4244c0764a92c5f1519e24d4d7839 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 16:36:44 +0000 Subject: [PATCH 31/55] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pycqa/isort: 5.12.0 → 5.13.0](https://github.com/pycqa/isort/compare/5.12.0...5.13.0) - [github.com/astral-sh/ruff-pre-commit: v0.1.6 → v0.1.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.6...v0.1.7) - [github.com/python-jsonschema/check-jsonschema: 0.27.2 → 0.27.3](https://github.com/python-jsonschema/check-jsonschema/compare/0.27.2...0.27.3) --- .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 258362e7..218229e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: hooks: - id: absolufy-imports - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 5.13.0 hooks: - id: isort - repo: https://github.com/psf/black @@ -40,7 +40,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.1.6' + rev: 'v0.1.7' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] @@ -84,7 +84,7 @@ repos: hooks: - id: pyprojectsort - repo: https://github.com/python-jsonschema/check-jsonschema - rev: "0.27.2" + rev: "0.27.3" hooks: - id: check-github-workflows - id: check-github-actions From 024592ff5c292930f4a8fa441887d7719e17a317 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 1 Feb 2024 09:42:24 +0000 Subject: [PATCH 32/55] update codecov --- .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 4cf1a2cd..2b091f68 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -30,7 +30,7 @@ jobs: with: fail_ci_if_error: true verbose: true - token: ${{ env.CODECOV_TOKEN }} + token: ${{ secrets.CODECOV_TOKEN }} static-tests: runs-on: ubuntu-latest From 1c3f13e4ae2b2519cac5c6eced16eddf036cfc49 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 21 Mar 2024 10:56:30 +0000 Subject: [PATCH 33/55] Add hypothesis strategies for generating points in different spatial reference systems --- pygeoif/hypothesis/__init__.py | 1 + pygeoif/hypothesis/strategies.py | 130 +++++++++++++++++++++++++++++++ tests/conftest.py | 52 ------------- tests/hypothesis/__init__.py | 1 + tests/hypothesis/test_point.py | 30 +++++++ 5 files changed, 162 insertions(+), 52 deletions(-) create mode 100644 pygeoif/hypothesis/__init__.py create mode 100644 pygeoif/hypothesis/strategies.py delete mode 100644 tests/conftest.py create mode 100644 tests/hypothesis/__init__.py create mode 100644 tests/hypothesis/test_point.py diff --git a/pygeoif/hypothesis/__init__.py b/pygeoif/hypothesis/__init__.py new file mode 100644 index 00000000..0797f949 --- /dev/null +++ b/pygeoif/hypothesis/__init__.py @@ -0,0 +1 @@ +"""Hypothesis strategies for pygeoif.""" diff --git a/pygeoif/hypothesis/strategies.py b/pygeoif/hypothesis/strategies.py new file mode 100644 index 00000000..184a4350 --- /dev/null +++ b/pygeoif/hypothesis/strategies.py @@ -0,0 +1,130 @@ +"""Data-generating strategies for property-based testing.""" + +from dataclasses import dataclass +from functools import partial +from typing import Callable +from typing import Optional +from typing import Tuple +from typing import TypeVar +from typing import Union +from typing import cast + +import hypothesis.strategies as st + +from pygeoif.types import Point2D +from pygeoif.types import Point3D + +__all__ = ["Srs", "epsg4326", "point_types", "points_2d", "points_3d"] + +T = TypeVar("T") +Draw = TypeVar( + "Draw", + bound=Callable[[st.SearchStrategy[T]], T], # type: ignore [valid-type] +) + + +@dataclass(frozen=True) +class Srs: + """ + Represents a spatial reference system (SRS). + + Attributes + ---------- + name (str): The name of the SRS. + min_xyz (Tuple[Optional[float], Optional[float], Optional[float]]): + The minimum x, y, and z values of the SRS. + max_xyz (Tuple[Optional[float], Optional[float], Optional[float]]): + The maximum x, y, and z values of the SRS. + + """ + + name: str + min_xyz: Tuple[Optional[float], Optional[float], Optional[float]] + max_xyz: Tuple[Optional[float], Optional[float], Optional[float]] + + +epsg4326 = Srs( + name="EPSG:4326", + min_xyz=(-180.0, -90.0, -1_000_000_000_000.0), + max_xyz=(180.0, 90.0, 1_000_000_000_000.0), +) + +coordinate = partial(st.floats, allow_infinity=False, allow_nan=False) + + +@st.composite +def points_2d( + draw: Draw, + srs: Optional[Srs] = None, +) -> st.SearchStrategy[Point2D]: + """ + Generate 2D points using the given draw function. + + Args: + ---- + draw: The draw function used to generate the points. + srs: Optional spatial reference system (Srs) object. + + Returns: + ------- + A tuple of latitude and longitude values generated using the draw function. + + """ + longitudes = coordinate() + latitudes = coordinate() + if srs: + longitudes = coordinate(min_value=srs.min_xyz[0], max_value=srs.max_xyz[0]) + latitudes = coordinate(min_value=srs.min_xyz[1], max_value=srs.max_xyz[1]) + return cast(st.SearchStrategy[Point2D], draw(st.tuples(latitudes, longitudes))) + + +@st.composite +def points_3d( + draw: Draw, + srs: Optional[Srs] = None, +) -> st.SearchStrategy[Point3D]: + """ + Generate 3D points using the given draw function. + + Args: + ---- + draw: The draw function used to generate the points. + srs: Optional spatial reference system (Srs) object. + + Returns: + ------- + A tuple representing the generated 3D points. + + """ + longitudes = coordinate() + latitudes = coordinate() + elevations = coordinate() + if srs: + longitudes = coordinate(min_value=srs.min_xyz[0], max_value=srs.max_xyz[0]) + latitudes = coordinate(min_value=srs.min_xyz[1], max_value=srs.max_xyz[1]) + elevations = coordinate(min_value=srs.min_xyz[2], max_value=srs.max_xyz[2]) + return cast( + st.SearchStrategy[Point3D], + draw(st.tuples(latitudes, longitudes, elevations)), + ) + + +@st.composite +def point_types(draw: Draw, srs: Optional[Srs] = None) -> Union[ + st.SearchStrategy[Point2D], + st.SearchStrategy[Point3D], +]: + """ + Generate a random point in either 2D or 3D space. + + Args: + ---- + draw: The draw function from the hypothesis library. + srs: An optional parameter specifying the spatial reference system. + + Returns: + ------- + A randomly generated point in either 2D or 3D space. + + """ + return draw(st.one_of(points_2d(srs), points_3d(srs))) diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index a6365803..00000000 --- a/tests/conftest.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Data-generating strategies for property-based testing.""" -import hypothesis.strategies as st - -# Incomplete list of allowed spatial reference systems to generate data -# - EPSG 4326 -# - EPSG 3857 -# ...? - - -# EPSG:4326 primitives -latitudes = st.floats( - min_value=-90.0, - max_value=90.0, - allow_nan=False, - allow_infinity=False, -) -longitudes = st.floats( - min_value=-180.0, - max_value=180.0, - allow_nan=False, - allow_infinity=False, -) -elevations = st.floats(allow_nan=False, allow_infinity=False) - - -# Point2D -@st.composite -def points_2d(draw, srs="EPSG:4326"): - if srs == "EPSG:4326": - return draw(st.tuples(latitudes, longitudes)) - raise NotImplementedError - - -# Point3D -@st.composite -def points_3d(draw, srs="EPSG:4326"): - if srs == "EPSG:4326": - return draw(st.tuples(latitudes, longitudes, elevations)) - raise NotImplementedError - - -# PointType -@st.composite -def points(draw, srs="EPSG:4326"): - if srs == "EPSG:4326": - return draw(st.one_of(points_2d(), points_3d())) - raise NotImplementedError - - -# LineType - -# Geometries diff --git a/tests/hypothesis/__init__.py b/tests/hypothesis/__init__.py new file mode 100644 index 00000000..e0f40b83 --- /dev/null +++ b/tests/hypothesis/__init__.py @@ -0,0 +1 @@ +"""Hypothesis tests for the project.""" diff --git a/tests/hypothesis/test_point.py b/tests/hypothesis/test_point.py new file mode 100644 index 00000000..5bba9f14 --- /dev/null +++ b/tests/hypothesis/test_point.py @@ -0,0 +1,30 @@ +"""Test the Point class using Hypothesis.""" + +from hypothesis import given + +from pygeoif import geometry +from pygeoif.factories import from_wkt +from pygeoif.factories import shape +from pygeoif.hypothesis.strategies import epsg4326 +from pygeoif.hypothesis.strategies import point_types + + +@given(point_types(epsg4326)) +def test_repr_eval_epsg_4326(point) -> None: + point = geometry.Point(*point) + + assert eval(repr(point), {}, {"Point": geometry.Point}) == point + + +@given(point_types(epsg4326)) +def test_shape_epsg_4326(point) -> None: + point = geometry.Point(*point) + + assert point == shape(point) + + +@given(point_types(epsg4326)) +def test_from_wkt_epsg_4326(point) -> None: + point = geometry.Point(*point) + + assert point == from_wkt(str(point)) From 74d6b7ee943f6cb1aa02cb56a3a61796cba5c4af Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 21 Mar 2024 11:25:20 +0000 Subject: [PATCH 34/55] Add hypothesis as an additional dependency for mypy and typing --- .pre-commit-config.yaml | 2 ++ pyproject.toml | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 51bab723..b8f5b42f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -71,6 +71,8 @@ repos: rev: v1.9.0 hooks: - id: mypy + additional_dependencies: + - hypothesis # - repo: https://github.com/Lucas-C/pre-commit-hooks-markup # rev: v1.0.1 # hooks: diff --git a/pyproject.toml b/pyproject.toml index 87a7ecd7..cbfe8963 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,9 @@ dev = [ "pygeoif[tests]", "pygeoif[typing]", ] +hypothesis = [ + "hypothesis", +] linting = [ "black", "flake8", @@ -85,6 +88,7 @@ tests = [ "pytest-cov", ] typing = [ + "hypothesis", "mypy", ] From 1e39ba2b4cb93d460cc0a91184706cdbaff88a9d Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 21 Mar 2024 13:46:43 +0000 Subject: [PATCH 35/55] differentiate 2D and 3D types --- pygeoif/factories.py | 38 ++++++++++++++++++++------------ pygeoif/functions.py | 5 +++-- pygeoif/geometry.py | 16 +++++++++----- pygeoif/hypothesis/strategies.py | 4 ++-- pygeoif/types.py | 23 +++++++++++++------ 5 files changed, 55 insertions(+), 31 deletions(-) diff --git a/pygeoif/factories.py b/pygeoif/factories.py index ff8e32ee..3e44b758 100644 --- a/pygeoif/factories.py +++ b/pygeoif/factories.py @@ -35,11 +35,11 @@ from pygeoif.geometry import MultiPolygon from pygeoif.geometry import Point from pygeoif.geometry import Polygon -from pygeoif.types import Exteriors from pygeoif.types import GeoCollectionInterface from pygeoif.types import GeoCollectionType from pygeoif.types import GeoInterface from pygeoif.types import GeoType +from pygeoif.types import Interiors from pygeoif.types import LineType from pygeoif.types import PointType from pygeoif.types import PolygonType @@ -188,27 +188,34 @@ def _point_from_wkt_coordinates(coordinates: str) -> Point: def _line_from_wkt_coordinates(coordinates: str) -> LineString: coords = coordinates.split(",") return LineString( - [cast(PointType, tuple(num(c) for c in coord.split())) for coord in coords], + cast( + LineType, # + [tuple(num(c) for c in coord.split()) for coord in coords], + ), ) def _ring_from_wkt_coordinates(coordinates: str) -> LinearRing: coords = coordinates.split(",") return LinearRing( - [cast(PointType, tuple(num(c) for c in coord.split())) for coord in coords], + cast( + LineType, + [tuple(num(c) for c in coord.split()) for coord in coords], + ), ) def _shell_holes_from_wkt_coords( coords: List[str], -) -> Tuple[LineType, Exteriors]: +) -> Tuple[LineType, Interiors]: """Extract shell and holes from polygon wkt coordinates.""" - interior: LineType = [ - cast(PointType, tuple(num(c) for c in coord.split())) for coord in coords[0] - ] + exterior: LineType = cast( + LineType, + [tuple(num(c) for c in coord.split()) for coord in coords[0]], + ) if len(coords) > 1: # we have a polygon with holes - exteriors = [ + interiors = [ cast( LineType, [ @@ -219,8 +226,8 @@ def _shell_holes_from_wkt_coords( for ext in coords[1:] ] else: - exteriors = None - return interior, exteriors + interiors = None + return exterior, interiors def _polygon_from_wkt_coordinates(coordinates: str) -> Polygon: @@ -243,10 +250,13 @@ def _multipoint_from_wkt_coordinates(coordinates: str) -> MultiPoint: def _multiline_from_wkt_coordinates(coordinates: str) -> MultiLineString: coords = [ - [ - cast(PointType, tuple(num(c) for c in coord.split())) - for coord in lines.strip("()").split(",") - ] + cast( + LineType, + [ + tuple(num(c) for c in coord.split()) + for coord in lines.strip("()").split(",") + ], + ) for lines in inner.findall(coordinates) ] return MultiLineString(coords) diff --git a/pygeoif/functions.py b/pygeoif/functions.py index 19cd7b75..8d6d24f6 100644 --- a/pygeoif/functions.py +++ b/pygeoif/functions.py @@ -28,6 +28,7 @@ from pygeoif.types import CoordinatesType from pygeoif.types import GeoCollectionInterface from pygeoif.types import GeoInterface +from pygeoif.types import Line2D from pygeoif.types import LineType from pygeoif.types import MultiCoordinatesType from pygeoif.types import Point2D @@ -95,7 +96,7 @@ def _build_hull(points: Iterable[Point2D]) -> List[Point2D]: return hull -def convex_hull(points: Iterable[Point2D]) -> LineType: +def convex_hull(points: Iterable[Point2D]) -> Line2D: """ Compute the convex hull of a set of 2D points. @@ -137,7 +138,7 @@ def convex_hull(points: Iterable[Point2D]) -> LineType: def dedupe(coords: LineType) -> LineType: """Remove duplicate Points from a LineString.""" - return tuple(coord for coord, _count in groupby(coords)) + return cast(LineType, tuple(coord for coord, _count in groupby(coords))) def compare_coordinates( diff --git a/pygeoif/geometry.py b/pygeoif/geometry.py index 63ba77ba..c500ebb1 100644 --- a/pygeoif/geometry.py +++ b/pygeoif/geometry.py @@ -376,7 +376,7 @@ def geoms(self) -> Tuple[Point, ...]: @property def coords(self) -> LineType: """Return the geometry coordinates.""" - return tuple(point.coords[0] for point in self.geoms) + return cast(LineType, tuple(point.coords[0] for point in self.geoms)) @property def is_empty(self) -> bool: @@ -404,14 +404,14 @@ def __geo_interface__(self) -> GeoInterface: return geo_interface @classmethod - def from_coordinates(cls, coordinates: Sequence[PointType]) -> "LineString": + def from_coordinates(cls, coordinates: LineType) -> "LineString": """Construct a linestring from coordinates.""" return cls(coordinates) @classmethod def from_points(cls, *args: Point) -> "LineString": """Create a linestring from points.""" - return cls(tuple(point.coords[0] for point in args)) + return cls(cast(LineType, tuple(point.coords[0] for point in args))) @classmethod def _from_dict(cls, geo_interface: GeoInterface) -> "LineString": @@ -578,10 +578,14 @@ def coords(self) -> PolygonType: Note that this is not implemented in Shapely. """ if self._geoms[1]: - return self.exterior.coords, tuple( - interior.coords for interior in self.interiors if interior + return cast( + PolygonType, + ( + self.exterior.coords, + tuple(interior.coords for interior in self.interiors if interior), + ), ) - return (self.exterior.coords,) + return cast(PolygonType, (self.exterior.coords,)) @property def has_z(self) -> Optional[bool]: diff --git a/pygeoif/hypothesis/strategies.py b/pygeoif/hypothesis/strategies.py index 184a4350..c188425a 100644 --- a/pygeoif/hypothesis/strategies.py +++ b/pygeoif/hypothesis/strategies.py @@ -32,9 +32,9 @@ class Srs: ---------- name (str): The name of the SRS. min_xyz (Tuple[Optional[float], Optional[float], Optional[float]]): - The minimum x, y, and z values of the SRS. + - The minimum x, y, and z values of the SRS. max_xyz (Tuple[Optional[float], Optional[float], Optional[float]]): - The maximum x, y, and z values of the SRS. + - The maximum x, y, and z values of the SRS. """ diff --git a/pygeoif/types.py b/pygeoif/types.py index 9738b64f..0e6a3bd4 100644 --- a/pygeoif/types.py +++ b/pygeoif/types.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2012 -2022 Christian Ledermann +# Copyright (C) 2012 -2024 Christian Ledermann # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -30,14 +30,23 @@ Point2D = Tuple[float, float] Point3D = Tuple[float, float, float] PointType = Union[Point2D, Point3D] -LineType = Sequence[PointType] +Line2D = Sequence[Point2D] +Line3D = Sequence[Point3D] +LineType = Union[Line2D, Line3D] +Interiors = Optional[Sequence[LineType]] +Poly2d = Union[ + Tuple[Line2D, Sequence[Line2D]], + Tuple[Line2D], +] +Poly3d = Union[ + Tuple[Line3D, Sequence[Line3D]], + Tuple[Line3D], +] PolygonType = Union[ - Tuple[LineType, Sequence[LineType]], - Tuple[LineType], + Poly2d, + Poly3d, ] -Exteriors = Optional[Sequence[LineType]] - MultiGeometryType = Sequence[Union[PointType, LineType, PolygonType]] Bounds = Tuple[float, float, float, float] @@ -103,7 +112,7 @@ def __geo_interface__(self) -> GeoCollectionInterface: __all__ = [ "Bounds", "CoordinatesType", - "Exteriors", + "Interiors", "GeoCollectionInterface", "GeoCollectionType", "GeoFeatureCollectionInterface", From 1cd9b67d845d2b57c9ece677894d81154b5d75a4 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 21 Mar 2024 15:07:53 +0000 Subject: [PATCH 36/55] Add point generation strategy --- pygeoif/hypothesis/strategies.py | 65 +++++++++++++++++++------------- pygeoif/types.py | 2 +- tests/hypothesis/test_point.py | 31 +++++++++++++-- 3 files changed, 66 insertions(+), 32 deletions(-) diff --git a/pygeoif/hypothesis/strategies.py b/pygeoif/hypothesis/strategies.py index c188425a..73b654ff 100644 --- a/pygeoif/hypothesis/strategies.py +++ b/pygeoif/hypothesis/strategies.py @@ -2,25 +2,24 @@ from dataclasses import dataclass from functools import partial -from typing import Callable from typing import Optional from typing import Tuple -from typing import TypeVar -from typing import Union -from typing import cast import hypothesis.strategies as st +from pygeoif.geometry import Point from pygeoif.types import Point2D from pygeoif.types import Point3D +from pygeoif.types import PointType -__all__ = ["Srs", "epsg4326", "point_types", "points_2d", "points_3d"] - -T = TypeVar("T") -Draw = TypeVar( - "Draw", - bound=Callable[[st.SearchStrategy[T]], T], # type: ignore [valid-type] -) +__all__ = [ + "Srs", + "epsg4326", + "point_coords", + "point_coords_2d", + "point_coords_3d", + "points", +] @dataclass(frozen=True) @@ -53,10 +52,10 @@ class Srs: @st.composite -def points_2d( - draw: Draw, +def point_coords_2d( + draw: st.DrawFn, srs: Optional[Srs] = None, -) -> st.SearchStrategy[Point2D]: +) -> Point2D: """ Generate 2D points using the given draw function. @@ -75,14 +74,14 @@ def points_2d( if srs: longitudes = coordinate(min_value=srs.min_xyz[0], max_value=srs.max_xyz[0]) latitudes = coordinate(min_value=srs.min_xyz[1], max_value=srs.max_xyz[1]) - return cast(st.SearchStrategy[Point2D], draw(st.tuples(latitudes, longitudes))) + return draw(st.tuples(latitudes, longitudes)) @st.composite -def points_3d( - draw: Draw, +def point_coords_3d( + draw: st.DrawFn, srs: Optional[Srs] = None, -) -> st.SearchStrategy[Point3D]: +) -> Point3D: """ Generate 3D points using the given draw function. @@ -103,17 +102,29 @@ def points_3d( longitudes = coordinate(min_value=srs.min_xyz[0], max_value=srs.max_xyz[0]) latitudes = coordinate(min_value=srs.min_xyz[1], max_value=srs.max_xyz[1]) elevations = coordinate(min_value=srs.min_xyz[2], max_value=srs.max_xyz[2]) - return cast( - st.SearchStrategy[Point3D], - draw(st.tuples(latitudes, longitudes, elevations)), - ) + return draw(st.tuples(latitudes, longitudes, elevations)) + + +@st.composite +def point_coords(draw: st.DrawFn, srs: Optional[Srs] = None) -> PointType: + """ + Generate a random point in either 2D or 3D space. + + Args: + ---- + draw: The draw function from the hypothesis library. + srs: An optional parameter specifying the spatial reference system. + + Returns: + ------- + A randomly generated point in either 2D or 3D space. + + """ + return draw(st.one_of(point_coords_2d(srs), point_coords_3d(srs))) @st.composite -def point_types(draw: Draw, srs: Optional[Srs] = None) -> Union[ - st.SearchStrategy[Point2D], - st.SearchStrategy[Point3D], -]: +def points(draw: st.DrawFn, srs: Optional[Srs] = None) -> Point: """ Generate a random point in either 2D or 3D space. @@ -127,4 +138,4 @@ def point_types(draw: Draw, srs: Optional[Srs] = None) -> Union[ A randomly generated point in either 2D or 3D space. """ - return draw(st.one_of(points_2d(srs), points_3d(srs))) + return Point(*draw(point_coords(srs))) diff --git a/pygeoif/types.py b/pygeoif/types.py index 0e6a3bd4..f12ec899 100644 --- a/pygeoif/types.py +++ b/pygeoif/types.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2012 -2024 Christian Ledermann +# Copyright (C) 2012 - 2024 Christian Ledermann # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public diff --git a/tests/hypothesis/test_point.py b/tests/hypothesis/test_point.py index 5bba9f14..78a2a370 100644 --- a/tests/hypothesis/test_point.py +++ b/tests/hypothesis/test_point.py @@ -3,28 +3,51 @@ from hypothesis import given from pygeoif import geometry +from pygeoif.factories import force_2d from pygeoif.factories import from_wkt from pygeoif.factories import shape from pygeoif.hypothesis.strategies import epsg4326 -from pygeoif.hypothesis.strategies import point_types +from pygeoif.hypothesis.strategies import point_coords +from pygeoif.hypothesis.strategies import points -@given(point_types(epsg4326)) +@given(point_coords(epsg4326)) def test_repr_eval_epsg_4326(point) -> None: point = geometry.Point(*point) assert eval(repr(point), {}, {"Point": geometry.Point}) == point -@given(point_types(epsg4326)) +@given(point_coords(epsg4326)) def test_shape_epsg_4326(point) -> None: point = geometry.Point(*point) assert point == shape(point) -@given(point_types(epsg4326)) +@given(point_coords(epsg4326)) def test_from_wkt_epsg_4326(point) -> None: point = geometry.Point(*point) assert point == from_wkt(str(point)) + + +@given(points()) +def test_repr_eval(point) -> None: + assert eval(repr(point), {}, {"Point": geometry.Point}) == point + + +@given(points()) +def test_shape(point) -> None: + assert point == shape(point) + + +@given(points()) +def test_bounds(point: geometry.Point) -> None: + assert point.bounds == (point.x, point.y, point.x, point.y) + assert point.bounds == force_2d(point).bounds + + +@given(points()) +def test_convex_hull(point: geometry.Point) -> None: + assert point.convex_hull == force_2d(point) From f0640cca4787cdaa53fa7bebde583fcd47e36269 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 21 Mar 2024 15:37:14 +0000 Subject: [PATCH 37/55] Refactor point_coords function to support generating 2D or 3D points --- pygeoif/hypothesis/strategies.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/pygeoif/hypothesis/strategies.py b/pygeoif/hypothesis/strategies.py index 73b654ff..f37deaa4 100644 --- a/pygeoif/hypothesis/strategies.py +++ b/pygeoif/hypothesis/strategies.py @@ -16,8 +16,6 @@ "Srs", "epsg4326", "point_coords", - "point_coords_2d", - "point_coords_3d", "points", ] @@ -52,7 +50,7 @@ class Srs: @st.composite -def point_coords_2d( +def _point_coords_2d( draw: st.DrawFn, srs: Optional[Srs] = None, ) -> Point2D: @@ -78,7 +76,7 @@ def point_coords_2d( @st.composite -def point_coords_3d( +def _point_coords_3d( draw: st.DrawFn, srs: Optional[Srs] = None, ) -> Point3D: @@ -106,7 +104,11 @@ def point_coords_3d( @st.composite -def point_coords(draw: st.DrawFn, srs: Optional[Srs] = None) -> PointType: +def point_coords( + draw: st.DrawFn, + srs: Optional[Srs] = None, + has_z: Optional[bool] = None, +) -> PointType: """ Generate a random point in either 2D or 3D space. @@ -114,17 +116,26 @@ def point_coords(draw: st.DrawFn, srs: Optional[Srs] = None) -> PointType: ---- draw: The draw function from the hypothesis library. srs: An optional parameter specifying the spatial reference system. + has_z: An optional parameter specifying whether to generate 2D or 3D points. Returns: ------- - A randomly generated point in either 2D or 3D space. + A tuple representing the point in either 2D or 3D space. """ - return draw(st.one_of(point_coords_2d(srs), point_coords_3d(srs))) + if has_z is True: + return draw(_point_coords_3d(srs)) + if has_z is False: + return draw(_point_coords_2d(srs)) + return draw(st.one_of(_point_coords_2d(srs), _point_coords_3d(srs))) @st.composite -def points(draw: st.DrawFn, srs: Optional[Srs] = None) -> Point: +def points( + draw: st.DrawFn, + srs: Optional[Srs] = None, + has_z: Optional[bool] = None, +) -> Point: """ Generate a random point in either 2D or 3D space. @@ -132,10 +143,11 @@ def points(draw: st.DrawFn, srs: Optional[Srs] = None) -> Point: ---- draw: The draw function from the hypothesis library. srs: An optional parameter specifying the spatial reference system. + has_z: An optional parameter specifying whether to generate 2D or 3D points. Returns: ------- A randomly generated point in either 2D or 3D space. """ - return Point(*draw(point_coords(srs))) + return Point(*draw(point_coords(srs, has_z))) From 8ff098b45561bf7b24142846033f9eec8960c467 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 21 Mar 2024 15:43:14 +0000 Subject: [PATCH 38/55] remove redundant tests --- tests/hypothesis/test_point.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/hypothesis/test_point.py b/tests/hypothesis/test_point.py index 78a2a370..b50dcbd3 100644 --- a/tests/hypothesis/test_point.py +++ b/tests/hypothesis/test_point.py @@ -11,20 +11,6 @@ from pygeoif.hypothesis.strategies import points -@given(point_coords(epsg4326)) -def test_repr_eval_epsg_4326(point) -> None: - point = geometry.Point(*point) - - assert eval(repr(point), {}, {"Point": geometry.Point}) == point - - -@given(point_coords(epsg4326)) -def test_shape_epsg_4326(point) -> None: - point = geometry.Point(*point) - - assert point == shape(point) - - @given(point_coords(epsg4326)) def test_from_wkt_epsg_4326(point) -> None: point = geometry.Point(*point) From 41ac669d74b907a5d84f887bbc8ec9ea87a02f69 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 21 Mar 2024 21:12:40 +0000 Subject: [PATCH 39/55] Add line string tests to hypothesis module --- pygeoif/functions.py | 71 ++++++++++++--------------- pygeoif/hypothesis/strategies.py | 84 ++++++++++++++++++++++++++++++++ tests/hypothesis/test_line.py | 45 +++++++++++++++++ tests/hypothesis/test_point.py | 7 +-- tests/test_functions.py | 8 +-- 5 files changed, 169 insertions(+), 46 deletions(-) create mode 100644 tests/hypothesis/test_line.py diff --git a/pygeoif/functions.py b/pygeoif/functions.py index 8d6d24f6..1ce8818b 100644 --- a/pygeoif/functions.py +++ b/pygeoif/functions.py @@ -28,7 +28,6 @@ from pygeoif.types import CoordinatesType from pygeoif.types import GeoCollectionInterface from pygeoif.types import GeoInterface -from pygeoif.types import Line2D from pygeoif.types import LineType from pygeoif.types import MultiCoordinatesType from pygeoif.types import Point2D @@ -75,65 +74,59 @@ def centroid(coords: LineType) -> Tuple[Point2D, float]: return cast(Point2D, tuple(ans)), signed_area / 2.0 -def _cross(o: Point2D, a: Point2D, b: Point2D) -> float: +def _orientation(p: Point2D, q: Point2D, r: Point2D) -> float: """ - 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product. + Calculate orientation of three points (p, q, r). + + Returns + ------- + negative if counterclockwise + 0 if colinear + positive if clockwise - Returns a positive value, if OAB makes a counter-clockwise turn, - negative for clockwise turn, and zero if the points are collinear. """ - return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]) + return (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1]) -def _build_hull(points: Iterable[Point2D]) -> List[Point2D]: - hull: List[Point2D] = [] +def _hull(points: Iterable[Point2D]) -> List[Point2D]: + """Construct the upper/lower hull of a set of points.""" + stack: List[Point2D] = [] for p in points: while ( - len(hull) >= 2 and _cross(o=hull[-2], a=hull[-1], b=p) <= 0 # noqa: PLR2004 + len(stack) >= 2 # noqa: PLR2004 + and _orientation(stack[-2], stack[-1], p) >= 0 ): - hull.pop() - hull.append(p) - return hull + stack.pop() + stack.append(p) + return stack -def convex_hull(points: Iterable[Point2D]) -> Line2D: +def convex_hull(points: Iterable[Point2D]) -> LineType: """ - Compute the convex hull of a set of 2D points. - - Input: an iterable sequence of (x, y) pairs representing the points. - Output: a list of vertices of the convex hull in counter-clockwise order, - starting from the vertex with the lexicographically smallest coordinates. + Return the convex hull of a set of points using Andrew's monotone chain algorithm. - Andrew's monotone chain convex hull algorithm constructs the convex hull - of a set of 2-dimensional points in O(n log n) time. + Args: + ---- + points (Iterable[Point2D]): A collection of 2D points. - It does so by first sorting the points lexicographically - (first by x-coordinate, and in case of a tie, by y-coordinate), - and then constructing upper and lower hulls of the points. + Returns: + ------- + LineType: The convex hull, represented as a list of points. - An upper hull is the part of the convex hull, which is visible from the above. - It runs from its rightmost point to the leftmost point in counterclockwise order. - Lower hull is the remaining part of the convex hull. """ - # Sort the points lexicographically (tuples are compared lexicographically). - # Remove duplicates to detect the case we have just one unique point. points = sorted(set(points)) - - # Boring case: no points, a single point or a line between two points, - # possibly repeated multiple times. + # No points, a single point or a line between two points if len(points) <= 2: # noqa: PLR2004 return points - lower = _build_hull(points) - upper = _build_hull(reversed(points)) - + # Construct the upper and lower hulls + upper = _hull(points) + lower = _hull(reversed(points)) if len(lower) == len(upper) == 2 and set(lower) == set(upper): # noqa: PLR2004 # all points are in a straight line - return lower - # Concatenation of the lower and upper hulls gives the convex hull. - # Last point of lower list is omitted - # because it is repeated at the beginning of the upper list. - return lower[:-1] + upper + return upper + # Remove duplicate points (at the end of upper and beginning of lower) + return dedupe(upper + lower) def dedupe(coords: LineType) -> LineType: diff --git a/pygeoif/hypothesis/strategies.py b/pygeoif/hypothesis/strategies.py index f37deaa4..7d646f8f 100644 --- a/pygeoif/hypothesis/strategies.py +++ b/pygeoif/hypothesis/strategies.py @@ -4,10 +4,13 @@ from functools import partial from typing import Optional from typing import Tuple +from typing import cast import hypothesis.strategies as st +from pygeoif.geometry import LineString from pygeoif.geometry import Point +from pygeoif.types import LineType from pygeoif.types import Point2D from pygeoif.types import Point3D from pygeoif.types import PointType @@ -15,6 +18,8 @@ __all__ = [ "Srs", "epsg4326", + "line_coords", + "line_string", "point_coords", "points", ] @@ -151,3 +156,82 @@ def points( """ return Point(*draw(point_coords(srs, has_z))) + + +@st.composite +def line_coords( # noqa: PLR0913 + draw: st.DrawFn, + min_points: int, + max_points: Optional[int] = None, + srs: Optional[Srs] = None, + has_z: Optional[bool] = None, + unique: bool = False, # noqa: FBT001,FBT002 +) -> LineType: + """ + Generate a random line in either 2D or 3D space. + + Args: + ---- + draw: The draw function from the hypothesis library. + min_points: Minimum number of points in the line + max_points: Maximum number of points in the line + srs: An optional parameter specifying the spatial reference system. + has_z: An optional parameter specifying whether to generate 2D or 3D points. + unique: Optional flag to generate unique points (default False). + + Returns: + ------- + A list of point coordinates representing the line in either 2D or 3D space. + + """ + if has_z is None: + has_z = draw(st.booleans()) + return cast( + LineType, + draw( + st.lists( + point_coords(srs, has_z), + min_size=min_points, + max_size=max_points, + unique=unique, + ), + ), + ) + + +@st.composite +def line_string( + draw: st.DrawFn, + max_points: Optional[int] = None, + srs: Optional[Srs] = None, + has_z: Optional[bool] = None, +) -> LineString: + """ + Generate a random linestring in either 2D or 3D space. + + Args: + ---- + draw: The draw function from the hypothesis library. + max_points: Maximum number of points in the line (must be greater than 1) + srs: An optional parameter specifying the spatial reference system. + has_z: An optional parameter specifying whether to generate 2D or 3D points. + + Returns: + ------- + A LineString representing the randomly generated linestring in either 2D or 3D + space. + + """ + if max_points is not None and max_points < 2: # noqa: PLR2004 + raise ValueError("max_points must be greater than 1") # noqa: TRY003,EM101 + return LineString( + draw( + line_coords( + min_points=2, + max_points=max_points, + srs=srs, + has_z=has_z, + unique=True, + ), + ), + ) diff --git a/tests/hypothesis/test_line.py b/tests/hypothesis/test_line.py new file mode 100644 index 00000000..8639127a --- /dev/null +++ b/tests/hypothesis/test_line.py @@ -0,0 +1,45 @@ +"""Test LineStrings with Hypothesis.""" + +from hypothesis import given + +from pygeoif import geometry +from pygeoif.factories import force_2d +from pygeoif.factories import force_3d +from pygeoif.factories import from_wkt +from pygeoif.factories import shape +from pygeoif.hypothesis.strategies import epsg4326 +from pygeoif.hypothesis.strategies import line_string + + +@given(line_string(srs=epsg4326)) +def test_from_wkt_epsg_4326(line: geometry.LineString) -> None: + + assert line == from_wkt(str(line)) + + +@given(line_string()) +def test_repr_eval(line: geometry.LineString) -> None: + + assert eval(repr(line), {}, {"LineString": geometry.LineString}) == line + + +@given(line_string()) +def test_shape(line: geometry.LineString) -> None: + assert line == shape(line) + + +@given(line_string()) +def test_bounds(line: geometry.LineString) -> None: + assert line.bounds == force_2d(line).bounds + + +@given(line_string()) +def test_convex_hull(line: geometry.LineString) -> None: + assert line.convex_hull == force_2d(line.convex_hull) + assert line.convex_hull == force_2d(line).convex_hull + assert line.convex_hull == force_3d(line).convex_hull + + +@given(line_string()) +def test_convex_hull_bounds(line: geometry.LineString) -> None: + assert line.convex_hull.bounds == line.bounds diff --git a/tests/hypothesis/test_point.py b/tests/hypothesis/test_point.py index b50dcbd3..052a2c0d 100644 --- a/tests/hypothesis/test_point.py +++ b/tests/hypothesis/test_point.py @@ -9,22 +9,23 @@ from pygeoif.hypothesis.strategies import epsg4326 from pygeoif.hypothesis.strategies import point_coords from pygeoif.hypothesis.strategies import points +from pygeoif.types import PointType @given(point_coords(epsg4326)) -def test_from_wkt_epsg_4326(point) -> None: +def test_from_wkt_epsg_4326(point: PointType) -> None: point = geometry.Point(*point) assert point == from_wkt(str(point)) @given(points()) -def test_repr_eval(point) -> None: +def test_repr_eval(point: geometry.Point) -> None: assert eval(repr(point), {}, {"Point": geometry.Point}) == point @given(points()) -def test_shape(point) -> None: +def test_shape(point: geometry.Point) -> None: assert point == shape(point) diff --git a/tests/test_functions.py b/tests/test_functions.py index 23715543..b56f83fe 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -151,7 +151,7 @@ def test_line_minimal() -> None: hull = convex_hull(pts) - assert hull == [(0, 0), (1, 0), (1, 1), (0, 0)] + assert hull == ((0, 0), (1, 0), (1, 1), (0, 0)) def test_line2() -> None: @@ -173,7 +173,7 @@ def test_line3() -> None: def test_square() -> None: pts = list(itertools.product(range(100), range(100))) hull = convex_hull(pts) - assert hull == [(0, 0), (99, 0), (99, 99), (0, 99), (0, 0)] + assert hull == ((0, 0), (99, 0), (99, 99), (0, 99), (0, 0)) def test_triangle() -> None: @@ -181,7 +181,7 @@ def test_triangle() -> None: for x in range(100): pts.extend((x, y) for y in range(x + 1)) hull = convex_hull(pts) - assert hull == [(0, 0), (99, 0), (99, 99), (0, 0)] + assert hull == ((0, 0), (99, 0), (99, 99), (0, 0)) def test_trapezoid() -> None: @@ -189,7 +189,7 @@ def test_trapezoid() -> None: for x in range(100): pts.extend((x, y) for y in range(-x - 1, x + 1)) hull = convex_hull(pts) - assert hull == [(0, -1), (99, -100), (99, 99), (0, 0), (0, -1)] + assert hull == ((0, -1), (99, -100), (99, 99), (0, 0), (0, -1)) def test_circles() -> None: From 76c58d64cbe5617f19e01c35d4af5c26c8fcd595 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Fri, 22 Mar 2024 09:27:30 +0000 Subject: [PATCH 40/55] exclude subnormals from coordinate strategy --- pygeoif/functions.py | 2 +- pygeoif/hypothesis/strategies.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pygeoif/functions.py b/pygeoif/functions.py index 1ce8818b..97c67449 100644 --- a/pygeoif/functions.py +++ b/pygeoif/functions.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2012 -2023 Christian Ledermann +# Copyright (C) 2012 - 2024 Christian Ledermann # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public diff --git a/pygeoif/hypothesis/strategies.py b/pygeoif/hypothesis/strategies.py index 7d646f8f..b9e949ee 100644 --- a/pygeoif/hypothesis/strategies.py +++ b/pygeoif/hypothesis/strategies.py @@ -51,7 +51,12 @@ class Srs: max_xyz=(180.0, 90.0, 1_000_000_000_000.0), ) -coordinate = partial(st.floats, allow_infinity=False, allow_nan=False) +coordinate = partial( + st.floats, + allow_infinity=False, + allow_nan=False, + allow_subnormal=False, +) @st.composite From cc0cff488afbeaf78253cd254029068c6d786494 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Fri, 22 Mar 2024 13:25:29 +0000 Subject: [PATCH 41/55] limit generated coordinates to 32 bit floats, that should be enough for geospatial use cases --- pygeoif/functions.py | 10 +++++----- pygeoif/hypothesis/strategies.py | 5 +++-- tests/conftest.py | 10 ++++++++++ tests/test_factories.py | 6 ++++++ 4 files changed, 24 insertions(+), 7 deletions(-) create mode 100644 tests/conftest.py diff --git a/pygeoif/functions.py b/pygeoif/functions.py index 97c67449..61a9a1c7 100644 --- a/pygeoif/functions.py +++ b/pygeoif/functions.py @@ -74,6 +74,11 @@ def centroid(coords: LineType) -> Tuple[Point2D, float]: return cast(Point2D, tuple(ans)), signed_area / 2.0 +def dedupe(coords: LineType) -> LineType: + """Remove duplicate Points from a LineString.""" + return cast(LineType, tuple(coord for coord, _count in groupby(coords))) + + def _orientation(p: Point2D, q: Point2D, r: Point2D) -> float: """ Calculate orientation of three points (p, q, r). @@ -129,11 +134,6 @@ def convex_hull(points: Iterable[Point2D]) -> LineType: return dedupe(upper + lower) -def dedupe(coords: LineType) -> LineType: - """Remove duplicate Points from a LineString.""" - return cast(LineType, tuple(coord for coord, _count in groupby(coords))) - - def compare_coordinates( coords: Union[float, CoordinatesType, MultiCoordinatesType], other: Union[float, CoordinatesType, MultiCoordinatesType], diff --git a/pygeoif/hypothesis/strategies.py b/pygeoif/hypothesis/strategies.py index b9e949ee..ff3eeba5 100644 --- a/pygeoif/hypothesis/strategies.py +++ b/pygeoif/hypothesis/strategies.py @@ -47,8 +47,8 @@ class Srs: epsg4326 = Srs( name="EPSG:4326", - min_xyz=(-180.0, -90.0, -1_000_000_000_000.0), - max_xyz=(180.0, 90.0, 1_000_000_000_000.0), + min_xyz=(-180.0, -90.0, -999_999_995_904.0), + max_xyz=(180.0, 90.0, 999_999_995_904.0), ) coordinate = partial( @@ -56,6 +56,7 @@ class Srs: allow_infinity=False, allow_nan=False, allow_subnormal=False, + width=32, ) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..996290db --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,10 @@ +""" +Configure the tests. + +Register the hypothesis 'exhaustive' profile to run 10 thousand examples. +Run this profile with ``pytest --hypothesis-profile=exhaustive`` +""" + +from hypothesis import settings + +settings.register_profile("exhaustive", max_examples=10_000) diff --git a/tests/test_factories.py b/tests/test_factories.py index f56528bc..cf2a4374 100644 --- a/tests/test_factories.py +++ b/tests/test_factories.py @@ -503,6 +503,12 @@ def test_point(self) -> None: s = factories.shape(f) assert f.__geo_interface__ == s.__geo_interface__ + def test_point_00(self) -> None: + f = geometry.Point(0, 0) + + assert f + assert f == factories.shape(f) + def test_linestring(self) -> None: f = geometry.LineString([(0, 0), (1, 1)]) s = factories.shape(f) From 2a82961e2ce606ef4d11c7733073d608f63d0b98 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Fri, 22 Mar 2024 20:12:00 +0000 Subject: [PATCH 42/55] Update regular expression pattern in factories.py and add hypothesis tests for LinearRing, MultiPoint, and GeometryCollection --- pygeoif/factories.py | 4 +- pygeoif/hypothesis/strategies.py | 414 ++++++++++++++++++-- tests/hypothesis/test_geometrycollection.py | 42 ++ tests/hypothesis/test_line.py | 21 +- tests/hypothesis/test_linear_ring.py | 26 ++ tests/hypothesis/test_multilinestring.py | 52 +++ tests/hypothesis/test_multipoint.py | 39 ++ tests/hypothesis/test_multipolygon.py | 55 +++ tests/hypothesis/test_point.py | 3 +- tests/hypothesis/test_polygon.py | 57 +++ tests/test_geometrycollection.py | 34 ++ 11 files changed, 714 insertions(+), 33 deletions(-) create mode 100644 tests/hypothesis/test_geometrycollection.py create mode 100644 tests/hypothesis/test_linear_ring.py create mode 100644 tests/hypothesis/test_multilinestring.py create mode 100644 tests/hypothesis/test_multipoint.py create mode 100644 tests/hypothesis/test_multipolygon.py create mode 100644 tests/hypothesis/test_polygon.py diff --git a/pygeoif/factories.py b/pygeoif/factories.py index 3e44b758..660f7223 100644 --- a/pygeoif/factories.py +++ b/pygeoif/factories.py @@ -55,7 +55,9 @@ ), flags=re.I, ) -gcre: Pattern[str] = re.compile(r"POINT|LINESTRING|LINEARRING|POLYGON") +gcre: Pattern[str] = re.compile( + r"POINT|LINESTRING|LINEARRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON", +) outer: Pattern[str] = re.compile(r"\((.+)\)") inner: Pattern[str] = re.compile(r"\([^)]*\)") mpre: Pattern[str] = re.compile(r"\(\((.+?)\)\)") diff --git a/pygeoif/hypothesis/strategies.py b/pygeoif/hypothesis/strategies.py index ff3eeba5..aa66a726 100644 --- a/pygeoif/hypothesis/strategies.py +++ b/pygeoif/hypothesis/strategies.py @@ -1,4 +1,8 @@ -"""Data-generating strategies for property-based testing.""" +""" +Data-generating strategies for property-based testing. + +Coordinates are limited to 32 bit floats to avoid precision issues. +""" from dataclasses import dataclass from functools import partial @@ -8,8 +12,14 @@ import hypothesis.strategies as st +from pygeoif.geometry import GeometryCollection +from pygeoif.geometry import LinearRing from pygeoif.geometry import LineString +from pygeoif.geometry import MultiLineString +from pygeoif.geometry import MultiPoint +from pygeoif.geometry import MultiPolygon from pygeoif.geometry import Point +from pygeoif.geometry import Polygon from pygeoif.types import LineType from pygeoif.types import Point2D from pygeoif.types import Point3D @@ -17,14 +27,28 @@ __all__ = [ "Srs", - "epsg4326", + "geometry_collections", "line_coords", - "line_string", + "line_strings", + "linear_rings", + "multi_line_strings", + "multi_points", + "multi_polygons", "point_coords", "points", + "polygons", ] +coordinate = partial( + st.floats, + allow_infinity=False, + allow_nan=False, + allow_subnormal=False, + width=32, +) + + @dataclass(frozen=True) class Srs: """ @@ -44,6 +68,33 @@ class Srs: min_xyz: Tuple[Optional[float], Optional[float], Optional[float]] max_xyz: Tuple[Optional[float], Optional[float], Optional[float]] + def longitudes(self) -> st.SearchStrategy[float]: + """ + Generate a search strategy for generating longitudes. + + Returns a search strategy that generates floats within the longitude bounds of + the SRS. + """ + return coordinate(min_value=self.min_xyz[0], max_value=self.max_xyz[0]) + + def latitudes(self) -> st.SearchStrategy[float]: + """ + Generate a search strategy for generating latitudes. + + Returns a search strategy that generates floats within the latitude bounds of + the SRS. + """ + return coordinate(min_value=self.min_xyz[1], max_value=self.max_xyz[1]) + + def elevations(self) -> st.SearchStrategy[float]: + """ + Generate a search strategy for generating elevations. + + Returns a search strategy that generates floats within the elevation bounds of + the SRS. + """ + return coordinate(min_value=self.min_xyz[2], max_value=self.max_xyz[2]) + epsg4326 = Srs( name="EPSG:4326", @@ -51,18 +102,11 @@ class Srs: max_xyz=(180.0, 90.0, 999_999_995_904.0), ) -coordinate = partial( - st.floats, - allow_infinity=False, - allow_nan=False, - allow_subnormal=False, - width=32, -) - @st.composite def _point_coords_2d( draw: st.DrawFn, + *, srs: Optional[Srs] = None, ) -> Point2D: """ @@ -81,14 +125,15 @@ def _point_coords_2d( longitudes = coordinate() latitudes = coordinate() if srs: - longitudes = coordinate(min_value=srs.min_xyz[0], max_value=srs.max_xyz[0]) - latitudes = coordinate(min_value=srs.min_xyz[1], max_value=srs.max_xyz[1]) + longitudes = srs.longitudes() + latitudes = srs.latitudes() return draw(st.tuples(latitudes, longitudes)) @st.composite def _point_coords_3d( draw: st.DrawFn, + *, srs: Optional[Srs] = None, ) -> Point3D: """ @@ -108,15 +153,16 @@ def _point_coords_3d( latitudes = coordinate() elevations = coordinate() if srs: - longitudes = coordinate(min_value=srs.min_xyz[0], max_value=srs.max_xyz[0]) - latitudes = coordinate(min_value=srs.min_xyz[1], max_value=srs.max_xyz[1]) - elevations = coordinate(min_value=srs.min_xyz[2], max_value=srs.max_xyz[2]) + longitudes = srs.longitudes() + latitudes = srs.latitudes() + elevations = srs.elevations() return draw(st.tuples(latitudes, longitudes, elevations)) @st.composite def point_coords( draw: st.DrawFn, + *, srs: Optional[Srs] = None, has_z: Optional[bool] = None, ) -> PointType: @@ -135,15 +181,16 @@ def point_coords( """ if has_z is True: - return draw(_point_coords_3d(srs)) + return draw(_point_coords_3d(srs=srs)) if has_z is False: - return draw(_point_coords_2d(srs)) - return draw(st.one_of(_point_coords_2d(srs), _point_coords_3d(srs))) + return draw(_point_coords_2d(srs=srs)) + return draw(st.one_of(_point_coords_2d(srs=srs), _point_coords_3d(srs=srs))) @st.composite def points( draw: st.DrawFn, + *, srs: Optional[Srs] = None, has_z: Optional[bool] = None, ) -> Point: @@ -161,17 +208,18 @@ def points( A randomly generated point in either 2D or 3D space. """ - return Point(*draw(point_coords(srs, has_z))) + return Point(*draw(point_coords(srs=srs, has_z=has_z))) @st.composite def line_coords( # noqa: PLR0913 draw: st.DrawFn, + *, min_points: int, max_points: Optional[int] = None, srs: Optional[Srs] = None, has_z: Optional[bool] = None, - unique: bool = False, # noqa: FBT001,FBT002 + unique: bool = False, ) -> LineType: """ Generate a random line in either 2D or 3D space. @@ -196,7 +244,7 @@ def line_coords( # noqa: PLR0913 LineType, draw( st.lists( - point_coords(srs, has_z), + point_coords(srs=srs, has_z=has_z), min_size=min_points, max_size=max_points, unique=unique, @@ -206,8 +254,9 @@ def line_coords( # noqa: PLR0913 @st.composite -def line_string( +def line_strings( draw: st.DrawFn, + *, max_points: Optional[int] = None, srs: Optional[Srs] = None, has_z: Optional[bool] = None, @@ -220,7 +269,7 @@ def line_string( draw: The draw function from the hypothesis library. max_points: Maximum number of points in the line (must be greater than 1) srs: An optional parameter specifying the spatial reference system. - has_z: An optional parameter specifying whether to generate 2D or 3D points. + has_z: An optional parameter specifying whether to generate 2D or 3D lines. Returns: ------- @@ -241,3 +290,320 @@ def line_string( ), ), ) + + +@st.composite +def linear_rings( + draw: st.DrawFn, + *, + max_points: Optional[int] = None, + srs: Optional[Srs] = None, + has_z: Optional[bool] = None, +) -> LinearRing: + """ + Generate a linear ring using the provided draw function. + + Args: + ---- + draw (st.DrawFn): The draw function used to generate the coordinates. + max_points (Optional[int]): The maximum number of points in the linear ring. + If not specified, there is no limit. + srs (Optional[Srs]): The spatial reference system of the linear ring. + If not specified, the default SRS is used. + has_z (Optional[bool]): Whether the linear ring has z-coordinates. + If not specified, 2D or 3D coordinates are generated. + + Returns: + ------- + LinearRing: The generated linear ring. + + Raises: + ------ + ValueError: If max_points is less than 4. + + """ + if max_points is not None and max_points < 4: # noqa: PLR2004 + raise ValueError("max_points must be greater than 3") # noqa: TRY003,EM101 + return LinearRing( + draw( + line_coords( + min_points=3, + max_points=max_points, + srs=srs, + has_z=has_z, + unique=True, + ), + ), + ) + + +@st.composite +def polygons( # noqa: PLR0913 + draw: st.DrawFn, + *, + max_points: Optional[int] = None, + min_interiors: int = 0, + max_interiors: Optional[int] = None, + srs: Optional[Srs] = None, + has_z: Optional[bool] = None, +) -> Polygon: + """ + Generate a random polygon using the given strategies. + + Args: + ---- + draw (st.DrawFn): The draw function used to generate random values. + max_points (Optional[int]): The maximum number of points in the polygon. + If not specified, there is no limit. + min_interiors (Optional[int]): The minimum number of interior rings (holes) + in the polygon. Defaults to 0. + max_interiors (Optional[int]): The maximum number of interior rings (holes) + in the polygon. If not specified, there is no limit. + srs (Optional[Srs]): The spatial reference system of the polygon. + Defaults to None. + has_z (Optional[bool]): Whether the polygon has z-coordinates. + If not specified, a random boolean value will be used. + + Returns: + ------- + Polygon: The generated polygon. + + Raises: + ------ + ValueError: If max_points is specified and is less than 4. + + """ + if has_z is None: + has_z = draw(st.booleans()) + if max_points is not None and max_points < 4: # noqa: PLR2004 + raise ValueError("max_points must be greater than 3") # noqa: TRY003,EM101 + return Polygon( + draw( + line_coords( + min_points=3, + max_points=max_points, + srs=srs, + has_z=has_z, + unique=True, + ), + ), + holes=draw( + st.lists( + line_coords( + min_points=3, + max_points=max_points, + srs=srs, + has_z=has_z, + unique=True, + ), + min_size=min_interiors, + max_size=max_interiors, + ), + ), + ) + + +@st.composite +def multi_points( + draw: st.DrawFn, + *, + min_points: int = 1, + max_points: Optional[int] = None, + srs: Optional[Srs] = None, + has_z: Optional[bool] = None, +) -> MultiPoint: + """ + Generate a MultiPoint geometry object with random coordinates. + + Args: + ---- + draw (st.DrawFn): The draw function from the hypothesis library. + min_points (int): The minimum number of points in the MultiPoint. Default is 1. + max_points (Optional[int]): The maximum number of points in the MultiPoint. + srs (Optional[Srs]): The spatial reference system of the coordinates. + has_z (Optional[bool]): Whether the coordinates have a Z component. + if not specified, 2D and 3D coordinates will be generated randomly. + + Returns: + ------- + MultiPoint: The generated MultiPoint geometry object. + + """ + if has_z is None: + has_z = draw(st.booleans()) + return MultiPoint( + draw( + st.lists( + point_coords(srs=srs, has_z=has_z), + min_size=min_points, + max_size=max_points, + unique=True, + ), + ), + ) + + +@st.composite +def multi_line_strings( # noqa: PLR0913 + draw: st.DrawFn, + *, + min_lines: int = 1, + max_lines: Optional[int] = None, + max_points: Optional[int] = None, + srs: Optional[Srs] = None, + has_z: Optional[bool] = None, +) -> MultiLineString: + """ + Generate a random MultiLineString object. + + Args: + ---- + draw (st.DrawFn): The Hypothesis draw function. + min_lines (int, optional): The minimum number of lines in the MultiLineString. + max_lines (int, optional): The maximum number of lines in the MultiLineString. + max_points (int, optional): The maximum number of points in each line. + srs (Srs, optional): The spatial reference system of the MultiLineString. + has_z (bool, optional): Whether the MultiLineString has z-coordinates. + + Returns: + ------- + MultiLineString: The generated MultiLineString object. + + """ + if has_z is None: + has_z = draw(st.booleans()) + return MultiLineString( + draw( + st.lists( + line_coords( + min_points=2, + max_points=max_points, + srs=srs, + has_z=has_z, + unique=True, + ), + min_size=min_lines, + max_size=max_lines, + ), + ), + ) + + +@st.composite +def multi_polygons( # noqa: PLR0913 + draw: st.DrawFn, + *, + min_polygons: int = 1, + max_polygons: Optional[int] = 5, + max_points: Optional[int] = 20, + min_interiors: int = 0, + max_interiors: int = 5, + srs: Optional[Srs] = None, + has_z: Optional[bool] = None, +) -> MultiPolygon: + """ + Generate a random MultiPolygon object. + + Args: + ---- + draw (st.DrawFn): The Hypothesis draw function. + min_polygons (int, optional): The min number of polygons in the MultiPolygon. + max_polygons (int, optional): The max number of polygons in the MultiPolygon. + max_points (int, optional): The maximum number of points in each polygon. + min_interiors (int, optional): The minimum number of interiors in each polygon. + max_interiors (int, optional): The maximum number of interiors in each polygon. + srs (Optional[Srs], optional): The spatial reference system of the MultiPolygon. + has_z (Optional[bool], optional): Whether the MultiPolygon has z-coordinates. + + Returns: + ------- + MultiPolygon: The generated MultiPolygon object. + + """ + if has_z is None: + has_z = draw(st.booleans()) + return MultiPolygon.from_polygons( + *draw( + st.lists( + polygons( + max_points=max_points, + min_interiors=min_interiors, + max_interiors=max_interiors, + srs=srs, + has_z=has_z, + ), + min_size=min_polygons, + max_size=max_polygons, + ), + ), + ) + + +@st.composite +def geometry_collections( # noqa: PLR0913 + draw: st.DrawFn, + *, + min_geoms: int = 1, + max_geoms: Optional[int] = None, + max_points: int = 20, + min_interiors: int = 0, + max_interiors: int = 5, + srs: Optional[Srs] = None, + has_z: Optional[bool] = None, +) -> GeometryCollection: + """ + Generate a random GeometryCollection object. + + Args: + ---- + draw (st.DrawFn): The Hypothesis draw function. + min_geoms (int, optional): The minimum number of geometries in the collection. + max_geoms (int, optional): The maximum number of geometries in the collection. + max_points (int, optional): The maximum number of points in each geometry. + min_interiors (int, optional): The minimum number of interiors in each polygon. + max_interiors (int, optional): The maximum number of interiors in each polygon. + srs (Optional[Srs], optional): The spatial reference system of the geometries. + has_z (Optional[bool], optional): Whether the geometries have Z coordinates. + + Returns: + ------- + GeometryCollection: A randomly generated GeometryCollection object. + + """ + if has_z is None: + has_z = draw(st.booleans()) + return GeometryCollection( + draw( + st.lists( + st.one_of( + points(srs=srs, has_z=has_z), + line_strings(max_points=max_points, srs=srs, has_z=has_z), + linear_rings(max_points=max_points, srs=srs, has_z=has_z), + polygons( + max_points=max_points, + min_interiors=min_interiors, + max_interiors=max_interiors, + srs=srs, + has_z=has_z, + ), + multi_points(max_points=max_points, srs=srs, has_z=has_z), + multi_line_strings( + max_points=max_points, + max_lines=max_geoms, + srs=srs, + has_z=has_z, + ), + multi_polygons( + max_points=max_points, + min_interiors=min_interiors, + max_interiors=max_interiors, + max_polygons=max_geoms, + srs=srs, + has_z=has_z, + ), + ), + min_size=min_geoms, + max_size=max_geoms, + ), + ), + ) diff --git a/tests/hypothesis/test_geometrycollection.py b/tests/hypothesis/test_geometrycollection.py new file mode 100644 index 00000000..761b66a6 --- /dev/null +++ b/tests/hypothesis/test_geometrycollection.py @@ -0,0 +1,42 @@ +"""Test MultiPolygons with Hypothesis.""" + +from hypothesis import given + +from pygeoif import geometry +from pygeoif.factories import from_wkt +from pygeoif.factories import shape +from pygeoif.hypothesis.strategies import epsg4326 +from pygeoif.hypothesis.strategies import geometry_collections + + +@given(geometry_collections(srs=epsg4326)) +def test_from_wkt_epsg_4326(multi_poly: geometry.GeometryCollection) -> None: + + assert multi_poly == from_wkt(str(multi_poly)) + + +@given(geometry_collections(srs=epsg4326)) +def test_repr_eval(multi_poly: geometry.GeometryCollection) -> None: + + assert ( + eval( + repr(multi_poly), + {}, + { + "GeometryCollection": geometry.GeometryCollection, + "MultiPolygon": geometry.MultiPolygon, + "Polygon": geometry.Polygon, + "Point": geometry.Point, + "LineString": geometry.LineString, + "LinearRing": geometry.LinearRing, + "MultiPoint": geometry.MultiPoint, + "MultiLineString": geometry.MultiLineString, + }, + ) + == multi_poly + ) + + +@given(geometry_collections(srs=epsg4326)) +def test_shape(multi_poly: geometry.GeometryCollection) -> None: + assert multi_poly == shape(multi_poly) diff --git a/tests/hypothesis/test_line.py b/tests/hypothesis/test_line.py index 8639127a..b3c3f373 100644 --- a/tests/hypothesis/test_line.py +++ b/tests/hypothesis/test_line.py @@ -8,38 +8,45 @@ from pygeoif.factories import from_wkt from pygeoif.factories import shape from pygeoif.hypothesis.strategies import epsg4326 -from pygeoif.hypothesis.strategies import line_string +from pygeoif.hypothesis.strategies import line_strings -@given(line_string(srs=epsg4326)) +@given(line_strings(srs=epsg4326)) def test_from_wkt_epsg_4326(line: geometry.LineString) -> None: assert line == from_wkt(str(line)) -@given(line_string()) +@given(line_strings()) def test_repr_eval(line: geometry.LineString) -> None: assert eval(repr(line), {}, {"LineString": geometry.LineString}) == line -@given(line_string()) +@given(line_strings()) def test_shape(line: geometry.LineString) -> None: assert line == shape(line) -@given(line_string()) +@given(line_strings()) def test_bounds(line: geometry.LineString) -> None: assert line.bounds == force_2d(line).bounds -@given(line_string()) +@given(line_strings()) def test_convex_hull(line: geometry.LineString) -> None: assert line.convex_hull == force_2d(line.convex_hull) assert line.convex_hull == force_2d(line).convex_hull assert line.convex_hull == force_3d(line).convex_hull -@given(line_string()) +@given(line_strings()) def test_convex_hull_bounds(line: geometry.LineString) -> None: + """ + Test that the convex hull calculation preserves the original bounds. + + The bounds of the convex hull of a LineString must be equal to the bounds of + the LineString itself. + """ + assert line.convex_hull assert line.convex_hull.bounds == line.bounds diff --git a/tests/hypothesis/test_linear_ring.py b/tests/hypothesis/test_linear_ring.py new file mode 100644 index 00000000..ec3e00df --- /dev/null +++ b/tests/hypothesis/test_linear_ring.py @@ -0,0 +1,26 @@ +"""Test LinearRings with Hypothesis.""" + +from hypothesis import given + +from pygeoif import geometry +from pygeoif.factories import from_wkt +from pygeoif.factories import shape +from pygeoif.hypothesis.strategies import epsg4326 +from pygeoif.hypothesis.strategies import linear_rings + + +@given(linear_rings(srs=epsg4326)) +def test_from_wkt_epsg_4326(line: geometry.LinearRing) -> None: + + assert line == from_wkt(str(line)) + + +@given(linear_rings()) +def test_repr_eval(line: geometry.LinearRing) -> None: + + assert eval(repr(line), {}, {"LinearRing": geometry.LinearRing}) == line + + +@given(linear_rings()) +def test_shape(line: geometry.LinearRing) -> None: + assert line == shape(line) diff --git a/tests/hypothesis/test_multilinestring.py b/tests/hypothesis/test_multilinestring.py new file mode 100644 index 00000000..b815c209 --- /dev/null +++ b/tests/hypothesis/test_multilinestring.py @@ -0,0 +1,52 @@ +"""Test LineStrings with Hypothesis.""" + +from hypothesis import given + +from pygeoif import geometry +from pygeoif.factories import force_2d +from pygeoif.factories import force_3d +from pygeoif.factories import from_wkt +from pygeoif.factories import shape +from pygeoif.hypothesis.strategies import epsg4326 +from pygeoif.hypothesis.strategies import multi_line_strings + + +@given(multi_line_strings(srs=epsg4326)) +def test_from_wkt_epsg_4326(line: geometry.MultiLineString) -> None: + + assert line == from_wkt(str(line)) + + +@given(multi_line_strings()) +def test_repr_eval(line: geometry.MultiLineString) -> None: + + assert eval(repr(line), {}, {"MultiLineString": geometry.MultiLineString}) == line + + +@given(multi_line_strings()) +def test_shape(line: geometry.MultiLineString) -> None: + assert line == shape(line) + + +@given(multi_line_strings()) +def test_bounds(line: geometry.MultiLineString) -> None: + assert line.bounds == force_2d(line).bounds + + +@given(multi_line_strings()) +def test_convex_hull(line: geometry.MultiLineString) -> None: + assert line.convex_hull == force_2d(line.convex_hull) + assert line.convex_hull == force_2d(line).convex_hull + assert line.convex_hull == force_3d(line).convex_hull + + +@given(multi_line_strings()) +def test_convex_hull_bounds(line: geometry.MultiLineString) -> None: + """ + Test that the convex hull calculation preserves the original bounds. + + The bounds of the convex hull of a MultiLineString must be equal to the bounds of + the MultiLineString itself. + """ + assert line.convex_hull + assert line.convex_hull.bounds == line.bounds diff --git a/tests/hypothesis/test_multipoint.py b/tests/hypothesis/test_multipoint.py new file mode 100644 index 00000000..cf98c319 --- /dev/null +++ b/tests/hypothesis/test_multipoint.py @@ -0,0 +1,39 @@ +"""Test the MultiPoint class using Hypothesis.""" + +from hypothesis import given + +from pygeoif import geometry +from pygeoif.factories import force_2d +from pygeoif.factories import from_wkt +from pygeoif.factories import shape +from pygeoif.hypothesis.strategies import epsg4326 +from pygeoif.hypothesis.strategies import multi_points + + +@given(multi_points(srs=epsg4326)) +def test_from_wkt_epsg_4326(multi_point: geometry.MultiPoint) -> None: + + assert multi_point == from_wkt(str(multi_point)) + + +@given(multi_points()) +def test_repr_eval(multi_point: geometry.MultiPoint) -> None: + assert ( + eval(repr(multi_point), {}, {"MultiPoint": geometry.MultiPoint}) == multi_point + ) + + +@given(multi_points()) +def test_shape(multi_point: geometry.MultiPoint) -> None: + assert multi_point == shape(multi_point) + + +@given(multi_points()) +def test_bounds(multi_point: geometry.MultiPoint) -> None: + assert multi_point.bounds == force_2d(multi_point).bounds + + +@given(multi_points()) +def test_convex_hull(multi_point: geometry.MultiPoint) -> None: + assert multi_point.convex_hull == force_2d(multi_point).convex_hull + assert multi_point.convex_hull.bounds == multi_point.bounds diff --git a/tests/hypothesis/test_multipolygon.py b/tests/hypothesis/test_multipolygon.py new file mode 100644 index 00000000..3a5dad28 --- /dev/null +++ b/tests/hypothesis/test_multipolygon.py @@ -0,0 +1,55 @@ +"""Test MultiPolygons with Hypothesis.""" + +from hypothesis import given + +from pygeoif import geometry +from pygeoif.factories import force_2d +from pygeoif.factories import force_3d +from pygeoif.factories import from_wkt +from pygeoif.factories import shape +from pygeoif.hypothesis.strategies import epsg4326 +from pygeoif.hypothesis.strategies import multi_polygons + + +@given(multi_polygons(srs=epsg4326)) +def test_from_wkt_epsg_4326(multi_poly: geometry.MultiPolygon) -> None: + + assert multi_poly == from_wkt(str(multi_poly)) + + +@given(multi_polygons(srs=epsg4326)) +def test_repr_eval(multi_poly: geometry.MultiPolygon) -> None: + + assert ( + eval(repr(multi_poly), {}, {"MultiPolygon": geometry.MultiPolygon}) + == multi_poly + ) + + +@given(multi_polygons(srs=epsg4326)) +def test_shape(multi_poly: geometry.MultiPolygon) -> None: + assert multi_poly == shape(multi_poly) + + +@given(multi_polygons(srs=epsg4326)) +def test_bounds(multi_poly: geometry.MultiPolygon) -> None: + assert multi_poly.bounds == force_2d(multi_poly).bounds + + +@given(multi_polygons(srs=epsg4326)) +def test_convex_hull(multi_poly: geometry.MultiPolygon) -> None: + assert multi_poly.convex_hull == force_2d(multi_poly.convex_hull) + assert multi_poly.convex_hull == force_2d(multi_poly).convex_hull + assert multi_poly.convex_hull == force_3d(multi_poly).convex_hull + + +@given(multi_polygons(srs=epsg4326)) +def test_convex_hull_bounds(multi_poly: geometry.MultiPolygon) -> None: + """ + Test that the convex hull calculation preserves the original bounds. + + The bounds of the convex hull of a MultiPolygon must be equal to the bounds of + the MultiPolygon itself. + """ + assert multi_poly.convex_hull + assert multi_poly.convex_hull.bounds == multi_poly.bounds diff --git a/tests/hypothesis/test_point.py b/tests/hypothesis/test_point.py index 052a2c0d..0aef4d82 100644 --- a/tests/hypothesis/test_point.py +++ b/tests/hypothesis/test_point.py @@ -12,7 +12,7 @@ from pygeoif.types import PointType -@given(point_coords(epsg4326)) +@given(point_coords(srs=epsg4326)) def test_from_wkt_epsg_4326(point: PointType) -> None: point = geometry.Point(*point) @@ -38,3 +38,4 @@ def test_bounds(point: geometry.Point) -> None: @given(points()) def test_convex_hull(point: geometry.Point) -> None: assert point.convex_hull == force_2d(point) + assert point.convex_hull.bounds == point.bounds diff --git a/tests/hypothesis/test_polygon.py b/tests/hypothesis/test_polygon.py new file mode 100644 index 00000000..4577463e --- /dev/null +++ b/tests/hypothesis/test_polygon.py @@ -0,0 +1,57 @@ +"""Test Polygons with Hypothesis.""" + +from hypothesis import given + +from pygeoif import geometry +from pygeoif.factories import force_2d +from pygeoif.factories import from_wkt +from pygeoif.factories import shape +from pygeoif.hypothesis.strategies import epsg4326 +from pygeoif.hypothesis.strategies import polygons + + +@given(polygons(srs=epsg4326)) +def test_from_wkt_epsg_4326(poly: geometry.Polygon) -> None: + + assert poly == from_wkt(str(poly)) + + +@given(polygons()) +def test_repr_eval(poly: geometry.Polygon) -> None: + + assert eval(repr(poly), {}, {"Polygon": geometry.Polygon}) == poly + + +@given(polygons()) +def test_shape(poly: geometry.Polygon) -> None: + assert poly == shape(poly) + + +@given(polygons()) +def test_bounds(poly: geometry.Polygon) -> None: + assert poly.bounds == force_2d(poly).bounds + + +@given(polygons()) +def test_convex_hull_bounds(poly: geometry.Polygon) -> None: + """ + Test that the convex hull calculation preserves the original bounds. + + The bounds of the convex hull of a Polygon must be equal to the bounds of + the Polygon itself. + """ + assert poly.convex_hull + assert poly.convex_hull.bounds == poly.bounds + assert poly.exterior.convex_hull == poly.convex_hull + + +@given(polygons(min_interiors=1)) +def test_interiors(poly: geometry.Polygon) -> None: + """Test that the strategy generates Polygons with interiors.""" + assert tuple(poly.interiors) + + +@given(polygons(max_interiors=0)) +def test_no_interiors(poly: geometry.Polygon) -> None: + """Test that the strategy generates Polygons without interiors.""" + assert not tuple(poly.interiors) diff --git a/tests/test_geometrycollection.py b/tests/test_geometrycollection.py index 51b016cc..7621551b 100644 --- a/tests/test_geometrycollection.py +++ b/tests/test_geometrycollection.py @@ -1,6 +1,7 @@ """Test Baseclass.""" from pygeoif import geometry +from pygeoif.factories import from_wkt def test_geo_interface() -> None: @@ -393,6 +394,7 @@ def test_geometry_collection_neq_when_empty() -> None: assert gc1 != gc2 assert gc2 != gc1 + assert gc1 != gc1 # noqa: PLR0124 def test_nested_geometry_collection_repr_eval() -> None: @@ -425,3 +427,35 @@ def test_nested_geometry_collection_repr_eval() -> None: ).__geo_interface__ == gc.__geo_interface__ ) + + +def test_multipoint_collection_wkt_roundtrip() -> None: + gc = geometry.GeometryCollection((geometry.MultiPoint(((0.0, 0.0),)),)) + assert from_wkt(str(gc)) == gc + + +def test_multi_geometry_collection_wkt() -> None: + multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) + line = geometry.LineString([(0, 0), (3, 1)]) + lines = geometry.MultiLineString( + ([(0, 0), (1, 1), (1, 2), (2, 2)], [[0.0, 0.0], [1.0, 2.0]]), + ) + polys = geometry.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, 1), (1, 1), (1, 0)),), + ( + ((0, 0), (0, 1), (1, 1), (1, 0)), + (((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1)),), + ), + (((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)),), + ], + unique=True, + ) + ring = geometry.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)]) + gc = geometry.GeometryCollection([multipoint, line, lines, polys, ring]) + + assert from_wkt(str(gc)) == gc From ba3cee5b61f11d96b4248f53856a3a87fba7c4a5 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Fri, 22 Mar 2024 20:29:08 +0000 Subject: [PATCH 43/55] Update max_interiors and max_points in strategies.py --- pygeoif/hypothesis/strategies.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pygeoif/hypothesis/strategies.py b/pygeoif/hypothesis/strategies.py index aa66a726..e50f7d02 100644 --- a/pygeoif/hypothesis/strategies.py +++ b/pygeoif/hypothesis/strategies.py @@ -343,7 +343,7 @@ def polygons( # noqa: PLR0913 *, max_points: Optional[int] = None, min_interiors: int = 0, - max_interiors: Optional[int] = None, + max_interiors: int = 5, srs: Optional[Srs] = None, has_z: Optional[bool] = None, ) -> Polygon: @@ -448,8 +448,8 @@ def multi_line_strings( # noqa: PLR0913 draw: st.DrawFn, *, min_lines: int = 1, - max_lines: Optional[int] = None, - max_points: Optional[int] = None, + max_lines: int = 5, + max_points: int = 10, srs: Optional[Srs] = None, has_z: Optional[bool] = None, ) -> MultiLineString: @@ -494,10 +494,10 @@ def multi_polygons( # noqa: PLR0913 draw: st.DrawFn, *, min_polygons: int = 1, - max_polygons: Optional[int] = 5, - max_points: Optional[int] = 20, + max_polygons: int = 3, + max_points: int = 10, min_interiors: int = 0, - max_interiors: int = 5, + max_interiors: int = 2, srs: Optional[Srs] = None, has_z: Optional[bool] = None, ) -> MultiPolygon: @@ -544,7 +544,7 @@ def geometry_collections( # noqa: PLR0913 draw: st.DrawFn, *, min_geoms: int = 1, - max_geoms: Optional[int] = None, + max_geoms: int = 5, max_points: int = 20, min_interiors: int = 0, max_interiors: int = 5, From e06f2cf77a1f4d9c5b3e9d242ebf4dd2911375b2 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Fri, 22 Mar 2024 20:51:00 +0000 Subject: [PATCH 44/55] Only run hypothesis test once --- .github/workflows/run-all-tests.yml | 34 ++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index a5baf17b..5c427858 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -21,9 +21,11 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install -e ".[tests]" - - name: Test with pytest - run: | - pytest tests --cov=tests --cov=pygeoif --cov-report=xml + - name: Test with pytest, excluding hypothesis tests + run: >- + pytest tests + --cov=tests --cov=pygeoif --cov-report=xml + --ignore=tests/hypothesis - name: "Upload coverage to Codecov" if: ${{ matrix.python-version==3.11 }} uses: codecov/codecov-action@v4 @@ -32,6 +34,26 @@ jobs: verbose: true token: ${{ secrets.CODECOV_TOKEN }} + hypothesis-tests: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.12'] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -e ".[tests]" + - name: Test with pytest including hypothesis tests + run: | + pytest tests + static-tests: runs-on: ubuntu-latest strategy: @@ -78,13 +100,13 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install -e ".[tests]" - - name: Test with pytest + - name: Test with pytest, excluding hypothesis tests run: | - pytest tests + pytest tests --ignore=tests/hypothesis publish: if: "github.event_name == 'push' && github.repository == 'cleder/pygeoif'" - needs: [cpython, static-tests, pypy] + needs: [cpython, static-tests, pypy, hypothesis-tests] name: Build and publish to PyPI and TestPyPI runs-on: ubuntu-latest steps: From 059b422e40fd4049471d2cd2cacecb3b61fc54e9 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Fri, 22 Mar 2024 20:59:32 +0000 Subject: [PATCH 45/55] Add test for nested GeometryCollections in WKT parsing --- tests/test_geometrycollection.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_geometrycollection.py b/tests/test_geometrycollection.py index 7621551b..1ab7ec89 100644 --- a/tests/test_geometrycollection.py +++ b/tests/test_geometrycollection.py @@ -1,5 +1,7 @@ """Test Baseclass.""" +import pytest + from pygeoif import geometry from pygeoif.factories import from_wkt @@ -459,3 +461,15 @@ def test_multi_geometry_collection_wkt() -> None: gc = geometry.GeometryCollection([multipoint, line, lines, polys, ring]) assert from_wkt(str(gc)) == gc + + +@pytest.mark.xfail(reason="WKT parsing for nested GeometryCollections not implemented.") +def test_nested_geometry_collections_wkt() -> None: + multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) + gc1 = geometry.GeometryCollection([geometry.Point(0, 0), multipoint]) + line = geometry.LineString([(0, 0), (3, 1)]) + gc2 = geometry.GeometryCollection([gc1, line]) + poly1 = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) + gc3 = geometry.GeometryCollection([gc2, poly1]) + + assert from_wkt(str(gc3)) == gc3 From 8cd463122cc49444a30cb24d8cdf9b5ad151bc94 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Fri, 22 Mar 2024 21:35:00 +0000 Subject: [PATCH 46/55] Update documentation --- README.rst | 19 +++++++++++++------ docs/HISTORY.rst | 6 ++++-- pyproject.toml | 3 +-- tox.ini | 2 +- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index a32bbdd1..c83934bb 100644 --- a/README.rst +++ b/README.rst @@ -33,26 +33,32 @@ out of points, polygons from linear rings, multi polygons from polygons, etc. It was inspired by shapely and implements the geometries in a way that when you are familiar with pygeoif, you will feel right at home with shapely or the other way round. +It provides Hypothesis strategies for all geometries for property based +testing with Hypothesis_. It was written to provide clean and python only geometries for fastkml_ +.. 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 :target: https://github.com/cleder/pygeoif/actions/workflows/run-all-tests.yml :alt: GitHub Actions -.. image:: https://readthedocs.org/projects/pygeoif/badge/?version=latest - :target: https://pygeoif.readthedocs.io/en/latest/?badge=latest - :alt: Documentation Status - .. 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/code%20style-black-000000.svg +.. image:: https://img.shields.io/badge/property_based_tests-hypothesis-green + :target: https://hypothesis.works + :alt: Hypothesis + +.. 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%20checker-mypy-blue +.. image:: https://img.shields.io/badge/type_checker-mypy-blue :target: http://mypy-lang.org/ :alt: Mypy @@ -411,3 +417,4 @@ The tests were improved with mutmut_ which discovered some nasty edge cases. .. _mutmut: https://github.com/boxed/mutmut .. _GeoJSON: https://geojson.org/ .. _fastkml: http://pypi.python.org/pypi/fastkml/ +.. _Hypothesis: https://hypothesis.works diff --git a/docs/HISTORY.rst b/docs/HISTORY.rst index d3e2cc87..bd57078c 100644 --- a/docs/HISTORY.rst +++ b/docs/HISTORY.rst @@ -3,8 +3,10 @@ Changelog 1.4.0 (unreleased) ------------------ - - + - add Hypothesis tests [Ben Shaver, Christian Ledermann] + - fix ``convex_hull`` edge-cases discovered by Hypothesis tests + - fix ``from_wkt`` to include multi geometries in GeometryCollections + - drop Python 3.7 support 1.3.0 (2024/02/05) ------------------ diff --git a/pyproject.toml b/pyproject.toml index cbfe8963..5ada7481 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -40,7 +39,7 @@ keywords = [ "WKT", ] name = "pygeoif" -requires-python = ">=3.7" +requires-python = ">=3.8" [project.license] text = "LGPL" diff --git a/tox.ini b/tox.ini index eacef83a..4867526c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [flake8] -min_python_version = 3.7 +min_python_version = 3.8 exclude = .git,__pycache__,docs/source/conf.py,old,build,dist max_line_length = 89 ignore= From c7ccf2ebd9eae8aedb933b618ce9de9ed0542dcd Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Fri, 22 Mar 2024 21:40:34 +0000 Subject: [PATCH 47/55] add strategies to documentation --- docs/pygeoif.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/pygeoif.rst b/docs/pygeoif.rst index 024b062e..61e21222 100644 --- a/docs/pygeoif.rst +++ b/docs/pygeoif.rst @@ -65,3 +65,11 @@ pygeoif.about module :members: :undoc-members: :special-members: __version__ + +pygeoif.hypothesis.strategies module +-------------------- + +.. automodule:: pygeoif.hypothesis.strategies + :members: + :undoc-members: + :special-members: __version__ From 8bb2ee0198ff1522947f326ad014b767f8393fb5 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Fri, 22 Mar 2024 21:45:44 +0000 Subject: [PATCH 48/55] remove Python 3.7 and PyPy versions in GitHub workflow --- .github/workflows/run-all-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index 5c427858..415983b0 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', '3.13-dev'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13-dev'] steps: - uses: actions/checkout@v4 @@ -89,7 +89,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - pypy-version: ['pypy-3.7', 'pypy-3.8', 'pypy-3.9', 'pypy-3.10'] + pypy-version: ['pypy-3.8', 'pypy-3.9', 'pypy-3.10'] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.pypy-version }} From 473e97d2ec6ee1aeb3d9e1e92c93af2bc15efa5d Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Fri, 22 Mar 2024 22:03:36 +0000 Subject: [PATCH 49/55] measure test coverage including the hypothesis tests --- .github/workflows/run-all-tests.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index 415983b0..33df0377 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -24,15 +24,7 @@ jobs: - name: Test with pytest, excluding hypothesis tests run: >- pytest tests - --cov=tests --cov=pygeoif --cov-report=xml --ignore=tests/hypothesis - - name: "Upload coverage to Codecov" - if: ${{ matrix.python-version==3.11 }} - uses: codecov/codecov-action@v4 - with: - fail_ci_if_error: true - verbose: true - token: ${{ secrets.CODECOV_TOKEN }} hypothesis-tests: runs-on: ubuntu-latest @@ -52,7 +44,13 @@ jobs: python -m pip install -e ".[tests]" - name: Test with pytest including hypothesis tests run: | - pytest tests + pytest tests --cov=tests --cov=pygeoif --cov-report=xml + - name: "Upload coverage to Codecov" + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true + verbose: true + token: ${{ secrets.CODECOV_TOKEN }} static-tests: runs-on: ubuntu-latest From bf9cbb4f4131f8e16007c5466697e4607b1907d6 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Fri, 22 Mar 2024 22:29:02 +0000 Subject: [PATCH 50/55] Add test coverage hypothesis profile for pytest --- .github/workflows/run-all-tests.yml | 24 +++++++++++++++++++++++- tests/conftest.py | 1 + 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index 33df0377..b1602d8e 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -43,8 +43,30 @@ jobs: python -m pip install --upgrade pip python -m pip install -e ".[tests]" - name: Test with pytest including hypothesis tests + run: >- + pytest tests + + test-coverage: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.12'] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies run: | - pytest tests --cov=tests --cov=pygeoif --cov-report=xml + python -m pip install --upgrade pip + python -m pip install -e ".[tests]" + - name: Test with pytest including hypothesis tests and coverage + run: >- + pytest tests + --cov=tests --cov=pygeoif --cov-report=xml + --hypothesis-profile=coverage - name: "Upload coverage to Codecov" uses: codecov/codecov-action@v4 with: diff --git a/tests/conftest.py b/tests/conftest.py index 996290db..7b2f68cd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,3 +8,4 @@ from hypothesis import settings settings.register_profile("exhaustive", max_examples=10_000) +settings.register_profile("coverage", max_examples=10) From 059cc6f429170e4f126d053df10f578b4305d266 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Fri, 22 Mar 2024 22:39:10 +0000 Subject: [PATCH 51/55] Add HealthCheck suppression to coverage profile --- tests/conftest.py | 7 ++++++- tests/hypothesis/test_geometrycollection.py | 2 -- tests/hypothesis/test_line.py | 2 -- tests/hypothesis/test_linear_ring.py | 2 -- tests/hypothesis/test_multilinestring.py | 2 -- tests/hypothesis/test_multipoint.py | 1 - tests/hypothesis/test_multipolygon.py | 2 -- tests/hypothesis/test_polygon.py | 2 -- 8 files changed, 6 insertions(+), 14 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7b2f68cd..7a4dc903 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,12 @@ Run this profile with ``pytest --hypothesis-profile=exhaustive`` """ +from hypothesis import HealthCheck from hypothesis import settings settings.register_profile("exhaustive", max_examples=10_000) -settings.register_profile("coverage", max_examples=10) +settings.register_profile( + "coverage", + max_examples=10, + suppress_health_check=[HealthCheck.too_slow], +) diff --git a/tests/hypothesis/test_geometrycollection.py b/tests/hypothesis/test_geometrycollection.py index 761b66a6..c965c39b 100644 --- a/tests/hypothesis/test_geometrycollection.py +++ b/tests/hypothesis/test_geometrycollection.py @@ -11,13 +11,11 @@ @given(geometry_collections(srs=epsg4326)) def test_from_wkt_epsg_4326(multi_poly: geometry.GeometryCollection) -> None: - assert multi_poly == from_wkt(str(multi_poly)) @given(geometry_collections(srs=epsg4326)) def test_repr_eval(multi_poly: geometry.GeometryCollection) -> None: - assert ( eval( repr(multi_poly), diff --git a/tests/hypothesis/test_line.py b/tests/hypothesis/test_line.py index b3c3f373..195191f9 100644 --- a/tests/hypothesis/test_line.py +++ b/tests/hypothesis/test_line.py @@ -13,13 +13,11 @@ @given(line_strings(srs=epsg4326)) def test_from_wkt_epsg_4326(line: geometry.LineString) -> None: - assert line == from_wkt(str(line)) @given(line_strings()) def test_repr_eval(line: geometry.LineString) -> None: - assert eval(repr(line), {}, {"LineString": geometry.LineString}) == line diff --git a/tests/hypothesis/test_linear_ring.py b/tests/hypothesis/test_linear_ring.py index ec3e00df..84fe77a8 100644 --- a/tests/hypothesis/test_linear_ring.py +++ b/tests/hypothesis/test_linear_ring.py @@ -11,13 +11,11 @@ @given(linear_rings(srs=epsg4326)) def test_from_wkt_epsg_4326(line: geometry.LinearRing) -> None: - assert line == from_wkt(str(line)) @given(linear_rings()) def test_repr_eval(line: geometry.LinearRing) -> None: - assert eval(repr(line), {}, {"LinearRing": geometry.LinearRing}) == line diff --git a/tests/hypothesis/test_multilinestring.py b/tests/hypothesis/test_multilinestring.py index b815c209..740727a6 100644 --- a/tests/hypothesis/test_multilinestring.py +++ b/tests/hypothesis/test_multilinestring.py @@ -13,13 +13,11 @@ @given(multi_line_strings(srs=epsg4326)) def test_from_wkt_epsg_4326(line: geometry.MultiLineString) -> None: - assert line == from_wkt(str(line)) @given(multi_line_strings()) def test_repr_eval(line: geometry.MultiLineString) -> None: - assert eval(repr(line), {}, {"MultiLineString": geometry.MultiLineString}) == line diff --git a/tests/hypothesis/test_multipoint.py b/tests/hypothesis/test_multipoint.py index cf98c319..481718ab 100644 --- a/tests/hypothesis/test_multipoint.py +++ b/tests/hypothesis/test_multipoint.py @@ -12,7 +12,6 @@ @given(multi_points(srs=epsg4326)) def test_from_wkt_epsg_4326(multi_point: geometry.MultiPoint) -> None: - assert multi_point == from_wkt(str(multi_point)) diff --git a/tests/hypothesis/test_multipolygon.py b/tests/hypothesis/test_multipolygon.py index 3a5dad28..4089b318 100644 --- a/tests/hypothesis/test_multipolygon.py +++ b/tests/hypothesis/test_multipolygon.py @@ -13,13 +13,11 @@ @given(multi_polygons(srs=epsg4326)) def test_from_wkt_epsg_4326(multi_poly: geometry.MultiPolygon) -> None: - assert multi_poly == from_wkt(str(multi_poly)) @given(multi_polygons(srs=epsg4326)) def test_repr_eval(multi_poly: geometry.MultiPolygon) -> None: - assert ( eval(repr(multi_poly), {}, {"MultiPolygon": geometry.MultiPolygon}) == multi_poly diff --git a/tests/hypothesis/test_polygon.py b/tests/hypothesis/test_polygon.py index 4577463e..6331af26 100644 --- a/tests/hypothesis/test_polygon.py +++ b/tests/hypothesis/test_polygon.py @@ -12,13 +12,11 @@ @given(polygons(srs=epsg4326)) def test_from_wkt_epsg_4326(poly: geometry.Polygon) -> None: - assert poly == from_wkt(str(poly)) @given(polygons()) def test_repr_eval(poly: geometry.Polygon) -> None: - assert eval(repr(poly), {}, {"Polygon": geometry.Polygon}) == poly From 5556e26f847735c8c40137e848c96d2b262a069b Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Fri, 22 Mar 2024 23:04:27 +0000 Subject: [PATCH 52/55] Increase coverage --- tests/hypothesis/test_geometrycollection.py | 9 +++++++-- tests/hypothesis/test_line.py | 6 ++++++ tests/hypothesis/test_linear_ring.py | 6 ++++++ tests/hypothesis/test_polygon.py | 6 ++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/hypothesis/test_geometrycollection.py b/tests/hypothesis/test_geometrycollection.py index c965c39b..5178ea67 100644 --- a/tests/hypothesis/test_geometrycollection.py +++ b/tests/hypothesis/test_geometrycollection.py @@ -35,6 +35,11 @@ def test_repr_eval(multi_poly: geometry.GeometryCollection) -> None: ) -@given(geometry_collections(srs=epsg4326)) -def test_shape(multi_poly: geometry.GeometryCollection) -> None: +@given(geometry_collections(srs=epsg4326, has_z=False)) +def test_shape_2d(multi_poly: geometry.GeometryCollection) -> None: + assert multi_poly == shape(multi_poly) + + +@given(geometry_collections(srs=epsg4326, has_z=True)) +def test_shape_3d(multi_poly: geometry.GeometryCollection) -> None: assert multi_poly == shape(multi_poly) diff --git a/tests/hypothesis/test_line.py b/tests/hypothesis/test_line.py index 195191f9..d70afa10 100644 --- a/tests/hypothesis/test_line.py +++ b/tests/hypothesis/test_line.py @@ -1,5 +1,6 @@ """Test LineStrings with Hypothesis.""" +import pytest from hypothesis import given from pygeoif import geometry @@ -11,6 +12,11 @@ from pygeoif.hypothesis.strategies import line_strings +def test_max_points_lt_2() -> None: + with pytest.raises(ValueError, match="^max_points must be greater than 1$"): + line_strings(max_points=1).example() + + @given(line_strings(srs=epsg4326)) def test_from_wkt_epsg_4326(line: geometry.LineString) -> None: assert line == from_wkt(str(line)) diff --git a/tests/hypothesis/test_linear_ring.py b/tests/hypothesis/test_linear_ring.py index 84fe77a8..03db5e96 100644 --- a/tests/hypothesis/test_linear_ring.py +++ b/tests/hypothesis/test_linear_ring.py @@ -1,5 +1,6 @@ """Test LinearRings with Hypothesis.""" +import pytest from hypothesis import given from pygeoif import geometry @@ -9,6 +10,11 @@ from pygeoif.hypothesis.strategies import linear_rings +def test_max_points_lt_3() -> None: + with pytest.raises(ValueError, match="^max_points must be greater than 3$"): + linear_rings(max_points=3).example() + + @given(linear_rings(srs=epsg4326)) def test_from_wkt_epsg_4326(line: geometry.LinearRing) -> None: assert line == from_wkt(str(line)) diff --git a/tests/hypothesis/test_polygon.py b/tests/hypothesis/test_polygon.py index 6331af26..08f1fa35 100644 --- a/tests/hypothesis/test_polygon.py +++ b/tests/hypothesis/test_polygon.py @@ -1,5 +1,6 @@ """Test Polygons with Hypothesis.""" +import pytest from hypothesis import given from pygeoif import geometry @@ -10,6 +11,11 @@ from pygeoif.hypothesis.strategies import polygons +def test_max_points_lt_3() -> None: + with pytest.raises(ValueError, match="^max_points must be greater than 3$"): + polygons(max_points=3).example() + + @given(polygons(srs=epsg4326)) def test_from_wkt_epsg_4326(poly: geometry.Polygon) -> None: assert poly == from_wkt(str(poly)) From bd4d656faa18a3603b6d6119392eb05d03f08151 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Fri, 22 Mar 2024 23:10:03 +0000 Subject: [PATCH 53/55] hypothesis messes with the exception message --- tests/hypothesis/test_line.py | 2 +- tests/hypothesis/test_linear_ring.py | 2 +- tests/hypothesis/test_polygon.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/hypothesis/test_line.py b/tests/hypothesis/test_line.py index d70afa10..6cac10ce 100644 --- a/tests/hypothesis/test_line.py +++ b/tests/hypothesis/test_line.py @@ -13,7 +13,7 @@ def test_max_points_lt_2() -> None: - with pytest.raises(ValueError, match="^max_points must be greater than 1$"): + with pytest.raises(ValueError, match="^max_points must be greater than 1"): line_strings(max_points=1).example() diff --git a/tests/hypothesis/test_linear_ring.py b/tests/hypothesis/test_linear_ring.py index 03db5e96..035825d0 100644 --- a/tests/hypothesis/test_linear_ring.py +++ b/tests/hypothesis/test_linear_ring.py @@ -11,7 +11,7 @@ def test_max_points_lt_3() -> None: - with pytest.raises(ValueError, match="^max_points must be greater than 3$"): + with pytest.raises(ValueError, match="^max_points must be greater than 3"): linear_rings(max_points=3).example() diff --git a/tests/hypothesis/test_polygon.py b/tests/hypothesis/test_polygon.py index 08f1fa35..8e31edf5 100644 --- a/tests/hypothesis/test_polygon.py +++ b/tests/hypothesis/test_polygon.py @@ -12,7 +12,7 @@ def test_max_points_lt_3() -> None: - with pytest.raises(ValueError, match="^max_points must be greater than 3$"): + with pytest.raises(ValueError, match="^max_points must be greater than 3"): polygons(max_points=3).example() From f3cb47fe5bef0d6df009e6f8a9373d42747b185e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 16:36:15 +0000 Subject: [PATCH 54/55] [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.3 → v0.3.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.3...v0.3.4) --- .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 b8f5b42f..6a971e43 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.3' + rev: 'v0.3.4' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From af89dce1642827a8abc42de67d9961502ecadb7b Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 25 Mar 2024 17:35:58 +0000 Subject: [PATCH 55/55] prepare 1.4 release --- .gitignore | 2 +- .sourcery.yaml | 2 +- docs/HISTORY.rst | 18 +++++++++--------- pygeoif/about.py | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 87d5c97f..c4995dec 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,7 @@ coverage.xml .watchmanconfig html/ .dccache -.pytest_cache +.pytest_cache/ .pyre/ .mypy_cache/ .pytype/ diff --git a/.sourcery.yaml b/.sourcery.yaml index f300aba4..35fa4dc6 100644 --- a/.sourcery.yaml +++ b/.sourcery.yaml @@ -1,2 +1,2 @@ refactor: - python_version: '3.7' + python_version: '3.8' diff --git a/docs/HISTORY.rst b/docs/HISTORY.rst index bd57078c..23935c13 100644 --- a/docs/HISTORY.rst +++ b/docs/HISTORY.rst @@ -1,12 +1,13 @@ Changelog ========= -1.4.0 (unreleased) +1.4.0 (2024/03/25) ------------------ - - add Hypothesis tests [Ben Shaver, Christian Ledermann] - - fix ``convex_hull`` edge-cases discovered by Hypothesis tests - - fix ``from_wkt`` to include multi geometries in GeometryCollections - - drop Python 3.7 support + +- add Hypothesis tests [Ben Shaver, Christian Ledermann] +- fix ``convex_hull`` edge-cases discovered by Hypothesis tests +- fix ``from_wkt`` to include multi geometries in GeometryCollections +- drop Python 3.7 support 1.3.0 (2024/02/05) ------------------ @@ -18,14 +19,13 @@ Changelog 1.2.0 (2023/11/27) ------------------ - - remove Python 3.7 support - - Geometries are now immutable (but not hashable) - - add ``force_2d`` and ``force_3d`` factories [Alex Svetkin] +- Geometries are now immutable (but not hashable) +- add ``force_2d`` and ``force_3d`` factories [Alex Svetkin] 1.1.1 (2023/10/27) ------------------ - - Use ``pyproject.toml``, remove ``setup.py`` and ``MANIFEST.in`` +- Use ``pyproject.toml``, remove ``setup.py`` and ``MANIFEST.in`` 1.1 (2023/10/13) ----------------- diff --git a/pygeoif/about.py b/pygeoif/about.py index 29e2e3a5..e274e4dd 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.3.0" +__version__ = "1.4.0"