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

Bas Couwenberg (@sebastic) gitlab at salsa.debian.org
Fri Jun 5 19:14:47 BST 2026



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


Commits:
86e5499c by Bas Couwenberg at 2026-06-05T20:05:28+02:00
New upstream version 3.0.11
- - - - -
bf943c6b by Bas Couwenberg at 2026-06-05T20:05:37+02:00
Update upstream source from tag 'upstream/3.0.11'

Update to upstream version '3.0.11'
with Debian dir c28d828fc144e68e1f92d34864e74f1beb03900b
- - - - -
1b89406a by Bas Couwenberg at 2026-06-05T20:06:17+02:00
New upstream release.

- - - - -
eb9cd16e by Bas Couwenberg at 2026-06-05T20:10:44+02:00
Set distribution to unstable.

- - - - -


9 changed files:

- .github/actions/test/action.yml
- .github/workflows/run_checks_build_and_test.yml
- README.md
- changelog.txt
- debian/changelog
- pyproject.toml
- src/shapefile.py
- tests/hypothesis_tests.py
- tests/run_doctests.py


Changes:

=====================================
.github/actions/test/action.yml
=====================================
@@ -7,7 +7,7 @@ description:
 inputs:
   extra_args:
     description: Extra command line args for Pytest and python shapefile.py
-    default: '-m "not network"'
+    default: '-m "not network and not hypothesis"'
     required: false
   replace_remote_urls_with_localhost:
     description: true or false.  Test loading shapefiles from a url, without overloading an external server from 30 parallel workflows.
@@ -17,6 +17,11 @@ inputs:
     description: Path to where the PyShp repo was checked out to (to keep separate from Shapefiles & artefacts repo).
     required: false
     default: '.'
+  run_doctests:
+    description: Whether to run the doctests or not.
+    required: false
+    default: 'yes'
+
 
 
 
@@ -81,6 +86,7 @@ runs:
         python -m pip install $WHEEL_NAME --group ../Pyshp/pyproject.toml:test
 
     - name: Doctests
+      if: ${{ inputs.run_doctests == 'yes' }}
       shell: bash
       working-directory: ${{ inputs.pyshp_repo_directory }}
       env:


=====================================
.github/workflows/run_checks_build_and_test.yml
=====================================
@@ -51,7 +51,36 @@ jobs:
     - name: Build wheel from the project repo
       uses: ./.github/actions/build_wheel_and_sdist
 
-  test_on_all_platforms:
+  property-based_tests:
+    needs: build_wheel_and_sdist
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version: [
+          "3.14",
+        ]
+        os: [
+          "ubuntu-24.04",
+        ]
+    runs-on: ${{ matrix.os }}
+    steps:
+    - uses: actions/setup-python at v6
+      with:
+        python-version: ${{ matrix.python-version }}
+
+    - uses: actions/checkout at v6
+      with:
+        path: ./Pyshp
+
+    - name: "Hypothesis tests"
+      if:
+      uses: ./Pyshp/.github/actions/test
+      with:
+        extra_args: '-m hypothesis'
+        run_doctests: 'no'
+        pyshp_repo_directory: ./Pyshp
+
+  Pytest_and_doctests:
     needs: build_wheel_and_sdist
     strategy:
       fail-fast: false
@@ -94,3 +123,4 @@ jobs:
         replace_remote_urls_with_localhost: ${{ !(matrix.os == 'ubuntu-24.04' && matrix.python-version == '3.14') }}
         # Checkout to ./PyShp, as the test job also needs to check out the artefact repo
         pyshp_repo_directory: ./Pyshp
+


=====================================
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.10
-- **Date**: 4th June 2026
+- **Version**: 3.0.11
+- **Date**: 5th June 2026
 - **License**: [MIT](https://github.com/GeospatialPython/pyshp/blob/master/LICENSE.TXT)
 
 ## Contents
@@ -93,6 +93,19 @@ part of your geospatial project.
 
 # Version Changes
 
+## 3.0.11
+### Edge case handling
+ - Raise ShapefileException i) when creating Non-null Shapes without (or with empty) points
+   and ii) when creating Null Shapes with non-empty points.
+ - Ensure Shape.z and Shape.partTypes are _Arrays.
+ - Make Shape stricter about its args, e.g. only points or lines, only one point for Points.
+
+### Bug fixes
+ - Multipoints with only a single point, now have their bbox calculated.
+
+### Testing
+ - Round trip property-based tests for Multipoints (passes).
+
 ## 3.0.10
 ### Bug fix
  - Convert directly supplied m values to None if they are strictly below ISDATA_LOWER_BOUND (-1e38).


