Skip to content

Commit e1d7928

Browse files
committed
Improved internal performance
1 parent 4a025c5 commit e1d7928

1 file changed

Lines changed: 116 additions & 38 deletions

File tree

‎src/codext/__common__.py‎

Lines changed: 116 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@
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

6165
MACROS = {}
6266
PERS_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()
626635
codecs.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'):
897910
codecs.decode = decode
898911

899912

913+
_REPEAT = re.compile(r"\[(\d+)\]$")
914+
915+
900916
def 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

920936
def 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

951986
def 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):
9741010
codecs.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+
9771053
def 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)
10091077
codecs.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+
12731355
def __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

Comments
 (0)