Skip to content

Commit d922eee

Browse files
committed
v3.0.10 Bugfix. Ensure m < ISDATA becomes None on PointM. Add round tri…
Update hypothesis_tests.py
1 parent d02303b commit d922eee

7 files changed

Lines changed: 169 additions & 16 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,7 @@ repos:
1616
entry: uv run mypy --strict .
1717
language: system
1818
pass_filenames: false
19-
# - repo: https://github.com/astral-sh/ruff-pre-commit
20-
# rev: v0.15.13
21-
# hooks:
22-
# # Run the linter
23-
# - id: ruff-check
24-
# args: [ --fix ]
25-
# # Run the formatter
26-
# - id: ruff-format
19+
2720
- repo: https://github.com/pre-commit/pre-commit-hooks
2821
rev: v6.0.0
2922
hooks:

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ The Python Shapefile Library (PyShp) reads and writes ESRI Shapefiles in pure Py
88

99
- **Author**: [Joel Lawhead](https://github.com/GeospatialPython)
1010
- **Maintainers**: [James Parrott](https://github.com/JamesParrott) & [Karim Bahgat](https://github.com/karimbahgat)
11-
- **Version**: 3.0.9
12-
- **Date**: 27th May 2026
11+
- **Version**: 3.0.10
12+
- **Date**: 4th June 2026
1313
- **License**: [MIT](https://github.com/GeospatialPython/pyshp/blob/master/LICENSE.TXT)
1414

1515
## Contents
@@ -93,6 +93,14 @@ part of your geospatial project.
9393

9494
# Version Changes
9595

96+
## 3.0.10
97+
### Bug fix
98+
- Convert directly supplied m values to None if they are strictly below ISDATA_LOWER_BOUND (-1e38).
99+
### Testing
100+
- Move tests into ./tests.
101+
- Remove doctest runner from user land.
102+
- Add round trip property tests for Point and PointM using hypothesis.
103+
96104
## 3.0.9
97105
### Testing
98106
- Try to make tests not rely on downloads from Github repo URLs, to avoid 404s & 426s due to rate limits.

changelog.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
VERSION 3.0.10
2+
3+
2026-06-04
4+
Bug fix
5+
* Convert directly supplied m values strictly below ISDATA_LOWER_BOUND (-1e38) to None.
6+
Testing
7+
* Move tests into ./tests.
8+
* Remove doctest runner from user land.
9+
* Add round trip property tests for Point and PointM using hypothesis.
10+
111
VERSION 3.0.9
212

313
2026-05-27

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@ lint = [
4242
"ruff",
4343
]
4444
test = [
45-
"pytest",
45+
"pytest", "hypothesis"
4646
]
4747

48+
4849
[project.urls]
4950
Repository = "https://github.com/GeospatialPython/pyshp"
5051

@@ -74,7 +75,9 @@ path = "src/shapefile.py"
7475
markers = [
7576
"network: marks tests requiring network access",
7677
"slow: marks other tests that cause bottlenecks",
78+
"hypothesis: tests that require hypothesis",
7779
]
80+
python_files = "test_*.py *_test.py *_tests.py"
7881

7982
[tool.ruff]
8083
# Exclude a variety of commonly ignored directories.

src/shapefile.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from __future__ import annotations
1010

11-
__version__ = "3.0.9.dev"
11+
__version__ = "3.0.10"
1212

1313
import abc
1414
import array
@@ -701,9 +701,16 @@ class _NoShapeTypeSentinel:
701701
"""
702702

703703

704+
def _ensure_within_bounds(m: float | None) -> float | None:
705+
if m is not None and m >= ISDATA_LOWER_BOUND:
706+
return m
707+
return None
708+
709+
704710
def _m_from_point(point: PointT, i_m: int) -> float | None:
705711
if 2 <= i_m < len(point):
706-
return point[i_m]
712+
m = point[i_m]
713+
return _ensure_within_bounds(m)
707714
return None
708715

709716

@@ -810,7 +817,7 @@ def __init__(
810817

811818
ms_found = True
812819
if m:
813-
self.m: Sequence[float | None] = m
820+
self.m: Sequence[float | None] = [_ensure_within_bounds(x) for x in m]
814821
elif self.shapeType in _HasM_shapeTypes:
815822
i_m = 3 if self.shapeType in _HasZ_shapeTypes | PointZ_shapeTypes else 2
816823
self.m = [_m_from_point(p, i_m) for p in self.points]
@@ -3020,7 +3027,11 @@ def _shape(
30203027

30213028
ShapeClass = SHAPE_CLASS_FROM_SHAPETYPE[shapeType]
30223029
shape = ShapeClass.from_byte_stream(
3023-
shapeType, b_io, shape_len_B, oid=oid, bbox=bbox
3030+
shapeType=shapeType,
3031+
b_io=b_io,
3032+
next_shape_pos=shape_len_B,
3033+
oid=oid,
3034+
bbox=bbox,
30243035
)
30253036

30263037
# Seek to the end of this record as defined by the record header because

tests/hypothesis_tests.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import io
2+
3+
import pytest
4+
from hypothesis import given
5+
from hypothesis.strategies import (
6+
builds,
7+
floats,
8+
integers,
9+
none,
10+
one_of,
11+
)
12+
13+
import shapefile as shp
14+
15+
float_nums = floats(allow_nan=False, allow_infinity=False)
16+
17+
points_2D = builds(shp.Point, float_nums, float_nums, one_of(none(), integers()))
18+
pointMs = builds(
19+
shp.PointM,
20+
float_nums,
21+
float_nums,
22+
one_of(none(), float_nums),
23+
one_of(none(), integers()),
24+
)
25+
26+
27+
@pytest.mark.hypothesis
28+
@given(expected=points_2D, i=integers(min_value=1))
29+
def test_Point_2D_roundtrips(
30+
expected: shp.Point,
31+
i: int,
32+
) -> None:
33+
stream = io.BytesIO()
34+
n = shp.Point.write_to_byte_stream(b_io=stream, s=expected, i=i)
35+
assert n == stream.tell()
36+
stream.seek(0)
37+
actual = shp.Point.from_byte_stream(
38+
shapeType=shp.POINT,
39+
b_io=stream,
40+
next_shape_pos=n,
41+
oid=expected.oid,
42+
bbox=None,
43+
)
44+
assert isinstance(actual, shp.Point)
45+
assert actual.points == expected.points
46+
assert actual.oid == expected.oid
47+
48+
49+
@pytest.mark.hypothesis
50+
@given(expected=pointMs, i=integers(min_value=1))
51+
def test_Point_M_roundtrips(
52+
expected: shp.Point,
53+
i: int,
54+
) -> None:
55+
stream = io.BytesIO()
56+
n = shp.PointM.write_to_byte_stream(b_io=stream, s=expected, i=i)
57+
assert n == stream.tell()
58+
stream.seek(0)
59+
actual = shp.PointM.from_byte_stream(
60+
shapeType=shp.POINTM,
61+
b_io=stream,
62+
next_shape_pos=n,
63+
oid=expected.oid,
64+
bbox=None,
65+
)
66+
assert isinstance(actual, shp.PointM)
67+
assert actual.points == expected.points
68+
assert actual.m == expected.m
69+
assert actual.oid == expected.oid

uv.lock

Lines changed: 60 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)