# The MIT License (MIT) # # Copyright (c) 2020 Jim Bennett # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ `quote` ================================================================================ The quote function %-escapes all characters that are neither in the unreserved chars ("always safe") nor the additional chars set via the safe arg. """ _ALWAYS_SAFE = frozenset(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ" b"abcdefghijklmnopqrstuvwxyz" b"0123456789" b"_.-~") _ALWAYS_SAFE_BYTES = bytes(_ALWAYS_SAFE) SAFE_QUOTERS = {} def quote(bytes_val: bytes, safe="/"): """The quote function %-escapes all characters that are neither in the unreserved chars ("always safe") nor the additional chars set via the safe arg. """ if not isinstance(bytes_val, (bytes, bytearray)): raise TypeError("quote_from_bytes() expected bytes") if not bytes_val: return "" if isinstance(safe, str): # Normalize 'safe' by converting to bytes and removing non-ASCII chars safe = safe.encode("ascii", "ignore") else: safe = bytes([char for char in safe if char < 128]) if not bytes_val.rstrip(_ALWAYS_SAFE_BYTES + safe): return bytes_val.decode() try: quoter = SAFE_QUOTERS[safe] except KeyError: SAFE_QUOTERS[safe] = quoter = Quoter(safe).__getitem__ return "".join([quoter(char) for char in bytes_val]) # pylint: disable=C0103 class defaultdict: """ Default Dict Implementation. Defaultdcit that returns the key if the key is not found in dictionnary (see unswap in karma-lib): >>> d = defaultdict(default=lambda key: key) >>> d['foo'] = 'bar' >>> d['foo'] 'bar' >>> d['baz'] 'baz' DefaultDict that returns an empty string if the key is not found (see prefix in karma-lib for typical usage): >>> d = defaultdict(default=lambda key: '') >>> d['foo'] = 'bar' >>> d['foo'] 'bar' >>> d['baz'] '' Representation of a default dict: >>> defaultdict([('foo', 'bar')]) defaultdict(None, {'foo': 'bar'}) """ @staticmethod # pylint: disable=W0613 def __new__(cls, default_factory=None, **kwargs): self = super(defaultdict, cls).__new__(cls) # pylint: disable=C0103 self.d = {} return self def __init__(self, default_factory=None, **kwargs): self.d = kwargs self.default_factory = default_factory def __getitem__(self, key): try: return self.d[key] except KeyError: val = self.__missing__(key) self.d[key] = val return val def __setitem__(self, key, val): self.d[key] = val def __delitem__(self, key): del self.d[key] def __contains__(self, key): return key in self.d def __missing__(self, key): if self.default_factory is None: raise KeyError(key) return self.default_factory() class Quoter(defaultdict): """A mapping from bytes (in range(0,256)) to strings. String values are percent-encoded byte values, unless the key < 128, and in the "safe" set (either the specified safe set, or default set). """ # Keeps a cache internally, using defaultdict, for efficiency (lookups # of cached keys don't call Python code at all). def __init__(self, safe): """safe: bytes object.""" super(Quoter, self).__init__() self.safe = _ALWAYS_SAFE.union(safe) def __missing__(self, b): # Handle a cache miss. Store quoted string in cache and return. res = chr(b) if b in self.safe else "%{:02X}".format(b) self[b] = res return res