=====================================
changelog.txt
=====================================
@@ -1,3 +1,18 @@
+VERSION 3.0.11
+
+2026-06-04
+	Edge case handling
+	* Raise ShapefileException i) when creating Non-null Shapes without (or with empty) points
+	  and ii) when creating Null Shapes with non-empty points.
+	* Ensure Shape.z and Shape.partTypes are _Arrays.
+	* Make Shape stricter about its args, e.g. only points or lines, only one point for Points.
+
+	Bug fixes
+	* Multipoints with only a single point, now have their bbox calculated.
+
+	Testing
+	* Round trip property-based tests for Multipoints (passes).
+
 VERSION 3.0.10
 
 2026-06-04


=====================================
debian/changelog
=====================================
@@ -1,3 +1,10 @@
+pyshp (3.0.11-1) unstable; urgency=medium
+
+  * Team upload.
+  * New upstream release.
+
+ -- Bas Couwenberg <sebastic at debian.org>  Fri, 05 Jun 2026 20:09:19 +0200
+
 pyshp (3.0.10-1) unstable; urgency=medium
 
   * Team upload.


=====================================
pyproject.toml
=====================================
@@ -55,9 +55,7 @@ mypy_path = "src"
 explicit_package_bases = true
 exclude_gitignore = true
 exclude=[  # Mypy requires regexes, not globs:
-    'tests/test_shapefile\.py',
-    'tests/run_benchmarks\.py',
-    'tests/run_doctests\.py',
+    'tests/.*\.py',
 ]
 
 
@@ -108,6 +106,7 @@ exclude = [
     "node_modules",
     "site-packages",
     "venv",
+    "tests",
 ]
 
 # Same as Black.


=====================================
src/shapefile.py
=====================================
@@ -8,7 +8,7 @@ Compatible with Python versions >=3.9
 
 from __future__ import annotations
 
-__version__ = "3.0.10"
+__version__ = "3.0.11"
 
 import abc
 import array
@@ -773,11 +773,56 @@ class Shape:
             self.shapeType = shapeType
 
         if partTypes is not None:
-            self.partTypes = partTypes
+            if self.shapeType != MULTIPATCH:
+                raise ShapefileException(
+                    f"Only a Multipatch shape supports partTypes, not: {self.__class__.__name__} "
+                    f" (shape type: {self.shapeTypeName}) "
+                    f"Got: {partTypes=}"
+                )
+            self.partTypes = _Array[int]("i", partTypes)
 
         default_points: PointsT = []
         default_parts: list[int] = []
 
+        if points and lines:
+            raise ShapefileException(
+                "Constructing meaningful Shapes unambiguously from both "
+                "points and lines is not supported.  Provide one only. "
+                f" Got: {points=} and {lines=}"
+            )
+        elif not points and not lines:
+            if self.shapeType != NULL:
+                raise ShapefileException(
+                    f"Shape: {self.__class__.__name__} or shape type: {self.shapeTypeName} "
+                    "requires non-empty points or non-empty lines."
+                    f" Got: {points=} and {lines=}"
+                )
+        elif self.shapeType == NULL:
+            raise ShapefileException(
+                f"NullShape or shape type: {self.shapeTypeName} "
+                "must have zero points and zero lines (or neither set, or both None). "
+                f" Got: {points=} and {lines=}"
+            )
+        elif self.shapeType in Point_shapeTypes:
+            if not points or len(points) >= 2:
+                raise ShapefileException(
+                    f"Single point Shape: {self.__class__.__name__}, shape type: {self.shapeTypeName} "
+                    "requires one or  points (and possibly a z co-ordinate and m value), not "
+                    f"lines. Got: {points=} and {lines=}"
+                )
+            if lines:
+                raise ShapefileException(
+                    f"Single point shape: {self.__class__.__name__}, shape type: {self.shapeTypeName} "
+                    f"does not support lines. Got: {lines=}"
+                )
+        elif self.shapeType in MultiPoint_shapeTypes and lines and len(lines) >= 2:
+            raise ShapefileException(
+                f"Multipoint shape: {self.__class__.__name__}, shape type: {self.shapeTypeName} "
+                f"is a single part shape, but was given multiple parts - got {lines=}. "
+                "Point clouds can be constructed from a list of list points supplied to lines "
+                "(instead of points) but only one single 'line' is supported. "
+            )
+
         if lines is not None:
             if self.shapeType in Polygon_shapeTypes:
                 lines = list(lines)
@@ -800,20 +845,19 @@ class Shape:
             # _from_geojson.
             default_parts = [0]
 
+        # 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
 
