Skip to content

Commit 050b62f

Browse files
committed
Store count of shp shapes, fixing misc bugs if dbf is absent, add tests for loading shp/shx/dbf separately
1 parent f2f91ce commit 050b62f

File tree

2 files changed

+127
-7
lines changed

2 files changed

+127
-7
lines changed

shapefile.py

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,7 @@ def __init__(self, *args, **kwargs):
796796
self._offsets = []
797797
self.shpLength = None
798798
self.numRecords = None
799+
self.numShapes = None
799800
self.fields = []
800801
self.__dbfHdrLength = 0
801802
self.__fieldposition_lookup = {}
@@ -862,7 +863,38 @@ def __exit__(self, exc_type, exc_val, exc_tb):
862863

863864
def __len__(self):
864865
"""Returns the number of shapes/records in the shapefile."""
865-
return self.numRecords
866+
if self.dbf:
867+
# Preferably use dbf record count
868+
if self.numRecords is None:
869+
self.__dbfHeader()
870+
871+
return self.numRecords
872+
873+
elif self.shp:
874+
# Otherwise use shape count
875+
if self.shx:
876+
# Use index file to get total count
877+
if self.numShapes is None:
878+
# File length (16-bit word * 2 = bytes) - header length
879+
self.shx.seek(24)
880+
shxRecordLength = (unpack(">i", self.shx.read(4))[0] * 2) - 100
881+
self.numShapes = shxRecordLength // 8
882+
883+
return self.numShapes
884+
885+
else:
886+
# Index file not available, iterate all shapes to get total count
887+
if self.numShapes is None:
888+
i = 0
889+
for i,shape in enumerate(self.iterShapes()):
890+
i += 1
891+
self.numShapes = i
892+
893+
return self.numShapes
894+
895+
else:
896+
# No file loaded yet, treat as 'empty' shapefile
897+
return 0
866898

867899
def __iter__(self):
868900
"""Iterates through the shapes/records in the shapefile."""
@@ -1072,15 +1104,16 @@ def __shapeIndex(self, i=None):
10721104
if not shx:
10731105
return None
10741106
if not self._offsets:
1075-
# File length (16-bit word * 2 = bytes) - header length
1076-
shx.seek(24)
1077-
shxRecordLength = (unpack(">i", shx.read(4))[0] * 2) - 100
1078-
numRecords = shxRecordLength // 8
1107+
if self.numShapes is None:
1108+
# File length (16-bit word * 2 = bytes) - header length
1109+
shx.seek(24)
1110+
shxRecordLength = (unpack(">i", shx.read(4))[0] * 2) - 100
1111+
self.numShapes = shxRecordLength // 8
10791112
# Jump to the first record.
10801113
shx.seek(100)
10811114
shxRecords = _Array('i')
10821115
# Each offset consists of two nrs, only the first one matters
1083-
shxRecords.fromfile(shx, 2 * numRecords)
1116+
shxRecords.fromfile(shx, 2 * self.numShapes)
10841117
if sys.byteorder != 'big':
10851118
shxRecords.byteswap()
10861119
self._offsets = [2 * el for el in shxRecords[::2]]

test_shapefile.py

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,93 @@ def test_reader_fields():
276276
assert isinstance(field[3], int) # decimal length
277277

278278

279+
def test_reader_shapefile_extension_ignored():
280+
"""
281+
Assert that the filename's extension is
282+
ignored when reading a shapefile.
283+
"""
284+
base = "shapefiles/blockgroups"
285+
ext = ".abc"
286+
filename = base + ext
287+
with shapefile.Reader(filename) as sf:
288+
assert len(sf) == 663
289+
290+
# assert test.abc does not exist
291+
assert not os.path.exists(filename)
292+
293+
294+
def test_reader_dbf_only():
295+
"""
296+
Assert that specifying just the
297+
dbf argument to the shapefile reader
298+
reads just the dbf file.
299+
"""
300+
with shapefile.Reader(dbf="shapefiles/blockgroups.dbf") as sf:
301+
assert len(sf) == 663
302+
record = sf.record(3)
303+
assert record[1:3] == ['060750601001', 4715]
304+
305+
306+
def test_reader_shp_shx_only():
307+
"""
308+
Assert that specifying just the
309+
shp and shx argument to the shapefile reader
310+
reads just the shp and shx file.
311+
"""
312+
with shapefile.Reader(shp="shapefiles/blockgroups.shp", shx="shapefiles/blockgroups.shx") as sf:
313+
assert len(sf) == 663
314+
shape = sf.shape(3)
315+
assert len(shape.points) is 173
316+
317+
318+
def test_reader_shx_optional():
319+
"""
320+
Assert that specifying just the
321+
shp argument to the shapefile reader
322+
reads just the shp file (shx optional).
323+
"""
324+
with shapefile.Reader(shp="shapefiles/blockgroups.shp") as sf:
325+
assert len(sf) == 663
326+
shape = sf.shape(3)
327+
assert len(shape.points) is 173
328+
329+
330+
def test_reader_filelike_dbf_only():
331+
"""
332+
Assert that specifying just the
333+
dbf argument to the shapefile reader
334+
reads just the dbf file.
335+
"""
336+
with shapefile.Reader(dbf=open("shapefiles/blockgroups.dbf", "rb")) as sf:
337+
assert len(sf) == 663
338+
record = sf.record(3)
339+
assert record[1:3] == ['060750601001', 4715]
340+
341+
342+
def test_reader_filelike_shp_shx_only():
343+
"""
344+
Assert that specifying just the
345+
shp and shx argument to the shapefile reader
346+
reads just the shp and shx file.
347+
"""
348+
with shapefile.Reader(shp=open("shapefiles/blockgroups.shp", "rb"), shx=open("shapefiles/blockgroups.shx", "rb")) as sf:
349+
assert len(sf) == 663
350+
shape = sf.shape(3)
351+
assert len(shape.points) is 173
352+
353+
354+
def test_reader_filelike_shx_optional():
355+
"""
356+
Assert that specifying just the
357+
shp argument to the shapefile reader
358+
reads just the shp file (shx optional).
359+
"""
360+
with shapefile.Reader(shp=open("shapefiles/blockgroups.shp", "rb")) as sf:
361+
assert len(sf) == 663
362+
shape = sf.shape(3)
363+
assert len(shape.points) is 173
364+
365+
279366
def test_records_match_shapes():
280367
"""
281368
Assert that the number of records matches
@@ -498,7 +585,7 @@ def test_write_geojson(tmpdir):
498585
@pytest.mark.parametrize("shape_type", shape_types)
499586
def test_write_empty_shapefile(tmpdir, shape_type):
500587
"""
501-
Assert that can write an empty shapefile
588+
Assert that can write an empty shapefile, for all different shape types.
502589
"""
503590
filename = tmpdir.join("test").strpath
504591
with shapefile.Writer(filename, shapeType=shape_type) as w:

0 commit comments

Comments
 (0)