[Git][debian-gis-team/pyshp][master] 4 commits: New upstream version 3.0.12

Bas Couwenberg (@sebastic) gitlab at salsa.debian.org
Sat Jun 6 05:31:37 BST 2026



Bas Couwenberg pushed to branch master at Debian GIS Project / pyshp


Commits:
be019752 by Bas Couwenberg at 2026-06-06T06:26:00+02:00
New upstream version 3.0.12
- - - - -
4c42b0a6 by Bas Couwenberg at 2026-06-06T06:26:09+02:00
Update upstream source from tag 'upstream/3.0.12'

Update to upstream version '3.0.12'
with Debian dir 9787dc31152dd8e91c081ab627b53b786c989ca6
- - - - -
a8511d61 by Bas Couwenberg at 2026-06-06T06:26:38+02:00
New upstream release.

- - - - -
f8d1a3b1 by Bas Couwenberg at 2026-06-06T06:27:06+02:00
Set distribution to unstable.

- - - - -


5 changed files:

- README.md
- changelog.txt
- debian/changelog
- src/shapefile.py
- tests/hypothesis_tests.py


Changes:

=====================================
README.md
=====================================
@@ -8,7 +8,7 @@ The Python Shapefile Library (PyShp) reads and writes ESRI Shapefiles in pure Py
 
 - **Author**: [Joel Lawhead](https://github.com/GeospatialPython)
 - **Maintainers**: [James Parrott](https://github.com/JamesParrott) & [Karim Bahgat](https://github.com/karimbahgat)
-- **Version**: 3.0.11
+- **Version**: 3.0.12
 - **Date**: 5th June 2026
 - **License**: [MIT](https://github.com/GeospatialPython/pyshp/blob/master/LICENSE.TXT)
 
@@ -93,6 +93,13 @@ part of your geospatial project.
 
 # Version Changes
 
+## 3.0.12
+### Data consistency
+ - Add Shape.points_2D and Shape.points_3D properties - lists of guaranteed length tuples (2 and 3 respectively).
+
+### Testing
+ - Round trip property-based tests for Polylines and Polygons (both pass).
+
 ## 3.0.11
 ### Edge case handling
  - Raise ShapefileException i) when creating Non-null Shapes without (or with empty) points
@@ -597,13 +604,14 @@ index which is 7.
 
 
 	>>> # Read the bbox of the 8th shape to verify
+	>>> s.bbox
+	BBox(xmin=-122.449637, ymin=37.80149, xmax=-122.442109, ymax=37.807958)
 	>>> # Round coordinates to 3 decimal places
-	>>> ['%.3f' % coord for coord in s.bbox]
+	>>> [f'{coord:.3f}' for coord in s.bbox]
 	['-122.450', '37.801', '-122.442', '37.808']
 
 Each shape record (except Points) contains the following attributes. Records of
 shapeType Point do not have a bounding box 'bbox'.
-# TODO!!  Fix attributes
 
 	>>> for name in dir(shapes[3]):
 	...     if not name.startswith('_'):
@@ -613,6 +621,8 @@ shapeType Point do not have a bounding box 'bbox'.
 	'oid'
 	'parts'
 	'points'
+	'points_2D'
+	'points_3D'
 	'shapeType'
 	'shapeTypeName'
 	'write_to_byte_stream'
@@ -636,16 +646,17 @@ shapeType Point do not have a bounding box 'bbox'.
 		>>> shapes[3].shapeTypeName
 		'POLYGON'
 
-  * `bbox`: If the shape type contains multiple points this tuple describes the
+  * `bbox`: If the shape type contains multiple points this named tuple describes the
 	  lower left (x,y) coordinate and upper right corner coordinate creating a
 	  complete box around the points. If the shapeType is a
 	  Null (shapeType == 0) then an AttributeError is raised.
 
 
 		>>> # Get the bounding box of the 4th shape.
+		>>> shapes[3].bbox
+		BBox(xmin=-122.485792, ymin=37.786931, xmax=-122.446285, ymax=37.811019)
 		>>> # Round coordinates to 3 decimal places
-		>>> bbox = shapes[3].bbox
-		>>> ['%.3f' % coord for coord in bbox]
+		>>> [f'{coord:.3f}' for coord in shapes[3].bbox]
 		['-122.486', '37.787', '-122.446', '37.811']
 
   * `parts`: Parts simply group collections of points into shapes. If the shape
@@ -657,16 +668,16 @@ shapeType Point do not have a bounding box 'bbox'.
 		>>> shapes[3].parts
 		[0]
 
-  * `points`: The points attribute contains a list of tuples containing an
-	  (x,y) coordinate for each point in the shape.
+  * `points_2D`/`points_3D`: The points_2D and points_3D attributes contain lists
+  		of tuples containing (x,y) or (x,y,z) coordinates respectively for each
+		point in the shape.  If no z data is available, z is set to 0 is used.
 
-
-		>>> len(shapes[3].points)
+		>>> len(shapes[3].points_2D)
 		173
 		>>> # Get the 8th point of the fourth shape
 		>>> # Truncate coordinates to 3 decimal places
-		>>> shape = shapes[3].points[7]
-		>>> ['%.3f' % coord for coord in shape]
+		>>> coords = shapes[3].points_2D[7]
+		>>> [f'{coord:.3f}' for coord in coords]
 		['-122.471', '37.787']
 
 In most cases, however, if you need to do more than just type or bounds checking, you may want
@@ -1563,6 +1574,9 @@ To examine a Z-type shapefile you can do:
 	>>> r.shape(0).z # flat list of Z-values
 	[18.0, 20.0, 22.0, 0.0, 0.0, 0.0, 0.0, 15.0, 13.0, 14.0]
 
+	>>> r.shape(0).points_3D # list of 3D coordinates incorporating the Z-values
+	[(1.0, 5.0, 18.0), (5.0, 5.0, 20.0), (5.0, 1.0, 22.0), (3.0, 3.0, 0.0), (1.0, 1.0, 0.0), (3.0, 2.0, 0.0), (2.0, 6.0, 0.0), (3.0, 2.0, 15.0), (2.0, 6.0, 13.0), (1.0, 9.0, 14.0)]
+
 	>>> r.close()
 
 ### 3D MultiPatch Shapefiles


=====================================
changelog.txt
=====================================
@@ -1,3 +1,12 @@
+VERSION 3.0.12
+
+2026-06-05
+	Data consistency
+	* Add Shape.points_2D and Shape.points_3D properties - lists of guaranteed length tuples (2 and 3 respectively).
+
+	Testing
+	* Round trip property-based tests for Polylines and Polygons (both pass).
+
 VERSION 3.0.11
 
 2026-06-04


=====================================
debian/changelog
=====================================
@@ -1,3 +1,10 @@
+pyshp (3.0.12-1) unstable; urgency=medium
+
+  * Team upload.
+  * New upstream release.
+
+ -- Bas Couwenberg <sebastic at debian.org>  Sat, 06 Jun 2026 06:26:56 +0200
+
 pyshp (3.0.11-1) unstable; urgency=medium
 
   * Team upload.


=====================================
src/shapefile.py
=====================================
@@ -8,7 +8,7 @@ Compatible with Python versions >=3.9
 
 from __future__ import annotations
 
-__version__ = "3.0.11"
+__version__ = "3.0.12"
 
 import abc
 import array
@@ -133,7 +133,6 @@ class BBox(NamedTuple):
     ymin: float
     xmax: float
     ymax: float
-    # = tuple[float, float, float, float]
 
 
 def _min_not_None(m1: float | None, m2: float | None) -> float | None:
@@ -162,13 +161,10 @@ class MBox(NamedTuple):
             _max_not_None(self.mmax, other.mmax),
         )
 
-    # = tuple[float, float]
-
 
 class ZBox(NamedTuple):
     zmin: float
     zmax: float
-    # = tuple[float, float]
 
 
 class WriteableBinStream(Protocol):
@@ -720,6 +716,35 @@ def _z_from_point(point: PointT) -> float:
     return 0.0
 
 
+def _with_polygon_rings_closed(
+    parts: Iterable[PointsT],
+) -> list[PointsT]:
+    return [part if part[0] == part[-1] else part + [part[0]] for part in parts]
+
+
+def _points_and_part_indices(
+    parts: list[PointsT],
+) -> tuple[PointsT, list[int]]:
+    # Intended for Union[Polyline, Polygon, MultiPoint, MultiPatch]
+    """From a list of parts (each part a list of points) return
+    a flattened list of points, and a list of indexes into that
+    flattened list corresponding to the start of each part.
+
+    Internal method for both multipoints (formed entirely by a single part),
+    and shapes that have multiple collections of points (each one
+    a part): (poly)lines, polygons, and multipatchs.
+    """
+    part_indexes: list[int] = []
+    points: PointsT = []
+
+    for part in parts:
+        # set part index position
+        part_indexes.append(len(points))
+        points.extend(part)
+
+    return points, part_indexes
+
+
 class CanHaveBboxNoLinesKwargs(TypedDict, total=False):
     oid: int | None
     points: PointsT | None
@@ -825,21 +850,17 @@ class Shape:
 
         if lines is not None:
             if self.shapeType in Polygon_shapeTypes:
-                lines = list(lines)
-                self._ensure_polygon_rings_closed(lines)
+                lines = _with_polygon_rings_closed(lines)
 
-            default_points, default_parts = self._points_and_parts_indexes_from_lines(
-                lines
-            )
-        elif points and self.shapeType in _CanHaveBBox_shapeTypes:
+            default_points, default_parts = _points_and_part_indices(lines)
+
+        elif not parts and self.shapeType in _CanHaveBBox_shapeTypes:
             # TODO:  Raise issue.
             # This ensures Polylines, Polygons and Multipatches with no part information are a single
             # Polyline, Polygon or Multipatch respectively.
             #
-            # However this also allows MultiPoints shapes to have a single part index 0 as
-            # documented in README.md,also when set from points
-            # (even though this is just an artefact of initialising them as a length-1 nested
-            # list of points via _points_and_parts_indexes_from_lines).
+            # This is consistent with  MultiPoints shapes having single part index 0 as
+            # documented in README.md, also when set from points
             #
             # Alternatively single points could be given parts = [0] too, as they do if formed
             # _from_geojson.
@@ -848,7 +869,7 @@ class Shape:
         # PyShp 2 API compatibility requires self.points = []
         # on NullShapes (and self.parts = []).
         self.points: PointsT = points or default_points
-        self.parts: Sequence[int] = parts or default_parts
+        self.parts = _Array[int]("i", parts or default_parts)
 
         # and a dict to record any captured errors encountered in GeoJSON
         self._errors: dict[str, int] = {}
@@ -900,43 +921,23 @@ class Shape:
     def shapeTypeName(self) -> str:
         return SHAPETYPE_LOOKUP[self.shapeType]
 
+    @property
+    def points_2D(self) -> list[Point2D]:
+        return [(x, y) for (x, y, *_rest) in self.points]
+
+    @property
+    def points_3D(self) -> list[Point3D]:
+        zs = getattr(self, "z", None)
+        if zs is None:
+            return [(x, y, _z_from_point((x, y))) for (x, y, *_rest) in self.points]
+        return [(x, y, z) for (x, y, *_rest), z in zip(self.points, zs)]
+
     def __repr__(self) -> str:
         class_name = self.__class__.__name__
         if class_name == "Shape":
             return f"Shape #{self.__oid}: {self.shapeTypeName}"
         return f"{class_name} #{self.__oid}"
 
-    @staticmethod
-    def _ensure_polygon_rings_closed(
-        parts: list[PointsT],  # Mutated
-    ) -> None:
-        for part in parts:
-            if part[0] != part[-1]:
-                part.append(part[0])
-
-    @staticmethod
-    def _points_and_parts_indexes_from_lines(
-        parts: list[PointsT],
-    ) -> tuple[PointsT, list[int]]:
-        # Intended for Union[Polyline, Polygon, MultiPoint, MultiPatch]
-        """From a list of parts (each part a list of points) return
-        a flattened list of points, and a list of indexes into that
-        flattened list corresponding to the start of each part.
-
-        Internal method for both multipoints (formed entirely by a single part),
-        and shapes that have multiple collections of points (each one
-        a part): (poly)lines, polygons, and multipatchs.
-        """
-        part_indexes: list[int] = []
-        points: PointsT = []
-
-        for part in parts:
-            # set part index position
-            part_indexes.append(len(points))
-            points.extend(part)
-
-        return points, part_indexes
-
     def _bbox_from_points(self) -> BBox:
         xs: list[float] = []
         ys: list[float] = []


=====================================
tests/hypothesis_tests.py
=====================================
@@ -72,7 +72,8 @@ def test_Point_2D_roundtrips(
         bbox=None,
     )
     assert isinstance(actual, shp.Point)
-    assert actual.points == expected.points
+    assert actual.points_2D == expected.points_2D
+
     assert actual.oid == expected.oid
 
 
@@ -94,7 +95,8 @@ def test_PointM_roundtrips(
         bbox=None,
     )
     assert isinstance(actual, shp.PointM)
-    assert actual.points == expected.points
+    assert actual.points_2D == expected.points_2D
+
     assert actual.m == expected.m
     assert actual.oid == expected.oid
 
@@ -117,7 +119,8 @@ def test_PointZ_roundtrips(
         bbox=None,
     )
     assert isinstance(actual, shp.PointM)
-    assert actual.points == expected.points
+    assert actual.points_3D == expected.points_3D
+
     assert actual.z == expected.z
     assert actual.m == expected.m
     assert actual.oid == expected.oid
@@ -144,7 +147,8 @@ def test_MultiPoint_roundtrips(
         bbox=None,
     )
     assert isinstance(actual, shp.MultiPoint)
-    assert actual.points == expected.points
+    assert actual.points_2D == expected.points_2D
+
     assert actual.oid == expected.oid
 
 
@@ -177,7 +181,8 @@ def test_MultiPointM_roundtrips(
         bbox=None,
     )
     assert isinstance(actual, shp.MultiPointM)
-    assert actual.points == expected.points
+    assert actual.points_2D == expected.points_2D
+
     assert actual.m == expected.m
     assert actual.oid == expected.oid
 
@@ -212,7 +217,165 @@ def test_MultiPointZ_roundtrips(
         bbox=None,
     )
     assert isinstance(actual, shp.MultiPointZ)
-    assert actual.points == expected.points
+    assert actual.points_3D == expected.points_3D
+
     assert actual.m == expected.m, f"{type(actual.m)=}, {type(expected.m)=}"
     assert actual.z == expected.z,  f"{type(actual.z)=}, {type(expected.z)=}"
     assert actual.oid == expected.oid
+
+polyline = builds(shp.Polyline, lines=lists(lists(tuples(xs, ys), min_size=1), min_size=1), oid=oid)
+polylinem = builds(shp.PolylineM, lines=lists(lists(tuples(xs, ys, ms), min_size=1), min_size=1), oid=oid)
+polylinez = builds(shp.PolylineZ, lines=lists(lists(tuples(xs, ys, zs, ms), min_size=1), min_size=1), oid=oid)
+
+ at pytest.mark.hypothesis
+ at given(expected=polyline, i=integers(min_value=1))
+def test_Polyline_roundtrips(
+    expected: shp.Polyline,
+    i: int,
+) -> None:
+    stream = io.BytesIO()
+    n = shp.Polyline.write_to_byte_stream(b_io=stream, s=expected, i=i)
+    assert n == stream.tell()
+    stream.seek(0)
+    actual = shp.Polyline.from_byte_stream(
+        shapeType=shp.POLYLINE,
+        b_io=stream,
+        next_shape_pos=n,
+        oid=expected.oid,
+        bbox=None,
+    )
+    assert isinstance(actual, shp.Polyline)
+    assert actual.points_2D == expected.points_2D
+
+    assert actual.parts == expected.parts, f"{type(actual.parts)=}, {type(expected.parts)=}"
+    assert actual.oid == expected.oid
+
+ at pytest.mark.hypothesis
+ at settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
+ at given(expected=polylinem, i=integers(min_value=1))
+def test_PolylineM_roundtrips(
+    expected: shp.PolylineM,
+    i: int,
+) -> None:
+    stream = io.BytesIO()
+    n = shp.PolylineM.write_to_byte_stream(b_io=stream, s=expected, i=i)
+    assert n == stream.tell()
+    stream.seek(0)
+    actual = shp.PolylineM.from_byte_stream(
+        shapeType=shp.POLYLINEM,
+        b_io=stream,
+        next_shape_pos=n,
+        oid=expected.oid,
+        bbox=None,
+    )
+    assert isinstance(actual, shp.PolylineM)
+    assert actual.points_2D == expected.points_2D
+
+    assert actual.parts == expected.parts, f"{type(actual.parts)=}, {type(expected.parts)=}"
+    assert actual.m == expected.m, f"{type(actual.m)=}, {type(expected.m)=}"
+    assert actual.oid == expected.oid
+
+ at pytest.mark.hypothesis
+ at settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
+ at given(expected=polylinez, i=integers(min_value=1))
+def test_PolylineZ_roundtrips(
+    expected: shp.PolylineZ,
+    i: int,
+) -> None:
+    stream = io.BytesIO()
+    n = shp.PolylineZ.write_to_byte_stream(b_io=stream, s=expected, i=i)
+    assert n == stream.tell()
+    stream.seek(0)
+    actual = shp.PolylineZ.from_byte_stream(
+        shapeType=shp.POLYLINEZ,
+        b_io=stream,
+        next_shape_pos=n,
+        oid=expected.oid,
+        bbox=None,
+    )
+    assert isinstance(actual, shp.PolylineZ)
+    assert actual.points_3D == expected.points_3D
+
+    assert actual.parts == expected.parts, f"{type(actual.parts)=}, {type(expected.parts)=}"
+    assert actual.m == expected.m, f"{type(actual.m)=}, {type(expected.m)=}"
+    assert actual.z == expected.z,  f"{type(actual.z)=}, {type(expected.z)=}"
+    assert actual.oid == expected.oid
+
+# Relies on Shape._ensure_polygon_rings_closed to close the Polygons
+polygon = builds(shp.Polygon, lines=lists(lists(tuples(xs, ys), min_size=1), min_size=1), oid=oid)
+polygonm = builds(shp.PolygonM, lines=lists(lists(tuples(xs, ys, ms), min_size=1), min_size=1), oid=oid)
+polygonz = builds(shp.PolygonZ, lines=lists(lists(tuples(xs, ys, zs, ms), min_size=1), min_size=1), oid=oid)
+
+ at pytest.mark.hypothesis
+ at given(expected=polygon, i=integers(min_value=1))
+def test_Polygon_roundtrips(
+    expected: shp.Polygon,
+    i: int,
+) -> None:
+    stream = io.BytesIO()
+    n = shp.Polygon.write_to_byte_stream(b_io=stream, s=expected, i=i)
+    assert n == stream.tell()
+    stream.seek(0)
+    actual = shp.Polygon.from_byte_stream(
+        shapeType=shp.POLYGON,
+        b_io=stream,
+        next_shape_pos=n,
+        oid=expected.oid,
+        bbox=None,
+    )
+    assert isinstance(actual, shp.Polygon)
+    assert actual.points_2D == expected.points_2D
+
+    assert actual.parts == expected.parts, f"{type(actual.parts)=}, {type(expected.parts)=}"
+    assert actual.oid == expected.oid
+
+ at pytest.mark.hypothesis
+ at settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
+ at given(expected=polygonm, i=integers(min_value=1))
+def test_PolygonM_roundtrips(
+    expected: shp.PolygonM,
+    i: int,
+) -> None:
+    stream = io.BytesIO()
+    n = shp.PolygonM.write_to_byte_stream(b_io=stream, s=expected, i=i)
+    assert n == stream.tell()
+    stream.seek(0)
+    actual = shp.PolygonM.from_byte_stream(
+        shapeType=shp.POLYGONM,
+        b_io=stream,
+        next_shape_pos=n,
+        oid=expected.oid,
+        bbox=None,
+    )
+    assert isinstance(actual, shp.PolygonM)
+    assert actual.points_2D == expected.points_2D
+
+    assert actual.parts == expected.parts, f"{type(actual.parts)=}, {type(expected.parts)=}"
+    assert actual.m == expected.m, f"{type(actual.m)=}, {type(expected.m)=}"
+    assert actual.oid == expected.oid
+
+ at pytest.mark.hypothesis
+ at settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
+ at given(expected=polygonz, i=integers(min_value=1))
+def test_PolygonZ_roundtrips(
+    expected: shp.PolygonZ,
+    i: int,
+) -> None:
+    stream = io.BytesIO()
+    n = shp.PolygonZ.write_to_byte_stream(b_io=stream, s=expected, i=i)
+    assert n == stream.tell()
+    stream.seek(0)
+    actual = shp.PolygonZ.from_byte_stream(
+        shapeType=shp.POLYGONZ,
+        b_io=stream,
+        next_shape_pos=n,
+        oid=expected.oid,
+        bbox=None,
+    )
+    assert isinstance(actual, shp.PolygonZ)
+    assert actual.points_3D == expected.points_3D
+
+    assert actual.parts == expected.parts, f"{type(actual.parts)=}, {type(expected.parts)=}"
+    assert actual.m == expected.m, f"{type(actual.m)=}, {type(expected.m)=}"
+    assert actual.z == expected.z,  f"{type(actual.z)=}, {type(expected.z)=}"
+    assert actual.oid == expected.oid
\ No newline at end of file



View it on GitLab: https://salsa.debian.org/debian-gis-team/pyshp/-/compare/eb9cd16ed5443e8520d46b4e63019512f20bc896...f8d1a3b11e1156b0d9bc7fddb4a64ac7da4aa3d0

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/pyshp/-/compare/eb9cd16ed5443e8520d46b4e63019512f20bc896...f8d1a3b11e1156b0d9bc7fddb4a64ac7da4aa3d0
You're receiving this email because of your account on salsa.debian.org. Manage all notifications: https://salsa.debian.org/-/profile/notifications | Help: https://salsa.debian.org/help


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20260606/927b8f4a/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list