diff --git a/pyproject.toml b/pyproject.toml index aa11da4..a8e14c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -114,6 +114,7 @@ load-plugins=[ # Silence warning: shapefile.py:2076:20: W0212: Access to a protected # member _from_geojson of a client class (protected-access) +# shapefile.py:950:16: W0201: Attribute 'm' defined outside __init__ (attribute-defined-outside-init) # Silence remarks: # src\shapefile.py:338:0: R0914: Too many local variables (21/15) (too-many-locals) # src\shapefile.py:338:0: R0912: Too many branches (24/12) (too-many-branches) @@ -133,6 +134,6 @@ load-plugins=[ # https://github.com/christopherpickering/pylint-per-file-ignores/issues/160 [tool.pylint.'messages control'] per-file-ignores = [ - "/src/shapefile.py:W0212,R0902,R0903,R0904,R0911,R0912,R0913,R0914,R0915,R0917,R1732", + "/src/shapefile.py:W0212,W0201,R0902,R0903,R0904,R0911,R0912,R0913,R0914,R0915,R0917,R1732", "test_shapefile.py:W0212,R1732", ] diff --git a/src/shapefile.py b/src/shapefile.py index 14da4bf..321b215 100644 --- a/src/shapefile.py +++ b/src/shapefile.py @@ -844,6 +844,112 @@ def shapeTypeName(self) -> str: def __repr__(self): return f"Shape #{self.__oid}: {self.shapeTypeName}" + # pylint: disable=unused-argument + def _set_bbox_from_shp_file(self, f): + pass + + @staticmethod + def _get_nparts_from_shp_file(f): + return None + + @staticmethod + def _get_npoints_from_shp_file(f): + return None + + def _set_parts_from_shp_file(self, f, nParts): + pass + + def _set_part_types_from_shp_file(self, f, nParts): + pass + + def _set_points_from_shp_file(self, f, nPoints): + pass + + def _set_z_from_shp_file(self, f, nPoints): + pass + + def _set_m_from_shp_file(self, f, nPoints, next_shape): + pass + + def _get_and_set_2D_point_from_shp_file(self, f): + return None + + def _set_single_point_z_from_shp_file(self, f): + pass + + def _set_single_point_m_from_shp_file(self, f, next_shape): + pass + + # pylint: enable=unused-argument + + @classmethod + def _from_shp_file(cls, f, next_shape, oid=None, bbox=None): + shape = cls(oid=oid) + + shape._set_bbox_from_shp_file(f) # pylint: disable=assignment-from-none + + # if bbox specified and no overlap, skip this shape + if bbox is not None and not bbox_overlap(bbox, tuple(shape.bbox)): # pylint: disable=no-member + # because we stop parsing this shape, skip to beginning of + # next shape before we return + return None + + nParts: Optional[int] = shape._get_nparts_from_shp_file(f) + nPoints: Optional[int] = shape._get_npoints_from_shp_file(f) + # Previously, we also set __zmin = __zmax = __mmin = __mmax = None + + if nParts: + shape._set_parts_from_shp_file(f, nParts) + shape._set_part_types_from_shp_file(f, nParts) + + if nPoints: + shape._set_points_from_shp_file(f, nPoints) + + shape._set_z_from_shp_file(f, nPoints) + + shape._set_m_from_shp_file(f, nPoints, next_shape) + + # Read a single point + # if shapeType in (1, 11, 21): + point_2D = shape._get_and_set_2D_point_from_shp_file(f) # pylint: disable=assignment-from-none + + if bbox is not None and point_2D is not None: + x, y = point_2D # pylint: disable=unpacking-non-sequence + # create bounding box for Point by duplicating coordinates + # skip shape if no overlap with bounding box + if not bbox_overlap(bbox, (x, y, x, y)): + return None + + shape._set_single_point_z_from_shp_file(f) + + shape._set_single_point_m_from_shp_file(f, next_shape) + + return shape + + +def _read_shape_from_shp_file( + f, oid=None, bbox=None +): # oid: Optional[int] = None, bbox: Optional[BBox] = None): + """Constructs a Shape from an open .shp file. Something else + is required to have first read the .shp file's header. + Leaves the shp file's .tell() in the correct position for + a subsequent call to this, to build the next shape. + """ + # shape = Shape(oid=oid) + (__recNum, recLength) = unpack_2_int32_be(f.read(8)) + # Determine the start of the next record + next_shape = f.tell() + (2 * recLength) + shapeType = unpack("= 16: + __mmin, __mmax = unpack("<2d", f.read(16)) + # Measure values less than -10e38 are nodata values according to the spec + if next_shape - f.tell() >= nPoints * 8: + self.m = [] + for m in _Array[float]("d", unpack(f"<{nPoints}d", f.read(nPoints * 8))): + if m > NODATA: + self.m.append(m) + else: + self.m.append(None) + else: + self.m = [None for _ in range(nPoints)] + class _HasZ(Shape): z: Sequence[float] + def _set_z_from_shp_file(self, f, nPoints): + __zmin, __zmax = unpack("<2d", f.read(16)) # pylint: disable=unused-private-member + self.z = _Array[float]("d", unpack(f"<{nPoints}d", f.read(nPoints * 8))) + + +class MultiPatch(_HasM, _HasZ, _CanHaveParts): + shapeType = MULTIPATCH -class MultiPatch(_HasM, _HasZ, _CanHaveBBox): - shapeType = 31 + def _set_part_types_from_shp_file(self, f, nParts): + self.partTypes = _Array[int]("i", unpack(f"<{nParts}i", f.read(nParts * 4))) class PointM(Point, _HasM): - shapeType = 21 + shapeType = POINTM # same default as in Writer.__shpRecord (if s.shapeType in (11, 21):) # PyShp encodes None m values as NODATA m = (None,) + def _set_single_point_m_from_shp_file(self, f, next_shape): + if next_shape - f.tell() >= 8: + (m,) = unpack(" NODATA: + self.m = (m,) + else: + self.m = (None,) + class PolylineM(Polyline, _HasM): - shapeType = 23 + shapeType = POLYLINEM class PolygonM(Polygon, _HasM): - shapeType = 25 + shapeType = POLYGONM class MultiPointM(MultiPoint, _HasM): - shapeType = 28 + shapeType = MULTIPOINTM class PointZ(PointM, _HasZ): - shapeType = 11 + shapeType = POINTZ # same default as in Writer.__shpRecord (if s.shapeType == 11:) z: Sequence[float] = (0.0,) + def _set_single_point_z_from_shp_file(self, f): + self.z = tuple(unpack(" Optional[Shape]: """Returns the header info and geometry for a single shape.""" - # pylint: disable=attribute-defined-outside-init f = self.__getFileObj(self.shp) - record = SHAPE_CLASS_FROM_SHAPETYPE[self.shapeType](oid=oid) - # record = Shape(oid=oid) - # Previously, we also set __zmin = __zmax = __mmin = __mmax = None - nParts: Optional[int] = None - nPoints: Optional[int] = None - (__recNum, recLength) = unpack(">2i", f.read(8)) - # Determine the start of the next record - next_shape = f.tell() + (2 * recLength) - shapeType = unpack("= 16: - __mmin, __mmax = unpack("<2d", f.read(16)) - # Measure values less than -10e38 are nodata values according to the spec - if next_shape - f.tell() >= nPoints * 8: - record.m = [] - for m in _Array[float]( - "d", unpack(f"<{nPoints}d", f.read(nPoints * 8)) - ): - if m > NODATA: - record.m.append(m) - else: - record.m.append(None) - else: - record.m = [None for _ in range(nPoints)] - - # Read a single point - # if shapeType in (1, 11, 21): - if isinstance(record, Point): - x, y = _Array[float]("d", unpack("<2d", f.read(16))) - - record.points = [(x, y)] - if bbox is not None: - # create bounding box for Point by duplicating coordinates - # skip shape if no overlap with bounding box - if not bbox_overlap(bbox, (x, y, x, y)): - f.seek(next_shape) - return None - - # Read a single Z value - # if shapeType == 11: - if isinstance(record, PointZ): - record.z = tuple(unpack("= 8: - (m,) = unpack(" NODATA: - record.m = (m,) - else: - record.m = (None,) - - # pylint: enable=attribute-defined-outside-init - # Seek to the end of this record as defined by the record header because - # the shapefile spec doesn't require the actual content to meet the header - # definition. Probably allowed for lazy feature deletion. - - f.seek(next_shape) - - return record + return shape def __shxHeader(self): """Reads the header information from a .shx file.""" @@ -1885,7 +1937,6 @@ def iterShapes(self, bbox: Optional[BBox] = None) -> Iterator[Optional[Shape]]: def __dbfHeader(self): """Reads a dbf header. Xbase-related code borrows heavily from ActiveState Python Cookbook Recipe 362715 by Raymond Hettinger""" - # pylint: disable=attribute-defined-outside-init if not self.dbf: raise ShapefileException( "Shapefile Reader requires a shapefile or file-like object. (no dbf file found)" @@ -1932,8 +1983,6 @@ def __dbfHeader(self): self.__fullRecStruct = recStruct self.__fullRecLookup = recLookup - # pylint: enable=attribute-defined-outside-init - def __recordFmt(self, fields: Optional[Container[str]] = None) -> tuple[str, int]: """Calculates the format and size of a .dbf record. Optional 'fields' arg specifies which fieldnames to unpack and which to ignore. Note that this