[Git][debian-gis-team/python-shapely][experimental] 11 commits: Revert "Update branch in gbp.conf & Vcs-Git URL."
Bas Couwenberg (@sebastic)
gitlab at salsa.debian.org
Mon May 19 14:56:55 BST 2025
Bas Couwenberg pushed to branch experimental at Debian GIS Project / python-shapely
Commits:
b36b7b10 by Bas Couwenberg at 2025-04-03T14:58:41+02:00
Revert "Update branch in gbp.conf & Vcs-Git URL."
This reverts commit 238c827100f0386a4c90150be2eaf6572bf8c72b.
- - - - -
e70273be by Bas Couwenberg at 2025-04-03T14:59:11+02:00
New upstream version 2.1.0
- - - - -
f46818a2 by Bas Couwenberg at 2025-04-03T14:59:12+02:00
Update upstream source from tag 'upstream/2.1.0'
Update to upstream version '2.1.0'
with Debian dir 76c007f2a3afd2ee9fb22712628be20cbab536a3
- - - - -
3fa17b70 by Bas Couwenberg at 2025-04-03T14:59:26+02:00
New upstream release.
- - - - -
6019024a by Bas Couwenberg at 2025-04-03T15:05:45+02:00
Update lintian overrides.
- - - - -
e4401f01 by Bas Couwenberg at 2025-04-03T15:05:53+02:00
Set distribution to unstable.
- - - - -
34c4f02f by Bas Couwenberg at 2025-05-19T15:50:50+02:00
Update branch in gbp.conf & Vcs-Git URL.
- - - - -
b561a23a by Bas Couwenberg at 2025-05-19T15:52:22+02:00
New upstream version 2.1.1
- - - - -
993b7c04 by Bas Couwenberg at 2025-05-19T15:52:24+02:00
Update upstream source from tag 'upstream/2.1.1'
Update to upstream version '2.1.1'
with Debian dir e1e2e7188ea174e8d68c4d1d8a5e914be7e71100
- - - - -
c75a4fe1 by Bas Couwenberg at 2025-05-19T15:52:44+02:00
New upstream release.
- - - - -
f5458efe by Bas Couwenberg at 2025-05-19T15:53:46+02:00
Set distribution to experimental.
- - - - -
21 changed files:
- .github/workflows/release.yml
- .github/workflows/tests.yml
- CHANGES.txt
- CITATION.cff
- debian/changelog
- debian/lintian-overrides
- + docs/code/coverage_simplify.py
- docs/coverage.rst
- docs/environment.yml
- docs/geometry.rst
- docs/manual.rst
- docs/migration.rst
- docs/release/2.x.rst
- shapely/_geometry_helpers.pyx
- shapely/_version.py
- shapely/decorators.py
- shapely/io.py
- shapely/tests/legacy/test_polylabel.py
- shapely/tests/test_constructive.py
- + shapely/tests/test_decorators.py
- shapely/tests/test_ragged_array.py
Changes:
=====================================
.github/workflows/release.yml
=====================================
@@ -113,7 +113,7 @@ jobs:
shell: bash
- name: Build wheels
- uses: pypa/cibuildwheel at v2.23.0
+ uses: pypa/cibuildwheel at v2.23.3
env:
CIBW_ARCHS: ${{ matrix.arch }}
# TEMP don't use automated/isolated build environment, but manually
@@ -186,7 +186,7 @@ jobs:
if: ${{ matrix.msvc_arch }}
- name: Build wheels
- uses: pypa/cibuildwheel at v2.23.0
+ uses: pypa/cibuildwheel at v2.23.3
env:
CIBW_ARCHS: ${{ matrix.arch }}
CIBW_ENVIRONMENT_MACOS:
@@ -236,7 +236,7 @@ jobs:
merge-multiple: true
path: dist
- name: Upload wheels to Anaconda Cloud
- uses: scientific-python/upload-nightly-action at 82396a2ed4269ba06c6b2988bb4fd568ef3c3d6b # 0.6.1
+ uses: scientific-python/upload-nightly-action at b36e8c0c10dbcfd2e05bf95f17ef8c14fd708dbf # 0.6.2
with:
artifacts_path: dist
anaconda_nightly_upload_token: ${{secrets.ANACONDA_ORG_UPLOAD_TOKEN}}
=====================================
.github/workflows/tests.yml
=====================================
@@ -32,13 +32,13 @@ jobs:
- python: "3.12"
geos: 3.12.3
numpy: 1.26.4
- matplotlib: true
- doctest: true
- extra_pytest_args: "-W error" # error on warnings
# 2024
- python: "3.13"
geos: 3.13.1
numpy: 2.1.3
+ matplotlib: true
+ doctest: true
+ extra_pytest_args: "-W error" # error on warnings
# free threaded Python (no numpy version to indicate installing nightly cython and numpy)
- os: ubuntu-22.04
python: "3.13t"
@@ -187,10 +187,10 @@ jobs:
pytest shapely/tests -r a --cov --cov-report term-missing ${{ matrix.extra_pytest_args }}
# Only run doctests on 1 runner (because of typographic differences in doctest results)
- - name: Run doctests on manual
+ - name: Run doctests on manual docs
if: ${{ matrix.os == 'ubuntu-latest' && matrix.doctest }}
run: |
- python -m pytest --doctest-modules docs/manual.rst
+ python -m pytest --doctest-modules docs/*.rst
- name: Run doctests on shapely module
if: ${{ matrix.os == 'ubuntu-latest' && matrix.doctest }}
=====================================
CHANGES.txt
=====================================
@@ -1,88 +1,31 @@
Changes
=======
-2.1.0 (unreleased)
+2.1.1 (2025-05-19)
------------------
-API changes:
-
-- Equality of geometries (``geom1 == geom2``) now considers NaN coordinate
- values in the same location to be equal (#1775). It is recommended however to
- ensure geometries don't have NaN values in the first place, for which you can
- now use the ``handle_nan`` parameter in construction functions.
-
Bug fixes:
-- Prevent crash when serializing a number > 1e100 to WKT with GEOS < 3.13. (#1907)
-- Ensure ``plot_polygon`` does not color the interiors of polygons (#1933).
-- Fixes GeoJSON serialization of empty points (#2118)
-- Fixes `__geo_interface__` handling of empty points (#2120)
-- Fixes ``GeometryCollection()`` constructor accepting an array of geometries (#2017).
+- Fix performance degradation calling shapely functions (caused by deprecation
+ of certain positional arguments) (#2283).
+- Fix crash caused by `from_ragged_array()` (#2291).
+- Fix compilation error building with recent LLVM toolchain (#2293).
-Improvements:
-- Add a ``handle_nan`` parameter to ``shapely.points()``,
- ``shapely.linestrings()`` and ``shapely.linearrings()`` to allow, skip, or
- error on nonfinite (NaN / Inf) coordinates. The default behaviour (allow) is
- backwards compatible (#1594, #1811).
-- Add an ``interleaved`` parameter to ``shapely.transform()`` allowing a transposed call
- signature in the ``transformation`` function.
-- The ``include_z`` in ``shapely.transform()`` now also allows ``None``, which
- lets it automatically detect the dimensionality of each input geometry.
-- Add an ``include_m`` keyword in ``to_ragged_array`` and ``get_coordinates`` (#2234, #2235)
-- Add parameters ``method`` and ``keep_collapsed`` to ``shapely.make_valid()`` (#1941)
-- The ``voronoi_polygons`` now accepts the ``ordered`` keyword, optionally forcing the
- order of polygons within the GeometryCollection to follow the order of input
- coordinates. Requires at least GEOS 3.12. (#1968)
-- Add option on ``invalid="fix"`` to ``from_wkb`` and ``from_wkt`` (#2094)
-- Add a ``normalize`` keyword to ``equals_exact`` (#1231)
-- Handle ``Feature`` type in ``shapely.geometry.shape`` (#1815)
-- Add support to split polygons by multilinestrings (#2206)
-- Add an ``m`` attribute on the Point class and an ``has_m`` attribute on the base Geometry class.
-
-New functions:
-
-- Add ``disjoint_subset_union`` and ``disjoint_subset_union_all`` as an optimized
- version of union and union_all, assuming inputs can be divided into subsets that do
- not intersect. Requires at least GEOS 3.12.
-- Add function ``minimum_clearance_line`` (#2106)
-- Add function ``maximum_inscribed_circle`` (#1307)
-- Add function ``orient_polygons`` (#2147)
-- Add function ``constrained_delaunay_triangles`` (#1685)
-- Add function ``coverage_simplify`` to allow topological simplification of polygonal
- coverages (#1969)
-- Add function ``coverage_is_valid`` and ``coverage_invalid_edges`` to validate
- an array of geometries as valid topological coverage (#2156)
-- Add function ``equals_identical`` (#1760)
-- Add function ``has_m`` (#2008)
-- Add function ``get_m`` (#2019)
-
-Breaking changes in GEOS 3.12:
-
-- ``oriented_envelope`` / ``minimum_rotated_rectangle`` changed its implementation
- in GEOS 3.12. Be aware that results will change when updating GEOS. Coincidentally
- the implementation is similar to the shapely 1.x approach. (#1885)
-- ``get_coordinate_dimension`` / ``has_z`` now considers geometries three dimensional if
- they have a NaN z coordinate. (#1885)
-- ``voronoi_polygons`` changed its output from a LINESTRING to a MULTILINESTRING in case
- ``only_edges=True``. (#1885)
-- The WKT representation of a MULTIPOINT changed from for example "MULTIPOINT (0 0, 1 1)"
- to "MULTIPOINT ((0 0), (1 1))". (#1885)
+2.1.0 (2025-04-03)
+------------------
-Deprecations:
+Shapely 2.1.0 is a feature release with various new functions,
+improvements and bug fixes. Highlights include initial support for geometries
+with M or ZM values, functionality for coverage validation and
+simplification, and a set of new top-level functions.
-- The ``shapely.geos`` module is deprecated. All GEOS-version related attributes are
- available directly from the top-level ``shapely`` namespace as well (already since
- shapely 2.0) (#2145).
-- The ``shapely.vectorized`` module is deprecated. The two functions (``contains and
- ``touches``) can be replaced by the top-level vectorized functions ``contains_xy``
- and ``intersects_xy`` (#1630).
+Shapely supports Python >= 3.10, and binary wheels on PyPI include GEOS 3.13.1
+and are now also provided for musllinux (Alpine) x86_64 platforms.
-Packaging:
+For a full changelog, see
+https://shapely.readthedocs.io/en/latest/release/2.x.html#version-2-1-0
-- Require GEOS >= 3.9, NumPy >= 1.21, and Python >= 3.10 (#1802, #1885, #2124)
-- Binary wheels are now built for musllinux (Alpine) x86_64 platforms (#1996).
-- Upgraded the GEOS version in the binary wheel distributions to 3.13.1.
2.0.7 (2025-01-30)
------------------
=====================================
CITATION.cff
=====================================
@@ -2,8 +2,8 @@ cff-version: 1.2.0
message: "Please cite this software using these metadata."
type: software
title: Shapely
-version: "2.0.7"
-date-released: "2025-01-30"
+version: "2.1.1"
+date-released: "2025-05-19"
doi: 10.5281/zenodo.5597138
abstract: "Manipulation and analysis of geometric objects in the Cartesian plane."
repository-artifact: https://pypi.org/project/Shapely
=====================================
debian/changelog
=====================================
@@ -1,8 +1,17 @@
-python-shapely (2.1.0~rc1-1~exp2) UNRELEASED; urgency=medium
+python-shapely (2.1.1-1~exp1) experimental; urgency=medium
+ * New upstream release.
+
+ -- Bas Couwenberg <sebastic at debian.org> Mon, 19 May 2025 15:53:31 +0200
+
+python-shapely (2.1.0-1) unstable; urgency=medium
+
+ * New upstream release.
* Bump Standards-Version to 4.7.2, no changes.
+ * Update lintian overrides.
+ * Move from experimental to unstable.
- -- Bas Couwenberg <sebastic at debian.org> Thu, 20 Mar 2025 06:21:17 +0100
+ -- Bas Couwenberg <sebastic at debian.org> Thu, 03 Apr 2025 15:00:18 +0200
python-shapely (2.1.0~rc1-1~exp1) experimental; urgency=medium
=====================================
debian/lintian-overrides
=====================================
@@ -2,9 +2,6 @@
# Fortify Source functions: no, only unprotected functions found!
hardening-no-fortify-functions *
-# False positive (#1094364)
-missing-dependency-on-numpy-abi *
-
# False positive
package-contains-documentation-outside-usr-share-doc [usr/lib/python3/dist-packages/*.*-info/*.txt]
=====================================
docs/code/coverage_simplify.py
=====================================
@@ -0,0 +1,53 @@
+import urllib.request
+
+import numpy as np
+import matplotlib.pyplot as plt
+
+import shapely
+from shapely.plotting import plot_polygon
+
+from figures import SIZE, BLUE
+
+## Downloading and preprocessing data
+
+# download countries geojson from https://datahub.io/core/geo-countries
+with urllib.request.urlopen("https://datahub.io/core/geo-countries/_r/-/data/countries.geojson") as f:
+ geojson = f.read().decode("utf-8")
+
+geoms = np.asarray(shapely.from_geojson(geojson).geoms)
+
+# select countries of Africa
+clip_polygon = shapely.from_wkt("POLYGON ((-23.714863 14.983714, 0.434636 -51.130505, 52.292267 -50.120681, 60.184885 -22.43548, 57.448957 14.358282, 44.596307 11.524365, 33.72577 34.648769, 7.226985 39.73104, -16.429284 33.649053, -23.714863 14.983714))")
+geoms_africa = geoms[shapely.within(geoms, clip_polygon)]
+
+# remove small islands for nicer illustration of coverage
+temp = geoms_africa[shapely.area(geoms_africa) > 0.1]
+parts, indices = shapely.get_parts(temp, return_index=True)
+mask = shapely.area(parts) > 0.08
+temp = shapely.multipolygons(parts[mask], indices=indices[mask])
+
+# set precision to improve coverage validity
+geoms_africa2 = shapely.set_precision(temp, 0.001)
+
+## Coverage simplify
+
+geoms_africa_simplified = shapely.coverage_simplify(geoms_africa2, tolerance=5.0)
+
+# plot
+fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=SIZE, dpi=90)
+
+for geom in geoms_africa2:
+ plot_polygon(geom, ax=ax1, add_points=False, color=BLUE)
+
+ax1.set_title('a) original data')
+ax1.axis("off")
+ax1.set_aspect("equal")
+
+for geom in geoms_africa_simplified:
+ plot_polygon(geom, ax=ax2, add_points=False, color=BLUE)
+
+ax2.set_title('b) coverage simplified')
+ax2.axis("off")
+ax2.set_aspect("equal")
+
+plt.show()
=====================================
docs/coverage.rst
=====================================
@@ -9,3 +9,5 @@ Coverage operations
{% for function in get_module_functions("_coverage") %}
{{ function }}
{% endfor %}
+ coverage_union
+ coverage_union_all
=====================================
docs/environment.yml
=====================================
@@ -2,8 +2,8 @@ name: shapely_docs
channels:
- conda-forge
dependencies:
- - python=3.10
- - geos=3.11
+ - python=3.12
+ - geos=3.13
- numpy
- cython
- sphinx-book-theme
=====================================
docs/geometry.rst
=====================================
@@ -84,7 +84,7 @@ Therefore, geometries are equal if and only if their WKB representations are equ
>>> point_1 = Point(5.2, 52.1)
>>> point_2 = Point(1, 1)
>>> point_3 = Point(5.2, 52.1)
- >>> {point_1, point_2, point_3}
+ >>> {point_1, point_2, point_3} # doctest: +SKIP
{<POINT (1 1)>, <POINT (5.2 52.1)>}
.. warning:: Due to limitations of WKB, linearrings will equal linestrings if they contain the exact same points.
@@ -113,9 +113,9 @@ The most convenient is to use ``.wkb_hex`` and ``.wkt`` properties.
>>> from shapely import Point, to_wkb, to_wkt, to_geojson
>>> pt = Point(-169.910918, -18.997564)
>>> pt.wkb_hex
- 0101000000CF6A813D263D65C0BDAAB35A60FF32C0
+ '0101000000CF6A813D263D65C0BDAAB35A60FF32C0'
>>> pt.wkt
- POINT (-169.910918 -18.997564)
+ 'POINT (-169.910918 -18.997564)'
More output options can be found using using :func:`~shapely.to_wkb`,
:func:`~shapely.to_wkt`, and :func:`~shapely.to_geojson` functions.
@@ -123,9 +123,9 @@ More output options can be found using using :func:`~shapely.to_wkb`,
.. code:: python
>>> to_wkb(pt, hex=True, byte_order=0)
- 0000000001C0653D263D816ACFC032FF605AB3AABD
+ '0000000001C0653D263D816ACFC032FF605AB3AABD'
>>> to_wkt(pt, rounding_precision=3)
- POINT (-169.911 -18.998)
+ 'POINT (-169.911 -18.998)'
>>> print(to_geojson(pt, indent=2))
{
"type": "Point",
@@ -158,21 +158,35 @@ Semantic for format specification
Format types ``'f'`` and ``'F'`` are to use a fixed-point notation, which is
activated by setting GEOS' trim option off.
-The upper case variant converts ``nan`` to ``NAN`` and ``inf`` to ``INF``.
-Format types ``'g'`` and ``'G'`` are to use a "general format",
+Format types ``'g'`` (default) and ``'G'`` are to use a "general format",
where unnecessary digits are trimmed. This notation is activated by setting
-GEOS' trim option on. The upper case variant is similar to
-``'F'``, and may also display an upper-case ``"E"`` if scientific notation
-is required. Note that this representation may be different for GEOS 3.10.0
-and later, which does not use scientific notation.
+GEOS' trim option on. This option sometimes enables
+:ref:`scientific-formatting`.
-For numeric outputs ``'f'`` and ``'g'``, the precision is optional, and if not
+For numeric outputs ``[fFgG]``, the precision is optional, and if not
specified, rounding precision will be disabled showing full precision.
Format types ``'x'`` and ``'X'`` show a hex-encoded string representation of
-WKB or Well-Known Binary, with the case of the output matched the
-case of the format type character.
+WKB or Well-Known Binary.
+
+The upper case letter variant converts all non-numeric values to uppercase,
+e.g. ``nan`` to ``NAN``, or ``e`` to ``E``.
+
+.. _scientific-formatting:
+
+Scientific formatting
+---------------------
+
+WKT outputs may sometimes use scientific formatting, depending on the GEOS
+version, coordinate value and WKT writer options. GEOS versions 3.10 to 3.12
+never use scientific formatting (only showing positional), while older and
+newer versions may use scientific formatting for large and small coordinate
+values (e.g. ``POINT (1.234e+18 1.234e-11)``).
+
+Scientific formatting can always be disabled by setting ``trim=False`` for
+:func:`~shapely.to_wkt` or other WKT writer methods,
+which sets the WKT writer to use fixed-precision number formatting.
.. _canonical-form:
=====================================
docs/manual.rst
=====================================
@@ -1285,34 +1285,7 @@ and that copies of these are collected into a list
>>> features = [c, a, d, b, c]
that we'd prefer to have ordered as ``[d, c, c, b, a]`` in reverse containment
-order. As explained in the Python `Sorting HowTo`_, we can define a key
-function that operates on each list element and returns a value for comparison.
-Our key function will be a wrapper class that implements ``__lt__()`` using
-Shapely's binary :meth:`~object.within` predicate.
-
-.. code-block:: python
-
- >>> class Within:
- ... def __init__(self, o):
- ... self.o = o
- ... def __lt__(self, other):
- ... return self.o.within(other.o)
-
-As the howto says, the `less than` comparison is guaranteed to be used in
-sorting. That's what we'll rely on to spatially sort. Trying it out on features
-`d` and `c`, we see that it works.
-
-.. code-block:: pycon
-
- >>> Within(d) < Within(c)
- False
-
-It also works on the list of features, producing the order we want.
-
-.. code-block:: pycon
-
- >>> [d, c, c, b, a] == sorted(features, key=Within, reverse=True)
- True
+order.
DE-9IM Relationships
--------------------
=====================================
docs/migration.rst
=====================================
@@ -5,9 +5,9 @@ Migrating to Shapely 1.8 / 2.0
==============================
Shapely 1.8.0 is a transitional version introducing several warnings in
-preparation of the upcoming changes in 2.0.0.
+transition to 2.0.0.
-Shapely 2.0.0 will be a major release with a refactor of the internals with
+Shapely 2.0.0 was a major release with a refactor of the internals with
considerable performance improvements (based on the developments in the
`PyGEOS <https://github.com/pygeos/pygeos>`__ package), along with several
breaking changes.
@@ -33,13 +33,13 @@ can change their coordinates in-place. Illustrative code::
>>> print(line)
LINESTRING (0 0, 2 2)
- >>> line.coords = [(0, 0), (10, 0), (10, 10)]
- >>> print(line)
+ >>> line.coords = [(0, 0), (10, 0), (10, 10)] # doctest: +SKIP
+ >>> print(line) # doctest: +SKIP
LINESTRING (0 0, 10 0, 10 10)
In Shapely 1.8, this will start raising a warning::
- >>> line.coords = [(0, 0), (10, 0), (10, 10)]
+ >>> line.coords = [(0, 0), (10, 0), (10, 10)] # doctest: +SKIP
ShapelyDeprecationWarning: Setting the 'coords' to mutate a Geometry
in place is deprecated, and will not be possible any more in Shapely 2.0
@@ -56,15 +56,16 @@ Setting custom attributes
-------------------------
Another consequence of the geometry objects becoming immutable is that
-assigning custom attributes, which currently works, will no longer be possible.
+assigning custom attributes, which previously worked, will no longer be
+possible.
-Currently you can do::
+Previously, custom attributes could have ben added::
- >>> line.name = "my_geometry"
- >>> line.name
+ >>> line.name = "my_geometry" # doctest: +SKIP
+ >>> line.name # doctest: +SKIP
'my_geometry'
-In Shapely 1.8, this will start raising a warning, and will raise an
+In Shapely 1.8, this will raise a warning, and will raise an
AttributeError in Shapely 2.0.
**How do I update my code?** There is no direct alternative for adding custom
@@ -85,18 +86,18 @@ Some examples of this with Shapely 1.x:
>>> from shapely.geometry import Point, MultiPoint
>>> mp = MultiPoint([(1, 1), (2, 2), (3, 3)])
- >>> print(mp)
+ >>> print(mp) # doctest: +SKIP
MULTIPOINT (1 1, 2 2, 3 3)
- >>> for part in mp:
- ... print(part)
+ >>> for part in mp: # doctest: +SKIP
+ ... print(part) # doctest: +SKIP
POINT (1 1)
POINT (2 2)
POINT (3 3)
- >>> print(mp[1])
+ >>> print(mp[1]) # doctest: +SKIP
POINT (2 2)
- >>> len(mp)
+ >>> len(mp) # doctest: +SKIP
3
- >>> list(mp)
+ >>> list(mp) # doctest: +SKIP
[<shapely.geometry.point.Point at 0x7f2e0912bf10>,
<shapely.geometry.point.Point at 0x7f2e09fed820>,
<shapely.geometry.point.Point at 0x7f2e09fed4c0>]
@@ -104,8 +105,8 @@ Some examples of this with Shapely 1.x:
Starting with Shapely 1.8, all the examples above will start raising a
deprecation warning. For example:
- >>> for part in mp:
- ... print(part)
+ >>> for part in mp: # doctest: +SKIP
+ ... print(part) # doctest: +SKIP
ShapelyDeprecationWarning: Iteration over multi-part geometries is deprecated
and will be removed in Shapely 2.0. Use the `geoms` property to access the
constituent parts of a multi-part geometry.
@@ -129,7 +130,7 @@ The examples above can be updated to::
POINT (2 2)
>>> len(mp.geoms)
3
- >>> list(mp.geoms)
+ >>> list(mp.geoms) # doctest: +SKIP
[<shapely.geometry.point.Point at 0x7f2e0912bf10>,
<shapely.geometry.point.Point at 0x7f2e09fed820>,
<shapely.geometry.point.Point at 0x7f2e09fed4c0>]
@@ -152,7 +153,7 @@ A small example::
>>> line = LineString([(0, 0), (1, 1), (2, 2)])
>>> import numpy as np
- >>> np.asarray(line)
+ >>> np.asarray(line) # doctest: +SKIP
array([[0., 0.],
[1., 1.],
[2., 2.]])
@@ -160,9 +161,9 @@ A small example::
In addition, there are also the explicit ``array_interface()`` method and
``ctypes`` attribute to get access to the coordinates as array data:
- >>> line.ctypes
+ >>> line.ctypes # doctest: +SKIP
<shapely.geometry.linestring.c_double_Array_6 at 0x7f75261eb740>
- >>> line.array_interface()
+ >>> line.array_interface() # doctest: +SKIP
{'version': 3,
'typestr': '<f8',
'data': <shapely.geometry.linestring.c_double_Array_6 at 0x7f752664ae40>,
@@ -177,7 +178,7 @@ removed from those geometry classes, and limited to the ``coords``.
Starting with Shapely 1.8, converting a geometry object to a NumPy array
directly will start raising a warning::
- >>> np.asarray(line)
+ >>> np.asarray(line) # doctest: +SKIP
ShapelyDeprecationWarning: The array interface is deprecated and will no longer
work in Shapely 2.0. Convert the '.coords' to a NumPy array instead.
array([[0., 0.],
@@ -209,7 +210,7 @@ as follows::
>>> from shapely.geometry import Point
>>> arr = np.array([Point(0, 0), Point(1, 1), Point(2, 2)])
- >>> arr
+ >>> arr # doctest: +SKIP
array([<shapely.geometry.point.Point object at 0x7fb798407cd0>,
<shapely.geometry.point.Point object at 0x7fb7982831c0>,
<shapely.geometry.point.Point object at 0x7fb798283b80>],
@@ -300,7 +301,7 @@ creation methods. A small example for an empty Polygon geometry:
>>> g1 = Polygon()
>>> type(g1)
<class 'shapely.geometry.polygon.Polygon'>
- >>> g1.wkt
+ >>> g1.wkt # doctest: +SKIP
GEOMETRYCOLLECTION EMPTY
# Converting from WKT gives a correct empty polygon
@@ -309,7 +310,7 @@ creation methods. A small example for an empty Polygon geometry:
>>> type(g2)
<class 'shapely.geometry.polygon.Polygon'>
>>> g2.wkt
- POLYGON EMPTY
+ 'POLYGON EMPTY'
Shapely 1.8 does not yet change this inconsistent behaviour, but starting
with Shapely 2.0, the different methods will always consistently give an
=====================================
docs/release/2.x.rst
=====================================
@@ -1,9 +1,35 @@
Version 2.x
===========
+.. _version-2-1-1:
+
+Version 2.1.1 (2025-05-19)
+--------------------------
+
+Bug fixes:
+
+- Fix performance degradation calling shapely functions (caused by deprecation
+ of certain positional arguments) (#2283).
+- Fix crash caused by `from_ragged_array()` (#2291).
+- Fix compilation error building with recent LLVM toolchain (#2293).
+
+Acknowledgments
+^^^^^^^^^^^^^^^
+
+Thanks to everyone who contributed to this release!
+People with a "+" by their names contributed a patch for the first time.
+
+A total of 5 people contributed patches to this release. People with a
+"+" by their names contributed a patch for the first time.
+
+* Joris Van den Bossche
+* Kamil Monicz +
+* Kurt Schwehr +
+* Mike Taves
+
.. _version-2-1-0:
-Version 2.1.0 (unreleased)
+Version 2.1.0 (2025-04-03)
--------------------------
New features
@@ -14,6 +40,18 @@ Initial support for geometries with M or ZM values
Shapely geometries can now represent coordinates with M values (measure) in
addition to X, Y, and Z (requires GEOS >= 3.12).
+
+.. code:: python
+
+ >>> import shapely
+ >>> point_m = shapely.from_wkt("POINT M (5.2 52.1 15.3)")
+ >>> point_m
+ <POINT M (5.2 52.1 15.3)>
+ >>> point_m.has_m
+ True
+ >>> point_m.m
+ 15.3
+
The initial support includes:
- Creating geometries from WKT or WKB with M values will now preserve the M
@@ -27,6 +65,22 @@ The initial support includes:
- Add an ``include_m`` keyword in ``to_ragged_array`` and ``get_coordinates``
(#2234, #2235).
+Coverage validation and simplification
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Several functions have been added to work with coverages (requires GEOS >=
+3.12), where a coverage is made up of a collection of valid (multi)polygons that
+do not overlap and are edge-matched (vertices along shared edges are identical).
+
+The :func:`.coverage_is_valid` and :func:`.coverage_invalid_edges` functions
+help to validate an array of geometries as a topological coverage and
+inspect invalid edges. The :func:`.coverage_simplify` function then allows
+topological simplification of the coverage (in contrast to the existing
+:func:`.simplify` function, which simplifies an array of geometries one by one,
+independently).
+
+.. plot:: code/coverage_simplify.py
+
New functions
~~~~~~~~~~~~~
=====================================
shapely/_geometry_helpers.pyx
=====================================
@@ -284,7 +284,7 @@ def get_parts(object[:] array, bint extract_rings=0):
@cython.boundscheck(False)
@cython.wraparound(False)
-cdef void _deallocate_arr(void* handle, np.intp_t[:] arr, Py_ssize_t last_geom_i) noexcept nogil:
+cdef void _deallocate_arr(GEOSContextHandle_t handle, np.intp_t[:] arr, Py_ssize_t last_geom_i) noexcept nogil:
"""Deallocate a temporary geometry array to prevent memory leaks"""
cdef Py_ssize_t i = 0
cdef GEOSGeometry *g
@@ -421,7 +421,7 @@ def collections_1d(object geometries, object indices, int geometry_type = 7, obj
coll = GEOSGeom_createPolygon_r(
geos_handle,
<GEOSGeometry*> temp_geoms_view[0],
- NULL if coll_size <= 1 else <GEOSGeometry**> &temp_geoms_view[1],
+ <GEOSGeometry**> NULL if coll_size <= 1 else <GEOSGeometry**> &temp_geoms_view[1],
coll_size - 1
)
else: # Polygon, empty
@@ -454,7 +454,7 @@ def _from_ragged_array_multi_linear(
MultiLineString (geometry_type 5): linear_type is a LineString (1)
"""
cdef:
- Py_ssize_t n_geoms
+ Py_ssize_t n_total_coords, n_rings, n_geoms
Py_ssize_t i, k
Py_ssize_t i1, i2, k1, k2
Py_ssize_t n_coords, linear_idx
@@ -463,12 +463,26 @@ def _from_ragged_array_multi_linear(
GEOSGeometry *linear = NULL
GEOSGeometry *geom = NULL
+ n_total_coords = coordinates.shape[0]
+ n_rings = offsets1.shape[0] - 1
n_geoms = offsets2.shape[0] - 1
+ if offsets2[n_geoms] > n_rings:
+ raise ValueError(
+ f"Number of rings indicated by the geometry offsets ({offsets2[n_geoms]}) "
+ f"larger than indicated by the shape of the linear offsets array ({n_rings})"
+ )
+
+ if offsets1[n_rings] > n_total_coords:
+ raise ValueError(
+ f"Number of coordinates indicated by the linear offsets ({offsets1[n_rings]}) "
+ f"larger than the shape of the coordinates array ({n_total_coords})"
+ )
+
# A temporary array for the geometries that will be given to CreatePolygon/Collection.
- # For simplicity, we use n_geoms instead of calculating
- # the max needed size (trading performance for a bit more memory usage)
- temp_linear = np.empty(shape=(n_geoms, ), dtype=np.intp)
+ # For simplicity, we use n_rings instead of calculating the max needed size
+ # as max(diff(offsets2)) (trading performance for a bit more memory usage)
+ temp_linear = np.empty(shape=(n_rings, ), dtype=np.intp)
cdef np.intp_t[:] temp_linear_view = temp_linear
# A temporary array for resulting geometries
temp_geoms = np.empty(shape=(n_geoms, ), dtype=np.intp)
@@ -570,7 +584,7 @@ def _from_ragged_array_multipolygon(
Create MultiPolygons from coordinate and offset arrays.
"""
cdef:
- Py_ssize_t n_geoms
+ Py_ssize_t n_total_coords, n_rings, n_parts, n_geoms
Py_ssize_t i, j, k
Py_ssize_t i1, i2, j1, j2, k1, k2
Py_ssize_t n_coords, rings_idx, parts_idx
@@ -580,14 +594,35 @@ def _from_ragged_array_multipolygon(
GEOSGeometry *part = NULL
GEOSGeometry *geom = NULL
+ n_total_coords = coordinates.shape[0]
+ n_rings = offsets1.shape[0] - 1
+ n_parts = offsets2.shape[0] - 1
n_geoms = offsets3.shape[0] - 1
+ if offsets3[n_geoms] > n_parts:
+ raise ValueError(
+ f"Number of geometry parts indicated by the geometry offsets ({offsets3[n_geoms]}) "
+ f"larger than indicated by the shape of the part offsets array ({n_parts})"
+ )
+
+ if offsets2[n_parts] > n_rings:
+ raise ValueError(
+ f"Number of rings indicated by the part offsets ({offsets2[n_parts]}) "
+ f"larger than indicated by the shape of the linear offsets array ({n_rings})"
+ )
+
+ if offsets1[n_rings] > n_total_coords:
+ raise ValueError(
+ f"Number of coordinates indicated by the linear offsets ({offsets1[n_rings]}) "
+ f"larger than the shape of the coordinates array ({n_total_coords})"
+ )
+
# A temporary array for the geometries that will be given to CreatePolygon
- # and CreateCollection. For simplicity, we use n_geoms instead of calculating
- # the max needed size (trading performance for a bit more memory usage)
- temp_rings = np.empty(shape=(n_geoms, ), dtype=np.intp)
+ # and CreateCollection. For simplicity, we use n_rings/n_parts instead of
+ # calculating the max needed size (trading performance for a bit more memory usage)
+ temp_rings = np.empty(shape=(n_rings, ), dtype=np.intp)
cdef np.intp_t[:] temp_rings_view = temp_rings
- temp_parts = np.empty(shape=(n_geoms, ), dtype=np.intp)
+ temp_parts = np.empty(shape=(n_parts, ), dtype=np.intp)
cdef np.intp_t[:] temp_parts_view = temp_parts
# A temporary array for resulting geometries
temp_geoms = np.empty(shape=(n_geoms, ), dtype=np.intp)
=====================================
shapely/_version.py
=====================================
@@ -25,9 +25,9 @@ def get_keywords():
# setup.py/versioneer.py will grep for the variable names, so they must
# each be defined on a line of their own. _version.py will just call
# get_keywords().
- git_refnames = " (HEAD -> main, tag: 2.1.0rc1)"
- git_full = "7cd599893ea3b478b662189449514a239da5751d"
- git_date = "2025-03-15 15:43:48 +0100"
+ git_refnames = " (HEAD -> main, tag: 2.1.1)"
+ git_full = "b49155ea7eaec698a0357fb9bc1147533b566916"
+ git_date = "2025-05-19 12:40:46 +0200"
keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
return keywords
=====================================
shapely/decorators.py
=====================================
@@ -1,9 +1,10 @@
"""Decorators for Shapely functions."""
-import inspect
import os
import warnings
-from functools import wraps
+from collections.abc import Callable, Iterable
+from functools import lru_cache, wraps
+from inspect import unwrap
import numpy as np
@@ -92,41 +93,90 @@ def multithreading_enabled(func):
return wrapped
-def deprecate_positional(should_be_kwargs, category=DeprecationWarning):
- """Show warning if positional arguments are used that should be keyword."""
+def deprecate_positional(
+ should_be_kwargs: Iterable[str],
+ category: type[Warning] = DeprecationWarning,
+):
+ """Show warning if positional arguments are used that should be keyword.
+
+ Parameters
+ ----------
+ should_be_kwargs : Iterable[str]
+ Names of parameters that should be passed as keyword arguments.
+ category : type[Warning], optional (default: DeprecationWarning)
+ Warning category to use for deprecation warnings.
+
+ Returns
+ -------
+ callable
+ Decorator function that adds positional argument deprecation warnings.
+
+ Examples
+ --------
+ >>> from shapely.decorators import deprecate_positional
+ >>> @deprecate_positional(['b', 'c'])
+ ... def example(a, b, c=None):
+ ... return a, b, c
+ ...
+ >>> example(1, 2) # doctest: +SKIP
+ DeprecationWarning: positional argument `b` for `example` is deprecated. ...
+ (1, 2, None)
+ >>> example(1, b=2) # No warnings
+ (1, 2, None)
+ """
+
+ def decorator(func: Callable):
+ code = unwrap(func).__code__
+
+ # positional parameters are the first co_argcount names
+ pos_names = code.co_varnames[: code.co_argcount]
+ # build a name -> index map
+ name_to_idx = {name: idx for idx, name in enumerate(pos_names)}
+ # pick out only those names we care about
+ deprecate_positions = [
+ (name_to_idx[name], name)
+ for name in should_be_kwargs
+ if name in name_to_idx
+ ]
+
+ # early exit if there are no deprecated positional args
+ if not deprecate_positions:
+ return func
+
+ # earliest position where a warning could occur
+ warn_from = min(deprecate_positions)[0]
+
+ @lru_cache(10)
+ def make_msg(n_args: int):
+ used = [name for idx, name in deprecate_positions if idx < n_args]
+
+ if len(used) == 1:
+ args_txt = f"`{used[0]}`"
+ plr = ""
+ isare = "is"
+ else:
+ plr = "s"
+ isare = "are"
+ if len(used) == 2:
+ args_txt = " and ".join(f"`{u}`" for u in used)
+ else:
+ args_txt = ", ".join(f"`{u}`" for u in used[:-1])
+ args_txt += f", and `{used[-1]}`"
+
+ return (
+ f"positional argument{plr} {args_txt} for `{func.__name__}` "
+ f"{isare} deprecated. Please use keyword argument{plr} instead."
+ )
- def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
- # call the function first, to make sure the signature matches
- ret_value = func(*args, **kwargs)
-
- # check signature to see which positional args were used
- sig = inspect.signature(func)
- args_bind = sig.bind_partial(*args)
- warn_args = [
- f"`{arg}`"
- for arg in args_bind.arguments.keys()
- if arg in should_be_kwargs
- ]
- if warn_args:
- if len(warn_args) == 1:
- plr = ""
- isare = "is"
- args = warn_args[0]
- else:
- plr = "s"
- isare = "are"
- if len(warn_args) < 3:
- args = " and ".join(warn_args)
- else:
- args = ", ".join(warn_args[:-1]) + ", and " + warn_args[-1]
- msg = (
- f"positional argument{plr} {args} for `{func.__name__}` "
- f"{isare} deprecated. Please use keyword argument{plr} instead."
- )
- warnings.warn(msg, category=category, stacklevel=2)
- return ret_value
+ result = func(*args, **kwargs)
+
+ n = len(args)
+ if n > warn_from:
+ warnings.warn(make_msg(n), category=category, stacklevel=2)
+
+ return result
return wrapper
=====================================
shapely/io.py
=====================================
@@ -56,7 +56,8 @@ def to_wkt(
The rounding precision when writing the WKT string. Set to a value of
-1 to indicate the full precision.
trim : bool, default True
- If True, trim unnecessary decimals (trailing zeros).
+ If True, trim unnecessary decimals (trailing zeros). If False,
+ use fixed-precision number formatting.
output_dimension : int, default None
The output dimension for the WKT string. Supported values are 2, 3 and
4 for GEOS 3.12+. Default None will automatically choose 3 or 4,
=====================================
shapely/tests/legacy/test_polylabel.py
=====================================
@@ -48,7 +48,10 @@ class PolylabelTestCase(unittest.TestCase):
]
)
label = polylabel(polygon)
- if shapely.geos_version >= (3, 12, 0):
+ if shapely.geos_version >= (3, 14, 0):
+ # https://github.com/libgeos/geos/issues/1265
+ assert label.coords[:] == [(32.722025, -117.195155)]
+ elif shapely.geos_version >= (3, 12, 0):
# recent GEOS corrects for this
assert label.coords[:] == [(32.722025, -117.201875)]
else:
=====================================
shapely/tests/test_constructive.py
=====================================
@@ -1184,7 +1184,7 @@ def test_maximum_inscribed_circle_all_types(geometry):
GEOSException,
match=(
"Argument must be Polygonal or LinearRing|" # GEOS < 3.10.4
- "Input geometry must be a Polygon or MultiPolygon|"
+ "must be a Polygon or MultiPolygon|"
"Operation not supported by GeometryCollection"
),
):
@@ -1193,7 +1193,7 @@ def test_maximum_inscribed_circle_all_types(geometry):
if geometry.is_empty:
with pytest.raises(
- GEOSException, match="Empty input geometry is not supported"
+ GEOSException, match="Empty input(?: geometry)? is not supported"
):
shapely.maximum_inscribed_circle(geometry)
return
@@ -1227,13 +1227,15 @@ def test_maximum_inscribed_circle_empty():
GEOSException,
match=(
"Argument must be Polygonal or LinearRing|" # GEOS < 3.10.4
- "Input geometry must be a Polygon or MultiPolygon"
+ "must be a Polygon or MultiPolygon"
),
):
shapely.maximum_inscribed_circle(geometry)
geometry = shapely.from_wkt("POLYGON EMPTY")
- with pytest.raises(GEOSException, match="Empty input geometry is not supported"):
+ with pytest.raises(
+ GEOSException, match="Empty input(?: geometry)? is not supported"
+ ):
shapely.maximum_inscribed_circle(geometry)
=====================================
shapely/tests/test_decorators.py
=====================================
@@ -0,0 +1,138 @@
+import pytest
+from pytest import WarningsRecorder
+
+from shapely.decorators import deprecate_positional
+
+
+ at deprecate_positional(["b", "c"])
+def func_two(a, b=2, c=3):
+ return a, b, c
+
+
+ at deprecate_positional(["b", "c", "d"])
+def func_three(a, b=1, c=2, d=3):
+ return a, b, c, d
+
+
+ at deprecate_positional(["b", "d"])
+def func_noncontig(a, b=1, c=2, d=3):
+ return a, b, c, d
+
+
+ at deprecate_positional(["b"], category=UserWarning)
+def func_custom_category(a, b=1):
+ return a, b
+
+
+ at deprecate_positional(["b"])
+def func_varargs(a, b=1, *args):
+ return a, b, args
+
+
+ at deprecate_positional([])
+def func_no_deprecations(a, b=1):
+ return a, b
+
+
+def test_all_kwargs_no_warning(recwarn: WarningsRecorder) -> None:
+ assert func_two(a=10, b=20, c=30) == (10, 20, 30)
+ assert not recwarn.list
+
+
+def test_only_required_arg_no_warning(recwarn: WarningsRecorder) -> None:
+ assert func_two(1) == (1, 2, 3)
+ assert not recwarn.list
+
+
+def test_single_positional_warning() -> None:
+ with pytest.warns(
+ DeprecationWarning, match="positional argument `b` for `func_two` is deprecated"
+ ):
+ out = func_two(1, 4)
+ assert out == (1, 4, 3)
+
+
+def test_multiple_positional_warning() -> None:
+ with pytest.warns(
+ DeprecationWarning,
+ match="positional arguments `b` and `c` for `func_two` are deprecated",
+ ):
+ out = func_two(1, 4, 5)
+ assert out == (1, 4, 5)
+
+
+def test_three_positional_warning_oxford_comma() -> None:
+ with pytest.warns(
+ DeprecationWarning,
+ match="positional arguments `b`, `c`, and `d` for `func_three` are deprecated",
+ ):
+ out = func_three(1, 2, 3, 4)
+ assert out == (1, 2, 3, 4)
+
+
+def test_noncontiguous_partial_warning() -> None:
+ with pytest.warns(
+ DeprecationWarning,
+ match="positional argument `b` for `func_noncontig` is deprecated",
+ ):
+ out = func_noncontig(1, 2, 3)
+ assert out == (1, 2, 3, 3)
+
+
+def test_noncontiguous_full_warning() -> None:
+ with pytest.warns(
+ DeprecationWarning,
+ match="positional arguments `b` and `d` for `func_noncontig` are deprecated",
+ ):
+ out = func_noncontig(1, 2, 3, 4)
+ assert out == (1, 2, 3, 4)
+
+
+def test_custom_warning_category() -> None:
+ with pytest.warns(
+ UserWarning,
+ match="positional argument `b` for `func_custom_category` is deprecated",
+ ):
+ out = func_custom_category(1, 2)
+ assert out == (1, 2)
+
+
+def test_func_no_deprecations_never_warns(recwarn: WarningsRecorder) -> None:
+ out = func_no_deprecations(7, 8)
+ assert out == (7, 8)
+ assert not recwarn.list
+
+
+def test_missing_required_arg_no_warning(recwarn: WarningsRecorder) -> None:
+ with pytest.raises(TypeError):
+ func_two() # missing required 'a' # type: ignore
+ assert not recwarn.list
+
+
+def test_unknown_keyword_no_warning(recwarn: WarningsRecorder) -> None:
+ with pytest.raises(TypeError):
+ func_two(1, 4, d=5) # unknown keyword 'd' # type: ignore
+ assert not recwarn.list
+
+
+def test_varargs_behavior_and_deprecation() -> None:
+ with pytest.warns(
+ DeprecationWarning,
+ match="positional argument `b` for `func_varargs` is deprecated",
+ ):
+ out = func_varargs(1, 2, 3, 4)
+ assert out == (1, 2, (3, 4))
+
+
+def test_varargs_no_warning(recwarn: WarningsRecorder) -> None:
+ out = func_varargs(1)
+ assert out == (1, 1, ())
+ assert not recwarn.list
+
+
+def test_repeated_warnings() -> None:
+ with pytest.warns(DeprecationWarning) as record:
+ func_two(1, 4, 5)
+ func_two(1, 4, 5)
+ assert len(record) == 2
+ assert str(record[0].message) == str(record[1].message)
=====================================
shapely/tests/test_ragged_array.py
=====================================
@@ -586,3 +586,80 @@ def test_from_ragged_wrong_offsets():
np.array([[0, 0], [0, 1]]),
offsets=(np.array([0, 1]),),
)
+
+
+def test_from_ragged_crash_2284():
+ # caused segfault in shapely 2.1.0
+ # https://github.com/shapely/shapely/discussions/2284
+
+ # one of the geometries has more rings than the total number of geometries
+ coords = np.random.default_rng().random(120).reshape((60, 2))
+ offsets1 = np.array([0, 10, 20, 30, 40, 50, 60])
+ offsets2 = np.array([0, 1, 5, 6])
+
+ for _ in range(10):
+ polygons = shapely.from_ragged_array(
+ shapely.GeometryType.POLYGON, coords, (offsets1, offsets2)
+ )
+ # just ensure it didn't crash
+ assert len(polygons) == 3
+
+ offsets3 = np.array([0, 3])
+ for _ in range(10):
+ polygons = shapely.from_ragged_array(
+ shapely.GeometryType.MULTIPOLYGON, coords, (offsets1, offsets2, offsets3)
+ )
+ # just ensure it didn't crash
+ assert len(polygons) == 1
+
+
+def test_from_ragged_wrong_offsets_values():
+ # caused segfault in shapely 2.1.0
+
+ # outer offsets indicates more rings than the shape of the ring offsets
+ coords = np.random.default_rng().random(70).reshape((35, 2))
+ offsets1 = np.array([0, 10, 20], dtype=np.uint32)
+ offsets2 = np.array([0, 1, 5], dtype=np.uint32)
+ offsets3 = np.array([0, 2])
+
+ with pytest.raises(
+ ValueError, match="Number of rings indicated by the geometry offsets"
+ ):
+ shapely.from_ragged_array(
+ shapely.GeometryType.POLYGON, coords, (offsets1, offsets2)
+ )
+
+ with pytest.raises(
+ ValueError, match="Number of rings indicated by the part offsets"
+ ):
+ shapely.from_ragged_array(
+ shapely.GeometryType.MULTIPOLYGON, coords, (offsets1, offsets2, offsets3)
+ )
+
+ # inner offsets indicating more coordinats that the shape of the coordinates
+ coords = np.random.default_rng().random(70).reshape((35, 2))
+ offsets1 = np.array([0, 10, 40], dtype=np.uint32)
+ offsets2 = np.array([0, 1, 2], dtype=np.uint32)
+
+ with pytest.raises(
+ ValueError, match="Number of coordinates indicated by the linear offsets"
+ ):
+ shapely.from_ragged_array(
+ shapely.GeometryType.POLYGON, coords, (offsets1, offsets2)
+ )
+
+ with pytest.raises(
+ ValueError, match="Number of coordinates indicated by the linear offsets"
+ ):
+ shapely.from_ragged_array(
+ shapely.GeometryType.MULTIPOLYGON, coords, (offsets1, offsets2, offsets3)
+ )
+
+ # outer multipolygon offsets indicating too many parts
+ offsets3 = np.array([0, 3])
+ with pytest.raises(
+ ValueError, match="Number of geometry parts indicated by the geometry offsets"
+ ):
+ shapely.from_ragged_array(
+ shapely.GeometryType.MULTIPOLYGON, coords, (offsets1, offsets2, offsets3)
+ )
View it on GitLab: https://salsa.debian.org/debian-gis-team/python-shapely/-/compare/53595bc01b2ef94f34e252120073744955994f0c...f5458efe350e93acea6658e2c13be6d3f9fa12fc
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/python-shapely/-/compare/53595bc01b2ef94f34e252120073744955994f0c...f5458efe350e93acea6658e2c13be6d3f9fa12fc
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20250519/77da58b1/attachment-0001.htm>
More information about the Pkg-grass-devel
mailing list