diff --git a/.travis.yml b/.travis.yml index efe10ae..89f0a7e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ language: python python: - - "2.7" - - "3.5" - "3.6" - "3.7" - "3.8" diff --git a/README.md b/README.md index e985ade..27868a8 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Both the Esri and XBase file-formats are very simple in design and memory efficient which is part of the reason the shapefile format remains popular despite the numerous ways to store and exchange GIS data available today. -Pyshp is compatible with Python 2.7-3.x. +Pyshp is compatible with Python 3. This document provides examples for using PyShp to read and write shapefiles. However many more examples are continually added to the blog [http://GeospatialPython.com](http://GeospatialPython.com), diff --git a/setup.py b/setup.py index 64c1f62..9671013 100644 --- a/setup.py +++ b/setup.py @@ -19,11 +19,9 @@ def read_file(file): license='MIT', zip_safe=False, keywords='gis geospatial geographic shapefile shapefiles', - python_requires='>= 2.7', + python_requires='>= 3.6', classifiers=['Programming Language :: Python', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', diff --git a/shapefile.py b/shapefile.py index 07c8aa7..f3b7fee 100644 --- a/shapefile.py +++ b/shapefile.py @@ -3,7 +3,7 @@ Provides read and write support for ESRI Shapefiles. author: jlawheadgeospatialpython.com version: 2.1.3 -Compatible with Python versions 2.7-3.x +Compatible with Python 3 """ __version__ = "2.1.3" @@ -70,85 +70,42 @@ 5: 'RING'} -# Python 2-3 handling - -PYTHON3 = sys.version_info[0] == 3 - -if PYTHON3: - xrange = range - izip = zip -else: - from itertools import izip - - # Helpers 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'): - if isinstance(v, str): - # For python 3 encode str to bytes. - return v.encode(encoding, encodingErrors) - elif isinstance(v, bytes): - # Already bytes. - return v - elif v is None: - # Since we're dealing with text, interpret None as "" - return b"" - else: - # Force string representation. - return str(v).encode(encoding, encodingErrors) - - def u(v, encoding='utf-8', encodingErrors='strict'): - if isinstance(v, bytes): - # For python 3 decode bytes to str. - return v.decode(encoding, encodingErrors) - elif isinstance(v, str): - # Already str. - return v - elif v is None: - # Since we're dealing with text, interpret None as "" - return "" - else: - # Force string representation. - return bytes(v).decode(encoding, encodingErrors) - - def is_string(v): - return isinstance(v, str) - -else: - def b(v, encoding='utf-8', encodingErrors='strict'): - if isinstance(v, unicode): - # For python 2 encode unicode to bytes. - return v.encode(encoding, encodingErrors) - elif isinstance(v, bytes): - # Already bytes. - return v - elif v is None: - # Since we're dealing with text, interpret None as "" - return "" - else: - # Force string representation. - return unicode(v).encode(encoding, encodingErrors) - - def u(v, encoding='utf-8', encodingErrors='strict'): - if isinstance(v, bytes): - # For python 2 decode bytes to unicode. - return v.decode(encoding, encodingErrors) - elif isinstance(v, unicode): - # Already unicode. - return v - elif v is None: - # Since we're dealing with text, interpret None as "" - return u"" - else: - # Force string representation. - return bytes(v).decode(encoding, encodingErrors) - def is_string(v): - return isinstance(v, basestring) +def b(v, encoding='utf-8', encodingErrors='strict'): + """Return value as bytes.""" + if isinstance(v, str): + # Encode str to bytes. + return v.encode(encoding, encodingErrors) + elif isinstance(v, bytes): + # Already bytes. + return v + elif v is None: + # Since we're dealing with text, interpret None as "" + return b"" + else: + # Force string representation. + return str(v).encode(encoding, encodingErrors) + + +def u(v, encoding='utf-8', encodingErrors='strict'): + """Return value as str.""" + if isinstance(v, bytes): + # Decode bytes to str. + return v.decode(encoding, encodingErrors) + elif isinstance(v, str): + # Already str. + return v + elif v is None: + # Since we're dealing with text, interpret None as "" + return "" + else: + # Force string representation. + return bytes(v).decode(encoding, encodingErrors) # Begin @@ -261,8 +218,7 @@ def ring_sample(coords, ccw=False): triplet = [] def itercoords(): # iterate full closed ring - for p in coords: - yield p + yield from coords # finally, yield the second coordinate to the end to allow checking the last triplet yield coords[1] @@ -298,7 +254,7 @@ def itercoords(): def ring_contains_ring(coords1, coords2): '''Returns True if all vertexes in coords2 are fully inside coords1. ''' - return all((ring_contains_point(coords1, p2) for p2 in coords2)) + 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. @@ -347,7 +303,7 @@ 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 = {hole_i:[] for hole_i in range(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]) @@ -427,7 +383,7 @@ def organize_polygon_rings(rings, return_errors=None): polys = [[ext] for ext in exteriors] return polys -class Shape(object): +class Shape: 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 @@ -519,7 +475,7 @@ def __geo_interface__(self): else: # get all polygon rings rings = [] - for i in xrange(len(self.parts)): + for i in range(len(self.parts)): # get indexes of start and end points of the ring start = self.parts[i] try: @@ -538,7 +494,7 @@ 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) + header = f'Possible issue encountered when converting Shape #{self.oid} to GeoJSON: ' 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, \ @@ -660,7 +616,7 @@ def shapeTypeName(self): return SHAPETYPE_LOOKUP[self.shapeType] def __repr__(self): - return 'Shape #{}: {}'.format(self.__oid, self.shapeTypeName) + return f'Shape #{self.__oid}: {self.shapeTypeName}' class _Record(list): """ @@ -708,9 +664,9 @@ def __getattr__(self, item): index = self.__field_positions[item] return list.__getitem__(self, index) except KeyError: - raise AttributeError('{} is not a field name'.format(item)) + raise AttributeError(f'{item} is not a field name') except IndexError: - raise IndexError('{} found as a field but not enough values available.'.format(item)) + raise IndexError(f'{item} found as a field but not enough values available.') def __setattr__(self, key, value): """ @@ -726,7 +682,7 @@ def __setattr__(self, key, value): index = self.__field_positions[key] return list.__setitem__(self, index, value) except KeyError: - raise AttributeError('{} is not a field name'.format(key)) + raise AttributeError(f'{key} is not a field name') def __getitem__(self, item): """ @@ -747,7 +703,7 @@ def __getitem__(self, item): if index is not None: return list.__getitem__(self, index) else: - raise IndexError('"{}" is not a field name and not an int'.format(item)) + raise IndexError(f'"{item}" is not a field name and not an int') def __setitem__(self, key, value): """ @@ -765,7 +721,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(f'{key} is not a field name and not an int') @property def oid(self): @@ -777,15 +733,15 @@ def as_dict(self, date_strings=False): Returns this Record as a dictionary using the field names as keys :return: dict """ - dct = dict((f, self[i]) for f, i in self.__field_positions.items()) + dct = {f: self[i] for f, i in self.__field_positions.items()} if date_strings: for k,v in dct.items(): if isinstance(v, date): - dct[k] = '{:04d}{:02d}{:02d}'.format(v.year, v.month, v.day) + dct[k] = f'{v.year:04d}{v.month:02d}{v.day:02d}' return dct def __repr__(self): - return 'Record #{}: {}'.format(self.__oid, list(self)) + return f'Record #{self.__oid}: {list(self)}' def __dir__(self): """ @@ -795,10 +751,10 @@ 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) + fnames = list(self.__field_positions.keys()) # plus field names return default + fnames -class ShapeRecord(object): +class ShapeRecord: """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): @@ -818,7 +774,7 @@ class Shapes(list): to return a GeometryCollection dictionary.""" def __repr__(self): - return 'Shapes: {}'.format(list(self)) + return f'Shapes: {list(self)}' @property def __geo_interface__(self): @@ -835,7 +791,7 @@ class ShapeRecords(list): to return a FeatureCollection dictionary.""" def __repr__(self): - return 'ShapeRecords: {}'.format(list(self)) + return f'ShapeRecords: {list(self)}' @property def __geo_interface__(self): @@ -883,7 +839,7 @@ class ShapefileException(Exception): # msg = '\n'.join(messages) # logging.warning(msg) -class Reader(object): +class Reader: """Reads the three files of a shapefile as a unit or separately. If one of the three files (.shp, .shx, .dbf) is missing no exception is thrown until you try @@ -917,7 +873,7 @@ def __init__(self, *args, **kwargs): self.encodingErrors = kwargs.pop('encodingErrors', 'strict') # See if a shapefile name was passed as the first argument if len(args) > 0: - if is_string(args[0]): + if isinstance(args[0], str): self.load(args[0]) return # Otherwise, load from separate shp/shx/dbf args (must be file-like) @@ -1019,8 +975,7 @@ def __len__(self): def __iter__(self): """Iterates through the shapes/records in the shapefile.""" - for shaperec in self.iterShapeRecords(): - yield shaperec + yield from self.iterShapeRecords() @property def __geo_interface__(self): @@ -1044,7 +999,7 @@ 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(f"Unable to open {shapeName}.dbf or {shapeName}.shp.") if self.shp: self.__shpHeader() if self.dbf: @@ -1056,11 +1011,11 @@ def load_shp(self, shapefile_name): """ shp_ext = 'shp' try: - self.shp = open("%s.%s" % (shapefile_name, shp_ext), "rb") - except IOError: + self.shp = open(f"{shapefile_name}.{shp_ext}", "rb") + except OSError: try: - self.shp = open("%s.%s" % (shapefile_name, shp_ext.upper()), "rb") - except IOError: + self.shp = open(f"{shapefile_name}.{shp_ext.upper()}", "rb") + except OSError: pass def load_shx(self, shapefile_name): @@ -1069,11 +1024,11 @@ def load_shx(self, shapefile_name): """ shx_ext = 'shx' try: - self.shx = open("%s.%s" % (shapefile_name, shx_ext), "rb") - except IOError: + self.shx = open(f"{shapefile_name}.{shx_ext}", "rb") + except OSError: try: - self.shx = open("%s.%s" % (shapefile_name, shx_ext.upper()), "rb") - except IOError: + self.shx = open(f"{shapefile_name}.{shx_ext.upper()}", "rb") + except OSError: pass def load_dbf(self, shapefile_name): @@ -1082,11 +1037,11 @@ def load_dbf(self, shapefile_name): """ dbf_ext = 'dbf' try: - self.dbf = open("%s.%s" % (shapefile_name, dbf_ext), "rb") - except IOError: + self.dbf = open(f"{shapefile_name}.{dbf_ext}", "rb") + except OSError: try: - self.dbf = open("%s.%s" % (shapefile_name, dbf_ext.upper()), "rb") - except IOError: + self.dbf = open(f"{shapefile_name}.{dbf_ext.upper()}", "rb") + except OSError: pass def __del__(self): @@ -1097,7 +1052,7 @@ def close(self): if hasattr(attribute, 'close'): try: attribute.close() - except IOError: + except OSError: pass def __getFileObj(self, f): @@ -1176,7 +1131,7 @@ def __shape(self, oid=None): # Read points - produces a list of [x,y] values if nPoints: flat = unpack("<%sd" % (2 * nPoints), f.read(16*nPoints)) - record.points = list(izip(*(iter(flat),) * 2)) + record.points = list(zip(*(iter(flat),) * 2)) # Read z extremes and values if shapeType in (13,15,18,31): (zmin, zmax) = unpack("<2d", f.read(16)) @@ -1314,7 +1269,7 @@ def __dbfHeader(self): self.__recStruct = Struct(fmt) # Store the field positions - self.__fieldposition_lookup = dict((f[0], i) for i, f in enumerate(self.fields[1:])) + self.__fieldposition_lookup = {f[0]: i for i, f in enumerate(self.fields[1:])} def __recordFmt(self): """Calculates the format and size of a .dbf record.""" @@ -1378,7 +1333,7 @@ def __record(self, oid=None): y, m, d = int(value[:4]), int(value[4:6]), int(value[6:8]) value = date(y, m, d) except: - # if invalid date, just return as unicode string so user can decide + # if invalid date, just return as str so user can decide value = u(value.strip()) elif typ == 'L': # logical: 1 byte - initialized to 0x20 (space) otherwise T or F. @@ -1392,7 +1347,7 @@ def __record(self, oid=None): else: value = None # unknown value is set to missing else: - # anything else is forced to string/unicode + # anything else is forced to str value = u(value, self.encoding, self.encodingErrors) value = value.strip() record.append(value) @@ -1430,7 +1385,7 @@ def iterRecords(self): self.__dbfHeader() f = self.__getFileObj(self.dbf) f.seek(self.__dbfHdrLength) - for i in xrange(self.numRecords): + for i in range(self.numRecords): r = self.__record(oid=i) if r: yield r @@ -1449,11 +1404,11 @@ def shapeRecords(self): def iterShapeRecords(self): """Returns a generator of combination geometry/attribute records for all records in a shapefile.""" - for shape, record in izip(self.iterShapes(), self.iterRecords()): + for shape, record in zip(self.iterShapes(), self.iterRecords()): yield ShapeRecord(shape=shape, record=record) -class Writer(object): +class Writer: """Provides write support for ESRI Shapefiles.""" def __init__(self, target=None, shapeType=None, autoBalance=False, **kwargs): self.target = target @@ -1462,8 +1417,8 @@ def __init__(self, target=None, shapeType=None, autoBalance=False, **kwargs): self.shapeType = shapeType self.shp = self.shx = self.dbf = None if target: - if not is_string(target): - raise Exception('The target filepath {} must be of type str/unicode, not {}.'.format(repr(target), type(target)) ) + if not isinstance(target, str): + raise Exception(f'The target filepath {target!r} must be of type str, not {type(target)}.' ) self.shp = self.__getFileObj(os.path.splitext(target)[0] + '.shp') self.shx = self.__getFileObj(os.path.splitext(target)[0] + '.shx') self.dbf = self.__getFileObj(os.path.splitext(target)[0] + '.dbf') @@ -1546,7 +1501,7 @@ def close(self): if hasattr(attribute, 'close'): try: attribute.close() - except IOError: + except OSError: pass def __getFileObj(self, f): @@ -1734,7 +1689,7 @@ def __dbfHeader(self): if headerLength >= 65535: raise ShapefileException( "Shapefile dbf header length exceeds maximum length.") - recordLength = sum([int(field[2]) for field in fields]) + 1 + recordLength = sum(int(field[2]) for field in fields) + 1 header = pack('