From 8adac2878f183e2250d1ed53457b60f19e7316f9 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 16 Jul 2012 10:46:44 +0200 Subject: [PATCH] put automatic funcarg_ API to Py*objects only, refine internal subclassing and initialisation logic --- _pytest/capture.py | 5 +- _pytest/main.py | 153 +++------------------------------------- _pytest/python.py | 156 +++++++++++++++++++++++++++++++++++++---- testing/test_python.py | 18 +++++ 4 files changed, 175 insertions(+), 157 deletions(-) diff --git a/_pytest/capture.py b/_pytest/capture.py index ea908da91..f260e2413 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -119,8 +119,9 @@ class CaptureManager: return "", "" def activate_funcargs(self, pyfuncitem): - if pyfuncitem.funcargs: - for name, capfuncarg in pyfuncitem.funcargs.items(): + funcargs = getattr(pyfuncitem, "funcargs", None) + if funcargs is not None: + for name, capfuncarg in funcargs.items(): if name in ('capsys', 'capfd'): assert not hasattr(self, '_capturing_funcarg') self._capturing_funcarg = capfuncarg diff --git a/_pytest/main.py b/_pytest/main.py index 89b6352f1..abc4c1a5e 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -3,7 +3,6 @@ import py import pytest, _pytest import os, sys, imp -from _pytest.monkeypatch import monkeypatch tracebackcutdir = py.path.local(_pytest.__file__).dirpath() @@ -151,130 +150,6 @@ def compatproperty(name): return property(fget) -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(),) - return property(get, None, None, doc) - -class Request(object): - _argprefix = "pytest_funcarg__" - - class LookupError(LookupError): - """ error while performing funcarg factory lookup. """ - - def _initattr(self): - self._name2factory = {} - self._currentarg = None - - @property - def _plugins(self): - extra = [obj for obj in (self.module, self.instance) if obj] - return self.getplugins() + extra - - def _getscopeitem(self, scope): - if scope == "function": - return self - elif scope == "session": - return None - elif scope == "class": - x = self.getparent(pytest.Class) - if x is not None: - return x - scope = "module" - if scope == "module": - return self.getparent(pytest.Module) - raise ValueError("unknown scope %r" %(scope,)) - - def getfuncargvalue(self, argname): - """ Retrieve a named function argument value. - - This function looks up a matching factory and invokes - it to obtain the return value. The factory receives - the same request object and can itself perform recursive - calls to this method, effectively allowing to make use of - multiple other funcarg values or to decorate values from - other name-matching factories. - """ - try: - return self.funcargs[argname] - except KeyError: - pass - except TypeError: - self.funcargs = getattr(self, "_funcargs", {}) - if argname not in self._name2factory: - self._name2factory[argname] = self.config.pluginmanager.listattr( - plugins=self._plugins, - attrname=self._argprefix + str(argname) - ) - #else: we are called recursively - if not self._name2factory[argname]: - self._raiselookupfailed(argname) - funcargfactory = self._name2factory[argname].pop() - oldarg = self._currentarg - mp = monkeypatch() - mp.setattr(self, '_currentarg', argname) - try: - param = self.callspec.getparam(argname) - except (AttributeError, ValueError): - pass - else: - mp.setattr(self, 'param', param, raising=False) - try: - self.funcargs[argname] = res = funcargfactory(self) - finally: - mp.undo() - return res - - def addfinalizer(self, finalizer): - """ add a no-args finalizer function to be called when the underlying - node is torn down.""" - self.session._setupstate.addfinalizer(finalizer, self) - - def cached_setup(self, setup, teardown=None, - scope="module", extrakey=None): - """ Return a cached testing resource created by ``setup`` & - detroyed by a respective ``teardown(resource)`` call. - - :arg teardown: function receiving a previously setup resource. - :arg setup: a no-argument function creating a resource. - :arg scope: a string value out of ``function``, ``class``, ``module`` - or ``session`` indicating the caching lifecycle of the resource. - :arg extrakey: added to internal caching key. - """ - if not hasattr(self.config, '_setupcache'): - self.config._setupcache = {} # XXX weakref? - colitem = self._getscopeitem(scope) - cachekey = (self._currentarg, colitem, extrakey) - cache = self.config._setupcache - try: - val = cache[cachekey] - except KeyError: - val = setup() - cache[cachekey] = val - if teardown is not None: - def finalizer(): - del cache[cachekey] - teardown(val) - self.session._setupstate.addfinalizer(finalizer, colitem) - return val - - def _raiselookupfailed(self, argname): - available = [] - for plugin in self._plugins: - for name in vars(plugin): - if name.startswith(self._argprefix): - name = name[len(self._argprefix):] - if name not in available: - available.append(name) - fspath, lineno, msg = self.reportinfo() - msg = "LookupError: no factory found for function argument %r" % (argname,) - msg += "\n available funcargs: %s" %(", ".join(available),) - msg += "\n use 'py.test --funcargs [testpath]' for help on them." - raise self.LookupError(msg) class Node(object): """ base class for Collector and Item the test collection tree. @@ -302,6 +177,12 @@ class Node(object): #: fspath sensitive hook proxy used to call pytest hooks self.ihook = self.session.gethookproxy(self.fspath) + self.extrainit() + + def extrainit(self): + """"extra initialization after Node is initialized. Implemented + by some subclasses. """ + Module = compatproperty("Module") Class = compatproperty("Class") Instance = compatproperty("Instance") @@ -309,11 +190,6 @@ class Node(object): File = compatproperty("File") Item = compatproperty("Item") - module = pyobj_property("Module") - cls = pyobj_property("Class") - instance = pyobj_property("Instance") - - def _getcustomclass(self, name): cls = getattr(self, name) if cls != getattr(pytest, name): @@ -476,20 +352,12 @@ class FSCollector(Collector): class File(FSCollector): """ base class for collecting tests from a file. """ -class Item(Node, Request): +class Item(Node): """ a basic test invocation item. Note that for a single function there might be multiple test invocation items. """ nextitem = None - def __init__(self, name, parent=None, config=None, session=None): - super(Item, self).__init__(name, parent, config, session) - self._initattr() - self.funcargs = None # later set to a dict from fillfuncargs() or - # from getfuncargvalue(). Setting it to - # None prevents users from performing - # "name in item.funcargs" checks too early. - def reportinfo(self): return self.fspath, None, "" @@ -532,10 +400,9 @@ class Session(FSCollector): __module__ = 'builtins' # for py3 def __init__(self, config): - super(Session, self).__init__(py.path.local(), parent=None, - config=config, session=self) - assert self.config.pluginmanager.register( - self, name="session", prepend=True) + FSCollector.__init__(self, py.path.local(), parent=None, + config=config, session=self) + self.config.pluginmanager.register(self, name="session", prepend=True) self._testsfailed = 0 self.shouldstop = False self.trace = config.trace.root.get("collection") diff --git a/_pytest/python.py b/_pytest/python.py index 9b36229ec..2b3a4b06d 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -4,7 +4,7 @@ import inspect import sys import pytest from py._code.code import TerminalRepr -from _pytest.main import Request, Item +from _pytest.monkeypatch import monkeypatch import _pytest cutdir = py.path.local(_pytest.__file__).dirpath() @@ -24,6 +24,135 @@ def cached_property(f): return x return property(get) +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(),) + return property(get, None, None, doc) + +class Request(object): + _argprefix = "pytest_funcarg__" + + class LookupError(LookupError): + """ error while performing funcarg factory lookup. """ + + def extrainit(self): + self._name2factory = {} + self._currentarg = None + self.funcargs = None # later set to a dict from fillfuncargs() or + # from getfuncargvalue(). Setting it to + # None prevents users from performing + # "name in item.funcargs" checks too early. + + @property + def _plugins(self): + extra = [obj for obj in (self.module, self.instance) if obj] + return self.getplugins() + extra + + def _getscopeitem(self, scope): + if scope == "function": + return self + elif scope == "session": + return None + elif scope == "class": + x = self.getparent(pytest.Class) + if x is not None: + return x + scope = "module" + if scope == "module": + return self.getparent(pytest.Module) + raise ValueError("unknown scope %r" %(scope,)) + + def getfuncargvalue(self, argname): + """ Retrieve a named function argument value. + + This function looks up a matching factory and invokes + it to obtain the return value. The factory receives + can itself perform recursive calls to this method, + either for using multiple other funcarg values under the hood + or to decorate values from other factories matching the same name. + """ + try: + return self.funcargs[argname] + except KeyError: + pass + except TypeError: + self.funcargs = getattr(self, "_funcargs", {}) + if argname not in self._name2factory: + self._name2factory[argname] = self.config.pluginmanager.listattr( + plugins=self._plugins, + attrname=self._argprefix + str(argname) + ) + #else: we are called recursively + if not self._name2factory[argname]: + self._raiselookupfailed(argname) + funcargfactory = self._name2factory[argname].pop() + oldarg = self._currentarg + mp = monkeypatch() + mp.setattr(self, '_currentarg', argname) + try: + param = self.callspec.getparam(argname) + except (AttributeError, ValueError): + pass + else: + mp.setattr(self, 'param', param, raising=False) + try: + self.funcargs[argname] = res = funcargfactory(self) + finally: + mp.undo() + return res + + def addfinalizer(self, finalizer): + """ add a no-args finalizer function to be called when the underlying + node is torn down.""" + self.session._setupstate.addfinalizer(finalizer, self) + + def cached_setup(self, setup, teardown=None, + scope="module", extrakey=None): + """ Return a cached testing resource created by ``setup`` & + detroyed by a respective ``teardown(resource)`` call. + + :arg teardown: function receiving a previously setup resource. + :arg setup: a no-argument function creating a resource. + :arg scope: a string value out of ``function``, ``class``, ``module`` + or ``session`` indicating the caching lifecycle of the resource. + :arg extrakey: added to internal caching key. + """ + if not hasattr(self.config, '_setupcache'): + self.config._setupcache = {} # XXX weakref? + colitem = self._getscopeitem(scope) + cachekey = (self._currentarg, colitem, extrakey) + cache = self.config._setupcache + try: + val = cache[cachekey] + except KeyError: + val = setup() + cache[cachekey] = val + if teardown is not None: + def finalizer(): + del cache[cachekey] + teardown(val) + self.session._setupstate.addfinalizer(finalizer, colitem) + return val + + def _raiselookupfailed(self, argname): + available = [] + for plugin in self._plugins: + for name in vars(plugin): + if name.startswith(self._argprefix): + name = name[len(self._argprefix):] + if name not in available: + available.append(name) + fspath, lineno, msg = self.reportinfo() + msg = "LookupError: no factory found for function argument %r" % (argname,) + msg += "\n available funcargs: %s" %(", ".join(available),) + msg += "\n use 'py.test --funcargs [testpath]' for help on them." + raise self.LookupError(msg) + + def pytest_addoption(parser): group = parser.getgroup("general") @@ -133,8 +262,12 @@ def is_generator(func): # assume them to not be generators return False -class PyobjMixin(object): +class PyobjContext(object): + module = pyobj_property("Module") + cls = pyobj_property("Class") + instance = pyobj_property("Instance") +class PyobjMixin(Request, PyobjContext): def obj(): def fget(self): try: @@ -203,7 +336,7 @@ class PyobjMixin(object): assert isinstance(lineno, int) return fspath, lineno, modpath -class PyCollectorMixin(PyobjMixin, pytest.Collector): +class PyCollector(PyobjMixin, pytest.Collector): def funcnamefilter(self, name): for prefix in self.config.getini("python_functions"): @@ -283,7 +416,7 @@ def transfer_markers(funcobj, cls, mod): else: pytestmark(funcobj) -class Module(pytest.File, PyCollectorMixin): +class Module(pytest.File, PyCollector): """ Collector for test classes and functions. """ def _getobj(self): return self._memoizedcall('_obj', self._importtestmodule) @@ -331,7 +464,7 @@ class Module(pytest.File, PyCollectorMixin): else: self.obj.teardown_module() -class Class(PyCollectorMixin, pytest.Collector): +class Class(PyCollector): """ Collector for test methods. """ def collect(self): return [self._getcustomclass("Instance")(name="()", parent=self)] @@ -350,7 +483,7 @@ class Class(PyCollectorMixin, pytest.Collector): teardown_class = getattr(teardown_class, '__func__', teardown_class) teardown_class(self.obj) -class Instance(PyCollectorMixin, pytest.Collector): +class Instance(PyCollector): def _getobj(self): return self.parent.obj() @@ -437,7 +570,7 @@ class FuncargLookupErrorRepr(TerminalRepr): tw.line("%s:%d" % (self.filename, self.firstlineno+1)) -class Generator(FunctionMixin, PyCollectorMixin, pytest.Collector): +class Generator(FunctionMixin, PyCollector): def collect(self): # test generators are seen as collectors but they also # invoke setup/teardown on popular request @@ -882,7 +1015,7 @@ def itemapi_property(name, set=False): return property(get, set, None, doc) -class OldFuncargRequest(Request): +class OldFuncargRequest(Request, PyobjContext): """ (deprecated) helper interactions with a test function invocation. Note that there is an optional ``param`` attribute in case @@ -892,9 +1025,11 @@ class OldFuncargRequest(Request): """ def __init__(self, pyfuncitem): self._pyfuncitem = pyfuncitem - Request._initattr(self) + Request.extrainit(self) + self.funcargs = pyfuncitem.funcargs self.getplugins = self._pyfuncitem.getplugins self.reportinfo = self._pyfuncitem.reportinfo + self.getparent = self._pyfuncitem.getparent try: self.param = self._pyfuncitem.param except AttributeError: @@ -906,9 +1041,6 @@ class OldFuncargRequest(Request): _getscopeitem = itemapi_property("_getscopeitem") funcargs = itemapi_property("funcargs", set=True) keywords = itemapi_property("keywords") - module = itemapi_property("module") - cls = itemapi_property("cls") - instance = itemapi_property("instance") config = itemapi_property("config") session = itemapi_property("session") fspath = itemapi_property("fspath") diff --git a/testing/test_python.py b/testing/test_python.py index 7861a71ad..0a46b8e9a 100644 --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1647,3 +1647,21 @@ class TestRequestAPI: ]) + +class TestResourceIntegrationFunctional: + def test_parametrize_with_ids(self, testdir): + testdir.makepyfile(""" + import pytest + def pytest_generate_tests(metafunc): + metafunc.parametrize(("a", "b"), [(1,1), (1,2)], + ids=["basic", "advanced"]) + + def test_function(a, b): + assert a == b + """) + result = testdir.runpytest("-v") + assert result.ret == 1 + result.stdout.fnmatch_lines([ + "*test_function*basic*PASSED", + "*test_function*advanced*FAILED", + ])