Skip to content

Commit 8d04f71

Browse files
committed
Add shx round trip test
Reformat
1 parent c61f5c2 commit 8d04f71

4 files changed

Lines changed: 69 additions & 12 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ part of your geospatial project.
9595

9696
## 3.0.14.dev
9797
### ShpWriter.shape API Tweak (small breaking change).
98-
- Make ShpWriter.shape return shape length in bytes (the
98+
- Make ShpWriter.shape return shape length in bytes (the
9999
same as for offset) not in 16 bit words.
100100

101101

changelog.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
VERSION 3.0.14.dev
22

33
2026-06-20
4-
* API Tweak (small breaking change). Make ShpWriter.shape return shape length in bytes
4+
* API Tweak (small breaking change). Make ShpWriter.shape return shape length in bytes
55
(the same as for offset) not in 16 bit words.
66

77
VERSION 3.0.13

src/shapefile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4134,7 +4134,7 @@ def _header(self) -> None:
41344134
def _write_file_length(self) -> None:
41354135
# self.file required to be at correct position, e.g.
41364136
# if called by self._header
4137-
4137+
41384138
# Calculate size as 16-bit words
41394139
size_B = self._shp_file_length_B()
41404140
size_16b_words = size_B // 2

tests/hypothesis_tests.py

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,6 @@ def multipointM_from_xyms(point_ms: tuple[float, float, float | None], oid_: int
163163
multipointm = builds(multipointM_from_xyms, lists(tuples(xs, ys, ms), min_size=1), oid)
164164

165165
@pytest.mark.hypothesis
166-
# @settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
167166
@given(expected=multipointm, i=integers(min_value=1))
168167
def test_MultiPointM_roundtrips(
169168
expected: shp.MultiPointM,
@@ -196,7 +195,6 @@ def multipointZ_from_xyzms(pointz_ms: tuple[float, float, float, float | None],
196195

197196

198197
@pytest.mark.hypothesis
199-
# @settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
200198
@given(expected=multipointz, i=integers(min_value=1))
201199
def test_MultiPointZ_roundtrips(
202200
expected: shp.MultiPointZ,
@@ -248,7 +246,6 @@ def test_Polyline_roundtrips(
248246
assert actual.oid == expected.oid
249247

250248
@pytest.mark.hypothesis
251-
# @settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
252249
@given(expected=polylinem, i=integers(min_value=1))
253250
def test_PolylineM_roundtrips(
254251
expected: shp.PolylineM,
@@ -273,7 +270,6 @@ def test_PolylineM_roundtrips(
273270
assert actual.oid == expected.oid
274271

275272
@pytest.mark.hypothesis
276-
# @settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
277273
@given(expected=polylinez, i=integers(min_value=1))
278274
def test_PolylineZ_roundtrips(
279275
expected: shp.PolylineZ,
@@ -327,7 +323,6 @@ def test_Polygon_roundtrips(
327323
assert actual.oid == expected.oid
328324

329325
@pytest.mark.hypothesis
330-
# @settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
331326
@given(expected=polygonm, i=integers(min_value=1))
332327
def test_PolygonM_roundtrips(
333328
expected: shp.PolygonM,
@@ -352,7 +347,6 @@ def test_PolygonM_roundtrips(
352347
assert actual.oid == expected.oid
353348

354349
@pytest.mark.hypothesis
355-
# @settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
356350
@given(expected=polygonz, i=integers(min_value=1))
357351
def test_PolygonZ_roundtrips(
358352
expected: shp.PolygonZ,
@@ -392,7 +386,6 @@ def multipatch_from_xyzms_and_types(
392386

393387

394388
@pytest.mark.hypothesis
395-
# @settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
396389
@given(expected=multipatch, i=integers(min_value=1))
397390
def test_MultiPatch_roundtrips(
398391
expected: shp.MultiPatch,
@@ -418,6 +411,12 @@ def test_MultiPatch_roundtrips(
418411
assert actual.oid == expected.oid
419412
assert actual.partTypes == expected.partTypes, f"{type(actual.partTypes)=}, {type(expected.partTypes)=}"
420413

414+
MAX_FILE_SIZE_16bw = (1 << 31) - 1 # This bound comes from encoding the
415+
# actual file size (in 16 bit words)
416+
# as a 4 byte signed integer.
417+
MAX_NUM_SHAPES = (MAX_FILE_SIZE_16bw - 50) // 6 # Minus 100B header, 12 bytes
418+
# per record (the minimum for
419+
# a Null shape).
421420

422421
shape_codes_names_and_strategies = [
423422
# (0, "Null Shape"),
@@ -438,7 +437,7 @@ def test_MultiPatch_roundtrips(
438437

439438
def code_and_shape_strat_from_triple(t):
440439
x, _name, shapes = t
441-
return tuples(just(x), lists(shapes, min_size = 0)) # Empty shp files are in the esri spec.
440+
return tuples(just(x), lists(shapes, min_size = 0, max_size=MAX_NUM_SHAPES)) # Empty shp files are in the esri spec.
442441

443442
codes_and_shapes_strats = [
444443
code_and_shape_strat_from_triple(t)
@@ -448,7 +447,6 @@ def code_and_shape_strat_from_triple(t):
448447
codes_and_shapes = one_of(codes_and_shapes_strats)
449448

450449
@pytest.mark.hypothesis
451-
# @settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large])
452450
@given(codes_and_shapes=codes_and_shapes)
453451
def test_shp_reader_writer_roundtrip(codes_and_shapes)-> None:
454452
code_ex, expected_shapes = codes_and_shapes
@@ -483,3 +481,62 @@ def test_shp_reader_writer_roundtrip(codes_and_shapes)-> None:
483481
assert actual.partTypes == expected.partTypes, f"{type(actual.partTypes)=}, {type(expected.partTypes)=}"
484482
else:
485483
assert not hasattr(expected, "partTypes")
484+
485+
486+
487+
# SHX_UB = MAX_FILE_SIZE_16bw - 50
488+
489+
490+
# ## Surprisingly slow. Doesn't add enough value to merit waiting for
491+
# @composite
492+
# def positive_ints_with_bounded_sum(
493+
# draw,
494+
# min_x: int = 6,
495+
# upper_bound: int = SHX_UB,
496+
# max_len: int = MAX_NUM_SHAPES,
497+
# ):
498+
# assert min_x >= 1
499+
# assert upper_bound >= max_len
500+
# length = draw(integers(min_value=0, max_value=max_len))
501+
# if length == 0:
502+
# return []
503+
504+
# max_x = upper_bound - (length - 1)
505+
# result = []
506+
507+
# for i in range(length):
508+
# if max_x < min_x :
509+
# break
510+
# x = draw(integers(min_value=min_x, max_value=max_x))
511+
# result.append(x)
512+
# max_x -= x
513+
514+
# return result
515+
516+
517+
@pytest.mark.hypothesis
518+
@given(codes_and_shapes=codes_and_shapes)
519+
def test_shx_reader_writer_roundtrip(codes_and_shapes)-> None:
520+
code_ex, expected_shapes = codes_and_shapes
521+
522+
sizes_B = []
523+
offsets_B = []
524+
offset_B = 100 # "Thus, the offset for the first record in the
525+
# main file is 50 (16bw), given the 100-byte header. "
526+
shp_stream = io.BytesIO()
527+
shx_stream = io.BytesIO()
528+
with shp.ShpWriter(shp=shp_stream, shapeType=code_ex) as shp_w:
529+
with shp.ShxWriter(shx=shx_stream, shp_writer = shp_w) as shx_w:
530+
for shape in expected_shapes:
531+
offset_B, size_B = shp_w.shape(shape)
532+
sizes_B.append(size_B)
533+
offsets_B.append(offset_B)
534+
shx_w._shx_record(offset_B, size_B)
535+
536+
shx_stream.seek(0)
537+
538+
with shp.ShxReader(shx=shx_stream) as r:
539+
assert r.numShapes == len(expected_shapes)
540+
assert r.offsets == offsets_B
541+
assert r.shape_lengths_B == sizes_B
542+

0 commit comments

Comments
 (0)