Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ lint = [
"ruff",
]
test = [
"pytest",
"pytest", "hypothesis"
]


[project.urls]
Repository = "https://github.com/GeospatialPython/pyshp"

Expand Down Expand Up @@ -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.
Expand Down
19 changes: 15 additions & 4 deletions src/shapefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from __future__ import annotations

__version__ = "3.0.9.dev"
__version__ = "3.0.10"

import abc
import array
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
69 changes: 69 additions & 0 deletions tests/hypothesis_tests.py
Original file line number Diff line number Diff line change
@@ -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
61 changes: 60 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading