Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/turbohtml/migration/markupsafe.py: 48%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

153 statements  

1""" 

2A safe-string for composing HTML, a drop-in for markupsafe's public surface. 

3 

4A :class:`Markup` is text already safe to place in HTML. Combining it with untrusted values escapes those values, so a 

5string built up through ``+``, ``%``, :meth:`Markup.format`, :meth:`Markup.join`, and the inherited ``str`` operations 

6stays safe without a manual escape at each step. ``escape``, ``escape_silent``, and ``soft_str`` are the C entry points 

7from :mod:`turbohtml._html`, bound here unwrapped so template autoescaping stays a single C call. 

8 

9The surface matches markupsafe's so the template stack (Jinja2, WTForms, Werkzeug, ...) can migrate by changing the 

10import: ``Markup`` overrides every ``str`` method that returns text to keep its result a ``Markup``, because under 

11autoescaping a plain ``str`` return would be escaped a second time. The implementation is turbohtml's own, though: the 

12escape runs in C, the overrides call ``str`` methods unbound to skip the ``super()`` proxy, and :meth:`Markup.striptags` 

13and :meth:`Markup.unescape` run on turbohtml's tokenizer and HTML5 reference resolution, which are faster and resolve 

14references markupsafe's regex stripping can miss. 

15""" 

16 

17from __future__ import annotations 

18 

19import string 

20from typing import TYPE_CHECKING, SupportsIndex, cast 

21 

22from turbohtml._html import _collapse_whitespace, _register_markup, parse_fragment, unescape 

23from turbohtml._html import _markup_escape as escape 

24from turbohtml._html import _markup_escape_silent as escape_silent 

25from turbohtml._html import _markup_soft_str as soft_str 

26 

27if TYPE_CHECKING: 

28 from collections.abc import Callable, Iterable, Mapping 

29 from typing import Protocol, Self, SupportsFloat, SupportsInt 

30 

31 class _HasHtml(Protocol): 

32 """An object that renders itself as already-safe HTML.""" 

33 

34 def __html__(self) -> str: ... 

35 

36 class _HasHtmlFormat(Protocol): 

37 """An object that renders itself safely under a format spec.""" 

38 

39 def __html_format__(self, format_spec: str, /) -> str: ... # ruff:ignore[bad-dunder-method-name] # markupsafe render protocol 

40 

41 class _SupportsGetItem(Protocol): 

42 """Any object supporting subscript, the shape a ``%`` mapping operand needs.""" 

43 

44 def __getitem__(self, key: object, /) -> object: ... 

45 

46 class _SupportsStrGetItem(Protocol): 

47 """The mapping shape ``str.format_map`` accepts: a string-keyed ``__getitem__``.""" 

48 

49 def __getitem__(self, key: str, /) -> object: ... 

50 

51 class _SupportsIntGetItem(Protocol): 

52 """The table shape ``str.translate`` accepts: an int-keyed ``__getitem__``.""" 

53 

54 def __getitem__(self, key: int, /) -> str | int | None: ... 

55 

56 

57# A str subclass is required, not error-prone here: escape builds the Markup in C through str's own constructor, and 

58# isinstance(value, str) must hold for the template stack, so FURB189 is suppressed. PLR0904 is suppressed because the 

59# class mirrors str's full method surface on purpose: any text-returning method that fell back to str would have its 

60# already-safe result escaped a second time under autoescaping. 

61class Markup(str): # ruff:ignore[subclass-builtin, too-many-public-methods] 

62 """ 

63 Text that is already safe to embed in HTML and is not escaped again. 

64 

65 Wrap a value to declare it trusted. Operations that combine it with other text escape that text first, so the 

66 result stays safe. Constructing ``Markup`` directly trusts its argument without escaping, so wrap only content you 

67 control or have already escaped; call :meth:`escape` to make untrusted text safe. 

68 

69 :param base: the value to trust as safe HTML; its ``__html__`` method is used when present, and ``bytes`` is 

70 decoded with ``encoding``. 

71 :param encoding: the codec to decode ``base`` with when it is ``bytes``; ``None`` leaves a ``str`` as is. 

72 :param errors: the decoding error policy passed to :meth:`bytes.decode` when ``encoding`` is given. 

73 """ 

74 

75 __slots__ = () 

76 

77 def __new__(cls, base: object = "", encoding: str | None = None, errors: str = "strict") -> Self: 

78 """Trust ``base`` as safe HTML, calling its ``__html__`` if present and decoding bytes when asked.""" 

79 # An exact str (every escape result and str-method wrap) has no __html__, so skip the attribute probe. 

