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

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

93 statements  

1""" 

2turbohtml.query: search a tree, the one namespace for querying nodes. 

3 

4Two facades over the same native CSS and XPath engines sit here. :class:`Query` is a pyquery-style fluent, chainable 

5wrapper for code migrating off `pyquery <https://github.com/gawel/pyquery>`_'s jQuery-style chaining: it wraps an 

6ordered, duplicate-free set of elements and every traversal and mutation method returns a :class:`Query`, so calls 

7compose (``Query(html)("div").find("a").filter(".x").eq(0).attr("href")``). Alongside it, the soupsieve-shaped surface 

8(:func:`compile`, :class:`Matcher`, the module-level :func:`select`/:func:`select_one`/:func:`iselect`/:func:`match`/ 

9:func:`filter`/:func:`closest`, :func:`escape_identifier`, and the :class:`Matching` config) mirrors soupsieve's call 

10shapes for a BeautifulSoup port -- see :mod:`turbohtml.query._match` for the detail. 

11 

12Both are kept out of the core API (which is one-name-per-concept and not chainable) so the extra shapes stay optional. 

13The :class:`Query` method names are turbohtml's own -- ``add_class`` rather than pyquery's ``addClass`` -- so a port 

14adjusts the spelling but keeps the structure. 

15""" 

16 

17from __future__ import annotations 

18 

19from typing import TYPE_CHECKING, cast, overload 

20 

21from turbohtml._html import ( 

22 Document, 

23 Element, 

24 _matches_many, 

25 _query_add_class, 

26 _query_attr, 

27 _query_children, 

28 _query_closest, 

29 _query_has_class, 

30 _query_parents, 

31 _query_remove_class, 

32 _query_siblings, 

33 _query_text, 

34 _query_toggle_class, 

35 _query_unique, 

36 _select_many, 

37 parse, 

38) 

39 

40from ._match import ( 

41 DEBUG, 

42 Matcher, 

43 Matching, 

44 SelectorSyntaxError, 

45 closest, 

46 compile, # ruff:ignore[builtin-import-shadowing] # the soupsieve entry-point name 

47 css, 

48 escape_identifier, 

49 filter, # ruff:ignore[builtin-import-shadowing] # the soupsieve entry-point name 

50 iselect, 

51 match, 

52 select, 

53 select_one, 

54) 

55 

56if TYPE_CHECKING: 

57 from collections.abc import Iterable, Iterator 

58 

59__all__ = [ 

60 "DEBUG", 

61 "Matcher", 

62 "Matching", 

63 "Query", 

64 "SelectorSyntaxError", 

65 "closest", 

66 "compile", 

67 "css", 

68 "escape_identifier", 

69 "filter", 

70 "iselect", 

71 "match", 

72 "select", 

73 "select_one", 

74] 

75 

76 

77class Query: # ruff:ignore[too-many-public-methods] # a fluent wrapper mirrors pyquery's broad chainable method set 

78 """ 

79 An ordered, duplicate-free set of elements with chainable traversal and mutation. 

80 

81 :param source: HTML to parse, a single Element or Document, or an iterable of Elements. 

82 """ 

83 

84 def __init__(self, source: str | Element | Document | Iterable[Element]) -> None: 

85 """Wrap a source into a query set.""" 

86 if isinstance(source, str): 

87 source = parse(source) 

88 if isinstance(source, Document): 

89 # a parsed document always has an <html> root, though the type admits None 

90 self._nodes: list[Element] = [cast("Element", source.root)] 

91 elif isinstance(source, Element): 

92 self._nodes = [source] 

93 else: 

94 self._nodes = _query_unique(source) 

95 

96 @classmethod 

97 def _wrap(cls, nodes: list[Element]) -> Query: 

98 """Build a query around an already-unique, document-ordered node list, skipping the dedupe.""" 

99 query = cls.__new__(cls) 

100 query._nodes = nodes # ruff:ignore[private-member-access] # a same-class factory setting its own private attribute 

101 return query 

102 

103 def __call__(self, selector: str) -> Query: 

104 """ 

105 Select the descendants of the wrapped elements matching a selector, like ``find``. 

106 

107 :param selector: the CSS selector. 

108 :returns: a query over the matching descendants. 

109 """ 

110 return self.find(selector) 

111 

112 def find(self, selector: str) -> Query: 

113 """ 

114 Select every descendant of the wrapped elements matching a selector. 

115 

116 :param selector: the CSS selector. 

117 :returns: a query over the matching descendants in document order. 

118 """ 

119 if len(self._nodes) == 1: 

120 # the C select() already returns unique results in document order 

121 return Query._wrap(self._nodes[0].select(selector)) 

122 return Query._wrap(_select_many(self._nodes, selector)) 

123 

124 def filter(self, selector: str) -> Query: 

125 """ 

126 Keep the wrapped elements that themselves match a selector. 

127 

128 :param selector: the CSS selector. 

129 :returns: a query over the elements that match. 

130 """ 

131 return Query._wrap(_matches_many(self._nodes, selector)) 

132 

133 def eq(self, index: int) -> Query: 

134 """ 

135 Reduce the set to the element at one index. 

136 

137 :param index: the position in the set. 

138 :returns: a query over that element, or an empty query when the index is out of range. 

139 """ 

