diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 18f0b5d..f065f59 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,4 +9,7 @@ repos: hooks: - id: isort name: isort (python) - +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.4 + hooks: + - id: ruff-format diff --git a/pyproject.toml b/pyproject.toml index e4a82a1..df8e737 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,72 @@ requires = ["setuptools"] build-backend = "setuptools.build_meta" + +[tool.ruff] +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +# Same as Black. +line-length = 88 +indent-width = 4 + +# Assume Python 3.9 +target-version = "py37" + +[tool.ruff.lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +select = ["E4", "E7", "E9", "F"] +ignore = [] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + + + [tool.pylint.MASTER] load-plugins=[ "pylint_per_file_ignores", diff --git a/shapefile.py b/shapefile.py index b218103..12af74d 100644 --- a/shapefile.py +++ b/shapefile.py @@ -42,20 +42,21 @@ MULTIPATCH = 31 SHAPETYPE_LOOKUP = { - 0: 'NULL', - 1: 'POINT', - 3: 'POLYLINE', - 5: 'POLYGON', - 8: 'MULTIPOINT', - 11: 'POINTZ', - 13: 'POLYLINEZ', - 15: 'POLYGONZ', - 18: 'MULTIPOINTZ', - 21: 'POINTM', - 23: 'POLYLINEM', - 25: 'POLYGONM', - 28: 'MULTIPOINTM', - 31: 'MULTIPATCH'} + 0: "NULL", + 1: "POINT", + 3: "POLYLINE", + 5: "POLYGON", + 8: "MULTIPOINT", + 11: "POINTZ", + 13: "POLYLINEZ", + 15: "POLYGONZ", + 18: "MULTIPOINTZ", + 21: "POINTM", + 23: "POLYLINEM", + 25: "POLYGONM", + 28: "MULTIPOINTM", + 31: "MULTIPATCH", +} TRIANGLE_STRIP = 0 TRIANGLE_FAN = 1 @@ -65,12 +66,13 @@ RING = 5 PARTTYPE_LOOKUP = { - 0: 'TRIANGLE_STRIP', - 1: 'TRIANGLE_FAN', - 2: 'OUTER_RING', - 3: 'INNER_RING', - 4: 'FIRST_RING', - 5: 'RING'} + 0: "TRIANGLE_STRIP", + 1: "TRIANGLE_FAN", + 2: "OUTER_RING", + 3: "INNER_RING", + 4: "FIRST_RING", + 5: "RING", +} # Python 2-3 handling @@ -94,11 +96,12 @@ # Helpers -MISSING = [None,''] -NODATA = -10e38 # as per the ESRI shapefile spec, only used for m-values. +MISSING = [None, ""] +NODATA = -10e38 # as per the ESRI shapefile spec, only used for m-values. if PYTHON3: - def b(v, encoding='utf-8', encodingErrors='strict'): + + def b(v, encoding="utf-8", encodingErrors="strict"): if isinstance(v, str): # For python 3 encode str to bytes. return v.encode(encoding, encodingErrors) @@ -112,7 +115,7 @@ def b(v, encoding='utf-8', encodingErrors='strict'): # Force string representation. return str(v).encode(encoding, encodingErrors) - def u(v, encoding='utf-8', encodingErrors='strict'): + def u(v, encoding="utf-8", encodingErrors="strict"): if isinstance(v, bytes): # For python 3 decode bytes to str. return v.decode(encoding, encodingErrors) @@ -130,7 +133,8 @@ def is_string(v): return isinstance(v, str) else: - def b(v, encoding='utf-8', encodingErrors='strict'): + + def b(v, encoding="utf-8", encodingErrors="strict"): if isinstance(v, unicode): # For python 2 encode unicode to bytes. return v.encode(encoding, encodingErrors) @@ -144,7 +148,7 @@ def b(v, encoding='utf-8', encodingErrors='strict'): # Force string representation. return unicode(v).encode(encoding, encodingErrors) - def u(v, encoding='utf-8', encodingErrors='strict'): + def u(v, encoding="utf-8", encodingErrors="strict"): if isinstance(v, bytes): # For python 2 decode bytes to unicode. return v.decode(encoding, encodingErrors) @@ -153,7 +157,7 @@ def u(v, encoding='utf-8', encodingErrors='strict'): return v elif v is None: # Since we're dealing with text, interpret None as "" - return u"" + return "" else: # Force string representation. return bytes(v).decode(encoding, encodingErrors) @@ -161,13 +165,16 @@ def u(v, encoding='utf-8', encodingErrors='strict'): def is_string(v): return isinstance(v, basestring) + if sys.version_info[0:2] >= (3, 6): + def pathlike_obj(path): if isinstance(path, os.PathLike): return os.fsdecode(path) else: return path else: + def pathlike_obj(path): if is_string(path): return path @@ -182,27 +189,31 @@ def pathlike_obj(path): # Begin + class _Array(array.array): """Converts python tuples to lists of the appropriate type. Used to unpack different shapefile header parts.""" + def __repr__(self): return str(self.tolist()) + def signed_area(coords, fast=False): """Return the signed area enclosed by a ring using the linear time algorithm. A value >= 0 indicates a counter-clockwise oriented ring. A faster version is possible by setting 'fast' to True, which returns 2x the area, e.g. if you're only interested in the sign of the area. """ - xs, ys = map(list, list(zip(*coords))[:2]) # ignore any z or m values + xs, ys = map(list, list(zip(*coords))[:2]) # ignore any z or m values xs.append(xs[1]) ys.append(ys[1]) - area2 = sum(xs[i]*(ys[i+1]-ys[i-1]) for i in range(1, len(coords))) + area2 = sum(xs[i] * (ys[i + 1] - ys[i - 1]) for i in range(1, len(coords))) if fast: return area2 else: return area2 / 2.0 + def is_cw(coords): """Returns True if a polygon ring has clockwise orientation, determined by a negatively signed area. @@ -210,34 +221,35 @@ def is_cw(coords): area2 = signed_area(coords, fast=True) return area2 < 0 + def rewind(coords): - """Returns the input coords in reversed order. - """ + """Returns the input coords in reversed order.""" return list(reversed(coords)) + def ring_bbox(coords): - """Calculates and returns the bounding box of a ring. - """ - xs,ys = zip(*coords) - bbox = min(xs),min(ys),max(xs),max(ys) + """Calculates and returns the bounding box of a ring.""" + xs, ys = zip(*coords) + bbox = min(xs), min(ys), max(xs), max(ys) return bbox + def bbox_overlap(bbox1, bbox2): - """Tests whether two bounding boxes overlap, returning a boolean - """ - xmin1,ymin1,xmax1,ymax1 = bbox1 - xmin2,ymin2,xmax2,ymax2 = bbox2 - overlap = (xmin1 <= xmax2 and xmax1 >= xmin2 and ymin1 <= ymax2 and ymax1 >= ymin2) + """Tests whether two bounding boxes overlap, returning a boolean""" + xmin1, ymin1, xmax1, ymax1 = bbox1 + xmin2, ymin2, xmax2, ymax2 = bbox2 + overlap = xmin1 <= xmax2 and xmax1 >= xmin2 and ymin1 <= ymax2 and ymax1 >= ymin2 return overlap + def bbox_contains(bbox1, bbox2): - """Tests whether bbox1 fully contains bbox2, returning a boolean - """ - xmin1,ymin1,xmax1,ymax1 = bbox1 - xmin2,ymin2,xmax2,ymax2 = bbox2 - contains = (xmin1 < xmin2 and xmax1 > xmax2 and ymin1 < ymin2 and ymax1 > ymax2) + """Tests whether bbox1 fully contains bbox2, returning a boolean""" + xmin1, ymin1, xmax1, ymax1 = bbox1 + xmin2, ymin2, xmax2, ymax2 = bbox2 + contains = xmin1 < xmin2 and xmax1 > xmax2 and ymin1 < ymin2 and ymax1 > ymax2 return contains + def ring_contains_point(coords, p): """Fast point-in-polygon crossings algorithm, MacMartin optimization. @@ -249,29 +261,31 @@ def ring_contains_point(coords, p): compare vertex Y values to the testing point's Y and quickly discard edges which are entirely to one side of the test ray. """ - tx,ty = p + tx, ty = p # get initial test bit for above/below X axis vtx0 = coords[0] - yflag0 = ( vtx0[1] >= ty ) + yflag0 = vtx0[1] >= ty inside_flag = False for vtx1 in coords[1:]: - yflag1 = ( vtx1[1] >= ty ) + yflag1 = vtx1[1] >= ty # check if endpoints straddle (are on opposite sides) of X axis # (i.e. the Y's differ); if so, +X ray could intersect this edge. if yflag0 != yflag1: - xflag0 = ( vtx0[0] >= tx ) + xflag0 = vtx0[0] >= tx # check if endpoints are on same side of the Y axis (i.e. X's # are the same); if so, it's easy to test if edge hits or misses. - if xflag0 == ( vtx1[0] >= tx ): + if xflag0 == (vtx1[0] >= tx): # if edge's X values both right of the point, must hit if xflag0: inside_flag = not inside_flag else: # compute intersection of pgon segment with +X ray, note # if >= point's X; if so, the ray hits it. - if ( vtx1[0] - (vtx1[1]-ty) * ( vtx0[0]-vtx1[0]) / (vtx0[1]-vtx1[1]) ) >= tx: + if ( + vtx1[0] - (vtx1[1] - ty) * (vtx0[0] - vtx1[0]) / (vtx0[1] - vtx1[1]) + ) >= tx: inside_flag = not inside_flag # move to next pair of vertices, retaining info as possible @@ -280,6 +294,7 @@ def ring_contains_point(coords, p): return inside_flag + def ring_sample(coords, ccw=False): """Return a sample point guaranteed to be within a ring, by efficiently finding the first centroid of a coordinate triplet whose orientation @@ -288,6 +303,7 @@ def ring_sample(coords, ccw=False): (counter-clockwise) is set to True. """ triplet = [] + def itercoords(): # iterate full closed ring for p in coords: @@ -303,7 +319,9 @@ def itercoords(): # new triplet, try to get sample if len(triplet) == 3: # check that triplet does not form a straight line (not a triangle) - is_straight_line = (triplet[0][1] - triplet[1][1]) * (triplet[0][0] - triplet[2][0]) == (triplet[0][1] - triplet[2][1]) * (triplet[0][0] - triplet[1][0]) + is_straight_line = (triplet[0][1] - triplet[1][1]) * ( + triplet[0][0] - triplet[2][0] + ) == (triplet[0][1] - triplet[2][1]) * (triplet[0][0] - triplet[1][0]) if not is_straight_line: # get triplet orientation closed_triplet = triplet + [triplet[0]] @@ -311,26 +329,27 @@ def itercoords(): # check that triplet has the same orientation as the ring (means triangle is inside the ring) if ccw == triplet_ccw: # get triplet centroid - xs,ys = zip(*triplet) - xmean,ymean = sum(xs) / 3.0, sum(ys) / 3.0 + xs, ys = zip(*triplet) + xmean, ymean = sum(xs) / 3.0, sum(ys) / 3.0 # check that triplet centroid is truly inside the ring - if ring_contains_point(coords, (xmean,ymean)): - return xmean,ymean + if ring_contains_point(coords, (xmean, ymean)): + return xmean, ymean # failed to get sample point from this triplet # remove oldest triplet coord to allow iterating to next triplet triplet.pop(0) else: - raise Exception('Unexpected error: Unable to find a ring sample point.') + raise Exception("Unexpected error: Unable to find a ring sample point.") + def ring_contains_ring(coords1, coords2): - '''Returns True if all vertexes in coords2 are fully inside coords1. - ''' + """Returns True if all vertexes in coords2 are fully inside coords1.""" return all((ring_contains_point(coords1, p2) for p2 in coords2)) + def organize_polygon_rings(rings, return_errors=None): - '''Organize a list of coordinate rings into one or more polygons with holes. + """Organize a list of coordinate rings into one or more polygons with holes. Returns a list of polygons, where each polygon is composed of a single exterior ring, and one or more interior holes. If a return_errors dict is provided (optional), any errors encountered will be added to it. @@ -340,7 +359,7 @@ def organize_polygon_rings(rings, return_errors=None): holes if they run in counter-clockwise direction. This method is used to construct GeoJSON (multi)polygons from the shapefile polygon shape type, which does not explicitly store the structure of the polygons beyond exterior/interior ring orientation. - ''' + """ # first iterate rings and classify as exterior or hole exteriors = [] holes = [] @@ -374,17 +393,16 @@ def organize_polygon_rings(rings, return_errors=None): return polys # first determine each hole's candidate exteriors based on simple bbox contains test - hole_exteriors = dict([(hole_i,[]) for hole_i in xrange(len(holes))]) + hole_exteriors = dict([(hole_i, []) for hole_i in xrange(len(holes))]) exterior_bboxes = [ring_bbox(ring) for ring in exteriors] for hole_i in hole_exteriors.keys(): hole_bbox = ring_bbox(holes[hole_i]) - for ext_i,ext_bbox in enumerate(exterior_bboxes): + for ext_i, ext_bbox in enumerate(exterior_bboxes): if bbox_contains(ext_bbox, hole_bbox): - hole_exteriors[hole_i].append( ext_i ) + hole_exteriors[hole_i].append(ext_i) # then, for holes with still more than one possible exterior, do more detailed hole-in-ring test - for hole_i,exterior_candidates in hole_exteriors.items(): - + for hole_i, exterior_candidates in hole_exteriors.items(): if len(exterior_candidates) > 1: # get hole sample point ccw = not is_cw(holes[hole_i]) @@ -393,7 +411,9 @@ def organize_polygon_rings(rings, return_errors=None): new_exterior_candidates = [] for ext_i in exterior_candidates: # check that hole sample point is inside exterior - hole_in_exterior = ring_contains_point(exteriors[ext_i], hole_sample) + hole_in_exterior = ring_contains_point( + exteriors[ext_i], hole_sample + ) if hole_in_exterior: new_exterior_candidates.append(ext_i) @@ -401,31 +421,33 @@ def organize_polygon_rings(rings, return_errors=None): hole_exteriors[hole_i] = new_exterior_candidates # if still holes with more than one possible exterior, means we have an exterior hole nested inside another exterior's hole - for hole_i,exterior_candidates in hole_exteriors.items(): - + for hole_i, exterior_candidates in hole_exteriors.items(): if len(exterior_candidates) > 1: # exterior candidate with the smallest area is the hole's most immediate parent - ext_i = sorted(exterior_candidates, key=lambda x: abs(signed_area(exteriors[x], fast=True)))[0] + ext_i = sorted( + exterior_candidates, + key=lambda x: abs(signed_area(exteriors[x], fast=True)), + )[0] hole_exteriors[hole_i] = [ext_i] # separate out holes that are orphaned (not contained by any exterior) orphan_holes = [] - for hole_i,exterior_candidates in list(hole_exteriors.items()): + for hole_i, exterior_candidates in list(hole_exteriors.items()): if not exterior_candidates: - orphan_holes.append( hole_i ) + orphan_holes.append(hole_i) del hole_exteriors[hole_i] continue # each hole should now only belong to one exterior, group into exterior-holes polygons polys = [] - for ext_i,ext in enumerate(exteriors): + for ext_i, ext in enumerate(exteriors): poly = [ext] # find relevant holes poly_holes = [] - for hole_i,exterior_candidates in list(hole_exteriors.items()): + for hole_i, exterior_candidates in list(hole_exteriors.items()): # hole is relevant if previously matched with this exterior if exterior_candidates[0] == ext_i: - poly_holes.append( holes[hole_i] ) + poly_holes.append(holes[hole_i]) poly += poly_holes polys.append(poly) @@ -437,21 +459,24 @@ def organize_polygon_rings(rings, return_errors=None): polys.append(poly) if orphan_holes and return_errors is not None: - return_errors['polygon_orphaned_holes'] = len(orphan_holes) + return_errors["polygon_orphaned_holes"] = len(orphan_holes) return polys # no exteriors, be nice and assume due to incorrect winding order else: if return_errors is not None: - return_errors['polygon_only_holes'] = len(holes) + return_errors["polygon_only_holes"] = len(holes) exteriors = holes # add as single exterior without any holes polys = [[ext] for ext in exteriors] return polys + class Shape(object): - def __init__(self, shapeType=NULL, points=None, parts=None, partTypes=None, oid=None): + def __init__( + self, shapeType=NULL, points=None, parts=None, partTypes=None, oid=None + ): """Stores the geometry of the different shape types specified in the Shapefile spec. Shape types are usually point, polyline, or polygons. Every shape type @@ -486,42 +511,39 @@ def __geo_interface__(self): # the shape has no coordinate information, i.e. is 'empty' # the geojson spec does not define a proper null-geometry type # however, it does allow geometry types with 'empty' coordinates to be interpreted as null-geometries - return {'type':'Point', 'coordinates':tuple()} + return {"type": "Point", "coordinates": tuple()} else: - return { - 'type': 'Point', - 'coordinates': tuple(self.points[0]) - } + return {"type": "Point", "coordinates": tuple(self.points[0])} elif self.shapeType in [MULTIPOINT, MULTIPOINTM, MULTIPOINTZ]: if len(self.points) == 0: # the shape has no coordinate information, i.e. is 'empty' # the geojson spec does not define a proper null-geometry type # however, it does allow geometry types with 'empty' coordinates to be interpreted as null-geometries - return {'type':'MultiPoint', 'coordinates':[]} + return {"type": "MultiPoint", "coordinates": []} else: # multipoint return { - 'type': 'MultiPoint', - 'coordinates': [tuple(p) for p in self.points] + "type": "MultiPoint", + "coordinates": [tuple(p) for p in self.points], } elif self.shapeType in [POLYLINE, POLYLINEM, POLYLINEZ]: if len(self.parts) == 0: # the shape has no coordinate information, i.e. is 'empty' # the geojson spec does not define a proper null-geometry type # however, it does allow geometry types with 'empty' coordinates to be interpreted as null-geometries - return {'type':'LineString', 'coordinates':[]} + return {"type": "LineString", "coordinates": []} elif len(self.parts) == 1: # linestring return { - 'type': 'LineString', - 'coordinates': [tuple(p) for p in self.points] + "type": "LineString", + "coordinates": [tuple(p) for p in self.points], } else: # multilinestring ps = None coordinates = [] for part in self.parts: - if ps == None: + if ps is None: ps = part continue else: @@ -529,16 +551,13 @@ def __geo_interface__(self): ps = part else: coordinates.append([tuple(p) for p in self.points[part:]]) - return { - 'type': 'MultiLineString', - 'coordinates': coordinates - } + return {"type": "MultiLineString", "coordinates": coordinates} elif self.shapeType in [POLYGON, POLYGONM, POLYGONZ]: if len(self.parts) == 0: # the shape has no coordinate information, i.e. is 'empty' # the geojson spec does not define a proper null-geometry type # however, it does allow geometry types with 'empty' coordinates to be interpreted as null-geometries - return {'type':'Polygon', 'coordinates':[]} + return {"type": "Polygon", "coordinates": []} else: # get all polygon rings rings = [] @@ -546,7 +565,7 @@ def __geo_interface__(self): # get indexes of start and end points of the ring start = self.parts[i] try: - end = self.parts[i+1] + end = self.parts[i + 1] except IndexError: end = len(self.points) @@ -561,35 +580,40 @@ def __geo_interface__(self): # if VERBOSE is True, issue detailed warning about any shape errors # encountered during the Shapefile to GeoJSON conversion if VERBOSE and self._errors: - header = 'Possible issue encountered when converting Shape #{} to GeoJSON: '.format(self.oid) - orphans = self._errors.get('polygon_orphaned_holes', None) + header = "Possible issue encountered when converting Shape #{} to GeoJSON: ".format( + self.oid + ) + orphans = self._errors.get("polygon_orphaned_holes", None) if orphans: - msg = header + 'Shapefile format requires that all polygon interior holes be contained by an exterior ring, \ + msg = ( + header + + "Shapefile format requires that all polygon interior holes be contained by an exterior ring, \ but the Shape contained interior holes (defined by counter-clockwise orientation in the shapefile format) that were \ orphaned, i.e. not contained by any exterior rings. The rings were still included but were \ -encoded as GeoJSON exterior rings instead of holes.' +encoded as GeoJSON exterior rings instead of holes." + ) logger.warning(msg) - only_holes = self._errors.get('polygon_only_holes', None) + only_holes = self._errors.get("polygon_only_holes", None) if only_holes: - msg = header + 'Shapefile format requires that polygons contain at least one exterior ring, \ + msg = ( + header + + "Shapefile format requires that polygons contain at least one exterior ring, \ but the Shape was entirely made up of interior holes (defined by counter-clockwise orientation in the shapefile format). The rings were \ -still included but were encoded as GeoJSON exterior rings instead of holes.' +still included but were encoded as GeoJSON exterior rings instead of holes." + ) logger.warning(msg) # return as geojson if len(polys) == 1: - return { - 'type': 'Polygon', - 'coordinates': polys[0] - } + return {"type": "Polygon", "coordinates": polys[0]} else: - return { - 'type': 'MultiPolygon', - 'coordinates': polys - } + return {"type": "MultiPolygon", "coordinates": polys} else: - raise Exception('Shape type "%s" cannot be represented as GeoJSON.' % SHAPETYPE_LOOKUP[self.shapeType]) + raise Exception( + 'Shape type "%s" cannot be represented as GeoJSON.' + % SHAPETYPE_LOOKUP[self.shapeType] + ) @staticmethod def _from_geojson(geoj): @@ -617,16 +641,16 @@ def _from_geojson(geoj): # set points and parts if geojType == "Point": - shape.points = [ geoj["coordinates"] ] + shape.points = [geoj["coordinates"]] shape.parts = [0] - elif geojType in ("MultiPoint","LineString"): + elif geojType in ("MultiPoint", "LineString"): shape.points = geoj["coordinates"] shape.parts = [0] elif geojType in ("Polygon"): points = [] parts = [] index = 0 - for i,ext_or_hole in enumerate(geoj["coordinates"]): + for i, ext_or_hole in enumerate(geoj["coordinates"]): # although the latest GeoJSON spec states that exterior rings should have # counter-clockwise orientation, we explicitly check orientation since older # GeoJSONs might not enforce this. @@ -656,7 +680,7 @@ def _from_geojson(geoj): parts = [] index = 0 for polygon in geoj["coordinates"]: - for i,ext_or_hole in enumerate(polygon): + for i, ext_or_hole in enumerate(polygon): # although the latest GeoJSON spec states that exterior rings should have # counter-clockwise orientation, we explicitly check orientation since older # GeoJSONs might not enforce this. @@ -683,7 +707,8 @@ def shapeTypeName(self): return SHAPETYPE_LOOKUP[self.shapeType] def __repr__(self): - return 'Shape #{}: {}'.format(self.__oid, self.shapeTypeName) + return "Shape #{}: {}".format(self.__oid, self.shapeTypeName) + class _Record(list): """ @@ -728,14 +753,16 @@ def __getattr__(self, item): corresponding value in the Record does not exist """ try: - if item == "__setstate__": # Prevent infinite loop from copy.deepcopy() - raise AttributeError('_Record does not implement __setstate__') + if item == "__setstate__": # Prevent infinite loop from copy.deepcopy() + raise AttributeError("_Record does not implement __setstate__") index = self.__field_positions[item] return list.__getitem__(self, index) except KeyError: - raise AttributeError('{} is not a field name'.format(item)) + raise AttributeError("{} is not a field name".format(item)) except IndexError: - raise IndexError('{} found as a field but not enough values available.'.format(item)) + raise IndexError( + "{} found as a field but not enough values available.".format(item) + ) def __setattr__(self, key, value): """ @@ -745,13 +772,13 @@ def __setattr__(self, key, value): :return: None :raises: AttributeError, if key is not a field of the shapefile """ - if key.startswith('_'): # Prevent infinite loop when setting mangled attribute + if key.startswith("_"): # Prevent infinite loop when setting mangled attribute return list.__setattr__(self, key, value) try: index = self.__field_positions[key] return list.__setitem__(self, index, value) except KeyError: - raise AttributeError('{} is not a field name'.format(key)) + raise AttributeError("{} is not a field name".format(key)) def __getitem__(self, item): """ @@ -790,7 +817,7 @@ def __setitem__(self, key, value): if index is not None: return list.__setitem__(self, index, value) else: - raise IndexError('{} is not a field name and not an int'.format(key)) + raise IndexError("{} is not a field name and not an int".format(key)) @property def oid(self): @@ -804,13 +831,13 @@ def as_dict(self, date_strings=False): """ dct = dict((f, self[i]) for f, i in self.__field_positions.items()) if date_strings: - for k,v in dct.items(): + for k, v in dct.items(): if isinstance(v, date): - dct[k] = '{:04d}{:02d}{:02d}'.format(v.year, v.month, v.day) + dct[k] = "{:04d}{:02d}{:02d}".format(v.year, v.month, v.day) return dct def __repr__(self): - return 'Record #{}: {}'.format(self.__oid, list(self)) + return "Record #{}: {}".format(self.__oid, list(self)) def __dir__(self): """ @@ -819,22 +846,33 @@ def __dir__(self): :return: List of method names and fields """ - default = list(dir(type(self))) # default list methods and attributes of this class - fnames = list(self.__field_positions.keys()) # plus field names (random order if Python version < 3.6) + default = list( + dir(type(self)) + ) # default list methods and attributes of this class + fnames = list( + self.__field_positions.keys() + ) # plus field names (random order if Python version < 3.6) return default + fnames + class ShapeRecord(object): """A ShapeRecord object containing a shape along with its attributes. Provides the GeoJSON __geo_interface__ to return a Feature dictionary.""" + def __init__(self, shape=None, record=None): self.shape = shape self.record = record @property def __geo_interface__(self): - return {'type': 'Feature', - 'properties': self.record.as_dict(date_strings=True), - 'geometry': None if self.shape.shapeType == NULL else self.shape.__geo_interface__} + return { + "type": "Feature", + "properties": self.record.as_dict(date_strings=True), + "geometry": None + if self.shape.shapeType == NULL + else self.shape.__geo_interface__, + } + class Shapes(list): """A class to hold a list of Shape objects. Subclasses list to ensure compatibility with @@ -843,16 +881,19 @@ class Shapes(list): to return a GeometryCollection dictionary.""" def __repr__(self): - return 'Shapes: {}'.format(list(self)) + return "Shapes: {}".format(list(self)) @property def __geo_interface__(self): # Note: currently this will fail if any of the shapes are null-geometries # could be fixed by storing the shapefile shapeType upon init, returning geojson type with empty coords - collection = {'type': 'GeometryCollection', - 'geometries': [shape.__geo_interface__ for shape in self]} + collection = { + "type": "GeometryCollection", + "geometries": [shape.__geo_interface__ for shape in self], + } return collection + class ShapeRecords(list): """A class to hold a list of ShapeRecord objects. Subclasses list to ensure compatibility with former work and to reuse all the optimizations of the builtin list. @@ -860,18 +901,23 @@ class ShapeRecords(list): to return a FeatureCollection dictionary.""" def __repr__(self): - return 'ShapeRecords: {}'.format(list(self)) + return "ShapeRecords: {}".format(list(self)) @property def __geo_interface__(self): - collection = {'type': 'FeatureCollection', - 'features': [shaperec.__geo_interface__ for shaperec in self]} + collection = { + "type": "FeatureCollection", + "features": [shaperec.__geo_interface__ for shaperec in self], + } return collection + class ShapefileException(Exception): """An exception to handle shapefile specific problems.""" + pass + class Reader(object): """Reads the three files of a shapefile as a unit or separately. If one of the three files (.shp, .shx, @@ -892,6 +938,7 @@ class Reader(object): efficiently as possible. Shapefiles are usually not large but they can be. """ + def __init__(self, *args, **kwargs): self.shp = None self.shx = None @@ -905,61 +952,81 @@ def __init__(self, *args, **kwargs): self.fields = [] self.__dbfHdrLength = 0 self.__fieldLookup = {} - self.encoding = kwargs.pop('encoding', 'utf-8') - self.encodingErrors = kwargs.pop('encodingErrors', 'strict') + self.encoding = kwargs.pop("encoding", "utf-8") + self.encodingErrors = kwargs.pop("encodingErrors", "strict") # See if a shapefile name was passed as the first argument if len(args) > 0: path = pathlike_obj(args[0]) if is_string(path): - - if '.zip' in path: + if ".zip" in path: # Shapefile is inside a zipfile - if path.count('.zip') > 1: + if path.count(".zip") > 1: # Multiple nested zipfiles - raise ShapefileException('Reading from multiple nested zipfiles is not supported: %s' % path) + raise ShapefileException( + "Reading from multiple nested zipfiles is not supported: %s" + % path + ) # Split into zipfile and shapefile paths - if path.endswith('.zip'): + if path.endswith(".zip"): zpath = path shapefile = None else: - zpath = path[:path.find('.zip')+4] - shapefile = path[path.find('.zip')+4+1:] + zpath = path[: path.find(".zip") + 4] + shapefile = path[path.find(".zip") + 4 + 1 :] # Create a zip file handle - if zpath.startswith('http'): + if zpath.startswith("http"): # Zipfile is from a url # Download to a temporary url and treat as normal zipfile - req = Request(zpath, headers={'User-agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36'}) + req = Request( + zpath, + headers={ + "User-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36" + }, + ) resp = urlopen(req) # write zipfile data to a read+write tempfile and use as source, gets deleted when garbage collected - zipfileobj = tempfile.NamedTemporaryFile(mode='w+b', suffix='.zip', delete=True) + zipfileobj = tempfile.NamedTemporaryFile( + mode="w+b", suffix=".zip", delete=True + ) zipfileobj.write(resp.read()) zipfileobj.seek(0) else: # Zipfile is from a file - zipfileobj = open(zpath, mode='rb') + zipfileobj = open(zpath, mode="rb") # Open the zipfile archive - with zipfile.ZipFile(zipfileobj, 'r') as archive: + with zipfile.ZipFile(zipfileobj, "r") as archive: if not shapefile: # Only the zipfile path is given # Inspect zipfile contents to find the full shapefile path - shapefiles = [name - for name in archive.namelist() - if (name.endswith('.SHP') or name.endswith('.shp'))] + shapefiles = [ + name + for name in archive.namelist() + if (name.endswith(".SHP") or name.endswith(".shp")) + ] # The zipfile must contain exactly one shapefile if len(shapefiles) == 0: - raise ShapefileException('Zipfile does not contain any shapefiles') + raise ShapefileException( + "Zipfile does not contain any shapefiles" + ) elif len(shapefiles) == 1: shapefile = shapefiles[0] else: - raise ShapefileException('Zipfile contains more than one shapefile: %s. Please specify the full \ - path to the shapefile you would like to open.' % shapefiles ) + raise ShapefileException( + "Zipfile contains more than one shapefile: %s. Please specify the full \ + path to the shapefile you would like to open." + % shapefiles + ) # Try to extract file-like objects from zipfile - shapefile = os.path.splitext(shapefile)[0] # root shapefile name - for ext in ['SHP','SHX','DBF','shp','shx','dbf']: + shapefile = os.path.splitext(shapefile)[ + 0 + ] # root shapefile name + for ext in ["SHP", "SHX", "DBF", "shp", "shx", "dbf"]: try: - member = archive.open(shapefile+'.'+ext) + member = archive.open(shapefile + "." + ext) # write zipfile member data to a read+write tempfile and use as source, gets deleted on close() - fileobj = tempfile.NamedTemporaryFile(mode='w+b', delete=True) + fileobj = tempfile.NamedTemporaryFile( + mode="w+b", delete=True + ) fileobj.write(member.read()) fileobj.seek(0) setattr(self, ext.lower(), fileobj) @@ -967,44 +1034,57 @@ def __init__(self, *args, **kwargs): except: pass # Close and delete the temporary zipfile - try: zipfileobj.close() - except: pass + try: + zipfileobj.close() + except: + pass # Try to load shapefile - if (self.shp or self.dbf): + if self.shp or self.dbf: # Load and exit early self.load() return else: - raise ShapefileException("No shp or dbf file found in zipfile: %s" % path) + raise ShapefileException( + "No shp or dbf file found in zipfile: %s" % path + ) - elif path.startswith('http'): + elif path.startswith("http"): # Shapefile is from a url # Download each file to temporary path and treat as normal shapefile path urlinfo = urlparse(path) urlpath = urlinfo[2] - urlpath,_ = os.path.splitext(urlpath) + urlpath, _ = os.path.splitext(urlpath) shapefile = os.path.basename(urlpath) - for ext in ['shp','shx','dbf']: + for ext in ["shp", "shx", "dbf"]: try: _urlinfo = list(urlinfo) - _urlinfo[2] = urlpath + '.' + ext + _urlinfo[2] = urlpath + "." + ext _path = urlunparse(_urlinfo) - req = Request(_path, headers={'User-agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36'}) + req = Request( + _path, + headers={ + "User-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36" + }, + ) resp = urlopen(req) # write url data to a read+write tempfile and use as source, gets deleted on close() - fileobj = tempfile.NamedTemporaryFile(mode='w+b', delete=True) + fileobj = tempfile.NamedTemporaryFile( + mode="w+b", delete=True + ) fileobj.write(resp.read()) fileobj.seek(0) setattr(self, ext, fileobj) self._files_to_close.append(fileobj) except HTTPError: pass - if (self.shp or self.dbf): + if self.shp or self.dbf: # Load and exit early self.load() return else: - raise ShapefileException("No shp or dbf file found at url: %s" % path) + raise ShapefileException( + "No shp or dbf file found at url: %s" % path + ) else: # Local file path to a shapefile @@ -1057,14 +1137,18 @@ def __str__(self): """ Use some general info on the shapefile as __str__ """ - info = ['shapefile Reader'] + info = ["shapefile Reader"] if self.shp: - info.append(" {} shapes (type '{}')".format( - len(self), SHAPETYPE_LOOKUP[self.shapeType])) + info.append( + " {} shapes (type '{}')".format( + len(self), SHAPETYPE_LOOKUP[self.shapeType] + ) + ) if self.dbf: - info.append(' {} records ({} fields)'.format( - len(self), len(self.fields))) - return '\n'.join(info) + info.append( + " {} records ({} fields)".format(len(self), len(self.fields)) + ) + return "\n".join(info) def __enter__(self): """ @@ -1101,11 +1185,11 @@ def __len__(self): # Determine length of shp file shp = self.shp checkpoint = shp.tell() - shp.seek(0,2) + shp.seek(0, 2) shpLength = shp.tell() shp.seek(100) # Do a fast shape iteration until end of file. - unpack = Struct('>2i').unpack + unpack = Struct(">2i").unpack offsets = [] pos = shp.tell() while pos < shpLength: @@ -1136,7 +1220,7 @@ def __iter__(self): def __geo_interface__(self): shaperecords = self.shapeRecords() fcollection = shaperecords.__geo_interface__ - fcollection['bbox'] = list(self.bbox) + fcollection["bbox"] = list(self.bbox) return fcollection @property @@ -1154,7 +1238,9 @@ def load(self, shapefile=None): self.load_shx(shapeName) self.load_dbf(shapeName) if not (self.shp or self.dbf): - raise ShapefileException("Unable to open %s.dbf or %s.shp." % (shapeName, shapeName)) + raise ShapefileException( + "Unable to open %s.dbf or %s.shp." % (shapeName, shapeName) + ) if self.shp: self.__shpHeader() if self.dbf: @@ -1166,7 +1252,7 @@ def load_shp(self, shapefile_name): """ Attempts to load file with .shp extension as both lower and upper case """ - shp_ext = 'shp' + shp_ext = "shp" try: self.shp = open("%s.%s" % (shapefile_name, shp_ext), "rb") self._files_to_close.append(self.shp) @@ -1181,7 +1267,7 @@ def load_shx(self, shapefile_name): """ Attempts to load file with .shx extension as both lower and upper case """ - shx_ext = 'shx' + shx_ext = "shx" try: self.shx = open("%s.%s" % (shapefile_name, shx_ext), "rb") self._files_to_close.append(self.shx) @@ -1196,7 +1282,7 @@ def load_dbf(self, shapefile_name): """ Attempts to load file with .dbf extension as both lower and upper case """ - dbf_ext = 'dbf' + dbf_ext = "dbf" try: self.dbf = open("%s.%s" % (shapefile_name, dbf_ext), "rb") self._files_to_close.append(self.dbf) @@ -1213,7 +1299,7 @@ def __del__(self): def close(self): # Close any files that the reader opened (but not those given by user) for attribute in self._files_to_close: - if hasattr(attribute, 'close'): + if hasattr(attribute, "close"): try: attribute.close() except IOError: @@ -1224,7 +1310,9 @@ def __getFileObj(self, f): """Checks to see if the requested shapefile file object is available. If not a ShapefileException is raised.""" if not f: - raise ShapefileException("Shapefile Reader requires a shapefile or file-like object.") + raise ShapefileException( + "Shapefile Reader requires a shapefile or file-like object." + ) if self.shp and self.shpLength is None: self.load() if self.dbf and len(self.fields) == 0: @@ -1238,27 +1326,30 @@ def __restrictIndex(self, i): rmax = self.numRecords - 1 if abs(i) > rmax: raise IndexError("Shape or Record index out of range.") - if i < 0: i = range(self.numRecords)[i] + if i < 0: + i = range(self.numRecords)[i] return i def __shpHeader(self): """Reads the header information from a .shp file.""" if not self.shp: - raise ShapefileException("Shapefile Reader requires a shapefile or file-like object. (no shp file found") + raise ShapefileException( + "Shapefile Reader requires a shapefile or file-like object. (no shp file found" + ) shp = self.shp # File length (16-bit word * 2 = bytes) shp.seek(24) self.shpLength = unpack(">i", shp.read(4))[0] * 2 # Shape type shp.seek(32) - self.shapeType= unpack(" NODATA: self.mbox.append(m) @@ -1279,8 +1370,8 @@ def __shape(self, oid=None, bbox=None): if shapeType == 0: record.points = [] # All shape types capable of having a bounding box - elif shapeType in (3,5,8,13,15,18,23,25,28,31): - record.bbox = _Array('d', unpack("<4d", f.read(32))) + elif shapeType in (3, 5, 8, 13, 15, 18, 23, 25, 28, 31): + record.bbox = _Array("d", unpack("<4d", f.read(32))) # if bbox specified and no overlap, skip this shape if bbox is not None and not bbox_overlap(bbox, record.bbox): # because we stop parsing this shape, skip to beginning of @@ -1288,33 +1379,33 @@ def __shape(self, oid=None, bbox=None): f.seek(next) return None # Shape types with parts - if shapeType in (3,5,13,15,23,25,31): + if shapeType in (3, 5, 13, 15, 23, 25, 31): nParts = unpack("= 16: (mmin, mmax) = unpack("<2d", f.read(16)) # Measure values less than -10e38 are nodata values according to the spec if next - f.tell() >= nPoints * 8: record.m = [] - for m in _Array('d', unpack("<%sd" % nPoints, f.read(nPoints * 8))): + for m in _Array("d", unpack("<%sd" % nPoints, f.read(nPoints * 8))): if m > NODATA: record.m.append(m) else: @@ -1322,8 +1413,8 @@ def __shape(self, oid=None, bbox=None): else: record.m = [None for _ in range(nPoints)] # Read a single point - if shapeType in (1,11,21): - record.points = [_Array('d', unpack("<2d", f.read(16)))] + if shapeType in (1, 11, 21): + record.points = [_Array("d", unpack("<2d", f.read(16)))] if bbox is not None: # create bounding box for Point by duplicating coordinates point_bbox = list(record.points[0] + record.points[0]) @@ -1335,7 +1426,7 @@ def __shape(self, oid=None, bbox=None): if shapeType == 11: record.z = list(unpack("= 8: (m,) = unpack("i", shx.read(4))[0] * 2) - 100 self.numShapes = shxRecordLength // 8 def __shxOffsets(self): - '''Reads the shape offset positions from a .shx file''' + """Reads the shape offset positions from a .shx file""" shx = self.shx if not shx: - raise ShapefileException("Shapefile Reader requires a shapefile or file-like object. (no shx file found") + raise ShapefileException( + "Shapefile Reader requires a shapefile or file-like object. (no shx file found" + ) # Jump to the first record. shx.seek(100) # Each index record consists of two nrs, we only want the first one - shxRecords = _Array('i', shx.read(2 * self.numShapes * 4) ) - if sys.byteorder != 'big': + shxRecords = _Array("i", shx.read(2 * self.numShapes * 4)) + if sys.byteorder != "big": shxRecords.byteswap() self._offsets = [2 * el for el in shxRecords[::2]] @@ -1379,7 +1474,7 @@ def __shapeIndex(self, i=None): in the .shx index file.""" shx = self.shx # Return None if no shx or no index requested - if not shx or i == None: + if not shx or i is None: return None # At this point, we know the shx file exists if not self._offsets: @@ -1398,11 +1493,11 @@ def shape(self, i=0, bbox=None): if not offset: # Shx index not available. # Determine length of shp file - shp.seek(0,2) + shp.seek(0, 2) shpLength = shp.tell() shp.seek(100) # Do a fast shape iteration until the requested index or end of file. - unpack = Struct('>2i').unpack + unpack = Struct(">2i").unpack _i = 0 offset = shp.tell() while offset < shpLength: @@ -1417,7 +1512,11 @@ def shape(self, i=0, bbox=None): _i += 1 # If the index was not found, it likely means the .shp file is incomplete if _i != i: - raise ShapefileException('Shape index {} is out of bounds; the .shp file only contains {} shapes'.format(i, _i)) + raise ShapefileException( + "Shape index {} is out of bounds; the .shp file only contains {} shapes".format( + i, _i + ) + ) # Seek to the offset and read the shape shp.seek(offset) @@ -1443,7 +1542,7 @@ def iterShapes(self, bbox=None): # shp file length in the header. Can't trust # that so we seek to the end of the file # and figure it out. - shp.seek(0,2) + shp.seek(0, 2) shpLength = shp.tell() shp.seek(100) @@ -1477,12 +1576,15 @@ def iterShapes(self, bbox=None): def __dbfHeader(self): """Reads a dbf header. Xbase-related code borrows heavily from ActiveState Python Cookbook Recipe 362715 by Raymond Hettinger""" if not self.dbf: - raise ShapefileException("Shapefile Reader requires a shapefile or file-like object. (no dbf file found)") + raise ShapefileException( + "Shapefile Reader requires a shapefile or file-like object. (no dbf file found)" + ) dbf = self.dbf # read relevant header parts dbf.seek(0) - self.numRecords, self.__dbfHdrLength, self.__recordLength = \ - unpack("6i", 9994,0,0,0,0,0)) + f.write(pack(">6i", 9994, 0, 0, 0, 0, 0)) # File length (Bytes / 2 = 16-bit words) - if headerType == 'shp': + if headerType == "shp": f.write(pack(">i", self.__shpFileLength())) - elif headerType == 'shx': - f.write(pack('>i', ((100 + (self.shpNum * 8)) // 2))) + elif headerType == "shx": + f.write(pack(">i", ((100 + (self.shpNum * 8)) // 2))) # Version, Shape type if self.shapeType is None: self.shapeType = NULL @@ -1997,37 +2142,41 @@ def __shapefileHeader(self, fileObj, headerType='shp'): # In such cases of empty shapefiles, ESRI spec says the bbox values are 'unspecified'. # Not sure what that means, so for now just setting to 0s, which is the same behavior as in previous versions. # This would also make sense since the Z and M bounds are similarly set to 0 for non-Z/M type shapefiles. - bbox = [0,0,0,0] + bbox = [0, 0, 0, 0] f.write(pack("<4d", *bbox)) except error: - raise ShapefileException("Failed to write shapefile bounding box. Floats required.") + raise ShapefileException( + "Failed to write shapefile bounding box. Floats required." + ) else: - f.write(pack("<4d", 0,0,0,0)) + f.write(pack("<4d", 0, 0, 0, 0)) # Elevation - if self.shapeType in (11,13,15,18): + if self.shapeType in (11, 13, 15, 18): # Z values are present in Z type zbox = self.zbox() if zbox is None: # means we have empty shapefile/only null geoms (see commentary on bbox above) - zbox = [0,0] + zbox = [0, 0] else: # As per the ESRI shapefile spec, the zbox for non-Z type shapefiles are set to 0s - zbox = [0,0] + zbox = [0, 0] # Measure - if self.shapeType in (11,13,15,18,21,23,25,28,31): + if self.shapeType in (11, 13, 15, 18, 21, 23, 25, 28, 31): # M values are present in M or Z type mbox = self.mbox() if mbox is None: # means we have empty shapefile/only null geoms (see commentary on bbox above) - mbox = [0,0] + mbox = [0, 0] else: # As per the ESRI shapefile spec, the mbox for non-M type shapefiles are set to 0s - mbox = [0,0] + mbox = [0, 0] # Try writing try: f.write(pack("<4d", zbox[0], zbox[1], mbox[0], mbox[1])) except error: - raise ShapefileException("Failed to write shapefile elevation and measure values. Floats required.") + raise ShapefileException( + "Failed to write shapefile elevation and measure values. Floats required." + ) def __dbfHeader(self): """Writes the dbf header and field descriptors.""" @@ -2037,32 +2186,43 @@ def __dbfHeader(self): year, month, day = time.localtime()[:3] year -= 1900 # Get all fields, ignoring DeletionFlag if specified - fields = [field for field in self.fields if field[0] != 'DeletionFlag'] + fields = [field for field in self.fields if field[0] != "DeletionFlag"] # Ensure has at least one field if not fields: - raise ShapefileException("Shapefile dbf file must contain at least one field.") + raise ShapefileException( + "Shapefile dbf file must contain at least one field." + ) numRecs = self.recNum numFields = len(fields) headerLength = numFields * 32 + 33 if headerLength >= 65535: raise ShapefileException( - "Shapefile dbf header length exceeds maximum length.") + "Shapefile dbf header length exceeds maximum length." + ) recordLength = sum([int(field[2]) for field in fields]) + 1 - header = pack(' 2 else 0)) for p in s.points] except error: - raise ShapefileException("Failed to write elevation values for record %s. Expected floats." % self.shpNum) + raise ShapefileException( + "Failed to write elevation values for record %s. Expected floats." + % self.shpNum + ) # Write m extremes and values # When reading a file, pyshp converts NODATA m values to None, so here we make sure to convert them back to NODATA # Note: missing m values are autoset to NODATA. - if s.shapeType in (13,15,18,23,25,28,31): + if s.shapeType in (13, 15, 18, 23, 25, 28, 31): try: f.write(pack("<2d", *self.__mbox(s))) except error: - raise ShapefileException("Failed to write measure extremes for record %s. Expected floats" % self.shpNum) + raise ShapefileException( + "Failed to write measure extremes for record %s. Expected floats" + % self.shpNum + ) try: - if hasattr(s,"m"): + if hasattr(s, "m"): # if m values are stored in attribute - f.write(pack("<%sd" % len(s.m), *[m if m is not None else NODATA for m in s.m])) + # fmt: off + f.write( + pack( + "<%sd" % len(s.m), + *[m if m is not None else NODATA for m in s.m] + ) + ) + # fmt: on else: # if m values are stored as 3rd/4th dimension # 0-index position of m value is 3 if z type (x,y,z,m), or 2 if m type (x,y,m) - mpos = 3 if s.shapeType in (13,15,18,31) else 2 - [f.write(pack(" mpos and p[mpos] is not None else NODATA)) for p in s.points] + mpos = 3 if s.shapeType in (13, 15, 18, 31) else 2 + [ + f.write( + pack( + " mpos and p[mpos] is not None + else NODATA, + ) + ) + for p in s.points + ] except error: - raise ShapefileException("Failed to write measure values for record %s. Expected floats" % self.shpNum) + raise ShapefileException( + "Failed to write measure values for record %s. Expected floats" + % self.shpNum + ) # Write a single point - if s.shapeType in (1,11,21): + if s.shapeType in (1, 11, 21): try: f.write(pack("<2d", s.points[0][0], s.points[0][1])) except error: - raise ShapefileException("Failed to write point for record %s. Expected floats." % self.shpNum) + raise ShapefileException( + "Failed to write point for record %s. Expected floats." + % self.shpNum + ) # Write a single Z value # Note: missing z values are autoset to 0, but not sure if this is ideal. if s.shapeType == 11: @@ -2182,7 +2385,10 @@ def __shpRecord(self, s): s.z = (0,) f.write(pack("i", length)) f.seek(finish) - return offset,length + return offset, length def __shxRecord(self, offset, length): - """Writes the shx records.""" - f = self.__getFileObj(self.shx) - try: - f.write(pack(">i", offset // 2)) - except error: - raise ShapefileException('The .shp file has reached its file size limit > 4294967294 bytes (4.29 GB). To fix this, break up your file into multiple smaller ones.') - f.write(pack(">i", length)) + """Writes the shx records.""" + f = self.__getFileObj(self.shx) + try: + f.write(pack(">i", offset // 2)) + except error: + raise ShapefileException( + "The .shp file has reached its file size limit > 4294967294 bytes (4.29 GB). To fix this, break up your file into multiple smaller ones." + ) + f.write(pack(">i", length)) def record(self, *recordList, **recordDict): """Creates a dbf attribute record. You can submit either a sequence of @@ -2247,7 +2464,7 @@ def record(self, *recordList, **recordDict): if self.autoBalance and self.recNum > self.shpNum: self.balance() - fieldCount = sum((1 for field in self.fields if field[0] != 'DeletionFlag')) + fieldCount = sum((1 for field in self.fields if field[0] != "DeletionFlag")) if recordList: record = list(recordList) while len(record) < fieldCount: @@ -2255,8 +2472,8 @@ def record(self, *recordList, **recordDict): elif recordDict: record = [] for field in self.fields: - if field[0] == 'DeletionFlag': - continue # ignore deletionflag field in case it was specified + if field[0] == "DeletionFlag": + continue # ignore deletionflag field in case it was specified if field[0] in recordDict: val = recordDict[field[0]] if val is None: @@ -2264,7 +2481,7 @@ def record(self, *recordList, **recordDict): else: record.append(val) else: - record.append("") # need empty value for missing dict entries + record.append("") # need empty value for missing dict entries else: # Blank fields for empty record record = ["" for _ in range(fieldCount)] @@ -2279,18 +2496,20 @@ def __dbfRecord(self, record): # cannot change the fields after this point self.__dbfHeader() # first byte of the record is deletion flag, always disabled - f.write(b' ') + f.write(b" ") # begin self.recNum += 1 - fields = (field for field in self.fields if field[0] != 'DeletionFlag') # ignore deletionflag field in case it was specified + fields = ( + field for field in self.fields if field[0] != "DeletionFlag" + ) # ignore deletionflag field in case it was specified for (fieldName, fieldType, size, deci), value in zip(fields, record): # write fieldType = fieldType.upper() size = int(size) - if fieldType in ("N","F"): + if fieldType in ("N", "F"): # numeric or float: number stored as a string, right justified, and padded with blanks to the width of the field. if value in MISSING: - value = b"*"*size # QGIS NULL + value = b"*" * size # QGIS NULL elif not deci: # force to int try: @@ -2301,42 +2520,54 @@ def __dbfRecord(self, record): except ValueError: # forcing directly to int failed, so was probably a float. value = int(float(value)) - value = format(value, "d")[:size].rjust(size) # caps the size if exceeds the field size + value = format(value, "d")[:size].rjust( + size + ) # caps the size if exceeds the field size else: value = float(value) - value = format(value, ".%sf"%deci)[:size].rjust(size) # caps the size if exceeds the field size + value = format(value, ".%sf" % deci)[:size].rjust( + size + ) # caps the size if exceeds the field size elif fieldType == "D": # date: 8 bytes - date stored as a string in the format YYYYMMDD. if isinstance(value, date): - value = '{:04d}{:02d}{:02d}'.format(value.year, value.month, value.day) + value = "{:04d}{:02d}{:02d}".format( + value.year, value.month, value.day + ) elif isinstance(value, list) and len(value) == 3: - value = '{:04d}{:02d}{:02d}'.format(*value) + value = "{:04d}{:02d}{:02d}".format(*value) elif value in MISSING: - value = b'0' * 8 # QGIS NULL for date type + value = b"0" * 8 # QGIS NULL for date type elif is_string(value) and len(value) == 8: - pass # value is already a date string + pass # value is already a date string else: - raise ShapefileException("Date values must be either a datetime.date object, a list, a YYYYMMDD string, or a missing value.") - elif fieldType == 'L': + raise ShapefileException( + "Date values must be either a datetime.date object, a list, a YYYYMMDD string, or a missing value." + ) + elif fieldType == "L": # logical: 1 byte - initialized to 0x20 (space) otherwise T or F. if value in MISSING: - value = b' ' # missing is set to space - elif value in [True,1]: - value = b'T' - elif value in [False,0]: - value = b'F' + value = b" " # missing is set to space + elif value in [True, 1]: + value = b"T" + elif value in [False, 0]: + value = b"F" else: - value = b' ' # unknown is set to space + value = b" " # unknown is set to space else: # anything else is forced to string, truncated to the length of the field value = b(value, self.encoding, self.encodingErrors)[:size].ljust(size) if not isinstance(value, bytes): # just in case some of the numeric format() and date strftime() results are still in unicode (Python 3 only) - value = b(value, 'ascii', self.encodingErrors) # should be default ascii encoding + value = b( + value, "ascii", self.encodingErrors + ) # should be default ascii encoding if len(value) != size: raise ShapefileException( "Shapefile Writer unable to pack incorrect sized value" - " (size %d) into field '%s' (size %d)." % (len(value), fieldName, size)) + " (size %d) into field '%s' (size %d)." + % (len(value), fieldName, size) + ) f.write(value) def balance(self): @@ -2348,12 +2579,10 @@ def balance(self): while self.recNum < self.shpNum: self.record() - def null(self): """Creates a null shape.""" self.shape(Shape(NULL)) - def point(self, x, y): """Creates a POINT shape.""" shapeType = POINT @@ -2378,12 +2607,13 @@ def pointz(self, x, y, z=0, m=None): pointShape.points.append([x, y, z, m]) self.shape(pointShape) - def multipoint(self, points): """Creates a MULTIPOINT shape. Points is a list of xy values.""" shapeType = MULTIPOINT - points = [points] # nest the points inside a list to be compatible with the generic shapeparts method + points = [ + points + ] # nest the points inside a list to be compatible with the generic shapeparts method self._shapeparts(parts=points, shapeType=shapeType) def multipointm(self, points): @@ -2391,7 +2621,9 @@ def multipointm(self, points): Points is a list of xym values. If the m (measure) value is not included, it defaults to None (NoData).""" shapeType = MULTIPOINTM - points = [points] # nest the points inside a list to be compatible with the generic shapeparts method + points = [ + points + ] # nest the points inside a list to be compatible with the generic shapeparts method self._shapeparts(parts=points, shapeType=shapeType) def multipointz(self, points): @@ -2400,10 +2632,11 @@ def multipointz(self, points): If the z (elevation) value is not included, it defaults to 0. If the m (measure) value is not included, it defaults to None (NoData).""" shapeType = MULTIPOINTZ - points = [points] # nest the points inside a list to be compatible with the generic shapeparts method + points = [ + points + ] # nest the points inside a list to be compatible with the generic shapeparts method self._shapeparts(parts=points, shapeType=shapeType) - def line(self, lines): """Creates a POLYLINE shape. Lines is a collection of lines, each made up of a list of xy values.""" @@ -2425,7 +2658,6 @@ def linez(self, lines): shapeType = POLYLINEZ self._shapeparts(parts=lines, shapeType=shapeType) - def poly(self, polys): """Creates a POLYGON shape. Polys is a collection of polygons, each made up of a list of xy values. @@ -2453,7 +2685,6 @@ def polyz(self, polys): shapeType = POLYGONZ self._shapeparts(parts=polys, shapeType=shapeType) - def multipatch(self, parts, partTypes): """Creates a MULTIPATCH shape. Parts is a collection of 3D surface patches, each made up of a list of xyzm values. @@ -2479,7 +2710,6 @@ def multipatch(self, parts, partTypes): # write the shape self.shape(polyShape) - def _shapeparts(self, parts, shapeType): """Internal method for adding a shape that has multiple collections of points (parts): lines, polygons, and multipoint shapes. @@ -2488,7 +2718,7 @@ def _shapeparts(self, parts, shapeType): polyShape.parts = [] polyShape.points = [] # Make sure polygon rings (parts) are closed - if shapeType in (5,15,25,31): + if shapeType in (5, 15, 25, 31): for part in parts: if part[0] != part[-1]: part.append(part[0]) @@ -2515,20 +2745,23 @@ def field(self, name, fieldType="C", size="50", decimal=0): decimal = 0 if len(self.fields) >= 2046: raise ShapefileException( - "Shapefile Writer reached maximum number of fields: 2046.") + "Shapefile Writer reached maximum number of fields: 2046." + ) self.fields.append((name, fieldType, size, decimal)) # Begin Testing def test(**kwargs): import doctest + doctest.NORMALIZE_WHITESPACE = 1 - verbosity = kwargs.get('verbose', 0) + verbosity = kwargs.get("verbose", 0) if verbosity == 0: - print('Running doctests...') + print("Running doctests...") # ignore py2-3 unicode differences import re + class Py23DocChecker(doctest.OutputChecker): def check_output(self, want, got, optionflags): if sys.version_info[0] == 2: @@ -2536,13 +2769,20 @@ def check_output(self, want, got, optionflags): got = re.sub('u"(.*?)"', '"\\1"', got) res = doctest.OutputChecker.check_output(self, want, got, optionflags) return res + def summarize(self): doctest.OutputChecker.summarize(True) # run tests runner = doctest.DocTestRunner(checker=Py23DocChecker(), verbose=verbosity) - with open("README.md","rb") as fobj: - test = doctest.DocTestParser().get_doctest(string=fobj.read().decode("utf8").replace('\r\n','\n'), globs={}, name="README", filename="README.md", lineno=0) + with open("README.md", "rb") as fobj: + test = doctest.DocTestParser().get_doctest( + string=fobj.read().decode("utf8").replace("\r\n", "\n"), + globs={}, + name="README", + filename="README.md", + lineno=0, + ) failure_count, test_count = runner.run(test) # print results @@ -2550,12 +2790,13 @@ def summarize(self): runner.summarize(True) else: if failure_count == 0: - print('All test passed successfully') + print("All test passed successfully") elif failure_count > 0: runner.summarize(verbosity) return failure_count + if __name__ == "__main__": """ Doctests are contained in the file 'README.md', and are tested using the built-in diff --git a/test_shapefile.py b/test_shapefile.py index f5dd718..08561c6 100644 --- a/test_shapefile.py +++ b/test_shapefile.py @@ -19,184 +19,418 @@ import shapefile # define various test shape tuples of (type, points, parts indexes, and expected geo interface output) -geo_interface_tests = [ (shapefile.POINT, # point - [(1,1)], - [], - {'type':'Point','coordinates':(1,1)} - ), - (shapefile.MULTIPOINT, # multipoint - [(1,1),(2,1),(2,2)], - [], - {'type':'MultiPoint','coordinates':[(1,1),(2,1),(2,2)]} - ), - (shapefile.POLYLINE, # single linestring - [(1,1),(2,1)], - [0], - {'type':'LineString','coordinates':[(1,1),(2,1)]} - ), - (shapefile.POLYLINE, # multi linestring - [(1,1),(2,1), # line 1 - (10,10),(20,10)], # line 2 - [0,2], - {'type':'MultiLineString','coordinates':[ - [(1,1),(2,1)], # line 1 - [(10,10),(20,10)] # line 2 - ]} - ), - (shapefile.POLYGON, # single polygon, no holes - [(1,1),(1,9),(9,9),(9,1),(1,1), # exterior - ], - [0], - {'type':'Polygon','coordinates':[ - [(1,1),(1,9),(9,9),(9,1),(1,1)], - ]} - ), - (shapefile.POLYGON, # single polygon, holes (ordered) - [(1,1),(1,9),(9,9),(9,1),(1,1), # exterior - (2,2),(4,2),(4,4),(2,4),(2,2), # hole 1 - (5,5),(7,5),(7,7),(5,7),(5,5), # hole 2 - ], - [0,5,5+5], - {'type':'Polygon','coordinates':[ - [(1,1),(1,9),(9,9),(9,1),(1,1)], # exterior - [(2,2),(4,2),(4,4),(2,4),(2,2)], # hole 1 - [(5,5),(7,5),(7,7),(5,7),(5,5)], # hole 2 - ]} - ), - (shapefile.POLYGON, # single polygon, holes (unordered) - [ - (2,2),(4,2),(4,4),(2,4),(2,2), # hole 1 - (1,1),(1,9),(9,9),(9,1),(1,1), # exterior - (5,5),(7,5),(7,7),(5,7),(5,5), # hole 2 - ], - [0,5,5+5], - {'type':'Polygon','coordinates':[ - [(1,1),(1,9),(9,9),(9,1),(1,1)], # exterior - [(2,2),(4,2),(4,4),(2,4),(2,2)], # hole 1 - [(5,5),(7,5),(7,7),(5,7),(5,5)], # hole 2 - ]} - ), - (shapefile.POLYGON, # multi polygon, no holes - [(1,1),(1,9),(9,9),(9,1),(1,1), # exterior - (11,11),(11,19),(19,19),(19,11),(11,11), # exterior - ], - [0,5], - {'type':'MultiPolygon','coordinates':[ - [ # poly 1 - [(1,1),(1,9),(9,9),(9,1),(1,1)], - ], - [ # poly 2 - [(11,11),(11,19),(19,19),(19,11),(11,11)], - ], - ]} - ), - (shapefile.POLYGON, # multi polygon, holes (unordered) - [(1,1),(1,9),(9,9),(9,1),(1,1), # exterior 1 - (11,11),(11,19),(19,19),(19,11),(11,11), # exterior 2 - (12,12),(14,12),(14,14),(12,14),(12,12), # hole 2.1 - (15,15),(17,15),(17,17),(15,17),(15,15), # hole 2.2 - (2,2),(4,2),(4,4),(2,4),(2,2), # hole 1.1 - (5,5),(7,5),(7,7),(5,7),(5,5), # hole 1.2 - ], - [0,5,10,15,20,25], - {'type':'MultiPolygon','coordinates':[ - [ # poly 1 - [(1,1),(1,9),(9,9),(9,1),(1,1)], # exterior - [(2,2),(4,2),(4,4),(2,4),(2,2)], # hole 1 - [(5,5),(7,5),(7,7),(5,7),(5,5)], # hole 2 - ], - [ # poly 2 - [(11,11),(11,19),(19,19),(19,11),(11,11)], # exterior - [(12,12),(14,12),(14,14),(12,14),(12,12)], # hole 1 - [(15,15),(17,15),(17,17),(15,17),(15,15)], # hole 2 - ], - ]} - ), - (shapefile.POLYGON, # multi polygon, nested exteriors with holes (unordered) - [(1,1),(1,9),(9,9),(9,1),(1,1), # exterior 1 - (3,3),(3,7),(7,7),(7,3),(3,3), # exterior 2 - (4.5,4.5),(4.5,5.5),(5.5,5.5),(5.5,4.5),(4.5,4.5), # exterior 3 - (4,4),(6,4),(6,6),(4,6),(4,4), # hole 2.1 - (2,2),(8,2),(8,8),(2,8),(2,2), # hole 1.1 - ], - [0,5,10,15,20], - {'type':'MultiPolygon','coordinates':[ - [ # poly 1 - [(1,1),(1,9),(9,9),(9,1),(1,1)], # exterior 1 - [(2,2),(8,2),(8,8),(2,8),(2,2)], # hole 1.1 - ], - [ # poly 2 - [(3,3),(3,7),(7,7),(7,3),(3,3)], # exterior 2 - [(4,4),(6,4),(6,6),(4,6),(4,4)], # hole 2.1 - ], - [ # poly 3 - [(4.5,4.5),(4.5,5.5),(5.5,5.5),(5.5,4.5),(4.5,4.5)], # exterior 3 - ], - ]} - ), - (shapefile.POLYGON, # multi polygon, nested exteriors with holes (unordered and tricky holes designed to throw off ring_sample() test) - [(1,1),(1,9),(9,9),(9,1),(1,1), # exterior 1 - (3,3),(3,7),(7,7),(7,3),(3,3), # exterior 2 - (4.5,4.5),(4.5,5.5),(5.5,5.5),(5.5,4.5),(4.5,4.5), # exterior 3 - (4,4),(4,4),(6,4),(6,4),(6,4),(6,6),(4,6),(4,4), # hole 2.1 (hole has duplicate coords) - (2,2),(3,3),(4,2),(8,2),(8,8),(4,8),(2,8),(2,4),(2,2), # hole 1.1 (hole coords form straight line and starts in concave orientation) - ], - [0,5,10,15,20+3], - {'type':'MultiPolygon','coordinates':[ - [ # poly 1 - [(1,1),(1,9),(9,9),(9,1),(1,1)], # exterior 1 - [(2,2),(3,3),(4,2),(8,2),(8,8),(4,8),(2,8),(2,4),(2,2)], # hole 1.1 - ], - [ # poly 2 - [(3,3),(3,7),(7,7),(7,3),(3,3)], # exterior 2 - [(4,4),(4,4),(6,4),(6,4),(6,4),(6,6),(4,6),(4,4)], # hole 2.1 - ], - [ # poly 3 - [(4.5,4.5),(4.5,5.5),(5.5,5.5),(5.5,4.5),(4.5,4.5)], # exterior 3 - ], - ]} - ), - (shapefile.POLYGON, # multi polygon, holes incl orphaned holes (unordered), should raise warning - [(1,1),(1,9),(9,9),(9,1),(1,1), # exterior 1 - (11,11),(11,19),(19,19),(19,11),(11,11), # exterior 2 - (12,12),(14,12),(14,14),(12,14),(12,12), # hole 2.1 - (15,15),(17,15),(17,17),(15,17),(15,15), # hole 2.2 - (95,95),(97,95),(97,97),(95,97),(95,95), # hole x.1 (orphaned hole, should be interpreted as exterior) - (2,2),(4,2),(4,4),(2,4),(2,2), # hole 1.1 - (5,5),(7,5),(7,7),(5,7),(5,5), # hole 1.2 - ], - [0,5,10,15,20,25,30], - {'type':'MultiPolygon','coordinates':[ - [ # poly 1 - [(1,1),(1,9),(9,9),(9,1),(1,1)], # exterior - [(2,2),(4,2),(4,4),(2,4),(2,2)], # hole 1 - [(5,5),(7,5),(7,7),(5,7),(5,5)], # hole 2 - ], - [ # poly 2 - [(11,11),(11,19),(19,19),(19,11),(11,11)], # exterior - [(12,12),(14,12),(14,14),(12,14),(12,12)], # hole 1 - [(15,15),(17,15),(17,17),(15,17),(15,15)], # hole 2 - ], - [ # poly 3 (orphaned hole) - [(95,95),(97,95),(97,97),(95,97),(95,95)], # exterior - ], - ]} - ), - (shapefile.POLYGON, # multi polygon, exteriors with wrong orientation (be nice and interpret as such), should raise warning - [(1,1),(9,1),(9,9),(1,9),(1,1), # exterior with hole-orientation - (11,11),(19,11),(19,19),(11,19),(11,11), # exterior with hole-orientation - ], - [0,5], - {'type':'MultiPolygon','coordinates':[ - [ # poly 1 - [(1,1),(9,1),(9,9),(1,9),(1,1)], - ], - [ # poly 2 - [(11,11),(19,11),(19,19),(11,19),(11,11)], - ], - ]} - ), - ] +geo_interface_tests = [ + ( + shapefile.POINT, # point + [(1, 1)], + [], + {"type": "Point", "coordinates": (1, 1)}, + ), + ( + shapefile.MULTIPOINT, # multipoint + [(1, 1), (2, 1), (2, 2)], + [], + {"type": "MultiPoint", "coordinates": [(1, 1), (2, 1), (2, 2)]}, + ), + ( + shapefile.POLYLINE, # single linestring + [(1, 1), (2, 1)], + [0], + {"type": "LineString", "coordinates": [(1, 1), (2, 1)]}, + ), + ( + shapefile.POLYLINE, # multi linestring + [ + (1, 1), + (2, 1), # line 1 + (10, 10), + (20, 10), + ], # line 2 + [0, 2], + { + "type": "MultiLineString", + "coordinates": [ + [(1, 1), (2, 1)], # line 1 + [(10, 10), (20, 10)], # line 2 + ], + }, + ), + ( + shapefile.POLYGON, # single polygon, no holes + [ + (1, 1), + (1, 9), + (9, 9), + (9, 1), + (1, 1), # exterior + ], + [0], + { + "type": "Polygon", + "coordinates": [ + [(1, 1), (1, 9), (9, 9), (9, 1), (1, 1)], + ], + }, + ), + ( + shapefile.POLYGON, # single polygon, holes (ordered) + [ + (1, 1), + (1, 9), + (9, 9), + (9, 1), + (1, 1), # exterior + (2, 2), + (4, 2), + (4, 4), + (2, 4), + (2, 2), # hole 1 + (5, 5), + (7, 5), + (7, 7), + (5, 7), + (5, 5), # hole 2 + ], + [0, 5, 5 + 5], + { + "type": "Polygon", + "coordinates": [ + [(1, 1), (1, 9), (9, 9), (9, 1), (1, 1)], # exterior + [(2, 2), (4, 2), (4, 4), (2, 4), (2, 2)], # hole 1 + [(5, 5), (7, 5), (7, 7), (5, 7), (5, 5)], # hole 2 + ], + }, + ), + ( + shapefile.POLYGON, # single polygon, holes (unordered) + [ + (2, 2), + (4, 2), + (4, 4), + (2, 4), + (2, 2), # hole 1 + (1, 1), + (1, 9), + (9, 9), + (9, 1), + (1, 1), # exterior + (5, 5), + (7, 5), + (7, 7), + (5, 7), + (5, 5), # hole 2 + ], + [0, 5, 5 + 5], + { + "type": "Polygon", + "coordinates": [ + [(1, 1), (1, 9), (9, 9), (9, 1), (1, 1)], # exterior + [(2, 2), (4, 2), (4, 4), (2, 4), (2, 2)], # hole 1 + [(5, 5), (7, 5), (7, 7), (5, 7), (5, 5)], # hole 2 + ], + }, + ), + ( + shapefile.POLYGON, # multi polygon, no holes + [ + (1, 1), + (1, 9), + (9, 9), + (9, 1), + (1, 1), # exterior + (11, 11), + (11, 19), + (19, 19), + (19, 11), + (11, 11), # exterior + ], + [0, 5], + { + "type": "MultiPolygon", + "coordinates": [ + [ # poly 1 + [(1, 1), (1, 9), (9, 9), (9, 1), (1, 1)], + ], + [ # poly 2 + [(11, 11), (11, 19), (19, 19), (19, 11), (11, 11)], + ], + ], + }, + ), + ( + shapefile.POLYGON, # multi polygon, holes (unordered) + [ + (1, 1), + (1, 9), + (9, 9), + (9, 1), + (1, 1), # exterior 1 + (11, 11), + (11, 19), + (19, 19), + (19, 11), + (11, 11), # exterior 2 + (12, 12), + (14, 12), + (14, 14), + (12, 14), + (12, 12), # hole 2.1 + (15, 15), + (17, 15), + (17, 17), + (15, 17), + (15, 15), # hole 2.2 + (2, 2), + (4, 2), + (4, 4), + (2, 4), + (2, 2), # hole 1.1 + (5, 5), + (7, 5), + (7, 7), + (5, 7), + (5, 5), # hole 1.2 + ], + [0, 5, 10, 15, 20, 25], + { + "type": "MultiPolygon", + "coordinates": [ + [ # poly 1 + [(1, 1), (1, 9), (9, 9), (9, 1), (1, 1)], # exterior + [(2, 2), (4, 2), (4, 4), (2, 4), (2, 2)], # hole 1 + [(5, 5), (7, 5), (7, 7), (5, 7), (5, 5)], # hole 2 + ], + [ # poly 2 + [(11, 11), (11, 19), (19, 19), (19, 11), (11, 11)], # exterior + [(12, 12), (14, 12), (14, 14), (12, 14), (12, 12)], # hole 1 + [(15, 15), (17, 15), (17, 17), (15, 17), (15, 15)], # hole 2 + ], + ], + }, + ), + ( + shapefile.POLYGON, # multi polygon, nested exteriors with holes (unordered) + [ + (1, 1), + (1, 9), + (9, 9), + (9, 1), + (1, 1), # exterior 1 + (3, 3), + (3, 7), + (7, 7), + (7, 3), + (3, 3), # exterior 2 + (4.5, 4.5), + (4.5, 5.5), + (5.5, 5.5), + (5.5, 4.5), + (4.5, 4.5), # exterior 3 + (4, 4), + (6, 4), + (6, 6), + (4, 6), + (4, 4), # hole 2.1 + (2, 2), + (8, 2), + (8, 8), + (2, 8), + (2, 2), # hole 1.1 + ], + [0, 5, 10, 15, 20], + { + "type": "MultiPolygon", + "coordinates": [ + [ # poly 1 + [(1, 1), (1, 9), (9, 9), (9, 1), (1, 1)], # exterior 1 + [(2, 2), (8, 2), (8, 8), (2, 8), (2, 2)], # hole 1.1 + ], + [ # poly 2 + [(3, 3), (3, 7), (7, 7), (7, 3), (3, 3)], # exterior 2 + [(4, 4), (6, 4), (6, 6), (4, 6), (4, 4)], # hole 2.1 + ], + [ # poly 3 + [ + (4.5, 4.5), + (4.5, 5.5), + (5.5, 5.5), + (5.5, 4.5), + (4.5, 4.5), + ], # exterior 3 + ], + ], + }, + ), + ( + shapefile.POLYGON, # multi polygon, nested exteriors with holes (unordered and tricky holes designed to throw off ring_sample() test) + [ + (1, 1), + (1, 9), + (9, 9), + (9, 1), + (1, 1), # exterior 1 + (3, 3), + (3, 7), + (7, 7), + (7, 3), + (3, 3), # exterior 2 + (4.5, 4.5), + (4.5, 5.5), + (5.5, 5.5), + (5.5, 4.5), + (4.5, 4.5), # exterior 3 + (4, 4), + (4, 4), + (6, 4), + (6, 4), + (6, 4), + (6, 6), + (4, 6), + (4, 4), # hole 2.1 (hole has duplicate coords) + (2, 2), + (3, 3), + (4, 2), + (8, 2), + (8, 8), + (4, 8), + (2, 8), + (2, 4), + ( + 2, + 2, + ), # hole 1.1 (hole coords form straight line and starts in concave orientation) + ], + [0, 5, 10, 15, 20 + 3], + { + "type": "MultiPolygon", + "coordinates": [ + [ # poly 1 + [(1, 1), (1, 9), (9, 9), (9, 1), (1, 1)], # exterior 1 + [ + (2, 2), + (3, 3), + (4, 2), + (8, 2), + (8, 8), + (4, 8), + (2, 8), + (2, 4), + (2, 2), + ], # hole 1.1 + ], + [ # poly 2 + [(3, 3), (3, 7), (7, 7), (7, 3), (3, 3)], # exterior 2 + [ + (4, 4), + (4, 4), + (6, 4), + (6, 4), + (6, 4), + (6, 6), + (4, 6), + (4, 4), + ], # hole 2.1 + ], + [ # poly 3 + [ + (4.5, 4.5), + (4.5, 5.5), + (5.5, 5.5), + (5.5, 4.5), + (4.5, 4.5), + ], # exterior 3 + ], + ], + }, + ), + ( + shapefile.POLYGON, # multi polygon, holes incl orphaned holes (unordered), should raise warning + [ + (1, 1), + (1, 9), + (9, 9), + (9, 1), + (1, 1), # exterior 1 + (11, 11), + (11, 19), + (19, 19), + (19, 11), + (11, 11), # exterior 2 + (12, 12), + (14, 12), + (14, 14), + (12, 14), + (12, 12), # hole 2.1 + (15, 15), + (17, 15), + (17, 17), + (15, 17), + (15, 15), # hole 2.2 + (95, 95), + (97, 95), + (97, 97), + (95, 97), + (95, 95), # hole x.1 (orphaned hole, should be interpreted as exterior) + (2, 2), + (4, 2), + (4, 4), + (2, 4), + (2, 2), # hole 1.1 + (5, 5), + (7, 5), + (7, 7), + (5, 7), + (5, 5), # hole 1.2 + ], + [0, 5, 10, 15, 20, 25, 30], + { + "type": "MultiPolygon", + "coordinates": [ + [ # poly 1 + [(1, 1), (1, 9), (9, 9), (9, 1), (1, 1)], # exterior + [(2, 2), (4, 2), (4, 4), (2, 4), (2, 2)], # hole 1 + [(5, 5), (7, 5), (7, 7), (5, 7), (5, 5)], # hole 2 + ], + [ # poly 2 + [(11, 11), (11, 19), (19, 19), (19, 11), (11, 11)], # exterior + [(12, 12), (14, 12), (14, 14), (12, 14), (12, 12)], # hole 1 + [(15, 15), (17, 15), (17, 17), (15, 17), (15, 15)], # hole 2 + ], + [ # poly 3 (orphaned hole) + [(95, 95), (97, 95), (97, 97), (95, 97), (95, 95)], # exterior + ], + ], + }, + ), + ( + shapefile.POLYGON, # multi polygon, exteriors with wrong orientation (be nice and interpret as such), should raise warning + [ + (1, 1), + (9, 1), + (9, 9), + (1, 9), + (1, 1), # exterior with hole-orientation + (11, 11), + (19, 11), + (19, 19), + (11, 19), + (11, 11), # exterior with hole-orientation + ], + [0, 5], + { + "type": "MultiPolygon", + "coordinates": [ + [ # poly 1 + [(1, 1), (9, 1), (9, 9), (1, 9), (1, 1)], + ], + [ # poly 2 + [(11, 11), (19, 11), (19, 19), (11, 19), (11, 11)], + ], + ], + }, + ), +] + def test_empty_shape_geo_interface(): """ @@ -206,7 +440,8 @@ def test_empty_shape_geo_interface(): """ shape = shapefile.Shape() with pytest.raises(Exception): - getattr(shape, '__geo_interface__') + getattr(shape, "__geo_interface__") + @pytest.mark.parametrize("typ,points,parts,expected", geo_interface_tests) def test_expected_shape_geo_interface(typ, points, parts, expected): @@ -222,22 +457,22 @@ def test_expected_shape_geo_interface(typ, points, parts, expected): def test_reader_geo_interface(): with shapefile.Reader("shapefiles/blockgroups") as r: geoj = r.__geo_interface__ - assert geoj['type'] == 'FeatureCollection' - assert 'bbox' in geoj + assert geoj["type"] == "FeatureCollection" + assert "bbox" in geoj assert json.dumps(geoj) def test_shapes_geo_interface(): with shapefile.Reader("shapefiles/blockgroups") as r: geoj = r.shapes().__geo_interface__ - assert geoj['type'] == 'GeometryCollection' + assert geoj["type"] == "GeometryCollection" assert json.dumps(geoj) def test_shaperecords_geo_interface(): with shapefile.Reader("shapefiles/blockgroups") as r: geoj = r.shapeRecords().__geo_interface__ - assert geoj['type'] == 'FeatureCollection' + assert geoj["type"] == "FeatureCollection" assert json.dumps(geoj) @@ -299,14 +534,18 @@ def test_reader_zip(): pass # test specifying the path when reading multi-shapefile zipfile (with extension) - with shapefile.Reader("shapefiles/blockgroups_multishapefile.zip/blockgroups2.shp") as sf: + with shapefile.Reader( + "shapefiles/blockgroups_multishapefile.zip/blockgroups2.shp" + ) as sf: for __recShape in sf.iterShapeRecords(): pass assert len(sf) > 0 assert sf.shp.closed is sf.shx.closed is sf.dbf.closed is True # test specifying the path when reading multi-shapefile zipfile (without extension) - with shapefile.Reader("shapefiles/blockgroups_multishapefile.zip/blockgroups2") as sf: + with shapefile.Reader( + "shapefiles/blockgroups_multishapefile.zip/blockgroups2" + ) as sf: for __recShape in sf.iterShapeRecords(): pass assert len(sf) > 0 @@ -346,9 +585,9 @@ def test_reader_close_filelike(): """ # note uses an actual shapefile from # the projects "shapefiles" directory - shp = open("shapefiles/blockgroups.shp", mode='rb') - shx = open("shapefiles/blockgroups.shx", mode='rb') - dbf = open("shapefiles/blockgroups.dbf", mode='rb') + shp = open("shapefiles/blockgroups.shp", mode="rb") + shx = open("shapefiles/blockgroups.shx", mode="rb") + dbf = open("shapefiles/blockgroups.dbf", mode="rb") sf = shapefile.Reader(shp=shp, shx=shx, dbf=dbf) sf.close() @@ -389,9 +628,9 @@ def test_reader_context_filelike(): """ # note uses an actual shapefile from # the projects "shapefiles" directory - shp = open("shapefiles/blockgroups.shp", mode='rb') - shx = open("shapefiles/blockgroups.shx", mode='rb') - dbf = open("shapefiles/blockgroups.dbf", mode='rb') + shp = open("shapefiles/blockgroups.shp", mode="rb") + shx = open("shapefiles/blockgroups.shx", mode="rb") + dbf = open("shapefiles/blockgroups.dbf", mode="rb") with shapefile.Reader(shp=shp, shx=shx, dbf=dbf) as sf: pass @@ -410,7 +649,7 @@ def test_reader_shapefile_type(): is returned correctly. """ with shapefile.Reader("shapefiles/blockgroups") as sf: - assert sf.shapeType == 5 # 5 means Polygon + assert sf.shapeType == 5 # 5 means Polygon assert sf.shapeType == shapefile.POLYGON assert sf.shapeTypeName == "POLYGON" @@ -428,7 +667,7 @@ def test_reader_shapefile_length(): def test_shape_metadata(): with shapefile.Reader("shapefiles/blockgroups") as sf: shape = sf.shape(0) - assert shape.shapeType == 5 # Polygon + assert shape.shapeType == 5 # Polygon assert shape.shapeType == shapefile.POLYGON assert sf.shapeTypeName == "POLYGON" @@ -445,10 +684,10 @@ def test_reader_fields(): assert isinstance(fields, list) field = fields[0] - assert isinstance(field[0], str) # field name - assert field[1] in ["C", "N", "F", "L", "D", "M"] # field type - assert isinstance(field[2], int) # field length - assert isinstance(field[3], int) # decimal length + assert isinstance(field[0], str) # field name + assert field[1] in ["C", "N", "F", "L", "D", "M"] # field type + assert isinstance(field[2], int) # field length + assert isinstance(field[3], int) # decimal length def test_reader_shapefile_extension_ignored(): @@ -484,7 +723,7 @@ def test_reader_dbf_only(): with shapefile.Reader(dbf="shapefiles/blockgroups.dbf") as sf: assert len(sf) == 663 record = sf.record(3) - assert record[1:3] == ['060750601001', 4715] + assert record[1:3] == ["060750601001", 4715] def test_reader_shp_shx_only(): @@ -493,7 +732,9 @@ def test_reader_shp_shx_only(): shp and shx argument to the shapefile reader reads just the shp and shx file. """ - with shapefile.Reader(shp="shapefiles/blockgroups.shp", shx="shapefiles/blockgroups.shx") as sf: + with shapefile.Reader( + shp="shapefiles/blockgroups.shp", shx="shapefiles/blockgroups.shx" + ) as sf: assert len(sf) == 663 shape = sf.shape(3) assert len(shape.points) == 173 @@ -505,12 +746,14 @@ def test_reader_shp_dbf_only(): shp and shx argument to the shapefile reader reads just the shp and dbf file. """ - with shapefile.Reader(shp="shapefiles/blockgroups.shp", dbf="shapefiles/blockgroups.dbf") as sf: + with shapefile.Reader( + shp="shapefiles/blockgroups.shp", dbf="shapefiles/blockgroups.dbf" + ) as sf: assert len(sf) == 663 shape = sf.shape(3) assert len(shape.points) == 173 record = sf.record(3) - assert record[1:3] == ['060750601001', 4715] + assert record[1:3] == ["060750601001", 4715] def test_reader_shp_only(): @@ -534,7 +777,7 @@ def test_reader_filelike_dbf_only(): with shapefile.Reader(dbf=open("shapefiles/blockgroups.dbf", "rb")) as sf: assert len(sf) == 663 record = sf.record(3) - assert record[1:3] == ['060750601001', 4715] + assert record[1:3] == ["060750601001", 4715] def test_reader_filelike_shp_shx_only(): @@ -543,7 +786,10 @@ def test_reader_filelike_shp_shx_only(): shp and shx argument to the shapefile reader reads just the shp and shx file. """ - with shapefile.Reader(shp=open("shapefiles/blockgroups.shp", "rb"), shx=open("shapefiles/blockgroups.shx", "rb")) as sf: + with shapefile.Reader( + shp=open("shapefiles/blockgroups.shp", "rb"), + shx=open("shapefiles/blockgroups.shx", "rb"), + ) as sf: assert len(sf) == 663 shape = sf.shape(3) assert len(shape.points) == 173 @@ -555,12 +801,15 @@ def test_reader_filelike_shp_dbf_only(): shp and shx argument to the shapefile reader reads just the shp and dbf file. """ - with shapefile.Reader(shp=open("shapefiles/blockgroups.shp", "rb"), dbf=open("shapefiles/blockgroups.dbf", "rb")) as sf: + with shapefile.Reader( + shp=open("shapefiles/blockgroups.shp", "rb"), + dbf=open("shapefiles/blockgroups.dbf", "rb"), + ) as sf: assert len(sf) == 663 shape = sf.shape(3) assert len(shape.points) == 173 record = sf.record(3) - assert record[1:3] == ['060750601001', 4715] + assert record[1:3] == ["060750601001", 4715] def test_reader_filelike_shp_only(): @@ -619,7 +868,9 @@ def test_record_attributes(fields=None): else: # default all fields record = full_record - fields = [field[0] for field in sf.fields[1:]] # fieldnames, sans del flag + fields = [ + field[0] for field in sf.fields[1:] + ] # fieldnames, sans del flag # check correct length assert len(record) == len(set(fields)) # check record values (should be in same order as shapefile fields) @@ -627,7 +878,9 @@ def test_record_attributes(fields=None): for field in sf.fields: field_name = field[0] if field_name in fields: - assert record[i] == record[field_name] == getattr(record, field_name) + assert ( + record[i] == record[field_name] == getattr(record, field_name) + ) i += 1 @@ -636,7 +889,7 @@ def test_record_subfields(): Assert that reader correctly retrieves only a subset of fields when specified. """ - fields = ["AREA","POP1990","MALES","FEMALES","MOBILEHOME"] + fields = ["AREA", "POP1990", "MALES", "FEMALES", "MOBILEHOME"] test_record_attributes(fields=fields) @@ -646,7 +899,7 @@ def test_record_subfields_unordered(): of fields when specified, given in random order but retrieved in the order of the shapefile fields. """ - fields = sorted(["AREA","POP1990","MALES","FEMALES","MOBILEHOME"]) + fields = sorted(["AREA", "POP1990", "MALES", "FEMALES", "MOBILEHOME"]) test_record_attributes(fields=fields) @@ -654,7 +907,7 @@ def test_record_subfields_delflag_notvalid(): """ Assert that reader does not consider DeletionFlag as a valid field name. """ - fields = ["DeletionFlag","AREA","POP1990","MALES","FEMALES","MOBILEHOME"] + fields = ["DeletionFlag", "AREA", "POP1990", "MALES", "FEMALES", "MOBILEHOME"] with pytest.raises(ValueError): test_record_attributes(fields=fields) @@ -664,7 +917,7 @@ def test_record_subfields_duplicates(): Assert that reader correctly retrieves only a subset of fields when specified, handling duplicate input fields. """ - fields = ["AREA","AREA","AREA","MALES","MALES","MOBILEHOME"] + fields = ["AREA", "AREA", "AREA", "MALES", "MALES", "MOBILEHOME"] test_record_attributes(fields=fields) # check that only 3 values with shapefile.Reader("shapefiles/blockgroups") as sf: @@ -709,13 +962,13 @@ def test_record_oid(): record = sf.record(i) assert record.oid == i - for i,record in enumerate(sf.records()): + for i, record in enumerate(sf.records()): assert record.oid == i - for i,record in enumerate(sf.iterRecords()): + for i, record in enumerate(sf.iterRecords()): assert record.oid == i - for i,shaperec in enumerate(sf.iterShapeRecords()): + for i, shaperec in enumerate(sf.iterShapeRecords()): assert shaperec.record.oid == i @@ -729,13 +982,13 @@ def test_shape_oid(): shape = sf.shape(i) assert shape.oid == i - for i,shape in enumerate(sf.shapes()): + for i, shape in enumerate(sf.shapes()): assert shape.oid == i - for i,shape in enumerate(sf.iterShapes()): + for i, shape in enumerate(sf.iterShapes()): assert shape.oid == i - for i,shaperec in enumerate(sf.iterShapeRecords()): + for i, shaperec in enumerate(sf.iterShapeRecords()): assert shaperec.shape.oid == i @@ -745,30 +998,32 @@ def test_shape_oid_no_shx(): its index in the shapefile, when shx file is missing. """ basename = "shapefiles/blockgroups" - shp = open(basename + ".shp", 'rb') - dbf = open(basename + ".dbf", 'rb') - with shapefile.Reader(shp=shp, dbf=dbf) as sf, \ - shapefile.Reader(basename) as sf_expected: - for i in range(len(sf)): - shape = sf.shape(i) - assert shape.oid == i - shape_expected = sf_expected.shape(i) - assert shape.__geo_interface__ == shape_expected.__geo_interface__ - - for i,shape in enumerate(sf.shapes()): - assert shape.oid == i - shape_expected = sf_expected.shape(i) - assert shape.__geo_interface__ == shape_expected.__geo_interface__ - - for i,shape in enumerate(sf.iterShapes()): - assert shape.oid == i - shape_expected = sf_expected.shape(i) - assert shape.__geo_interface__ == shape_expected.__geo_interface__ - - for i,shaperec in enumerate(sf.iterShapeRecords()): - assert shaperec.shape.oid == i - shape_expected = sf_expected.shape(i) - assert shaperec.shape.__geo_interface__ == shape_expected.__geo_interface__ + shp = open(basename + ".shp", "rb") + dbf = open(basename + ".dbf", "rb") + with shapefile.Reader(shp=shp, dbf=dbf) as sf: + with shapefile.Reader(basename) as sf_expected: + for i in range(len(sf)): + shape = sf.shape(i) + assert shape.oid == i + shape_expected = sf_expected.shape(i) + assert shape.__geo_interface__ == shape_expected.__geo_interface__ + + for i, shape in enumerate(sf.shapes()): + assert shape.oid == i + shape_expected = sf_expected.shape(i) + assert shape.__geo_interface__ == shape_expected.__geo_interface__ + + for i, shape in enumerate(sf.iterShapes()): + assert shape.oid == i + shape_expected = sf_expected.shape(i) + assert shape.__geo_interface__ == shape_expected.__geo_interface__ + + for i, shaperec in enumerate(sf.iterShapeRecords()): + assert shaperec.shape.oid == i + shape_expected = sf_expected.shape(i) + assert ( + shaperec.shape.__geo_interface__ == shape_expected.__geo_interface__ + ) def test_reader_offsets(): @@ -791,8 +1046,8 @@ def test_reader_offsets_no_shx(): the offsets unless necessary, i.e. reading all the shapes. """ basename = "shapefiles/blockgroups" - shp = open(basename + ".shp", 'rb') - dbf = open(basename + ".dbf", 'rb') + shp = open(basename + ".shp", "rb") + dbf = open(basename + ".dbf", "rb") with shapefile.Reader(shp=shp, dbf=dbf) as sf: # offsets should not be built during loading assert not sf._offsets @@ -805,7 +1060,6 @@ def test_reader_offsets_no_shx(): assert len(sf._offsets) == len(shapes) - def test_reader_numshapes(): """ Assert that reader reads the numShapes attribute from the @@ -814,7 +1068,7 @@ def test_reader_numshapes(): basename = "shapefiles/blockgroups" with shapefile.Reader(basename) as sf: # numShapes should be set during loading - assert sf.numShapes != None + assert sf.numShapes is not None # numShapes should equal the number of shapes assert sf.numShapes == len(sf.shapes()) @@ -826,11 +1080,11 @@ def test_reader_numshapes_no_shx(): reading all the shapes will set the numShapes attribute. """ basename = "shapefiles/blockgroups" - shp = open(basename + ".shp", 'rb') - dbf = open(basename + ".dbf", 'rb') + shp = open(basename + ".shp", "rb") + dbf = open(basename + ".dbf", "rb") with shapefile.Reader(shp=shp, dbf=dbf) as sf: # numShapes should be unknown due to missing shx file - assert sf.numShapes == None + assert sf.numShapes is None # numShapes should be set after reading all the shapes shapes = sf.shapes() assert sf.numShapes == len(shapes) @@ -861,7 +1115,7 @@ def test_reader_len_dbf_only(): is equal to length of all records. """ basename = "shapefiles/blockgroups" - dbf = open(basename + ".dbf", 'rb') + dbf = open(basename + ".dbf", "rb") with shapefile.Reader(dbf=dbf) as sf: assert len(sf) == len(sf.records()) @@ -872,8 +1126,8 @@ def test_reader_len_no_dbf(): is equal to length of all shapes. """ basename = "shapefiles/blockgroups" - shp = open(basename + ".shp", 'rb') - shx = open(basename + ".shx", 'rb') + shp = open(basename + ".shp", "rb") + shx = open(basename + ".shx", "rb") with shapefile.Reader(shp=shp, shx=shx) as sf: assert len(sf) == len(sf.shapes()) @@ -884,7 +1138,7 @@ def test_reader_len_no_dbf_shx(): is equal to length of all shapes. """ basename = "shapefiles/blockgroups" - shp = open(basename + ".shp", 'rb') + shp = open(basename + ".shp", "rb") with shapefile.Reader(shp=shp) as sf: assert len(sf) == len(sf.shapes()) @@ -902,10 +1156,10 @@ def test_reader_corrupt_files(): # add 10 line geoms for _ in range(10): w.record("value") - w.line([[(1,1),(1,2),(2,2)]]) + w.line([[(1, 1), (1, 2), (2, 2)]]) # add junk byte data to end of dbf and shp files - w.dbf.write(b'12345') - w.shp.write(b'12345') + w.dbf.write(b"12345") + w.shp.write(b"12345") # read the corrupt shapefile and assert that it reads correctly with shapefile.Reader(basename) as sf: @@ -958,7 +1212,7 @@ def test_bboxfilter_shapes(): # compare assert len(shapes) == len(manual) # check that they line up - for shape,man in zip(shapes,manual): + for shape, man in zip(shapes, manual): assert shape.oid == man.oid assert shape.__geo_interface__ == man.__geo_interface__ @@ -991,7 +1245,7 @@ def test_bboxfilter_itershapes(): # compare assert len(shapes) == len(manual) # check that they line up - for shape,man in zip(shapes,manual): + for shape, man in zip(shapes, manual): assert shape.oid == man.oid assert shape.__geo_interface__ == man.__geo_interface__ @@ -1031,7 +1285,7 @@ def test_bboxfilter_shaperecords(): # compare assert len(shaperecs) == len(manual) # check that they line up - for shaperec,man in zip(shaperecs,manual): + for shaperec, man in zip(shaperecs, manual): # oids assert shaperec.shape.oid == shaperec.record.oid # same shape as manual @@ -1059,7 +1313,7 @@ def test_bboxfilter_itershaperecords(): # compare assert len(shaperecs) == len(manual) # check that they line up - for shaperec,man in zip(shaperecs,manual): + for shaperec, man in zip(shaperecs, manual): # oids assert shaperec.shape.oid == shaperec.record.oid # same shape as manual @@ -1112,7 +1366,7 @@ def test_shaperecord_record(): shaperec = sf.shapeRecord(3) record = shaperec.record - assert record[1:3] == ['060750601001', 4715] + assert record[1:3] == ["060750601001", 4715] def test_write_field_name_limit(tmpdir): @@ -1121,11 +1375,11 @@ def test_write_field_name_limit(tmpdir): """ filename = tmpdir.join("test.shp").strpath with shapefile.Writer(filename) as writer: - writer.field('a'*5, 'C') # many under length limit - writer.field('a'*9, 'C') # 1 under length limit - writer.field('a'*10, 'C') # at length limit - writer.field('a'*11, 'C') # 1 over length limit - writer.field('a'*20, 'C') # many over limit + writer.field("a" * 5, "C") # many under length limit + writer.field("a" * 9, "C") # 1 under length limit + writer.field("a" * 10, "C") # at length limit + writer.field("a" * 11, "C") # 1 over length limit + writer.field("a" * 20, "C") # many over limit with shapefile.Reader(filename) as reader: fields = reader.fields[1:] @@ -1143,7 +1397,7 @@ def test_write_shp_only(tmpdir): creates just a shp file. """ filename = tmpdir.join("test").strpath - with shapefile.Writer(shp=filename+'.shp') as writer: + with shapefile.Writer(shp=filename + ".shp") as writer: writer.point(1, 1) assert writer.shp and not writer.shx and not writer.dbf assert writer.shpNum == 1 @@ -1151,19 +1405,22 @@ def test_write_shp_only(tmpdir): assert writer.shp.closed is True # assert test.shp exists - assert os.path.exists(filename+'.shp') + assert os.path.exists(filename + ".shp") # test that can read shapes - with shapefile.Reader(shp=filename+'.shp') as reader: + with shapefile.Reader(shp=filename + ".shp") as reader: assert reader.shp and not reader.shx and not reader.dbf - assert (reader.numRecords, reader.numShapes) == (None, None) # numShapes is unknown in the absence of shx file + assert (reader.numRecords, reader.numShapes) == ( + None, + None, + ) # numShapes is unknown in the absence of shx file assert len(reader.shapes()) == 1 # assert test.shx does not exist - assert not os.path.exists(filename+'.shx') + assert not os.path.exists(filename + ".shx") # assert test.dbf does not exist - assert not os.path.exists(filename+'.dbf') + assert not os.path.exists(filename + ".dbf") def test_write_shp_shx_only(tmpdir): @@ -1173,7 +1430,7 @@ def test_write_shp_shx_only(tmpdir): creates just a shp and shx file. """ filename = tmpdir.join("test").strpath - with shapefile.Writer(shp=filename+'.shp', shx=filename+'.shx') as writer: + with shapefile.Writer(shp=filename + ".shp", shx=filename + ".shx") as writer: writer.point(1, 1) assert writer.shp and writer.shx and not writer.dbf assert writer.shpNum == 1 @@ -1181,21 +1438,21 @@ def test_write_shp_shx_only(tmpdir): assert writer.shp.closed is writer.shx.closed is True # assert test.shp exists - assert os.path.exists(filename+'.shp') + assert os.path.exists(filename + ".shp") # assert test.shx exists - assert os.path.exists(filename+'.shx') + assert os.path.exists(filename + ".shx") # test that can read shapes and offsets - with shapefile.Reader(shp=filename+'.shp', shx=filename+'.shx') as reader: + with shapefile.Reader(shp=filename + ".shp", shx=filename + ".shx") as reader: assert reader.shp and reader.shx and not reader.dbf assert (reader.numRecords, reader.numShapes) == (None, 1) - reader.shape(0) # trigger reading of shx offsets + reader.shape(0) # trigger reading of shx offsets assert len(reader._offsets) == 1 assert len(reader.shapes()) == 1 # assert test.dbf does not exist - assert not os.path.exists(filename+'.dbf') + assert not os.path.exists(filename + ".dbf") def test_write_shp_dbf_only(tmpdir): @@ -1205,9 +1462,9 @@ def test_write_shp_dbf_only(tmpdir): creates just a shp and dbf file. """ filename = tmpdir.join("test").strpath - with shapefile.Writer(shp=filename+'.shp', dbf=filename+'.dbf') as writer: - writer.field('field1', 'C') # required to create a valid dbf file - writer.record('value') + with shapefile.Writer(shp=filename + ".shp", dbf=filename + ".dbf") as writer: + writer.field("field1", "C") # required to create a valid dbf file + writer.record("value") writer.point(1, 1) assert writer.shp and not writer.shx and writer.dbf assert writer.shpNum == writer.recNum == 1 @@ -1215,20 +1472,23 @@ def test_write_shp_dbf_only(tmpdir): assert writer.shp.closed is writer.dbf.closed is True # assert test.shp exists - assert os.path.exists(filename+'.shp') + assert os.path.exists(filename + ".shp") # assert test.dbf exists - assert os.path.exists(filename+'.dbf') + assert os.path.exists(filename + ".dbf") # test that can read records and shapes - with shapefile.Reader(shp=filename+'.shp', dbf=filename+'.dbf') as reader: + with shapefile.Reader(shp=filename + ".shp", dbf=filename + ".dbf") as reader: assert reader.shp and not reader.shx and reader.dbf - assert (reader.numRecords, reader.numShapes) == (1, None) # numShapes is unknown in the absence of shx file + assert (reader.numRecords, reader.numShapes) == ( + 1, + None, + ) # numShapes is unknown in the absence of shx file assert len(reader.records()) == 1 assert len(reader.shapes()) == 1 # assert test.shx does not exist - assert not os.path.exists(filename+'.shx') + assert not os.path.exists(filename + ".shx") def test_write_dbf_only(tmpdir): @@ -1238,28 +1498,28 @@ def test_write_dbf_only(tmpdir): creates just a dbf file. """ filename = tmpdir.join("test").strpath - with shapefile.Writer(dbf=filename+'.dbf') as writer: - writer.field('field1', 'C') # required to create a valid dbf file - writer.record('value') + with shapefile.Writer(dbf=filename + ".dbf") as writer: + writer.field("field1", "C") # required to create a valid dbf file + writer.record("value") assert not writer.shp and not writer.shx and writer.dbf assert writer.recNum == 1 assert len(writer) == 1 assert writer.dbf.closed is True # assert test.dbf exists - assert os.path.exists(filename+'.dbf') + assert os.path.exists(filename + ".dbf") # test that can read records - with shapefile.Reader(dbf=filename+'.dbf') as reader: + with shapefile.Reader(dbf=filename + ".dbf") as reader: assert not writer.shp and not writer.shx and writer.dbf assert (reader.numRecords, reader.numShapes) == (1, None) assert len(reader.records()) == 1 # assert test.shp does not exist - assert not os.path.exists(filename+'.shp') + assert not os.path.exists(filename + ".shp") # assert test.shx does not exist - assert not os.path.exists(filename+'.shx') + assert not os.path.exists(filename + ".shx") def test_write_default_shp_shx_dbf(tmpdir): @@ -1270,8 +1530,8 @@ def test_write_default_shp_shx_dbf(tmpdir): """ filename = tmpdir.join("test").strpath with shapefile.Writer(filename) as writer: - writer.field('field1', 'C') # required to create a valid dbf file - writer.record('value') + writer.field("field1", "C") # required to create a valid dbf file + writer.record("value") writer.null() # assert shp, shx, dbf files exist @@ -1288,8 +1548,8 @@ def test_write_pathlike(tmpdir): filename = tmpdir.join("test") assert not isinstance(filename, str) with shapefile.Writer(filename) as writer: - writer.field('field1', 'C') - writer.record('value') + writer.field("field1", "C") + writer.record("value") writer.null() assert (filename + ".shp").ensure() assert (filename + ".shx").ensure() @@ -1300,12 +1560,12 @@ def test_write_filelike(tmpdir): """ Assert that file-like objects are written correctly. """ - shp = open(tmpdir.join("test.shp").strpath, mode='wb+') - shx = open(tmpdir.join("test.shx").strpath, mode='wb+') - dbf = open(tmpdir.join("test.dbf").strpath, mode='wb+') + shp = open(tmpdir.join("test.shp").strpath, mode="wb+") + shx = open(tmpdir.join("test.shx").strpath, mode="wb+") + dbf = open(tmpdir.join("test.dbf").strpath, mode="wb+") with shapefile.Writer(shx=shx, dbf=dbf, shp=shp) as writer: - writer.field('field1', 'C') # required to create a valid dbf file - writer.record('value') + writer.field("field1", "C") # required to create a valid dbf file + writer.record("value") writer.null() # test that filelike objects were written correctly @@ -1320,9 +1580,9 @@ def test_write_close_path(tmpdir): closes the shp, shx, and dbf files on exit, if given paths. """ - sf = shapefile.Writer(tmpdir.join('test')) - sf.field('field1', 'C') # required to create a valid dbf file - sf.record('value') + sf = shapefile.Writer(tmpdir.join("test")) + sf.field("field1", "C") # required to create a valid dbf file + sf.record("value") sf.null() sf.close() @@ -1331,7 +1591,7 @@ def test_write_close_path(tmpdir): assert sf.shx.closed is True # test that opens and reads correctly after - with shapefile.Reader(tmpdir.join('test')) as reader: + with shapefile.Reader(tmpdir.join("test")) as reader: assert len(reader) == 1 assert reader.shape(0).shapeType == shapefile.NULL @@ -1342,12 +1602,12 @@ def test_write_close_filelike(tmpdir): leaves the shp, shx, and dbf files open on exit, if given filelike objects. """ - shp = open(tmpdir.join("test.shp").strpath, mode='wb+') - shx = open(tmpdir.join("test.shx").strpath, mode='wb+') - dbf = open(tmpdir.join("test.dbf").strpath, mode='wb+') + shp = open(tmpdir.join("test.shp").strpath, mode="wb+") + shx = open(tmpdir.join("test.shx").strpath, mode="wb+") + dbf = open(tmpdir.join("test.dbf").strpath, mode="wb+") sf = shapefile.Writer(shx=shx, dbf=dbf, shp=shp) - sf.field('field1', 'C') # required to create a valid dbf file - sf.record('value') + sf.field("field1", "C") # required to create a valid dbf file + sf.record("value") sf.null() sf.close() @@ -1367,9 +1627,9 @@ def test_write_context_path(tmpdir): closes the shp, shx, and dbf files on exit, if given paths. """ - with shapefile.Writer(tmpdir.join('test')) as sf: - sf.field('field1', 'C') # required to create a valid dbf file - sf.record('value') + with shapefile.Writer(tmpdir.join("test")) as sf: + sf.field("field1", "C") # required to create a valid dbf file + sf.record("value") sf.null() assert sf.shp.closed is True @@ -1377,7 +1637,7 @@ def test_write_context_path(tmpdir): assert sf.shx.closed is True # test that opens and reads correctly after - with shapefile.Reader(tmpdir.join('test')) as reader: + with shapefile.Reader(tmpdir.join("test")) as reader: assert len(reader) == 1 assert reader.shape(0).shapeType == shapefile.NULL @@ -1388,12 +1648,12 @@ def test_write_context_filelike(tmpdir): leaves the shp, shx, and dbf files open on exit, if given filelike objects. """ - shp = open(tmpdir.join("test.shp").strpath, mode='wb+') - shx = open(tmpdir.join("test.shx").strpath, mode='wb+') - dbf = open(tmpdir.join("test.dbf").strpath, mode='wb+') + shp = open(tmpdir.join("test.shp").strpath, mode="wb+") + shx = open(tmpdir.join("test.shx").strpath, mode="wb+") + dbf = open(tmpdir.join("test.dbf").strpath, mode="wb+") with shapefile.Writer(shx=shx, dbf=dbf, shp=shp) as sf: - sf.field('field1', 'C') # required to create a valid dbf file - sf.record('value') + sf.field("field1", "C") # required to create a valid dbf file + sf.record("value") sf.null() assert sf.shp.closed is False @@ -1415,7 +1675,7 @@ def test_write_shapefile_extension_ignored(tmpdir): ext = ".abc" filename = tmpdir.join(base + ext).strpath with shapefile.Writer(filename) as writer: - writer.field('field1', 'C') # required to create a valid dbf file + writer.field("field1", "C") # required to create a valid dbf file # assert shp, shx, dbf files exist basepath = tmpdir.join(base).strpath @@ -1436,12 +1696,12 @@ def test_write_record(tmpdir): with shapefile.Writer(filename) as writer: writer.autoBalance = True - writer.field('one', 'C') - writer.field('two', 'C') - writer.field('three', 'C') - writer.field('four', 'C') + writer.field("one", "C") + writer.field("two", "C") + writer.field("three", "C") + writer.field("four", "C") - values = ['one','two','three','four'] + values = ["one", "two", "three", "four"] writer.record(*values) writer.record(*values) @@ -1463,12 +1723,12 @@ def test_write_partial_record(tmpdir): with shapefile.Writer(filename) as writer: writer.autoBalance = True - writer.field('one', 'C') - writer.field('two', 'C') - writer.field('three', 'C') - writer.field('four', 'C') + writer.field("one", "C") + writer.field("two", "C") + writer.field("three", "C") + writer.field("four", "C") - values = ['one','two'] + values = ["one", "two"] writer.record(*values) writer.record(*values) @@ -1478,7 +1738,7 @@ def test_write_partial_record(tmpdir): with shapefile.Reader(filename) as reader: expected = list(values) - expected.extend(['','']) + expected.extend(["", ""]) for record in reader.iterRecords(): assert record == expected @@ -1491,13 +1751,13 @@ def test_write_geojson(tmpdir): """ filename = tmpdir.join("test").strpath with shapefile.Writer(filename) as w: - w.field('TEXT', 'C') - w.field('NUMBER', 'N') - w.field('DATE', 'D') - w.record('text', 123, datetime.date(1898,1,30)) - w.record('text', 123, [1998,1,30]) - w.record('text', 123, '19980130') - w.record('text', 123, '-9999999') # faulty date + w.field("TEXT", "C") + w.field("NUMBER", "N") + w.field("DATE", "D") + w.record("text", 123, datetime.date(1898, 1, 30)) + w.record("text", 123, [1998, 1, 30]) + w.record("text", 123, "19980130") + w.record("text", 123, "-9999999") # faulty date w.record(None, None, None) w.null() w.null() @@ -1512,7 +1772,9 @@ def test_write_geojson(tmpdir): assert json.dumps(r.__geo_interface__) -shape_types = [k for k in shapefile.SHAPETYPE_LOOKUP.keys() if k != 31] # exclude multipatch +shape_types = [ + k for k in shapefile.SHAPETYPE_LOOKUP.keys() if k != 31 +] # exclude multipatch @pytest.mark.parametrize("shape_type", shape_types) @@ -1522,7 +1784,7 @@ def test_write_empty_shapefile(tmpdir, shape_type): """ filename = tmpdir.join("test").strpath with shapefile.Writer(filename, shapeType=shape_type) as w: - w.field('field1', 'C') # required to create a valid dbf file + w.field("field1", "C") # required to create a valid dbf file with shapefile.Reader(filename) as r: # test correct shape type