__init__.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. import functools
  2. import re
  3. import string
  4. import typing as t
  5. if t.TYPE_CHECKING:
  6. import typing_extensions as te
  7. class HasHTML(te.Protocol):
  8. def __html__(self) -> str:
  9. pass
  10. __version__ = "2.0.1"
  11. _striptags_re = re.compile(r"(<!--.*?-->|<[^>]*>)")
  12. def _simple_escaping_wrapper(name: str) -> t.Callable[..., "Markup"]:
  13. orig = getattr(str, name)
  14. @functools.wraps(orig)
  15. def wrapped(self: "Markup", *args: t.Any, **kwargs: t.Any) -> "Markup":
  16. args = _escape_argspec(list(args), enumerate(args), self.escape) # type: ignore
  17. _escape_argspec(kwargs, kwargs.items(), self.escape)
  18. return self.__class__(orig(self, *args, **kwargs))
  19. return wrapped
  20. class Markup(str):
  21. """A string that is ready to be safely inserted into an HTML or XML
  22. document, either because it was escaped or because it was marked
  23. safe.
  24. Passing an object to the constructor converts it to text and wraps
  25. it to mark it safe without escaping. To escape the text, use the
  26. :meth:`escape` class method instead.
  27. >>> Markup("Hello, <em>World</em>!")
  28. Markup('Hello, <em>World</em>!')
  29. >>> Markup(42)
  30. Markup('42')
  31. >>> Markup.escape("Hello, <em>World</em>!")
  32. Markup('Hello &lt;em&gt;World&lt;/em&gt;!')
  33. This implements the ``__html__()`` interface that some frameworks
  34. use. Passing an object that implements ``__html__()`` will wrap the
  35. output of that method, marking it safe.
  36. >>> class Foo:
  37. ... def __html__(self):
  38. ... return '<a href="/foo">foo</a>'
  39. ...
  40. >>> Markup(Foo())
  41. Markup('<a href="/foo">foo</a>')
  42. This is a subclass of :class:`str`. It has the same methods, but
  43. escapes their arguments and returns a ``Markup`` instance.
  44. >>> Markup("<em>%s</em>") % ("foo & bar",)
  45. Markup('<em>foo &amp; bar</em>')
  46. >>> Markup("<em>Hello</em> ") + "<foo>"
  47. Markup('<em>Hello</em> &lt;foo&gt;')
  48. """
  49. __slots__ = ()
  50. def __new__(
  51. cls, base: t.Any = "", encoding: t.Optional[str] = None, errors: str = "strict"
  52. ) -> "Markup":
  53. if hasattr(base, "__html__"):
  54. base = base.__html__()
  55. if encoding is None:
  56. return super().__new__(cls, base)
  57. return super().__new__(cls, base, encoding, errors)
  58. def __html__(self) -> "Markup":
  59. return self
  60. def __add__(self, other: t.Union[str, "HasHTML"]) -> "Markup":
  61. if isinstance(other, str) or hasattr(other, "__html__"):
  62. return self.__class__(super().__add__(self.escape(other)))
  63. return NotImplemented
  64. def __radd__(self, other: t.Union[str, "HasHTML"]) -> "Markup":
  65. if isinstance(other, str) or hasattr(other, "__html__"):
  66. return self.escape(other).__add__(self)
  67. return NotImplemented
  68. def __mul__(self, num: int) -> "Markup":
  69. if isinstance(num, int):
  70. return self.__class__(super().__mul__(num))
  71. return NotImplemented # type: ignore
  72. __rmul__ = __mul__
  73. def __mod__(self, arg: t.Any) -> "Markup":
  74. if isinstance(arg, tuple):
  75. arg = tuple(_MarkupEscapeHelper(x, self.escape) for x in arg)
  76. else:
  77. arg = _MarkupEscapeHelper(arg, self.escape)
  78. return self.__class__(super().__mod__(arg))
  79. def __repr__(self) -> str:
  80. return f"{self.__class__.__name__}({super().__repr__()})"
  81. def join(self, seq: t.Iterable[t.Union[str, "HasHTML"]]) -> "Markup":
  82. return self.__class__(super().join(map(self.escape, seq)))
  83. join.__doc__ = str.join.__doc__
  84. def split( # type: ignore
  85. self, sep: t.Optional[str] = None, maxsplit: int = -1
  86. ) -> t.List["Markup"]:
  87. return [self.__class__(v) for v in super().split(sep, maxsplit)]
  88. split.__doc__ = str.split.__doc__
  89. def rsplit( # type: ignore
  90. self, sep: t.Optional[str] = None, maxsplit: int = -1
  91. ) -> t.List["Markup"]:
  92. return [self.__class__(v) for v in super().rsplit(sep, maxsplit)]
  93. rsplit.__doc__ = str.rsplit.__doc__
  94. def splitlines(self, keepends: bool = False) -> t.List["Markup"]: # type: ignore
  95. return [self.__class__(v) for v in super().splitlines(keepends)]
  96. splitlines.__doc__ = str.splitlines.__doc__
  97. def unescape(self) -> str:
  98. """Convert escaped markup back into a text string. This replaces
  99. HTML entities with the characters they represent.
  100. >>> Markup("Main &raquo; <em>About</em>").unescape()
  101. 'Main » <em>About</em>'
  102. """
  103. from html import unescape
  104. return unescape(str(self))
  105. def striptags(self) -> str:
  106. """:meth:`unescape` the markup, remove tags, and normalize
  107. whitespace to single spaces.
  108. >>> Markup("Main &raquo;\t<em>About</em>").striptags()
  109. 'Main » About'
  110. """
  111. stripped = " ".join(_striptags_re.sub("", self).split())
  112. return Markup(stripped).unescape()
  113. @classmethod
  114. def escape(cls, s: t.Any) -> "Markup":
  115. """Escape a string. Calls :func:`escape` and ensures that for
  116. subclasses the correct type is returned.
  117. """
  118. rv = escape(s)
  119. if rv.__class__ is not cls:
  120. return cls(rv)
  121. return rv
  122. for method in (
  123. "__getitem__",
  124. "capitalize",
  125. "title",
  126. "lower",
  127. "upper",
  128. "replace",
  129. "ljust",
  130. "rjust",
  131. "lstrip",
  132. "rstrip",
  133. "center",
  134. "strip",
  135. "translate",
  136. "expandtabs",
  137. "swapcase",
  138. "zfill",
  139. ):
  140. locals()[method] = _simple_escaping_wrapper(method)
  141. del method
  142. def partition(self, sep: str) -> t.Tuple["Markup", "Markup", "Markup"]:
  143. l, s, r = super().partition(self.escape(sep))
  144. cls = self.__class__
  145. return cls(l), cls(s), cls(r)
  146. def rpartition(self, sep: str) -> t.Tuple["Markup", "Markup", "Markup"]:
  147. l, s, r = super().rpartition(self.escape(sep))
  148. cls = self.__class__
  149. return cls(l), cls(s), cls(r)
  150. def format(self, *args: t.Any, **kwargs: t.Any) -> "Markup":
  151. formatter = EscapeFormatter(self.escape)
  152. return self.__class__(formatter.vformat(self, args, kwargs))
  153. def __html_format__(self, format_spec: str) -> "Markup":
  154. if format_spec:
  155. raise ValueError("Unsupported format specification for Markup.")
  156. return self
  157. class EscapeFormatter(string.Formatter):
  158. __slots__ = ("escape",)
  159. def __init__(self, escape: t.Callable[[t.Any], Markup]) -> None:
  160. self.escape = escape
  161. super().__init__()
  162. def format_field(self, value: t.Any, format_spec: str) -> str:
  163. if hasattr(value, "__html_format__"):
  164. rv = value.__html_format__(format_spec)
  165. elif hasattr(value, "__html__"):
  166. if format_spec:
  167. raise ValueError(
  168. f"Format specifier {format_spec} given, but {type(value)} does not"
  169. " define __html_format__. A class that defines __html__ must define"
  170. " __html_format__ to work with format specifiers."
  171. )
  172. rv = value.__html__()
  173. else:
  174. # We need to make sure the format spec is str here as
  175. # otherwise the wrong callback methods are invoked.
  176. rv = string.Formatter.format_field(self, value, str(format_spec))
  177. return str(self.escape(rv))
  178. _ListOrDict = t.TypeVar("_ListOrDict", list, dict)
  179. def _escape_argspec(
  180. obj: _ListOrDict, iterable: t.Iterable[t.Any], escape: t.Callable[[t.Any], Markup]
  181. ) -> _ListOrDict:
  182. """Helper for various string-wrapped functions."""
  183. for key, value in iterable:
  184. if isinstance(value, str) or hasattr(value, "__html__"):
  185. obj[key] = escape(value)
  186. return obj
  187. class _MarkupEscapeHelper:
  188. """Helper for :meth:`Markup.__mod__`."""
  189. __slots__ = ("obj", "escape")
  190. def __init__(self, obj: t.Any, escape: t.Callable[[t.Any], Markup]) -> None:
  191. self.obj = obj
  192. self.escape = escape
  193. def __getitem__(self, item: t.Any) -> "_MarkupEscapeHelper":
  194. return _MarkupEscapeHelper(self.obj[item], self.escape)
  195. def __str__(self) -> str:
  196. return str(self.escape(self.obj))
  197. def __repr__(self) -> str:
  198. return str(self.escape(repr(self.obj)))
  199. def __int__(self) -> int:
  200. return int(self.obj)
  201. def __float__(self) -> float:
  202. return float(self.obj)
  203. # circular import
  204. try:
  205. from ._speedups import escape as escape
  206. from ._speedups import escape_silent as escape_silent
  207. from ._speedups import soft_str as soft_str
  208. from ._speedups import soft_unicode
  209. except ImportError:
  210. from ._native import escape as escape
  211. from ._native import escape_silent as escape_silent # noqa: F401
  212. from ._native import soft_str as soft_str # noqa: F401
  213. from ._native import soft_unicode # noqa: F401