Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1import os 

2import warnings 

3from functools import lru_cache 

4from typing import Any 

5from typing import Dict 

6from typing import List 

7from typing import Optional 

8from typing import Set 

9from typing import Tuple 

10from typing import Union 

11 

12import py 

13 

14import _pytest._code 

15from _pytest._code.code import ExceptionChainRepr 

16from _pytest._code.code import ExceptionInfo 

17from _pytest._code.code import ReprExceptionInfo 

18from _pytest.compat import cached_property 

19from _pytest.compat import getfslineno 

20from _pytest.compat import TYPE_CHECKING 

21from _pytest.config import Config 

22from _pytest.fixtures import FixtureDef 

23from _pytest.fixtures import FixtureLookupError 

24from _pytest.fixtures import FixtureLookupErrorRepr 

25from _pytest.mark.structures import Mark 

26from _pytest.mark.structures import MarkDecorator 

27from _pytest.mark.structures import NodeKeywords 

28from _pytest.outcomes import Failed 

29 

30if TYPE_CHECKING: 

31 # Imported here due to circular import. 

32 from _pytest.main import Session # noqa: F401 

33 

34SEP = "/" 

35 

36tracebackcutdir = py.path.local(_pytest.__file__).dirpath() 

37 

38 

39@lru_cache(maxsize=None) 

40def _splitnode(nodeid): 

41 """Split a nodeid into constituent 'parts'. 

42 

43 Node IDs are strings, and can be things like: 

44 '' 

45 'testing/code' 

46 'testing/code/test_excinfo.py' 

47 'testing/code/test_excinfo.py::TestFormattedExcinfo' 

48 

49 Return values are lists e.g. 

50 [] 

51 ['testing', 'code'] 

52 ['testing', 'code', 'test_excinfo.py'] 

53 ['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo'] 

54 """ 

55 if nodeid == "": 

56 # If there is no root node at all, return an empty list so the caller's logic can remain sane 

57 return () 

58 parts = nodeid.split(SEP) 

59 # Replace single last element 'test_foo.py::Bar' with multiple elements 'test_foo.py', 'Bar' 

60 parts[-1:] = parts[-1].split("::") 

61 # Convert parts into a tuple to avoid possible errors with caching of a mutable type 

62 return tuple(parts) 

63 

64 

65def ischildnode(baseid, nodeid): 

66 """Return True if the nodeid is a child node of the baseid. 

67 

68 E.g. 'foo/bar::Baz' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz', but not of 'foo/blorp' 

69 """ 

70 base_parts = _splitnode(baseid) 

71 node_parts = _splitnode(nodeid) 

72 if len(node_parts) < len(base_parts): 

73 return False 

74 return node_parts[: len(base_parts)] == base_parts 

75 

76 

77class Node: 

78 """ base class for Collector and Item the test collection tree. 

79 Collector subclasses have children, Items are terminal nodes.""" 

80 

81 def __init__( 

82 self, 

83 name: str, 

84 parent: Optional["Node"] = None, 

85 config: Optional[Config] = None, 

86 session: Optional["Session"] = None, 

87 fspath: Optional[py.path.local] = None, 

88 nodeid: Optional[str] = None, 

89 ) -> None: 

90 #: a unique name within the scope of the parent node 

91 self.name = name 

92 

93 #: the parent collector node. 

94 self.parent = parent 

95 

96 #: the pytest config object 

97 if config: 

98 self.config = config 

99 else: 

100 if not parent: 

101 raise TypeError("config or parent must be provided") 

102 self.config = parent.config 

103 

104 #: the session this node is part of 

105 if session: 

106 self.session = session 

107 else: 

108 if not parent: 

109 raise TypeError("session or parent must be provided") 

110 self.session = parent.session 

111 

112 #: filesystem path where this node was collected from (can be None) 

113 self.fspath = fspath or getattr(parent, "fspath", None) 

114 

115 #: keywords/markers collected from all scopes 

116 self.keywords = NodeKeywords(self) 

117 

118 #: the marker objects belonging to this node 

