From 12626411f3da61f2e2d6f464a1668c4f0bb887f2 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 24 Mar 2016 10:58:09 +0100 Subject: [PATCH 01/15] turn mark module into package --- _pytest/{mark.py => mark/__init__.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename _pytest/{mark.py => mark/__init__.py} (100%) diff --git a/_pytest/mark.py b/_pytest/mark/__init__.py similarity index 100% rename from _pytest/mark.py rename to _pytest/mark/__init__.py From 4f03b736e9723b2c205959ee2fe48b14862cf47e Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 24 Mar 2016 13:19:54 +0100 Subject: [PATCH 02/15] restructure strict marker config parsing --- _pytest/mark/__init__.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/_pytest/mark/__init__.py b/_pytest/mark/__init__.py index 1a763540240..d530d3deafb 100644 --- a/_pytest/mark/__init__.py +++ b/_pytest/mark/__init__.py @@ -165,6 +165,12 @@ def pytest_configure(config): pytest.mark._config = config +def _parsed_markers(config): + for line in config.getini("markers"): + beginning = line.split(":", 1) + yield beginning[0].split("(", 1)[0] + + class MarkGenerator: """ Factory for :class:`MarkDecorator` objects - exposed as a ``pytest.mark`` singleton instance. Example:: @@ -177,6 +183,9 @@ def test_function(): will set a 'slowtest' :class:`MarkInfo` object on the ``test_function`` object. """ + def __init__(self): + self.__known_markers = set() + def __getattr__(self, name): if name[0] == "_": raise AttributeError("Marker name must NOT start with underscore") @@ -185,17 +194,12 @@ def __getattr__(self, name): return MarkDecorator(name) def _check(self, name): - try: - if name in self._markers: - return - except AttributeError: - pass - self._markers = l = set() - for line in self._config.getini("markers"): - beginning = line.split(":", 1) - x = beginning[0].split("(", 1)[0] - l.add(x) - if name not in self._markers: + if name in self.__known_markers: + return + + self.__known_markers.update(_parsed_markers(self._config)) + + if name not in self.__known_markers: raise AttributeError("%r not a registered marker" % (name,)) def istestfunc(func): From 0c11238dfb5e66196202a5db1206b7ff2cbca633 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 24 Mar 2016 14:32:41 +0100 Subject: [PATCH 03/15] add .env to collectignore --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index ac604b88e0d..d3b13eb09ed 100644 --- a/tox.ini +++ b/tox.ini @@ -157,7 +157,7 @@ rsyncdirs=tox.ini pytest.py _pytest testing python_files=test_*.py *_test.py testing/*/*.py python_classes=Test Acceptance python_functions=test -norecursedirs = .tox ja .hg cx_freeze_source +norecursedirs = .tox ja .hg cx_freeze_source .env [flake8] From 09eb969ac225b0263731913807ca4dd9363d741c Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 24 Mar 2016 16:56:44 +0100 Subject: [PATCH 04/15] move mark evaluator to the mark plugin --- _pytest/mark/evalexpr.py | 112 +++++++++++++++++++++++++++++++++++++++ _pytest/skipping.py | 112 +-------------------------------------- 2 files changed, 113 insertions(+), 111 deletions(-) create mode 100644 _pytest/mark/evalexpr.py diff --git a/_pytest/mark/evalexpr.py b/_pytest/mark/evalexpr.py new file mode 100644 index 00000000000..4787abcd1ff --- /dev/null +++ b/_pytest/mark/evalexpr.py @@ -0,0 +1,112 @@ +import sys +import traceback +import pytest +import os +import py + + +def cached_eval(config, expr, d): + if not hasattr(config, '_evalcache'): + config._evalcache = {} + try: + return config._evalcache[expr] + except KeyError: + import _pytest._code + exprcode = _pytest._code.compile(expr, mode="eval") + config._evalcache[expr] = x = eval(exprcode, d) + return x + + +class MarkEvaluator: + def __init__(self, item, name): + self.item = item + self.name = name + + @property + def holder(self): + return self.item.keywords.get(self.name) + + def __bool__(self): + return bool(self.holder) + __nonzero__ = __bool__ + + def wasvalid(self): + return not hasattr(self, 'exc') + + def invalidraise(self, exc): + raises = self.get('raises') + if not raises: + return + return not isinstance(exc, raises) + + def istrue(self): + try: + return self._istrue() + except Exception: + self.exc = sys.exc_info() + if isinstance(self.exc[1], SyntaxError): + msg = [" " * (self.exc[1].offset + 4) + "^",] + msg.append("SyntaxError: invalid syntax") + else: + msg = traceback.format_exception_only(*self.exc[:2]) + pytest.fail("Error evaluating %r expression\n" + " %s\n" + "%s" + %(self.name, self.expr, "\n".join(msg)), + pytrace=False) + + def _getglobals(self): + d = {'os': os, 'sys': sys, 'config': self.item.config} + func = self.item.obj + try: + d.update(func.__globals__) + except AttributeError: + d.update(func.func_globals) + return d + + def _istrue(self): + if hasattr(self, 'result'): + return self.result + if self.holder: + d = self._getglobals() + if self.holder.args: + self.result = False + # "holder" might be a MarkInfo or a MarkDecorator; only + # MarkInfo keeps track of all parameters it received in an + # _arglist attribute + if hasattr(self.holder, '_arglist'): + arglist = self.holder._arglist + else: + arglist = [(self.holder.args, self.holder.kwargs)] + for args, kwargs in arglist: + for expr in args: + self.expr = expr + if isinstance(expr, py.builtin._basestring): + result = cached_eval(self.item.config, expr, d) + else: + if "reason" not in kwargs: + # XXX better be checked at collection time + msg = "you need to specify reason=STRING " \ + "when using booleans as conditions." + pytest.fail(msg) + result = bool(expr) + if result: + self.result = True + self.reason = kwargs.get('reason', None) + self.expr = expr + return self.result + else: + self.result = True + return getattr(self, 'result', False) + + def get(self, attr, default=None): + return self.holder.kwargs.get(attr, default) + + def getexplanation(self): + expl = getattr(self, 'reason', None) or self.get('reason', None) + if not expl: + if not hasattr(self, 'expr'): + return "" + else: + return "condition: " + str(self.expr) + return expl diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 69157f485a0..8d91653b644 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -1,12 +1,7 @@ """ support for skip/xfail functions and markers. """ -import os -import sys -import traceback - -import py import pytest from _pytest.mark import MarkInfo, MarkDecorator - +from _pytest.mark.evalexpr import MarkEvaluator def pytest_addoption(parser): group = parser.getgroup("general") @@ -63,100 +58,6 @@ def xfail(reason=""): xfail.Exception = XFailed -class MarkEvaluator: - def __init__(self, item, name): - self.item = item - self.name = name - - @property - def holder(self): - return self.item.keywords.get(self.name) - - def __bool__(self): - return bool(self.holder) - __nonzero__ = __bool__ - - def wasvalid(self): - return not hasattr(self, 'exc') - - def invalidraise(self, exc): - raises = self.get('raises') - if not raises: - return - return not isinstance(exc, raises) - - def istrue(self): - try: - return self._istrue() - except Exception: - self.exc = sys.exc_info() - if isinstance(self.exc[1], SyntaxError): - msg = [" " * (self.exc[1].offset + 4) + "^",] - msg.append("SyntaxError: invalid syntax") - else: - msg = traceback.format_exception_only(*self.exc[:2]) - pytest.fail("Error evaluating %r expression\n" - " %s\n" - "%s" - %(self.name, self.expr, "\n".join(msg)), - pytrace=False) - - def _getglobals(self): - d = {'os': os, 'sys': sys, 'config': self.item.config} - func = self.item.obj - try: - d.update(func.__globals__) - except AttributeError: - d.update(func.func_globals) - return d - - def _istrue(self): - if hasattr(self, 'result'): - return self.result - if self.holder: - d = self._getglobals() - if self.holder.args: - self.result = False - # "holder" might be a MarkInfo or a MarkDecorator; only - # MarkInfo keeps track of all parameters it received in an - # _arglist attribute - if hasattr(self.holder, '_arglist'): - arglist = self.holder._arglist - else: - arglist = [(self.holder.args, self.holder.kwargs)] - for args, kwargs in arglist: - for expr in args: - self.expr = expr - if isinstance(expr, py.builtin._basestring): - result = cached_eval(self.item.config, expr, d) - else: - if "reason" not in kwargs: - # XXX better be checked at collection time - msg = "you need to specify reason=STRING " \ - "when using booleans as conditions." - pytest.fail(msg) - result = bool(expr) - if result: - self.result = True - self.reason = kwargs.get('reason', None) - self.expr = expr - return self.result - else: - self.result = True - return getattr(self, 'result', False) - - def get(self, attr, default=None): - return self.holder.kwargs.get(attr, default) - - def getexplanation(self): - expl = getattr(self, 'reason', None) or self.get('reason', None) - if not expl: - if not hasattr(self, 'expr'): - return "" - else: - return "condition: " + str(self.expr) - return expl - @pytest.hookimpl(tryfirst=True) def pytest_runtest_setup(item): @@ -312,17 +213,6 @@ def show_xpassed(terminalreporter, lines): reason = rep.wasxfail lines.append("XPASS %s %s" %(pos, reason)) -def cached_eval(config, expr, d): - if not hasattr(config, '_evalcache'): - config._evalcache = {} - try: - return config._evalcache[expr] - except KeyError: - import _pytest._code - exprcode = _pytest._code.compile(expr, mode="eval") - config._evalcache[expr] = x = eval(exprcode, d) - return x - def folded_skips(skipped): d = {} From 99422816b32de98e4e4bd647eb96834f00ac8ed9 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 24 Apr 2016 08:01:10 +0200 Subject: [PATCH 05/15] apply yapf to mark plugin --- _pytest/mark/__init__.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/_pytest/mark/__init__.py b/_pytest/mark/__init__.py index d530d3deafb..84723af89ad 100644 --- a/_pytest/mark/__init__.py +++ b/_pytest/mark/__init__.py @@ -3,7 +3,6 @@ class MarkerError(Exception): - """Error in use of a pytest marker/attribute.""" @@ -12,6 +11,7 @@ def pytest_namespace(): def pytest_addoption(parser): + # yapf: disable group = parser.getgroup("general") group._addoption( '-k', @@ -40,7 +40,7 @@ def pytest_addoption(parser): ) parser.addini("markers", "markers for test functions", 'linelist') - + # yapf: enable def pytest_cmdline_main(config): import _pytest.config @@ -54,6 +54,8 @@ def pytest_cmdline_main(config): tw.line() config._ensure_unconfigure() return 0 + + pytest_cmdline_main.tryfirst = True @@ -94,6 +96,7 @@ def pytest_collection_modifyitems(items, config): class MarkMapping: """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(): @@ -109,6 +112,7 @@ class KeywordMapping: """Provides a local mapping for keywords. Given a list of names, map any substring of one of these names to True. """ + def __init__(self, names): self._names = names @@ -200,12 +204,14 @@ def _check(self, name): self.__known_markers.update(_parsed_markers(self._config)) if name not in self.__known_markers: - raise AttributeError("%r not a registered marker" % (name,)) + raise AttributeError("%r not a registered marker" % (name, )) + def istestfunc(func): return hasattr(func, "__call__") and \ getattr(func, "__name__", "") != "" + class MarkDecorator: """ A decorator for test functions and test classes. When applied it will create :class:`MarkInfo` objects which may be @@ -239,6 +245,7 @@ def test_function(): additional keyword or positional arguments. """ + def __init__(self, name, args=None, kwargs=None): self.name = name self.args = args or () @@ -246,7 +253,7 @@ def __init__(self, name, args=None, kwargs=None): @property def markname(self): - return self.name # for backward-compat (2.4.1 had this attr) + return self.name # for backward-compat (2.4.1 had this attr) def __repr__(self): d = self.__dict__.copy() @@ -274,9 +281,7 @@ def __call__(self, *args, **kwargs): else: holder = getattr(func, self.name, None) if holder is None: - holder = MarkInfo( - self.name, self.args, self.kwargs - ) + holder = MarkInfo(self.name, self.args, self.kwargs) setattr(func, self.name, holder) else: holder.add(self.args, self.kwargs) @@ -289,6 +294,7 @@ def __call__(self, *args, **kwargs): class MarkInfo: """ Marking object created by :class:`MarkDecorator` instances. """ + def __init__(self, name, args, kwargs): #: name of attribute self.name = name @@ -299,9 +305,8 @@ def __init__(self, name, args, kwargs): self._arglist = [(args, kwargs.copy())] def __repr__(self): - return "" % ( - self.name, self.args, self.kwargs - ) + return "" % (self.name, self.args, + self.kwargs) def add(self, args, kwargs): """ add a MarkInfo with the given args and kwargs. """ From f9002e1f98a0b35579d5a722a8a994220d8fa5c3 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 24 Apr 2016 08:03:23 +0200 Subject: [PATCH 06/15] yapf mark.evalexpr --- _pytest/mark/evalexpr.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/_pytest/mark/evalexpr.py b/_pytest/mark/evalexpr.py index 4787abcd1ff..e3f4b8bba82 100644 --- a/_pytest/mark/evalexpr.py +++ b/_pytest/mark/evalexpr.py @@ -28,6 +28,7 @@ def holder(self): def __bool__(self): return bool(self.holder) + __nonzero__ = __bool__ def wasvalid(self): @@ -45,14 +46,13 @@ def istrue(self): except Exception: self.exc = sys.exc_info() if isinstance(self.exc[1], SyntaxError): - msg = [" " * (self.exc[1].offset + 4) + "^",] + msg = [" " * (self.exc[1].offset + 4) + "^", ] msg.append("SyntaxError: invalid syntax") else: msg = traceback.format_exception_only(*self.exc[:2]) pytest.fail("Error evaluating %r expression\n" " %s\n" - "%s" - %(self.name, self.expr, "\n".join(msg)), + "%s" % (self.name, self.expr, "\n".join(msg)), pytrace=False) def _getglobals(self): From 69d5f86ad8b26eb0118a1e8301a2b34f43fa93d6 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 24 Apr 2016 08:07:41 +0200 Subject: [PATCH 07/15] mark plugin: split the data carrying classes into a own module --- _pytest/mark/__init__.py | 115 +-------------------------------------- _pytest/mark/model.py | 114 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 114 deletions(-) create mode 100644 _pytest/mark/model.py diff --git a/_pytest/mark/__init__.py b/_pytest/mark/__init__.py index 84723af89ad..18ff93a0076 100644 --- a/_pytest/mark/__init__.py +++ b/_pytest/mark/__init__.py @@ -1,5 +1,5 @@ """ generic mechanism for marking and selecting python functions. """ -import inspect +from .model import MarkInfo, MarkDecorator class MarkerError(Exception): @@ -205,116 +205,3 @@ def _check(self, name): if name not in self.__known_markers: raise AttributeError("%r not a registered marker" % (name, )) - - -def istestfunc(func): - return hasattr(func, "__call__") and \ - getattr(func, "__name__", "") != "" - - -class MarkDecorator: - """ A decorator for test functions and test classes. When applied - it will create :class:`MarkInfo` objects which may be - :ref:`retrieved by hooks as item keywords `. - MarkDecorator instances are often created like this:: - - mark1 = pytest.mark.NAME # simple MarkDecorator - mark2 = pytest.mark.NAME(name1=value) # parametrized MarkDecorator - - and can then be applied as decorators to test functions:: - - @mark2 - def test_function(): - pass - - When a MarkDecorator instance is called it does the following: - 1. If called with a single class as its only positional argument and no - additional keyword arguments, it attaches itself to the class so it - gets applied automatically to all test cases found in that class. - 2. If called with a single function as its only positional argument and - no additional keyword arguments, it attaches a MarkInfo object to the - function, containing all the arguments already stored internally in - the MarkDecorator. - 3. When called in any other case, it performs a 'fake construction' call, - i.e. it returns a new MarkDecorator instance with the original - MarkDecorator's content updated with the arguments passed to this - call. - - Note: The rules above prevent MarkDecorator objects from storing only a - single function or class reference as their positional argument with no - additional keyword or positional arguments. - - """ - - def __init__(self, name, args=None, kwargs=None): - 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('name') - return "" % (name, d) - - def __call__(self, *args, **kwargs): - """ if passed a single callable argument: decorate it with mark info. - otherwise add *args/**kwargs in-place to mark information. """ - if args and not kwargs: - func = args[0] - is_class = inspect.isclass(func) - if len(args) == 1 and (istestfunc(func) or is_class): - if is_class: - if hasattr(func, 'pytestmark'): - mark_list = func.pytestmark - if not isinstance(mark_list, list): - mark_list = [mark_list] - # always work on a copy to avoid updating pytestmark - # from a superclass by accident - mark_list = mark_list + [self] - func.pytestmark = mark_list - else: - func.pytestmark = [self] - else: - holder = getattr(func, self.name, None) - if holder is None: - holder = MarkInfo(self.name, self.args, self.kwargs) - 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.name, args=args, kwargs=kw) - - -class MarkInfo: - """ Marking object created by :class:`MarkDecorator` instances. """ - - def __init__(self, name, args, kwargs): - #: name of attribute - self.name = name - #: positional argument list, empty if none specified - self.args = args - #: keyword argument dictionary, empty if nothing specified - self.kwargs = kwargs.copy() - self._arglist = [(args, kwargs.copy())] - - def __repr__(self): - return "" % (self.name, self.args, - self.kwargs) - - def add(self, args, kwargs): - """ add a MarkInfo with the given args and kwargs. """ - self._arglist.append((args, kwargs)) - self.args += args - self.kwargs.update(kwargs) - - def __iter__(self): - """ yield MarkInfo objects each relating to a marking-call. """ - for args, kwargs in self._arglist: - yield MarkInfo(self.name, args, kwargs) diff --git a/_pytest/mark/model.py b/_pytest/mark/model.py new file mode 100644 index 00000000000..783bb56788a --- /dev/null +++ b/_pytest/mark/model.py @@ -0,0 +1,114 @@ +import inspect + + +def istestfunc(func): + return hasattr(func, "__call__") and \ + getattr(func, "__name__", "") != "" + + +class MarkDecorator: + """ A decorator for test functions and test classes. When applied + it will create :class:`MarkInfo` objects which may be + :ref:`retrieved by hooks as item keywords `. + MarkDecorator instances are often created like this:: + + mark1 = pytest.mark.NAME # simple MarkDecorator + mark2 = pytest.mark.NAME(name1=value) # parametrized MarkDecorator + + and can then be applied as decorators to test functions:: + + @mark2 + def test_function(): + pass + + When a MarkDecorator instance is called it does the following: + 1. If called with a single class as its only positional argument and no + additional keyword arguments, it attaches itself to the class so it + gets applied automatically to all test cases found in that class. + 2. If called with a single function as its only positional argument and + no additional keyword arguments, it attaches a MarkInfo object to the + function, containing all the arguments already stored internally in + the MarkDecorator. + 3. When called in any other case, it performs a 'fake construction' call, + i.e. it returns a new MarkDecorator instance with the original + MarkDecorator's content updated with the arguments passed to this + call. + + Note: The rules above prevent MarkDecorator objects from storing only a + single function or class reference as their positional argument with no + additional keyword or positional arguments. + + """ + + def __init__(self, name, args=None, kwargs=None): + 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('name') + return "" % (name, d) + + def __call__(self, *args, **kwargs): + """ if passed a single callable argument: decorate it with mark info. + otherwise add *args/**kwargs in-place to mark information. """ + if args and not kwargs: + func = args[0] + is_class = inspect.isclass(func) + if len(args) == 1 and (istestfunc(func) or is_class): + if is_class: + if hasattr(func, 'pytestmark'): + mark_list = func.pytestmark + if not isinstance(mark_list, list): + mark_list = [mark_list] + # always work on a copy to avoid updating pytestmark + # from a superclass by accident + mark_list = mark_list + [self] + func.pytestmark = mark_list + else: + func.pytestmark = [self] + else: + holder = getattr(func, self.name, None) + if holder is None: + holder = MarkInfo(self.name, self.args, self.kwargs) + 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.name, args=args, kwargs=kw) + + +class MarkInfo: + """ Marking object created by :class:`MarkDecorator` instances. """ + + def __init__(self, name, args, kwargs): + #: name of attribute + self.name = name + #: positional argument list, empty if none specified + self.args = args + #: keyword argument dictionary, empty if nothing specified + self.kwargs = kwargs.copy() + self._arglist = [(args, kwargs.copy())] + + def __repr__(self): + return "" % (self.name, self.args, + self.kwargs) + + def add(self, args, kwargs): + """ add a MarkInfo with the given args and kwargs. """ + self._arglist.append((args, kwargs)) + self.args += args + self.kwargs.update(kwargs) + + def __iter__(self): + """ yield MarkInfo objects each relating to a marking-call. """ + for args, kwargs in self._arglist: + yield MarkInfo(self.name, args, kwargs) From 3c1dc1ab713e30ad4786881a4472775866af0ca5 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 24 Apr 2016 08:08:58 +0200 Subject: [PATCH 08/15] mark plugin: ensure the data carrying classes are newstyle classes --- _pytest/mark/model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_pytest/mark/model.py b/_pytest/mark/model.py index 783bb56788a..bedf28a5909 100644 --- a/_pytest/mark/model.py +++ b/_pytest/mark/model.py @@ -6,7 +6,7 @@ def istestfunc(func): getattr(func, "__name__", "") != "" -class MarkDecorator: +class MarkDecorator(object): """ A decorator for test functions and test classes. When applied it will create :class:`MarkInfo` objects which may be :ref:`retrieved by hooks as item keywords `. @@ -86,7 +86,7 @@ def __call__(self, *args, **kwargs): return self.__class__(self.name, args=args, kwargs=kw) -class MarkInfo: +class MarkInfo(object): """ Marking object created by :class:`MarkDecorator` instances. """ def __init__(self, name, args, kwargs): From fbd17b3a0f45ddf4b4ee49085206d28d3c646edf Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 24 Apr 2016 08:13:44 +0200 Subject: [PATCH 09/15] mark plugin: move MarkGenerator to model --- _pytest/mark/__init__.py | 34 +--------------------------------- _pytest/mark/model.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/_pytest/mark/__init__.py b/_pytest/mark/__init__.py index 18ff93a0076..08ddd1677c1 100644 --- a/_pytest/mark/__init__.py +++ b/_pytest/mark/__init__.py @@ -1,5 +1,5 @@ """ generic mechanism for marking and selecting python functions. """ -from .model import MarkInfo, MarkDecorator +from .model import MarkInfo, MarkDecorator, MarkGenerator class MarkerError(Exception): @@ -173,35 +173,3 @@ def _parsed_markers(config): for line in config.getini("markers"): beginning = line.split(":", 1) yield beginning[0].split("(", 1)[0] - - -class MarkGenerator: - """ Factory for :class:`MarkDecorator` objects - exposed as - a ``pytest.mark`` singleton instance. Example:: - - import pytest - @pytest.mark.slowtest - def test_function(): - pass - - will set a 'slowtest' :class:`MarkInfo` object - on the ``test_function`` object. """ - - def __init__(self): - self.__known_markers = set() - - def __getattr__(self, name): - if name[0] == "_": - raise AttributeError("Marker name must NOT start with underscore") - if hasattr(self, '_config'): - self._check(name) - return MarkDecorator(name) - - def _check(self, name): - if name in self.__known_markers: - return - - self.__known_markers.update(_parsed_markers(self._config)) - - if name not in self.__known_markers: - raise AttributeError("%r not a registered marker" % (name, )) diff --git a/_pytest/mark/model.py b/_pytest/mark/model.py index bedf28a5909..e5bea4b8b0a 100644 --- a/_pytest/mark/model.py +++ b/_pytest/mark/model.py @@ -112,3 +112,35 @@ def __iter__(self): """ yield MarkInfo objects each relating to a marking-call. """ for args, kwargs in self._arglist: yield MarkInfo(self.name, args, kwargs) + + +class MarkGenerator: + """ Factory for :class:`MarkDecorator` objects - exposed as + a ``pytest.mark`` singleton instance. Example:: + + import py + @pytest.mark.slowtest + def test_function(): + pass + + will set a 'slowtest' :class:`MarkInfo` object + on the ``test_function`` object. """ + + def __init__(self): + self.__known_markers = set() + + def __getattr__(self, name): + if name[0] == "_": + raise AttributeError("Marker name must NOT start with underscore") + if hasattr(self, '_config'): + self._check(name) + return MarkDecorator(name) + + def _check(self, name): + if name in self.__known_markers: + return + from . import _parsed_markers + self.__known_markers.update(_parsed_markers(self._config)) + + if name not in self.__known_markers: + raise AttributeError("%r not a registered marker" % (name, )) From 8c13fd5cffad50bbbc7ae64e779a62c4e5401dab Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 24 Apr 2016 08:15:37 +0200 Subject: [PATCH 10/15] mark plugin: ensure MarkGenerator is always anewstyle class --- _pytest/mark/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_pytest/mark/model.py b/_pytest/mark/model.py index e5bea4b8b0a..b23d7506d51 100644 --- a/_pytest/mark/model.py +++ b/_pytest/mark/model.py @@ -114,7 +114,7 @@ def __iter__(self): yield MarkInfo(self.name, args, kwargs) -class MarkGenerator: +class MarkGenerator(object): """ Factory for :class:`MarkDecorator` objects - exposed as a ``pytest.mark`` singleton instance. Example:: From 205a289ccc22f2c82c3a278c1ddc10559c3d6a10 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 24 Apr 2016 11:07:16 +0200 Subject: [PATCH 11/15] move marker transfer to mark plugin and fix behaviour xfail a few tests that broke discovered a dependent test that breaks as well --- _pytest/mark/model.py | 106 ++++++---- _pytest/mark/utils.py | 31 +++ _pytest/python.py | 481 ++++++++++++++++++++++++++---------------- testing/test_mark.py | 86 +++++--- 4 files changed, 448 insertions(+), 256 deletions(-) create mode 100644 _pytest/mark/utils.py diff --git a/_pytest/mark/model.py b/_pytest/mark/model.py index b23d7506d51..da53d9889b7 100644 --- a/_pytest/mark/model.py +++ b/_pytest/mark/model.py @@ -1,4 +1,9 @@ import inspect +from operator import attrgetter + + +def alias(name): + return property(fget=attrgetter(name), doc='alias for self.' + name) def istestfunc(func): @@ -6,6 +11,32 @@ def istestfunc(func): getattr(func, "__name__", "") != "" +def apply_mark(mark, obj): + # unwrap MarkDecorator + mark = getattr(mark, 'mark', mark) + if not isinstance(mark, Mark): + raise TypeError('%r is not a marker' % (mark, )) + is_class = inspect.isclass(obj) + if is_class: + if hasattr(obj, 'pytestmark'): + mark_list = obj.pytestmark + if not isinstance(mark_list, list): + mark_list = [mark_list] + # always work on a copy to avoid updating pytestmark + # from a superclass by accident + mark_list = mark_list + [mark] + obj.pytestmark = mark_list + else: + obj.pytestmark = [mark] + else: + holder = getattr(obj, mark.name, None) + if holder is None: + holder = MarkInfo(mark) + setattr(obj, mark.name, holder) + else: + holder.add(mark) + + class MarkDecorator(object): """ A decorator for test functions and test classes. When applied it will create :class:`MarkInfo` objects which may be @@ -41,18 +72,15 @@ def test_function(): """ def __init__(self, name, args=None, kwargs=None): - self.name = name - self.args = args or () - self.kwargs = kwargs or {} + self.mark = Mark(name, args or (), kwargs or {}) - @property - def markname(self): - return self.name # for backward-compat (2.4.1 had this attr) + name = markname = alias('mark.name') + args = alias('mark.args') + kwargs = alias('mark.kwargs') def __repr__(self): - d = self.__dict__.copy() - name = d.pop('name') - return "" % (name, d) + + return repr(self.mark).replace('Mark', 'MarkDecorator') def __call__(self, *args, **kwargs): """ if passed a single callable argument: decorate it with mark info. @@ -61,25 +89,9 @@ def __call__(self, *args, **kwargs): func = args[0] is_class = inspect.isclass(func) if len(args) == 1 and (istestfunc(func) or is_class): - if is_class: - if hasattr(func, 'pytestmark'): - mark_list = func.pytestmark - if not isinstance(mark_list, list): - mark_list = [mark_list] - # always work on a copy to avoid updating pytestmark - # from a superclass by accident - mark_list = mark_list + [self] - func.pytestmark = mark_list - else: - func.pytestmark = [self] - else: - holder = getattr(func, self.name, None) - if holder is None: - holder = MarkInfo(self.name, self.args, self.kwargs) - setattr(func, self.name, holder) - else: - holder.add(self.args, self.kwargs) + apply_mark(mark=self.mark, obj=func) return func + kw = self.kwargs.copy() kw.update(kwargs) args = self.args + args @@ -88,30 +100,44 @@ def __call__(self, *args, **kwargs): class MarkInfo(object): """ Marking object created by :class:`MarkDecorator` instances. """ + name = markname = alias('mark.name') + args = alias('mark.args') + kwargs = alias('mark.kwargs') - def __init__(self, name, args, kwargs): + def __init__(self, mark): + self.mark = mark #: name of attribute - self.name = name - #: positional argument list, empty if none specified - self.args = args - #: keyword argument dictionary, empty if nothing specified - self.kwargs = kwargs.copy() - self._arglist = [(args, kwargs.copy())] + self._mark_list = [mark] def __repr__(self): return "" % (self.name, self.args, self.kwargs) - def add(self, args, kwargs): + def add(self, mark): """ add a MarkInfo with the given args and kwargs. """ - self._arglist.append((args, kwargs)) - self.args += args - self.kwargs.update(kwargs) + self._mark_list.append(mark) + self.mark += mark def __iter__(self): """ yield MarkInfo objects each relating to a marking-call. """ - for args, kwargs in self._arglist: - yield MarkInfo(self.name, args, kwargs) + return iter(self._mark_list) + + +class Mark(object): + def __init__(self, name, args, kwargs): + self.name = name + self.args = args + self.kwargs = kwargs + + def __add__(self, other): + assert isinstance(other, Mark) + assert other.name == self.name + return Mark(self.name, self.args + other.args, + dict(self.kwargs, **other.kwargs)) + + def __repr__(self): + return "" % (self.name, self.args, + self.kwargs) class MarkGenerator(object): diff --git a/_pytest/mark/utils.py b/_pytest/mark/utils.py new file mode 100644 index 00000000000..5403f622741 --- /dev/null +++ b/_pytest/mark/utils.py @@ -0,0 +1,31 @@ +from .model import apply_mark + + +def _marked(func, mark): + """ Returns True if :func: is already marked with :mark:, False otherwise. + This can happen if marker is applied to class and the test file is + invoked more than once. + """ + try: + func_mark = getattr(func, mark.name) + except AttributeError: + return False + return mark.args == func_mark.args and mark.kwargs == func_mark.kwargs + + +def transfer_markers(funcobj, cls, mod): + # XXX this should rather be code in the mark plugin or the mark + # plugin should merge with the python plugin. + for holder in (cls, mod): + try: + pytestmark = holder.pytestmark + except AttributeError: + continue + if isinstance(pytestmark, list): + for mark in pytestmark: + + if not _marked(funcobj, mark): + apply_mark(mark=mark, obj=funcobj) + else: + if not _marked(funcobj, pytestmark): + apply_mark(mark=pytestmark, obj=funcobj) diff --git a/_pytest/python.py b/_pytest/python.py index a18f13b43cf..496031d202f 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -13,7 +13,7 @@ import pytest from _pytest._code.code import TerminalRepr from _pytest.mark import MarkDecorator, MarkerError - +from _pytest.mark.utils import transfer_markers try: import enum except ImportError: # pragma: no cover @@ -26,7 +26,6 @@ cutdir2 = py.path.local(_pytest.__file__).dirpath() cutdir1 = py.path.local(pluggy.__file__.rstrip("oc")) - NoneType = type(None) NOTSET = object() isfunction = inspect.isfunction @@ -40,15 +39,18 @@ _PY3 = sys.version_info > (3, 0) _PY2 = not _PY3 - if hasattr(inspect, 'signature'): + def _format_args(func): return str(inspect.signature(func)) else: + def _format_args(func): return inspect.formatargspec(*inspect.getargspec(func)) -if sys.version_info[:2] == (2, 6): + +if sys.version_info[:2] == (2, 6): + def isclass(object): """ Return true if the object is a class. Overrides inspect.isclass for python 2.6 because it will return True for objects which always return @@ -57,6 +59,7 @@ def isclass(object): """ return isinstance(object, (type, types.ClassType)) + def _has_positional_arg(func): return func.__code__.co_argcount @@ -85,6 +88,7 @@ def get_real_func(obj): obj = obj.func return obj + def getfslineno(obj): # xxx let decorators etc specify a sane ordering obj = get_real_func(obj) @@ -94,6 +98,7 @@ def getfslineno(obj): assert isinstance(fslineno[1], int), obj return fslineno + def getimfunc(func): try: return func.__func__ @@ -103,6 +108,7 @@ def getimfunc(func): except AttributeError: return func + def safe_getattr(object, name, default): """ Like getattr but return default upon any Exception. @@ -128,7 +134,7 @@ def __init__(self, scope, params, def __call__(self, function): if isclass(function): raise ValueError( - "class fixtures not supported (may be in the future)") + "class fixtures not supported (may be in the future)") function._pytestfixturefunction = self return function @@ -175,6 +181,7 @@ def fixture(scope="function", params=None, autouse=False, ids=None, name=None): params = list(params) return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name) + def yield_fixture(scope="function", params=None, autouse=False, ids=None): """ (return a) decorator to mark a yield-fixture factory function (EXPERIMENTAL). @@ -186,44 +193,68 @@ def yield_fixture(scope="function", params=None, autouse=False, ids=None): """ if callable(scope) and params is None and autouse == False: # direct decoration - return FixtureFunctionMarker( - "function", params, autouse, yieldctx=True)(scope) + return FixtureFunctionMarker("function", + params, + autouse, + yieldctx=True)(scope) else: - return FixtureFunctionMarker(scope, params, autouse, - yieldctx=True, ids=ids) + return FixtureFunctionMarker(scope, + params, + autouse, + yieldctx=True, + ids=ids) + defaultfuncargprefixmarker = fixture() + def pyobj_property(name): def get(self): node = self.getparent(getattr(pytest, name)) if node is not None: return node.obj + doc = "python %s object this node was collected from (can be None)." % ( - name.lower(),) + name.lower(), ) return property(get, None, None, doc) def pytest_addoption(parser): group = parser.getgroup("general") - group.addoption('--fixtures', '--funcargs', - action="store_true", dest="showfixtures", default=False, - help="show available fixtures, sorted by plugin appearance") - parser.addini("usefixtures", type="args", default=[], - help="list of default fixtures to be used with this project") - parser.addini("python_files", type="args", + group.addoption( + '--fixtures', + '--funcargs', + action="store_true", + dest="showfixtures", + default=False, + help="show available fixtures, sorted by plugin appearance") + parser.addini("usefixtures", + type="args", + default=[], + help="list of default fixtures to be used with this project") + parser.addini( + "python_files", + type="args", default=['test_*.py', '*_test.py'], help="glob-style file patterns for Python test module discovery") - parser.addini("python_classes", type="args", default=["Test",], + parser.addini( + "python_classes", + type="args", + default=["Test", ], help="prefixes or glob names for Python test class discovery") - parser.addini("python_functions", type="args", default=["test",], - help="prefixes or glob names for Python test function and " - "method discovery") - - group.addoption("--import-mode", default="prepend", - choices=["prepend", "append"], dest="importmode", + parser.addini("python_functions", + type="args", + default=["test", ], + help="prefixes or glob names for Python test function and " + "method discovery") + + group.addoption( + "--import-mode", + default="prepend", + choices=["prepend", "append"], + dest="importmode", help="prepend/append to sys.path when importing test modules, " - "default is to prepend.") + "default is to prepend.") def pytest_cmdline_main(config): @@ -247,8 +278,10 @@ def pytest_generate_tests(metafunc): for marker in markers: metafunc.parametrize(*marker.args, **marker.kwargs) + def pytest_configure(config): - config.addinivalue_line("markers", + config.addinivalue_line( + "markers", "parametrize(argnames, argvalues): call a test function multiple " "times passing in different arguments in turn. argvalues generally " "needs to be a list of values if argnames specifies only one name " @@ -256,16 +289,17 @@ def pytest_configure(config): "Example: @parametrize('arg1', [1,2]) would lead to two calls of the " "decorated test function, one with arg1=1 and another with arg1=2." "see http://pytest.org/latest/parametrize.html for more info and " - "examples." - ) - config.addinivalue_line("markers", + "examples.") + config.addinivalue_line( + "markers", "usefixtures(fixturename1, fixturename2, ...): mark tests as needing " - "all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures " - ) + "all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures ") + def pytest_sessionstart(session): session._fixturemanager = FixtureManager(session) + @pytest.hookimpl(trylast=True) def pytest_namespace(): raises.Exception = pytest.fail.Exception @@ -275,11 +309,16 @@ def pytest_namespace(): 'raises': raises, 'approx': approx, 'collect': { - 'Module': Module, 'Class': Class, 'Instance': Instance, - 'Function': Function, 'Generator': Generator, - '_fillfuncargs': fillfixtures} + 'Module': Module, + 'Class': Class, + 'Instance': Instance, + 'Function': Function, + 'Generator': Generator, + '_fillfuncargs': fillfixtures + } } + @fixture(scope="session") def pytestconfig(request): """ the pytest config object with access to command line opts.""" @@ -299,6 +338,7 @@ def pytest_pyfunc_call(pyfuncitem): testfunction(**testargs) return True + def pytest_collect_file(path, parent): ext = path.ext if ext == ".py": @@ -307,13 +347,15 @@ def pytest_collect_file(path, parent): if path.fnmatch(pat): break else: - return + return ihook = parent.session.gethookproxy(path) return ihook.pytest_pycollect_makemodule(path=path, parent=parent) + def pytest_pycollect_makemodule(path, parent): return Module(path, parent) + @pytest.hookimpl(hookwrapper=True) def pytest_pycollect_makeitem(collector, name, obj): outcome = yield @@ -332,9 +374,10 @@ def pytest_pycollect_makeitem(collector, name, obj): # or a funtools.wrapped. # We musn't if it's been wrapped with mock.patch (python 2 only) if not (isfunction(obj) or isfunction(get_real_func(obj))): - collector.warn(code="C2", message= - "cannot collect %r because it is not a function." - % name, ) + collector.warn( + code="C2", + message="cannot collect %r because it is not a function." % + name, ) elif getattr(obj, "__test__", True): if is_generator(obj): res = Generator(name, parent=collector) @@ -342,18 +385,22 @@ def pytest_pycollect_makeitem(collector, name, obj): res = list(collector._genfunctions(name, obj)) outcome.force_result(res) + def is_generator(func): try: - return _pytest._code.getrawcode(func).co_flags & 32 # generator function - except AttributeError: # builtin functions have no bytecode + return _pytest._code.getrawcode( + func).co_flags & 32 # generator function + except AttributeError: # builtin functions have no bytecode # assume them to not be generators return False + class PyobjContext(object): module = pyobj_property("Module") cls = pyobj_property("Class") instance = pyobj_property("Instance") + class PyobjMixin(PyobjContext): def obj(): def fget(self): @@ -362,9 +409,12 @@ def fget(self): except AttributeError: self._obj = obj = self._getobj() return obj + def fset(self, value): self._obj = value + return property(fget, fset, None, "underlying python object") + obj = obj() def _getobj(self): @@ -410,8 +460,8 @@ def reportinfo(self): assert isinstance(lineno, int) return fspath, lineno, modpath -class PyCollector(PyobjMixin, pytest.Collector): +class PyCollector(PyobjMixin, pytest.Collector): def funcnamefilter(self, name): return self._matches_prefix_or_glob_option('python_functions', name) @@ -428,10 +478,9 @@ def classnamefilter(self, name): return self._matches_prefix_or_glob_option('python_classes', name) def istestfunction(self, obj, name): - return ( - (self.funcnamefilter(name) or self.isnosetest(obj)) and - safe_getattr(obj, "__call__", False) and getfixturemarker(obj) is None - ) + return ((self.funcnamefilter(name) or self.isnosetest(obj)) and + safe_getattr(obj, "__call__", False) and + getfixturemarker(obj) is None) def istestclass(self, obj, name): return self.classnamefilter(name) or self.isnosetest(obj) @@ -479,8 +528,9 @@ def collect(self): def makeitem(self, name, obj): #assert self.ihook.fspath == self.fspath, self - return self.ihook.pytest_pycollect_makeitem( - collector=self, name=name, obj=obj) + return self.ihook.pytest_pycollect_makeitem(collector=self, + name=name, + obj=obj) def _genfunctions(self, name, funcobj): module = self.getparent(Module).obj @@ -489,16 +539,20 @@ def _genfunctions(self, name, funcobj): transfer_markers(funcobj, cls, module) fm = self.session._fixturemanager fixtureinfo = fm.getfixtureinfo(self, funcobj, cls) - metafunc = Metafunc(funcobj, fixtureinfo, self.config, - cls=cls, module=module) + metafunc = Metafunc(funcobj, + fixtureinfo, + self.config, + cls=cls, + module=module) methods = [] if hasattr(module, "pytest_generate_tests"): methods.append(module.pytest_generate_tests) if hasattr(cls, "pytest_generate_tests"): methods.append(cls().pytest_generate_tests) if methods: - self.ihook.pytest_generate_tests.call_extra(methods, - dict(metafunc=metafunc)) + self.ihook.pytest_generate_tests.call_extra( + methods, + dict(metafunc=metafunc)) else: self.ihook.pytest_generate_tests(metafunc=metafunc) @@ -510,11 +564,14 @@ def _genfunctions(self, name, funcobj): add_funcarg_pseudo_fixture_def(self, metafunc, fm) for callspec in metafunc._calls: - subname = "%s[%s]" %(name, callspec.id) - yield Function(name=subname, parent=self, - callspec=callspec, callobj=funcobj, + subname = "%s[%s]" % (name, callspec.id) + yield Function(name=subname, + parent=self, + callspec=callspec, + callobj=funcobj, fixtureinfo=fixtureinfo, - keywords={callspec.id:True}) + keywords={callspec.id: True}) + def add_funcarg_pseudo_fixture_def(collector, metafunc, fixturemanager): # this function will transform all collected calls to a functions @@ -524,7 +581,7 @@ def add_funcarg_pseudo_fixture_def(collector, metafunc, fixturemanager): # XXX we can probably avoid this algorithm if we modify CallSpec2 # to directly care for creating the fixturedefs within its methods. if not metafunc._calls[0].funcargs: - return # this function call does not have direct parametrization + return # this function call does not have direct parametrization # collect funcargs of all callspecs into a list of values arg2params = {} arg2scope = {} @@ -560,10 +617,9 @@ def add_funcarg_pseudo_fixture_def(collector, metafunc, fixturemanager): if node and argname in node._name2pseudofixturedef: arg2fixturedefs[argname] = [node._name2pseudofixturedef[argname]] else: - fixturedef = FixtureDef(fixturemanager, '', argname, - get_direct_param_fixture_func, - arg2scope[argname], - valuelist, False, False) + fixturedef = FixtureDef( + fixturemanager, '', argname, get_direct_param_fixture_func, + arg2scope[argname], valuelist, False, False) arg2fixturedefs[argname] = [fixturedef] if node is not None: node._name2pseudofixturedef[argname] = fixturedef @@ -572,6 +628,7 @@ def add_funcarg_pseudo_fixture_def(collector, metafunc, fixturemanager): def get_direct_param_fixture_func(request): return request.param + class FuncFixtureInfo: def __init__(self, argnames, names_closure, name2fixturedefs): self.argnames = argnames @@ -579,36 +636,9 @@ def __init__(self, argnames, names_closure, name2fixturedefs): self.name2fixturedefs = name2fixturedefs -def _marked(func, mark): - """ Returns True if :func: is already marked with :mark:, False otherwise. - This can happen if marker is applied to class and the test file is - invoked more than once. - """ - try: - func_mark = getattr(func, mark.name) - except AttributeError: - return False - return mark.args == func_mark.args and mark.kwargs == func_mark.kwargs - - -def transfer_markers(funcobj, cls, mod): - # XXX this should rather be code in the mark plugin or the mark - # plugin should merge with the python plugin. - for holder in (cls, mod): - try: - pytestmark = holder.pytestmark - except AttributeError: - continue - if isinstance(pytestmark, list): - for mark in pytestmark: - if not _marked(funcobj, mark): - mark(funcobj) - else: - if not _marked(funcobj, pytestmark): - pytestmark(funcobj) - class Module(pytest.File, PyCollector): """ Collector for test classes and functions. """ + def _getobj(self): return self._memoizedcall('_obj', self._importtestmodule) @@ -622,8 +652,8 @@ def _importtestmodule(self): try: mod = self.fspath.pyimport(ensuresyspath=importmode) except SyntaxError: - raise self.CollectError( - _pytest._code.ExceptionInfo().getrepr(style="short")) + raise self.CollectError(_pytest._code.ExceptionInfo().getrepr( + style="short")) except self.fspath.ImportMismatchError: e = sys.exc_info()[1] raise self.CollectError( @@ -633,9 +663,7 @@ def _importtestmodule(self): "which is not the same as the test file we want to collect:\n" " %s\n" "HINT: remove __pycache__ / .pyc files and/or use a " - "unique basename for your test file modules" - % e.args - ) + "unique basename for your test file modules" % e.args) except ImportError: exc_class, exc, _ = sys.exc_info() raise self.CollectError( @@ -676,10 +704,11 @@ def setup(self): class Class(PyCollector): """ Collector for test methods. """ + def collect(self): if hasinit(self.obj): self.warn("C1", "cannot collect test class %r because it has a " - "__init__ constructor" % self.obj.__name__) + "__init__ constructor" % self.obj.__name__) return [] return [self._getcustomclass("Instance")(name="()", parent=self)] @@ -696,6 +725,7 @@ def setup(self): fin_class = getattr(fin_class, '__func__', fin_class) self.addfinalizer(lambda: fin_class(self.obj)) + class Instance(PyCollector): def _getobj(self): obj = self.parent.obj() @@ -709,6 +739,7 @@ def newinstance(self): self.obj = self._getobj() return self.obj + class FunctionMixin(PyobjMixin): """ mixin for the code common to Function and Generator. """ @@ -762,7 +793,7 @@ def _repr_failure_py(self, excinfo, style="long"): if not excinfo.value.pytrace: return py._builtin._totext(excinfo.value) return super(FunctionMixin, self)._repr_failure_py(excinfo, - style=style) + style=style) def repr_failure(self, excinfo, outerr=None): assert outerr is None, "XXX outerr usage is deprecated" @@ -785,20 +816,22 @@ def collect(self): for i, x in enumerate(self.obj()): name, call, args = self.getcallargs(x) if not callable(call): - raise TypeError("%r yielded non callable test %r" %(self.obj, call,)) + raise TypeError("%r yielded non callable test %r" % (self.obj, + call, )) if name is None: name = "[%d]" % i else: name = "['%s']" % name if name in seen: - raise ValueError("%r generated tests with non-unique name %r" %(self, name)) + raise ValueError("%r generated tests with non-unique name %r" % + (self, name)) seen[name] = True l.append(self.Function(name, self, args=args, callobj=call)) return l def getcallargs(self, obj): if not isinstance(obj, (tuple, list)): - obj = (obj,) + obj = (obj, ) # explict naming if isinstance(obj[0], py.builtin._basestring): name = obj[0] @@ -816,7 +849,6 @@ def hasinit(obj): return True - def fillfixtures(function): """ fill missing funcargs for a test function. """ try: @@ -841,6 +873,7 @@ def fillfixtures(function): _notexists = object() + class CallSpec2(object): def __init__(self, metafunc): self.metafunc = metafunc @@ -869,7 +902,7 @@ def copy(self, metafunc): def _checkargnotcontained(self, arg): if arg in self.params or arg in self.funcargs: - raise ValueError("duplicate %r" %(arg,)) + raise ValueError("duplicate %r" % (arg, )) def getparam(self, name): try: @@ -885,7 +918,7 @@ def id(self): def setmulti(self, valtypes, argnames, valset, id, keywords, scopenum, param_index): - for arg,val in zip(argnames, valset): + for arg, val in zip(argnames, valset): self._checkargnotcontained(arg) valtype_for_arg = valtypes[arg] getattr(self, valtype_for_arg)[arg] = val @@ -913,11 +946,13 @@ class FuncargnamesCompatAttr: """ helper class so that Metafunc, Function and FixtureRequest don't need to each define the "funcargnames" compatibility attribute. """ + @property def funcargnames(self): """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" return self.fixturenames + class Metafunc(FuncargnamesCompatAttr): """ Metafunc objects are passed to the ``pytest_generate_tests`` hook. @@ -940,6 +975,7 @@ class Metafunc(FuncargnamesCompatAttr): .. deprecated:: 2.3 Use ``fixturenames`` instead. """ + def __init__(self, function, fixtureinfo, config, cls=None, module=None): self.config = config self.module = module @@ -950,8 +986,12 @@ def __init__(self, function, fixtureinfo, config, cls=None, module=None): self._calls = [] self._ids = py.builtin.set() - def parametrize(self, argnames, argvalues, indirect=False, ids=None, - scope=None): + def parametrize(self, + argnames, + argvalues, + indirect=False, + ids=None, + scope=None): """ Add new invocations to the underlying test function using the list of argvalues for the given argnames. Parametrization is performed during the collection phase. If you need to setup expensive resources @@ -997,8 +1037,8 @@ def parametrize(self, argnames, argvalues, indirect=False, ids=None, unwrapped_argvalues = [] for i, argval in enumerate(argvalues): while isinstance(argval, MarkDecorator): - newmark = MarkDecorator(argval.markname, - argval.args[:-1], argval.kwargs) + newmark = MarkDecorator(argval.markname, argval.args[:-1], + argval.kwargs) newmarks = newkeywords.setdefault(i, {}) newmarks[newmark.markname] = newmark argval = argval.args[-1] @@ -1008,9 +1048,9 @@ def parametrize(self, argnames, argvalues, indirect=False, ids=None, if not isinstance(argnames, (tuple, list)): argnames = [x.strip() for x in argnames.split(",") if x.strip()] if len(argnames) == 1: - argvalues = [(val,) for val in argvalues] + argvalues = [(val, ) for val in argvalues] if not argvalues: - argvalues = [(_notexists,) * len(argnames)] + argvalues = [(_notexists, ) * len(argnames)] if scope is None: scope = "function" @@ -1018,7 +1058,8 @@ def parametrize(self, argnames, argvalues, indirect=False, ids=None, valtypes = {} for arg in argnames: if arg not in self.fixturenames: - raise ValueError("%r uses no fixture %r" %(self.function, arg)) + raise ValueError("%r uses no fixture %r" % + (self.function, arg)) if indirect is True: valtypes = dict.fromkeys(argnames, "params") @@ -1028,25 +1069,26 @@ def parametrize(self, argnames, argvalues, indirect=False, ids=None, valtypes = dict.fromkeys(argnames, "funcargs") for arg in indirect: if arg not in argnames: - raise ValueError("indirect given to %r: fixture %r doesn't exist" %( - self.function, arg)) + raise ValueError( + "indirect given to %r: fixture %r doesn't exist" % ( + self.function, arg)) valtypes[arg] = "params" idfn = None if callable(ids): idfn = ids ids = None if ids and len(ids) != len(argvalues): - raise ValueError('%d tests specified with %d ids' %( - len(argvalues), len(ids))) + raise ValueError('%d tests specified with %d ids' % + (len(argvalues), len(ids))) ids = idmaker(argnames, argvalues, idfn, ids) newcalls = [] for callspec in self._calls or [CallSpec2(self)]: for param_index, valset in enumerate(argvalues): assert len(valset) == len(argnames) newcallspec = callspec.copy(self) - newcallspec.setmulti(valtypes, argnames, valset, ids[param_index], - newkeywords.get(param_index, {}), scopenum, - param_index) + newcallspec.setmulti( + valtypes, argnames, valset, ids[param_index], + newkeywords.get(param_index, {}), scopenum, param_index) newcalls.append(newcallspec) self._calls = newcalls @@ -1157,7 +1199,7 @@ def _idval(val, argname, idx, idfn): return str(val) elif isclass(val) and hasattr(val, '__name__'): return val.__name__ - return str(argname)+str(idx) + return str(argname) + str(idx) def _idvalset(idx, valset, argnames, idfn, ids): if ids is None or ids[idx] is None: @@ -1180,10 +1222,12 @@ def idmaker(argnames, argvalues, idfn=None, ids=None): counters[testid] += 1 return ids + def showfixtures(config): from _pytest.main import wrap_session return wrap_session(config, _showfixtures_main) + def _showfixtures_main(config, session): import _pytest.config session.perform_collect() @@ -1200,10 +1244,9 @@ def _showfixtures_main(config, session): continue fixturedef = fixturedefs[-1] loc = getlocation(fixturedef.func, curdir) - available.append((len(fixturedef.baseid), - fixturedef.func.__module__, - curdir.bestrelpath(loc), - fixturedef.argname, fixturedef)) + available.append((len(fixturedef.baseid), fixturedef.func.__module__, + curdir.bestrelpath( + loc), fixturedef.argname, fixturedef)) available.sort() currentmodule = None @@ -1211,12 +1254,12 @@ def _showfixtures_main(config, session): if currentmodule != module: if not module.startswith("_pytest."): tw.line() - tw.sep("-", "fixtures defined from %s" %(module,)) + tw.sep("-", "fixtures defined from %s" % (module, )) currentmodule = module if verbose <= 0 and argname[0] == "_": continue if verbose > 0: - funcargspec = "%s -- %s" %(argname, bestrel,) + funcargspec = "%s -- %s" % (argname, bestrel, ) else: funcargspec = argname tw.line(funcargspec, green=True) @@ -1226,8 +1269,8 @@ def _showfixtures_main(config, session): for line in doc.strip().split("\n"): tw.line(" " + line.strip()) else: - tw.line(" %s: no docstring available" %(loc,), - red=True) + tw.line(" %s: no docstring available" % (loc, ), red=True) + def getlocation(function, curdir): import inspect @@ -1235,10 +1278,11 @@ def getlocation(function, curdir): lineno = py.builtin._getcode(function).co_firstlineno if fn.relto(curdir): fn = fn.relto(curdir) - return "%s:%d" %(fn, lineno+1) + return "%s:%d" % (fn, lineno + 1) # builtin pytest.raises helper + def raises(expected_exception, *args, **kwargs): """ Assert that a code block/function call raises ``expected_exception`` @@ -1351,6 +1395,7 @@ def raises(expected_exception, *args, **kwargs): return _pytest._code.ExceptionInfo() pytest.fail("DID NOT RAISE {0}".format(expected_exception)) + class RaisesContext(object): def __init__(self, expected_exception): self.expected_exception = expected_exception @@ -1627,15 +1672,26 @@ def tolerance(self): # the basic pytest Function item # + class Function(FunctionMixin, pytest.Item, FuncargnamesCompatAttr): """ a Function Item is responsible for setting up and executing a Python test function. """ _genid = None - def __init__(self, name, parent, args=None, config=None, - callspec=None, callobj=NOTSET, keywords=None, session=None, + + def __init__(self, + name, + parent, + args=None, + config=None, + callspec=None, + callobj=NOTSET, + keywords=None, + session=None, fixtureinfo=None): - super(Function, self).__init__(name, parent, config=config, + super(Function, self).__init__(name, + parent, + config=config, session=session) self._args = args if callobj is not NOTSET: @@ -1650,7 +1706,9 @@ def __init__(self, name, parent, args=None, config=None, if fixtureinfo is None: fixtureinfo = self.session._fixturemanager.getfixtureinfo( - self.parent, self.obj, self.cls, + self.parent, + self.obj, + self.cls, funcargs=not self._isyieldedfunction()) self._fixtureinfo = fixtureinfo self.fixturenames = fixtureinfo.names_closure @@ -1677,7 +1735,7 @@ def function(self): def _getobj(self): name = self.name - i = name.find("[") # parametrization + i = name.find("[") # parametrization if i != -1: name = name[:i] return getattr(self.parent.obj, name) @@ -1702,27 +1760,31 @@ def setup(self): pass else: fs, lineno = self._getfslineno() - pytest.skip("got empty parameter set, function %s at %s:%d" %( - self.function.__name__, fs, lineno)) + pytest.skip("got empty parameter set, function %s at %s:%d" % + (self.function.__name__, fs, lineno)) super(Function, self).setup() fillfixtures(self) scope2props = dict(session=()) scope2props["module"] = ("fspath", "module") -scope2props["class"] = scope2props["module"] + ("cls",) +scope2props["class"] = scope2props["module"] + ("cls", ) scope2props["instance"] = scope2props["class"] + ("instance", ) scope2props["function"] = scope2props["instance"] + ("function", "keywords") + def scopeproperty(name=None, doc=None): def decoratescope(func): scopename = name or func.__name__ + def provide(self): if func.__name__ in scope2props[self.scope]: return func(self) - raise AttributeError("%s not available in %s-scoped context" % ( - scopename, self.scope)) + raise AttributeError("%s not available in %s-scoped context" % + (scopename, self.scope)) + return property(provide, None, None, func.__doc__) + return decoratescope @@ -1740,7 +1802,7 @@ def __init__(self, pyfuncitem): self.fixturename = None #: Scope string, one of "function", "cls", "module", "session" self.scope = "function" - self._funcargs = {} + self._funcargs = {} self._fixturedefs = {} fixtureinfo = pyfuncitem._fixtureinfo self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() @@ -1753,7 +1815,6 @@ def node(self): """ underlying collection node (depends on current request scope)""" return self._getscopeitem(self.scope) - def _getnextfixturedef(self, argname): fixturedefs = self._arg2fixturedefs.get(argname, None) if fixturedefs is None: @@ -1761,7 +1822,7 @@ def _getnextfixturedef(self, argname): # getfuncargvalue(argname) usage which was naturally # not known at parsing/collection time fixturedefs = self._fixturemanager.getfixturedefs( - argname, self._pyfuncitem.parent.nodeid) + argname, self._pyfuncitem.parent.nodeid) self._arg2fixturedefs[argname] = fixturedefs # fixturedefs list is immutable so we maintain a decreasing index index = self._arg2index.get(argname, 0) - 1 @@ -1775,7 +1836,6 @@ def config(self): """ the pytest config object associated with this request. """ return self._pyfuncitem.config - @scopeproperty() def function(self): """ test function object if the request has a per-function scope. """ @@ -1828,8 +1888,8 @@ def addfinalizer(self, finalizer): def _addfinalizer(self, finalizer, scope): colitem = self._getscopeitem(scope) - self._pyfuncitem.session._setupstate.addfinalizer( - finalizer=finalizer, colitem=colitem) + self._pyfuncitem.session._setupstate.addfinalizer(finalizer=finalizer, + colitem=colitem) def applymarker(self, marker): """ Apply a marker to a single test function invocation. @@ -1855,7 +1915,11 @@ def _fillfixtures(self): if argname not in item.funcargs: item.funcargs[argname] = self.getfuncargvalue(argname) - def cached_setup(self, setup, teardown=None, scope="module", extrakey=None): + def cached_setup(self, + setup, + teardown=None, + scope="module", + extrakey=None): """ (deprecated) Return a testing resource managed by ``setup`` & ``teardown`` calls. ``scope`` and ``extrakey`` determine when the ``teardown`` function will be called so that subsequent calls to @@ -1871,7 +1935,7 @@ def cached_setup(self, setup, teardown=None, scope="module", extrakey=None): :arg extrakey: added to internal caching key of (funcargname, scope). """ if not hasattr(self.config, '_setupcache'): - self.config._setupcache = {} # XXX weakref? + self.config._setupcache = {} # XXX weakref? cachekey = (self.fixturename, self._getscopeitem(scope), extrakey) cache = self.config._setupcache try: @@ -1881,9 +1945,11 @@ def cached_setup(self, setup, teardown=None, scope="module", extrakey=None): val = setup() cache[cachekey] = val if teardown is not None: + def finalizer(): del cache[cachekey] teardown(val) + self._addfinalizer(finalizer, scope=scope) return val @@ -1906,9 +1972,11 @@ def _get_active_fixturedef(self, argname): fixturedef = self._getnextfixturedef(argname) except FixtureLookupError: if argname == "request": + class PseudoFixtureDef: cached_result = (self, [0], None) scope = "function" + return PseudoFixtureDef raise # remove indent to prevent the python3 exception @@ -1972,10 +2040,11 @@ def _check_scope(self, argname, invoking_scope, requested_scope): if scopemismatch(invoking_scope, requested_scope): # try to report something helpful lines = self._factorytraceback() - pytest.fail("ScopeMismatch: You tried to access the %r scoped " + pytest.fail( + "ScopeMismatch: You tried to access the %r scoped " "fixture %r with a %r scoped request object, " - "involved factories\n%s" %( - (requested_scope, argname, invoking_scope, "\n".join(lines))), + "involved factories\n%s" % + ((requested_scope, argname, invoking_scope, "\n".join(lines))), pytrace=False) def _factorytraceback(self): @@ -1985,8 +2054,8 @@ def _factorytraceback(self): fs, lineno = getfslineno(factory) p = self._pyfuncitem.session.fspath.bestrelpath(fs) args = _format_args(factory) - lines.append("%s:%d: def %s%s" %( - p, lineno, factory.__name__, args)) + lines.append("%s:%d: def %s%s" % (p, lineno, factory.__name__, + args)) return lines def _getscopeitem(self, scope): @@ -2001,12 +2070,13 @@ def _getscopeitem(self, scope): return node def __repr__(self): - return "" %(self.node) + return "" % (self.node) class SubRequest(FixtureRequest): """ a sub request for handling getting a fixture from a test function/fixture. """ + def __init__(self, request, scope, param, param_index, fixturedef): self._parent_request = request self.fixturename = fixturedef.argname @@ -2017,7 +2087,7 @@ def __init__(self, request, scope, param, param_index, fixturedef): self._fixturedef = fixturedef self.addfinalizer = fixturedef.addfinalizer self._pyfuncitem = request._pyfuncitem - self._funcargs = request._funcargs + self._funcargs = request._funcargs self._fixturedefs = request._fixturedefs self._arg2fixturedefs = request._arg2fixturedefs self._arg2index = request._arg2index @@ -2033,14 +2103,18 @@ class ScopeMismatchError(Exception): which has a lower scope (e.g. a Session one calls a function one) """ + scopes = "session module class function".split() scopenum_function = scopes.index("function") + + def scopemismatch(currentscope, newscope): return scopes.index(newscope) > scopes.index(currentscope) class FixtureLookupError(LookupError): """ could not return a requested Fixture (missing or invalid). """ + def __init__(self, argname, request, msg=None): self.argname = argname self.request = request @@ -2063,9 +2137,9 @@ def formatrepr(self): lines, _ = inspect.getsourcelines(get_real_func(function)) except (IOError, IndexError): error_msg = "file %s, line %s: source code not available" - addline(error_msg % (fspath, lineno+1)) + addline(error_msg % (fspath, lineno + 1)) else: - addline("file %s, line %s" % (fspath, lineno+1)) + addline("file %s, line %s" % (fspath, lineno + 1)) for i, line in enumerate(lines): line = line.rstrip() addline(" " + line) @@ -2080,11 +2154,13 @@ def formatrepr(self): faclist = list(fm._matchfactories(fixturedef, parentid)) if faclist: available.append(name) - msg = "fixture %r not found" % (self.argname,) - msg += "\n available fixtures: %s" %(", ".join(available),) + msg = "fixture %r not found" % (self.argname, ) + msg += "\n available fixtures: %s" % (", ".join(available), ) msg += "\n use 'py.test --fixtures [testpath]' for help on them." - return FixtureLookupErrorRepr(fspath, lineno, tblines, msg, self.argname) + return FixtureLookupErrorRepr(fspath, lineno, tblines, msg, + self.argname) + class FixtureLookupErrorRepr(TerminalRepr): def __init__(self, filename, firstlineno, tblines, errorstring, argname): @@ -2101,7 +2177,8 @@ def toterminal(self, tw): for line in self.errorstring.split("\n"): tw.line(" " + line.strip(), red=True) tw.line() - tw.line("%s:%d" % (self.filename, self.firstlineno+1)) + tw.line("%s:%d" % (self.filename, self.firstlineno + 1)) + class FixtureManager: """ @@ -2145,10 +2222,10 @@ def __init__(self, session): self._arg2fixturedefs = {} self._holderobjseen = set() self._arg2finish = {} - self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))] + self._nodeid_and_autousenames = [("", + self.config.getini("usefixtures"))] session.config.pluginmanager.register(self, "funcmanage") - def getfixtureinfo(self, node, func, cls, funcargs=True): if funcargs and not hasattr(node, "nofuncargs"): if cls is not None: @@ -2190,13 +2267,12 @@ def _getautousenames(self, nodeid): if nodeid.startswith(baseid): if baseid: i = len(baseid) - nextchar = nodeid[i:i+1] + nextchar = nodeid[i:i + 1] if nextchar and nextchar not in ":/": continue autousenames.extend(basenames) # make sure autousenames are sorted by scope, scopenum 0 is session - autousenames.sort( - key=lambda x: self._arg2fixturedefs[x][-1].scopenum) + autousenames.sort(key=lambda x: self._arg2fixturedefs[x][-1].scopenum) return autousenames def getfixtureclosure(self, fixturenames, parentnode): @@ -2209,10 +2285,12 @@ def getfixtureclosure(self, fixturenames, parentnode): parentid = parentnode.nodeid fixturenames_closure = self._getautousenames(parentid) + def merge(otherlist): for arg in otherlist: if arg not in fixturenames_closure: fixturenames_closure.append(arg) + merge(fixturenames) arg2fixturedefs = {} lastlen = -1 @@ -2233,17 +2311,22 @@ def pytest_generate_tests(self, metafunc): if faclist: fixturedef = faclist[-1] if fixturedef.params is not None: - func_params = getattr(getattr(metafunc.function, 'parametrize', None), 'args', [[None]]) + func_params = getattr( + getattr(metafunc.function, 'parametrize', + None), 'args', [[None]]) # skip directly parametrized arguments argnames = func_params[0] if not isinstance(argnames, (tuple, list)): - argnames = [x.strip() for x in argnames.split(",") if x.strip()] + argnames = [x.strip() + for x in argnames.split(",") if x.strip()] if argname not in func_params and argname not in argnames: - metafunc.parametrize(argname, fixturedef.params, - indirect=True, scope=fixturedef.scope, + metafunc.parametrize(argname, + fixturedef.params, + indirect=True, + scope=fixturedef.scope, ids=fixturedef.ids) else: - continue # will raise FixtureLookupError at setup time + continue # will raise FixtureLookupError at setup time def pytest_collection_modifyitems(self, items): # separate parametrized setups @@ -2279,10 +2362,15 @@ def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False): if marker.name: name = marker.name assert not name.startswith(self._argprefix) - fixturedef = FixtureDef(self, nodeid, name, obj, - marker.scope, marker.params, + fixturedef = FixtureDef(self, + nodeid, + name, + obj, + marker.scope, + marker.params, yieldctx=marker.yieldctx, - unittest=unittest, ids=marker.ids) + unittest=unittest, + ids=marker.ids) faclist = self._arg2fixturedefs.setdefault(name, []) if fixturedef.has_location: faclist.append(fixturedef) @@ -2314,43 +2402,59 @@ def _matchfactories(self, fixturedefs, nodeid): def fail_fixturefunc(fixturefunc, msg): fs, lineno = getfslineno(fixturefunc) - location = "%s:%s" % (fs, lineno+1) + location = "%s:%s" % (fs, lineno + 1) source = _pytest._code.Source(fixturefunc) pytest.fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False) + def call_fixture_func(fixturefunc, request, kwargs, yieldctx): if yieldctx: if not is_generator(fixturefunc): - fail_fixturefunc(fixturefunc, + fail_fixturefunc( + fixturefunc, msg="yield_fixture requires yield statement in function") iter = fixturefunc(**kwargs) next = getattr(iter, "__next__", None) if next is None: next = getattr(iter, "next") res = next() + def teardown(): try: next() except StopIteration: pass else: - fail_fixturefunc(fixturefunc, + fail_fixturefunc( + fixturefunc, "yield_fixture function has more than one 'yield'") + request.addfinalizer(teardown) else: if is_generator(fixturefunc): - fail_fixturefunc(fixturefunc, + fail_fixturefunc( + fixturefunc, msg="pytest.fixture functions cannot use ``yield``. " - "Instead write and return an inner function/generator " - "and let the consumer call and iterate over it.") + "Instead write and return an inner function/generator " + "and let the consumer call and iterate over it.") res = fixturefunc(**kwargs) return res + class FixtureDef: """ A container for a factory definition. """ - def __init__(self, fixturemanager, baseid, argname, func, scope, params, - yieldctx, unittest=False, ids=None): + + def __init__(self, + fixturemanager, + baseid, + argname, + func, + scope, + params, + yieldctx, + unittest=False, + ids=None): self._fixturemanager = fixturemanager self.baseid = baseid or '' self.has_location = baseid is not None @@ -2434,6 +2538,7 @@ def __repr__(self): return ("" % (self.argname, self.scope, self.baseid)) + def num_mock_patch_args(function): """ return number of arguments used up by mock arguments (if any) """ patchings = getattr(function, "patchings", None) @@ -2441,8 +2546,9 @@ def num_mock_patch_args(function): return 0 mock = sys.modules.get("mock", sys.modules.get("unittest.mock", None)) if mock is not None: - return len([p for p in patchings - if not p.attribute_name and p.new is mock.DEFAULT]) + return len([p + for p in patchings + if not p.attribute_name and p.new is mock.DEFAULT]) return len(patchings) @@ -2478,6 +2584,7 @@ def getfuncargnames(function, startindex=None): # down to the lower scopes such as to minimize number of "high scope" # setups and teardowns + def reorder_items(items): argkeys_cache = {} for scopenum in range(0, scopenum_function): @@ -2488,6 +2595,7 @@ def reorder_items(items): d[item] = keys return reorder_items_atscope(items, set(), argkeys_cache, 0) + def reorder_items_atscope(items, ignore, argkeys_cache, scopenum): if scopenum >= scopenum_function or len(items) < 3: return items @@ -2495,8 +2603,8 @@ def reorder_items_atscope(items, ignore, argkeys_cache, scopenum): while 1: items_before, items_same, items_other, newignore = \ slice_items(items, ignore, argkeys_cache[scopenum]) - items_before = reorder_items_atscope( - items_before, ignore, argkeys_cache,scopenum+1) + items_before = reorder_items_atscope(items_before, ignore, + argkeys_cache, scopenum + 1) if items_same is None: # nothing to reorder in this scope assert items_other is None @@ -2536,6 +2644,7 @@ def slice_items(items, ignore, scoped_argkeys_cache): return (items_before, items_same, items_other, newignore) return items, None, None, None + def get_parametrized_fixture_keys(item, scopenum): """ return list of keys for all parametrized arguments which match the specified scope. """ @@ -2551,7 +2660,7 @@ def get_parametrized_fixture_keys(item, scopenum): for argname, param_index in cs.indices.items(): if cs._arg2scopenum[argname] != scopenum: continue - if scopenum == 0: # session + if scopenum == 0: # session key = (argname, param_index) elif scopenum == 1: # module key = (argname, param_index, item.fspath) @@ -2565,6 +2674,7 @@ def xunitsetup(obj, name): if getfixturemarker(meth) is None: return meth + def getfixturemarker(obj): """ return fixturemarker or None if it doesn't exist or raised exceptions.""" @@ -2577,11 +2687,10 @@ def getfixturemarker(obj): # we don't expect them to be fixture functions return None -scopename2class = { - 'class': Class, - 'module': Module, - 'function': pytest.Item, -} + +scopename2class = {'class': Class, 'module': Module, 'function': pytest.Item, } + + def get_scope_node(node, scope): cls = scopename2class.get(scope) if cls is None: diff --git a/testing/test_mark.py b/testing/test_mark.py index aa1be6f7c66..5e47e9ff3fd 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -3,10 +3,12 @@ import py, pytest from _pytest.mark import MarkGenerator as Mark + class TestMark: def test_markinfo_repr(self): from _pytest.mark import MarkInfo - m = MarkInfo("hello", (1,2), {}) + from _pytest.mark.model import Mark + m = MarkInfo(Mark("hello", (1, 2), {})) repr(m) def test_pytest_exists_in_namespace_all(self): @@ -23,15 +25,19 @@ def test_pytest_mark_name_starts_with_underscore(self): def test_pytest_mark_bare(self): mark = Mark() + def f(): pass + mark.hello(f) assert f.hello def test_pytest_mark_keywords(self): mark = Mark() + def f(): pass + mark.world(x=3, y=4)(f) assert f.world assert f.world.kwargs['x'] == 3 @@ -39,8 +45,10 @@ def f(): def test_apply_multiple_and_merge(self): mark = Mark() + def f(): pass + mark.world mark.world(x=3)(f) assert f.world.kwargs['x'] == 3 @@ -53,33 +61,43 @@ def f(): def test_pytest_mark_positional(self): mark = Mark() + def f(): pass + mark.world("hello")(f) assert f.world.args[0] == "hello" mark.world("world")(f) def test_pytest_mark_positional_func_and_keyword(self): mark = Mark() + def f(): raise Exception + m = mark.world(f, omega="hello") + def g(): pass + assert m(g) == g assert g.world.args[0] is f assert g.world.kwargs["omega"] == "hello" def test_pytest_mark_reuse(self): mark = Mark() + def f(): pass + w = mark.some w("hello", reason="123")(f) assert f.some.args[0] == "hello" assert f.some.kwargs['reason'] == "123" + def g(): pass + w("world", reason2="456")(g) assert g.some.args[0] == "world" assert 'reason' not in g.some.kwargs @@ -120,6 +138,7 @@ def test_markers(pytestconfig): rec = testdir.inline_run() rec.assertoutcome(passed=1) + def test_markers_option(testdir): testdir.makeini(""" [pytest] @@ -133,6 +152,7 @@ def test_markers_option(testdir): "*a1some*another marker", ]) + def test_markers_option_with_plugin_in_current_dir(testdir): testdir.makeconftest('pytest_plugins = "flip_flop"') testdir.makepyfile(flip_flop="""\ @@ -166,6 +186,7 @@ def test_hello(): reprec = testdir.inline_run() reprec.assertoutcome(passed=1) + def test_strict_prohibits_unregistered_markers(testdir): testdir.makepyfile(""" import pytest @@ -175,15 +196,15 @@ def test_hello(): """) result = testdir.runpytest("--strict") assert result.ret != 0 - result.stdout.fnmatch_lines([ - "*unregisteredmark*not*registered*", - ]) + result.stdout.fnmatch_lines(["*unregisteredmark*not*registered*", ]) + @pytest.mark.parametrize("spec", [ - ("xyz", ("test_one",)), - ("xyz and xyz2", ()), - ("xyz2", ("test_two",)), - ("xyz or xyz2", ("test_one", "test_two"),) + ("xyz", + ("test_one", )), ("xyz and xyz2", + ()), ("xyz2", + ("test_two", )), ("xyz or xyz2", + ("test_one", "test_two"), ) ]) def test_mark_option(spec, testdir): testdir.makepyfile(""" @@ -202,9 +223,10 @@ def test_two(): assert len(passed) == len(passed_result) assert list(passed) == list(passed_result) + @pytest.mark.parametrize("spec", [ - ("interface", ("test_interface",)), - ("not interface", ("test_nointer",)), + ("interface", ("test_interface", )), + ("not interface", ("test_nointer", )), ]) def test_mark_option_custom(spec, testdir): testdir.makeconftest(""" @@ -227,11 +249,12 @@ def test_nointer(): assert len(passed) == len(passed_result) assert list(passed) == list(passed_result) + @pytest.mark.parametrize("spec", [ - ("interface", ("test_interface",)), - ("not interface", ("test_nointer", "test_pass")), - ("pass", ("test_pass",)), - ("not pass", ("test_interface", "test_nointer")), + ("interface", ("test_interface", )), + ("not interface", ("test_nointer", "test_pass")), + ("pass", ("test_pass", )), + ("not pass", ("test_interface", "test_nointer")), ]) def test_keyword_option_custom(spec, testdir): testdir.makepyfile(""" @@ -251,9 +274,10 @@ def test_pass(): @pytest.mark.parametrize("spec", [ - ("None", ("test_func[None]",)), - ("1.3", ("test_func[1.3]",)), - ("2-3", ("test_func[2-3]",)) + ("None", + ("test_func[None]", )), ("1.3", + ("test_func[1.3]", )), ("2-3", + ("test_func[2-3]", )) ]) def test_keyword_option_parametrize(spec, testdir): testdir.makepyfile(""" @@ -285,8 +309,8 @@ def test_func(arg): rec.assertoutcome(passed=3) -class TestFunctional: +class TestFunctional: def test_mark_per_function(self, testdir): p = testdir.makepyfile(""" import pytest @@ -378,7 +402,7 @@ def test_func(self): # test the new __iter__ interface l = list(marker) assert len(l) == 3 - assert l[0].args == ("pos0",) + assert l[0].args == ("pos0", ) assert l[1].args == () assert l[2].args == ("pos1", ) @@ -398,7 +422,7 @@ def test_d(self): """) items, rec = testdir.inline_genitems(p) for item in items: - print (item, item.keywords) + print(item, item.keywords) assert 'a' in item.keywords def test_mark_decorator_subclass_does_not_propagate_to_base(self, testdir): @@ -416,7 +440,7 @@ class Test2(Base): def test_bar(self): pass """) items, rec = testdir.inline_genitems(p) - self.assert_markers(items, test_foo=('a', 'b'), test_bar=('a',)) + self.assert_markers(items, test_foo=('a', 'b'), test_bar=('a', )) def test_mark_decorator_baseclasses_merged(self, testdir): p = testdir.makepyfile(""" @@ -437,7 +461,8 @@ class Test2(Base2): def test_bar(self): pass """) items, rec = testdir.inline_genitems(p) - self.assert_markers(items, test_foo=('a', 'b', 'c'), + self.assert_markers(items, + test_foo=('a', 'b', 'c'), test_bar=('a', 'b', 'd')) def test_mark_with_wrong_marker(self, testdir): @@ -466,9 +491,7 @@ def test_func(arg): pass """) result = testdir.runpytest() - result.stdout.fnmatch_lines([ - "keyword: *hello*" - ]) + result.stdout.fnmatch_lines(["keyword: *hello*"]) def test_merging_markers_two_functions(self, testdir): p = testdir.makepyfile(""" @@ -484,8 +507,8 @@ def test_func(): marker = keywords['hello'] l = list(marker) assert len(l) == 2 - assert l[0].args == ("pos0",) - assert l[1].args == ("pos1",) + assert l[0].args == ("pos0", ) + assert l[1].args == ("pos1", ) def test_no_marker_match_on_unmarked_names(self, testdir): p = testdir.makepyfile(""" @@ -559,7 +582,8 @@ def assert_markers(self, items, **expected): items = dict((x.name, x) for x in items) for name, expected_markers in expected.items(): markers = items[name].keywords._markers - marker_names = set([name for (name, v) in markers.items() + marker_names = set([name + for (name, v) in markers.items() if isinstance(v, MarkInfo)]) assert marker_names == set(expected_markers) @@ -573,6 +597,7 @@ class TestClass(object): def test_method_one(self): assert 42 == 43 """) + def check(keyword, name): reprec = testdir.inline_run("-s", "-k", keyword, file_test) passed, skipped, failed = reprec.listoutcomes() @@ -586,7 +611,8 @@ def check(keyword, name): @pytest.mark.parametrize("keyword", [ 'xxx', 'xxx and test_2', 'TestClass', 'xxx and not test_1', - 'TestClass and test_2', 'xxx and TestClass and test_2']) + 'TestClass and test_2', 'xxx and TestClass and test_2' + ]) def test_select_extra_keywords(self, testdir, keyword): p = testdir.makepyfile(test_select=""" def test_1(): @@ -659,6 +685,7 @@ def test_no_magic_values(self, testdir): p = testdir.makepyfile(""" def test_one(): assert 1 """) + def assert_test_is_not_selected(keyword): reprec = testdir.inline_run("-k", keyword, p) passed, skipped, failed = reprec.countoutcomes() @@ -669,4 +696,3 @@ def assert_test_is_not_selected(keyword): assert_test_is_not_selected("__") assert_test_is_not_selected("()") - From ea3a402d25ad17630a5e6e729f7933cd70cac80b Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 24 Apr 2016 13:00:18 +0200 Subject: [PATCH 12/15] adapt mark evaluation to the new mark model --- _pytest/mark/evalexpr.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/_pytest/mark/evalexpr.py b/_pytest/mark/evalexpr.py index e3f4b8bba82..d51864ad0fd 100644 --- a/_pytest/mark/evalexpr.py +++ b/_pytest/mark/evalexpr.py @@ -72,19 +72,18 @@ def _istrue(self): if self.holder.args: self.result = False # "holder" might be a MarkInfo or a MarkDecorator; only - # MarkInfo keeps track of all parameters it received in an - # _arglist attribute - if hasattr(self.holder, '_arglist'): - arglist = self.holder._arglist - else: - arglist = [(self.holder.args, self.holder.kwargs)] - for args, kwargs in arglist: - for expr in args: + # MarkInfo keeps track of all parameters it received + try: + marklist = list(self.holder) + except: + marklist = [self.holder.mark] + for mark in marklist: + for expr in mark.args: self.expr = expr if isinstance(expr, py.builtin._basestring): result = cached_eval(self.item.config, expr, d) else: - if "reason" not in kwargs: + if "reason" not in mark.kwargs: # XXX better be checked at collection time msg = "you need to specify reason=STRING " \ "when using booleans as conditions." @@ -92,7 +91,7 @@ def _istrue(self): result = bool(expr) if result: self.result = True - self.reason = kwargs.get('reason', None) + self.reason = mark.kwargs.get('reason', None) self.expr = expr return self.result else: From c2cebdafda1ca6288bffb9366cb4b95c2793f3ee Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 24 Apr 2016 19:49:03 +0200 Subject: [PATCH 13/15] add _pytest.mark to setup.py --- setup.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6660f21604d..1f39b111b4b 100644 --- a/setup.py +++ b/setup.py @@ -75,7 +75,13 @@ def main(): # the following should be enabled for release install_requires=install_requires, extras_require=extras_require, - packages=['_pytest', '_pytest.assertion', '_pytest._code', '_pytest.vendored_packages'], + packages=[ + '_pytest', + '_pytest.assertion', + '_pytest._code', + '_pytest.vendored_packages', + '_pytest.mark', + ], py_modules=['pytest'], zip_safe=False, ) From 28a3594606fbbc3ce9172bb9e3677fc9b1d0019c Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 24 Apr 2016 22:13:09 +0200 Subject: [PATCH 14/15] mark plugin: remove python 2.5 compat code --- _pytest/mark/evalexpr.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/_pytest/mark/evalexpr.py b/_pytest/mark/evalexpr.py index d51864ad0fd..d00195e78f0 100644 --- a/_pytest/mark/evalexpr.py +++ b/_pytest/mark/evalexpr.py @@ -57,11 +57,7 @@ def istrue(self): def _getglobals(self): d = {'os': os, 'sys': sys, 'config': self.item.config} - func = self.item.obj - try: - d.update(func.__globals__) - except AttributeError: - d.update(func.func_globals) + d.update(self.item.obj.__globals__) return d def _istrue(self): From 8ec6a530eb0b28e9f9539a1e90875c171468ef3b Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 24 Apr 2016 22:21:06 +0200 Subject: [PATCH 15/15] marker plugin: restructure and simplify marker transfer --- _pytest/mark/utils.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/_pytest/mark/utils.py b/_pytest/mark/utils.py index 5403f622741..209546ca3f5 100644 --- a/_pytest/mark/utils.py +++ b/_pytest/mark/utils.py @@ -1,4 +1,4 @@ -from .model import apply_mark +from .model import apply_mark, MarkDecorator def _marked(func, mark): @@ -13,19 +13,22 @@ def _marked(func, mark): return mark.args == func_mark.args and mark.kwargs == func_mark.kwargs +def get_marks(obj): + try: + maybe_mark = obj.pytestmark + except AttributeError: + return [] + else: + if isinstance(maybe_mark, list): + return maybe_mark + elif isinstance(maybe_mark, MarkDecorator): + return [maybe_mark.mark] + else: + raise TypeError('%r is not a mark' % (maybe_mark,)) + + def transfer_markers(funcobj, cls, mod): - # XXX this should rather be code in the mark plugin or the mark - # plugin should merge with the python plugin. for holder in (cls, mod): - try: - pytestmark = holder.pytestmark - except AttributeError: - continue - if isinstance(pytestmark, list): - for mark in pytestmark: - - if not _marked(funcobj, mark): - apply_mark(mark=mark, obj=funcobj) - else: - if not _marked(funcobj, pytestmark): - apply_mark(mark=pytestmark, obj=funcobj) + for mark in get_marks(holder): + if not _marked(funcobj, mark): + apply_mark(mark=mark, obj=funcobj)