def test_get_real_func(): """Check that get_real_func correctly unwraps decorators until reaching the real function""" def decorator(f): @wraps(f) def inner(): pass if six.PY2: inner.__wrapped__ = f return inner def func(): pass wrapped_func = decorator(decorator(func)) assert get_real_func(wrapped_func) is func wrapped_func2 = decorator(decorator(wrapped_func)) assert get_real_func(wrapped_func2) is func # special case for __pytest_wrapped__ attribute: used to obtain the function up until the point # a function was wrapped by pytest itself wrapped_func2.__pytest_wrapped__ = _PytestWrapper(wrapped_func) assert get_real_func(wrapped_func2) is wrapped_func
def test_get_real_func_partial() -> None: """Test get_real_func handles partial instances correctly""" def foo(x): return x assert get_real_func(foo) is foo assert get_real_func(partial(foo)) is foo
def test_get_real_func(): """Check that get_real_func correctly unwraps decorators until reaching the real function""" def decorator(f): @wraps(f) def inner(): pass # pragma: no cover if six.PY2: inner.__wrapped__ = f return inner def func(): pass # pragma: no cover wrapped_func = decorator(decorator(func)) assert get_real_func(wrapped_func) is func wrapped_func2 = decorator(decorator(wrapped_func)) assert get_real_func(wrapped_func2) is func # special case for __pytest_wrapped__ attribute: used to obtain the function up until the point # a function was wrapped by pytest itself wrapped_func2.__pytest_wrapped__ = _PytestWrapper(wrapped_func) assert get_real_func(wrapped_func2) is wrapped_func
def pytest_pycollect_makeitem(collector, name, obj): outcome = yield res = outcome.get_result() if res is not None: return # nothing was collected elsewhere, let's do it here if safe_isclass(obj): if collector.istestclass(obj, name): outcome.force_result(Class(name, parent=collector)) elif collector.istestfunction(obj, name): # mock seems to store unbound methods (issue473), normalize it obj = getattr(obj, "__func__", obj) # We need to try and unwrap the function if it's a functools.partial # 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))): filename, lineno = getfslineno(obj) warnings.warn_explicit( message=PytestWarning( "cannot collect %r because it is not a function." % name ), category=None, filename=str(filename), lineno=lineno + 1, ) elif getattr(obj, "__test__", True): if is_generator(obj): res = Function(name, parent=collector) reason = deprecated.YIELD_TESTS.format(name=name) res.add_marker(MARK_GEN.xfail(run=False, reason=reason)) res.warn(PytestWarning(reason)) else: res = list(collector._genfunctions(name, obj)) outcome.force_result(res)
def getfslineno(obj: Any) -> Tuple[Union[str, py.path.local], int]: """ Return source location (path, lineno) for the given object. If the source cannot be determined return ("", -1). The line number is 0-based. """ from .code import Code # xxx let decorators etc specify a sane ordering # NOTE: this used to be done in _pytest.compat.getfslineno, initially added # in 6ec13a2b9. It ("place_as") appears to be something very custom. obj = get_real_func(obj) if hasattr(obj, "place_as"): obj = obj.place_as try: code = Code(obj) except TypeError: try: fn = inspect.getsourcefile(obj) or inspect.getfile(obj) except TypeError: return "", -1 fspath = fn and py.path.local(fn) or "" lineno = -1 if fspath: try: _, lineno = findsource(obj) except IOError: pass return fspath, lineno else: return code.path, code.firstlineno
def test_source_with_decorator() -> None: """Test behavior with Source / Code().source with regard to decorators.""" from _pytest.compat import get_real_func @pytest.mark.foo def deco_mark(): assert False src = inspect.getsource(deco_mark) assert textwrap.indent(str(Source(deco_mark)), " ") + "\n" == src assert src.startswith(" @pytest.mark.foo") @pytest.fixture def deco_fixture(): assert False src = inspect.getsource(deco_fixture) assert src == " @pytest.fixture\n def deco_fixture():\n assert False\n" # currently Source does not unwrap decorators, testing the # existing behavior here for explicitness, but perhaps we should revisit/change this # in the future assert str(Source(deco_fixture)).startswith("@functools.wraps(function)") assert ( textwrap.indent(str(Source(get_real_func(deco_fixture))), " ") + "\n" == src )
def pytest_pycollect_makeitem(collector, name, obj): outcome = yield res = outcome.get_result() if res is not None: return # nothing was collected elsewhere, let's do it here if isclass(obj): if collector.istestclass(obj, name): Class = collector._getcustomclass("Class") outcome.force_result(Class(name, parent=collector)) elif collector.istestfunction(obj, name): # mock seems to store unbound methods (issue473), normalize it obj = getattr(obj, "__func__", obj) # We need to try and unwrap the function if it's a functools.partial # 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, ) elif getattr(obj, "__test__", True): if is_generator(obj): res = Generator(name, parent=collector) else: res = list(collector._genfunctions(name, obj)) outcome.force_result(res)
def pytest_pycollect_makeitem(collector, name, obj): outcome = yield res = outcome.get_result() if res is not None: return # nothing was collected elsewhere, let's do it here if isclass(obj): if collector.istestclass(obj, name): Class = collector._getcustomclass("Class") outcome.force_result(Class(name, parent=collector)) elif collector.istestfunction(obj, name): # mock seems to store unbound methods (issue473), normalize it obj = getattr(obj, "__func__", obj) # We need to try and unwrap the function if it's a functools.partial # 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, ) elif getattr(obj, "__test__", True): if is_generator(obj): res = Generator(name, parent=collector) else: res = list(collector._genfunctions(name, obj)) outcome.force_result(res)
def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False): if nodeid is not NOTSET: holderobj = node_or_obj else: holderobj = node_or_obj.obj nodeid = node_or_obj.nodeid if holderobj in self._holderobjseen: return self._holderobjseen.add(holderobj) autousenames = [] for name in dir(holderobj): # The attribute can be an arbitrary descriptor, so the attribute # access below can raise. safe_getatt() ignores such exceptions. obj = safe_getattr(holderobj, name, None) marker = getfixturemarker(obj) if not isinstance(marker, FixtureFunctionMarker): # magic globals with __getattr__ might have got us a wrong # fixture attribute continue if marker.name: name = marker.name # during fixture definition we wrap the original fixture function # to issue a warning if called directly, so here we unwrap it in order to not emit the warning # when pytest itself calls the fixture function if six.PY2 and unittest: # hack on Python 2 because of the unbound methods obj = get_real_func(obj) else: obj = get_real_method(obj, holderobj) fixture_def = FixtureDef( self, nodeid, name, obj, marker.scope, marker.params, unittest=unittest, ids=marker.ids, ) faclist = self._arg2fixturedefs.setdefault(name, []) if fixture_def.has_location: faclist.append(fixture_def) else: # fixturedefs with no location are at the front # so this inserts the current fixturedef after the # existing fixturedefs from external plugins but # before the fixturedefs provided in conftests. i = len([f for f in faclist if not f.has_location]) faclist.insert(i, fixture_def) if marker.autouse: autousenames.append(name) if autousenames: self._nodeid_and_autousenames.append((nodeid or "", autousenames))
def test_real_func_loop_limit(): class Evil: def __init__(self): self.left = 1000 def __repr__(self): return "<Evil left={left}>".format(left=self.left) def __getattr__(self, attr): if not self.left: raise RuntimeError("it's over") # pragma: no cover self.left -= 1 return self evil = Evil() with pytest.raises( ValueError, match=("could not find real function of <Evil left=800>\n" "stopped at <Evil left=800>"), ): get_real_func(evil)
def pytest_pycollect_makeitem(collector, name, obj): outcome = yield item = outcome.get_result() concurrent_mark = getattr(obj, 'concurrent', None) if not concurrent_mark: concurrent_mark = getattr(obj, 'async', None) if isinstance(concurrent_mark, MarkInfo): if isinstance(item, Generator): obj = get_real_func(obj) items = list(collector._genfunctions(name, obj)) outcome.force_result(items) else: raise Exception( 'Attempt to set `concurrent` mark for non generator %s' % name)
def formatrepr(self): tblines = [] addline = tblines.append stack = [self.request._pyfuncitem.obj] stack.extend(map(lambda x: x.func, self.fixturestack)) msg = self.msg if msg is not None: # the last fixture raise an error, let's present # it at the requesting side stack = stack[:-1] for function in stack: fspath, lineno = getfslineno(function) try: lines, _ = inspect.getsourcelines(get_real_func(function)) except (IOError, IndexError, TypeError): error_msg = "file %s, line %s: source code not available" addline(error_msg % (fspath, lineno + 1)) else: addline("file %s, line %s" % (fspath, lineno + 1)) for i, line in enumerate(lines): line = line.rstrip() addline(" " + line) if line.lstrip().startswith("def"): break if msg is None: fm = self.request._fixturemanager available = set() parentid = self.request._pyfuncitem.parent.nodeid for name, fixturedefs in fm._arg2fixturedefs.items(): faclist = list(fm._matchfactories(fixturedefs, parentid)) if faclist: available.add(name) if self.argname in available: msg = " recursive dependency involving fixture '{}' detected".format( self.argname) else: msg = "fixture '{}' not found".format(self.argname) msg += "\n available fixtures: {}".format(", ".join( sorted(available))) msg += "\n use 'pytest --fixtures [testpath]' for help on them." return FixtureLookupErrorRepr(fspath, lineno, tblines, msg, self.argname)
def test_real_func_loop_limit(): class Evil(object): def __init__(self): self.left = 1000 def __repr__(self): return "<Evil left={left}>".format(left=self.left) def __getattr__(self, attr): if not self.left: raise RuntimeError("its over") self.left -= 1 return self evil = Evil() with pytest.raises(ValueError): res = get_real_func(evil) print(res)
def formatrepr(self): tblines = [] addline = tblines.append stack = [self.request._pyfuncitem.obj] stack.extend(map(lambda x: x.func, self.fixturestack)) msg = self.msg if msg is not None: # the last fixture raise an error, let's present # it at the requesting side stack = stack[:-1] for function in stack: fspath, lineno = getfslineno(function) try: lines, _ = inspect.getsourcelines(get_real_func(function)) except (IOError, IndexError, TypeError): error_msg = "file %s, line %s: source code not available" addline(error_msg % (fspath, lineno + 1)) else: addline("file %s, line %s" % (fspath, lineno + 1)) for i, line in enumerate(lines): line = line.rstrip() addline(" " + line) if line.lstrip().startswith("def"): break if msg is None: fm = self.request._fixturemanager available = set() parentid = self.request._pyfuncitem.parent.nodeid for name, fixturedefs in fm._arg2fixturedefs.items(): faclist = list(fm._matchfactories(fixturedefs, parentid)) if faclist: available.add(name) if self.argname in available: msg = " recursive dependency involving fixture '{}' detected".format( self.argname ) else: msg = "fixture '{}' not found".format(self.argname) msg += "\n available fixtures: {}".format(", ".join(sorted(available))) msg += "\n use 'pytest --fixtures [testpath]' for help on them." return FixtureLookupErrorRepr(fspath, lineno, tblines, msg, self.argname)
def _prunetraceback(self, excinfo): if hasattr(self, "_obj") and not self.config.option.fulltrace: code = _pytest._code.Code(get_real_func(self.obj)) path, firstlineno = code.path, code.firstlineno traceback = excinfo.traceback ntraceback = traceback.cut(path=path, firstlineno=firstlineno) if ntraceback == traceback: ntraceback = ntraceback.cut(path=path) if ntraceback == traceback: ntraceback = ntraceback.filter(filter_traceback) if not ntraceback: ntraceback = traceback excinfo.traceback = ntraceback.filter() # issue364: mark all but first and last frames to # only show a single-line message for each frame if self.config.option.tbstyle == "auto": if len(excinfo.traceback) > 2: for entry in excinfo.traceback[1:-1]: entry.set_repr_style("short")
def pytest_pycollect_makeitem(collector, name, obj): """ @see PyCollector._genfunctions @see _pytest.python """ if safe_isclass(obj): if collector.istestclass(obj, name) or (issubclass(obj, AbstractTestCase) and obj != AbstractTestCase): if hasattr(pytest.Class, "from_parent"): return pytest.Class.from_parent(collector, name=name) else: return pytest.Class(name, parent=collector) else: obj = getattr(obj, "__func__", obj) if (inspect.isfunction(obj) or inspect.isfunction(get_real_func(obj))) and getattr(obj, "__test__", True) and isinstance(collector, pytest.Instance) and hasattr(obj, "pytestmark"): if not is_generator(obj): return list(collector._genfunctions(name, obj)) else: return [] else: return []
def wrap_fixture( fixturefunc: Callable, wrapped_param: str = 'wrapped', ignore: Union[str, Iterable[str]] = (), ) -> Callable[[Callable], Callable]: """Wrap a fixture function, extending its argspec w/ the decorated method pytest will prune the fixture dependency graph of any unneeded fixtures. It does this by reading the expected arg names of fixtures. When wrapping a fixture function, merely currying along **kwargs will cripple pytest's pruning. This method retains the arg names from the original fixture function, and returns a wrapper method that includes those original arg names, as well as any fixtures requested by the decorated function. The decorated method will be passed a wrapper of the passed fixturefunc that can be called with no arguments — the fixtures it requested will receive automagical defaults, though these may be overridden. The argument name of this wrapped fixturefunc may be customized with the `wrapped_param` arg, so as to avoid any collision with other fixture names. Example (contrived): bare_user = lambda_fixture(lambda user_factory: user_factory( username='******', password='******', )) @pytest.fixture @wrap_fixture(bare_user) def admin_user(team, wrapped): user = wrapped() team.add_member(user, role_id=TeamRole.Roles.ADMIN) return user :param fixturefunc: The fixture function to wrap :param wrapped_param: Name of parameter to pass the wrapped fixturefunc as :param ignore: Name of parameter(s) from fixturefunc to not include in wrapping fixture's args (and thus not request as fixtures from pytest) """ if isinstance(ignore, str): ignore = (ignore,) fixturefunc = get_real_func(fixturefunc) def decorator(fn: Callable): decorated_arg_names = set(getfuncargnames(fn)) if wrapped_param not in decorated_arg_names: raise TypeError( f'The decorated method must include an arg named {wrapped_param} ' f'as the wrapped fixture func.') # Don't include the wrapped param in the argspec we expose to pytest decorated_arg_names -= {wrapped_param} fixture_arg_names = set(getfuncargnames(fixturefunc)) - set(ignore) all_arg_names = fixture_arg_names | decorated_arg_names | {'request'} def extension_impl(**all_args): request = all_args['request'] ### # kwargs requested by the wrapped fixture # fixture_args = { name: value for name, value in all_args.items() if name in fixture_arg_names } ### # kwargs requested by the decorated method # decorated_args = { name: value for name, value in all_args.items() if name in decorated_arg_names } @functools.wraps(fixturefunc) def wrapped(**overridden_args): kwargs = { **fixture_args, **overridden_args, } return call_fixture_func(fixturefunc, request, kwargs) decorated_args[wrapped_param] = wrapped return call_fixture_func(fn, request, decorated_args) extension = build_wrapped_method(fn.__name__, all_arg_names, extension_impl) return extension return decorator