[Git][debian-gis-team/pyshp][upstream] New upstream version 3.0.13

Bas Couwenberg (@sebastic) gitlab at salsa.debian.org
Sat Jun 20 04:50:49 BST 2026



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


Commits:
2bbf00cf by Bas Couwenberg at 2026-06-20T05:46:40+02:00
New upstream version 3.0.13
- - - - -


4 changed files:

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


Changes:

=====================================
README.md
=====================================
@@ -8,8 +8,8 @@ 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.12
-- **Date**: 5th June 2026
+- **Version**: 3.0.13
+- **Date**: 19th June 2026
 - **License**: [MIT](https://github.com/GeospatialPython/pyshp/blob/master/LICENSE.TXT)
 
 ## Contents
@@ -93,6 +93,12 @@ part of your geospatial project.
 
 # Version Changes
 
+## 3.0.13
+### Bug fix
+ - Fix bug when reading empty shp files.
+### Testing
+ - Add round trip tests for Multipatch and shp files (both passed).
+
 ## 3.0.12
 ### Data consistency
  - Add Shape.points_2D and Shape.points_3D properties - lists of guaranteed length tuples (2 and 3 respectively).


=====================================
changelog.txt
=====================================
@@ -1,3 +1,13 @@
+VERSION 3.0.13
+
+2026-06-19
+	Bug fix
+	* Fix bug when reading empty shp files.
+
+	Testing
+	* Add round trip tests for Multipatch and shp files (both passed).
+
+
 VERSION 3.0.12
 
 2026-06-05


=====================================
src/shapefile.py
=====================================
@@ -8,7 +8,7 @@ Compatible with Python versions >=3.9
 
 from __future__ import annotations
 
-__version__ = "3.0.12"
+__version__ = "3.0.13"
 
 import abc
 import array
@@ -3141,6 +3141,8 @@ class ShpReader(_HasCheckedReadableFile):
             # MAYBE: check if more left of file or exit early?
             return
 
+        n = 0  # counter for num of actual shapes found
+
         # No shx file, unknown nr of shapes
         # Instead iterate until reach end of file
         # Calculate the offset indices during iteration
@@ -3149,10 +3151,10 @@ class ShpReader(_HasCheckedReadableFile):
             shape = self._shape(shape_len_B=shape_len_B, oid=i, bbox=bbox)
             # # pos = self.file.tell()
             # pos += num_bytes
+            n += 1
             if shape is not None or outside_bbox_as_None:
                 yield shape
 
-        n = i + 1  # num shapes yielded, having iterated over entire shp file.
         assert n == len(self.headers_cache), f"{n=}, {len(self.headers_cache)=}"
 
 


=====================================
tests/hypothesis_tests.py
=====================================
@@ -1,12 +1,13 @@
 from __future__ import annotations
 
 import io
+import itertools
 
 import pytest
 from hypothesis import HealthCheck, given, settings
 from hypothesis.strategies import (
     builds,
-    composite,
+    composite, # Preferably avoid.  Shrinking composite strategies is slow.
     floats,
     integers,
     just,
@@ -14,6 +15,7 @@ from hypothesis.strategies import (
     none,
     one_of,
     tuples,
+    sampled_from,
 )
 
 import shapefile as shp
@@ -26,14 +28,14 @@ zs = one_of(just(0.0), float_nums)
 PointsLengths = integers(min_value=1, max_value=8000)  # length of points
 oid = one_of(none(), integers(min_value=0))
 point_2D = builds(shp.Point, x=xs, y=ys, oid=oid)
-pointM = builds(
+pointm = builds(
     shp.PointM,
     x=xs,
     y=ys,
     m=ms,
     oid=oid,
 )
-pointZ = builds(
+pointz = builds(
     shp.PointZ,
     x=xs,
     y=ys,
@@ -78,7 +80,7 @@ def test_Point_2D_roundtrips(
 
 
 @pytest.mark.hypothesis
- at given(expected=pointM, i=integers(min_value=1))
+ at given(expected=pointm, i=integers(min_value=1))
 def test_PointM_roundtrips(
     expected: shp.Point,
     i: int,
@@ -102,7 +104,7 @@ def test_PointM_roundtrips(
 
 
 @pytest.mark.hypothesis
- at given(expected=pointZ, i=integers(min_value=1))
+ at given(expected=pointz, i=integers(min_value=1))
 def test_PointZ_roundtrips(
     expected: shp.Point,
     i: int,
@@ -152,19 +154,17 @@ def test_MultiPoint_roundtrips(
     assert actual.oid == expected.oid
 
 
- at composite
-def multipointM(draw):
-    N = draw(PointsLengths)
-    return shp.MultiPointM(
-        points=draw(coords_2D_list(min_size=N, max_size=N)),
-        m=draw(lists(ms, min_size=N, max_size=N)),
-        oid=oid,
-    )
 
+def multipointM_from_xyms(point_ms: tuple[float, float, float | None], oid_: int) -> shp.MultiPointM:
+    x_vals, y_vals, m_vals = zip(*point_ms)
+    xy_vals = zip(x_vals, y_vals)
+    return shp.MultiPointM(points=list(xy_vals), m=list(m_vals), oid=oid_)
+
+multipointm = builds(multipointM_from_xyms, lists(tuples(xs, ys, ms), min_size=1), oid)
 
 @pytest.mark.hypothesis
- at settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
- at given(expected=multipointM(), i=integers(min_value=1))
+# @settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
+ at given(expected=multipointm, i=integers(min_value=1))
 def test_MultiPointM_roundtrips(
     expected: shp.MultiPointM,
     i: int,
@@ -187,20 +187,17 @@ def test_MultiPointM_roundtrips(
     assert actual.oid == expected.oid
 
 
- at composite
-def multipointZ(draw):
-    N = draw(PointsLengths)
-    return shp.MultiPointZ(
-        points=draw(coords_2D_list(min_size=N, max_size=N)),
-        z=draw(lists(zs, min_size=N, max_size=N)),
-        m=draw(lists(ms, min_size=N, max_size=N)),
-        oid=oid,
-    )
+def multipointZ_from_xyzms(pointz_ms: tuple[float, float, float, float | None], oid_: int) -> shp.MultiPointZ:
+    x_vals, y_vals, z_vals, m_vals = zip(*pointz_ms)
+    xy_vals = zip(x_vals, y_vals)
+    return shp.MultiPointZ(points=list(xy_vals), z=list(z_vals), m=list(m_vals), oid=oid_)
+
+multipointz = builds(multipointZ_from_xyzms, lists(tuples(xs, ys, zs, ms), min_size=1), oid)
 
 
 @pytest.mark.hypothesis
- at settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
- at given(expected=multipointZ(), i=integers(min_value=1))
+# @settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
+ at given(expected=multipointz, i=integers(min_value=1))
 def test_MultiPointZ_roundtrips(
     expected: shp.MultiPointZ,
     i: int,
@@ -251,7 +248,7 @@ def test_Polyline_roundtrips(
     assert actual.oid == expected.oid
 
 @pytest.mark.hypothesis
- at settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
+# @settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
 @given(expected=polylinem, i=integers(min_value=1))
 def test_PolylineM_roundtrips(
     expected: shp.PolylineM,
@@ -276,7 +273,7 @@ def test_PolylineM_roundtrips(
     assert actual.oid == expected.oid
 
 @pytest.mark.hypothesis
- at settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
+# @settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
 @given(expected=polylinez, i=integers(min_value=1))
 def test_PolylineZ_roundtrips(
     expected: shp.PolylineZ,
@@ -330,7 +327,7 @@ def test_Polygon_roundtrips(
     assert actual.oid == expected.oid
 
 @pytest.mark.hypothesis
- at settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
+# @settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
 @given(expected=polygonm, i=integers(min_value=1))
 def test_PolygonM_roundtrips(
     expected: shp.PolygonM,
@@ -355,7 +352,7 @@ def test_PolygonM_roundtrips(
     assert actual.oid == expected.oid
 
 @pytest.mark.hypothesis
- at settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
+# @settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
 @given(expected=polygonz, i=integers(min_value=1))
 def test_PolygonZ_roundtrips(
     expected: shp.PolygonZ,
@@ -378,4 +375,111 @@ def test_PolygonZ_roundtrips(
     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
+    assert actual.oid == expected.oid
+
+part_types = sampled_from(range(6)) # 0: Triangle Strip, ..., 5: Ring
+
+def multipatch_from_xyzms_and_types(
+    xyzms_and_types: list[tuple[list[tuple[float, float, float, float | None]], int]],
+    oid: int,
+    ) -> shp.MultiPatch:
+    xyzm_vals, p_types = zip(*xyzms_and_types)
+    return shp.MultiPatch(lines = xyzm_vals, partTypes = p_types, oid=oid)
+
+multipatch = builds(
+    multipatch_from_xyzms_and_types,
+    lists(tuples(lists(tuples(xs, ys, zs, ms), min_size=1), part_types), min_size=1), oid)
+
+
+ at pytest.mark.hypothesis
+# @settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
+ at given(expected=multipatch, i=integers(min_value=1))
+def test_MultiPatch_roundtrips(
+    expected: shp.MultiPatch,
+    i: int,
+) -> None:
+    stream = io.BytesIO()
+    n = shp.MultiPatch.write_to_byte_stream(b_io=stream, s=expected, i=i)
+    assert n == stream.tell()
+    stream.seek(0)
+    actual = shp.MultiPatch.from_byte_stream(
+        shapeType=shp.MULTIPATCH,
+        b_io=stream,
+        next_shape_pos=n,
+        oid=expected.oid,
+        bbox=None,
+    )
+    assert isinstance(actual, shp.MultiPatch)
+    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
+    assert actual.partTypes == expected.partTypes, f"{type(actual.partTypes)=}, {type(expected.partTypes)=}"
+
+
+shape_codes_names_and_strategies = [
+# (0, "Null Shape"),
+(1, "Point", point_2D),
+(3, "PolyLine", polyline),
+(5, "Polygon", polygon),
+(8, "MultiPoint", multipoint),
+(11, "PointZ", pointz),
+(13, "PolyLineZ", polylinez),
+(15, "PolygonZ", polygonz),
+(18, "MultiPointZ", multipointz),
+(21, "PointM", pointm),
+(23, "PolyLineM", polylinem),
+(25, "PolygonM", polygonm),
+(28, "MultiPointM", multipointm),
+(31, "MultiPatch", multipatch),
+]
+
+def code_and_shape_strat_from_triple(t):
+    x, _name, shapes  = t
+    return tuples(just(x), lists(shapes, min_size = 0))  # Empty shp files are in the esri spec.
+
+codes_and_shapes_strats = [
+    code_and_shape_strat_from_triple(t)
+    for t in shape_codes_names_and_strategies
+]
+
+codes_and_shapes = one_of(codes_and_shapes_strats)
+
+ at pytest.mark.hypothesis
+# @settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
+ at given(codes_and_shapes=codes_and_shapes)
+def test_shp_reader_writer_roundtrip(codes_and_shapes)-> None:
+    code_ex, expected_shapes = codes_and_shapes
+    stream = io.BytesIO()
+    with shp.ShpWriter(shp=stream, shapeType=code_ex) as w:
+        for shape in expected_shapes:
+            w.shape(shape)
+    stream.seek(0)
+    with shp.ShpReader(shp=stream) as r:
+        assert r.shapeType == code_ex
+
+        for actual, expected in itertools.zip_longest(r.shapes(), expected_shapes):
+
+            assert isinstance(actual, shp.SHAPE_CLASS_FROM_SHAPETYPE[code_ex])
+            assert actual.points_3D == expected.points_3D
+            # Don't assert actual.oid == expected.oid it's defined by
+            # actual.oid indicates the order actual was written in, expected.oid
+            # is not currently encoded (as we'd have to resort the entire Shapefile after each shape)
+            assert actual.parts == expected.parts, f"{type(actual.parts)=}, {type(expected.parts)=}"
+
+            if (m := getattr(actual, "m", None)):
+                assert m == expected.m, f"{type(m)=}, {type(expected.m)=}"
+            else:
+                assert not hasattr(expected, "m")
+
+            if (z := getattr(actual, "z", None)):
+                assert z == expected.z, f"{type(z)=}, {type(expected.z)=}"
+            else:
+                assert not hasattr(expected, "z")
+
+            if (partTypes := getattr(actual, "partTypes", None)):
+                assert actual.partTypes == expected.partTypes, f"{type(actual.partTypes)=}, {type(expected.partTypes)=}"
+            else:
+                assert not hasattr(expected, "partTypes")



View it on GitLab: https://salsa.debian.org/debian-gis-team/pyshp/-/commit/2bbf00cf8216ed6802bc3db3b128c52719950575

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/pyshp/-/commit/2bbf00cf8216ed6802bc3db3b128c52719950575
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/20260620/21aab5e2/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list