Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/turbohtml/query/_match.py: 94%

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

65 statements  

1""" 

2The soupsieve-shaped CSS matching surface of :mod:`turbohtml.query`, over the native selector engine. 

3 

4`soupsieve <https://facelessuser.github.io/soupsieve/>`__ is BeautifulSoup's CSS selector engine. A bs4 stack reaches 

5it through ``soupsieve.compile(selector)`` (returning a reusable matcher) and the module-level 

6``select``/``select_one``/``iselect``/``match``/``filter``/``closest`` helpers. turbohtml already matches the same CSS 

7through :meth:`~turbohtml.Node.select`/:meth:`~turbohtml.Node.matches`/:meth:`~turbohtml.Node.closest`; this 

8surface wraps those node methods in the soupsieve call shapes so porting a soupsieve (or bs4 ``Tag.select``) codebase is 

9a mechanical import swap rather than a rewrite. It is re-exported from :mod:`turbohtml.query` next to the pyquery-style 

10:class:`~turbohtml.query.Query`, the one namespace for searching a tree. 

11 

12It is a pure-Python facade -- no second engine. :func:`compile` validates the selector once up front (soupsieve 

13compiles eagerly), raising :class:`turbohtml.SelectorSyntaxError` for a malformed selector, and the returned 

14:class:`Matcher` re-runs the native engine per call. The selector entry points on the C core take only the selector 

15string, so the soupsieve ``namespaces`` and ``flags`` arguments are bundled into one immutable :class:`Matching` config 

16that travels 

17with the matcher for API parity; they do not alter match *results* (mirroring soupsieve, whose ``flags`` are advisory 

18and whose namespace discrimination turbohtml does not yet apply -- see the module reference for the limitation). 

19""" 

20 

21from __future__ import annotations 

22 

23from dataclasses import dataclass 

24from typing import TYPE_CHECKING, Final, cast 

25 

26from turbohtml._html import ( 

27 Document, 

28 Element, 

29 _css_escape_identifier, 

30 _matches_many, 

31 _query_children, 

32 _select_limited, 

33 parse, 

34) 

35from turbohtml._internal._selectors import SelectorSyntaxError 

36 

37if TYPE_CHECKING: 

38 from collections.abc import Iterable, Iterator, Mapping 

39 

40#: The soupsieve ``DEBUG`` flag value, re-exported so a port can pass it without importing soupsieve. turbohtml exposes 

41#: no compiled-selector dump, so it is accepted and carried but has no effect. 

42DEBUG: Final = 0x1 

43 

44 

45def escape_identifier(ident: str) -> str: 

46 """ 

47 Escape a string so it is a valid CSS identifier, like ``soupsieve.escape`` / ``CSS.escape``. 

48 

49 Useful for building a selector around a class or id read from data: ``f"#{escape_identifier(raw_id)}"`` is safe even 

50 when ``raw_id`` starts with a digit or holds ``.``/``#``/spaces. 

51 

52 :param ident: the raw identifier text. 

53 :returns: the identifier with CSS-significant characters backslash- or hex-escaped per the CSSOM rules. 

54 """ 

55 return _css_escape_identifier(ident) 

56 

57 

58@dataclass(frozen=True) 

59class Matching: 

60 """ 

61 The soupsieve ``namespaces`` and ``flags`` arguments, bundled into one immutable, thread-safe config. 

62 

63 Pass it as the ``options`` of :func:`compile` (or any module-level helper) to mirror a soupsieve call. Both fields 

64 are carried for API parity and do not change which elements match: turbohtml selects by an element's local name, so 

65 a prefixed type selector (``svg|rect``) matches every ``rect`` regardless of the ``namespaces`` mapping, and 

66 ``flags`` is advisory as in soupsieve. ``Matching()`` reproduces soupsieve's default HTML matching. 

67 

68 :param namespaces: prefix-to-URI map for prefixed type selectors, accepted for soupsieve parity; the native engine 

69 does not discriminate by namespace URI (see the module reference). 

70 :param flags: the soupsieve flag bitmask (only :data:`DEBUG` is defined); accepted and carried but inert. 

71 """ 

72 

73 namespaces: Mapping[str, str] | None = None 

74 flags: int = 0 

75 

76 @classmethod 

77 def soupsieve(cls, namespaces: Mapping[str, str] | None = None, flags: int = 0) -> Matching: 

