diff --git a/CHANGELOG b/CHANGELOG index bb58b94f1..bdaa996de 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,16 @@ Changes between 2.4.1 and 2.4.2 - avoid tmpdir fixture to create too long filenames especially when parametrization is used (issue354) +- fix pytest-pep8 and pytest-flakes / pytest interactions + (collection names in mark plugin was assuming an item always + has a function which is not true for those plugins etc.) + Thanks Andi Zeidler. + +- introduce node.get_marker/node.add_marker API for plugins + like pytest-pep8 and pytest-flakes to avoid the messy + details of the node.keywords pseudo-dicts. Adapated + docs. + Changes between 2.4.0 and 2.4.1 ----------------------------------- diff --git a/_pytest/main.py b/_pytest/main.py index 4cca93ff1..5d9c243ee 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -321,6 +321,27 @@ class Node(object): chain.reverse() return chain + def add_marker(self, marker): + """ dynamically add a marker object to the node. + + ``marker`` can be a string or pytest.mark.* instance. + """ + from _pytest.mark import MarkDecorator + if isinstance(marker, py.builtin._basestring): + marker = MarkDecorator(marker) + elif not isinstance(marker, MarkDecorator): + raise ValueError("is not a string or pytest.mark.* Marker") + self.keywords[marker.name] = marker + + def get_marker(self, name): + """ get a marker object from this node or None if + the node doesn't have a marker with that name. """ + val = self.keywords.get(name, None) + if val is not None: + from _pytest.mark import MarkInfo, MarkDecorator + if isinstance(val, (MarkDecorator, MarkInfo)): + return val + def listextrakeywords(self): """ Return a set of all extra keywords in self and any parents.""" extra_keywords = set() diff --git a/_pytest/mark.py b/_pytest/mark.py index 22a6729e6..7fad6e9dd 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -81,11 +81,8 @@ def pytest_collection_modifyitems(items, config): class MarkMapping: - """Provides a local mapping for markers. - Only the marker names from the given :class:`NodeKeywords` will be mapped, - so the names are taken only from :class:`MarkInfo` or - :class:`MarkDecorator` items. - """ + """Provides a local mapping for markers where item access + resolves to True if the marker is present. """ def __init__(self, keywords): mymarks = set() for key, value in keywords.items(): @@ -93,8 +90,8 @@ class MarkMapping: mymarks.add(key) self._mymarks = mymarks - def __getitem__(self, markname): - return markname in self._mymarks + def __getitem__(self, name): + return name in self._mymarks class KeywordMapping: @@ -202,13 +199,17 @@ class MarkDecorator: pass """ def __init__(self, name, args=None, kwargs=None): - self.markname = name + self.name = name self.args = args or () self.kwargs = kwargs or {} + @property + def markname(self): + return self.name # for backward-compat (2.4.1 had this attr) + def __repr__(self): d = self.__dict__.copy() - name = d.pop('markname') + name = d.pop('name') return "" % (name, d) def __call__(self, *args, **kwargs): @@ -228,19 +229,19 @@ class MarkDecorator: else: func.pytestmark = [self] else: - holder = getattr(func, self.markname, None) + holder = getattr(func, self.name, None) if holder is None: holder = MarkInfo( - self.markname, self.args, self.kwargs + self.name, self.args, self.kwargs ) - setattr(func, self.markname, holder) + setattr(func, self.name, holder) else: holder.add(self.args, self.kwargs) return func kw = self.kwargs.copy() kw.update(kwargs) args = self.args + args - return self.__class__(self.markname, args=args, kwargs=kw) + return self.__class__(self.name, args=args, kwargs=kw) class MarkInfo: diff --git a/doc/en/example/markers.txt b/doc/en/example/markers.txt index afc6aa3b5..04b790cf7 100644 --- a/doc/en/example/markers.txt +++ b/doc/en/example/markers.txt @@ -235,7 +235,7 @@ specifies via named environments:: "env(name): mark test to run only on named environment") def pytest_runtest_setup(item): - envmarker = item.keywords.get("env", None) + envmarker = item.get_marker("env") if envmarker is not None: envname = envmarker.args[0] if envname != item.config.getoption("-E"): @@ -318,7 +318,7 @@ test function. From a conftest file we can read it like this:: import sys def pytest_runtest_setup(item): - g = item.keywords.get("glob", None) + g = item.get_marker("glob") if g is not None: for info in g: print ("glob args=%s kwargs=%s" %(info.args, info.kwargs)) @@ -353,7 +353,7 @@ for your particular platform, you could use the following plugin:: def pytest_runtest_setup(item): if isinstance(item, item.Function): plat = sys.platform - if plat not in item.keywords: + if not item.get_marker(plat): if ALL.intersection(item.keywords): pytest.skip("cannot run on platform %s" %(plat)) @@ -439,9 +439,9 @@ We want to dynamically define two markers and can do it in a def pytest_collection_modifyitems(items): for item in items: if "interface" in item.nodeid: - item.keywords["interface"] = pytest.mark.interface + item.add_marker(pytest.mark.interface) elif "event" in item.nodeid: - item.keywords["event"] = pytest.mark.event + item.add_marker(pytest.mark.event) We can now use the ``-m option`` to select one set:: diff --git a/plugin-test.sh b/plugin-test.sh new file mode 100644 index 000000000..65a072ae9 --- /dev/null +++ b/plugin-test.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# this assumes plugins are installed as sister directories + +set -e +cd ../pytest-pep8 +py.test +cd ../pytest-instafail +py.test +cd ../pytest-cache +py.test +#cd ../pytest-cov +#py.test +cd ../pytest-xdist +py.test + diff --git a/testing/test_mark.py b/testing/test_mark.py index fae13323a..8aa87c729 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -180,6 +180,7 @@ def test_keyword_option_custom(spec, testdir): assert len(passed) == len(passed_result) assert list(passed) == list(passed_result) + class TestFunctional: def test_mark_per_function(self, testdir): @@ -362,7 +363,6 @@ class TestFunctional: deselected_tests = dlist[0].items assert len(deselected_tests) == 2 - def test_keywords_at_node_level(self, testdir): p = testdir.makepyfile(""" import pytest @@ -383,6 +383,30 @@ class TestFunctional: reprec = testdir.inline_run() reprec.assertoutcome(passed=1) + def test_keyword_added_for_session(self, testdir): + testdir.makeconftest(""" + import pytest + def pytest_collection_modifyitems(session): + session.add_marker("mark1") + session.add_marker(pytest.mark.mark2) + session.add_marker(pytest.mark.mark3) + pytest.raises(ValueError, lambda: + session.add_marker(10)) + """) + testdir.makepyfile(""" + def test_some(request): + assert "mark1" in request.keywords + assert "mark2" in request.keywords + assert "mark3" in request.keywords + assert 10 not in request.keywords + marker = request.node.get_marker("mark1") + assert marker.name == "mark1" + assert marker.args == () + assert marker.kwargs == {} + """) + reprec = testdir.inline_run("-m", "mark1") + reprec.assertoutcome(passed=1) + class TestKeywordSelection: def test_select_simple(self, testdir): file_test = testdir.makepyfile("""