diff --git a/.github/workflows/codeball.yml b/.github/workflows/codeball.yml deleted file mode 100644 index ea0bf4a8..00000000 --- a/.github/workflows/codeball.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: Codeball -on: [pull_request] - -jobs: - codeball_job: - runs-on: ubuntu-latest - name: Codeball - steps: - # Run Codeball on all new Pull Requests 🚀 - # For customizations and more documentation, see https://github.com/sturdy-dev/codeball-action - - name: Codeball - uses: sturdy-dev/codeball-action@v2 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ed464619..2721ebe9 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,7 +39,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/codesee-arch-diagram.yml b/.github/workflows/codesee-arch-diagram.yml index d6752af0..3435bcd2 100644 --- a/.github/workflows/codesee-arch-diagram.yml +++ b/.github/workflows/codesee-arch-diagram.yml @@ -5,77 +5,16 @@ on: pull_request_target: types: [opened, synchronize, reopened] -name: CodeSee Map +name: CodeSee + +permissions: read-all jobs: - test_map_action: + codesee: runs-on: ubuntu-latest continue-on-error: true - name: Run CodeSee Map Analysis + name: Analyze the repo with CodeSee steps: - - name: checkout - id: checkout - uses: actions/checkout@v3 - with: - repository: ${{ github.event.pull_request.head.repo.full_name }} - ref: ${{ github.event.pull_request.head.ref }} - fetch-depth: 0 - - # codesee-detect-languages has an output with id languages. - - name: Detect Languages - id: detect-languages - uses: Codesee-io/codesee-detect-languages-action@latest - - - name: Configure JDK 16 - uses: actions/setup-java@v3 - if: ${{ fromJSON(steps.detect-languages.outputs.languages).java }} - with: - java-version: '16' - distribution: 'zulu' - - # CodeSee Maps Go support uses a static binary so there's no setup step required. - - - name: Configure Node.js 14 - uses: actions/setup-node@v3.5.0 - if: ${{ fromJSON(steps.detect-languages.outputs.languages).javascript }} - with: - node-version: '14' - - - name: Configure Python 3.x - uses: actions/setup-python@v4 - if: ${{ fromJSON(steps.detect-languages.outputs.languages).python }} - with: - python-version: '3.10' - architecture: 'x64' - - - name: Configure Ruby '3.x' - uses: ruby/setup-ruby@v1 - if: ${{ fromJSON(steps.detect-languages.outputs.languages).ruby }} - with: - ruby-version: '3.0' - - # CodeSee Maps Rust support uses a static binary so there's no setup step required. - - - name: Generate Map - id: generate-map - uses: Codesee-io/codesee-map-action@latest - with: - step: map - github_ref: ${{ github.ref }} - languages: ${{ steps.detect-languages.outputs.languages }} - - - name: Upload Map - id: upload-map - uses: Codesee-io/codesee-map-action@latest - with: - step: mapUpload - api_token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} - github_ref: ${{ github.ref }} - - - name: Insights - id: insights - uses: Codesee-io/codesee-map-action@latest + - uses: Codesee-io/codesee-action@v2 with: - step: insights - api_token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} - github_ref: ${{ github.ref }} + codesee-token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index efe7a516..f16bc71e 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -9,10 +9,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11-dev'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: @@ -23,9 +23,9 @@ jobs: pip install pytest pytest-cov typing_extensions - name: Test with pytest run: | - pytest pygeoif --cov=pygeoif --cov-fail-under=100 --cov-report=xml + pytest tests --cov=tests --cov=pygeoif --cov-report=xml - name: "Upload coverage to Codecov" - if: ${{ matrix.python-version==3.9 }} + if: ${{ matrix.python-version==3.11 }} uses: codecov/codecov-action@v3 with: fail_ci_if_error: true @@ -35,10 +35,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9'] + python-version: ['3.10'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: @@ -53,7 +53,7 @@ jobs: - name: Linting run: | flake8 pygeoif - black --check pygeoif + black --check pygeoif tests yamllint .github/workflows/ - name: Check complexity run: | @@ -65,9 +65,9 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - pypy-version: ['pypy-3.7', 'pypy-3.8', 'pypy-3.9'] + pypy-version: ['pypy-3.7', 'pypy-3.8', 'pypy-3.9', 'pypy-3.10'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.pypy-version }} uses: actions/setup-python@v4 with: @@ -78,7 +78,7 @@ jobs: pip install pytest typing_extensions - name: Test with pytest run: | - pytest pygeoif + pytest tests publish: if: "github.event_name == 'push' && github.repository == 'cleder/pygeoif'" @@ -86,7 +86,7 @@ jobs: name: Build and publish to PyPI and TestPyPI runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python 3.9 uses: actions/setup-python@v4 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2b5d1fc6..01ad2a38 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: hooks: - id: check-useless-excludes - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.5.0 hooks: - id: check-added-large-files - id: check-docstring-first @@ -24,7 +24,7 @@ repos: - id: requirements-txt-fixer - id: trailing-whitespace - repo: https://github.com/ikamensh/flynt/ - rev: '0.76' + rev: '1.0.1' hooks: - id: flynt - repo: https://github.com/MarcoGorelli/absolufy-imports @@ -32,36 +32,44 @@ repos: hooks: - id: absolufy-imports - repo: https://github.com/hakancelikdev/unimport - rev: 0.12.1 + rev: 1.0.0 hooks: - id: unimport args: [--remove, --include-star-import, --ignore-init, --gitignore] - repo: https://github.com/pycqa/isort - rev: 5.10.1 + rev: 5.12.0 hooks: - id: isort + # - repo: https://github.com/dhruvmanila/remove-print-statements + # rev: 'v0.5.0' + # hooks: + # - id: remove-print-statements + # args: ['--verbose'] # Show all the print statements to be removed - repo: https://github.com/psf/black - rev: 22.8.0 + rev: 23.9.1 hooks: - id: black + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: 'v0.0.292' + hooks: + - id: ruff - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 + rev: 6.1.0 hooks: - id: flake8 additional_dependencies: - - flake8 - - flake8-absolute-import - - flake8-awesome - - flake8-bandit - - flake8-blind-except + # - flake8-absolute-import + # - flake8-awesome + # - flake8-bandit + # - flake8-blind-except - flake8-bugbear # - flake8-class-attributes-order - flake8-cognitive-complexity - - flake8-commas + # - flake8-commas - flake8-comments - flake8-complex-f-strings - - flake8-continuation - - flake8-debugger + # - flake8-continuation + # - flake8-debugger - flake8-docstrings # - flake8-dunder-all - flake8-encodings @@ -79,28 +87,28 @@ repos: - flake8-string-format - flake8-super - flake8-typing-imports - - flake8-use-fstring + # - flake8-use-fstring - pep8-naming - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.981 + rev: v1.5.1 hooks: - id: mypy - - repo: https://github.com/Lucas-C/pre-commit-hooks-markup - rev: v1.0.1 - hooks: - - id: rst-linter + # - repo: https://github.com/Lucas-C/pre-commit-hooks-markup + # rev: v1.0.1 + # hooks: + # - id: rst-linter - repo: https://github.com/mgedmin/check-manifest - rev: "0.48" + rev: "0.49" hooks: - id: check-manifest - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.18.3 + rev: "0.27.0" hooks: - id: check-github-workflows - id: check-github-actions - id: check-readthedocs - - repo: https://github.com/regebro/pyroma - rev: "3.2" - hooks: - - id: pyroma + # - repo: https://github.com/regebro/pyroma + # rev: "4.1" + # hooks: + # - id: pyroma ... diff --git a/.sourcery.yaml b/.sourcery.yaml new file mode 100644 index 00000000..f300aba4 --- /dev/null +++ b/.sourcery.yaml @@ -0,0 +1,2 @@ +refactor: + python_version: '3.7' diff --git a/MANIFEST.in b/MANIFEST.in index f2aaed0c..3f1f5cd2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,3 +4,4 @@ recursive-exclude *.pyc *.pyo include docs/LICENSE.GPL include pygeoif/py.typed exclude pygeoif/.* +recursive-include tests *.py diff --git a/README.rst b/README.rst index 90b46d0f..1885abd4 100644 --- a/README.rst +++ b/README.rst @@ -45,7 +45,7 @@ It was written to provide clean and python only geometries for fastkml_ :target: https://www.codefactor.io/repository/github/cleder/pygeoif/overview/main :alt: CodeFactor -.. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white +.. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit :target: https://github.com/pre-commit/pre-commit :alt: pre-commit diff --git a/docs/HISTORY.txt b/docs/HISTORY.txt index fa011980..1916fb4e 100644 --- a/docs/HISTORY.txt +++ b/docs/HISTORY.txt @@ -1,6 +1,14 @@ Changelog ========= +1.1 (2023/10/13) +----------------- + +- Fix nested MultiGeometries +- Improve type annotations +- Add Python 3.12 to supported versions +- Last version to support Python 3.7 + 1.0 (2022/09/29) ------------------------ diff --git a/pygeoif/__init__.py b/pygeoif/__init__.py index bb9c53b2..d030abd7 100644 --- a/pygeoif/__init__.py +++ b/pygeoif/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2012 - 2022 Christian Ledermann # diff --git a/pygeoif/exceptions.py b/pygeoif/exceptions.py index 9b694ca9..0974b098 100644 --- a/pygeoif/exceptions.py +++ b/pygeoif/exceptions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2012 -2022 Christian Ledermann # diff --git a/pygeoif/factories.py b/pygeoif/factories.py index 675b8bf8..e5c2fa35 100644 --- a/pygeoif/factories.py +++ b/pygeoif/factories.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2012 -2022 Christian Ledermann # @@ -155,8 +154,8 @@ def shape( geometries = [ shape(fi) for fi in geometry["geometries"] # type: ignore [typeddict-item] ] - return GeometryCollection(geometries) # type: ignore [arg-type] - raise NotImplementedError(f"[{geometry['type']} is nor implemented") + return GeometryCollection(geometries) + raise NotImplementedError(f"[{geometry['type']} is not implemented") def num(number: str) -> float: @@ -269,9 +268,8 @@ def _multipolygon_from_wkt_coordinates(coordinates: str) -> MultiPolygon: def _multigeometry_from_wkt_coordinates(coordinates: str) -> GeometryCollection: gc_types = gcre.findall(coordinates) gc_coords = gcre.split(coordinates)[1:] - assert len(gc_types) == len(gc_coords) # noqa: S101 geometries: List[Geometry] = [] - for (gc_type, gc_coord) in zip(gc_types, gc_coords): + for gc_type, gc_coord in zip(gc_types, gc_coords): gc_wkt = gc_type + gc_coord[: gc_coord.rfind(")") + 1] geometries.append(cast(Geometry, from_wkt(gc_wkt))) return GeometryCollection(geometries) diff --git a/pygeoif/feature.py b/pygeoif/feature.py index f287c4a5..040d7766 100644 --- a/pygeoif/feature.py +++ b/pygeoif/feature.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2012 -2022 Christian Ledermann # diff --git a/pygeoif/functions.py b/pygeoif/functions.py index 2cefcda8..1c803dc2 100644 --- a/pygeoif/functions.py +++ b/pygeoif/functions.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- # -# Copyright (C) 2012 -2022 Christian Ledermann +# Copyright (C) 2012 -2023 Christian Ledermann # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -27,6 +26,8 @@ from typing import cast from pygeoif.types import CoordinatesType +from pygeoif.types import GeoCollectionInterface +from pygeoif.types import GeoInterface from pygeoif.types import LineType from pygeoif.types import MultiCoordinatesType from pygeoif.types import Point2D @@ -59,7 +60,6 @@ def centroid(coords: LineType) -> Tuple[Point2D, float]: # For all vertices for i, coord in enumerate(coords): - next_coord = coords[(i + 1) % n] # Calculate area using shoelace formula area = (coord[0] * next_coord[1]) - (next_coord[0] * coord[1]) @@ -76,7 +76,8 @@ def centroid(coords: LineType) -> Tuple[Point2D, float]: def _cross(o: Point2D, a: Point2D, b: Point2D) -> float: - """2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product. + """ + 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product. Returns a positive value, if OAB makes a counter-clockwise turn, negative for clockwise turn, and zero if the points are collinear. @@ -159,4 +160,36 @@ def compare_coordinates( return False -__all__ = ["centroid", "compare_coordinates", "convex_hull", "dedupe", "signed_area"] +def compare_geo_interface( + first: Union[GeoInterface, GeoCollectionInterface], + second: Union[GeoInterface, GeoCollectionInterface], +) -> bool: + """Compare two geo interfaces.""" + try: + if first["type"] != second["type"]: + return False + if first["type"] == "GeometryCollection": + return all( + compare_geo_interface(g1, g2) # type: ignore [arg-type] + for g1, g2 in zip_longest( + first["geometries"], # type: ignore [typeddict-item] + second["geometries"], # type: ignore [typeddict-item] + fillvalue={"type": None, "coordinates": ()}, + ) + ) + return compare_coordinates( + first["coordinates"], # type: ignore [typeddict-item] + second["coordinates"], # type: ignore [typeddict-item] + ) + except KeyError: + return False + + +__all__ = [ + "centroid", + "compare_coordinates", + "compare_geo_interface", + "convex_hull", + "dedupe", + "signed_area", +] diff --git a/pygeoif/geometry.py b/pygeoif/geometry.py index a3bdb64a..28d2f70d 100644 --- a/pygeoif/geometry.py +++ b/pygeoif/geometry.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- # -# Copyright (C) 2012 -2022 Christian Ledermann +# Copyright (C) 2012 -2023 Christian Ledermann # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -33,6 +32,7 @@ from pygeoif.exceptions import DimensionError from pygeoif.functions import centroid from pygeoif.functions import compare_coordinates +from pygeoif.functions import compare_geo_interface from pygeoif.functions import convex_hull from pygeoif.functions import dedupe from pygeoif.functions import signed_area @@ -102,6 +102,7 @@ def convex_hull(self) -> Optional[Union["Point", "LineString", "Polygon"]]: warnings.warn( "The convex Hull will only return the projection to" " 2 dimensions xy coordinates", + stacklevel=2, ) hull = convex_hull(self._prepare_hull()) @@ -251,11 +252,9 @@ def y(self) -> float: @property def z(self) -> Optional[float]: """Return z coordinate.""" - return ( - self._coordinates[2] # type: ignore[misc] - if len(self._coordinates) == 3 - else None - ) + if self.has_z: + return self._coordinates[2] # type: ignore [misc] + raise DimensionError(f"The {self!r} geometry does not have z values") @property def coords(self) -> Tuple[PointType]: @@ -1001,7 +1000,9 @@ class isn't generally supported by ordinary GIS sw (viewers and so on). So {'type': 'Point', 'coordinates': (1.0, -1.0)}]} """ - def __init__(self, geometries: Iterable[Geometry]) -> None: + def __init__( + self, geometries: Iterable[Union[Geometry, "GeometryCollection"]] + ) -> None: """ Initialize the MultiGeometry with Geometries. @@ -1017,6 +1018,8 @@ def __eq__(self, other: object) -> bool: Types and coordinates from all contained geometries must be equal. """ try: + if self.is_empty: + return False if ( other.__geo_interface__.get("type") # type: ignore [attr-defined] != self.geom_type @@ -1033,18 +1036,9 @@ def __eq__(self, other: object) -> bool: return False except AttributeError: return False - return all( - ( - s["type"] == o.get("type") - and compare_coordinates(s["coordinates"], o.get("coordinates")) - for s, o in zip( - (geom.__geo_interface__ for geom in self.geoms), - other.__geo_interface__.get( # type: ignore [attr-defined] - "geometries", - [], - ), - ) - ), + return compare_geo_interface( + self.__geo_interface__, + other.__geo_interface__, # type: ignore [attr-defined] ) def __len__(self) -> int: @@ -1068,7 +1062,7 @@ def _wkt_coords(self) -> str: def __geo_interface__(self) -> GeoCollectionInterface: # type: ignore [override] """Return the geo interface of the collection.""" return { - "type": self.geom_type, + "type": "GeometryCollection", "geometries": tuple(geom.__geo_interface__ for geom in self.geoms), } diff --git a/pygeoif/types.py b/pygeoif/types.py index d14a767c..fd86d0ad 100644 --- a/pygeoif/types.py +++ b/pygeoif/types.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2012 -2022 Christian Ledermann # @@ -23,6 +22,7 @@ from typing import Tuple from typing import Union +from typing_extensions import Literal from typing_extensions import Protocol from typing_extensions import TypedDict @@ -56,7 +56,7 @@ class GeoInterfaceBase(TypedDict): class GeoInterface(GeoInterfaceBase, total=False): - """Geointerfaces provides an optional bbox.""" + """GeoInterface provides an optional bbox.""" bbox: Bounds @@ -64,8 +64,8 @@ class GeoInterface(GeoInterfaceBase, total=False): class GeoCollectionInterface(TypedDict): """Geometry Collection Interface.""" - type: str - geometries: Sequence[GeoInterface] + type: Literal["GeometryCollection"] + geometries: Sequence[Union[GeoInterface, "GeoCollectionInterface"]] class GeoFeatureInterfaceBase(TypedDict): diff --git a/pyproject.toml b/pyproject.toml index 467593ca..7d93569f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ warn_return_any = true show_error_codes = true [[tool.mypy.overrides]] -module = "pygeoif.tests.*" +module = "tests.*" disallow_untyped_defs = false ignore_errors = true @@ -45,4 +45,7 @@ reportMissingImports = true reportMissingTypeStubs = true [tool.ruff] -line-length = 89 + +[tool.ruff.per-file-ignores] +"tests/*.py" = ["D103"] +"setup.py" = ["E501"] diff --git a/setup.py b/setup.py index 6f8f4493..6df7a579 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ def run_tests(self) -> None: sys.exit(errno) -version = "1.0.0" +version = "1.1.0" with open("README.rst", encoding="utf-8") as readme: README = readme.read() @@ -46,6 +46,7 @@ def run_tests(self) -> None: "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Intended Audience :: Developers", diff --git a/test-requirements.txt b/test-requirements.txt index 8e3b6220..ac52857f 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,15 +6,15 @@ bandit black #cohesion flake8 -flake8-absolute-import -flake8-awesome -flake8-bandit +#flake8-absolute-import +#flake8-awesome +#flake8-bandit flake8-blind-except -flake8-broken-line +# flake8-broken-line flake8-bugbear # flake8-class-attributes-order flake8-cognitive-complexity -flake8-commas +#flake8-commas flake8-comments flake8-complex-f-strings flake8-continuation @@ -58,6 +58,7 @@ pytest-randomly radon removestar ssort +typeforce yamlfixer-opt-nc yamllint yesqa diff --git a/pygeoif/tests/__init__.py b/tests/__init__.py similarity index 100% rename from pygeoif/tests/__init__.py rename to tests/__init__.py diff --git a/pygeoif/tests/test_base.py b/tests/test_base.py similarity index 96% rename from pygeoif/tests/test_base.py rename to tests/test_base.py index 48b04573..615a56e0 100644 --- a/pygeoif/tests/test_base.py +++ b/tests/test_base.py @@ -18,7 +18,6 @@ def test_bounds() -> None: """Subclasses must implement bounds.""" base_geo = geometry._Geometry() with pytest.raises(NotImplementedError, match="^Must be implemented by subclass$"): - assert base_geo.bounds @@ -26,14 +25,12 @@ def test_wkt() -> None: """Implement wkt in subclasses.""" base_geo = geometry._Geometry() with pytest.raises(NotImplementedError, match="^Must be implemented by subclass$"): - assert base_geo.wkt def test_empty() -> None: base_geo = geometry._Geometry() with pytest.raises(NotImplementedError, match="^Must be implemented by subclass$"): - assert base_geo.is_empty @@ -46,21 +43,18 @@ def test_wkt_inset() -> None: def test_wkt_coordinates() -> None: base_geo = geometry._Geometry() with pytest.raises(NotImplementedError, match="^Must be implemented by subclass$"): - assert base_geo._wkt_coords def test_from_dict() -> None: base_geo = geometry._Geometry() with pytest.raises(NotImplementedError, match="^Must be implemented by subclass$"): - - assert base_geo._from_dict({"type": "_Geometry"}) + assert base_geo._from_dict({"type": "_Geometry"}) # type: ignore def test_has_z() -> None: base_geo = geometry._Geometry() with pytest.raises(NotImplementedError, match="^Must be implemented by subclass$"): - assert base_geo.has_z @@ -71,12 +65,10 @@ def test_convex_hull() -> None: NotImplementedError, match="^Must be implemented by subclass$", ): - assert base_geo.convex_hull def test_get_bounds() -> None: base_geo = geometry._Geometry() with pytest.raises(NotImplementedError, match="^Must be implemented by subclass$"): - assert base_geo._get_bounds() diff --git a/pygeoif/tests/test_bounds.py b/tests/test_bounds.py similarity index 100% rename from pygeoif/tests/test_bounds.py rename to tests/test_bounds.py diff --git a/pygeoif/tests/test_factories.py b/tests/test_factories.py similarity index 93% rename from pygeoif/tests/test_factories.py rename to tests/test_factories.py index a763c989..5c714b95 100644 --- a/pygeoif/tests/test_factories.py +++ b/tests/test_factories.py @@ -8,17 +8,17 @@ def test_num_int() -> None: assert factories.num("1") == 1 - assert type(factories.num("1")) is int + assert isinstance(factories.num("1"), int) def test_num_intf() -> None: assert factories.num("1.0") == 1 - assert type(factories.num("1.0")) is int + assert isinstance(factories.num("1.0"), int) def test_num_float() -> None: assert factories.num("1.1") == 1.1 - assert type(factories.num("1.1")) is float + assert isinstance(factories.num("1.1"), float) def test_orient_true() -> None: @@ -87,7 +87,7 @@ def test_box_cw() -> None: def test_shell_holes_from_wkt_coords() -> None: shell, holes = factories._shell_holes_from_wkt_coords( [ - ["0 0", "10 20", "30 40", "0 0"], + ["0 0", "10 20", "30 40", "0 0"], # type: ignore ], ) assert holes is None @@ -95,7 +95,6 @@ def test_shell_holes_from_wkt_coords() -> None: class TestWKT: - # valid and supported WKTs wkt_ok = [ "POINT(6 10)", @@ -140,15 +139,17 @@ def test_linestring(self) -> None: "LINESTRING(-72.991 46.177,-73.079 46.16," "-73.146 46.124,-73.177 46.071,-73.164 46.044)", ) + + assert isinstance(line, geometry.LineString) assert ( line.wkt == "LINESTRING (-72.991 46.177, " "-73.079 46.16, -73.146 46.124, " "-73.177 46.071, -73.164 46.044)" ) - assert isinstance(line, geometry.LineString) def test_linearring(self) -> None: r = factories.from_wkt("LINEARRING (0 0,0 1,1 0,0 0)") + assert isinstance(r, geometry.LinearRing) assert r.wkt == "LINEARRING (0 0, 0 1, 1 0, 0 0)" @@ -160,12 +161,18 @@ def test_polygon(self) -> None: "-91.638 76.202,-91.647 76.211,-91.648 76.218," "-91.643 76.221,-91.636 76.222,-91.611 76.227))", ) + + assert isinstance(p, geometry.Polygon) assert p.exterior.coords[0][0] == -91.611 assert p.exterior.coords[0] == p.exterior.coords[-1] assert len(p.exterior.coords) == 14 + + def test_polygon_1(self) -> None: p = factories.from_wkt( "POLYGON((1 1,5 1,5 5,1 5,1 1),(2 2, 3 2, 3 3, 2 3,2 2))", ) + + assert isinstance(p, geometry.Polygon) assert p.exterior.coords[0] == p.exterior.coords[-1] assert p.exterior.coords[0] == (1.0, 1.0) assert len(list(p.interiors)) == 1 @@ -181,12 +188,20 @@ def test_polygon(self) -> None: "1 5, 1 1),(2 2, 3 2, " "3 3, 2 3, 2 2))" ) + + def test_polygon_2(self) -> None: p = factories.from_wkt("POLYGON ((30 10, 10 20, 20 40, 40 40, 30 10))") + + assert isinstance(p, geometry.Polygon) assert p.exterior.coords[0] == p.exterior.coords[-1] + + def test_polygon_3(self) -> None: p = factories.from_wkt( """POLYGON ((35 10, 10 20, 15 40, 45 45, 35 10), (20 30, 35 35, 30 20, 20 30))""", ) + + assert isinstance(p, geometry.Polygon) assert p.exterior.coords[0] == p.exterior.coords[-1] def test_multipoint(self) -> None: @@ -208,6 +223,8 @@ def test_multilinestring(self) -> None: p = factories.from_wkt( "MULTILINESTRING((3 4,10 50,20 25),(-5 -8,-10 -8,-15 -4))", ) + + assert isinstance(p, geometry.MultiLineString) assert list(p.geoms)[0].coords == (((3, 4), (10, 50), (20, 25))) assert list(p.geoms)[1].coords == (((-5, -8), (-10, -8), (-15, -4))) assert ( @@ -215,10 +232,14 @@ def test_multilinestring(self) -> None: "20 25),(-5 -8, " "-10 -8, -15 -4))" ) + + def test_multilinestring_1(self) -> None: p = factories.from_wkt( """MULTILINESTRING ((10 10, 20 20, 10 40), (40 40, 30 30, 40 20, 30 10))""", ) + + assert isinstance(p, geometry.MultiLineString) assert p.wkt == ( "MULTILINESTRING((10 10, 20 20, 10 40),(40 40, 30 30, 40 20, 30 10))" ) @@ -229,6 +250,8 @@ def test_multipolygon(self) -> None: "(1 1,2 2,3 3,1 1))," "((100 100,110 110,120 120,100 100)))", ) + + assert isinstance(p, geometry.MultiPolygon) # two polygons: the first one has an interior ring assert len(list(p.geoms)) == 2 assert list(p.geoms)[0].exterior.coords == ( @@ -256,15 +279,23 @@ def test_multipolygon(self) -> None: "((100 100, 110 110," " 120 120, 100 100)))" ) + + def test_multipolygon_1(self) -> None: p = factories.from_wkt( "MULTIPOLYGON(((1 1,5 1,5 5,1 5,1 1)," "(2 2, 3 2, 3 3, 2 3,2 2)),((3 3,6 2,6 4,3 3)))", ) + + assert isinstance(p, geometry.MultiPolygon) assert len(list(p.geoms)) == 2 + + def test_multipolygon_2(self) -> None: p = factories.from_wkt( "MULTIPOLYGON (((30 20, 10 40, 45 40, 30 20))," "((15 5, 40 10, 10 20, 5 10, 15 5)))", ) + + assert isinstance(p, geometry.MultiPolygon) assert p.__geo_interface__ == { "type": "MultiPolygon", "bbox": (5.0, 5.0, 45.0, 40.0), @@ -286,6 +317,8 @@ def test_geometrycollection(self) -> None: gc = factories.from_wkt( "GEOMETRYCOLLECTION(POINT(4 6), LINESTRING(4 6,7 10))", ) + + assert isinstance(gc, geometry.GeometryCollection) assert len(list(gc.geoms)) == 2 assert isinstance(list(gc.geoms)[0], geometry.Point) assert isinstance(list(gc.geoms)[1], geometry.LineString) diff --git a/pygeoif/tests/test_feature.py b/tests/test_feature.py similarity index 100% rename from pygeoif/tests/test_feature.py rename to tests/test_feature.py diff --git a/pygeoif/tests/test_functions.py b/tests/test_functions.py similarity index 80% rename from pygeoif/tests/test_functions.py rename to tests/test_functions.py index 8abb2c20..1872d64d 100644 --- a/pygeoif/tests/test_functions.py +++ b/tests/test_functions.py @@ -8,6 +8,7 @@ from pygeoif.functions import centroid from pygeoif.functions import compare_coordinates +from pygeoif.functions import compare_geo_interface from pygeoif.functions import convex_hull from pygeoif.functions import dedupe from pygeoif.functions import signed_area @@ -260,12 +261,10 @@ def test_random() -> None: def test_dedupe_point() -> None: - assert dedupe(((1, 2, 3),) * 10) == ((1, 2, 3),) def test_dedupe_line() -> None: - assert dedupe(((1, 2, 3), (4, 5, 6)) * 3) == ( (1, 2, 3), (4, 5, 6), @@ -378,3 +377,78 @@ def test_compare_lines(lines, expected: bool) -> None: def test_compare_polygons(polygons, expected: bool) -> None: """Compare nested sequences of coordinates.""" assert compare_coordinates(*polygons) is expected + + +def test_compare_eq_geo_interface() -> None: + geo_if = { + "geometries": ( + { + "geometries": ( + { + "geometries": ( + { + "bbox": (0, 0, 0, 0), + "coordinates": (0, 0), + "type": "Point", + }, + { + "bbox": (0, 0, 2, 2), + "coordinates": ((0, 0), (1, 1), (1, 2), (2, 2)), + "type": "MultiPoint", + }, + ), + "type": "GeometryCollection", + }, + { + "bbox": (0, 0, 3, 1), + "coordinates": ((0, 0), (3, 1)), + "type": "LineString", + }, + ), + "type": "GeometryCollection", + }, + {"coordinates": (((0, 0), (1, 1), (1, 0), (0, 0)),), "type": "Polygon"}, + { + "bbox": (0, 0, 2, 2), + "coordinates": ( + ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)), + ((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)), + ), + "type": "Polygon", + }, + {"coordinates": (0, 0), "type": "Point"}, + {"bbox": (-1, -1, -1, -1), "coordinates": (-1, -1), "type": "Point"}, + {"coordinates": ((0, 0), (1, 1), (1, 0), (0, 0)), "type": "LinearRing"}, + { + "bbox": (0, 0, 1, 1), + "coordinates": ((0, 0), (1, 1)), + "type": "LineString", + }, + ), + "type": "GeometryCollection", + } + + assert compare_geo_interface(geo_if, geo_if) is True + + +def test_compare_neq_geo_interface() -> None: + geo_if1 = { + "type": "Point", + "bbox": (0, 1, 0, 1), + "coordinates": (0.0, 1.0, 2.0), + } + geo_if2 = { + "coordinates": (0.0, 1.0, 3.0), + } + + assert compare_geo_interface(geo_if1, geo_if2) is False + + +def test_compare_neq_empty_geo_interface() -> None: + geo_if = { + "type": "Point", + "bbox": (0, 1, 0, 1), + "coordinates": (0.0, 1.0, 2.0), + } + + assert compare_geo_interface(geo_if, {}) is False diff --git a/pygeoif/tests/test_geometrycollection.py b/tests/test_geometrycollection.py similarity index 59% rename from pygeoif/tests/test_geometrycollection.py rename to tests/test_geometrycollection.py index e383a0bf..3064492d 100644 --- a/pygeoif/tests/test_geometrycollection.py +++ b/tests/test_geometrycollection.py @@ -1,4 +1,5 @@ """Test Baseclass.""" + from pygeoif import geometry @@ -264,3 +265,163 @@ def test_empty_bounds() -> None: gc = geometry.GeometryCollection([]) assert gc.bounds == () + + +def test_multipoint_wkt() -> None: + multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) + gc = geometry.GeometryCollection([multipoint]) + + assert gc.wkt == "GEOMETRYCOLLECTION(MULTIPOINT(0 0, 1 1, 1 2, 2 2))" + + +def test_multipoint_repr() -> None: + multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) + gc = geometry.GeometryCollection([multipoint]) + + assert ( + repr(gc) + == "GeometryCollection((MultiPoint(((0, 0), (1, 1), (1, 2), (2, 2))),))" + ) + + +def test_multipoint_geo_interface() -> None: + multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) + gc = geometry.GeometryCollection([multipoint]) + + assert gc.__geo_interface__ == { + "type": "GeometryCollection", + "geometries": ( + { + "type": "MultiPoint", + "coordinates": ((0, 0), (1, 1), (1, 2), (2, 2)), + "bbox": (0, 0, 2, 2), + }, + ), + } + + +def test_nested_geometry_collection() -> 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 gc3.wkt == ( + "GEOMETRYCOLLECTION(GEOMETRYCOLLECTION(GEOMETRYCOLLECTION(" + "POINT (0 0), MULTIPOINT(0 0, 1 1, 1 2, 2 2)), LINESTRING (0 0, 3 1)), " + "POLYGON ((0 0, 1 1, 1 0, 0 0)))" + ) + + +def test_nested_geometry_collection_geo_interface() -> 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 gc3.__geo_interface__ == { + "geometries": ( + { + "geometries": ( + { + "geometries": ( + { + "bbox": (0, 0, 0, 0), + "coordinates": (0, 0), + "type": "Point", + }, + { + "bbox": (0, 0, 2, 2), + "coordinates": ((0, 0), (1, 1), (1, 2), (2, 2)), + "type": "MultiPoint", + }, + ), + "type": "GeometryCollection", + }, + { + "bbox": (0, 0, 3, 1), + "coordinates": ((0, 0), (3, 1)), + "type": "LineString", + }, + ), + "type": "GeometryCollection", + }, + { + "bbox": (0, 0, 1, 1), + "coordinates": (((0, 0), (1, 1), (1, 0), (0, 0)),), + "type": "Polygon", + }, + ), + "type": "GeometryCollection", + } + + +def test_nested_geometry_collection_eq() -> 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]) + gc4 = geometry.GeometryCollection([gc2, poly1]) + + assert gc3 == gc4 + + +def test_nested_geometry_collection_neq() -> None: + multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) + gc1 = geometry.GeometryCollection([geometry.Point(0, 0), multipoint]) + gc1_1 = geometry.GeometryCollection( + [geometry.Point(0, 0), multipoint, geometry.Point(0, 0)] + ) + line = geometry.LineString([(0, 0), (3, 1)]) + gc2 = geometry.GeometryCollection([gc1, line]) + gc2_1 = geometry.GeometryCollection([gc1_1, line]) + poly1 = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) + gc3 = geometry.GeometryCollection([gc2, poly1]) + gc4 = geometry.GeometryCollection([gc2_1, poly1]) + + assert gc3 != gc4 + + +def test_geometry_collection_neq_when_empty() -> None: + gc1 = geometry.GeometryCollection([]) + gc2 = geometry.GeometryCollection([geometry.Point(0, 0)]) + + assert gc1 != gc2 + assert gc2 != gc1 + + +def test_nested_geometry_collection_repr_eval() -> 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 ( + eval( + repr(gc), + {}, + { + "LinearRing": geometry.LinearRing, + "Polygon": geometry.Polygon, + "Point": geometry.Point, + "LineString": geometry.LineString, + "GeometryCollection": geometry.GeometryCollection, + "MultiPoint": geometry.MultiPoint, + }, + ).__geo_interface__ + == gc.__geo_interface__ + ) diff --git a/pygeoif/tests/test_line.py b/tests/test_line.py similarity index 99% rename from pygeoif/tests/test_line.py rename to tests/test_line.py index 089c9768..b0b4df8a 100644 --- a/pygeoif/tests/test_line.py +++ b/tests/test_line.py @@ -82,7 +82,10 @@ def test_from_compatible() -> None: "coordinates": ((0.0, 0.0, 1.0), (1.0, 1.0, 2.0)), }, ) + line = geometry.LineString._from_interface(not_a_geometry) + + assert isinstance(line, geometry.LineString) assert line.coords == ((0.0, 0.0, 1.0), (1.0, 1.0, 2.0)) diff --git a/pygeoif/tests/test_linear_ring.py b/tests/test_linear_ring.py similarity index 99% rename from pygeoif/tests/test_linear_ring.py rename to tests/test_linear_ring.py index 5fe4cbe9..529d705c 100644 --- a/pygeoif/tests/test_linear_ring.py +++ b/tests/test_linear_ring.py @@ -86,6 +86,7 @@ def test_from_compatible() -> None: ring = geometry.LinearRing._from_interface(not_a_geometry) + assert isinstance(ring, geometry.LinearRing) assert ring.coords == ((0.0, 0.0, 1.0), (1.0, 1.0, 2.0), (0, 4, 3), (0, 0, 1)) diff --git a/pygeoif/tests/test_multiline.py b/tests/test_multiline.py similarity index 100% rename from pygeoif/tests/test_multiline.py rename to tests/test_multiline.py diff --git a/pygeoif/tests/test_multipoint.py b/tests/test_multipoint.py similarity index 100% rename from pygeoif/tests/test_multipoint.py rename to tests/test_multipoint.py diff --git a/pygeoif/tests/test_multipolygon.py b/tests/test_multipolygon.py similarity index 99% rename from pygeoif/tests/test_multipolygon.py rename to tests/test_multipolygon.py index b58856c8..d4bb9f1e 100644 --- a/pygeoif/tests/test_multipolygon.py +++ b/tests/test_multipolygon.py @@ -275,7 +275,6 @@ def test_empty_wkt() -> None: def test_repr_empty() -> None: - polys = geometry.MultiPolygon([]) assert repr(polys) == "MultiPolygon(())" diff --git a/pygeoif/tests/test_point.py b/tests/test_point.py similarity index 93% rename from pygeoif/tests/test_point.py rename to tests/test_point.py index 91db7902..96e9a709 100644 --- a/pygeoif/tests/test_point.py +++ b/tests/test_point.py @@ -5,6 +5,7 @@ import pytest from pygeoif import geometry +from pygeoif.exceptions import DimensionError def test_empty() -> None: @@ -52,11 +53,19 @@ def test_bounds3d() -> None: def test_xy() -> None: point = geometry.Point(1.0, 0.0) - assert point.z is None assert point.x == 1 assert point.y == 0 +def test_xy_raises_error_accessing_z() -> None: + point = geometry.Point(1, 0) + + with pytest.raises( + DimensionError, match=r"^The Point\(1, 0\) geometry does not have z values$" + ): + point.z + + def test_xyz() -> None: point = geometry.Point(1.0, 0.0, 2.0) @@ -161,6 +170,7 @@ def test_from_compatible() -> None: point = geometry.Point._from_interface(not_a_geometry) + assert isinstance(point, geometry.Point) assert point.coords == ((0, 1, 2),) @@ -183,7 +193,6 @@ def test_eq_floats() -> None: def test_neq_missing_interface() -> None: - point = geometry.Point(0, 1, 2) assert point != object() diff --git a/pygeoif/tests/test_polygon.py b/tests/test_polygon.py similarity index 100% rename from pygeoif/tests/test_polygon.py rename to tests/test_polygon.py diff --git a/tox.ini b/tox.ini index 2d73661c..74ef1378 100644 --- a/tox.ini +++ b/tox.ini @@ -1,40 +1,16 @@ -[tox] -envlist = - py26, - py27, - py32, - py33, - py34, - pypy, - pypy3, - pep8, - -[testenv] -deps = - pytest - coverage -commands = - coverage run -a --source=pygeoif setup.py test - -[testenv:pep8] -deps = - pep8 -commands = - pep8 --exclude test_main.py pygeoif - [flake8] -min_python_version = 3.6.10 +min_python_version = 3.7 exclude = .git,__pycache__,docs/source/conf.py,old,build,dist max_line_length = 89 ignore= W503,I900,IF100 per-file-ignores = - pygeoif/tests/*:S101,D103,S307,DALL000,S311,ECE001,FKA100 - pygeoif/tests/test_geometrycollection.py: ECE001,S101,D103,S307,DALL000 - pygeoif/tests/test_factories.py: ECE001,S10,D10,S307,DALL000,PT009,T003 - pygeoif/tests/test_feature.py: ECE001,S10,D10,S307,DALL000,PT009,T003,P103 - pygeoif/feature.py: A003 - pygeoif/types.py: A003 + tests/*:S101,D103,S307,DALL000,S311,ECE001,FKA100 + tests/test_geometrycollection.py: ECE001,S101,D103,S307,DALL000 + tests/test_factories.py: ECE001,S10,D10,S307,DALL000,PT009,T003 + tests/test_feature.py: ECE001,S10,D10,S307,DALL000,PT009,T003,P103 + feature.py: A003 + types.py: A003 kwargs_ignore_function_pattern_extend = '^cast$' literal_inline_quotes = double literal_multiline_quotes = double