diff --git a/_pytest/main.py b/_pytest/main.py index 359945175..9b59e03a2 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -300,7 +300,7 @@ class Session(nodes.FSCollector): def __init__(self, config): nodes.FSCollector.__init__( self, config.rootdir, parent=None, - config=config, session=self) + config=config, session=self, nodeid="") self.testsfailed = 0 self.testscollected = 0 self.shouldstop = False @@ -311,9 +311,6 @@ class Session(nodes.FSCollector): self.config.pluginmanager.register(self, name="session") - def _makeid(self): - return "" - @hookimpl(tryfirst=True) def pytest_collectstart(self): if self.shouldfail: diff --git a/_pytest/mark/structures.py b/_pytest/mark/structures.py index 5550dc546..c56972980 100644 --- a/_pytest/mark/structures.py +++ b/_pytest/mark/structures.py @@ -1,4 +1,4 @@ -from collections import namedtuple +from collections import namedtuple, MutableMapping as MappingMixin import warnings from operator import attrgetter import inspect @@ -27,7 +27,7 @@ def istestfunc(func): getattr(func, "__name__", "") != "" -def get_empty_parameterset_mark(config, argnames, function): +def get_empty_parameterset_mark(config, argnames, func): requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION) if requested_mark in ('', None, 'skip'): mark = MARK_GEN.skip @@ -35,9 +35,9 @@ def get_empty_parameterset_mark(config, argnames, function): mark = MARK_GEN.xfail(run=False) else: raise LookupError(requested_mark) - fs, lineno = getfslineno(function) + fs, lineno = getfslineno(func) reason = "got empty parameter set %r, function %s at %s:%d" % ( - argnames, function.__name__, fs, lineno) + argnames, func.__name__, fs, lineno) return mark(reason=reason) @@ -53,8 +53,8 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')): def param_extract_id(id=None): return id - id = param_extract_id(**kw) - return cls(values, marks, id) + id_ = param_extract_id(**kw) + return cls(values, marks, id_) @classmethod def extract_from(cls, parameterset, legacy_force_tuple=False): @@ -90,7 +90,7 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')): return cls(argval, marks=newmarks, id=None) @classmethod - def _for_parametrize(cls, argnames, argvalues, function, config): + def _for_parametrize(cls, argnames, argvalues, func, config): if not isinstance(argnames, (tuple, list)): argnames = [x.strip() for x in argnames.split(",") if x.strip()] force_tuple = len(argnames) == 1 @@ -102,7 +102,7 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')): del argvalues if not parameters: - mark = get_empty_parameterset_mark(config, argnames, function) + mark = get_empty_parameterset_mark(config, argnames, func) parameters.append(ParameterSet( values=(NOTSET,) * len(argnames), marks=[mark], @@ -328,3 +328,40 @@ class MarkGenerator(object): MARK_GEN = MarkGenerator() + + +class NodeKeywords(MappingMixin): + def __init__(self, node): + self.node = node + self.parent = node.parent + self._markers = {node.name: True} + + def __getitem__(self, key): + try: + return self._markers[key] + except KeyError: + if self.parent is None: + raise + return self.parent.keywords[key] + + def __setitem__(self, key, value): + self._markers[key] = value + + def __delitem__(self, key): + raise ValueError("cannot delete key in keywords dict") + + def __iter__(self): + seen = self._seen() + return iter(seen) + + def _seen(self): + seen = set(self._markers) + if self.parent is not None: + seen.update(self.parent.keywords) + return seen + + def __len__(self): + return len(self._seen()) + + def __repr__(self): + return "" % (self.node, ) diff --git a/_pytest/nodes.py b/_pytest/nodes.py index 7d802004f..97f4da602 100644 --- a/_pytest/nodes.py +++ b/_pytest/nodes.py @@ -1,5 +1,4 @@ from __future__ import absolute_import, division, print_function -from collections import MutableMapping as MappingMixin import os import six @@ -7,7 +6,9 @@ import py import attr import _pytest +import _pytest._code +from _pytest.mark.structures import NodeKeywords SEP = "/" @@ -66,47 +67,11 @@ class _CompatProperty(object): return getattr(__import__('pytest'), self.name) -class NodeKeywords(MappingMixin): - def __init__(self, node): - self.node = node - self.parent = node.parent - self._markers = {node.name: True} - - def __getitem__(self, key): - try: - return self._markers[key] - except KeyError: - if self.parent is None: - raise - return self.parent.keywords[key] - - def __setitem__(self, key, value): - self._markers[key] = value - - def __delitem__(self, key): - raise ValueError("cannot delete key in keywords dict") - - def __iter__(self): - seen = set(self._markers) - if self.parent is not None: - seen.update(self.parent.keywords) - return iter(seen) - - def __len__(self): - return len(self.__iter__()) - - def keys(self): - return list(self) - - def __repr__(self): - return "" % (self.node, ) - - class Node(object): """ base class for Collector and Item the test collection tree. Collector subclasses have children, Items are terminal nodes.""" - def __init__(self, name, parent=None, config=None, session=None): + def __init__(self, name, parent=None, config=None, session=None, fspath=None, nodeid=None): #: a unique name within the scope of the parent node self.name = name @@ -120,7 +85,7 @@ class Node(object): self.session = session or parent.session #: filesystem path where this node was collected from (can be None) - self.fspath = getattr(parent, 'fspath', None) + self.fspath = fspath or getattr(parent, 'fspath', None) #: keywords/markers collected from all scopes self.keywords = NodeKeywords(self) @@ -131,6 +96,12 @@ class Node(object): # used for storing artificial fixturedefs for direct parametrization self._name2pseudofixturedef = {} + if nodeid is not None: + self._nodeid = nodeid + else: + assert parent is not None + self._nodeid = self.parent.nodeid + "::" + self.name + @property def ihook(self): """ fspath sensitive hook proxy used to call pytest hooks""" @@ -174,14 +145,7 @@ class Node(object): @property def nodeid(self): """ a ::-separated string denoting its collection tree address. """ - try: - return self._nodeid - except AttributeError: - self._nodeid = x = self._makeid() - return x - - def _makeid(self): - return self.parent.nodeid + "::" + self.name + return self._nodeid def __hash__(self): return hash(self.nodeid) @@ -227,7 +191,6 @@ class Node(object): def listextrakeywords(self): """ Return a set of all extra keywords in self and any parents.""" extra_keywords = set() - item = self for item in self.listchain(): extra_keywords.update(item.extra_keyword_matches) return extra_keywords @@ -319,8 +282,14 @@ class Collector(Node): excinfo.traceback = ntraceback.filter() +def _check_initialpaths_for_relpath(session, fspath): + for initial_path in session._initialpaths: + if fspath.common(initial_path) == initial_path: + return fspath.relto(initial_path.dirname) + + class FSCollector(Collector): - def __init__(self, fspath, parent=None, config=None, session=None): + def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None): fspath = py.path.local(fspath) # xxx only for test_resultlog.py? name = fspath.basename if parent is not None: @@ -328,22 +297,19 @@ class FSCollector(Collector): if rel: name = rel name = name.replace(os.sep, SEP) - super(FSCollector, self).__init__(name, parent, config, session) self.fspath = fspath - def _check_initialpaths_for_relpath(self): - for initialpath in self.session._initialpaths: - if self.fspath.common(initialpath) == initialpath: - return self.fspath.relto(initialpath.dirname) + session = session or parent.session - def _makeid(self): - relpath = self.fspath.relto(self.config.rootdir) + if nodeid is None: + nodeid = self.fspath.relto(session.config.rootdir) - if not relpath: - relpath = self._check_initialpaths_for_relpath() - if os.sep != SEP: - relpath = relpath.replace(os.sep, SEP) - return relpath + if not nodeid: + nodeid = _check_initialpaths_for_relpath(session, fspath) + if os.sep != SEP: + nodeid = nodeid.replace(os.sep, SEP) + + super(FSCollector, self).__init__(name, parent, config, session, nodeid=nodeid, fspath=fspath) class File(FSCollector): @@ -356,8 +322,8 @@ class Item(Node): """ nextitem = None - def __init__(self, name, parent=None, config=None, session=None): - super(Item, self).__init__(name, parent, config, session) + def __init__(self, name, parent=None, config=None, session=None, nodeid=None): + super(Item, self).__init__(name, parent, config, session, nodeid=nodeid) self._report_sections = [] #: user properties is a list of tuples (name, value) that holds user diff --git a/_pytest/terminal.py b/_pytest/terminal.py index b84b2414c..15492ad4b 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -22,9 +22,9 @@ from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \ def pytest_addoption(parser): group = parser.getgroup("terminal reporting", "reporting", after="general") group._addoption('-v', '--verbose', action="count", - dest="verbose", default=0, help="increase verbosity."), + dest="verbose", default=0, help="increase verbosity.") group._addoption('-q', '--quiet', action="count", - dest="quiet", default=0, help="decrease verbosity."), + dest="quiet", default=0, help="decrease verbosity.") group._addoption('-r', action="store", dest="reportchars", default='', metavar="chars", help="show extra test summary info as specified by chars (f)ailed, " diff --git a/changelog/3291.trivial.rst b/changelog/3291.trivial.rst new file mode 100644 index 000000000..a2e65c2d7 --- /dev/null +++ b/changelog/3291.trivial.rst @@ -0,0 +1 @@ +``nodeids`` can now be passed explicitly to ``FSCollector`` and ``Node`` constructors. diff --git a/testing/test_resultlog.py b/testing/test_resultlog.py index 45fed7078..b1760721c 100644 --- a/testing/test_resultlog.py +++ b/testing/test_resultlog.py @@ -13,7 +13,7 @@ def test_generic_path(testdir): from _pytest.main import Session config = testdir.parseconfig() session = Session(config) - p1 = Node('a', config=config, session=session) + p1 = Node('a', config=config, session=session, nodeid='a') # assert p1.fspath is None p2 = Node('B', parent=p1) p3 = Node('()', parent=p2)