66import sys
77from functools import wraps
88from importlib import import_module
9+ from inspect import currentframe
910from six import binary_type , string_types , text_type
1011from types import FunctionType
1112try : # Python3
1617 from importlib import reload
1718except ImportError :
1819 pass
20+ try : # Python 2
21+ from string import maketrans
22+ except ImportError : # Python 3
23+ maketrans = str .maketrans
1924
2025
21- __all__ = ["add" , "b" , "clear" , "codecs" , "ensure_str" , "re" , "register" ,
22- "remove" , "reset" , " s2i" , "PY3" ]
26+ __all__ = ["add" , "add_map" , " b" , "clear" , "codecs" , "ensure_str" , "maketrans" , " re" , "register" , "remove" , "reset " ,
27+ "s2i" , "PY3" ]
2328CODECS_REGISTRY = None
2429PY3 = sys .version [0 ] == "3"
2530__codecs_registry = []
3237s2i = lambda s : int (codecs .encode (s , "base16" ), 16 )
3338
3439
35- def add (ename , encode = None , decode = None , pattern = None , text = True ,
36- add_to_codecs = False ):
40+ def add (ename , encode = None , decode = None , pattern = None , text = True , add_to_codecs = False ):
3741 """
38- This adds a new codec to the codecs module setting its encode and/or decode
39- functions, eventually dynamically naming the encoding with a pattern and
40- with file handling (if text is True).
42+ This adds a new codec to the codecs module setting its encode and/or decode functions, eventually dynamically naming
43+ the encoding with a pattern and with file handling (if text is True).
4144
42- :param ename: encoding name
43- :param encode: encoding function or None
44- :param decode: decoding function or None
45- :param pattern: pattern for dynamically naming the encoding
46- :param text: specify whether the codec is a text encoding
47- :param add_to_codecs: also add the search function to the native registry
48- NB: this will make the codec available in the
49- built-in open(...) but will make it impossible
50- to remove the codec later
45+ :param ename: encoding name
46+ :param encode: encoding function or None
47+ :param decode: decoding function or None
48+ :param pattern: pattern for dynamically naming the encoding
49+ :param text: specify whether the codec is a text encoding
50+ :param add_to_codecs: also add the search function to the native registry
51+ NB: this will make the codec available in the built-in open(...) but will make it impossible
52+ to remove the codec later
5153 """
5254 if encode and not isinstance (encode , FunctionType ):
5355 raise ValueError ("Bad 'encode' function" )
@@ -62,12 +64,12 @@ def getregentry(encoding):
6264 fenc , fdec , name = encode , decode , encoding
6365 # prepare CodecInfo input arguments
6466 class Codec (codecs .Codec ):
65- def encode (self , input , errors = ' strict' ):
67+ def encode (self , input , errors = " strict" ):
6668 if fenc is None :
6769 raise NotImplementedError
6870 return fenc (input , errors )
6971
70- def decode (self , input , errors = ' strict' ):
72+ def decode (self , input , errors = " strict" ):
7173 if fdec is None :
7274 raise NotImplementedError
7375 return fdec (input , errors )
@@ -91,17 +93,16 @@ def decode(self, input, final=False):
9193 if pattern :
9294 m = re .match (pattern , encoding )
9395 try :
94- g = m .group (1 )
96+ g = m .group (1 ) or ""
9597 if g .isdigit ():
9698 g = int (g )
9799 fenc = fenc (g ) if fenc else fenc
98100 fdec = fdec (g ) if fdec else fdec
99101 except AttributeError :
100102 return # this occurs when m is None, meaning no match
101103 except IndexError :
102- # this occurs while m is not None, but possibly no capture group
103- # that gives at least 1 group index ; in this case, if
104- # fenc/fdec is a decorated function, execute it with no arg
104+ # this occurs while m is not None, but possibly no capture group that gives at least 1 group index ; in
105+ # this case, if fenc/fdec is a decorated function, execute it with no arg
105106 if fenc and len (getfullargspec (fenc ).args ) == 1 :
106107 fenc = fenc ()
107108 if fdec and len (getfullargspec (fdec ).args ) == 1 :
@@ -136,6 +137,146 @@ class StreamReader(Codec, codecs.StreamReader):
136137 register (getregentry , add_to_codecs )
137138
138139
140+ def add_map (ename , encmap , repl_char = "?" , sep = "" , ignore_case = False , no_error = False , binary = False , ** kwargs ):
141+ """
142+ This adds a new mapping codec (that is, declarable with a simple character mapping dictionary) to the codecs module
143+ dynamically setting its encode and/or decode functions, eventually dynamically naming the encoding with a pattern
144+ and with file handling (if text is True).
145+
146+ :param ename: encoding name
147+ :param encmap: characters encoding map ; can be a dictionary of encoding maps (for use with the first capture
148+ group of the regex pattern)
149+ :param repl_char: replacement char (used when errors handling is set to "replace")
150+ :param sep: string of possible character separators (hence, only single-char separators are considered) ;
151+ - while encoding, the first separator is used
152+ - while decoding, separators can be mixed in the input text
153+ :param ignore_case: ignore text case
154+ :param no_error: this encoding triggers no error (hence, always in "leave" errors handling)
155+ :param binary: encoding applies to the binary string of the input text
156+ :param pattern: pattern for dynamically naming the encoding
157+ :param text: specify whether the codec is a text encoding
158+ :param add_to_codecs: also add the search function to the native registry
159+ NB: this will make the codec available in the built-in open(...) but will make it impossible
160+ to remove the codec later
161+ """
162+ def __generic_code (mapdict , exc , decode = False ):
163+ def _wrapper (param ):
164+ """
165+ The parameter for wrapping comes from the encoding regex pattern ; e.g.
166+ [no pattern] => param will be None everytime
167+ r"barbie[-_]?([1-4])$" => param could be int 1, 2, 3 or 4
168+ r"^morse(|[-_]?.{3})$" => param could be None, "-ABC" (for mapping to ".-/")
169+
170+ In order of precedence:
171+ 1. when param is a key in mapdict or mapdict is a list of encoding maps (hence in the case of "barbie...",
172+ param MUST be an int, otherwise for the first case it could clash with a character of the encoding map)
173+ 2. otherwise handle it as a new encoding character map "ABC" translates to ".-/" for morse
174+ """
175+ if isinstance (mapdict , dict ):
176+ smapdict = {k : v for k , v in mapdict .items ()}
177+ elif isinstance (mapdict , list ) and isinstance (mapdict [0 ], dict ):
178+ smapdict = {k : v for k , v in mapdict [0 ].items ()}
179+ else :
180+ raise ValueError ("Bad mapping dictionary or list of mapping dictionaries" )
181+ if param :
182+ # case 1: list or dictionary of parameter-dependent encodings
183+ if isinstance (param , int ):
184+ if isinstance (mapdict , list ):
185+ param -= 1
186+ if isinstance (mapdict , list ) and 0 <= param < len (mapdict ) or \
187+ isinstance (mapdict , dict ) and param in mapdict .keys ():
188+ smapdict = mapdict [param ]
189+ else :
190+ raise LookupError ("Bad parameter for encoding '{}': {}" .format (ename , param ))
191+ # case 2: encodinc characters translation
192+ else :
193+ # collect base tokens in order of appearance in the mapping dictionary
194+ base_tokens = ""
195+ for _ , c in sorted (mapdict .items ()):
196+ for t in c :
197+ if t not in base_tokens :
198+ base_tokens += t
199+ if param [0 ] in "-_" and len (param [1 :]) == len (set (param [1 :])) == len (base_tokens ):
200+ param = param [1 :]
201+ if len (param ) == len (set (param )) == len (base_tokens ):
202+ t = maketrans (base_tokens , param )
203+ for k , v in smapdict .items ():
204+ smapdict [k ] = v .translate (t )
205+ else :
206+ raise LookupError ("Bad parameter for encoding '{}': {}" .format (ename , param ))
207+ if ignore_case :
208+ case = ["upper" , "lower" ][any (c in "" .join (smapdict .keys ()) for c in "abcdefghijklmnopqrstuvwxyz" )]
209+ # use the first mapped group from the mapping dictionary to determine token length ; this is useful e.g. for
210+ # tokenizing a binary string when the text is to be converted as binary
211+ tlen = len (list (smapdict .keys ())[0 ])
212+ if decode :
213+ tmp = {}
214+ # this has a meaning for encoding maps that could have clashes in encoded chars (e.g. Bacon's cipher ;
215+ # I => abaaa but also J => abaaa, with the following, we keep I instead of letting J overwrite it)
216+ for k , v in smapdict .items ():
217+ if v not in tmp .keys ():
218+ tmp [v ] = k
219+ smapdict = tmp
220+ # this allows to avoid an error with Python2 in the "for i, c in enumerate(parts)" loop
221+ if '' not in smapdict .keys ():
222+ smapdict ['' ] = ""
223+
224+ def code (text , errors = "strict" ):
225+ if ignore_case :
226+ text = getattr (text , case )()
227+ if no_error :
228+ errors = "leave"
229+ text = ensure_str (text )
230+ if binary and not decode :
231+ text = "" .join ("{:0>8}" .format (bin (ord (c ))[2 :]) for c in text )
232+ text = [text [i :i + tlen ] for i in range (0 , len (text ), tlen )]
233+ parts = re .split ("[" + sep + "]" , text ) if decode and len (sep ) > 0 else text
234+ r = ""
235+ lsep = "" if decode else sep if len (sep ) <= 1 else sep [0 ]
236+ for i , c in enumerate (parts ):
237+ try :
238+ r += smapdict [c ] + lsep
239+ except KeyError :
240+ if errors == "strict" :
241+ raise exc ("'{}' codec can't {}code character '{}' in position {}"
242+ .format (ename , ["en" , "de" ][decode ], c , i ))
243+ elif errors == "leave" :
244+ r += c + lsep
245+ elif errors == "replace" :
246+ r += repl_char * [1 , tlen ][decode ] + lsep
247+ elif errors == "ignore" :
248+ continue
249+ else :
250+ raise ValueError ("Unsupported error handling '{}'" .format (errors ))
251+ if binary and decode :
252+ tmp , r = "" , r .replace (lsep , "" )
253+ for i in range (0 , len (r ), 8 ):
254+ bs = r [i :i + 8 ]
255+ try :
256+ tmp += chr (int (bs , 2 ))
257+ except ValueError :
258+ if len (bs ) > 0 :
259+ tmp += "[" + bs + "]"
260+ r = tmp + lsep
261+ return r [:len (r )- len (lsep )], len (text )
262+ return code
263+ if re .search (r"\([^(?:)]" , kwargs .get ('pattern' , "" )) is None :
264+ # in this case, there is no capturing group for parametrization
265+ return _wrapper (None )
266+ return _wrapper
267+
268+ name = "" .join (t .capitalize () for t in re .split (r"[-_]" , ename ))
269+ glob = currentframe ().f_back .f_globals
270+ # dynamically make dedicated exception classes
271+ decexc = "{}DecodeError" .format (name )
272+ exec ("class {}(ValueError): pass" .format (decexc ), glob )
273+ encexc = "{}EncodeError" .format (name )
274+ exec ("class {}(ValueError): pass" .format (encexc ), glob )
275+ # now use the generic add() function
276+ add (ename , __generic_code (encmap , glob [encexc ]), __generic_code (encmap , glob [decexc ], True ), ** kwargs )
277+ codecs .add_map = add_map
278+
279+
139280def clear ():
140281 """
141282 Clear codext's local registry of search functions.
@@ -231,8 +372,7 @@ def _wrapper(*args, **kwargs):
231372orig_register = _codecs .register
232373
233374
234- def __add (ename , encode = None , decode = None , pattern = None , text = True ,
235- add_to_codecs = True ):
375+ def __add (ename , encode = None , decode = None , pattern = None , text = True , add_to_codecs = True ):
236376 add (ename , encode , decode , pattern , text , add_to_codecs )
237377__add .__doc__ = add .__doc__
238378codecs .add = __add
0 commit comments