138 lines
4.6 KiB
Python
138 lines
4.6 KiB
Python
# 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
|