78 """ 

79 Build a config straight from soupsieve's ``(namespaces, flags)`` call convention. 

80 

81 Lets a port keep the original argument list -- ``Matching.soupsieve(namespaces=ns, flags=DEBUG)`` -- rather 

82 than renaming keywords at every call site. 

83 

84 :param namespaces: the soupsieve ``namespaces`` mapping. 

85 :param flags: the soupsieve ``flags`` bitmask. 

86 :returns: the equivalent :class:`Matching`. 

87 """ 

88 return cls(namespaces=namespaces, flags=flags) 

89 

90 

91_DEFAULT: Final = Matching() 

92# A throwaway element to validate selector syntax at compile time; matching against it parses the selector, raising on 

93# a malformed one, without needing the caller's node. 

94_PROBE: Final = cast("Element", parse("<x></x>").root) 

95 

96 

97class Matcher: 

98 """ 

99 A selector compiled against turbohtml's engine, with the soupsieve matcher methods. 

100 

101 Build one with :func:`compile` and reuse it; it is immutable and thread-safe. Each method runs the native selector 

102 engine over the node passed in, so one :class:`Matcher` serves many trees. 

103 

104 :param selector: the CSS selector; validated immediately, raising :class:`turbohtml.SelectorSyntaxError` when 

105 malformed. 

106 :param options: the :class:`Matching` config, or ``None`` for soupsieve's defaults. 

107 """ 

108 

109 def __init__(self, selector: str, options: Matching | None = None, /) -> None: 

110 """Validate the selector (a bad one raises :class:`turbohtml.SelectorSyntaxError`) and store it.""" 

111 _PROBE.matches(selector) 

112 self._selector = selector 

113 self._options = options if options is not None else _DEFAULT 

114 

115 @property 

116 def pattern(self) -> str: 

117 """The selector string, named as soupsieve's ``SoupSieve.pattern``.""" 

118 return self._selector 

119 

120 @property 

121 def namespaces(self) -> Mapping[str, str] | None: 

122 """The configured namespace map, named as soupsieve's ``SoupSieve.namespaces``.""" 

123 return self._options.namespaces 

124 

125 @property 

126 def flags(self) -> int: 

127 """The configured flag bitmask, named as soupsieve's ``SoupSieve.flags``.""" 

128 return self._options.flags 

129 

130 def match(self, node: Element) -> bool: 

131 """ 

132 Test whether one element matches the selector. 

133 

134 :param node: the element to test. 

135 :returns: whether it matches. 

136 """ 

137 return node.matches(self._selector) 

138 

139 def select(self, node: Element | Document, limit: int = 0) -> list[Element]: 

140 """ 

141 Collect the descendants of an element that match the selector, in document order. 

142 

143 :param node: the element or document whose descendants are searched. 

144 :param limit: the most matches to return, or ``0`` for all. 

145 :returns: the matching descendants. 

146 """ 

147 return _select_limited(node, self._selector, limit) 

148 

149 def select_one(self, node: Element | Document) -> Element | None: 

150 """ 

151 Return the first descendant of an element that matches the selector. 

152 

153 :param node: the element or document whose descendants are searched. 

154 :returns: the first match in document order, or ``None``. 

155 """ 

156 return node.select_one(self._selector) 

157 

158 def iselect(self, node: Element | Document, limit: int = 0) -> Iterator[Element]: 

159 """ 

160 Iterate the descendants of an element that match the selector, in document order. 

161 

162 :param node: the element or document whose descendants are searched. 

163 :param limit: the most matches to yield, or ``0`` for all. 

164 :returns: an iterator over the matching descendants. 

165 """ 

166 yield from self.select(node, limit) 

167 

168 def filter(self, iterable: Element | Document | Iterable[Element]) -> list[Element]: 

169 """ 

170 Keep the members of an iterable that match the selector. 

171 

172 :param iterable: elements to test, or one element or document whose direct element children are tested 

173 (soupsieve's rule for a single node). 

174 :returns: the members that match. 

175 """ 

176 if isinstance(iterable, (Element, Document)): 

177 return _matches_many(_query_children([iterable]), self._selector) 

178 return _matches_many(list(iterable), self._selector) 

179 

180 def closest(self, node: Element) -> Element | None: 

181 """ 

182 Return the nearest element at or above one that matches the selector. 

183 

184 :param node: the element to walk up from. 

185 :returns: the nearest self-or-ancestor that matches, or ``None``. 

186 """ 

187 return node.closest(self._selector) 

188 

189 

190def compile(selector: str, options: Matching | None = None, /) -> Matcher: # ruff:ignore[builtin-variable-shadowing] # the soupsieve entry-point name 

