[Git][debian-gis-team/pyshp][upstream] New upstream version 3.0.12
Bas Couwenberg (@sebastic)
gitlab at salsa.debian.org
Sat Jun 6 05:31:45 BST 2026
Bas Couwenberg pushed to branch upstream at Debian GIS Project / pyshp
Commits:
be019752 by Bas Couwenberg at 2026-06-06T06:26:00+02:00
New upstream version 3.0.12
- - - - -
4 changed files:
- README.md
- changelog.txt
- 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
=====================================
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/-/commit/be019752ee9a03d59828d75af6d23c1880b2ad3d
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/pyshp/-/commit/be019752ee9a03d59828d75af6d23c1880b2ad3d
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/2a59f109/attachment-0001.htm>
More information about the Pkg-grass-devel
mailing list