From 8c01dedf76b639b7a0582c4c3dd641e2ac45ba19 Mon Sep 17 00:00:00 2001 From: Christoph Fink Date: Fri, 16 Jan 2026 12:35:38 +0100 Subject: [PATCH 1/7] fix geopandas inheritance --- CHANGELOG.md | 5 +++++ pyproject.toml | 2 +- src/cartogram/__init__.py | 2 +- src/cartogram/cartogram.py | 24 ++++++++++++++++++++++++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 682d780..7064455 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +- **1.0.2** (2026-01-16): + - BREAKING: require cartogram_attribute to be a positional argument + - 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..1f41513 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ authors = [ ] dependencies = [ - "geopandas<1.1.0", + "geopandas", "joblib", "numpy", "pandas", 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..c490ea3 100644 --- a/src/cartogram/cartogram.py +++ b/src/cartogram/cartogram.py @@ -34,9 +34,33 @@ class Cartogram(geopandas.GeoDataFrame): """Compute continuous cartograms.""" + _constructor = geopandas.GeoDataFrame + + _constructor_sliced = pandas.Series + + @classmethod + def _geodataframe_constructor_with_fallback( + cls, *args, **kwargs + ): + """ + A flexible constructor for Cartogram. + + It which checks whether or not arguments of the child class are used. + """ + if "cartogram_attribute" in kwargs.keys(): + 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 + def __init__( self, input_polygon_geodataframe, + /, cartogram_attribute, max_iterations=10, max_average_error=0.1, From 8c5fb05eb8b602f76ba2bfec79c02b6a38da2255 Mon Sep 17 00:00:00 2001 From: Christoph Fink Date: Fri, 16 Jan 2026 12:38:50 +0100 Subject: [PATCH 2/7] typo --- src/cartogram/cartogram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cartogram/cartogram.py b/src/cartogram/cartogram.py index c490ea3..6efc746 100644 --- a/src/cartogram/cartogram.py +++ b/src/cartogram/cartogram.py @@ -45,7 +45,7 @@ def _geodataframe_constructor_with_fallback( """ A flexible constructor for Cartogram. - It which checks whether or not arguments of the child class are used. + Checks whether or not arguments of the child class are used. """ if "cartogram_attribute" in kwargs.keys(): df = cls(*args, **kwargs) From 2d0e673be5a9056c6f098ad6e62f2d8fc84cafd4 Mon Sep 17 00:00:00 2001 From: Christoph Fink Date: Fri, 16 Jan 2026 12:56:18 +0100 Subject: [PATCH 3/7] linted --- pyproject.toml | 66 +++++++++++++++++++++++++++++++++++--- src/cartogram/cartogram.py | 28 +++++++++------- tests/conftest.py | 1 - 3 files changed, 78 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1f41513..cae2309 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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/cartogram.py b/src/cartogram/cartogram.py index 6efc746..9b4a5ab 100644 --- a/src/cartogram/cartogram.py +++ b/src/cartogram/cartogram.py @@ -13,7 +13,6 @@ import pandas import shapely - __all__ = ["Cartogram"] @@ -39,15 +38,13 @@ class Cartogram(geopandas.GeoDataFrame): _constructor_sliced = pandas.Series @classmethod - def _geodataframe_constructor_with_fallback( - cls, *args, **kwargs - ): + def _geodataframe_constructor_with_fallback(cls, *args, **kwargs): """ - A flexible constructor for Cartogram. + Provide a flexible constructor for Cartogram. Checks whether or not arguments of the child class are used. """ - if "cartogram_attribute" in kwargs.keys(): + if "cartogram_attribute" in kwargs: df = cls(*args, **kwargs) else: df = geopandas.GeoDataFrame(*args, **kwargs) @@ -131,7 +128,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 @@ -179,7 +177,7 @@ 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: @@ -187,7 +185,9 @@ def _invalidate_cached_properties(self, properties=[]): # properties = [ # attribute # for attribute in self.__dict__.keys() - # if isinstance(getattr(self, attribute, None), functools.cached_property) + # if isinstance( + # getattr(self, attribute, None), + # functools.cached_property) # ] properties = [ attr @@ -215,7 +215,12 @@ 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)) + # 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, @@ -234,7 +239,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) 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 = ( From 1e547ae43834a6613ed67457651ce03bfb02876f Mon Sep 17 00:00:00 2001 From: Christoph Fink Date: Fri, 16 Jan 2026 12:57:36 +0100 Subject: [PATCH 4/7] Include Python 3.14 --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) 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' From c896c7b857c58d31f1a552b35ee0a7c57601586a Mon Sep 17 00:00:00 2001 From: Christoph Fink Date: Fri, 16 Jan 2026 14:56:05 +0100 Subject: [PATCH 5/7] properly inherit from geo data frames --- src/cartogram/cartogram.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/cartogram/cartogram.py b/src/cartogram/cartogram.py index 9b4a5ab..a828c2c 100644 --- a/src/cartogram/cartogram.py +++ b/src/cartogram/cartogram.py @@ -44,7 +44,10 @@ def _geodataframe_constructor_with_fallback(cls, *args, **kwargs): Checks whether or not arguments of the child class are used. """ - if "cartogram_attribute" in kwargs: + if ( + "cartogram_attribute" in kwargs + or isinstance(args[0], (str, pandas.Series)) + ): df = cls(*args, **kwargs) else: df = geopandas.GeoDataFrame(*args, **kwargs) @@ -54,10 +57,23 @@ def _geodataframe_constructor_with_fallback(cls, *args, **kwargs): 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, - /, cartogram_attribute, max_iterations=10, max_average_error=0.1, From c10565dad0c8af868d4cf77723acc72ce0794798 Mon Sep 17 00:00:00 2001 From: Christoph Fink Date: Fri, 16 Jan 2026 14:57:15 +0100 Subject: [PATCH 6/7] linted --- src/cartogram/cartogram.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/cartogram/cartogram.py b/src/cartogram/cartogram.py index a828c2c..1279bb0 100644 --- a/src/cartogram/cartogram.py +++ b/src/cartogram/cartogram.py @@ -44,10 +44,7 @@ def _geodataframe_constructor_with_fallback(cls, *args, **kwargs): Checks whether or not arguments of the child class are used. """ - if ( - "cartogram_attribute" in kwargs - or isinstance(args[0], (str, pandas.Series)) - ): + if "cartogram_attribute" in kwargs or isinstance(args[0], (str, pandas.Series)): df = cls(*args, **kwargs) else: df = geopandas.GeoDataFrame(*args, **kwargs) From d568bf71b4fba4fb0a9ec393a1189f780db33a7b Mon Sep 17 00:00:00 2001 From: Christoph Fink Date: Fri, 16 Jan 2026 15:03:15 +0100 Subject: [PATCH 7/7] clean-up --- CHANGELOG.md | 1 - src/cartogram/cartogram.py | 16 ---------------- 2 files changed, 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7064455..890726d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,4 @@ - **1.0.2** (2026-01-16): - - BREAKING: require cartogram_attribute to be a positional argument - fix inheritance from geopandas.GeoDataFrame - update geopandas dependency diff --git a/src/cartogram/cartogram.py b/src/cartogram/cartogram.py index 1279bb0..1701ca8 100644 --- a/src/cartogram/cartogram.py +++ b/src/cartogram/cartogram.py @@ -194,14 +194,6 @@ 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()) @@ -228,12 +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, @@ -291,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):