119 self.own_markers = [] # type: List[Mark] 

120 

121 #: allow adding of extra keywords to use for matching 

122 self.extra_keyword_matches = set() # type: Set[str] 

123 

124 # used for storing artificial fixturedefs for direct parametrization 

125 self._name2pseudofixturedef = {} # type: Dict[str, FixtureDef] 

126 

127 if nodeid is not None: 

128 assert "::()" not in nodeid 

129 self._nodeid = nodeid 

130 else: 

131 if not self.parent: 

132 raise TypeError("nodeid or parent must be provided") 

133 self._nodeid = self.parent.nodeid 

134 if self.name != "()": 

135 self._nodeid += "::" + self.name 

136 

137 @property 

138 def ihook(self): 

139 """ fspath sensitive hook proxy used to call pytest hooks""" 

140 return self.session.gethookproxy(self.fspath) 

141 

142 def __repr__(self): 

143 return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None)) 

144 

145 def warn(self, warning): 

146 """Issue a warning for this item. 

147 

148 Warnings will be displayed after the test session, unless explicitly suppressed 

149 

150 :param Warning warning: the warning instance to issue. Must be a subclass of PytestWarning. 

151 

152 :raise ValueError: if ``warning`` instance is not a subclass of PytestWarning. 

153 

154 Example usage: 

155 

156 .. code-block:: python 

157 

158 node.warn(PytestWarning("some message")) 

159 

160 """ 

161 from _pytest.warning_types import PytestWarning 

162 

163 if not isinstance(warning, PytestWarning): 

164 raise ValueError( 

165 "warning must be an instance of PytestWarning or subclass, got {!r}".format( 

166 warning 

167 ) 

168 ) 

169 path, lineno = get_fslocation_from_item(self) 

170 warnings.warn_explicit( 

171 warning, 

172 category=None, 

173 filename=str(path), 

174 lineno=lineno + 1 if lineno is not None else None, 

175 ) 

176 

177 # methods for ordering nodes 

178 @property 

179 def nodeid(self): 

180 """ a ::-separated string denoting its collection tree address. """ 

181 return self._nodeid 

182 

183 def __hash__(self): 

184 return hash(self.nodeid) 

185 

186 def setup(self): 

187 pass 

188 

189 def teardown(self): 

190 pass 

191 

192 def listchain(self): 

193 """ return list of all parent collectors up to self, 

194 starting from root of collection tree. """ 

195 chain = [] 

196 item = self # type: Optional[Node] 

197 while item is not None: 

198 chain.append(item) 

199 item = item.parent 

200 chain.reverse() 

201 return chain 

202 

203 def add_marker( 

204 self, marker: Union[str, MarkDecorator], append: bool = True 

205 ) -> None: 

206 """dynamically add a marker object to the node. 

207 

208 :type marker: ``str`` or ``pytest.mark.*`` object 

209 :param marker: 

210 ``append=True`` whether to append the marker, 

211 if ``False`` insert at position ``0``. 

212 """ 

213 from _pytest.mark import MARK_GEN 

214 

215 if isinstance(marker, MarkDecorator): 

216 marker_ = marker 

217 elif isinstance(marker, str): 

218 marker_ = getattr(MARK_GEN, marker) 

219 else: 

220 raise ValueError("is not a string or pytest.mark.* Marker") 

221 self.keywords[marker_.name] = marker 

222 if append: 

223 self.own_markers.append(marker_.mark) 

224 else: 

225 self.own_markers.insert(0, marker_.mark) 

226 

227 def iter_markers(self, name=None): 

228 """ 

229 :param name: if given, filter the results by the name attribute 

230 

231 iterate over all markers of the node 

232 """ 

233 return (x[1] for x in self.iter_markers_with_node(name=name)) 

234 

235 def iter_markers_with_node(self, name=None): 

236 """ 

237 :param name: if given, filter the results by the name attribute 

238 

239 iterate over all markers of the node 

240 returns sequence of tuples (node, mark) 

241 """ 

