Skip to content

Commit 478c638

Browse files
authored
Merge pull request #405 from JamesParrott/fewer_casts. Get rid of the GeoJSON casts.
Get rid of the GeoJSON casts, while satisfying mypy
2 parents 2e112a7 + 9b84275 commit 478c638

1 file changed

Lines changed: 64 additions & 95 deletions

File tree

src/shapefile.py

Lines changed: 64 additions & 95 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"
11+
__version__ = "3.0.9.dev"
1212

1313
import abc
1414
import array
@@ -286,6 +286,7 @@ def __repr__(self) -> str:
286286
RecordValue = Union[RecordValueNotDate, date]
287287

288288

289+
@runtime_checkable
289290
class HasGeoInterface(Protocol):
290291
@property
291292
def __geo_interface__(self) -> GeoJSONHomogeneousGeometryObject: ...
@@ -383,7 +384,9 @@ class GeoJSONFeatureCollectionWithBBox(GeoJSONFeatureCollection):
383384
# Helpers
384385

385386
MISSING = (None, "") # Don't make a set, as user input may not be Hashable
386-
ISDATA_LOWER_BOUND = -1e38 # as per the ESRI shapefile spec, only used for m-values.
387+
ISDATA_LOWER_BOUND = (
388+
-1e38
389+
) # Inclusive. As per the ESRI shapefile spec, only used for m-values.
387390
NODATA = (
388391
10 * ISDATA_LOWER_BOUND
389392
) # value to encode m=None as. Must be < ISDATA_LOWER_BOUND
@@ -698,37 +701,24 @@ class GeoJSON_Error(Exception):
698701

699702

700703
class _NoShapeTypeSentinel:
701-
"""For use as a default value for Shape.__init__ to
702-
preserve old behaviour for anyone who explictly
704+
"""An instance is the default value for Shape.__init__,
705+
to preserve old behaviour for anyone who explictly
703706
called Shape(shapeType=None).
704707
"""
705708

706709

707-
_NO_SHAPE_TYPE_SENTINEL: Final = _NoShapeTypeSentinel()
708-
709-
710-
def _m_from_point(point: PointMT | PointZT, mpos: int) -> float | None:
711-
if len(point) > mpos and point[mpos] is not None:
712-
return cast(float, point[mpos])
710+
def _m_from_point(point: PointT, i_m: int) -> float | None:
711+
if 2 <= i_m < len(point):
712+
return point[i_m]
713713
return None
714714

715715

716-
def _ms_from_points(
717-
points: list[PointMT] | list[PointZT], mpos: int
718-
) -> Iterator[float | None]:
719-
return (_m_from_point(p, mpos) for p in points)
720-
721-
722-
def _z_from_point(point: PointZT) -> float:
716+
def _z_from_point(point: PointT) -> float:
723717
if len(point) >= 3 and point[2] is not None:
724718
return point[2]
725719
return 0.0
726720

727721

728-
def _zs_from_points(points: Iterable[PointZT]) -> Iterator[float]:
729-
return (_z_from_point(p) for p in points)
730-
731-
732722
class CanHaveBboxNoLinesKwargs(TypedDict, total=False):
733723
oid: int | None
734724
points: PointsT | None
@@ -744,7 +734,7 @@ class CanHaveBboxNoLinesKwargs(TypedDict, total=False):
744734
class Shape:
745735
def __init__(
746736
self,
747-
shapeType: int | _NoShapeTypeSentinel = _NO_SHAPE_TYPE_SENTINEL,
737+
shapeType: int | _NoShapeTypeSentinel = _NoShapeTypeSentinel(),
748738
points: PointsT | None = None,
749739
parts: Sequence[int] | None = None, # index of start point of each part
750740
lines: list[PointsT] | None = None,
@@ -775,11 +765,11 @@ def __init__(
775765
"""
776766

777767
# Preserve previous behaviour for anyone who set self.shapeType = None
778-
if shapeType is not _NO_SHAPE_TYPE_SENTINEL:
779-
self.shapeType = cast(int, shapeType)
780-
else:
768+
if isinstance(shapeType, _NoShapeTypeSentinel):
781769
class_name = self.__class__.__name__
782770
self.shapeType = SHAPETYPENUM_LOOKUP.get(class_name.upper(), NULL)
771+
else:
772+
self.shapeType = shapeType
783773