140 try: 

141 return Query([self._nodes[index]]) 

142 except IndexError: 

143 return Query([]) 

144 

145 def parent(self) -> Query: 

146 """ 

147 Select the immediate parent of each wrapped element. 

148 

149 :returns: a query over the parent elements. 

150 """ 

151 return Query._wrap(_query_parents(self._nodes)) # a same-class factory 

152 

153 def children(self, selector: str | None = None) -> Query: 

154 """ 

155 Select the element children of each wrapped element. 

156 

157 :param selector: an optional CSS selector to keep only matching children. 

158 :returns: a query over the child elements. 

159 """ 

160 result = Query._wrap(_query_children(self._nodes)) # a same-class factory 

161 return result if selector is None else result.filter(selector) 

162 

163 def siblings(self, selector: str | None = None) -> Query: 

164 """ 

165 Select the sibling elements of each wrapped element. 

166 

167 :param selector: an optional CSS selector to keep only matching siblings. 

168 :returns: a query over the sibling elements. 

169 """ 

170 result = Query._wrap(_query_siblings(self._nodes)) # a same-class factory 

171 return result if selector is None else result.filter(selector) 

172 

173 def closest(self, selector: str) -> Query: 

174 """ 

175 Select the nearest self-or-ancestor of each wrapped element matching a selector. 

176 

177 :param selector: the CSS selector. 

178 :returns: a query over the nearest matching elements. 

179 """ 

180 return Query._wrap(_query_closest(self._nodes, selector)) # a same-class factory 

181 

182 def items(self) -> Iterator[Query]: 

183 """ 

184 Iterate the wrapped elements, each as its own single-element query. 

185 

186 :returns: an iterator of single-element queries. 

187 """ 

188 for node in self._nodes: 

189 yield Query([node]) 

190 

191 @overload 

192 def attr(self, name: str) -> str | None: ... 

193 @overload 

194 def attr(self, name: str, value: str) -> Query: ... 

195 def attr(self, name: str, value: str | None = None) -> str | Query | None: 

196 """ 

197 Get an attribute from the first element, or set it on every element. 

198 

199 :param name: the attribute name. 

200 :param value: the value to set; omit to read instead. 

201 :returns: the attribute value when reading, or the query when setting. 

202 """ 

203 if value is None: 

204 return _query_attr(self._nodes, name) 

205 for node in self._nodes: 

206 node.attrs[name] = value 

207 return self 

208 

209 @overload 

210 def text(self) -> str: ... 

211 @overload 

212 def text(self, value: str) -> Query: ... 

213 def text(self, value: str | None = None) -> str | Query: 

214 """ 

215 Get the combined text of every element, or set each element's text. 

216 

217 :param value: the text to set; omit to read instead. 

218 :returns: the combined text when reading, or the query when setting. 

219 """ 

220 if value is None: 

221 return _query_text(self._nodes) 

222 for node in self._nodes: 

223 node.text = value 

224 return self 

225 

226 def html(self) -> str | None: 

227 """ 

228 Return the inner HTML of the first element. 

229 

230 :returns: the first element's inner HTML, or None when the set is empty. 

231 """ 

232 return self._nodes[0].inner_html if self._nodes else None 

233 

234 def has_class(self, name: str) -> bool: 

235 """ 

236 Test the wrapped elements for a class. 

237 

238 :param name: the class name. 

239 :returns: whether any wrapped element carries the class. 

240 """ 

241 return _query_has_class(self._nodes, name) 

242 

243 def add_class(self, name: str) -> Query: 

244 """ 

245 Add a class to every wrapped element. 

246 

247 :param name: the class name to add. 

248 :returns: the query. 

249 """ 

250 _query_add_class(self._nodes, name) 

251 return self 

252 

253 def remove_class(self, name: str) -> Query: 

254 """ 

255 Remove a class from every wrapped element. 

256 

257 :param name: the class name to remove. 

258 :returns: the query. 

259 """ 

260 _query_remove_class(self._nodes, name) 

261 return self 

262 

263 def toggle_class(self, name: str) -> Query: 

264 """ 

265 Toggle a class on each element, adding it where absent and removing it where present. 

266 

267 :param name: the class name to toggle. 

268 :returns: the query. 

269 """ 

270 _query_toggle_class(self._nodes, name) 

271 return self 

272 

273 def __iter__(self) -> Iterator[Element]: 

274 """Iterate the wrapped elements.""" 

275 return iter(self._nodes) 

276 

277 def __len__(self) -> int: 

278 """Return the number of wrapped elements.""" 

279 return len(self._nodes) 

280 

281 def __getitem__(self, index: int) -> Element: 

282 """Return the wrapped element at the given index.""" 

283 return self._nodes[index] 

284 

285 def __eq__(self, other: object) -> bool: 

286 """Return whether other is a query wrapping the same elements in the same order.""" 

287 return isinstance(other, Query) and self._nodes == other._nodes 

288 

289 def __hash__(self) -> int: 

290 """Hash the wrapped elements by node identity.""" 

291 return hash(tuple(self._nodes)) 

292 

293 def __repr__(self) -> str: 

294 """Return a representation listing the wrapped elements' tags.""" 

295 return f"Query({[node.tag for node in self._nodes]!r})"