242 for node in reversed(self.listchain()): 

243 for mark in node.own_markers: 

244 if name is None or getattr(mark, "name", None) == name: 

245 yield node, mark 

246 

247 def get_closest_marker(self, name, default=None): 

248 """return the first marker matching the name, from closest (for example function) to farther level (for example 

249 module level). 

250 

251 :param default: fallback return value of no marker was found 

252 :param name: name to filter by 

253 """ 

254 return next(self.iter_markers(name=name), default) 

255 

256 def listextrakeywords(self): 

257 """ Return a set of all extra keywords in self and any parents.""" 

258 extra_keywords = set() # type: Set[str] 

259 for item in self.listchain(): 

260 extra_keywords.update(item.extra_keyword_matches) 

261 return extra_keywords 

262 

263 def listnames(self): 

264 return [x.name for x in self.listchain()] 

265 

266 def addfinalizer(self, fin): 

267 """ register a function to be called when this node is finalized. 

268 

269 This method can only be called when this node is active 

270 in a setup chain, for example during self.setup(). 

271 """ 

272 self.session._setupstate.addfinalizer(fin, self) 

273 

274 def getparent(self, cls): 

275 """ get the next parent node (including ourself) 

276 which is an instance of the given class""" 

277 current = self # type: Optional[Node] 

278 while current and not isinstance(current, cls): 

279 current = current.parent 

280 return current 

281 

282 def _prunetraceback(self, excinfo): 

283 pass 

284 

285 def _repr_failure_py( 

286 self, excinfo: ExceptionInfo[Union[Failed, FixtureLookupError]], style=None 

287 ) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]: 

288 if isinstance(excinfo.value, Failed): 

289 if not excinfo.value.pytrace: 

290 return str(excinfo.value) 

291 if isinstance(excinfo.value, FixtureLookupError): 

292 return excinfo.value.formatrepr() 

293 if self.config.getoption("fulltrace", False): 

294 style = "long" 

295 else: 

296 tb = _pytest._code.Traceback([excinfo.traceback[-1]]) 

297 self._prunetraceback(excinfo) 

298 if len(excinfo.traceback) == 0: 

299 excinfo.traceback = tb 

300 if style == "auto": 

301 style = "long" 

302 # XXX should excinfo.getrepr record all data and toterminal() process it? 

303 if style is None: 

304 if self.config.getoption("tbstyle", "auto") == "short": 

305 style = "short" 

306 else: 

307 style = "long" 

308 

309 if self.config.getoption("verbose", 0) > 1: 

310 truncate_locals = False 

311 else: 

312 truncate_locals = True 

313 

314 try: 

315 os.getcwd() 

316 abspath = False 

317 except OSError: 

318 abspath = True 

319 

320 return excinfo.getrepr( 

321 funcargs=True, 

322 abspath=abspath, 

323 showlocals=self.config.getoption("showlocals", False), 

324 style=style, 

325 tbfilter=False, # pruned already, or in --fulltrace mode. 

326 truncate_locals=truncate_locals, 

327 ) 

328 

329 def repr_failure( 

330 self, excinfo, style=None 

331 ) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]: 

332 return self._repr_failure_py(excinfo, style) 

333 

334 

335def get_fslocation_from_item(item): 

336 """Tries to extract the actual location from an item, depending on available attributes: 

337 

338 * "fslocation": a pair (path, lineno) 

339 * "obj": a Python object that the item wraps. 

340 * "fspath": just a path 

341 

342 :rtype: a tuple of (str|LocalPath, int) with filename and line number. 

343 """ 

344 result = getattr(item, "location", None) 

345 if result is not None: 

346 return result[:2] 

347 obj = getattr(item, "obj", None) 

348 if obj is not None: 

349 return getfslineno(obj) 

350 return getattr(item, "fspath", "unknown location"), -1 

351 

352 

353class Collector(Node): 

354 """ Collector instances create children through collect() 

355 and thus iteratively build a tree. 

356 """ 

357 

358 class CollectError(Exception): 