191 """ 

192 Compile a CSS selector into a reusable :class:`Matcher`, like ``soupsieve.compile``. 

193 

194 The selector is validated up front, so a malformed one raises :class:`turbohtml.SelectorSyntaxError` here, not at 

195 the first match. 

196 

197 :param selector: the CSS selector. 

198 :param options: the :class:`Matching` config, or ``None`` for soupsieve's defaults. 

199 :returns: a matcher bound to the selector. 

200 """ 

201 return Matcher(selector, options) 

202 

203 

204def css(selector: str, options: Matching | None = None, /) -> Matcher: 

205 """ 

206 Compile a CSS selector into a :class:`Matcher`; a readable alias of :func:`compile`. 

207 

208 :param selector: the CSS selector. 

209 :param options: the :class:`Matching` config, or ``None`` for soupsieve's defaults. 

210 :returns: a matcher bound to the selector. 

211 """ 

212 return Matcher(selector, options) 

213 

214 

215def match(selector: str, node: Element, options: Matching | None = None, /) -> bool: 

216 """ 

217 Test whether an element matches a selector, compiling it for this one call. 

218 

219 :param selector: the CSS selector. 

220 :param node: the element to test. 

221 :param options: the :class:`Matching` config, or ``None`` for soupsieve's defaults. 

222 :returns: whether the element matches. 

223 """ 

224 return Matcher(selector, options).match(node) 

225 

226 

227def select( 

228 selector: str, node: Element | Document, options: Matching | None = None, /, *, limit: int = 0 

229) -> list[Element]: 

230 """ 

231 Collect the descendants of an element matching a selector, compiling it for this one call. 

232 

233 :param selector: the CSS selector. 

234 :param node: the element or document whose descendants are searched. 

235 :param options: the :class:`Matching` config, or ``None`` for soupsieve's defaults. 

236 :param limit: the most matches to return, or ``0`` for all. 

237 :returns: the matching descendants in document order. 

238 """ 

239 return Matcher(selector, options).select(node, limit) 

240 

241 

242def select_one(selector: str, node: Element | Document, options: Matching | None = None, /) -> Element | None: 

243 """ 

244 Return the first descendant of an element matching a selector, compiling it for this one call. 

245 

246 :param selector: the CSS selector. 

247 :param node: the element or document whose descendants are searched. 

248 :param options: the :class:`Matching` config, or ``None`` for soupsieve's defaults. 

249 :returns: the first match in document order, or ``None``. 

250 """ 

251 return Matcher(selector, options).select_one(node) 

252 

253 

254def iselect( 

255 selector: str, node: Element | Document, options: Matching | None = None, /, *, limit: int = 0 

256) -> Iterator[Element]: 

257 """ 

258 Iterate the descendants of an element matching a selector, compiling it for this one call. 

259 

260 :param selector: the CSS selector. 

261 :param node: the element or document whose descendants are searched. 

262 :param options: the :class:`Matching` config, or ``None`` for soupsieve's defaults. 

263 :param limit: the most matches to yield, or ``0`` for all. 

264 :returns: an iterator over the matching descendants. 

265 """ 

266 return Matcher(selector, options).iselect(node, limit) 

267 

268 

269def filter( # ruff:ignore[builtin-variable-shadowing] # the soupsieve entry-point name 

270 selector: str, iterable: Element | Document | Iterable[Element], options: Matching | None = None, / 

271) -> list[Element]: 

272 """ 

273 Keep the members of an iterable matching a selector, compiling it for this one call. 

274 

275 :param selector: the CSS selector. 

276 :param iterable: elements to test, or one element or document whose direct element children are tested. 

277 :param options: the :class:`Matching` config, or ``None`` for soupsieve's defaults. 

278 :returns: the members that match. 

279 """ 

280 return Matcher(selector, options).filter(iterable) 

281 

282 

283def closest(selector: str, node: Element, options: Matching | None = None, /) -> Element | None: 

284 """ 

285 Return the nearest self-or-ancestor of an element matching a selector, compiling it for this one call. 

286 

287 :param selector: the CSS selector. 

288 :param node: the element to walk up from. 

289 :param options: the :class:`Matching` config, or ``None`` for soupsieve's defaults. 

290 :returns: the nearest matching element, or ``None``. 

291 """ 

292 return Matcher(selector, options).closest(node) 

293 

294 

295__all__ = [ 

296 "DEBUG", 

297 "Matcher", 

298 "Matching", 

299 "SelectorSyntaxError", 

300 "closest", 

301 "compile", 

302 "css", 

303 "escape_identifier", 

304 "filter", 

305 "iselect", 

306 "match", 

307 "select", 

308 "select_one", 

309]