784774
if partTypes is not None:
785775
self.partTypes = partTypes
@@ -828,25 +818,21 @@ def __init__(
828818
if m:
829819
self.m: Sequence[float | None] = m
830820
elif self.shapeType in _HasM_shapeTypes:
831-
mpos = 3 if self.shapeType in _HasZ_shapeTypes | PointZ_shapeTypes else 2
832-
points_m_z = cast(Union[list[PointMT], list[PointZT]], self.points)
833-
self.m = list(_ms_from_points(points_m_z, mpos))
821+
i_m = 3 if self.shapeType in _HasZ_shapeTypes | PointZ_shapeTypes else 2
822+
self.m = [_m_from_point(p, i_m) for p in self.points]
834823
elif self.shapeType in PointM_shapeTypes:
835-
mpos = 3 if self.shapeType == POINTZ else 2
836-
point_m_z = cast(Union[PointMT, PointZT], self.points[0])
837-
self.m = (_m_from_point(point_m_z, mpos),)
824+
i_m = 3 if self.shapeType == POINTZ else 2
825+
self.m = (_m_from_point(self.points[0], i_m),)
838826
else:
839827
ms_found = False
840828

841829
zs_found = True
842830
if z:
843831
self.z: Sequence[float] = z
844832
elif self.shapeType in _HasZ_shapeTypes:
845-
points_z = cast(list[PointZT], self.points)
846-
self.z = list(_zs_from_points(points_z))
833+
self.z = [_z_from_point(p) for p in self.points]
847834
elif self.shapeType == POINTZ:
848-
point_z = cast(PointZT, self.points[0])
849-
self.z = (_z_from_point(point_z),)
835+
self.z = (_z_from_point(self.points[0]),)
850836
else:
851837
zs_found = False
852838

@@ -1029,34 +1015,30 @@ def __geo_interface__(self) -> GeoJSONHomogeneousGeometryObject:
10291015

10301016
@staticmethod
10311017
def _from_geojson(geoj: GeoJSONHomogeneousGeometryObject) -> Shape:
1032-
# create empty shape
1033-
# set shapeType
1034-
geojType = geoj["type"] if geoj else "Null"
1035-
if geojType in GEOJSON_TO_SHAPETYPE:
1036-
shapeType = GEOJSON_TO_SHAPETYPE[geojType]
1037-
else:
1038-
raise GeoJSON_Error(f"Cannot create Shape from GeoJSON type '{geojType}'")
10391018

1040-
coordinates = geoj["coordinates"]
1041-
1042-
if coordinates == ():
1043-
raise GeoJSON_Error(f"Cannot create non-Null Shape from: {coordinates=}")
1019+
shapeType = GEOJSON_TO_SHAPETYPE.get(geoj["type"], None)
1020+
if shapeType is None:
1021+
raise GeoJSON_Error(
1022+
f"Cannot create Shape from GeoJSON type '{geoj['type']}'"
1023+
)
1024+
if shapeType == NULL or (geoj["type"] == "Point" and geoj["coordinates"] == ()):
1025+
return NullShape()
10441026

10451027
points: PointsT
10461028
parts: list[int]
10471029

10481030
# set points and parts
1049-
if geojType == "Point":
1050-
points = [cast(PointT, coordinates)]
1031+
if geoj["type"] == "Point" and isinstance(geoj["coordinates"], list):
1032+
points = [geoj["coordinates"]]
10511033
parts = [0]
1052-
elif geojType in ("MultiPoint", "LineString"):
1053-
points = cast(PointsT, coordinates)
1034+
elif geoj["type"] == "MultiPoint" or geoj["type"] == "LineString":
1035+
points = geoj["coordinates"]
10541036
parts = [0]
1055-
elif geojType == "Polygon":
1037+
elif geoj["type"] == "Polygon":
10561038
points = []
10571039
parts = []
10581040
index = 0
1059-
for i, ext_or_hole in enumerate(cast(list[PointsT], coordinates)):
1041+
for i, ext_or_hole in enumerate(geoj["coordinates"]):
10601042
# although the latest GeoJSON spec states that exterior rings should have
10611043
# counter-clockwise orientation, we explicitly check orientation since older
10621044
# GeoJSONs might not enforce this.
@@ -1069,19 +1051,19 @@ def _from_geojson(geoj: GeoJSONHomogeneousGeometryObject) -> Shape:
10691051
points.extend(ext_or_hole)
10701052
parts.append(index)
10711053
index += len(ext_or_hole)
1072-
elif geojType == "MultiLineString":
1054+
elif geoj["type"] == "MultiLineString":
10731055
points = []
10741056
parts = []
10751057
index = 0
1076-
for linestring in cast(list[PointsT], coordinates):
1058+
for linestring in geoj["coordinates"]:
10771059
points.extend(linestring)
10781060
parts.append(index)
10791061
index += len(linestring)
1080-
elif geojType == "MultiPolygon":
1062+
elif geoj["type"] == "MultiPolygon":
10811063
points = []
10821064
parts = []
10831065
index = 0
1084-
for polygon in cast(list[list[PointsT]], coordinates):
1066+
for polygon in geoj["coordinates"]:
10851067
for i, ext_or_hole in enumerate(polygon):
10861068
# although the latest GeoJSON spec states that exterior rings should have
10871069
# counter-clockwise orientation, we explicitly check orientation since older
@@ -1187,6 +1169,8 @@ def _write_bbox_to_byte_stream(
11871169
@staticmethod
11881170
def _read_npoints_from_byte_stream(b_io: ReadableBinStream) -> int:
11891171
(nPoints,) = unpack("<i", b_io.read(4))
1172+
# cast(int, ...) needed until mypy interprets struct fmt strings
1173+
# https://github.com/python/mypy/issues/20869
11901174
return cast(int, nPoints)
11911175

11921176
@staticmethod
@@ -1422,11 +1406,11 @@ def write_to_byte_stream(b_io: WriteableBinStream, s: Shape, i: int) -> int:
14221406

14231407
# Write a single Z value
14241408
if s.shapeType in PointZ_shapeTypes:
1425-
n += PointZ._write_single_point_z_to_byte_stream(b_io, s, i)
1409+
n += PointZ._write_single_point_z_to_byte_stream(b_io, cast(PointZ, s), i)
14261410

14271411
# Write a single M value
14281412
if s.shapeType in PointM_shapeTypes:
1429-
n += PointM._write_single_point_m_to_byte_stream(b_io, s, i)
1413+
n += PointM._write_single_point_m_to_byte_stream(b_io, cast(PointM, s), i)
14301414

14311415
return n
14321416

@@ -1558,7 +1542,7 @@ def _read_ms_from_byte_stream(
15581542

15591543
@staticmethod
15601544
def _write_ms_to_byte_stream(
1561-
b_io: WriteableBinStream, s: Shape, i: int, mbox: MBox | None
1545+
b_io: WriteableBinStream, s: _HasM, i: int, mbox: MBox | None
15621546
) -> int:
15631547
if not mbox or len(mbox) != 2:
15641548
raise ShapefileException(f"Two numbers required for mbox. Got: {mbox}")
@@ -1571,12 +1555,10 @@ def _write_ms_to_byte_stream(
15711555
raise ShapefileException(
15721556
f"Failed to write measure extremes for record {i}. Expected floats"
15731557
)
1574-
try:
1575-
ms = cast(_HasM, s).m
15761558

1577-
ms_to_encode = replace_None_with_NODATA(ms)
1578-
1579-
num_bytes_written += b_io.write(pack(f"<{len(ms)}d", *ms_to_encode))
1559+
ms_to_encode = replace_None_with_NODATA(s.m)
1560+
try:
1561+
num_bytes_written += b_io.write(pack(f"<{len(s.m)}d", *ms_to_encode))
15801562
except StructError:
15811563
raise ShapefileException(
15821564
f"Failed to write measure values for record {i}. Expected floats"
@@ -1608,7 +1590,7 @@ def _read_zs_from_byte_stream(
16081590

16091591
@staticmethod
16101592
def _write_zs_to_byte_stream(
1611-
b_io: WriteableBinStream, s: Shape, i: int, zbox: ZBox | None
1593+
b_io: WriteableBinStream, s: _HasZ, i: int, zbox: ZBox | None
16121594
) -> int:
16131595
if not zbox or len(zbox) != 2:
16141596
raise ShapefileException(f"Two numbers required for zbox. Got: {zbox}")
@@ -1622,8 +1604,7 @@ def _write_zs_to_byte_stream(
16221604
f"Failed to write elevation extremes for record {i}. Expected floats."
16231605
)
16241606
try:
1625-
zs = cast(_HasZ, s).z
1626-
num_bytes_written += b_io.write(pack(f"<{len(zs)}d", *zs))
1607+
num_bytes_written += b_io.write(pack(f"<{len(s.z)}d", *s.z))
16271608
except StructError:
16281609
raise ShapefileException(
16291610
f"Failed to write elevation values for record {i}. Expected floats."
@@ -1715,21 +1696,20 @@ def _read_single_point_ms_from_byte_stream(
17151696

17161697
@staticmethod
17171698
def _write_single_point_m_to_byte_stream(
1718-
b_io: WriteableBinStream, s: Shape, i: int
1699+
b_io: WriteableBinStream, s: PointM, i: int
17191700
) -> int:
1701+
1702+
m = s.m[0] if s.m else None
1703+
# Set missing m values to NODATA.
1704+
m_to_encode = m if m is not None else NODATA
1705+
17201706
try:
1721-
s = cast(_HasM, s)
1722-
m = s.m[0] if s.m else None
1707+
return b_io.write(pack("<1d", m_to_encode))
17231708
except StructError:
17241709
raise ShapefileException(
17251710
f"Failed to write measure value for record {i}. Expected floats."
17261711
)
17271712

1728-
# Note: missing m values are autoset to NODATA.
1729-
m_to_encode = m if m is not None else NODATA
1730-
1731-
return b_io.write(pack("<1d", m_to_encode))
1732-
17331713

17341714
PolylineM_shapeTypes = frozenset([POLYLINEM, POLYLINEZ])
17351715

@@ -1859,22 +1839,17 @@ def _read_single_point_zs_from_byte_stream(b_io: ReadableBinStream) -> tuple[flo
18591839

18601840
@staticmethod
18611841
def _write_single_point_z_to_byte_stream(
1862-
b_io: WriteableBinStream, s: Shape, i: int
1842+
b_io: WriteableBinStream, s: PointZ, i: int
18631843
) -> int:
18641844
# Note: missing z values are autoset to 0, but not sure if this is ideal.
1865-
z: float = 0.0
1866-
# then write value
1867-
1845+
z: float = s.z[0] if s.z else 0.0
18681846
try:
1869-
if s.z:
1870-
z = s.z[0]
1847+
return b_io.write(pack("<d", z))
18711848
except StructError:
18721849
raise ShapefileException(
18731850
f"Failed to write elevation value for record {i}. Expected floats."
18741851
)
18751852

1876-
return b_io.write(pack("<d", z))
1877-
18781853

18791854
PolylineZ_shapeTypes = frozenset([POLYLINEZ])
18801855

@@ -3126,16 +3101,13 @@ def iterShapes(
31263101

31273102

31283103
class _NoShpSentinel:
3129-
"""For use as a default value for shp to preserve the
3130-
behaviour (from when all keyword args were gathered
3104+
"""An instance is the default value for shp to preserve the
3105+
old behaviour (from when all keyword args were gathered
31313106
in the **kwargs dict) in case someone explictly
31323107
called Reader(shp=None) to load self.shx.
31333108
"""
31343109

31353110

3136-
_NO_SHP_SENTINEL = _NoShpSentinel()
3137-
3138-
31393111
class Reader(_HasExitStack):
31403112
"""Reads the three files of a shapefile as a unit or
31413113
separately. If one of the three files (.shp, .shx,
@@ -3170,7 +3142,7 @@ def __init__(
31703142
*,
31713143
encoding: str = "utf-8",
31723144
encodingErrors: str = "strict",
3173-
shp: _NoShpSentinel | BinaryFileT | None = _NO_SHP_SENTINEL,
3145+
shp: _NoShpSentinel | BinaryFileT | None = _NoShpSentinel(),
31743146
shx: BinaryFileT | None = None,
31753147
dbf: BinaryFileT | None = None,
31763148
# Keep kwargs even though unused, to preserve PyShp 2.4 API
@@ -3195,7 +3167,7 @@ def __init__(
31953167
" (or satisfy os.PathLike). "
31963168
)
31973169
discarded_kwargs = {}
3198-
if shp not in (None, _NO_SHP_SENTINEL):
3170+
if shp is not None and not isinstance(shp, _NoShpSentinel):
31993171
discarded_kwargs["shp"] = shp
32003172
if shx is not None:
32013173
discarded_kwargs["shx"] = shx
@@ -3268,8 +3240,7 @@ def __init__(
32683240
#
32693241
return
32703242

3271-
if shp is not _NO_SHP_SENTINEL:
3272-
shp = cast(Union[BinaryFileT, None], shp)
3243+
if not isinstance(shp, _NoShpSentinel):
32733244
self._shp = self._seek_0_on_file_obj_wrap_or_open_from_name(".shp", shp)
32743245
self._shx = self._seek_0_on_file_obj_wrap_or_open_from_name(".shx", shx)
32753246

@@ -4171,10 +4142,8 @@ def shape(
41714142
self,
41724143
s: Shape | HasGeoInterface | GeoJSONHomogeneousGeometryObject,
41734144
) -> tuple[int, int]:
4174-
# Check is shape or import from geojson
41754145
if not isinstance(s, Shape):
4176-
if hasattr(s, "__geo_interface__"):
4177-
s = cast(HasGeoInterface, s)
4146+
if isinstance(s, HasGeoInterface):
41784147
shape_dict = s.__geo_interface__
41794148
elif isinstance(s, dict): # TypedDict is a dict at runtime
41804149
shape_dict = s

0 commit comments

Comments
 (0)