1from __future__ import absolute_import, division, print_function 2import os 3 4import six 5import py 6import attr 7 8import _pytest 9import _pytest._code 10 11from _pytest.mark.structures import NodeKeywords, MarkInfo 12 13SEP = "/" 14 15tracebackcutdir = py.path.local(_pytest.__file__).dirpath() 16 17 18def _splitnode(nodeid): 19 """Split a nodeid into constituent 'parts'. 20 21 Node IDs are strings, and can be things like: 22 '' 23 'testing/code' 24 'testing/code/test_excinfo.py' 25 'testing/code/test_excinfo.py::TestFormattedExcinfo::()' 26 27 Return values are lists e.g. 28 [] 29 ['testing', 'code'] 30 ['testing', 'code', 'test_excinfo.py'] 31 ['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo', '()'] 32 """ 33 if nodeid == "": 34 # If there is no root node at all, return an empty list so the caller's logic can remain sane 35 return [] 36 parts = nodeid.split(SEP) 37 # Replace single last element 'test_foo.py::Bar::()' with multiple elements 'test_foo.py', 'Bar', '()' 38 parts[-1:] = parts[-1].split("::") 39 return parts 40 41 42def ischildnode(baseid, nodeid): 43 """Return True if the nodeid is a child node of the baseid. 44 45 E.g. 'foo/bar::Baz::()' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz', but not of 'foo/blorp' 46 """ 47 base_parts = _splitnode(baseid) 48 node_parts = _splitnode(nodeid) 49 if len(node_parts) < len(base_parts): 50 return False 51 return node_parts[:len(base_parts)] == base_parts 52 53 54@attr.s 55class _CompatProperty(object): 56 name = attr.ib() 57 58 def __get__(self, obj, owner): 59 if obj is None: 60 return self 61 62 # TODO: reenable in the features branch 63 # warnings.warn( 64 # "usage of {owner!r}.{name} is deprecated, please use pytest.{name} instead".format( 65 # name=self.name, owner=type(owner).__name__), 66 # PendingDeprecationWarning, stacklevel=2) 67 return getattr(__import__("pytest"), self.name) 68 69 70class Node(object): 71 """ base class for Collector and Item the test collection tree. 72 Collector subclasses have children, Items are terminal nodes.""" 73 74 def __init__( 75 self, name, parent=None, config=None, session=None, fspath=None, nodeid=None 76 ): 77 #: a unique name within the scope of the parent node 78 self.name = name 79 80 #: the parent collector node. 81 self.parent = parent 82 83 #: the pytest config object 84 self.config = config or parent.config 85 86 #: the session this node is part of 87 self.session = session or parent.session 88 89 #: filesystem path where this node was collected from (can be None) 90 self.fspath = fspath or getattr(parent, "fspath", None) 91 92 #: keywords/markers collected from all scopes 93 self.keywords = NodeKeywords(self) 94 95 #: the marker objects belonging to this node 96 self.own_markers = [] 97 98 #: allow adding of extra keywords to use for matching 99 self.extra_keyword_matches = set() 100 101 # used for storing artificial fixturedefs for direct parametrization 102 self._name2pseudofixturedef = {} 103 104 if nodeid is not None: 105 self._nodeid = nodeid 106 else: 107 assert parent is not None 108 self._nodeid = self.parent.nodeid + "::" + self.name 109 110 @property 111 def ihook(self): 112 """ fspath sensitive hook proxy used to call pytest hooks""" 113 return self.session.gethookproxy(self.fspath) 114 115 Module = _CompatProperty("Module") 116 Class = _CompatProperty("Class") 117 Instance = _CompatProperty("Instance") 118 Function = _CompatProperty("Function") 119 File = _CompatProperty("File") 120 Item = _CompatProperty("Item") 121 122 def _getcustomclass(self, name): 123 maybe_compatprop = getattr(type(self), name) 124 if isinstance(maybe_compatprop, _CompatProperty): 125 return getattr(__import__("pytest"), name) 126 else: 127 cls = getattr(self, name) 128 # TODO: reenable in the features branch 129 # warnings.warn("use of node.%s is deprecated, " 130 # "use pytest_pycollect_makeitem(...) to create custom " 131 # "collection nodes" % name, category=DeprecationWarning) 132 return cls 133 134 def __repr__(self): 135 return "<%s %r>" % (self.__class__.__name__, getattr(self, "name", None)) 136 137 def warn(self, code, message): 138 """ generate a warning with the given code and message for this 139 item. """ 140 assert isinstance(code, str) 141 fslocation = getattr(self, "location", None) 142 if fslocation is None: 143 fslocation = getattr(self, "fspath", None) 144 self.ihook.pytest_logwarning.call_historic( 145 kwargs=dict( 146 code=code, message=message, nodeid=self.nodeid, fslocation=fslocation 147 ) 148 ) 149 150 # methods for ordering nodes 151 @property 152 def nodeid(self): 153 """ a ::-separated string denoting its collection tree address. """ 154 return self._nodeid 155 156 def __hash__(self): 157 return hash(self.nodeid) 158 159 def setup(self): 160 pass 161 162 def teardown(self): 163 pass 164 165 def listchain(self): 166 """ return list of all parent collectors up to self, 167 starting from root of collection tree. """ 168 chain = [] 169 item = self 170 while item is not None: 171 chain.append(item) 172 item = item.parent 173 chain.reverse() 174 return chain 175 176 def add_marker(self, marker): 177 """dynamically add a marker object to the node. 178 179 :type marker: str or pytest.mark.* 180 """ 181 from _pytest.mark import MarkDecorator, MARK_GEN 182 183 if isinstance(marker, six.string_types): 184 marker = getattr(MARK_GEN, marker) 185 elif not isinstance(marker, MarkDecorator): 186 raise ValueError("is not a string or pytest.mark.* Marker") 187 self.keywords[marker.name] = marker 188 self.own_markers.append(marker.mark) 189 190 def iter_markers(self, name=None): 191 """ 192 :param name: if given, filter the results by the name attribute 193 194 iterate over all markers of the node 195 """ 196 return (x[1] for x in self.iter_markers_with_node(name=name)) 197 198 def iter_markers_with_node(self, name=None): 199 """ 200 :param name: if given, filter the results by the name attribute 201 202 iterate over all markers of the node 203 returns sequence of tuples (node, mark) 204 """ 205 for node in reversed(self.listchain()): 206 for mark in node.own_markers: 207 if name is None or getattr(mark, "name", None) == name: 208 yield node, mark 209 210 def get_closest_marker(self, name, default=None): 211 """return the first marker matching the name, from closest (for example function) to farther level (for example 212 module level). 213 214 :param default: fallback return value of no marker was found 215 :param name: name to filter by 216 """ 217 return next(self.iter_markers(name=name), default) 218 219 def get_marker(self, name): 220 """ get a marker object from this node or None if 221 the node doesn't have a marker with that name. 222 223 .. deprecated:: 3.6 224 This function has been deprecated in favor of 225 :meth:`Node.get_closest_marker <_pytest.nodes.Node.get_closest_marker>` and 226 :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>`, see :ref:`update marker code` 227 for more details. 228 """ 229 markers = list(self.iter_markers(name=name)) 230 if markers: 231 return MarkInfo(markers) 232 233 def listextrakeywords(self): 234 """ Return a set of all extra keywords in self and any parents.""" 235 extra_keywords = set() 236 for item in self.listchain(): 237 extra_keywords.update(item.extra_keyword_matches) 238 return extra_keywords 239 240 def listnames(self): 241 return [x.name for x in self.listchain()] 242 243 def addfinalizer(self, fin): 244 """ register a function to be called when this node is finalized. 245 246 This method can only be called when this node is active 247 in a setup chain, for example during self.setup(). 248 """ 249 self.session._setupstate.addfinalizer(fin, self) 250 251 def getparent(self, cls): 252 """ get the next parent node (including ourself) 253 which is an instance of the given class""" 254 current = self 255 while current and not isinstance(current, cls): 256 current = current.parent 257 return current 258 259 def _prunetraceback(self, excinfo): 260 pass 261 262 def _repr_failure_py(self, excinfo, style=None): 263 fm = self.session._fixturemanager 264 if excinfo.errisinstance(fm.FixtureLookupError): 265 return excinfo.value.formatrepr() 266 tbfilter = True 267 if self.config.option.fulltrace: 268 style = "long" 269 else: 270 tb = _pytest._code.Traceback([excinfo.traceback[-1]]) 271 self._prunetraceback(excinfo) 272 if len(excinfo.traceback) == 0: 273 excinfo.traceback = tb 274 tbfilter = False # prunetraceback already does it 275 if style == "auto": 276 style = "long" 277 # XXX should excinfo.getrepr record all data and toterminal() process it? 278 if style is None: 279 if self.config.option.tbstyle == "short": 280 style = "short" 281 else: 282 style = "long" 283 284 try: 285 os.getcwd() 286 abspath = False 287 except OSError: 288 abspath = True 289 290 return excinfo.getrepr( 291 funcargs=True, 292 abspath=abspath, 293 showlocals=self.config.option.showlocals, 294 style=style, 295 tbfilter=tbfilter, 296 ) 297 298 repr_failure = _repr_failure_py 299 300 301class Collector(Node): 302 """ Collector instances create children through collect() 303 and thus iteratively build a tree. 304 """ 305 306 class CollectError(Exception): 307 """ an error during collection, contains a custom message. """ 308 309 def collect(self): 310 """ returns a list of children (items and collectors) 311 for this collection node. 312 """ 313 raise NotImplementedError("abstract") 314 315 def repr_failure(self, excinfo): 316 """ represent a collection failure. """ 317 if excinfo.errisinstance(self.CollectError): 318 exc = excinfo.value 319 return str(exc.args[0]) 320 return self._repr_failure_py(excinfo, style="short") 321 322 def _prunetraceback(self, excinfo): 323 if hasattr(self, "fspath"): 324 traceback = excinfo.traceback 325 ntraceback = traceback.cut(path=self.fspath) 326 if ntraceback == traceback: 327 ntraceback = ntraceback.cut(excludepath=tracebackcutdir) 328 excinfo.traceback = ntraceback.filter() 329 330 331def _check_initialpaths_for_relpath(session, fspath): 332 for initial_path in session._initialpaths: 333 if fspath.common(initial_path) == initial_path: 334 return fspath.relto(initial_path.dirname) 335 336 337class FSCollector(Collector): 338 339 def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None): 340 fspath = py.path.local(fspath) # xxx only for test_resultlog.py? 341 name = fspath.basename 342 if parent is not None: 343 rel = fspath.relto(parent.fspath) 344 if rel: 345 name = rel 346 name = name.replace(os.sep, SEP) 347 self.fspath = fspath 348 349 session = session or parent.session 350 351 if nodeid is None: 352 nodeid = self.fspath.relto(session.config.rootdir) 353 354 if not nodeid: 355 nodeid = _check_initialpaths_for_relpath(session, fspath) 356 if os.sep != SEP: 357 nodeid = nodeid.replace(os.sep, SEP) 358 359 super(FSCollector, self).__init__( 360 name, parent, config, session, nodeid=nodeid, fspath=fspath 361 ) 362 363 364class File(FSCollector): 365 """ base class for collecting tests from a file. """ 366 367 368class Item(Node): 369 """ a basic test invocation item. Note that for a single function 370 there might be multiple test invocation items. 371 """ 372 nextitem = None 373 374 def __init__(self, name, parent=None, config=None, session=None, nodeid=None): 375 super(Item, self).__init__(name, parent, config, session, nodeid=nodeid) 376 self._report_sections = [] 377 378 #: user properties is a list of tuples (name, value) that holds user 379 #: defined properties for this test. 380 self.user_properties = [] 381 382 def add_report_section(self, when, key, content): 383 """ 384 Adds a new report section, similar to what's done internally to add stdout and 385 stderr captured output:: 386 387 item.add_report_section("call", "stdout", "report section contents") 388 389 :param str when: 390 One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``. 391 :param str key: 392 Name of the section, can be customized at will. Pytest uses ``"stdout"`` and 393 ``"stderr"`` internally. 394 395 :param str content: 396 The full contents as a string. 397 """ 398 if content: 399 self._report_sections.append((when, key, content)) 400 401 def reportinfo(self): 402 return self.fspath, None, "" 403 404 @property 405 def location(self): 406 try: 407 return self._location 408 except AttributeError: 409 location = self.reportinfo() 410 # bestrelpath is a quite slow function 411 cache = self.config.__dict__.setdefault("_bestrelpathcache", {}) 412 try: 413 fspath = cache[location[0]] 414 except KeyError: 415 fspath = self.session.fspath.bestrelpath(location[0]) 416 cache[location[0]] = fspath 417 location = (fspath, location[1], str(location[2])) 418 self._location = location 419 return location 420