Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/w3lib/_util.py: 55%
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
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
1from __future__ import annotations
3import re
4from functools import lru_cache
5from typing import TYPE_CHECKING
7if TYPE_CHECKING:
8 from collections.abc import Iterable
11def to_unicode(
12 text: str | bytes, encoding: str | None = None, errors: str = "strict"
13) -> str:
14 """Return the unicode representation of a bytes object `text`. If `text`
15 is already an unicode object, return it as-is."""
16 if isinstance(text, str):
17 return text
18 if not isinstance(text, (bytes, str)):
19 raise TypeError(
20 f"to_unicode must receive bytes or str, got {type(text).__name__}"
21 )
22 if encoding is None:
23 encoding = "utf-8"
24 return text.decode(encoding, errors)
27# An HTML comment, up to where it ends or the text does. As in the HTML
28# tokenizer, it ends at the first "-->" or "--!>" after its "<!--", or right
29# away at "<!-->" or "<!--->". Dashes are consumed one at a time only where no
30# closer starts, so there is a single way to match and nothing to backtrack
31# into.
32_COMMENT = r"<!--(?:-?>|[^-]*(?:-(?!-!?>)[^-]*)*(?:--!?>|$))"
34# The text of a tag after its name, up to where it ends or the text does, read
35# attribute by attribute as the HTML tokenizer reads it. Unlike _TAG_BODY in
36# w3lib.html, it consumes "<" as attribute text. Quoted values also consume
37# ">", and an unfinished value runs to the end of input.
38_TAG_SPACE = r"[\t\n\f\r ]"
39_TAG_ATTRIBUTE = (
40 rf"[^\t\n\f\r />][^\t\n\f\r />=]*+{_TAG_SPACE}*+"
41 rf"(?:={_TAG_SPACE}*+(?:\"[^\"]*(?:\"|$)|'[^']*(?:'|$)|[^\t\n\f\r >]*+))?+"
42)
43# Possessive repeats keep attribute names and unquoted values from being split
44# or read again as the scanner advances through a long tag.
45_TAG_REST = rf"(?:{_TAG_SPACE}|/|{_TAG_ATTRIBUTE})*+(?:>|$)"
46# Any start or end tag, whole. A scanner that consumes the tags it does not
47# look for does not read markup written inside them, e.g. the <base> in
48# <a title="<base href=...>">, which is an attribute value to a browser.
49_OTHER_TAG = rf"</?[a-zA-Z][^\t\n\f\r />]*+{_TAG_REST}"
52_ASCII = bytes(range(128))
53_ASCII_TEXT = _ASCII.decode()
56@lru_cache(maxsize=64)
57def _ascii_compatible(encoding: str | None) -> bool:
58 """Return whether ASCII bytes decode to the same ASCII characters under
59 *encoding*.
61 Unknown encodings are reported as not compatible.
62 """
63 try:
64 return _ASCII.decode(encoding or "utf-8", "replace") == _ASCII_TEXT
65 except LookupError:
66 return False
69# Bytes that an encoding can drop when decoding, dividing a character
70# sequence without leaving a trace in the text: ESC ( B redesignates ASCII
71# while ASCII is already in use, and SO and SI shift between character sets.
72_DROPPED = (b"\x1b(B", b"\x0e", b"\x0f")
75@lru_cache(maxsize=64)
76def _scannable(encoding: str | None) -> bool:
77 r"""Return whether every character sequence of a document written in
78 *encoding* is a byte sequence of its undecoded bytes, so that markup that
79 the bytes do not contain is markup that the document does not contain.
81 It takes ASCII compatibility, which makes ASCII characters their own
82 bytes, and an encoding that cannot write those bytes apart, e.g.
83 ``b"<ba\x1b(Bse"`` decodes to ``"<base"`` under ISO-2022-JP.
84 """
85 if not _ascii_compatible(encoding):
86 return False
87 return all(
88 (b"a" + dropped + b"b").decode(encoding or "utf-8", "replace") != "ab"
89 for dropped in _DROPPED
90 )
93# One attribute: a name and, optionally, a value, quoted or not. Each quoting
94# style captures its own group, so a matched value needs no quote stripping.
95_attr_re = re.compile(
96 r"""(?P<name>[^\s<>/=]+) # name
97 (?:\s*=\s*(?:"(?P<double>[^"]*)"|'(?P<single>[^']*)'|(?P<bare>[^\s"'>]*)))?""",
98 re.VERBOSE,
99)
102def iter_tag_attributes(attrs: str) -> Iterable[tuple[str, str]]:
103 """Yield ``(name, value)`` for every attribute with a value in ``attrs``,
104 the text of one tag after its name, in order.
106 ``name`` is lowercased, and ``value`` has its quotes removed, if any.
107 """
108 # finditer() matches one attribute at a time, from where the previous one
109 # ended, so a crafted tag cannot make it backtrack across attributes.
110 for attr in _attr_re.finditer(attrs):
111 # A valueless attribute matches the name group and nothing after it.
112 if attr.lastindex == 1:
113 continue
114 name, double, single, bare = attr.groups()
115 # Exactly one value group matched; the last "" covers all three being
116 # empty, which an empty value in any quoting style produces.
117 yield name.lower(), double or single or bare or ""