80 if type(base) is not str and hasattr(base, "__html__"): 

81 base = cast("_HasHtml", base).__html__() 

82 if encoding is None: 

83 return str.__new__(cls, base) 

84 return str.__new__(cls, cast("bytes", base), encoding, errors) 

85 

86 def __html__(self) -> Markup: 

87 """Return self, since a Markup already renders as safe HTML.""" 

88 return self 

89 

90 def __add__(self, value: str, /) -> Markup: 

91 """Concatenate, escaping ``value`` so the joined result stays safe.""" 

92 if isinstance(value, str) or hasattr(value, "__html__"): 

93 return self.__class__(str.__add__(self, self.escape(value))) 

94 return NotImplemented 

95 

96 def __radd__(self, value: str, /) -> Markup: 

97 """Concatenate with ``value`` on the left, escaping it so the result stays safe.""" 

98 if isinstance(value, str) or hasattr(value, "__html__"): 

99 return self.escape(value) + self 

100 return NotImplemented 

101 

102 def __mul__(self, count: SupportsIndex, /) -> Markup: 

103 """Repeat the safe string; the repetition stays safe too.""" 

104 return self.__class__(str.__mul__(self, count)) 

105 

106 __rmul__ = __mul__ 

107 

108 def __mod__(self, arg: object, /) -> Markup: 

109 """Interpolate with ``%``, escaping each value as ``%`` substitutes it.""" 

110 if isinstance(arg, tuple): 

111 arg = tuple(_MarkupEscapeHelper(value, self.escape) for value in arg) 

112 elif hasattr(type(arg), "__getitem__") and not isinstance(arg, str): 

113 arg = _MarkupEscapeHelper(arg, self.escape) 

114 else: 

115 arg = (_MarkupEscapeHelper(arg, self.escape),) 

116 return self.__class__(str.__mod__(self, arg)) 

117 

118 def __repr__(self) -> str: 

119 """Wrap the str repr so a safe string reads differently from a plain one in logs and tracebacks.""" 

120 return f"{self.__class__.__name__}({str.__repr__(self)})" 

121 

122 def join(self, iterable: Iterable[str], /) -> Markup: 

123 """ 

124 Join the items, escaping each one so the result stays safe. 

125 

126 :param iterable: the items to join. 

127 :returns: the joined text as a Markup. 

128 """ 

129 return self.__class__(str.join(self, map(self.escape, iterable))) 

130 

131 def split(self, sep: str | None = None, maxsplit: SupportsIndex = -1) -> list[str]: 

132 """Split into parts that stay safe; the list type is str since list is invariant, but each part is a Markup.""" 

133 return [self.__class__(part) for part in str.split(self, sep, maxsplit)] 

134 

135 def rsplit(self, sep: str | None = None, maxsplit: SupportsIndex = -1) -> list[str]: 

136 """Split from the right into parts that each stay a Markup.""" 

137 return [self.__class__(part) for part in str.rsplit(self, sep, maxsplit)] 

138 

139 def splitlines(self, keepends: bool = False) -> list[str]: # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] # keepends is str's own flag 

140 """Split into lines that each stay a Markup.""" 

141 return [self.__class__(line) for line in str.splitlines(self, keepends)] 

142 

143 def unescape(self) -> str: 

144 """ 

145 Resolve character references back to text. 

146 

147 :returns: the resolved text as a plain (no longer safe) :class:`str`. 

148 """ 

149 return unescape(str(self)) 

150 

151 def striptags(self) -> str: 

152 """ 

153 Parse the markup, drop tags and comments, collapse whitespace, and return the plain text. 

154 

155 This parses with turbohtml's tokenizer rather than scanning for ``<``, so references resolve and a comment 

156 that contains a ``<`` cannot end tag removal early. 

157 

158 :returns: the plain text, no longer safe markup. 

159 """ 

160 return _collapse_whitespace(parse_fragment(str(self)).text) 

161 

162 @classmethod 

163 def escape(cls, s: object, /) -> Markup: 

164 """ 

165 Escape a value to this class, the entry point the composing operations use to make operands safe. 

166 

167 :param s: the value to escape; an ``__html__`` method marks it already safe. 

168 :returns: a Markup holding the escaped text. 

169 """ 

170 rv = escape(s) 

171 if rv.__class__ is not cls: 

172 return cls(rv) 

173 return rv 

174 

175 def __getitem__(self, key: SupportsIndex | slice, /) -> Markup: 

176 """Index or slice into safe text; the piece stays a Markup.""" 

