88
99from __future__ import annotations
1010
11- __version__ = "3.1.6.dev "
11+ __version__ = "3.1.6"
1212
1313import abc
1414import array
@@ -2819,25 +2819,6 @@ def _try_to_download_binary_file(
28192819 return initial_bytes , cast (ReadableBinStream , resp )
28202820
28212821
2822- def _try_get_open_constituent_file (
2823- file : Path ,
2824- ext : Literal [".shp" , ".shx" , ".dbf" ],
2825- ) -> IO [bytes ] | None :
2826- """
2827- Attempts to open a .shp, .dbf or .shx file,
2828- with both lower case and upper case file extensions,
2829- and return it. If it was not possible to open the file, None is returned.
2830- """
2831- exts = {ext , ext .upper (), ext .lower ()}
2832-
2833- for candidate_ext in exts :
2834- try :
2835- return file .with_suffix (candidate_ext ).open ("rb" )
2836- except OSError :
2837- pass
2838- return None
2839-
2840-
28412822def ensure_within_bounds (i : int , num_records : int ) -> int :
28422823 """Provides list-like handling of a record index with a clearer
28432824 error message if the index is out of bounds."""
@@ -3359,7 +3340,7 @@ def shape_lengths_B(self) -> list[int]:
33593340
33603341
33613342class ShpReader (_HasCheckedReadableFile ):
3362- """Reads an shp file."""
3343+ """Reads a . shp file."""
33633344
33643345 FileProto = ReadSeekableBinStream
33653346 new_file_obj_mode = "rb"
@@ -3618,21 +3599,23 @@ def __init__(
36183599 shapefile_path : str | PathLike [Any ] = "" ,
36193600 / ,
36203601 * ,
3621- encoding : str = "utf-8" ,
3602+ encoding : str | None = None ,
36223603 encodingErrors : str = "strict" ,
36233604 shp : _NoShpSentinel | BinaryFileT | None = _NoShpSentinel (),
36243605 shx : BinaryFileT | None = None ,
36253606 dbf : BinaryFileT | None = None ,
3607+ cpg : BinaryFileT | None = None ,
36263608 # Keep kwargs even though unused, to preserve PyShp 2.4 API
36273609 ** kwargs : Any ,
36283610 ):
36293611 super ().__init__ ()
36303612 # Store encoding info to use if lazy loading DbfReader later.
3631- self .encoding = encoding
3613+ self ._user_specified_encoding = encoding
36323614 self .encodingErrors = encodingErrors
36333615 self ._shp = None
36343616 self ._shx = None
36353617 self ._dbf = None
3618+ self ._cpg = None
36363619 self .shapeName = "Not specified"
36373620 self .numShapes : int = 0
36383621 self .path : str | os .PathLike [Any ] | None = None
@@ -3651,13 +3634,15 @@ def __init__(
36513634 discarded_kwargs ["shx" ] = shx
36523635 if dbf is not None :
36533636 discarded_kwargs ["dbf" ] = dbf
3637+ if cpg is not None :
3638+ discarded_kwargs ["cpg" ] = cpg
36543639 if discarded_kwargs :
36553640 raise TypeError (
36563641 "Please be specific about the shapefile you want to load. "
36573642 f"Got: { shapefile_path } , plus the following unusable "
36583643 " kwargs: {discarded_kwargs} which previous versions of PyShp ignored. \n "
36593644 "Only either: i) exactly one positional arg \n "
3660- " or: ii) one or both of shp and dbf, optionally plus shx kwargs\n "
3645+ " or: ii) one or both of shp and dbf, optionally plus shx and cpg kwargs\n "
36613646 "is currently supported. All other kwargs may be set (or not). "
36623647 )
36633648 self .path = shapefile_path
@@ -3684,6 +3669,7 @@ def __init__(
36843669 zipfileobj = self ._download_binary_file_from_url (
36853670 url_info ,
36863671 ".zip" ,
3672+ add_to_exit_stack = False ,
36873673 suppress_http_errors = False ,
36883674 )
36893675 if zipfileobj is None :
@@ -3722,12 +3708,14 @@ def __init__(
37223708 self ._shp = self ._seek_0_on_file_obj_wrap_or_open_from_name (".shp" , shp )
37233709 self ._shx = self ._seek_0_on_file_obj_wrap_or_open_from_name (".shx" , shx )
37243710
3711+ self ._cpg = self ._seek_0_on_file_obj_wrap_or_open_from_name (".cpg" , cpg )
37253712 self ._dbf = self ._seek_0_on_file_obj_wrap_or_open_from_name (".dbf" , dbf )
37263713
37273714 # Load the files
37283715 if self ._shp :
37293716 self ._get_shp_reader ()
37303717 if self ._dbf :
3718+ # Sets self.encoding
37313719 self ._get_dbf_reader ()
37323720 if self ._shx :
37333721 self ._get_shx_reader ()
@@ -3761,12 +3749,37 @@ def _get_dbf_reader(self) -> DbfReader:
37613749 raise ShapefileException (
37623750 "DbfReader requires a .dbf file or file-like object."
37633751 )
3752+ self ._set_encoding ()
37643753 return DbfReader (
37653754 dbf = self ._dbf ,
37663755 encoding = self .encoding ,
37673756 encodingErrors = self .encodingErrors ,
37683757 )
37693758
3759+ @functools .cache
3760+ def _set_encoding (self ) -> None :
3761+ if self ._cpg is None :
3762+ encoding_from_cpg = ""
3763+ else :
3764+ encoding_from_cpg = (
3765+ self ._cpg .read ().decode ().lower ().replace ("-" , "_" ).strip ()
3766+ )
3767+ if not encoding_from_cpg :
3768+ warnings .warn ("Empty .cpg file (no encoding found). " )
3769+
3770+ if self ._user_specified_encoding is None :
3771+ encoding = encoding_from_cpg
3772+ else :
3773+ encoding = self ._user_specified_encoding .lower ().replace ("-" , "_" ).strip ()
3774+
3775+ if encoding_from_cpg and encoding != encoding_from_cpg :
3776+ warnings .warn (
3777+ f"Specified encoding: { encoding } "
3778+ "different to encoding read from "
3779+ f".cpg file: { encoding_from_cpg } " ,
3780+ )
3781+ self .encoding = encoding or "utf-8"
3782+
37703783 @property
37713784 def shp_reader (self ) -> ShpReader :
37723785 return self ._get_shp_reader ()
@@ -3854,20 +3867,41 @@ def iterRecords(
38543867 ) -> Iterator [_Record | None ]:
38553868 return self .dbf_reader .iterRecords (fields , start , stop , deleted_as_None )
38563869
3870+ def _try_get_open_constituent_file (
3871+ self ,
3872+ file : Path ,
3873+ ext : Literal [".shp" , ".shx" , ".dbf" , ".cpg" ],
3874+ ) -> IO [bytes ] | None :
3875+ """
3876+ Attempts to open a .shp, .dbf or .shx file,
3877+ with both lower case and upper case file extensions,
3878+ and return it. If it was not possible to open the file, None is returned.
3879+ """
3880+ exts = {ext , ext .upper (), ext .lower ()}
3881+
3882+ for candidate_ext in exts :
3883+ try :
3884+ file_obj = file .with_suffix (candidate_ext ).open ("rb" )
3885+ except OSError :
3886+ continue
3887+ self .exit_stack .enter_context (file_obj )
3888+ return file_obj
3889+ return None
3890+
38573891 def _seek_0_on_file_obj_wrap_or_open_from_name (
38583892 self ,
3859- ext : Literal [".shp" , ".shx" , ".dbf" ],
3893+ ext : Literal [".shp" , ".shx" , ".dbf" , ".cpg" ],
38603894 file : BinaryFileT | None ,
38613895 ) -> None | IO [bytes ]:
38623896 if file is None :
38633897 return None
38643898
38653899 if isinstance (file , (str , PathLike )):
3866- file_obj = _try_get_open_constituent_file (Path (file ), ext )
3867- if file_obj is not None :
3868- self .exit_stack .enter_context (file_obj )
3869- return file_obj
3900+ # Added to exit stack if opened.
3901+ return self ._try_get_open_constituent_file (Path (file ), ext )
38703902
3903+ # Other user-opened file objects not added to exit stack.
3904+ # The user must close them.
38713905 if hasattr (file , "read" ):
38723906 # Copy if required
38733907 try :
@@ -3884,7 +3918,8 @@ def _seek_0_on_file_obj_wrap_or_open_from_name(
38843918 def _download_binary_file_from_url (
38853919 self ,
38863920 urlinfo : SplitResult ,
3887- ext : Literal [".shp" , ".shx" , ".dbf" , ".zip" ],
3921+ ext : Literal [".shp" , ".shx" , ".dbf" , ".cpg" , ".zip" ],
3922+ add_to_exit_stack : bool = True ,
38883923 suppress_http_errors : bool = True ,
38893924 ) -> tempfile ._TemporaryFileWrapper [bytes ] | None :
38903925 sniffed_bytes , resp = _try_to_download_binary_file (
@@ -3896,26 +3931,19 @@ def _download_binary_file_from_url(
38963931 return None
38973932 # Use tempfile as source for url data.
38983933 fileobj = _save_to_named_tmp_file (resp , initial_bytes = sniffed_bytes )
3934+ if add_to_exit_stack :
3935+ self .exit_stack .enter_context (fileobj )
38993936 return fileobj
39003937
39013938 def _load_from_url (self , urlinfo : SplitResult ) -> None :
39023939 # Shapefile is from a url
39033940 # Download each file to temporary path and treat as normal shapefile path
39043941 self ._shp = self ._download_binary_file_from_url (urlinfo , ".shp" )
39053942 self ._shx = self ._download_binary_file_from_url (urlinfo , ".shx" )
3943+ self ._cpg = self ._download_binary_file_from_url (urlinfo , ".cpg" )
39063944 self ._dbf = self ._download_binary_file_from_url (urlinfo , ".dbf" )
39073945
3908- shp_or_dbf_loaded = False
3909- if self ._shx is not None :
3910- self .exit_stack .enter_context (self ._shx )
3911- if self ._shp is not None :
3912- self .exit_stack .enter_context (self ._shp )
3913- shp_or_dbf_loaded = True
3914- if self ._dbf is not None :
3915- self .exit_stack .enter_context (self ._dbf )
3916- shp_or_dbf_loaded = True
3917-
3918- if not shp_or_dbf_loaded :
3946+ if self ._shp is None and self ._dbf is None :
39193947 raise ShapefileException (
39203948 f"Failed to download .shp or .dbf from: { urlunsplit (urlinfo )} "
39213949 )
@@ -3924,7 +3952,7 @@ def _load_file_from_zip_to_tmp_file(
39243952 self ,
39253953 archive : zipfile .ZipFile ,
39263954 file : Path ,
3927- ext : Literal [".shp" , ".shx" , ".dbf" ],
3955+ ext : Literal [".shp" , ".shx" , ".dbf" , ".cpg" ],
39283956 ) -> tempfile ._TemporaryFileWrapper [bytes ] | None :
39293957 for cased_ext in {ext .lower (), ext .upper (), ext }:
39303958 try :
@@ -3978,7 +4006,7 @@ def _load_from_zipfileobj(
39784006 constituent_files = (
39794007 Path (name )
39804008 for name in archive .namelist ()
3981- if name .lower ().endswith ((".shp" , ".dbf" , ".shx" ))
4009+ if name .lower ().endswith ((".shp" , ".dbf" , ".shx" , ".cpg" ))
39824010 )
39834011
39844012 def without_ext (path : Path ) -> Path :
@@ -4001,6 +4029,7 @@ def without_ext(path: Path) -> Path:
40014029 # Try to extract file-like objects from zipfile
40024030 self ._shp = self ._load_file_from_zip_to_tmp_file (archive , shapefile , ".shp" )
40034031 self ._shx = self ._load_file_from_zip_to_tmp_file (archive , shapefile , ".shx" )
4032+ self ._cpg = self ._load_file_from_zip_to_tmp_file (archive , shapefile , ".cpg" )
40044033 self ._dbf = self ._load_file_from_zip_to_tmp_file (archive , shapefile , ".dbf" )
40054034
40064035 def load (self , file : str | os .PathLike [Any ]) -> None :
@@ -4024,27 +4053,25 @@ def load_shp(self, file: Path) -> None:
40244053 """
40254054 Attempts to load file with .shp extension as both lower and upper case
40264055 """
4027- self ._shp = _try_get_open_constituent_file (file , ".shp" )
4028- if self ._shp :
4029- self .exit_stack .enter_context (self ._shp )
4056+ self ._shp = self ._try_get_open_constituent_file (file , ".shp" )
4057+ if self ._shp is not None :
40304058 self ._get_shp_reader ()
40314059
40324060 def load_shx (self , file : Path ) -> None :
40334061 """
40344062 Attempts to load file with .shx extension as both lower and upper case
40354063 """
4036- self ._shx = _try_get_open_constituent_file (file , ".shx" )
4037- if self ._shx :
4038- self .exit_stack .enter_context (self ._shx )
4064+ self ._shx = self ._try_get_open_constituent_file (file , ".shx" )
4065+ if self ._shx is not None :
40394066 self ._get_shx_reader ()
40404067
40414068 def load_dbf (self , file : Path ) -> None :
40424069 """
40434070 Attempts to load file with .dbf extension as both lower and upper case
40444071 """
4045- self ._dbf = _try_get_open_constituent_file (file , ".dbf" )
4046- if self ._dbf :
4047- self .exit_stack . enter_context ( self . _dbf )
4072+ self ._dbf = self . _try_get_open_constituent_file (file , ".dbf" )
4073+ if self ._dbf is not None :
4074+ self ._cpg = self . _try_get_open_constituent_file ( file , ".cpg" )
40484075 self ._get_dbf_reader ()
40494076
40504077 def __len__ (self ) -> int :
0 commit comments