You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
bazarr/libs/markupsafe/__init__.py

333 lines
11 KiB

import functools
5 years ago
import string
import sys
import typing as t
5 years ago
if t.TYPE_CHECKING:
import typing_extensions as te
5 years ago
class HasHTML(te.Protocol):
def __html__(self) -> str:
pass
5 years ago
_P = te.ParamSpec("_P")
5 years ago
__version__ = "2.1.5"
5 years ago
def _simple_escaping_wrapper(func: "t.Callable[_P, str]") -> "t.Callable[_P, Markup]":
@functools.wraps(func)
def wrapped(self: "Markup", *args: "_P.args", **kwargs: "_P.kwargs") -> "Markup":
arg_list = _escape_argspec(list(args), enumerate(args), self.escape)
_escape_argspec(kwargs, kwargs.items(), self.escape)
return self.__class__(func(self, *arg_list, **kwargs)) # type: ignore[arg-type]
return wrapped # type: ignore[return-value]
class Markup(str):
5 years ago
"""A string that is ready to be safely inserted into an HTML or XML
document, either because it was escaped or because it was marked
safe.
Passing an object to the constructor converts it to text and wraps
it to mark it safe without escaping. To escape the text, use the
:meth:`escape` class method instead.
>>> Markup("Hello, <em>World</em>!")
5 years ago
Markup('Hello, <em>World</em>!')
>>> Markup(42)
Markup('42')
>>> Markup.escape("Hello, <em>World</em>!")
5 years ago
Markup('Hello &lt;em&gt;World&lt;/em&gt;!')
This implements the ``__html__()`` interface that some frameworks
use. Passing an object that implements ``__html__()`` will wrap the
output of that method, marking it safe.
>>> class Foo:
... def __html__(self):
... return '<a href="/foo">foo</a>'
...
>>> Markup(Foo())
Markup('<a href="/foo">foo</a>')
This is a subclass of :class:`str`. It has the same methods, but
escapes their arguments and returns a ``Markup`` instance.
5 years ago
>>> Markup("<em>%s</em>") % ("foo & bar",)
5 years ago
Markup('<em>foo &amp; bar</em>')
>>> Markup("<em>Hello</em> ") + "<foo>"
5 years ago
Markup('<em>Hello</em> &lt;foo&gt;')
"""
__slots__ = ()
def __new__(
cls, base: t.Any = "", encoding: t.Optional[str] = None, errors: str = "strict"
) -> "te.Self":
5 years ago
if hasattr(base, "__html__"):
base = base.__html__()
5 years ago
if encoding is None:
return super().__new__(cls, base)
5 years ago
return super().__new__(cls, base, encoding, errors)
def __html__(self) -> "te.Self":
5 years ago
return self
def __add__(self, other: t.Union[str, "HasHTML"]) -> "te.Self":
if isinstance(other, str) or hasattr(other, "__html__"):
return self.__class__(super().__add__(self.escape(other)))
5 years ago
return NotImplemented
def __radd__(self, other: t.Union[str, "HasHTML"]) -> "te.Self":
if isinstance(other, str) or hasattr(other, "__html__"):
5 years ago
return self.escape(other).__add__(self)
return NotImplemented
def __mul__(self, num: "te.SupportsIndex") -> "te.Self":
if isinstance(num, int):
return self.__class__(super().__mul__(num))
return NotImplemented
5 years ago
__rmul__ = __mul__
def __mod__(self, arg: t.Any) -> "te.Self":
5 years ago
if isinstance(arg, tuple):
# a tuple of arguments, each wrapped
5 years ago
arg = tuple(_MarkupEscapeHelper(x, self.escape) for x in arg)
elif hasattr(type(arg), "__getitem__") and not isinstance(arg, str):
# a mapping of arguments, wrapped
5 years ago
arg = _MarkupEscapeHelper(arg, self.escape)
else:
# a single argument, wrapped with the helper and a tuple
arg = (_MarkupEscapeHelper(arg, self.escape),)
5 years ago
return self.__class__(super().__mod__(arg))
def __repr__(self) -> str:
return f"{self.__class__.__name__}({super().__repr__()})"
5 years ago
def join(self, seq: t.Iterable[t.Union[str, "HasHTML"]]) -> "te.Self":
return self.__class__(super().join(map(self.escape, seq)))
5 years ago
join.__doc__ = str.join.__doc__
5 years ago
def split( # type: ignore[override]
self, sep: t.Optional[str] = None, maxsplit: int = -1
) -> t.List["te.Self"]:
return [self.__class__(v) for v in super().split(sep, maxsplit)]
5 years ago
split.__doc__ = str.split.__doc__
5 years ago
def rsplit( # type: ignore[override]
self, sep: t.Optional[str] = None, maxsplit: int = -1
) -> t.List["te.Self"]:
return [self.__class__(v) for v in super().rsplit(sep, maxsplit)]
5 years ago
rsplit.__doc__ = str.rsplit.__doc__
5 years ago
def splitlines( # type: ignore[override]
self, keepends: bool = False
) -> t.List["te.Self"]:
return [self.__class__(v) for v in super().splitlines(keepends)]
5 years ago
splitlines.__doc__ = str.splitlines.__doc__
5 years ago
def unescape(self) -> str:
5 years ago
"""Convert escaped markup back into a text string. This replaces
HTML entities with the characters they represent.
>>> Markup("Main &raquo; <em>About</em>").unescape()
5 years ago
'Main » <em>About</em>'
"""
from html import unescape
return unescape(str(self))
def striptags(self) -> str:
5 years ago
""":meth:`unescape` the markup, remove tags, and normalize
whitespace to single spaces.
>>> Markup("Main &raquo;\t<em>About</em>").striptags()
5 years ago
'Main » About'
"""
value = str(self)
# Look for comments then tags separately. Otherwise, a comment that
# contains a tag would end early, leaving some of the comment behind.
while True:
# keep finding comment start marks
start = value.find("<!--")
if start == -1:
break
# find a comment end mark beyond the start, otherwise stop
end = value.find("-->", start)
if end == -1:
break
value = f"{value[:start]}{value[end + 3:]}"
# remove tags using the same method
while True:
start = value.find("<")
if start == -1:
break
end = value.find(">", start)
if end == -1:
break
value = f"{value[:start]}{value[end + 1:]}"
# collapse spaces
value = " ".join(value.split())
return self.__class__(value).unescape()
5 years ago
@classmethod
def escape(cls, s: t.Any) -> "te.Self":
5 years ago
"""Escape a string. Calls :func:`escape` and ensures that for
subclasses the correct type is returned.
"""
rv = escape(s)
5 years ago
if rv.__class__ is not cls:
return cls(rv)
return rv # type: ignore[return-value]
__getitem__ = _simple_escaping_wrapper(str.__getitem__)
capitalize = _simple_escaping_wrapper(str.capitalize)
title = _simple_escaping_wrapper(str.title)
lower = _simple_escaping_wrapper(str.lower)
upper = _simple_escaping_wrapper(str.upper)
replace = _simple_escaping_wrapper(str.replace)
ljust = _simple_escaping_wrapper(str.ljust)
rjust = _simple_escaping_wrapper(str.rjust)
lstrip = _simple_escaping_wrapper(str.lstrip)
rstrip = _simple_escaping_wrapper(str.rstrip)
center = _simple_escaping_wrapper(str.center)
strip = _simple_escaping_wrapper(str.strip)
translate = _simple_escaping_wrapper(str.translate)
expandtabs = _simple_escaping_wrapper(str.expandtabs)
swapcase = _simple_escaping_wrapper(str.swapcase)
zfill = _simple_escaping_wrapper(str.zfill)
casefold = _simple_escaping_wrapper(str.casefold)
if sys.version_info >= (3, 9):
removeprefix = _simple_escaping_wrapper(str.removeprefix)
removesuffix = _simple_escaping_wrapper(str.removesuffix)
def partition(self, sep: str) -> t.Tuple["te.Self", "te.Self", "te.Self"]:
l, s, r = super().partition(self.escape(sep))
cls = self.__class__
return cls(l), cls(s), cls(r)
5 years ago
def rpartition(self, sep: str) -> t.Tuple["te.Self", "te.Self", "te.Self"]:
l, s, r = super().rpartition(self.escape(sep))
cls = self.__class__
return cls(l), cls(s), cls(r)
5 years ago
def format(self, *args: t.Any, **kwargs: t.Any) -> "te.Self":
5 years ago
formatter = EscapeFormatter(self.escape)
return self.__class__(formatter.vformat(self, args, kwargs))
def format_map( # type: ignore[override]
self, map: t.Mapping[str, t.Any]
) -> "te.Self":
formatter = EscapeFormatter(self.escape)
return self.__class__(formatter.vformat(self, (), map))
def __html_format__(self, format_spec: str) -> "te.Self":
5 years ago
if format_spec:
raise ValueError("Unsupported format specification for Markup.")
5 years ago
return self
class EscapeFormatter(string.Formatter):
__slots__ = ("escape",)
5 years ago
def __init__(self, escape: t.Callable[[t.Any], Markup]) -> None:
self.escape = escape
super().__init__()
def format_field(self, value: t.Any, format_spec: str) -> str:
if hasattr(value, "__html_format__"):
rv = value.__html_format__(format_spec)
elif hasattr(value, "__html__"):
if format_spec:
raise ValueError(
f"Format specifier {format_spec} given, but {type(value)} does not"
" define __html_format__. A class that defines __html__ must define"
" __html_format__ to work with format specifiers."
)
rv = value.__html__()
else:
# We need to make sure the format spec is str here as
# otherwise the wrong callback methods are invoked.
rv = string.Formatter.format_field(self, value, str(format_spec))
return str(self.escape(rv))
5 years ago
_ListOrDict = t.TypeVar("_ListOrDict", list, dict)
5 years ago
def _escape_argspec(
obj: _ListOrDict, iterable: t.Iterable[t.Any], escape: t.Callable[[t.Any], Markup]
) -> _ListOrDict:
5 years ago
"""Helper for various string-wrapped functions."""
for key, value in iterable:
if isinstance(value, str) or hasattr(value, "__html__"):
5 years ago
obj[key] = escape(value)
5 years ago
return obj
class _MarkupEscapeHelper:
"""Helper for :meth:`Markup.__mod__`."""
5 years ago
__slots__ = ("obj", "escape")
def __init__(self, obj: t.Any, escape: t.Callable[[t.Any], Markup]) -> None:
5 years ago
self.obj = obj
self.escape = escape
def __getitem__(self, item: t.Any) -> "te.Self":
return self.__class__(self.obj[item], self.escape)
5 years ago
def __str__(self) -> str:
return str(self.escape(self.obj))
5 years ago
def __repr__(self) -> str:
5 years ago
return str(self.escape(repr(self.obj)))
def __int__(self) -> int:
5 years ago
return int(self.obj)
def __float__(self) -> float:
5 years ago
return float(self.obj)
# circular import
5 years ago
try:
from ._speedups import escape as escape
from ._speedups import escape_silent as escape_silent
from ._speedups import soft_str as soft_str
5 years ago
except ImportError:
from ._native import escape as escape
from ._native import escape_silent as escape_silent # noqa: F401
from ._native import soft_str as soft_str # noqa: F401