-        # and a dict to silently record any errors encountered in GeoJSON
+        # and a dict to record any captured errors encountered in GeoJSON
         self._errors: dict[str, int] = {}
 
         # add oid
         self.__oid: int = -1 if oid is None else oid
 
-        if bbox is not None:
-            self.bbox: BBox = bbox
-        elif len(self.points) >= 2:
-            self.bbox = self._bbox_from_points()
+        if self.shapeType != NULL and self.shapeType not in Point_shapeTypes:
+            self.bbox: BBox = bbox or self._bbox_from_points()
 
         ms_found = True
         if m:
@@ -829,9 +873,9 @@ class Shape:
 
         zs_found = True
         if z:
-            self.z: Sequence[float] = z
+            self.z: Sequence[float] = _Array[float]("d", z)
         elif self.shapeType in _HasZ_shapeTypes:
-            self.z = [_z_from_point(p) for p in self.points]
+            self.z = _Array[float]("d", (_z_from_point(p) for p in self.points))
         elif self.shapeType == POINTZ:
             self.z = (_z_from_point(self.points[0]),)
         else:
@@ -847,6 +891,21 @@ class Shape:
         elif zs_found:
             self.zbox = self._zbox_from_zs()
 
+    @property
+    def oid(self) -> int:
+        """The index position of the shape in the original shapefile"""
+        return self.__oid
+
+    @property
+    def shapeTypeName(self) -> str:
+        return SHAPETYPE_LOOKUP[self.shapeType]
+
+    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
@@ -1080,21 +1139,6 @@ still included but were encoded as GeoJSON exterior rings instead of holes."
                     index += len(ext_or_hole)
         return Shape(shapeType=shapeType, points=points, parts=parts)
 
-    @property
-    def oid(self) -> int:
-        """The index position of the shape in the original shapefile"""
-        return self.__oid
-
-    @property
-    def shapeTypeName(self) -> str:
-        return SHAPETYPE_LOOKUP[self.shapeType]
-
-    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}"
-
 
 # Need unused arguments to keep the same call signature for
 # different implementations of from_byte_stream and write_to_byte_stream
@@ -1116,6 +1160,11 @@ class NullShape(Shape):
         oid: int | None = None,
         bbox: BBox | None = None,
     ) -> NullShape:
+        """In the ESRI spec, Null shapes are defined in .shp files
+        entirely by a single integer encoding shape type 0
+        (this happens in ShpWriter._shp_record, amongst the shape
+        record header code).
+        """
         # Shape.__init__ sets self.points = points or []
         return NullShape(oid=oid)
 
@@ -1125,6 +1174,7 @@ class NullShape(Shape):
         s: Shape,
         i: int,
     ) -> int:
+        """No op (see above)."""
         return 0
 
 
@@ -1602,13 +1652,13 @@ class _HasZ(_CanHaveBBox):
             num_bytes_written = b_io.write(pack("<2d", *zbox))
         except StructError:
             raise ShapefileException(
-                f"Failed to write elevation extremes for record {i}. Expected floats."
+                f"Failed to write elevation extremes (ZBox) for record {i}. Expected floats."
             )
         try:
             num_bytes_written += b_io.write(pack(f"<{len(s.z)}d", *s.z))
         except StructError:
             raise ShapefileException(
-                f"Failed to write elevation values for record {i}. Expected floats."
+                f"Failed to write elevation values (z) for record {i}. Expected floats."
             )
 
         return num_bytes_written


=====================================
tests/hypothesis_tests.py
=====================================
@@ -1,31 +1,61 @@
+from __future__ import annotations
+
 import io
 
 import pytest
-from hypothesis import given
+from hypothesis import HealthCheck, given, settings
 from hypothesis.strategies import (
     builds,
+    composite,
     floats,
     integers,
+    just,
+    lists,
     none,
     one_of,
+    tuples,
 )
 
 import shapefile as shp
 
 float_nums = floats(allow_nan=False, allow_infinity=False)
