From d922eee3abd1d282bf69bf4b0626fa265eea4184 Mon Sep 17 00:00:00 2001 From: James Parrott <80779630+JamesParrott@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:18:26 +0100 Subject: [PATCH] =?UTF-8?q?v3.0.10=20Bugfix.=20Ensure=20m=20<=20ISDATA=20b?= =?UTF-8?q?ecomes=20None=20on=20PointM.=20Add=20round=20tri=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update hypothesis_tests.py --- .pre-commit-config.yaml | 9 +---- README.md | 12 +++++-- changelog.txt | 10 ++++++ pyproject.toml | 5 ++- src/shapefile.py | 19 ++++++++--- tests/hypothesis_tests.py | 69 +++++++++++++++++++++++++++++++++++++++ uv.lock | 61 +++++++++++++++++++++++++++++++++- 7 files changed, 169 insertions(+), 16 deletions(-) create mode 100644 tests/hypothesis_tests.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d4bf0c90..486ca5c4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,14 +16,7 @@ repos: entry: uv run mypy --strict . language: system pass_filenames: false -# - repo: https://github.com/astral-sh/ruff-pre-commit -# rev: v0.15.13 -# hooks: -# # Run the linter -# - id: ruff-check -# args: [ --fix ] -# # Run the formatter -# - id: ruff-format + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: diff --git a/README.md b/README.md index 9b60f905..3ad62cb5 100644 --- a/README.md +++ b/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.9 -- **Date**: 27th May 2026 +- **Version**: 3.0.10 +- **Date**: 4th June 2026 - **License**: [MIT](https://github.com/GeospatialPython/pyshp/blob/master/LICENSE.TXT) ## Contents @@ -93,6 +93,14 @@ part of your geospatial project. # Version Changes +## 3.0.10 +### Bug fix + - Convert directly supplied m values to None if they are strictly below ISDATA_LOWER_BOUND (-1e38). +### Testing + - Move tests into ./tests. + - Remove doctest runner from user land. + - Add round trip property tests for Point and PointM using hypothesis. + ## 3.0.9 ### Testing - Try to make tests not rely on downloads from Github repo URLs, to avoid 404s & 426s due to rate limits. diff --git a/changelog.txt b/changelog.txt index 6b5303d0..ada972ba 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,13 @@ +VERSION 3.0.10 + +2026-06-04 + Bug fix + * Convert directly supplied m values strictly below ISDATA_LOWER_BOUND (-1e38) to None. + Testing + * Move tests into ./tests. + * Remove doctest runner from user land. + * Add round trip property tests for Point and PointM using hypothesis. + VERSION 3.0.9 2026-05-27 diff --git a/pyproject.toml b/pyproject.toml index 11f7c53e..52833fce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,9 +42,10 @@ lint = [ "ruff", ] test = [ - "pytest", + "pytest", "hypothesis" ] + [project.urls] Repository = "https://github.com/GeospatialPython/pyshp" @@ -74,7 +75,9 @@ path = "src/shapefile.py" markers = [ "network: marks tests requiring network access", "slow: marks other tests that cause bottlenecks", + "hypothesis: tests that require hypothesis", ] +python_files = "test_*.py *_test.py *_tests.py" [tool.ruff] # Exclude a variety of commonly ignored directories. diff --git a/src/shapefile.py b/src/shapefile.py index 63a675da..11d975f4 100644 --- a/src/shapefile.py +++ b/src/shapefile.py @@ -8,7 +8,7 @@ from __future__ import annotations -__version__ = "3.0.9.dev" +__version__ = "3.0.10" import abc import array @@ -701,9 +701,16 @@ class _NoShapeTypeSentinel: """ +def _ensure_within_bounds(m: float | None) -> float | None: + if m is not None and m >= ISDATA_LOWER_BOUND: + return m + return None + + def _m_from_point(point: PointT, i_m: int) -> float | None: if 2 <= i_m < len(point): - return point[i_m] + m = point[i_m] + return _ensure_within_bounds(m) return None @@ -810,7 +817,7 @@ def __init__( ms_found = True if m: - self.m: Sequence[float | None] = m + self.m: Sequence[float | None] = [_ensure_within_bounds(x) for x in m] elif self.shapeType in _HasM_shapeTypes: i_m = 3 if self.shapeType in _HasZ_shapeTypes | PointZ_shapeTypes else 2 self.m = [_m_from_point(p, i_m) for p in self.points] @@ -3020,7 +3027,11 @@ def _shape( ShapeClass = SHAPE_CLASS_FROM_SHAPETYPE[shapeType] shape = ShapeClass.from_byte_stream( - shapeType, b_io, shape_len_B, oid=oid, bbox=bbox + shapeType=shapeType, + b_io=b_io, + next_shape_pos=shape_len_B, + oid=oid, + bbox=bbox, ) # Seek to the end of this record as defined by the record header because diff --git a/tests/hypothesis_tests.py b/tests/hypothesis_tests.py new file mode 100644 index 00000000..5c2e73f4 --- /dev/null +++ b/tests/hypothesis_tests.py @@ -0,0 +1,69 @@ +import io + +import pytest +from hypothesis import given +from hypothesis.strategies import ( + builds, + floats, + integers, + none, + one_of, +) + +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( + shp.PointM, + float_nums, + float_nums, + one_of(none(), float_nums), + one_of(none(), integers()), +) + + +@pytest.mark.hypothesis +@given(expected=points_2D, i=integers(min_value=1)) +def test_Point_2D_roundtrips( + expected: shp.Point, + i: int, +) -> None: + stream = io.BytesIO() + n = shp.Point.write_to_byte_stream(b_io=stream, s=expected, i=i) + assert n == stream.tell() + stream.seek(0) + actual = shp.Point.from_byte_stream( + shapeType=shp.POINT, + b_io=stream, + next_shape_pos=n, + oid=expected.oid, + bbox=None, + ) + assert isinstance(actual, shp.Point) + assert actual.points == expected.points + assert actual.oid == expected.oid + + +@pytest.mark.hypothesis +@given(expected=pointMs, i=integers(min_value=1)) +def test_Point_M_roundtrips( + expected: shp.Point, + i: int, +) -> None: + stream = io.BytesIO() + n = shp.PointM.write_to_byte_stream(b_io=stream, s=expected, i=i) + assert n == stream.tell() + stream.seek(0) + actual = shp.PointM.from_byte_stream( + shapeType=shp.POINTM, + b_io=stream, + next_shape_pos=n, + oid=expected.oid, + bbox=None, + ) + assert isinstance(actual, shp.PointM) + assert actual.points == expected.points + assert actual.m == expected.m + assert actual.oid == expected.oid diff --git a/uv.lock b/uv.lock index dac6ea00..fa2ec976 100644 --- a/uv.lock +++ b/uv.lock @@ -13,6 +13,15 @@ exclude-newer-span = "P1W" [options.exclude-newer-package] pyshp-stubs = false +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + [[package]] name = "cfgv" version = "3.5.0" @@ -61,6 +70,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, ] +[[package]] +name = "hypothesis" +version = "6.141.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "attrs", marker = "python_full_version < '3.10'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "sortedcontainers", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/20/8aa62b3e69fea68bb30d35d50be5395c98979013acd8152d64dc927e4cdb/hypothesis-6.141.1.tar.gz", hash = "sha256:8ef356e1e18fbeaa8015aab3c805303b7fe4b868e5b506e87ad83c0bf951f46f", size = 467389, upload-time = "2025-10-15T19:12:25.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/9a/f901858f139694dd669776983781b08a7c1717911025da6720e526bd8ce3/hypothesis-6.141.1-py3-none-any.whl", hash = "sha256:a5b3c39c16d98b7b4c3c5c8d4262e511e3b2255e6814ced8023af49087ad60b3", size = 535000, upload-time = "2025-10-15T19:12:21.659Z" }, +] + +[[package]] +name = "hypothesis" +version = "6.154.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "sortedcontainers", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/a3/10b04ba828ccde01b8a6806d8f18a16755825701fa7d1bcda294cccedeff/hypothesis-6.154.1.tar.gz", hash = "sha256:5956848819cada59aaecafd984d70e3577580e20f2fa6bfc202b0edb27dcecad", size = 476035, upload-time = "2026-05-28T07:27:14.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/0e/2793685fcb9e9021d5614845088276ccf06ccc0517e34348fe9500936757/hypothesis-6.154.1-py3-none-any.whl", hash = "sha256:61d7caf9837d18d1675f427bf6bb23c8d2e5f511e1bb795bd6f4ed495cb38893", size = 542426, upload-time = "2026-05-28T07:27:11.383Z" }, +] + [[package]] name = "identify" version = "2.6.19" @@ -333,6 +375,8 @@ stubs = [ [package.dev-dependencies] dev = [ + { name = "hypothesis", version = "6.141.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "hypothesis", version = "6.154.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "mypy" }, { name = "pre-commit", marker = "python_full_version >= '3.10'" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -345,6 +389,8 @@ lint = [ { name = "ruff" }, ] test = [ + { name = "hypothesis", version = "6.141.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "hypothesis", version = "6.154.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] @@ -355,6 +401,7 @@ provides-extras = ["stubs"] [package.metadata.requires-dev] dev = [ + { name = "hypothesis" }, { name = "mypy", specifier = "==1.19.1" }, { name = "pre-commit", marker = "python_full_version >= '3.10'" }, { name = "pytest" }, @@ -365,7 +412,10 @@ lint = [ { name = "pre-commit", marker = "python_full_version >= '3.10'" }, { name = "ruff" }, ] -test = [{ name = "pytest" }] +test = [ + { name = "hypothesis" }, + { name = "pytest" }, +] [[package]] name = "pyshp-stubs" @@ -529,6 +579,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" }, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + [[package]] name = "tomli" version = "2.4.1"