@@ -3599,24 +3599,23 @@ def __init__(
35993599 shapefile_path : str | PathLike [Any ] = "" ,
36003600 / ,
36013601 * ,
3602- encoding : str = "utf-8" , # | None = None,
3602+ encoding : str | None = None ,
36033603 encodingErrors : str = "strict" ,
36043604 shp : _NoShpSentinel | BinaryFileT | None = _NoShpSentinel (),
36053605 shx : BinaryFileT | None = None ,
36063606 dbf : BinaryFileT | None = None ,
3607- # cpg: BinaryFileT | None = None,
3607+ cpg : BinaryFileT | None = None ,
36083608 # Keep kwargs even though unused, to preserve PyShp 2.4 API
36093609 ** kwargs : Any ,
36103610 ):
36113611 super ().__init__ ()
36123612 # Store encoding info to use if lazy loading DbfReader later.
3613- # encoding_from_cpg =
3614- self .encoding = encoding # encoding_from_cpg if encoding is None else encoding
3613+ self ._user_specified_encoding = encoding
36153614 self .encodingErrors = encodingErrors
36163615 self ._shp = None
36173616 self ._shx = None
36183617 self ._dbf = None
3619- # self._cpg = None
3618+ self ._cpg = None
36203619 self .shapeName = "Not specified"
36213620 self .numShapes : int = 0
36223621 self .path : str | os .PathLike [Any ] | None = None
@@ -3635,15 +3634,15 @@ def __init__(
36353634 discarded_kwargs ["shx" ] = shx
36363635 if dbf is not None :
36373636 discarded_kwargs ["dbf" ] = dbf
3638- # if cpg is not None:
3639- # discarded_kwargs["cpg"] = cpg
3637+ if cpg is not None :
3638+ discarded_kwargs ["cpg" ] = cpg
36403639 if discarded_kwargs :
36413640 raise TypeError (
36423641 "Please be specific about the shapefile you want to load. "
36433642 f"Got: { shapefile_path } , plus the following unusable "
36443643 " kwargs: {discarded_kwargs} which previous versions of PyShp ignored. \n "
36453644 "Only either: i) exactly one positional arg \n "
3646- " or: ii) one or both of shp and dbf, optionally plus shx \n " # and cpg kwargs\n"
3645+ " or: ii) one or both of shp and dbf, optionally plus shx and cpg kwargs\n "
36473646 "is currently supported. All other kwargs may be set (or not). "
36483647 )
36493648 self .path = shapefile_path
@@ -3670,6 +3669,7 @@ def __init__(
36703669 zipfileobj = self ._download_binary_file_from_url (
36713670 url_info ,
36723671 ".zip" ,
3672+ add_to_exit_stack = False ,
36733673 suppress_http_errors = False ,
36743674 )
36753675 if zipfileobj is None :
@@ -3708,12 +3708,14 @@ def __init__(
37083708 self ._shp = self ._seek_0_on_file_obj_wrap_or_open_from_name (".shp" , shp )
37093709 self ._shx = self ._seek_0_on_file_obj_wrap_or_open_from_name (".shx" , shx )
37103710
3711+ self ._cpg = self ._seek_0_on_file_obj_wrap_or_open_from_name (".cpg" , cpg )
37113712 self ._dbf = self ._seek_0_on_file_obj_wrap_or_open_from_name (".dbf" , dbf )
37123713
37133714 # Load the files
37143715 if self ._shp :
37153716 self ._get_shp_reader ()
37163717 if self ._dbf :
3718+ # Sets self.encoding
37173719 self ._get_dbf_reader ()
37183720 if self ._shx :
37193721 self ._get_shx_reader ()
@@ -3747,12 +3749,37 @@ def _get_dbf_reader(self) -> DbfReader:
37473749 raise ShapefileException (
37483750 "DbfReader requires a .dbf file or file-like object."
37493751 )
3752+ self ._set_encoding ()
37503753 return DbfReader (
37513754 dbf = self ._dbf ,
37523755 encoding = self .encoding ,
37533756 encodingErrors = self .encodingErrors ,
37543757 )
37553758
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+
37563783 @property
37573784 def shp_reader (self ) -> ShpReader :
37583785 return self ._get_shp_reader ()
@@ -3843,7 +3870,7 @@ def iterRecords(
38433870 def _try_get_open_constituent_file (
38443871 self ,
38453872 file : Path ,
3846- ext : Literal [".shp" , ".shx" , ".dbf" ],
3873+ ext : Literal [".shp" , ".shx" , ".dbf" , ".cpg" ],
38473874 ) -> IO [bytes ] | None :
38483875 """
38493876 Attempts to open a .shp, .dbf or .shx file,
@@ -3863,15 +3890,18 @@ def _try_get_open_constituent_file(
38633890
38643891 def _seek_0_on_file_obj_wrap_or_open_from_name (
38653892 self ,
3866- ext : Literal [".shp" , ".shx" , ".dbf" ],
3893+ ext : Literal [".shp" , ".shx" , ".dbf" , ".cpg" ],
38673894 file : BinaryFileT | None ,
38683895 ) -> None | IO [bytes ]:
38693896 if file is None :
38703897 return None
38713898
38723899 if isinstance (file , (str , PathLike )):
3900+ # Added to exit stack if opened.
38733901 return self ._try_get_open_constituent_file (Path (file ), ext )
38743902
3903+ # Other user-opened file objects not added to exit stack.
3904+ # The user must close them.
38753905 if hasattr (file , "read" ):
38763906 # Copy if required
38773907 try :
@@ -3888,7 +3918,8 @@ def _seek_0_on_file_obj_wrap_or_open_from_name(
38883918 def _download_binary_file_from_url (
38893919 self ,
38903920 urlinfo : SplitResult ,
3891- ext : Literal [".shp" , ".shx" , ".dbf" , ".zip" ],
3921+ ext : Literal [".shp" , ".shx" , ".dbf" , ".cpg" , ".zip" ],
3922+ add_to_exit_stack : bool = True ,
38923923 suppress_http_errors : bool = True ,
38933924 ) -> tempfile ._TemporaryFileWrapper [bytes ] | None :
38943925 sniffed_bytes , resp = _try_to_download_binary_file (
@@ -3900,26 +3931,19 @@ def _download_binary_file_from_url(
39003931 return None
39013932 # Use tempfile as source for url data.
39023933 fileobj = _save_to_named_tmp_file (resp , initial_bytes = sniffed_bytes )
3934+ if add_to_exit_stack :
3935+ self .exit_stack .enter_context (fileobj )
39033936 return fileobj
39043937
39053938 def _load_from_url (self , urlinfo : SplitResult ) -> None :
39063939 # Shapefile is from a url
39073940 # Download each file to temporary path and treat as normal shapefile path
39083941 self ._shp = self ._download_binary_file_from_url (urlinfo , ".shp" )
39093942 self ._shx = self ._download_binary_file_from_url (urlinfo , ".shx" )
3943+ self ._cpg = self ._download_binary_file_from_url (urlinfo , ".cpg" )
39103944 self ._dbf = self ._download_binary_file_from_url (urlinfo , ".dbf" )
39113945
3912- shp_or_dbf_loaded = False
3913- if self ._shx is not None :
3914- self .exit_stack .enter_context (self ._shx )
3915- if self ._shp is not None :
3916- self .exit_stack .enter_context (self ._shp )
3917- shp_or_dbf_loaded = True
3918- if self ._dbf is not None :
3919- self .exit_stack .enter_context (self ._dbf )
3920- shp_or_dbf_loaded = True
3921-
3922- if not shp_or_dbf_loaded :
3946+ if self ._shp is None and self ._dbf is None :
39233947 raise ShapefileException (
39243948 f"Failed to download .shp or .dbf from: { urlunsplit (urlinfo )} "
39253949 )
@@ -3928,7 +3952,7 @@ def _load_file_from_zip_to_tmp_file(
39283952 self ,
39293953 archive : zipfile .ZipFile ,
39303954 file : Path ,
3931- ext : Literal [".shp" , ".shx" , ".dbf" ],
3955+ ext : Literal [".shp" , ".shx" , ".dbf" , ".cpg" ],
39323956 ) -> tempfile ._TemporaryFileWrapper [bytes ] | None :
39333957 for cased_ext in {ext .lower (), ext .upper (), ext }:
39343958 try :
@@ -3982,7 +4006,7 @@ def _load_from_zipfileobj(
39824006 constituent_files = (
39834007 Path (name )
39844008 for name in archive .namelist ()
3985- if name .lower ().endswith ((".shp" , ".dbf" , ".shx" ))
4009+ if name .lower ().endswith ((".shp" , ".dbf" , ".shx" , ".cpg" ))
39864010 )
39874011
39884012 def without_ext (path : Path ) -> Path :
@@ -4005,6 +4029,7 @@ def without_ext(path: Path) -> Path:
40054029 # Try to extract file-like objects from zipfile
40064030 self ._shp = self ._load_file_from_zip_to_tmp_file (archive , shapefile , ".shp" )
40074031 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" )
40084033 self ._dbf = self ._load_file_from_zip_to_tmp_file (archive , shapefile , ".dbf" )
40094034
40104035 def load (self , file : str | os .PathLike [Any ]) -> None :
@@ -4046,6 +4071,7 @@ def load_dbf(self, file: Path) -> None:
40464071 """
40474072 self ._dbf = self ._try_get_open_constituent_file (file , ".dbf" )
40484073 if self ._dbf is not None :
4074+ self ._cpg = self ._try_get_open_constituent_file (file , ".cpg" )
40494075 self ._get_dbf_reader ()
40504076
40514077 def __len__ (self ) -> int :
0 commit comments