-
-points_2D = builds(shp.Point, float_nums, float_nums, one_of(none(), integers()))
-pointMs = builds(
+xs = float_nums
+ys = float_nums
+ms = one_of(none(), float_nums)
+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(
     shp.PointM,
-    float_nums,
-    float_nums,
-    one_of(none(), float_nums),
-    one_of(none(), integers()),
+    x=xs,
+    y=ys,
+    m=ms,
+    oid=oid,
+)
+pointZ = builds(
+    shp.PointZ,
+    x=xs,
+    y=ys,
+    z=zs,
+    m=ms,
+    oid=oid,
 )
 
 
+def coords_2D_list(
+    min_size: int = 1,
+    max_size: int | None = None,
+):
+    return lists(
+        tuples(xs, ys),
+        min_size=min_size,
+        max_size=max_size,
+    )
+
+
 @pytest.mark.hypothesis
- at given(expected=points_2D, i=integers(min_value=1))
+ at given(expected=point_2D, i=integers(min_value=1))
 def test_Point_2D_roundtrips(
     expected: shp.Point,
     i: int,
@@ -47,8 +77,8 @@ def test_Point_2D_roundtrips(
 
 
 @pytest.mark.hypothesis
- at given(expected=pointMs, i=integers(min_value=1))
-def test_Point_M_roundtrips(
+ at given(expected=pointM, i=integers(min_value=1))
+def test_PointM_roundtrips(
     expected: shp.Point,
     i: int,
 ) -> None:
@@ -67,3 +97,122 @@ def test_Point_M_roundtrips(
     assert actual.points == expected.points
     assert actual.m == expected.m
     assert actual.oid == expected.oid
+
+
+ at pytest.mark.hypothesis
+ at given(expected=pointZ, i=integers(min_value=1))
+def test_PointZ_roundtrips(
+    expected: shp.Point,
+    i: int,
+) -> None:
+    stream = io.BytesIO()
+    n = shp.PointZ.write_to_byte_stream(b_io=stream, s=expected, i=i)
+    assert n == stream.tell()
+    stream.seek(0)
+    actual = shp.PointZ.from_byte_stream(
+        shapeType=shp.POINTZ,
+        b_io=stream,
+        next_shape_pos=n,
+        oid=expected.oid,
+        bbox=None,
+    )
+    assert isinstance(actual, shp.PointM)
+    assert actual.points == expected.points
+    assert actual.z == expected.z
+    assert actual.m == expected.m
+    assert actual.oid == expected.oid
+
+
+multipoint = builds(shp.MultiPoint, points=coords_2D_list())
+
+
+ at pytest.mark.hypothesis
+ at given(expected=multipoint, i=integers(min_value=1))
+def test_MultiPoint_roundtrips(
+    expected: shp.MultiPoint,
+    i: int,
+) -> None:
+    stream = io.BytesIO()
+    n = shp.MultiPoint.write_to_byte_stream(b_io=stream, s=expected, i=i)
+    assert n == stream.tell()
+    stream.seek(0)
+    actual = shp.MultiPoint.from_byte_stream(
+        shapeType=shp.MULTIPOINT,
+        b_io=stream,
+        next_shape_pos=n,
+        oid=expected.oid,
+        bbox=None,
+    )
+    assert isinstance(actual, shp.MultiPoint)
+    assert actual.points == expected.points
+    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,
+    )
+
+
+ at pytest.mark.hypothesis
+ at 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,
+) -> None:
+    stream = io.BytesIO()
+    n = shp.MultiPointM.write_to_byte_stream(b_io=stream, s=expected, i=i)
+    assert n == stream.tell()
+    stream.seek(0)
+    actual = shp.MultiPointM.from_byte_stream(
+        shapeType=shp.MULTIPOINTM,
+        b_io=stream,
+        next_shape_pos=n,
+        oid=expected.oid,
+        bbox=None,
+    )
+    assert isinstance(actual, shp.MultiPointM)
+    assert actual.points == expected.points
+    assert actual.m == expected.m
+    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,
+    )
+
+
+ at pytest.mark.hypothesis
+ at 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,
+) -> None:
+    stream = io.BytesIO()
+    n = shp.MultiPointZ.write_to_byte_stream(b_io=stream, s=expected, i=i)
+    assert n == stream.tell()
+    stream.seek(0)
+    actual = shp.MultiPointZ.from_byte_stream(
+        shapeType=shp.MULTIPOINTZ,
+        b_io=stream,
+        next_shape_pos=n,
+        oid=expected.oid,
+        bbox=None,
+    )
+    assert isinstance(actual, shp.MultiPointZ)
+    assert actual.points == expected.points
+    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


=====================================
tests/run_doctests.py
=====================================
@@ -199,7 +199,7 @@ def main() -> None:
             temp_dir=Path(td).as_posix(),
             readme=namespace.readme,
             include_network=namespace.m.lower().strip() == "network",
-            include_non_network=namespace.m.lower().strip() == "not network",
+            include_non_network="not network" in namespace.m.lower(),
         )
     sys.exit(failure_count)
 



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

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/pyshp/-/compare/2db954515dd8b1cccb110d039a90514e13832470...eb9cd16ed5443e8520d46b4e63019512f20bc896
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/20260605/0cd92b2c/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list