22
33import contextlib
44import datetime
5+ import functools
56import io
67import itertools
78import os
@@ -66,6 +67,7 @@ def ignore_warnings(category=None):
6667)
6768
6869
70+
6971def coords_2D_list (
7072 min_size : int = 1 ,
7173 max_size : int | None = None ,
@@ -77,6 +79,7 @@ def coords_2D_list(
7779 )
7880
7981
82+
8083@pytest .mark .hypothesis
8184@given (expected = point_2D , i = integers (min_value = 1 ))
8285def test_Point_2D_roundtrips (
@@ -563,20 +566,90 @@ def test_shx_reader_writer_roundtrip(codes_and_shapes)-> None:
563566 "utf-32-le" ,
564567 "cp1140" ,
565568]
569+ POSSIBLY_NONINJECTIVE_CODECS = frozenset ({
570+ 'big5' , 'big5hkscs' , 'cp932' , 'cp949' , 'cp950' ,
571+ 'euc_jp' , 'euc_jis_2004' , 'euc_kr' , 'gb2312' , 'gbk' , 'gb18030' ,
572+ 'hz' , 'iso2022_jp' , 'iso2022_kr' , 'shift_jis' , 'shift_jis_2004'
573+ })
574+
575+ @functools .cache
576+ def _exclude_chars () -> dict [str ,list [str ]]:
577+
578+ exclude_chars = {} #"iso2022" : ["\x1b"]}
579+ """
580+ Not sure if bug in hypothesis generation, in core library, or just the way it is:
581+
582+ Python taking a short cut with Control characters in ISO_2022 stateful codecs
583+ Python 3.13.14 (main, Jul 18 2026, 17:02:37) [Clang 22.1.3 ] on linux
584+ Type "help", "copyright", "credits" or "license" for more information.
585+ >>> enc="iso2022_jp_1"
586+ >>> s="\x1b "
587+ >>> b=s.encode(enc)
588+ >>> b
589+ b'\x1b '
590+ >>> b.decode(enc)
591+ Traceback (most recent call last):
592+ File "<python-input-4>", line 1, in <module>
593+ b.decode(enc)
594+ ~~~~~~~~^^^^^
595+ UnicodeDecodeError: 'iso2022_jp_1' codec can't decode byte 0x1b in position 0: incomplete multibyte sequence
596+ decoding with 'iso2022_jp_1' codec failed
597+
598+ Non injective encodings (dealt with below)
599+ >>> enc="cp950"
600+ >>> "•".encode(enc)
601+ b'\xa1 E'
602+ >>> b=_
603+ >>> b.decode(enc)
604+ '‧'
605+ >>> s = _
606+ >>> s.encode(enc)
607+ b'\xa1 E'
608+
609+ """
610+
566611
567- def _encodings () -> set [str ]:
568612 from encodings .aliases import aliases
569- encs = set ()
570613 for enc in aliases .values ():
571- if enc in encs :
614+ if enc in exclude_chars :
572615 continue
616+ # I'm not sure why any encoding would fail on an empty string,
617+ # but I'd rather not have the tests get bogged by any that do exist.
573618 try :
574619 "" .encode (enc )
575620 except (UnicodeEncodeError , LookupError ):
576621 continue
577- encs .add (enc )
578- return encs
579- # assert _encodings() == {'utf_16_le', 'iso8859_7', 'cp437', 'iso2022_jp_3', 'shift_jis', 'cp775', 'cp1140',
622+
623+ exclude_chars [enc ] = []
624+
625+ if enc not in POSSIBLY_NONINJECTIVE_CODECS :
626+ continue
627+
628+ # find collisions of non-injective codecs
629+ # Iterate over BMP
630+ for code_point in range (0x10000 ):
631+ char = chr (code_point )
632+ try :
633+ b = char .encode (enc )
634+ except (UnicodeEncodeError , LookupError ):
635+ exclude_chars [enc ].append (char )
636+ continue
637+
638+ decoded = None
639+ try :
640+ decoded = b .decode (enc )
641+ except (UnicodeDecodeError , LookupError ):
642+ exclude_chars [enc ].append (char )
643+ continue
644+
645+ if decoded != char :
646+ exclude_chars [enc ].append (char )
647+
648+
649+
650+
651+ return exclude_chars
652+ # assert set(_encodings()) == {'utf_16_le', 'iso8859_7', 'cp437', 'iso2022_jp_3', 'shift_jis', 'cp775', 'cp1140',
580653# 'cp861', 'iso8859_11', 'iso8859_9', 'euc_jp', 'utf_16', 'cp950', 'mac_cyrillic', 'mac_turkish', 'iso2022_jp_1', 'iso8859_10',
581654# 'iso2022_jp_2004', 'cp866', 'mac_greek', 'hz', 'cp1257', 'cp037', 'cp863', 'iso8859_4', 'utf_16_be', 'gb18030', 'cp1250',
582655# 'cp850', 'iso8859_5', 'shift_jisx0213', 'iso8859_8', 'cp273', 'euc_jisx0213', 'cp932', 'cp862', 'tis_620', 'cp1125', 'koi8_r',
@@ -586,25 +659,29 @@ def _encodings() -> set[str]:
586659# 'iso2022_kr', 'cp1251', 'cp1255', 'mac_iceland', 'kz1048', 'iso8859_14', 'utf_32_be', 'ptcp154', 'iso8859_6', 'mac_roman',
587660# 'utf_32', 'iso2022_jp_2', 'iso8859_16', 'mbcs', 'cp500', 'iso8859_2', 'cp949', 'cp852', 'utf_7', 'big5hkscs', 'johab'}
588661
589- encodings = sampled_from (list (_encodings ())) # if IN_CI else ENCODINGS)
662+
663+ def strings_of_supported_code_points (encoding : str , min_size : int = 1 , max_size : int = 10 ):
664+
665+ return text (
666+ alphabet = characters (
667+ codec = encoding ,
668+ # https://en.wikipedia.org/wiki/Unicode_character_property#General_Category
669+ exclude_categories = ["Cs" , "Co" , "Cn" ], # Cs - surrogates
670+ exclude_characters = _exclude_chars ().get (encoding , []),
671+ ),
672+ min_size = min_size ,
673+ max_size = max_size ,
674+ )
675+
676+
677+ encodings = sampled_from (list (_exclude_chars ())) # if IN_CI else ENCODINGS)
590678
591679
592680@composite
593681def _dbf_fields_strategy (draw , encoding : str ) -> dict [str , str | int ]:
594682 field_type , bounds_dict = draw (sampled_from (list (DBF_FIELD_TYPES .items ())))
595683
596- name = draw (
597- text (
598- alphabet = characters (
599- codec = encoding ,
600- # https://en.wikipedia.org/wiki/Unicode_character_property#General_Category
601- exclude_categories = ["Cs" , "Co" , "Cn" ], # Cs - surrogates
602- # exclude_characters=[" "],
603- ),
604- min_size = 1 ,
605- max_size = 10 ,
606- )
607- )
684+ name = draw (strings_of_supported_code_points (encoding ))
608685
609686 max_length = bounds_dict .get ("max_length" , 254 )
610687 min_length = bounds_dict .get ("min_length" , 1 )
@@ -691,14 +768,10 @@ def test_dbf_Field_roundtrips(encoding_and_dbf_field: dict) -> None:
691768def date_to_str (d : datetime .date ) -> str :
692769 return d .strftime ("%Y%m%d" ).zfill (8 )
693770
694- def record_value_for_field (name : str , field_type : str , size : int , decimal : int , encoding : str ):
771+ def record_value_strat_for_field (name : str , field_type : str , size : int , decimal : int , encoding : str ):
695772
696773 if field_type == "C" :
697- return text (
698- alphabet = ascii_printable ,
699- min_size = 0 ,
700- max_size = size ,
701- )
774+ return strings_of_supported_code_points (encoding , 0 , size )
702775 if field_type in {"N" , "F" }:
703776
704777 int_digits = size if decimal == 0 else size - decimal - 1
@@ -732,7 +805,7 @@ def _dbf_encoding_fields_and_record_strategy(
732805
733806 fields = draw (lists (_dbf_fields_strategy (encoding ), min_size = 1 , max_size = max_fields ))
734807
735- record_strategy = tuples (* (record_value_for_field (encoding = encoding , ** field ) for field in fields ))
808+ record_strategy = tuples (* (record_value_strat_for_field (encoding = encoding , ** field ) for field in fields ))
736809
737810 return encoding , fields , record_strategy
738811
0 commit comments