From 8f01e97b766122214fafea7546d2d1b75ad88253 Mon Sep 17 00:00:00 2001 From: James Parrott <80779630+JamesParrott@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:35:08 +0100 Subject: [PATCH 01/17] Update .gitignore --- .gitignore | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index d173420..4108589 100644 --- a/.gitignore +++ b/.gitignore @@ -22,5 +22,8 @@ dist/ .vscode .dmypy.json .python-version -.venv -venv +.venv/ +venv/ +.mypy_cache/ +.pytest_cache/ +.ruff_cache/ From e2fb8c8d90fadea9babaaa63c5c50ee24a14acd4 Mon Sep 17 00:00:00 2001 From: James Parrott <80779630+JamesParrott@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:35:12 +0100 Subject: [PATCH 02/17] Update changelog.txt --- changelog.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/changelog.txt b/changelog.txt index be97791..c15a414 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,18 @@ +VERSION 3.0.0-alpha + +Python 2 and Python 3.8 support dropped + +2025-07-22 + Code quality + * Type hints + * f-strings + * Remove Python 2 specific functions. + * Run doctests against wheels. + * Testing of wheels before publishing them + * pyproject.toml src layout + * Slow test marked. + + VERSION 2.4.0 2025-07-21 From e77950189796655a41028d6cd0d581eb34012cfb Mon Sep 17 00:00:00 2001 From: James Parrott <80779630+JamesParrott@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:35:30 +0100 Subject: [PATCH 03/17] Update run_checks_build_and_test.yml --- .github/workflows/run_checks_build_and_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run_checks_build_and_test.yml b/.github/workflows/run_checks_build_and_test.yml index bbafa08..931a625 100644 --- a/.github/workflows/run_checks_build_and_test.yml +++ b/.github/workflows/run_checks_build_and_test.yml @@ -28,7 +28,7 @@ jobs: pip install -e . - name: run Pylint for errors and warnings only, on test_shapefile.py run: | - pylint --disable=R,C test_shapefile.py + pylint --disable=R,C test_shapefile.py src/shapefile.py build_wheel_and_sdist: runs-on: ubuntu-latest From d7823715372e2b2fb573553c3da880154307d463 Mon Sep 17 00:00:00 2001 From: James Parrott <80779630+JamesParrott@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:57:57 +0100 Subject: [PATCH 04/17] Delete unreachable else clause --- src/shapefile.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/shapefile.py b/src/shapefile.py index 4d5ad68..923a4c7 100644 --- a/src/shapefile.py +++ b/src/shapefile.py @@ -277,6 +277,10 @@ def ring_contains_point(coords: list[Coord], p: Point2D) -> bool: return inside_flag +class RingSamplingError(Exception): + pass + + def ring_sample(coords: list[Coord], ccw: bool = False) -> Point2D: """Return a sample point guaranteed to be within a ring, by efficiently finding the first centroid of a coordinate triplet whose orientation @@ -320,8 +324,11 @@ def itercoords(): # 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 RingSamplingError( + f"Unexpected error: Unable to find a ring sample point in: {coords}." + "Ensure the ring's coordinates are oriented clockwise, " + "and ensure the area enclosed is non-zero. " + ) def ring_contains_ring(coords1: list[Coord], coords2: list[Point2D]) -> bool: @@ -544,9 +551,7 @@ def __geo_interface__(self) -> GeoJsonShapeT: # coordinates.append([tuple(p) for p in self.points[ps:part]]) coordinates.append([p for p in self.points[ps:part]]) ps = part - else: - # coordinates.append([tuple(p) for p in self.points[part:]]) - coordinates.append([p for p in self.points[part:]]) + return {"type": "MultiLineString", "coordinates": coordinates} elif self.shapeType in [POLYGON, POLYGONM, POLYGONZ]: if len(self.parts) == 0: From 78684e9d5675379da13091fcfa1d38948e2ec590 Mon Sep 17 00:00:00 2001 From: James Parrott <80779630+JamesParrott@users.noreply.github.com> Date: Fri, 25 Jul 2025 13:01:02 +0100 Subject: [PATCH 05/17] Add (and use) specific GeoJSON Exception --- src/shapefile.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/shapefile.py b/src/shapefile.py index 923a4c7..3a960b4 100644 --- a/src/shapefile.py +++ b/src/shapefile.py @@ -19,7 +19,6 @@ import tempfile import time import zipfile -from collections.abc import Collection from datetime import date from struct import Struct, calcsize, error, pack, unpack from typing import IO, Any, Iterable, Iterator, Optional, Reversible, TypedDict, Union @@ -465,6 +464,10 @@ def organize_polygon_rings( return polys +class GeoJSON_Error(Exception): + pass + + class Shape: def __init__( self, @@ -610,7 +613,7 @@ def __geo_interface__(self) -> GeoJsonShapeT: return {"type": "MultiPolygon", "coordinates": polys} else: - raise Exception( + raise GeoJSON_Error( f'Shape type "{SHAPETYPE_LOOKUP[self.shapeType]}" cannot be represented as GeoJSON.' ) @@ -635,7 +638,7 @@ def _from_geojson(geoj) -> Shape: elif geojType == "MultiPolygon": shapeType = POLYGON else: - raise Exception(f"Cannot create Shape from GeoJSON type '{geojType}'") + raise GeoJSON_Error(f"Cannot create Shape from GeoJSON type '{geojType}'") shape.shapeType = shapeType # set points and parts From b98a82a5b8c5549a2f88b40b85a1695522989be6 Mon Sep 17 00:00:00 2001 From: James Parrott <80779630+JamesParrott@users.noreply.github.com> Date: Fri, 25 Jul 2025 14:10:02 +0100 Subject: [PATCH 06/17] Reformat --- src/shapefile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shapefile.py b/src/shapefile.py index 3a960b4..d295e26 100644 --- a/src/shapefile.py +++ b/src/shapefile.py @@ -325,8 +325,8 @@ def itercoords(): raise RingSamplingError( f"Unexpected error: Unable to find a ring sample point in: {coords}." - "Ensure the ring's coordinates are oriented clockwise, " - "and ensure the area enclosed is non-zero. " + "Ensure the ring's coordinates are oriented clockwise, " + "and ensure the area enclosed is non-zero. " ) From 4f1a850498198e166fb6b9b4f96f98d768b5d35d Mon Sep 17 00:00:00 2001 From: James Parrott <80779630+JamesParrott@users.noreply.github.com> Date: Fri, 25 Jul 2025 14:24:27 +0100 Subject: [PATCH 07/17] Restore code in else clause in Shape.__ geo_interface__ --- src/shapefile.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/shapefile.py b/src/shapefile.py index d295e26..367b0b5 100644 --- a/src/shapefile.py +++ b/src/shapefile.py @@ -326,7 +326,7 @@ def itercoords(): raise RingSamplingError( f"Unexpected error: Unable to find a ring sample point in: {coords}." "Ensure the ring's coordinates are oriented clockwise, " - "and ensure the area enclosed is non-zero. " + "and ensure the area enclosed is non-zero. " ) @@ -555,6 +555,9 @@ def __geo_interface__(self) -> GeoJsonShapeT: coordinates.append([p for p in self.points[ps:part]]) ps = part + # coordinates.append([tuple(p) for p in self.points[part:]]) + coordinates.append([p for p in self.points[part:]]) + return {"type": "MultiLineString", "coordinates": coordinates} elif self.shapeType in [POLYGON, POLYGONM, POLYGONZ]: if len(self.parts) == 0: From e365093ef5da30dca6d48a2464931488b5784149 Mon Sep 17 00:00:00 2001 From: James Parrott <80779630+JamesParrott@users.noreply.github.com> Date: Fri, 25 Jul 2025 15:41:29 +0100 Subject: [PATCH 08/17] Suppress Pylint undefined loop variable and consider raise from warnings --- .../workflows/run_checks_build_and_test.yml | 1 + src/shapefile.py | 31 +++++++++++++------ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/.github/workflows/run_checks_build_and_test.yml b/.github/workflows/run_checks_build_and_test.yml index 931a625..88ffe5a 100644 --- a/.github/workflows/run_checks_build_and_test.yml +++ b/.github/workflows/run_checks_build_and_test.yml @@ -27,6 +27,7 @@ jobs: pip install pytest pylint pylint-per-file-ignores pip install -e . - name: run Pylint for errors and warnings only, on test_shapefile.py + continue-on-error: true run: | pylint --disable=R,C test_shapefile.py src/shapefile.py diff --git a/src/shapefile.py b/src/shapefile.py index 367b0b5..3fd192b 100644 --- a/src/shapefile.py +++ b/src/shapefile.py @@ -556,8 +556,8 @@ def __geo_interface__(self) -> GeoJsonShapeT: ps = part # coordinates.append([tuple(p) for p in self.points[part:]]) - coordinates.append([p for p in self.points[part:]]) - + coordinates.append([p for p in self.points[part:]]) # pylint: disable=undefined-loop-variable + return {"type": "MultiLineString", "coordinates": coordinates} elif self.shapeType in [POLYGON, POLYGONM, POLYGONZ]: if len(self.parts) == 0: @@ -762,17 +762,19 @@ def __getattr__(self, item: str) -> RecordValue: and IndexError, if the field exists but the field's corresponding value in the Record does not exist """ + # pylint: disable=raise-missing-from try: 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(f"{item} is not a field name") + raise AttributeError(f"{item} is not a field name") except IndexError: raise IndexError( f"{item} found as a field but not enough values available." ) + # pylint: enable=raise-missing-from def __setattr__(self, key: str, value: RecordValue): """ @@ -788,7 +790,7 @@ def __setattr__(self, key: str, value: RecordValue): index = self.__field_positions[key] return list.__setitem__(self, index, value) except KeyError: - raise AttributeError(f"{key} is not a field name") + raise AttributeError(f"{key} is not a field name") # pylint: disable=raise-missing-from def __getitem__(self, item): """ @@ -827,7 +829,7 @@ def __setitem__(self, key, value): if index is not None: return list.__setitem__(self, index, value) else: - raise IndexError(f"{key} is not a field name and not an int") + raise IndexError(f"{key} is not a field name and not an int") # pylint: disable=raise-missing-from @property def oid(self) -> int: @@ -931,8 +933,6 @@ def __geo_interface__(self): class ShapefileException(Exception): """An exception to handle shapefile specific problems.""" - pass - class _NoShpSentinel(object): """For use as a default value for shp to preserve the @@ -941,9 +941,6 @@ class _NoShpSentinel(object): called Reader(shp=None) to load self.shx. """ - pass - - class Reader: """Reads the three files of a shapefile as a unit or separately. If one of the three files (.shp, .shx, @@ -1409,6 +1406,8 @@ def __shpHeader(self): def __shape(self, oid=None, bbox=None): """Returns the header info and geometry for a single shape.""" + + # pylint: disable=attribute-defined-outside-init f = self.__getFileObj(self.shp) record = Shape(oid=oid) nParts = nPoints = zmin = zmax = mmin = mmax = None @@ -1487,6 +1486,7 @@ def __shape(self, oid=None, bbox=None): record.m = [m] else: record.m = [None] + # pylint: enable=attribute-defined-outside-init # Seek to the end of this record as defined by the record header because # the shapefile spec doesn't require the actual content to meet the header # definition. Probably allowed for lazy feature deletion. @@ -2224,6 +2224,8 @@ def __shapefileHeader(self, fileObj, headerType="shp"): """Writes the specified header type to the specified file-like object. Several of the shapefile formats are so similar that a single generic method to read or write them is warranted.""" + + # pylint: disable=raise-missing-from f = self.__getFileObj(fileObj) f.seek(0) # File code, Unused bytes @@ -2281,6 +2283,8 @@ def __shapefileHeader(self, fileObj, headerType="shp"): raise ShapefileException( "Failed to write shapefile elevation and measure values. Floats required." ) + + # pylint: enable=raise-missing-from def __dbfHeader(self): """Writes the dbf header and field descriptors.""" @@ -2350,6 +2354,8 @@ def shape(self, s): self.__shxRecord(offset, length) def __shpRecord(self, s): + + # pylint: disable=raise-missing-from f = self.__getFileObj(self.shp) offset = f.tell() # Record number, Content length place holder @@ -2532,10 +2538,13 @@ def __shpRecord(self, s): f.seek(start - 4) f.write(pack(">i", length)) f.seek(finish) + # pylint: enable=raise-missing-from return offset, length def __shxRecord(self, offset, length): """Writes the shx records.""" + + # pylint: disable=raise-missing-from f = self.__getFileObj(self.shx) try: f.write(pack(">i", offset // 2)) @@ -2544,6 +2553,8 @@ def __shxRecord(self, offset, length): "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)) + + # pylint: enable=raise-missing-from def record(self, *recordList, **recordDict): """Creates a dbf attribute record. You can submit either a sequence of From 90c31c0acd96376d49ad49807f2aa7ef4c0904f7 Mon Sep 17 00:00:00 2001 From: James Parrott <80779630+JamesParrott@users.noreply.github.com> Date: Fri, 25 Jul 2025 15:53:59 +0100 Subject: [PATCH 09/17] Make some exceptions more specific, and unpack unassigned list comprehensions --- src/shapefile.py | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/shapefile.py b/src/shapefile.py index 3fd192b..5ff2ea4 100644 --- a/src/shapefile.py +++ b/src/shapefile.py @@ -326,7 +326,7 @@ def itercoords(): raise RingSamplingError( f"Unexpected error: Unable to find a ring sample point in: {coords}." "Ensure the ring's coordinates are oriented clockwise, " - "and ensure the area enclosed is non-zero. " + "and ensure the area enclosed is non-zero. " ) @@ -556,7 +556,7 @@ def __geo_interface__(self) -> GeoJsonShapeT: ps = part # coordinates.append([tuple(p) for p in self.points[part:]]) - coordinates.append([p for p in self.points[part:]]) # pylint: disable=undefined-loop-variable + coordinates.append([p for p in self.points[part:]]) # pylint: disable=undefined-loop-variable return {"type": "MultiLineString", "coordinates": coordinates} elif self.shapeType in [POLYGON, POLYGONM, POLYGONZ]: @@ -769,7 +769,7 @@ def __getattr__(self, item: str) -> RecordValue: index = self.__field_positions[item] return list.__getitem__(self, index) except KeyError: - raise AttributeError(f"{item} is not a field name") + raise AttributeError(f"{item} is not a field name") except IndexError: raise IndexError( f"{item} found as a field but not enough values available." @@ -790,7 +790,7 @@ def __setattr__(self, key: str, value: RecordValue): index = self.__field_positions[key] return list.__setitem__(self, index, value) except KeyError: - raise AttributeError(f"{key} is not a field name") # pylint: disable=raise-missing-from + raise AttributeError(f"{key} is not a field name") # pylint: disable=raise-missing-from def __getitem__(self, item): """ @@ -829,7 +829,7 @@ def __setitem__(self, key, value): if index is not None: return list.__setitem__(self, index, value) else: - raise IndexError(f"{key} is not a field name and not an int") # pylint: disable=raise-missing-from + raise IndexError(f"{key} is not a field name and not an int") # pylint: disable=raise-missing-from @property def oid(self) -> int: @@ -941,6 +941,7 @@ class _NoShpSentinel(object): called Reader(shp=None) to load self.shx. """ + class Reader: """Reads the three files of a shapefile as a unit or separately. If one of the three files (.shp, .shx, @@ -1406,7 +1407,7 @@ def __shpHeader(self): def __shape(self, oid=None, bbox=None): """Returns the header info and geometry for a single shape.""" - + # pylint: disable=attribute-defined-outside-init f = self.__getFileObj(self.shp) record = Shape(oid=oid) @@ -1996,7 +1997,7 @@ def __init__( if target: target = pathlike_obj(target) if not is_string(target): - raise Exception( + raise TypeError( f"The target filepath {target!r} must be of type str/unicode or path-like, not {type(target)}." ) self.shp = self.__getFileObj(os.path.splitext(target)[0] + ".shp") @@ -2010,7 +2011,7 @@ def __init__( if dbf: self.dbf = self.__getFileObj(dbf) else: - raise Exception( + raise TypeError( "Either the target filepath, or any of shp, shx, or dbf must be set to create a shapefile." ) # Initiate with empty headers, to be finalized upon closing @@ -2113,7 +2114,7 @@ def __getFileObj(self, f: Union[IO[bytes], str]) -> IO[bytes]: if hasattr(f, "write"): return f - raise Exception(f"Unsupported file-like: {f}") + raise ShapefileException(f"Unsupported file-like object: {f}") def __shpFileLength(self): """Calculates the file length of the shp file.""" @@ -2139,7 +2140,7 @@ def __bbox(self, s): # this should not happen. # any shape that is not null should have at least one point, and only those should be sent here. # could also mean that earlier code failed to add points to a non-null shape. - raise Exception( + raise ValueError( "Cannot create bbox. Expected a valid shape with at least one point. " f"Got a shape of type '{s.shapeType}' and 0 points." ) @@ -2224,7 +2225,7 @@ def __shapefileHeader(self, fileObj, headerType="shp"): """Writes the specified header type to the specified file-like object. Several of the shapefile formats are so similar that a single generic method to read or write them is warranted.""" - + # pylint: disable=raise-missing-from f = self.__getFileObj(fileObj) f.seek(0) @@ -2283,7 +2284,7 @@ def __shapefileHeader(self, fileObj, headerType="shp"): raise ShapefileException( "Failed to write shapefile elevation and measure values. Floats required." ) - + # pylint: enable=raise-missing-from def __dbfHeader(self): @@ -2343,10 +2344,10 @@ def shape(self, s): if isinstance(s, dict): s = Shape._from_geojson(s) else: - raise Exception( + raise TypeError( "Can only write Shape objects, GeoJSON dictionaries, " "or objects with the __geo_interface__, " - "not: %r" % s + f"not: {s}" ) # Write to file offset, length = self.__shpRecord(s) @@ -2354,7 +2355,6 @@ def shape(self, s): self.__shxRecord(offset, length) def __shpRecord(self, s): - # pylint: disable=raise-missing-from f = self.__getFileObj(self.shp) offset = f.tell() @@ -2366,7 +2366,7 @@ def __shpRecord(self, s): if self.shapeType is None and s.shapeType != NULL: self.shapeType = s.shapeType if s.shapeType != NULL and s.shapeType != self.shapeType: - raise Exception( + raise ShapefileException( f"The shape's type ({s.shapeType}) must match " f"the type of the shapefile ({self.shapeType})." ) @@ -2422,7 +2422,8 @@ def __shpRecord(self, s): f.write(pack(f"<{len(s.z)}d", *s.z)) else: # if z values are stored as 3rd dimension - [f.write(pack(" 2 else 0)) for p in s.points] + for p in s.points: + f.write(pack(" 2 else 0)) except error: raise ShapefileException( f"Failed to write elevation values for record {self.shpNum}. Expected floats." @@ -2452,7 +2453,7 @@ def __shpRecord(self, s): # 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 - [ + for p in s.points: f.write( pack( " 4294967294 bytes (4.29 GB). To fix this, break up your file into multiple smaller ones." ) f.write(pack(">i", length)) - + # pylint: enable=raise-missing-from def record(self, *recordList, **recordDict): From f791dae59a3ebd322c9e50ac4853a9937b13ddfe Mon Sep 17 00:00:00 2001 From: James Parrott <80779630+JamesParrott@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:00:33 +0100 Subject: [PATCH 10/17] Update changelog.txt --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index c15a414..48a534a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -11,7 +11,7 @@ Python 2 and Python 3.8 support dropped * Testing of wheels before publishing them * pyproject.toml src layout * Slow test marked. - + VERSION 2.4.0 From 3c58ae9064dff3280ecab61210bbd43ac9a43ec1 Mon Sep 17 00:00:00 2001 From: James Parrott <80779630+JamesParrott@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:00:58 +0100 Subject: [PATCH 11/17] Make except only catch specific exceptions --- src/shapefile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shapefile.py b/src/shapefile.py index 5ff2ea4..9b58788 100644 --- a/src/shapefile.py +++ b/src/shapefile.py @@ -979,7 +979,7 @@ def __init__( shp: Union[_NoShpSentinel, Optional[BinaryFileT]] = _NoShpSentinel(), shx: Optional[BinaryFileT] = None, dbf: Optional[BinaryFileT] = None, - **kwargs, + **kwargs, # pylint: disable=unused-argument ): self.shp = None self.shx = None @@ -1073,7 +1073,7 @@ def __init__( fileobj.seek(0) setattr(self, lower_ext, fileobj) self._files_to_close.append(fileobj) - except: + except (OSError, AttributeError): pass # Close and delete the temporary zipfile try: From 88ebe035ad49cfa265678c555d344a4f52910eb2 Mon Sep 17 00:00:00 2001 From: James Parrott <80779630+JamesParrott@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:05:48 +0100 Subject: [PATCH 12/17] Replace dbf Exception with ShapefileException --- src/shapefile.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/shapefile.py b/src/shapefile.py index 9b58788..4dd8201 100644 --- a/src/shapefile.py +++ b/src/shapefile.py @@ -979,7 +979,7 @@ def __init__( shp: Union[_NoShpSentinel, Optional[BinaryFileT]] = _NoShpSentinel(), shx: Optional[BinaryFileT] = None, dbf: Optional[BinaryFileT] = None, - **kwargs, # pylint: disable=unused-argument + **kwargs, # pylint: disable=unused-argument ): self.shp = None self.shx = None @@ -1078,7 +1078,7 @@ def __init__( # Close and delete the temporary zipfile try: zipfileobj.close() - except: + except Exception: pass # Try to load shapefile if self.shp or self.dbf: @@ -1886,7 +1886,9 @@ def iterRecords( if self.numRecords is None: self.__dbfHeader() if not isinstance(self.numRecords, int): - raise Exception("Error when reading number of Records in dbf file header") + raise ShapefileException( + "Error when reading number of Records in dbf file header" + ) f = self.__getFileObj(self.dbf) start = self.__restrictIndex(start) if stop is None: From be1f5d2b8e1afd1e94839043d073ecc7fab0cff3 Mon Sep 17 00:00:00 2001 From: James Parrott <80779630+JamesParrott@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:08:02 +0100 Subject: [PATCH 13/17] Suppress KeyErrors from opening Zip archives --- src/shapefile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shapefile.py b/src/shapefile.py index 4dd8201..aefaaec 100644 --- a/src/shapefile.py +++ b/src/shapefile.py @@ -1073,7 +1073,7 @@ def __init__( fileobj.seek(0) setattr(self, lower_ext, fileobj) self._files_to_close.append(fileobj) - except (OSError, AttributeError): + except (OSError, AttributeError, KeyError): pass # Close and delete the temporary zipfile try: From edbd735214616b7b14c42c14f01257171a42adda Mon Sep 17 00:00:00 2001 From: James Parrott <80779630+JamesParrott@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:21:11 +0100 Subject: [PATCH 14/17] Delete Py23DocChecker --- src/shapefile.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/shapefile.py b/src/shapefile.py index aefaaec..7173fed 100644 --- a/src/shapefile.py +++ b/src/shapefile.py @@ -1078,7 +1078,7 @@ def __init__( # Close and delete the temporary zipfile try: zipfileobj.close() - except Exception: + except: # pylint disable=broad-exception-caught pass # Try to load shapefile if self.shp or self.dbf: @@ -1988,7 +1988,7 @@ def __init__( shp=None, shx=None, dbf=None, - **kwargs, + **kwargs, # pylint: disable=unused-argument ): self.target = target self.autoBalance = autoBalance @@ -2976,15 +2976,7 @@ def _test(args: list[str] = sys.argv[1:], verbosity: bool = False) -> int: new_url = _replace_remote_url(old_url) example.source = example.source.replace(old_url, new_url) - class Py23DocChecker(doctest.OutputChecker): - def check_output(self, want, got, optionflags): - res = doctest.OutputChecker.check_output(self, want, got, optionflags) - return res - - def summarize(self): - doctest.OutputChecker.summarize(True) - - runner = doctest.DocTestRunner(checker=Py23DocChecker(), verbose=verbosity) + runner = doctest.DocTestRunner(verbose=verbosity) if verbosity == 0: print(f"Running {len(tests.examples)} doctests...") From 87d97eb8fb82df7df9d3c793edab73e8687e2c85 Mon Sep 17 00:00:00 2001 From: James Parrott <80779630+JamesParrott@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:27:28 +0100 Subject: [PATCH 15/17] Rename unpack helper --- src/shapefile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shapefile.py b/src/shapefile.py index 7173fed..88db493 100644 --- a/src/shapefile.py +++ b/src/shapefile.py @@ -1223,13 +1223,13 @@ def __len__(self): shpLength = shp.tell() shp.seek(100) # Do a fast shape iteration until end of file. - unpack = Struct(">2i").unpack + unpack_2_int32_be = Struct(">2i").unpack offsets = [] pos = shp.tell() while pos < shpLength: offsets.append(pos) # Unpack the shape header only - (recNum, recLength) = unpack(shp.read(8)) + (recNum, recLength) = unpack_2_int32_be(shp.read(8)) # Jump to next shape position pos += 8 + (2 * recLength) shp.seek(pos) From f8abdf2e5e1d6f02428a4ef42fbf7d8f2e45163e Mon Sep 17 00:00:00 2001 From: James Parrott <80779630+JamesParrott@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:40:42 +0100 Subject: [PATCH 16/17] Catch Type and ValueErrors only when forming date --- src/shapefile.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/shapefile.py b/src/shapefile.py index 88db493..9f9e143 100644 --- a/src/shapefile.py +++ b/src/shapefile.py @@ -119,6 +119,8 @@ class GeoJsonShapeT(TypedDict): MISSING = [None, ""] NODATA = -10e38 # as per the ESRI shapefile spec, only used for m-values. +unpack_2_int32_be = Struct(">2i").unpack + def b( v: Union[str, bytes], encoding: str = "utf-8", encodingErrors: str = "strict" @@ -1078,7 +1080,7 @@ def __init__( # Close and delete the temporary zipfile try: zipfileobj.close() - except: # pylint disable=broad-exception-caught + except: # pylint: disable=broad-exception-caught pass # Try to load shapefile if self.shp or self.dbf: @@ -1223,7 +1225,6 @@ def __len__(self): shpLength = shp.tell() shp.seek(100) # Do a fast shape iteration until end of file. - unpack_2_int32_be = Struct(">2i").unpack offsets = [] pos = shp.tell() while pos < shpLength: @@ -1414,7 +1415,7 @@ def __shape(self, oid=None, bbox=None): nParts = nPoints = zmin = zmax = mmin = mmax = None (recNum, recLength) = unpack(">2i", f.read(8)) # Determine the start of the next record - next = f.tell() + (2 * recLength) + next_shape = f.tell() + (2 * recLength) shapeType = unpack("= 16: + if next_shape - f.tell() >= 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: + if next_shape - f.tell() >= nPoints * 8: record.m = [] for m in _Array("d", unpack(f"<{nPoints}d", f.read(nPoints * 8))): if m > NODATA: @@ -1471,14 +1472,14 @@ def __shape(self, oid=None, bbox=None): point_bbox = list(record.points[0] + record.points[0]) # skip shape if no overlap with bounding box if not bbox_overlap(bbox, point_bbox): - f.seek(next) + f.seek(next_shape) return None # Read a single Z value if shapeType == 11: record.z = list(unpack("= 8: + if next_shape - f.tell() >= 8: (m,) = unpack("2i").unpack _i = 0 offset = shp.tell() while offset < shpLength: @@ -1557,7 +1557,7 @@ def shape(self, i=0, bbox=None): # Reached the requested index, exit loop with the offset value break # Unpack the shape header only - (recNum, recLength) = unpack(shp.read(8)) + (recNum, recLength) = unpack_2_int32_be(shp.read(8)) # Jump to next shape position offset += 8 + (2 * recLength) shp.seek(offset) @@ -1804,7 +1804,7 @@ def __record( # return as python date object y, m, d = int(value[:4]), int(value[4:6]), int(value[6:8]) value = date(y, m, d) - except: + except (TypeError, ValueError): # if invalid date, just return as unicode string so user can decide value = u(value.strip()) elif typ == "L": From 98077e5855048168758f43d6d709a2b4aee613b0 Mon Sep 17 00:00:00 2001 From: James Parrott <80779630+JamesParrott@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:54:24 +0100 Subject: [PATCH 17/17] Prefix names of unused variables with __ --- src/shapefile.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/shapefile.py b/src/shapefile.py index 9f9e143..b255dba 100644 --- a/src/shapefile.py +++ b/src/shapefile.py @@ -1080,7 +1080,7 @@ def __init__( # Close and delete the temporary zipfile try: zipfileobj.close() - except: # pylint: disable=broad-exception-caught + except: # pylint: disable=bare-except pass # Try to load shapefile if self.shp or self.dbf: @@ -1230,7 +1230,7 @@ def __len__(self): while pos < shpLength: offsets.append(pos) # Unpack the shape header only - (recNum, recLength) = unpack_2_int32_be(shp.read(8)) + (__recNum, recLength) = unpack_2_int32_be(shp.read(8)) # Jump to next shape position pos += 8 + (2 * recLength) shp.seek(pos) @@ -1266,7 +1266,7 @@ def load(self, shapefile=None): object. Normally this method would be called by the constructor with the file name as an argument.""" if shapefile: - (shapeName, ext) = os.path.splitext(shapefile) + (shapeName, __ext) = os.path.splitext(shapefile) self.shapeName = shapeName self.load_shp(shapeName) self.load_shx(shapeName) @@ -1386,6 +1386,8 @@ def __shpHeader(self): raise ShapefileException( "Shapefile Reader requires a shapefile or file-like object. (no shp file found" ) + + # pylint: disable=attribute-defined-outside-init shp = self.shp # File length (16-bit word * 2 = bytes) shp.seek(24) @@ -1406,14 +1408,17 @@ def __shpHeader(self): else: self.mbox.append(None) + # pylint: enable=attribute-defined-outside-init + def __shape(self, oid=None, bbox=None): """Returns the header info and geometry for a single shape.""" # pylint: disable=attribute-defined-outside-init f = self.__getFileObj(self.shp) record = Shape(oid=oid) - nParts = nPoints = zmin = zmax = mmin = mmax = None - (recNum, recLength) = unpack(">2i", f.read(8)) + # Formerly we also set __zmin = __zmax = __mmin = __mmax = None + nParts = nPoints = None + (__recNum, recLength) = unpack(">2i", f.read(8)) # Determine the start of the next record next_shape = f.tell() + (2 * recLength) shapeType = unpack("= 16: - (mmin, mmax) = unpack("<2d", f.read(16)) + __mmin, __mmax = unpack("<2d", f.read(16)) # Measure values less than -10e38 are nodata values according to the spec if next_shape - f.tell() >= nPoints * 8: record.m = [] @@ -1557,7 +1562,7 @@ def shape(self, i=0, bbox=None): # Reached the requested index, exit loop with the offset value break # Unpack the shape header only - (recNum, recLength) = unpack_2_int32_be(shp.read(8)) + (__recNum, recLength) = unpack_2_int32_be(shp.read(8)) # Jump to next shape position offset += 8 + (2 * recLength) shp.seek(offset) @@ -1625,6 +1630,8 @@ 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""" + + # pylint: disable=attribute-defined-outside-init if not self.dbf: raise ShapefileException( "Shapefile Reader requires a shapefile or file-like object. (no dbf file found)" @@ -1638,7 +1645,7 @@ def __dbfHeader(self): # read fields numFields = (self.__dbfHdrLength - 33) // 32 - for field in range(numFields): + for __field in range(numFields): fieldDesc = list(unpack("<11sc4xBB14x", dbf.read(32))) name = 0 idx = 0 @@ -1667,10 +1674,12 @@ def __dbfHeader(self): # by default, read all fields except the deletion flag, hence "[1:]" # note: recLookup gives the index position of a field inside a _Record list fieldnames = [f[0] for f in self.fields[1:]] - fieldTuples, recLookup, recStruct = self.__recordFields(fieldnames) + __fieldTuples, recLookup, recStruct = self.__recordFields(fieldnames) self.__fullRecStruct = recStruct self.__fullRecLookup = recLookup + # pylint: enable=attribute-defined-outside-init + def __recordFmt(self, fields=None): """Calculates the format and size of a .dbf record. Optional 'fields' arg specifies which fieldnames to unpack and which to ignore. Note that this @@ -1709,7 +1718,7 @@ def __recordFields(self, fields=None): # first ignore repeated field names (order doesn't matter) fields = list(set(fields)) # get the struct - fmt, fmtSize = self.__recordFmt(fields=fields) + fmt, __fmtSize = self.__recordFmt(fields=fields) recStruct = Struct(fmt) # make sure the given fieldnames exist for name in fields: @@ -1762,7 +1771,7 @@ def __record( # parse each value record = [] - for (name, typ, size, deci), value in zip(fieldTuples, recordContents): + for (__name, typ, __size, deci), value in zip(fieldTuples, recordContents): if typ in ("N", "F"): # numeric or float: number stored as a string, right justified, and padded with blanks to the width of the field. value = value.split(b"\0")[0] @@ -2980,7 +2989,7 @@ def _test(args: list[str] = sys.argv[1:], verbosity: bool = False) -> int: if verbosity == 0: print(f"Running {len(tests.examples)} doctests...") - failure_count, test_count = runner.run(tests) + failure_count, __test_count = runner.run(tests) # print results if verbosity: