88
99from __future__ import annotations
1010
11- __version__ = "3.0.9"
11+ __version__ = "3.0.9.dev "
1212
1313import abc
1414import array
@@ -286,6 +286,7 @@ def __repr__(self) -> str:
286286RecordValue = Union [RecordValueNotDate , date ]
287287
288288
289+ @runtime_checkable
289290class HasGeoInterface (Protocol ):
290291 @property
291292 def __geo_interface__ (self ) -> GeoJSONHomogeneousGeometryObject : ...
@@ -383,7 +384,9 @@ class GeoJSONFeatureCollectionWithBBox(GeoJSONFeatureCollection):
383384# Helpers
384385
385386MISSING = (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.
387390NODATA = (
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
700703class _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-
732722class CanHaveBboxNoLinesKwargs (TypedDict , total = False ):
733723 oid : int | None
734724 points : PointsT | None
@@ -744,7 +734,7 @@ class CanHaveBboxNoLinesKwargs(TypedDict, total=False):
744734class 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
17341714PolylineM_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
18791854PolylineZ_shapeTypes = frozenset ([POLYLINEZ ])
18801855
@@ -3126,16 +3101,13 @@ def iterShapes(
31263101
31273102
31283103class _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-
31393111class 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