diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 86454f3..aa5f1f6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,6 +29,7 @@ jobs: - windows-latest - macos-latest python_version: + - '3.14' - '3.13' - '3.12' - '3.11' diff --git a/CHANGELOG.md b/CHANGELOG.md index 682d780..890726d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +- **1.0.2** (2026-01-16): + - fix inheritance from geopandas.GeoDataFrame + - update geopandas dependency + - **1.0.1** (2025-06-27): - workaround for change in geopandas inheritance diff --git a/pyproject.toml b/pyproject.toml index a2efa25..cae2309 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ authors = [ ] dependencies = [ - "geopandas<1.1.0", + "geopandas", "joblib", "numpy", "pandas", @@ -31,11 +31,36 @@ license = {text = "GPL-3.0-or-later"} dynamic = ["version"] [project.optional-dependencies] -docs = ["folium", "GitPython", "jupyterlab_myst", "mapclassify", "matplotlib", - "myst-nb", "nbsphinx", "pybtex-apa7-style", "sphinx", - "sphinx-book-theme", "sphinx-design", "sphinxcontrib-bibtex", - "sphinxcontrib-images", "xyzservices"] -tests = ["pytest", "pytest-cov", "pytest-lazy-fixtures"] +dev = [ + "black", + "flake8", + "flake8-bugbear", + "flake8-pyproject", + "isort", + "pydocstyle", + "pylint", +] +docs = [ + "folium", + "GitPython", + "jupyterlab_myst", + "mapclassify", + "matplotlib", + "myst-nb", + "nbsphinx", + "pybtex-apa7-style", + "sphinx", + "sphinx-book-theme", + "sphinx-design", + "sphinxcontrib-bibtex", + "sphinxcontrib-images", + "xyzservices", +] +tests = [ + "pytest", + "pytest-cov", + "pytest-lazy-fixtures", +] [project.urls] Documentation = "https://python-cartogram.readthedocs.org/" @@ -43,9 +68,40 @@ Repository = "https://github.com/austromorph/python-cartogram" "Change log" = "https://github.com/austromorph/python-cartogram/blob/main/CHANGELOG.md" "Bug tracker" = "https://github.com/austromorph/python-cartogram/issues" +[tool.coverage.paths] +equivalent_sources = [ + "src/cartogram/", + ".virtualenv/lib/python*/site-packages/cartogram/", + "/opt/hostedtoolcache/Python/*/x64/lib/python*/site-packages/cartogram/", + "/Library/Frameworks/Python.framework/Versions/*/lib/python*/site-packages/cartogram/", + "C:/hostedtoolcache/windows/Python/*/x64/Lib/site-packages/cartogram/" +] + +[tool.coverage.report] +exclude_also = [ + 'if __name__ == .__main__.:', +] + [tool.coverage.run] omit = ["tests/*", ".virtualenv/**/*"] +[tool.flake8] +exclude = ["build", "dist", "docs/conf.py", ".virtualenv"] +extent-ignore = ["E203", "E501", "E701"] +extent-select = ["B950"] +max-line-length = 88 + +[tool.isort] +profile = "black" +skip = ["build", "dist", "docs/conf.py", ".virtualenv"] + +[tool.pydocstyle] +#match-dir = "^(src|tests|build_backend).*" + +[tool.pylintrc] +good-names = ["i", "j", "k", "e", "ex", "id", "Run", "_"] +max-line-length = 88 + [tool.pytest.ini_options] addopts = "--cov=cartogram --cov-report term-missing --cov-report xml" pythonpath = ["src"] diff --git a/src/cartogram/__init__.py b/src/cartogram/__init__.py index 04d1d6f..b2e4a8f 100644 --- a/src/cartogram/__init__.py +++ b/src/cartogram/__init__.py @@ -2,7 +2,7 @@ """Compute continuous cartograms.""" -__version__ = "1.0.1" +__version__ = "1.0.2" from .cartogram import Cartogram diff --git a/src/cartogram/cartogram.py b/src/cartogram/cartogram.py index 860cbaf..1701ca8 100644 --- a/src/cartogram/cartogram.py +++ b/src/cartogram/cartogram.py @@ -13,7 +13,6 @@ import pandas import shapely - __all__ = ["Cartogram"] @@ -34,6 +33,41 @@ class Cartogram(geopandas.GeoDataFrame): """Compute continuous cartograms.""" + _constructor = geopandas.GeoDataFrame + + _constructor_sliced = pandas.Series + + @classmethod + def _geodataframe_constructor_with_fallback(cls, *args, **kwargs): + """ + Provide a flexible constructor for Cartogram. + + Checks whether or not arguments of the child class are used. + """ + if "cartogram_attribute" in kwargs or isinstance(args[0], (str, pandas.Series)): + df = cls(*args, **kwargs) + else: + df = geopandas.GeoDataFrame(*args, **kwargs) + geometry_cols_mask = df.dtypes == "geometry" + if len(geometry_cols_mask) == 0 or geometry_cols_mask.sum() == 0: + df = pandas.DataFrame(df) + + return df + + _cartogram_attributes = [ + "cartogram_attribute", + "max_iterations", + "max_average_error", + "verbose", + ] + + def __setattr__(self, attr, val): + """Catch our own attributes here so we don’t mess with (geo)pandas columns.""" + if attr in self._cartogram_attributes: + object.__setattr__(self, attr, val) + else: + super().__setattr__(attr, val) + def __init__( self, input_polygon_geodataframe, @@ -107,7 +141,8 @@ def _check_geodata(self): for geometry_type in geometry_types: if geometry_type not in ["MultiPolygon", "Polygon"]: raise ValueError( - f"Only POLYGON or MULTIPOLYGON geometries supported, found {geometry_type}." + "Only POLYGON or MULTIPOLYGON geometries supported, " + f"found {geometry_type}." ) self._input_is_multipolygon = "MultiPolygon" in geometry_types @@ -155,16 +190,10 @@ def _feature_error(self, feature): return error - def _invalidate_cached_properties(self, properties=[]): + def _invalidate_cached_properties(self, properties=None): """Invalidate properties that were cached as `functools.cached_property`.""" # https://stackoverflow.com/a/68316608 if not properties: - # # clear all as default - # properties = [ - # attribute - # for attribute in self.__dict__.keys() - # if isinstance(getattr(self, attribute, None), functools.cached_property) - # ] properties = [ attr for attr in list(self.__dict__.keys()) @@ -191,7 +220,6 @@ def _transform(self): self.iteration < self.max_iterations and self.average_error > self.max_average_error ): - # self.geometry = self.geometry.apply(functools.partial(self._transform_geometry, features=self._cartogram_features)) with joblib.Parallel( verbose=(self.verbose * 10), n_jobs=NUM_THREADS, @@ -210,7 +238,8 @@ def _transform(self): self.iteration += 1 if self.verbose: print( - f"{self.average_error:0.5f} error left after {self.iteration:d} iteration(s)" + f"{self.average_error:0.5f} error left " + f"after {self.iteration:d} iteration(s)" ) self.geometry = self.geometry.buffer(0.0) @@ -248,8 +277,6 @@ def _transform_vertex(self, vertex, features, reduction_factor): x += (x0 - cx) * force y += (y0 - cy) * force - - # print(f" moved vertex by {x0-x}, {y0-y}") return [x, y] def _transform_vertices(self, vertices, features, reduction_factor): diff --git a/tests/conftest.py b/tests/conftest.py index 1ab01b3..e05668c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,6 @@ import geopandas import pytest - DATA_DIRECTORY = pathlib.Path(__file__).resolve().parent / "data" AUSTRIA_NUTS2_POPULATION = DATA_DIRECTORY / "Austria_PopulationByNUTS2.geojson" AUSTRIA_NUTS2_POPULATION_CARTOGRAM = (