177 return self.__class__(str.__getitem__(self, key)) 

178 

179 def capitalize(self) -> Markup: 

180 """Capitalize, keeping the result a Markup so autoescaping does not re-escape it.""" 

181 return self.__class__(str.capitalize(self)) 

182 

183 def title(self) -> Markup: 

184 """Title-case, keeping the result a Markup.""" 

185 return self.__class__(str.title(self)) 

186 

187 def lower(self) -> Markup: 

188 """Lowercase, keeping the result a Markup.""" 

189 return self.__class__(str.lower(self)) 

190 

191 def upper(self) -> Markup: 

192 """Uppercase, keeping the result a Markup.""" 

193 return self.__class__(str.upper(self)) 

194 

195 def swapcase(self) -> Markup: 

196 """Swap case, keeping the result a Markup.""" 

197 return self.__class__(str.swapcase(self)) 

198 

199 def casefold(self) -> Markup: 

200 """Casefold, keeping the result a Markup.""" 

201 return self.__class__(str.casefold(self)) 

202 

203 def replace(self, old: str, new: str, /, count: SupportsIndex = -1) -> Markup: 

204 """Replace occurrences, escaping ``new`` so substituted text stays safe.""" 

205 return self.__class__(str.replace(self, old, self.escape(new), count)) 

206 

207 def ljust(self, width: SupportsIndex, fillchar: str = " ", /) -> Markup: 

208 """Left-justify, escaping the fill character so padding stays safe.""" 

209 return self.__class__(str.ljust(self, width, self.escape(fillchar))) 

210 

211 def rjust(self, width: SupportsIndex, fillchar: str = " ", /) -> Markup: 

212 """Right-justify, escaping the fill character so padding stays safe.""" 

213 return self.__class__(str.rjust(self, width, self.escape(fillchar))) 

214 

215 def center(self, width: SupportsIndex, fillchar: str = " ", /) -> Markup: 

216 """Center, escaping the fill character so padding stays safe.""" 

217 return self.__class__(str.center(self, width, self.escape(fillchar))) 

218 

219 def strip(self, chars: str | None = None, /) -> Markup: 

220 """Strip surrounding characters, keeping the result a Markup.""" 

221 return self.__class__(str.strip(self, chars)) 

222 

223 def lstrip(self, chars: str | None = None, /) -> Markup: 

224 """Strip leading characters, keeping the result a Markup.""" 

225 return self.__class__(str.lstrip(self, chars)) 

226 

227 def rstrip(self, chars: str | None = None, /) -> Markup: 

228 """Strip trailing characters, keeping the result a Markup.""" 

229 return self.__class__(str.rstrip(self, chars)) 

230 

231 def removeprefix(self, prefix: str, /) -> Markup: 

232 """Remove a prefix, keeping the result a Markup.""" 

233 return self.__class__(str.removeprefix(self, prefix)) 

234 

235 def removesuffix(self, suffix: str) -> Markup: 

236 """Remove a suffix, keeping the result a Markup.""" 

237 return self.__class__(str.removesuffix(self, suffix)) 

238 

239 def expandtabs(self, tabsize: SupportsIndex = 8) -> Markup: 

240 """Expand tabs, keeping the result a Markup.""" 

241 return self.__class__(str.expandtabs(self, tabsize)) 

242 

243 def zfill(self, width: SupportsIndex, /) -> Markup: 

244 """Zero-fill, keeping the result a Markup.""" 

245 return self.__class__(str.zfill(self, width)) 

246 

247 def translate(self, table: _SupportsIntGetItem, /) -> Markup: 

248 """Translate through a table, keeping the result a Markup; the caller's table values are trusted.""" 

249 return self.__class__(str.translate(self, table)) 

250 

251 def partition(self, sep: str, /) -> tuple[Markup, Markup, Markup]: 

252 """Partition into three parts that each stay a Markup.""" 

253 left, middle, right = str.partition(self, sep) 

254 return self.__class__(left), self.__class__(middle), self.__class__(right) 

255 

256 def rpartition(self, sep: str, /) -> tuple[Markup, Markup, Markup]: 

257 """Partition from the right into three parts that each stay a Markup.""" 

258 left, middle, right = str.rpartition(self, sep) 

259 return self.__class__(left), self.__class__(middle), self.__class__(right) 

260 

261 def format(self, *args: object, **kwargs: object) -> Markup: 

262 """ 

263 Format, escaping each field unless it renders itself through ``__html_format__`` or ``__html__``. 

264 

265 :param args: positional values for the format fields. 

266 :param kwargs: keyword values for the format fields. 

267 :returns: the formatted text as a Markup. 

268 """ 

