@@ -137,7 +137,7 @@ class StreamReader(Codec, codecs.StreamReader):
137137 register (getregentry , add_to_codecs )
138138
139139
140- def add_map (ename , encmap , repl_char = "?" , sep = "" , ignore_case = False , no_error = False , binary = False , ** kwargs ):
140+ def add_map (ename , encmap , repl_char = "?" , sep = "" , ignore_case = None , no_error = False , binary = False , ** kwargs ):
141141 """
142142 This adds a new mapping codec (that is, declarable with a simple character mapping dictionary) to the codecs module
143143 dynamically setting its encode and/or decode functions, eventually dynamically naming the encoding with a pattern
@@ -150,7 +150,7 @@ def add_map(ename, encmap, repl_char="?", sep="", ignore_case=False, no_error=Fa
150150 :param sep: string of possible character separators (hence, only single-char separators are considered) ;
151151 - while encoding, the first separator is used
152152 - while decoding, separators can be mixed in the input text
153- :param ignore_case: ignore text case
153+ :param ignore_case: ignore text case while encoding and/or decoding
154154 :param no_error: this encoding triggers no error (hence, always in "leave" errors handling)
155155 :param binary: encoding applies to the binary string of the input text
156156 :param pattern: pattern for dynamically naming the encoding
@@ -159,6 +159,8 @@ def add_map(ename, encmap, repl_char="?", sep="", ignore_case=False, no_error=Fa
159159 NB: this will make the codec available in the built-in open(...) but will make it impossible
160160 to remove the codec later
161161 """
162+ if ignore_case not in [None , "encode" , "decode" , "both" ]:
163+ raise ValueError ("Bad ignore_case parameter while creating encoding map" )
162164 def __generic_code (mapdict , exc , decode = False ):
163165 def _wrapper (param ):
164166 """
@@ -227,11 +229,9 @@ def _wrapper(param):
227229 smapdict [k ] = v .translate (t )
228230 else :
229231 raise LookupError ("Bad parameter for encoding '{}': '{}'" .format (ename , p ))
230- if ignore_case :
231- case = ["upper" , "lower" ][any (c in "" .join (smapdict .keys ()) for c in "abcdefghijklmnopqrstuvwxyz" )]
232- # use the first mapped group from the mapping dictionary to determine token length ; this is useful e.g. for
233- # tokenizing a binary string when the text is to be converted as binary
234- tlen = len (list (set (smapdict .keys ()) - {"" })[0 ])
232+ if ignore_case is not None :
233+ case_d = ["upper" , "lower" ][any (c in "" .join (smapdict .values ()) for c in "abcdefghijklmnopqrstuvwxyz" )]
234+ case_e = ["upper" , "lower" ][any (c in "" .join (smapdict .keys ()) for c in "abcdefghijklmnopqrstuvwxyz" )]
235235 if decode :
236236 tmp = {}
237237 # this has a meaning for encoding maps that could have clashes in encoded chars (e.g. Bacon's cipher ;
@@ -243,34 +243,80 @@ def _wrapper(param):
243243 # this allows to avoid an error with Python2 in the "for i, c in enumerate(parts)" loop
244244 if '' not in smapdict .keys ():
245245 smapdict ['' ] = ""
246+ # determine token and result lengths
247+ tmaxlen = max (map (len , smapdict .keys ()))
248+ tminlen = max (1 , min (map (len , set (smapdict .keys ()) - {'' })))
249+ rminlen = max (1 , min (map (len , set (smapdict .values ()) - {'' })))
246250
251+ # generic encoding/decoding function for map encodings
247252 def code (text , errors = "strict" ):
248- if ignore_case :
249- text = getattr (text , case )()
253+ icase = ignore_case == "both" or \
254+ decode and ignore_case == "decode" or \
255+ not decode and ignore_case == "encode"
256+ if icase :
257+ case = case_d if decode else case_e
250258 if no_error :
251259 errors = "leave"
252260 text = ensure_str (text )
253261 if binary and not decode :
254262 text = "" .join ("{:0>8}" .format (bin (ord (c ))[2 :]) for c in text )
255- text = [text [i :i + tlen ] for i in range (0 , len (text ), tlen )]
256- parts = re .split ("[" + sep + "]" , text ) if decode and len (sep ) > 0 else text
257263 r = ""
258264 lsep = "" if decode else sep if len (sep ) <= 1 else sep [0 ]
259- for i , c in enumerate (parts ):
265+
266+ # get the value from the mapping dictionary, trying the token with its inverted case if relevant
267+ def __get_value (token , position , case_changed = False ):
260268 try :
261- r += smapdict [c ] + lsep
269+ return smapdict [token ] + lsep
262270 except KeyError :
263- if errors == "strict" :
264- raise exc ("'{}' codec can't {}code character '{}' in position {}"
265- .format (ename , ["en" , "de" ][decode ], c , i ))
266- elif errors == "leave" :
267- r += c + lsep
268- elif errors == "replace" :
269- r += repl_char * [1 , tlen ][decode ] + lsep
270- elif errors == "ignore" :
271- continue
271+ if icase and not case_changed :
272+ token_inv_case = getattr (token , case )()
273+ r = __get_value (token_inv_case , position , True )
274+ if r == token_inv_case + lsep and errors == "leave" :
275+ return token + lsep
276+ return r
277+ return __handle_error (token , position )
278+
279+ def __handle_error (token , position ):
280+ if errors == "strict" :
281+ raise exc ("'{}' codec can't {}code character '{}' in position {}"
282+ .format (ename , ["en" , "de" ][decode ], token , position ))
283+ elif errors == "leave" :
284+ return token + lsep
285+ elif errors == "replace" :
286+ return repl_char * rminlen + lsep
287+ elif errors == "ignore" :
288+ return ""
289+ else :
290+ raise ValueError ("Unsupported error handling '{}'" .format (errors ))
291+
292+ # if a separator is defined, rely on it by splitting the input text
293+ if decode and len (sep ) > 0 :
294+ for i , c in enumerate (re .split ("[" + sep + "]" , text )):
295+ r += __get_value (c , i )
296+ # otherwise, move through the text using a cursor for tokenizing it ; this allows defining more complex
297+ # encodings with variable token lengths
298+ else :
299+ cursor , bad = 0 , ""
300+ while cursor < len (text ):
301+ token = text [cursor :cursor + 1 ]
302+ for l in range (tminlen , tmaxlen + 1 ):
303+ token = text [cursor :cursor + l ]
304+ if token in smapdict .keys () or icase and getattr (token , case )() in smapdict .keys ():
305+ # do not forget to handle bad chars already collected at this point
306+ if len (bad ) > 0 :
307+ r += __get_value (bad , cursor - len (bad ))
308+ bad = ""
309+ r += __get_value (token , cursor )
310+ cursor += l
311+ break
272312 else :
273- raise ValueError ("Unsupported error handling '{}'" .format (errors ))
313+ # collect bad chars and only move the cursor one char to the right
314+ bad += text [cursor ]
315+ cursor += 1
316+ # if the number of bad chars is the minimum token length, consume it and start a new buffer
317+ if len (bad ) == tminlen :
318+ r += __handle_error (bad , cursor - len (bad ))
319+ bad = ""
274320 if binary and decode :
275321 tmp , r = "" , r .replace (lsep , "" )
276322 for i in range (0 , len (r ), 8 ):
0 commit comments