359 """ an error during collection, contains a custom message. """ 

360 

361 def collect(self): 

362 """ returns a list of children (items and collectors) 

363 for this collection node. 

364 """ 

365 raise NotImplementedError("abstract") 

366 

367 def repr_failure(self, excinfo): 

368 """ represent a collection failure. """ 

369 if excinfo.errisinstance(self.CollectError): 

370 exc = excinfo.value 

371 return str(exc.args[0]) 

372 

373 # Respect explicit tbstyle option, but default to "short" 

374 # (None._repr_failure_py defaults to "long" without "fulltrace" option). 

375 tbstyle = self.config.getoption("tbstyle", "auto") 

376 if tbstyle == "auto": 

377 tbstyle = "short" 

378 

379 return self._repr_failure_py(excinfo, style=tbstyle) 

380 

381 def _prunetraceback(self, excinfo): 

382 if hasattr(self, "fspath"): 

383 traceback = excinfo.traceback 

384 ntraceback = traceback.cut(path=self.fspath) 

385 if ntraceback == traceback: 

386 ntraceback = ntraceback.cut(excludepath=tracebackcutdir) 

387 excinfo.traceback = ntraceback.filter() 

388 

389 

390def _check_initialpaths_for_relpath(session, fspath): 

391 for initial_path in session._initialpaths: 

392 if fspath.common(initial_path) == initial_path: 

393 return fspath.relto(initial_path) 

394 

395 

396class FSCollector(Collector): 

397 def __init__( 

398 self, fspath: py.path.local, parent=None, config=None, session=None, nodeid=None 

399 ) -> None: 

400 name = fspath.basename 

401 if parent is not None: 

402 rel = fspath.relto(parent.fspath) 

403 if rel: 

404 name = rel 

405 name = name.replace(os.sep, SEP) 

406 self.fspath = fspath 

407 

408 session = session or parent.session 

409 

410 if nodeid is None: 

411 nodeid = self.fspath.relto(session.config.rootdir) 

412 

413 if not nodeid: 

414 nodeid = _check_initialpaths_for_relpath(session, fspath) 

415 if nodeid and os.sep != SEP: 

416 nodeid = nodeid.replace(os.sep, SEP) 

417 

418 super().__init__(name, parent, config, session, nodeid=nodeid, fspath=fspath) 

419 

420 

421class File(FSCollector): 

422 """ base class for collecting tests from a file. """ 

423 

424 

425class Item(Node): 

426 """ a basic test invocation item. Note that for a single function 

427 there might be multiple test invocation items. 

428 """ 

429 

430 nextitem = None 

431 

432 def __init__(self, name, parent=None, config=None, session=None, nodeid=None): 

433 super().__init__(name, parent, config, session, nodeid=nodeid) 

434 self._report_sections = [] # type: List[Tuple[str, str, str]] 

435 

436 #: user properties is a list of tuples (name, value) that holds user 

437 #: defined properties for this test. 

438 self.user_properties = [] # type: List[Tuple[str, Any]] 

439 

440 def add_report_section(self, when: str, key: str, content: str) -> None: 

441 """ 

442 Adds a new report section, similar to what's done internally to add stdout and 

443 stderr captured output:: 

444 

445 item.add_report_section("call", "stdout", "report section contents") 

446 

447 :param str when: 

448 One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``. 

449 :param str key: 

450 Name of the section, can be customized at will. Pytest uses ``"stdout"`` and 

451 ``"stderr"`` internally. 

452 

453 :param str content: 

454 The full contents as a string. 

455 """ 

456 if content: 

457 self._report_sections.append((when, key, content)) 

458 

459 def reportinfo(self) -> Tuple[Union[py.path.local, str], Optional[int], str]: 

460 return self.fspath, None, "" 

461 

462 @cached_property 

463 def location(self) -> Tuple[str, Optional[int], str]: 

464 location = self.reportinfo() 

465 fspath = self.session._node_location_to_relpath(location[0]) 

466 assert type(location[2]) is str 

467 return (fspath, location[1], location[2])