269 return self.__class__(EscapeFormatter(self.escape).vformat(self, args, kwargs)) 

270 

271 def format_map(self, mapping: _SupportsStrGetItem, /) -> Markup: 

272 """Format from a mapping, escaping each field the way :meth:`format` does.""" 

273 return self.__class__(EscapeFormatter(self.escape).vformat(self, (), cast("Mapping[str, object]", mapping))) 

274 

275 def __html_format__(self, format_spec: str, /) -> Markup: # ruff:ignore[bad-dunder-method-name] # markupsafe format protocol, not a dunder 

276 """Render under ``format()``: a Markup ignores an empty spec and rejects any other.""" 

277 if format_spec: 

278 msg = "Unsupported format specification for Markup." 

279 raise ValueError(msg) 

280 return self 

281 

282 

283class EscapeFormatter(string.Formatter): 

284 """ 

285 A :class:`string.Formatter` that escapes each field while rendering it. 

286 

287 :meth:`Markup.format` builds on str formatting but must escape interpolated values. A field that already renders 

288 itself safely through ``__html_format__`` or ``__html__`` stays trusted instead of escaping again. The class is 

289 public and subclassable so a template sandbox can mix it with its own formatter, the way Jinja2 does, so its calls 

290 go through ``super()`` to cooperate with that multiple inheritance. 

291 

292 :param escape: the function applied to each interpolated field value to make it safe. 

293 """ 

294 

295 __slots__ = ("escape",) 

296 

297 def __init__(self, escape: Callable[[object], str]) -> None: 

298 """Store the escape callable each field passes through.""" 

299 self.escape = escape 

300 super().__init__() 

301 

302 def format_field(self, value: object, format_spec: str) -> str: 

303 """ 

304 Render one field: trust its safe-rendering protocol if present, else escape the formatted value. 

305 

306 :param value: the field value to render. 

307 :param format_spec: the field's format specification. 

308 :returns: the rendered, escaped field text. 

309 """ 

310 if hasattr(value, "__html_format__"): 

311 rendered = cast("_HasHtmlFormat", value).__html_format__(format_spec) 

312 elif hasattr(value, "__html__"): 

313 if format_spec: 

314 msg = ( 

315 f"Format specifier {format_spec} given, but {type(value)} does not define __html_format__. " 

316 "A class that defines __html__ must define __html_format__ to work with format specifiers." 

317 ) 

318 raise ValueError(msg) 

319 rendered = cast("_HasHtml", value).__html__() # ruff:ignore[unnecessary-dunder-call] # __html__ is a render protocol, not an operator 

320 else: 

321 rendered = super().format_field(value, str(format_spec)) 

322 return str(self.escape(rendered)) 

323 

324 

325class _MarkupEscapeHelper: 

326 """ 

327 Wraps a ``%`` operand and escapes it only when interpolation coerces it. 

328 

329 :meth:`Markup.__mod__` cannot escape eagerly: ``%d``/``%f`` need the raw number and ``%(k)s`` needs item access 

330 first. The proxy defers escaping until str formatting coerces the operand, and forwards indexing and coercion. 

331 """ 

332 

333 def __init__(self, obj: object, escape: Callable[[object], str]) -> None: 

334 """Store the operand and the escape callable coercion applies to it.""" 

335 self.obj = obj 

336 self.escape = escape 

337 

338 def __getitem__(self, key: object) -> _MarkupEscapeHelper: 

339 """Defer escaping through ``%(name)s`` item access, wrapping the looked-up value in turn.""" 

340 return _MarkupEscapeHelper(cast("_SupportsGetItem", self.obj)[key], self.escape) 

341 

342 def __str__(self) -> str: 

343 """Escape the operand at the point ``%s`` coerces it to text.""" 

344 return str(self.escape(self.obj)) 

345 

346 def __repr__(self) -> str: 

347 """Escape the operand's repr at the point ``%r`` coerces it.""" 

348 return str(self.escape(repr(self.obj))) 

349 

350 def __int__(self) -> int: 

351 """Pass the operand through ``%d`` unescaped, since a number carries no markup.""" 

352 return int(cast("SupportsInt", self.obj)) 

353 

354 def __float__(self) -> float: 

355 """Pass the operand through ``%f`` unescaped, since a number carries no markup.""" 

356 return float(cast("SupportsFloat", self.obj)) 

357 

358 

359_register_markup(Markup) 

360 

361__all__ = ["EscapeFormatter", "Markup", "escape", "escape_silent", "soft_str"]