5757}
5858
5959__codecs_registry = []
60+ # caches depending on the state of the registry (cleared whenever it changes)
61+ __guess_lists = {}
62+ __names_table = []
63+ __lookup_cache = {}
6064
6165MACROS = {}
6266PERS_MACROS = {}
@@ -238,15 +242,22 @@ def add(ename, encode=None, decode=None, pattern=None, text=True, add_to_codecs=
238242 for exc in kwargs .get ('extra_exceptions' , []):
239243 _set_exc (exc ) # create additional custom exceptions as builtins
240244 glob = currentframe ().f_back .f_globals
245+ # the pattern is compiled once, as it is matched against every encoding name looked up
246+ rx = re .compile (pattern ) if pattern else None
247+ # category of the codec, from the folder of the module adding it
248+ cat = glob .get ('__file__' , os .path .join ("custom" , "_" )).split (os .path .sep )[- 2 ].rstrip ("s" )
249+ if cat not in CODECS_CATEGORIES :
250+ CODECS_CATEGORIES .append (cat )
251+ category = kwargs .get ('category' , cat )
241252 # search function for the new encoding
242253 @_with_repr (ename )
243254 def getregentry (encoding ):
244- if encoding != ename and not (pattern and re .match (pattern , encoding )):
255+ if encoding != ename and not (rx and rx .match (encoding )):
245256 return
246257 fenc , fdec , name = encode , decode , encoding
247258 # prepare CodecInfo input arguments
248259 if pattern :
249- m , args , i = re .match (pattern , encoding ), [], 1
260+ m , args , i = rx .match (encoding ), [], 1
250261 try :
251262 while True :
252263 try :
@@ -329,11 +340,7 @@ class StreamReader(Codec, codecs.StreamReader):
329340 ci .parameters ['add_to_codecs' ] = add_to_codecs
330341 ci .parameters ['pattern' ] = pattern
331342 ci .parameters ['text' ] = text
332- f = glob .get ('__file__' , os .path .join ("custom" , "_" ))
333- cat = f .split (os .path .sep )[- 2 ].rstrip ("s" )
334- if cat not in CODECS_CATEGORIES :
335- CODECS_CATEGORIES .append (cat )
336- ci .parameters ['category' ] = kwargs .get ('category' , cat )
343+ ci .parameters ['category' ] = category
337344 ci .parameters ['examples' ] = kwargs .get ('examples' , glob .get ('__examples__' ))
338345 ci .parameters ['guess' ] = kwargs .get ('guess' , glob .get ('__guess__' , [ename ])) or []
339346 ci .parameters ['module' ] = kwargs .get ('module' , glob .get ('__name__' ))
@@ -349,6 +356,7 @@ class StreamReader(Codec, codecs.StreamReader):
349356 if kwargs .get ('aliases' ):
350357 getregentry .__aliases__ = list (map (lambda n : re .sub (r"[\s\-]" , "_" , n ), kwargs ['aliases' ]))
351358 getregentry .__pattern__ = pattern
359+ getregentry .__category__ = category
352360 register (getregentry , add_to_codecs )
353361 return getregentry
354362
@@ -623,6 +631,7 @@ def clear():
623631 """ Clear codext's local registry of search functions. """
624632 global __codecs_registry , MACROS , PERS_MACROS # noqa: F824
625633 __codecs_registry , MACROS , PERS_MACROS = [], {}, {}
634+ __registry_changed ()
626635codecs .clear = clear
627636
628637
@@ -705,9 +714,11 @@ def list_encodings(*categories):
705714 enc .append (ci .name )
706715 for search_function in CODECS_OVERWRITTEN + __codecs_registry :
707716 name = search_function .__name__ .replace ("_" , "-" )
708- p = search_function .__pattern__
709- ci = search_function (name ) if p is None else search_function (generate_string_from_regex (p ))
710- c = "other" if ci is None else ci .parameters ['category' ]
717+ c = getattr (search_function , "__category__" , None )
718+ if c is None :
719+ p = getattr (search_function , "__pattern__" , None )
720+ ci = search_function (name ) if p is None else search_function (_sample_name (search_function ))
721+ c = "other" if ci is None else ci .parameters ['category' ]
711722 if (len (categories ) == 0 or c in categories ) and c not in exclude :
712723 enc .append (name )
713724 for category in categories :
@@ -731,6 +742,7 @@ def remove(name):
731742 tbr .append (search_function )
732743 for search_function in tbr :
733744 __codecs_registry .remove (search_function )
745+ __registry_changed ()
734746 try :
735747 del MACROS [name ]
736748 except KeyError :
@@ -769,6 +781,7 @@ def reset():
769781 # restore codext's registry
770782 else :
771783 __codecs_registry = CODECS_REGISTRY [:]
784+ __registry_changed ()
772785 # restore codext's embedded set of macros
773786 with open (os .path .join (os .path .dirname (__file__ ), "macros.json" )) as f :
774787 MACROS = json .load (f )
@@ -897,12 +910,15 @@ def decode(obj, encoding='utf-8', errors='strict'):
897910codecs .decode = decode
898911
899912
913+ _REPEAT = re .compile (r"\[(\d+)\]$" )
914+
915+
900916def encode (obj , encoding = 'utf-8' , errors = 'strict' ):
901917 """ Custom encode function relying on the hooked lookup function. """
902- n , m = 1 , re .search (r"\[(\d+)\]$" , encoding )
918+ n , m = 1 , _REPEAT .search (encoding )
903919 if m :
904920 n = int (m .group (1 ))
905- encoding = re . sub ( r"\[(\d+)\]$" , "" , encoding )
921+ encoding = encoding [: m . start ()]
906922 ci = lookup (encoding )
907923 for i in range (n ):
908924 try :
@@ -919,6 +935,26 @@ def encode(obj, encoding='utf-8', errors='strict'):
919935
920936def lookup (encoding , macro = True ):
921937 """ Hooked lookup function for searching first for codecs in the local registry of this module. """
938+ # the results are cached per encoding name, as searching through the registry is costly ; a macro is only returned
939+ # when allowed, and failures are not cached (a codec or a macro may be added afterwards)
940+ try :
941+ ci = __lookup_cache [encoding ]
942+ if macro or not isinstance (ci , CodecMacro ):
943+ return ci
944+ except (KeyError , TypeError ):
945+ pass
946+ ci = __lookup (encoding , macro )
947+ try :
948+ __lookup_cache [encoding ] = ci
949+ except TypeError :
950+ pass
951+ return ci
952+ codecs .lookup = lookup
953+
954+
955+ def __lookup (encoding , macro = True ):
956+ """ Search for the given encoding, first in the local registry of this module, then with the original lookup
957+ function and finally among the macros. """
922958 # first, try to match the given encoding with codecs' search functions
923959 for search_function in CODECS_OVERWRITTEN + __codecs_registry :
924960 codecinfo = search_function (encoding )
@@ -928,7 +964,7 @@ def lookup(encoding, macro=True):
928964 for search_function in CODECS_OVERWRITTEN + __codecs_registry :
929965 if search_function .__name__ .replace ("_" , "-" ) == encoding or \
930966 encoding in getattr (search_function , "__aliases__" , []):
931- codecinfo = search_function (generate_string_from_regex (search_function . __pattern__ ))
967+ codecinfo = search_function (_sample_name (search_function ))
932968 if codecinfo is not None :
933969 return codecinfo
934970 # finally, get a CodecInfo with the original lookup function and refine it with a dictionary of parameters
@@ -945,7 +981,6 @@ def lookup(encoding, macro=True):
945981 e = LookupError (f"unknown encoding: { encoding } " )
946982 e .__cause__ = e # stop exception chaining
947983 raise e
948- codecs .lookup = lookup
949984
950985
951986def register (search_function , add_to_codecs = False ):
@@ -964,6 +999,7 @@ def register(search_function, add_to_codecs=False):
964999 except LookupError :
9651000 l = __codecs_registry
9661001 l .append (search_function )
1002+ __registry_changed ()
9671003 if add_to_codecs :
9681004 __orig_register (search_function )
9691005
@@ -974,37 +1010,69 @@ def __register(search_function):
9741010codecs .register = __register
9751011
9761012
1013+ def __names ():
1014+ """ Get the table of names used for searching encodings, as tuples (names to be matched, resulting name, search
1015+ function), built once per state of the registry. """
1016+ if len (__names_table ) == 0 :
1017+ for search_function in CODECS_OVERWRITTEN + __codecs_registry :
1018+ n = search_function .__name__
1019+ __names_table .append (((n , n .replace ("_" , "-" )), n .replace ("_" , "-" ), search_function ))
1020+ for alias , name in ALIASES .items ():
1021+ __names_table .append (((alias , name ), name , None ))
1022+ return __names_table
1023+
1024+
1025+ def __registry_changed ():
1026+ """ Clear the caches depending on the state of the registry. """
1027+ __guess_lists .clear ()
1028+ __lookup_cache .clear ()
1029+ __names_table .clear ()
1030+
1031+
1032+ def _sample_name (search_function ):
1033+ """ Get the encoding name generated from the pattern of the given search function (used when looking up a codec by
1034+ its name) ; it is cached on the search function, as generating strings from a regex is costly. """
1035+ try :
1036+ return search_function .__sample__
1037+ except AttributeError :
1038+ search_function .__sample__ = s = generate_string_from_regex (getattr (search_function , "__pattern__" , None ))
1039+ return s
1040+
1041+
1042+ def _sample_rounds (search_function ):
1043+ """ Get 5 sets of encoding names generated from the pattern of the given search function, for the extended search ;
1044+ they are cached on the search function, as generating strings from a regex is costly. """
1045+ try :
1046+ return search_function .__samples__
1047+ except AttributeError :
1048+ p = getattr (search_function , "__pattern__" , None )
1049+ search_function .__samples__ = r = [list (generate_strings_from_regex (p )) for _ in range (5 )]
1050+ return r
1051+
1052+
9771053def search (encoding_regex , extended = True ):
9781054 """ Function similar to lookup but allows to search for an encoding based on a regex instead. It searches this way
9791055 into the local registry but also tries a simple lookup with the original lookup function. """
980- matches = []
981- for search_function in CODECS_OVERWRITTEN + __codecs_registry :
982- n = search_function .__name__
983- for name in [n , n .replace ("_" , "-" )]:
984- if re .search (encoding_regex , name ):
985- matches .append (n .replace ("_" , "-" ))
986- continue
987- if extended :
1056+ matches , rx = [], re .compile (encoding_regex )
1057+ for names , name , search_function in __names ():
1058+ if any (rx .search (n ) for n in names ):
1059+ matches .append (name )
1060+ if extended and search_function is not None :
9881061 # in some cases, encoding_regex can match a generated string that uses a particular portion of its
9891062 # generating pattern ; e.g. we expect encoding_regex="uu_" to find "uu" and "uu_codec" while it can also
9901063 # find "morse" or "atbash" very rarely because of their dynamic patterns and the limited number of randomly
9911064 # generated strings
9921065 # so, we can use a qualified majority voting to ensure we do not get a "junk" encoding in the list of
993- # matches ; executing 5 times the string generation for a given codec but adding the codec to the list of
1066+ # matches ; using 5 sets of generated strings for a given codec but adding the codec to the list of
9941067 # matches only if we get at least 3 matches ensures that we consider up to 2 failures that could be
9951068 # stochastic, therefore drastically decreasing the probability to get a "junk" encoding in the matches list
9961069 c = 0
997- for i in range (5 ):
998- for s in generate_strings_from_regex (search_function .__pattern__ ):
999- if re .search (encoding_regex , s ):
1000- c += 1
1070+ for strings in _sample_rounds (search_function ):
1071+ if any (rx .search (s ) for s in strings ):
1072+ c += 1
1073+ if c >= 3 :
1074+ matches .append (search_function .__name__ )
10011075 break
1002- if c >= 3 :
1003- matches .append (n )
1004- break
1005- for s , n in ALIASES .items ():
1006- if re .search (encoding_regex , s ) or re .search (encoding_regex , n ):
1007- matches .append (n )
10081076 return sorted (list (set (matches )), key = _human_keys )
10091077codecs .search = search
10101078
@@ -1270,6 +1338,20 @@ def __guess(prev_input, input, stop_func, depth, max_depth, min_depth, encodings
12701338 stop , show , scoring_heuristic , extended , debug )
12711339
12721340
1341+ def __guess_list (encoding ):
1342+ """ Get the list of encoding names to be tried when guessing for the given encoding, cached per state of the
1343+ registry. """
1344+ try :
1345+ return __guess_lists [encoding ]
1346+ except KeyError :
1347+ try :
1348+ l = lookup (encoding , False ).parameters ['guess' ]
1349+ except Exception :
1350+ l = []
1351+ __guess_lists [encoding ] = l
1352+ return l
1353+
1354+
12731355def __make_encodings_dict (include , exclude ):
12741356 """ Process encodings inclusion and exclusion lists, listing categories and developping codecs' lists of possible
12751357 encoding names. It also creates a cache with the CodecInfo objects for improving performance. """
@@ -1281,11 +1363,7 @@ def _develop(d, keep=True):
12811363 for enc in ((list_encodings (* cc ) if (len (cc ) > 0 or keep ) and len (sc ) == 0 else []) + sc ):
12821364 g = []
12831365 for e in (search (enc , False ) or [enc ]):
1284- try :
1285- ci = lookup (e , False )
1286- g .extend (ci .parameters ['guess' ])
1287- except :
1288- pass
1366+ g .extend (__guess_list (e ))
12891367 if enc in g : # e.g. "rot-1" => ["rot-1", "rot-2", ...] ; only "rot-1" is to be selected
12901368 l .append (enc )
12911369 else : # e.g. "rot" => ["rot-1", "rot-2", ...] ; all the "rot-N" shall be selected
0 commit comments