1 new commit in pytest:
https://bitbucket.org/pytest-dev/pytest/commits/af98ffcfb0fb/
Changeset: af98ffcfb0fb
User: hpk42
Date: 2015-05-06 13:02:05+00:00
Summary: Merged in hpk42/pytest-patches/pluggy1 (pull request #290)
integrate pluggy as external plugin manager
Affected #: 29 files
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -27,8 +27,8 @@
details like especially the pluginmanager.add_shutdown() API.
Thanks Holger Krekel.
-- pluginmanagement: introduce ``pytest.hookimpl_opts`` and
- ``pytest.hookspec_opts`` decorators for setting impl/spec
+- pluginmanagement: introduce ``pytest.hookimpl`` and
+ ``pytest.hookspec`` decorators for setting impl/spec
specific parameters. This substitutes the previous
now deprecated use of ``pytest.mark`` which is meant to
contain markers for test functions only.
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 _pytest/__init__.py
--- a/_pytest/__init__.py
+++ b/_pytest/__init__.py
@@ -1,2 +1,2 @@
#
-__version__ = '2.8.0.dev2'
+__version__ = '2.8.0.dev3'
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 _pytest/capture.py
--- a/_pytest/capture.py
+++ b/_pytest/capture.py
@@ -29,7 +29,7 @@
help="shortcut for --capture=no.")
[email protected]_opts(hookwrapper=True)
[email protected](hookwrapper=True)
def pytest_load_initial_conftests(early_config, parser, args):
ns = early_config.known_args_namespace
pluginmanager = early_config.pluginmanager
@@ -101,7 +101,7 @@
if capfuncarg is not None:
capfuncarg.close()
- @pytest.hookimpl_opts(hookwrapper=True)
+ @pytest.hookimpl(hookwrapper=True)
def pytest_make_collect_report(self, collector):
if isinstance(collector, pytest.File):
self.resumecapture()
@@ -115,13 +115,13 @@
else:
yield
- @pytest.hookimpl_opts(hookwrapper=True)
+ @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_setup(self, item):
self.resumecapture()
yield
self.suspendcapture_item(item, "setup")
- @pytest.hookimpl_opts(hookwrapper=True)
+ @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(self, item):
self.resumecapture()
self.activate_funcargs(item)
@@ -129,17 +129,17 @@
#self.deactivate_funcargs() called from suspendcapture()
self.suspendcapture_item(item, "call")
- @pytest.hookimpl_opts(hookwrapper=True)
+ @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_teardown(self, item):
self.resumecapture()
yield
self.suspendcapture_item(item, "teardown")
- @pytest.hookimpl_opts(tryfirst=True)
+ @pytest.hookimpl(tryfirst=True)
def pytest_keyboard_interrupt(self, excinfo):
self.reset_capturings()
- @pytest.hookimpl_opts(tryfirst=True)
+ @pytest.hookimpl(tryfirst=True)
def pytest_internalerror(self, excinfo):
self.reset_capturings()
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 _pytest/config.py
--- a/_pytest/config.py
+++ b/_pytest/config.py
@@ -8,8 +8,11 @@
import py
# DON't import pytest here because it causes import cycle troubles
import sys, os
-from _pytest import hookspec # the extension point definitions
-from _pytest.core import PluginManager, hookimpl_opts, varnames
+import _pytest.hookspec # the extension point definitions
+from pluggy import PluginManager, HookimplMarker, HookspecMarker
+
+hookimpl = HookimplMarker("pytest")
+hookspec = HookspecMarker("pytest")
# pytest startup
#
@@ -106,10 +109,10 @@
name.startswith("pytest_funcarg__")
+
class PytestPluginManager(PluginManager):
def __init__(self):
- super(PytestPluginManager, self).__init__(prefix="pytest_",
-
excludefunc=exclude_pytest_names)
+ super(PytestPluginManager, self).__init__("pytest",
implprefix="pytest_")
self._warnings = []
self._conftest_plugins = set()
@@ -118,7 +121,7 @@
self._conftestpath2mod = {}
self._confcutdir = None
- self.addhooks(hookspec)
+ self.add_hookspecs(_pytest.hookspec)
self.register(self)
if os.environ.get('PYTEST_DEBUG'):
err = sys.stderr
@@ -130,12 +133,39 @@
self.trace.root.setwriter(err.write)
self.enable_tracing()
+ def addhooks(self, module_or_class):
+ warning = dict(code="I2",
+ fslocation=py.code.getfslineno(sys._getframe(1)),
+ message="use pluginmanager.add_hookspecs instead of "
+ "deprecated addhooks() method.")
+ self._warnings.append(warning)
+ return self.add_hookspecs(module_or_class)
- def _verify_hook(self, hook, plugin):
- super(PytestPluginManager, self)._verify_hook(hook, plugin)
- method = getattr(plugin, hook.name)
- if "__multicall__" in varnames(method):
- fslineno = py.code.getfslineno(method)
+ def parse_hookimpl_opts(self, plugin, name):
+ if exclude_pytest_names(name):
+ return None
+
+ method = getattr(plugin, name)
+ opts = super(PytestPluginManager, self).parse_hookimpl_opts(plugin,
name)
+ if opts is not None:
+ for name in ("tryfirst", "trylast", "optionalhook", "hookwrapper"):
+ opts.setdefault(name, hasattr(method, name))
+ return opts
+
+ def parse_hookspec_opts(self, module_or_class, name):
+ opts = super(PytestPluginManager, self).parse_hookspec_opts(
+ module_or_class, name)
+ if opts is None:
+ method = getattr(module_or_class, name)
+ if name.startswith("pytest_"):
+ opts = {"firstresult": hasattr(method, "firstresult"),
+ "historic": hasattr(method, "historic")}
+ return opts
+
+ def _verify_hook(self, hook, hookmethod):
+ super(PytestPluginManager, self)._verify_hook(hook, hookmethod)
+ if "__multicall__" in hookmethod.argnames:
+ fslineno = py.code.getfslineno(hookmethod.function)
warning = dict(code="I1",
fslocation=fslineno,
message="%r hook uses deprecated __multicall__ "
@@ -154,7 +184,7 @@
return self.get_plugin(name)
def pytest_configure(self, config):
- # XXX now that the pluginmanager exposes hookimpl_opts(tryfirst...)
+ # XXX now that the pluginmanager exposes hookimpl(tryfirst...)
# we should remove tryfirst/trylast as markers
config.addinivalue_line("markers",
"tryfirst: mark a hook implementation function such that the "
@@ -797,7 +827,7 @@
if not hasattr(self.option, opt.dest):
setattr(self.option, opt.dest, opt.default)
- @hookimpl_opts(trylast=True)
+ @hookimpl(trylast=True)
def pytest_load_initial_conftests(self, early_config):
self.pluginmanager._set_initial_conftests(early_config.known_args_namespace)
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 _pytest/core.py
--- a/_pytest/core.py
+++ /dev/null
@@ -1,590 +0,0 @@
-"""
-PluginManager, basic initialization and tracing.
-"""
-import sys
-from inspect import isfunction, ismethod, isclass, formatargspec, getargspec
-import py
-
-py3 = sys.version_info > (3,0)
-
-def hookspec_opts(firstresult=False, historic=False):
- """ returns a decorator which will define a function as a hook
specfication.
-
- If firstresult is True the 1:N hook call (N being the number of registered
- hook implementation functions) will stop at I<=N when the I'th function
- returns a non-None result.
-
- If historic is True calls to a hook will be memorized and replayed
- on later registered plugins.
- """
- def setattr_hookspec_opts(func):
- if historic and firstresult:
- raise ValueError("cannot have a historic firstresult hook")
- if firstresult:
- func.firstresult = firstresult
- if historic:
- func.historic = historic
- return func
- return setattr_hookspec_opts
-
-
-def hookimpl_opts(hookwrapper=False, optionalhook=False,
- tryfirst=False, trylast=False):
- """ Return a decorator which marks a function as a hook implementation.
-
- If optionalhook is True a missing matching hook specification will not
result
- in an error (by default it is an error if no matching spec is found).
-
- If tryfirst is True this hook implementation will run as early as possible
- in the chain of N hook implementations for a specfication.
-
- If trylast is True this hook implementation will run as late as possible
- in the chain of N hook implementations.
-
- If hookwrapper is True the hook implementations needs to execute exactly
- one "yield". The code before the yield is run early before any
non-hookwrapper
- function is run. The code after the yield is run after all non-hookwrapper
- function have run. The yield receives an ``CallOutcome`` object
representing
- the exception or result outcome of the inner calls (including other
hookwrapper
- calls).
- """
- def setattr_hookimpl_opts(func):
- if hookwrapper:
- func.hookwrapper = True
- if optionalhook:
- func.optionalhook = True
- if tryfirst:
- func.tryfirst = True
- if trylast:
- func.trylast = True
- return func
- return setattr_hookimpl_opts
-
-
-class TagTracer:
- def __init__(self):
- self._tag2proc = {}
- self.writer = None
- self.indent = 0
-
- def get(self, name):
- return TagTracerSub(self, (name,))
-
- def format_message(self, tags, args):
- if isinstance(args[-1], dict):
- extra = args[-1]
- args = args[:-1]
- else:
- extra = {}
-
- content = " ".join(map(str, args))
- indent = " " * self.indent
-
- lines = [
- "%s%s [%s]\n" %(indent, content, ":".join(tags))
- ]
-
- for name, value in extra.items():
- lines.append("%s %s: %s\n" % (indent, name, value))
- return lines
-
- def processmessage(self, tags, args):
- if self.writer is not None and args:
- lines = self.format_message(tags, args)
- self.writer(''.join(lines))
- try:
- self._tag2proc[tags](tags, args)
- except KeyError:
- pass
-
- def setwriter(self, writer):
- self.writer = writer
-
- def setprocessor(self, tags, processor):
- if isinstance(tags, str):
- tags = tuple(tags.split(":"))
- else:
- assert isinstance(tags, tuple)
- self._tag2proc[tags] = processor
-
-
-class TagTracerSub:
- def __init__(self, root, tags):
- self.root = root
- self.tags = tags
-
- def __call__(self, *args):
- self.root.processmessage(self.tags, args)
-
- def setmyprocessor(self, processor):
- self.root.setprocessor(self.tags, processor)
-
- def get(self, name):
- return self.__class__(self.root, self.tags + (name,))
-
-
-def raise_wrapfail(wrap_controller, msg):
- co = wrap_controller.gi_code
- raise RuntimeError("wrap_controller at %r %s:%d %s" %
- (co.co_name, co.co_filename, co.co_firstlineno, msg))
-
-
-def wrapped_call(wrap_controller, func):
- """ Wrap calling to a function with a generator which needs to yield
- exactly once. The yield point will trigger calling the wrapped function
- and return its CallOutcome to the yield point. The generator then needs
- to finish (raise StopIteration) in order for the wrapped call to complete.
- """
- try:
- next(wrap_controller) # first yield
- except StopIteration:
- raise_wrapfail(wrap_controller, "did not yield")
- call_outcome = CallOutcome(func)
- try:
- wrap_controller.send(call_outcome)
- raise_wrapfail(wrap_controller, "has second yield")
- except StopIteration:
- pass
- return call_outcome.get_result()
-
-
-class CallOutcome:
- """ Outcome of a function call, either an exception or a proper result.
- Calling the ``get_result`` method will return the result or reraise
- the exception raised when the function was called. """
- excinfo = None
- def __init__(self, func):
- try:
- self.result = func()
- except BaseException:
- self.excinfo = sys.exc_info()
-
- def force_result(self, result):
- self.result = result
- self.excinfo = None
-
- def get_result(self):
- if self.excinfo is None:
- return self.result
- else:
- ex = self.excinfo
- if py3:
- raise ex[1].with_traceback(ex[2])
- py.builtin._reraise(*ex)
-
-
-class TracedHookExecution:
- def __init__(self, pluginmanager, before, after):
- self.pluginmanager = pluginmanager
- self.before = before
- self.after = after
- self.oldcall = pluginmanager._inner_hookexec
- assert not isinstance(self.oldcall, TracedHookExecution)
- self.pluginmanager._inner_hookexec = self
-
- def __call__(self, hook, methods, kwargs):
- self.before(hook, methods, kwargs)
- outcome = CallOutcome(lambda: self.oldcall(hook, methods, kwargs))
- self.after(outcome, hook, methods, kwargs)
- return outcome.get_result()
-
- def undo(self):
- self.pluginmanager._inner_hookexec = self.oldcall
-
-
-class PluginManager(object):
- """ Core Pluginmanager class which manages registration
- of plugin objects and 1:N hook calling.
-
- You can register new hooks by calling ``addhooks(module_or_class)``.
- You can register plugin objects (which contain hooks) by calling
- ``register(plugin)``. The Pluginmanager is initialized with a
- prefix that is searched for in the names of the dict of registered
- plugin objects. An optional excludefunc allows to blacklist names which
- are not considered as hooks despite a matching prefix.
-
- For debugging purposes you can call ``enable_tracing()``
- which will subsequently send debug information to the trace helper.
- """
-
- def __init__(self, prefix, excludefunc=None):
- self._prefix = prefix
- self._excludefunc = excludefunc
- self._name2plugin = {}
- self._plugin2hookcallers = {}
- self._plugin_distinfo = []
- self.trace = TagTracer().get("pluginmanage")
- self.hook = HookRelay(self.trace.root.get("hook"))
- self._inner_hookexec = lambda hook, methods, kwargs: \
- MultiCall(methods, kwargs,
hook.firstresult).execute()
-
- def _hookexec(self, hook, methods, kwargs):
- # called from all hookcaller instances.
- # enable_tracing will set its own wrapping function at
self._inner_hookexec
- return self._inner_hookexec(hook, methods, kwargs)
-
- def enable_tracing(self):
- """ enable tracing of hook calls and return an undo function. """
- hooktrace = self.hook._trace
-
- def before(hook, methods, kwargs):
- hooktrace.root.indent += 1
- hooktrace(hook.name, kwargs)
-
- def after(outcome, hook, methods, kwargs):
- if outcome.excinfo is None:
- hooktrace("finish", hook.name, "-->", outcome.result)
- hooktrace.root.indent -= 1
-
- return TracedHookExecution(self, before, after).undo
-
- def subset_hook_caller(self, name, remove_plugins):
- """ Return a new HookCaller instance for the named method
- which manages calls to all registered plugins except the
- ones from remove_plugins. """
- orig = getattr(self.hook, name)
- plugins_to_remove = [plugin for plugin in remove_plugins
- if hasattr(plugin, name)]
- if plugins_to_remove:
- hc = HookCaller(orig.name, orig._hookexec,
orig._specmodule_or_class)
- for plugin in orig._plugins:
- if plugin not in plugins_to_remove:
- hc._add_plugin(plugin)
- # we also keep track of this hook caller so it
- # gets properly removed on plugin unregistration
- self._plugin2hookcallers.setdefault(plugin, []).append(hc)
- return hc
- return orig
-
- def register(self, plugin, name=None):
- """ Register a plugin and return its canonical name or None if the name
- is blocked from registering. Raise a ValueError if the plugin is
already
- registered. """
- plugin_name = name or self.get_canonical_name(plugin)
-
- if plugin_name in self._name2plugin or plugin in
self._plugin2hookcallers:
- if self._name2plugin.get(plugin_name, -1) is None:
- return # blocked plugin, return None to indicate no
registration
- raise ValueError("Plugin already registered: %s=%s\n%s" %(
- plugin_name, plugin, self._name2plugin))
-
- self._name2plugin[plugin_name] = plugin
-
- # register prefix-matching hook specs of the plugin
- self._plugin2hookcallers[plugin] = hookcallers = []
- for name in dir(plugin):
- if name.startswith(self._prefix):
- hook = getattr(self.hook, name, None)
- if hook is None:
- if self._excludefunc is not None and
self._excludefunc(name):
- continue
- hook = HookCaller(name, self._hookexec)
- setattr(self.hook, name, hook)
- elif hook.has_spec():
- self._verify_hook(hook, plugin)
- hook._maybe_apply_history(getattr(plugin, name))
- hookcallers.append(hook)
- hook._add_plugin(plugin)
- return plugin_name
-
- def unregister(self, plugin=None, name=None):
- """ unregister a plugin object and all its contained hook
implementations
- from internal data structures. """
- if name is None:
- assert plugin is not None, "one of name or plugin needs to be
specified"
- name = self.get_name(plugin)
-
- if plugin is None:
- plugin = self.get_plugin(name)
-
- # if self._name2plugin[name] == None registration was blocked: ignore
- if self._name2plugin.get(name):
- del self._name2plugin[name]
-
- for hookcaller in self._plugin2hookcallers.pop(plugin, []):
- hookcaller._remove_plugin(plugin)
-
- return plugin
-
- def set_blocked(self, name):
- """ block registrations of the given name, unregister if already
registered. """
- self.unregister(name=name)
- self._name2plugin[name] = None
-
- def addhooks(self, module_or_class):
- """ add new hook definitions from the given module_or_class using
- the prefix/excludefunc with which the PluginManager was initialized.
"""
- names = []
- for name in dir(module_or_class):
- if name.startswith(self._prefix):
- hc = getattr(self.hook, name, None)
- if hc is None:
- hc = HookCaller(name, self._hookexec, module_or_class)
- setattr(self.hook, name, hc)
- else:
- # plugins registered this hook without knowing the spec
- hc.set_specification(module_or_class)
- for plugin in hc._plugins:
- self._verify_hook(hc, plugin)
- names.append(name)
-
- if not names:
- raise ValueError("did not find new %r hooks in %r"
- %(self._prefix, module_or_class))
-
- def get_plugins(self):
- """ return the set of registered plugins. """
- return set(self._plugin2hookcallers)
-
- def is_registered(self, plugin):
- """ Return True if the plugin is already registered. """
- return plugin in self._plugin2hookcallers
-
- def get_canonical_name(self, plugin):
- """ Return canonical name for a plugin object. Note that a plugin
- may be registered under a different name which was specified
- by the caller of register(plugin, name). To obtain the name
- of an registered plugin use ``get_name(plugin)`` instead."""
- return getattr(plugin, "__name__", None) or str(id(plugin))
-
- def get_plugin(self, name):
- """ Return a plugin or None for the given name. """
- return self._name2plugin.get(name)
-
- def get_name(self, plugin):
- """ Return name for registered plugin or None if not registered. """
- for name, val in self._name2plugin.items():
- if plugin == val:
- return name
-
- def _verify_hook(self, hook, plugin):
- method = getattr(plugin, hook.name)
- pluginname = self.get_name(plugin)
-
- if hook.is_historic() and hasattr(method, "hookwrapper"):
- raise PluginValidationError(
- "Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" %(
- pluginname, hook.name))
-
- for arg in varnames(method):
- if arg not in hook.argnames:
- raise PluginValidationError(
- "Plugin %r\nhook %r\nargument %r not available\n"
- "plugin definition: %s\n"
- "available hookargs: %s" %(
- pluginname, hook.name, arg, formatdef(method),
- ", ".join(hook.argnames)))
-
- def check_pending(self):
- """ Verify that all hooks which have not been verified against
- a hook specification are optional, otherwise raise
PluginValidationError"""
- for name in self.hook.__dict__:
- if name.startswith(self._prefix):
- hook = getattr(self.hook, name)
- if not hook.has_spec():
- for plugin in hook._plugins:
- method = getattr(plugin, hook.name)
- if not getattr(method, "optionalhook", False):
- raise PluginValidationError(
- "unknown hook %r in plugin %r" %(name, plugin))
-
- def load_setuptools_entrypoints(self, entrypoint_name):
- """ Load modules from querying the specified setuptools entrypoint
name.
- Return the number of loaded plugins. """
- from pkg_resources import iter_entry_points, DistributionNotFound
- for ep in iter_entry_points(entrypoint_name):
- # is the plugin registered or blocked?
- if self.get_plugin(ep.name) or ep.name in self._name2plugin:
- continue
- try:
- plugin = ep.load()
- except DistributionNotFound:
- continue
- self.register(plugin, name=ep.name)
- self._plugin_distinfo.append((ep.dist, plugin))
- return len(self._plugin_distinfo)
-
-
-class MultiCall:
- """ execute a call into multiple python functions/methods. """
-
- # XXX note that the __multicall__ argument is supported only
- # for pytest compatibility reasons. It was never officially
- # supported there and is explicitely deprecated since 2.8
- # so we can remove it soon, allowing to avoid the below recursion
- # in execute() and simplify/speed up the execute loop.
-
- def __init__(self, methods, kwargs, firstresult=False):
- self.methods = methods
- self.kwargs = kwargs
- self.kwargs["__multicall__"] = self
- self.firstresult = firstresult
-
- def execute(self):
- all_kwargs = self.kwargs
- self.results = results = []
- firstresult = self.firstresult
-
- while self.methods:
- method = self.methods.pop()
- args = [all_kwargs[argname] for argname in varnames(method)]
- if hasattr(method, "hookwrapper"):
- return wrapped_call(method(*args), self.execute)
- res = method(*args)
- if res is not None:
- if firstresult:
- return res
- results.append(res)
-
- if not firstresult:
- return results
-
- def __repr__(self):
- status = "%d meths" % (len(self.methods),)
- if hasattr(self, "results"):
- status = ("%d results, " % len(self.results)) + status
- return "<MultiCall %s, kwargs=%r>" %(status, self.kwargs)
-
-
-
-def varnames(func, startindex=None):
- """ return argument name tuple for a function, method, class or callable.
-
- In case of a class, its "__init__" method is considered.
- For methods the "self" parameter is not included unless you are passing
- an unbound method with Python3 (which has no supports for unbound methods)
- """
- cache = getattr(func, "__dict__", {})
- try:
- return cache["_varnames"]
- except KeyError:
- pass
- if isclass(func):
- try:
- func = func.__init__
- except AttributeError:
- return ()
- startindex = 1
- else:
- if not isfunction(func) and not ismethod(func):
- func = getattr(func, '__call__', func)
- if startindex is None:
- startindex = int(ismethod(func))
-
- rawcode = py.code.getrawcode(func)
- try:
- x = rawcode.co_varnames[startindex:rawcode.co_argcount]
- except AttributeError:
- x = ()
- else:
- defaults = func.__defaults__
- if defaults:
- x = x[:-len(defaults)]
- try:
- cache["_varnames"] = x
- except TypeError:
- pass
- return x
-
-
-class HookRelay:
- def __init__(self, trace):
- self._trace = trace
-
-
-class HookCaller(object):
- def __init__(self, name, hook_execute, specmodule_or_class=None):
- self.name = name
- self._plugins = []
- self._wrappers = []
- self._nonwrappers = []
- self._hookexec = hook_execute
- if specmodule_or_class is not None:
- self.set_specification(specmodule_or_class)
-
- def has_spec(self):
- return hasattr(self, "_specmodule_or_class")
-
- def set_specification(self, specmodule_or_class):
- assert not self.has_spec()
- self._specmodule_or_class = specmodule_or_class
- specfunc = getattr(specmodule_or_class, self.name)
- argnames = varnames(specfunc, startindex=isclass(specmodule_or_class))
- assert "self" not in argnames # sanity check
- self.argnames = ["__multicall__"] + list(argnames)
- self.firstresult = getattr(specfunc, 'firstresult', False)
- if hasattr(specfunc, "historic"):
- self._call_history = []
-
- def is_historic(self):
- return hasattr(self, "_call_history")
-
- def _remove_plugin(self, plugin):
- self._plugins.remove(plugin)
- meth = getattr(plugin, self.name)
- try:
- self._nonwrappers.remove(meth)
- except ValueError:
- self._wrappers.remove(meth)
-
- def _add_plugin(self, plugin):
- self._plugins.append(plugin)
- self._add_method(getattr(plugin, self.name))
-
- def _add_method(self, meth):
- if hasattr(meth, 'hookwrapper'):
- methods = self._wrappers
- else:
- methods = self._nonwrappers
-
- if hasattr(meth, 'trylast'):
- methods.insert(0, meth)
- elif hasattr(meth, 'tryfirst'):
- methods.append(meth)
- else:
- # find last non-tryfirst method
- i = len(methods) - 1
- while i >= 0 and hasattr(methods[i], "tryfirst"):
- i -= 1
- methods.insert(i + 1, meth)
-
- def __repr__(self):
- return "<HookCaller %r>" %(self.name,)
-
- def __call__(self, **kwargs):
- assert not self.is_historic()
- return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
-
- def call_historic(self, proc=None, kwargs=None):
- self._call_history.append((kwargs or {}, proc))
- # historizing hooks don't return results
- self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
-
- def call_extra(self, methods, kwargs):
- """ Call the hook with some additional temporarily participating
- methods using the specified kwargs as call parameters. """
- old = list(self._nonwrappers), list(self._wrappers)
- for method in methods:
- self._add_method(method)
- try:
- return self(**kwargs)
- finally:
- self._nonwrappers, self._wrappers = old
-
- def _maybe_apply_history(self, method):
- if self.is_historic():
- for kwargs, proc in self._call_history:
- res = self._hookexec(self, [method], kwargs)
- if res and proc is not None:
- proc(res[0])
-
-
-class PluginValidationError(Exception):
- """ plugin failed validation. """
-
-
-def formatdef(func):
- return "%s%s" % (
- func.__name__,
- formatargspec(*getargspec(func))
- )
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 _pytest/genscript.py
--- a/_pytest/genscript.py
+++ b/_pytest/genscript.py
@@ -4,7 +4,6 @@
import pkgutil
import py
-
import _pytest
@@ -33,6 +32,9 @@
for pyfile in toplevel.visit('*.py'):
pkg = pkgname(name, toplevel, pyfile)
name2src[pkg] = pyfile.read()
+ # with wheels py source code might be not be installed
+ # and the resulting genscript is useless, just bail out.
+ assert name2src, "no source code found for %r at %r" %(name, toplevel)
return name2src
def compress_mapping(mapping):
@@ -69,7 +71,7 @@
genscript = config.getvalue("genscript")
if genscript:
tw = py.io.TerminalWriter()
- deps = ['py', '_pytest', 'pytest']
+ deps = ['py', 'pluggy', '_pytest', 'pytest']
if sys.version_info < (2,7):
deps.append("argparse")
tw.line("generated script will run on python2.6-python3.3++")
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 _pytest/helpconfig.py
--- a/_pytest/helpconfig.py
+++ b/_pytest/helpconfig.py
@@ -22,7 +22,7 @@
help="store internal tracing debug information in
'pytestdebug.log'.")
[email protected]_opts(hookwrapper=True)
[email protected](hookwrapper=True)
def pytest_cmdline_parse():
outcome = yield
config = outcome.get_result()
@@ -96,10 +96,10 @@
def getpluginversioninfo(config):
lines = []
- plugininfo = config.pluginmanager._plugin_distinfo
+ plugininfo = config.pluginmanager.list_plugin_distinfo()
if plugininfo:
lines.append("setuptools registered plugins:")
- for dist, plugin in plugininfo:
+ for plugin, dist in plugininfo:
loc = getattr(plugin, '__file__', repr(plugin))
content = "%s-%s at %s" % (dist.project_name, dist.version, loc)
lines.append(" " + content)
@@ -117,7 +117,7 @@
if config.option.traceconfig:
lines.append("active plugins:")
- items = config.pluginmanager._name2plugin.items()
+ items = config.pluginmanager.list_name_plugin()
for name, plugin in items:
if hasattr(plugin, '__file__'):
r = plugin.__file__
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 _pytest/hookspec.py
--- a/_pytest/hookspec.py
+++ b/_pytest/hookspec.py
@@ -1,30 +1,32 @@
""" hook specifications for pytest plugins, invoked from main.py and builtin
plugins. """
-from _pytest.core import hookspec_opts
+from pluggy import HookspecMarker
+
+hookspec = HookspecMarker("pytest")
# -------------------------------------------------------------------------
# Initialization hooks called for every plugin
# -------------------------------------------------------------------------
-@hookspec_opts(historic=True)
+@hookspec(historic=True)
def pytest_addhooks(pluginmanager):
"""called at plugin registration time to allow adding new hooks via a call
to
- pluginmanager.addhooks(module_or_class, prefix)."""
+ pluginmanager.add_hookspecs(module_or_class, prefix)."""
-@hookspec_opts(historic=True)
+@hookspec(historic=True)
def pytest_namespace():
"""return dict of name->object to be made globally available in
the pytest namespace. This hook is called at plugin registration
time.
"""
-@hookspec_opts(historic=True)
+@hookspec(historic=True)
def pytest_plugin_registered(plugin, manager):
""" a new pytest plugin got registered. """
-@hookspec_opts(historic=True)
+@hookspec(historic=True)
def pytest_addoption(parser):
"""register argparse-style options and ini-style config values.
@@ -50,7 +52,7 @@
via (deprecated) ``pytest.config``.
"""
-@hookspec_opts(historic=True)
+@hookspec(historic=True)
def pytest_configure(config):
""" called after command line options have been parsed
and all plugins and initial conftest files been loaded.
@@ -63,14 +65,14 @@
# discoverable conftest.py local plugins.
# -------------------------------------------------------------------------
-@hookspec_opts(firstresult=True)
+@hookspec(firstresult=True)
def pytest_cmdline_parse(pluginmanager, args):
"""return initialized config object, parsing the specified args. """
def pytest_cmdline_preparse(config, args):
"""(deprecated) modify command line arguments before option parsing. """
-@hookspec_opts(firstresult=True)
+@hookspec(firstresult=True)
def pytest_cmdline_main(config):
""" called for performing the main command line action. The default
implementation will invoke the configure hooks and runtest_mainloop. """
@@ -84,7 +86,7 @@
# collection hooks
# -------------------------------------------------------------------------
-@hookspec_opts(firstresult=True)
+@hookspec(firstresult=True)
def pytest_collection(session):
""" perform the collection protocol for the given session. """
@@ -95,14 +97,14 @@
def pytest_collection_finish(session):
""" called after collection has been performed and modified. """
-@hookspec_opts(firstresult=True)
+@hookspec(firstresult=True)
def pytest_ignore_collect(path, config):
""" return True to prevent considering this path for collection.
This hook is consulted for all files and directories prior to calling
more specific hooks.
"""
-@hookspec_opts(firstresult=True)
+@hookspec(firstresult=True)
def pytest_collect_directory(path, parent):
""" called before traversing a directory for collection files. """
@@ -123,7 +125,7 @@
def pytest_deselected(items):
""" called for test items deselected by keyword. """
-@hookspec_opts(firstresult=True)
+@hookspec(firstresult=True)
def pytest_make_collect_report(collector):
""" perform ``collector.collect()`` and return a CollectReport. """
@@ -131,7 +133,7 @@
# Python test function related hooks
# -------------------------------------------------------------------------
-@hookspec_opts(firstresult=True)
+@hookspec(firstresult=True)
def pytest_pycollect_makemodule(path, parent):
""" return a Module collector or None for the given path.
This hook will be called for each matching test module path.
@@ -139,11 +141,11 @@
create test modules for files that do not match as a test module.
"""
-@hookspec_opts(firstresult=True)
+@hookspec(firstresult=True)
def pytest_pycollect_makeitem(collector, name, obj):
""" return custom item/collector for a python object in a module, or None.
"""
-@hookspec_opts(firstresult=True)
+@hookspec(firstresult=True)
def pytest_pyfunc_call(pyfuncitem):
""" call underlying test function. """
@@ -154,7 +156,7 @@
# generic runtest related hooks
# -------------------------------------------------------------------------
-@hookspec_opts(firstresult=True)
+@hookspec(firstresult=True)
def pytest_runtestloop(session):
""" called for performing the main runtest loop
(after collection finished). """
@@ -162,7 +164,7 @@
def pytest_itemstart(item, node):
""" (deprecated, use pytest_runtest_logstart). """
-@hookspec_opts(firstresult=True)
+@hookspec(firstresult=True)
def pytest_runtest_protocol(item, nextitem):
""" implements the runtest_setup/call/teardown protocol for
the given test item, including capturing exceptions and calling
@@ -195,7 +197,7 @@
so that nextitem only needs to call setup-functions.
"""
-@hookspec_opts(firstresult=True)
+@hookspec(firstresult=True)
def pytest_runtest_makereport(item, call):
""" return a :py:class:`_pytest.runner.TestReport` object
for the given :py:class:`pytest.Item` and
@@ -240,7 +242,7 @@
def pytest_report_header(config, startdir):
""" return a string to be displayed as header info for terminal
reporting."""
-@hookspec_opts(firstresult=True)
+@hookspec(firstresult=True)
def pytest_report_teststatus(report):
""" return result-category, shortletter and verbose word for reporting."""
@@ -256,7 +258,7 @@
# doctest hooks
# -------------------------------------------------------------------------
-@hookspec_opts(firstresult=True)
+@hookspec(firstresult=True)
def pytest_doctest_prepare_content(content):
""" return processed content for a given doctest"""
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 _pytest/main.py
--- a/_pytest/main.py
+++ b/_pytest/main.py
@@ -501,23 +501,23 @@
def __init__(self, config):
FSCollector.__init__(self, config.rootdir, parent=None,
config=config, session=self)
- self.config.pluginmanager.register(self, name="session")
+ self._fs2hookproxy = {}
self._testsfailed = 0
self.shouldstop = False
self.trace = config.trace.root.get("collection")
self._norecursepatterns = config.getini("norecursedirs")
self.startdir = py.path.local()
- self._fs2hookproxy = {}
+ self.config.pluginmanager.register(self, name="session")
def _makeid(self):
return ""
- @pytest.hookimpl_opts(tryfirst=True)
+ @pytest.hookimpl(tryfirst=True)
def pytest_collectstart(self):
if self.shouldstop:
raise self.Interrupted(self.shouldstop)
- @pytest.hookimpl_opts(tryfirst=True)
+ @pytest.hookimpl(tryfirst=True)
def pytest_runtest_logreport(self, report):
if report.failed and not hasattr(report, 'wasxfail'):
self._testsfailed += 1
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 _pytest/nose.py
--- a/_pytest/nose.py
+++ b/_pytest/nose.py
@@ -24,7 +24,7 @@
call.excinfo = call2.excinfo
[email protected]_opts(trylast=True)
[email protected](trylast=True)
def pytest_runtest_setup(item):
if is_potential_nosetest(item):
if isinstance(item.parent, pytest.Generator):
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 _pytest/pastebin.py
--- a/_pytest/pastebin.py
+++ b/_pytest/pastebin.py
@@ -11,7 +11,7 @@
choices=['failed', 'all'],
help="send failed|all info to bpaste.net pastebin service.")
[email protected]_opts(trylast=True)
[email protected](trylast=True)
def pytest_configure(config):
if config.option.pastebin == "all":
tr = config.pluginmanager.getplugin('terminalreporter')
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 _pytest/pytester.py
--- a/_pytest/pytester.py
+++ b/_pytest/pytester.py
@@ -13,7 +13,7 @@
import py
import pytest
from py.builtin import print_
-from _pytest.core import TracedHookExecution
+from pluggy import _TracedHookExecution
from _pytest.main import Session, EXIT_OK
@@ -80,7 +80,7 @@
else:
return True
- @pytest.hookimpl_opts(hookwrapper=True, tryfirst=True)
+ @pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_item(self, item):
lines1 = self.get_open_files()
yield
@@ -198,7 +198,7 @@
self.calls.append(ParsedCall(hook.name, kwargs))
def after(outcome, hook, method, kwargs):
pass
- executor = TracedHookExecution(pluginmanager, before, after)
+ executor = _TracedHookExecution(pluginmanager, before, after)
self._undo_wrapping = executor.undo
def finish_recording(self):
@@ -712,8 +712,20 @@
option "--runpytest" and return a :py:class:`RunResult`.
"""
+ args = self._ensure_basetemp(args)
return self._runpytest_method(*args, **kwargs)
+ def _ensure_basetemp(self, args):
+ args = [str(x) for x in args]
+ for x in args:
+ if str(x).startswith('--basetemp'):
+ #print ("basedtemp exists: %s" %(args,))
+ break
+ else:
+ args.append("--basetemp=%s" % self.tmpdir.dirpath('basetemp'))
+ #print ("added basetemp: %s" %(args,))
+ return args
+
def parseconfig(self, *args):
"""Return a new py.test Config instance from given commandline args.
@@ -726,12 +738,8 @@
modules which will be registered with the PluginManager.
"""
- args = [str(x) for x in args]
- for x in args:
- if str(x).startswith('--basetemp'):
- break
- else:
- args.append("--basetemp=%s" % self.tmpdir.dirpath('basetemp'))
+ args = self._ensure_basetemp(args)
+
import _pytest.config
config = _pytest.config._prepareconfig(args, self.plugins)
# we don't know what the test will do with this half-setup config
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 _pytest/python.py
--- a/_pytest/python.py
+++ b/_pytest/python.py
@@ -8,7 +8,11 @@
from py._code.code import TerminalRepr
import _pytest
-cutdir = py.path.local(_pytest.__file__).dirpath()
+import pluggy
+
+cutdir2 = py.path.local(_pytest.__file__).dirpath()
+cutdir1 = py.path.local(pluggy.__file__.rstrip("oc"))
+
NoneType = type(None)
NOTSET = object()
@@ -18,6 +22,11 @@
# used to work around a python2 exception info leak
exc_clear = getattr(sys, 'exc_clear', lambda: None)
+
+def filter_traceback(entry):
+ return entry.path != cutdir1 and not entry.path.relto(cutdir2)
+
+
def getfslineno(obj):
# xxx let decorators etc specify a sane ordering
while hasattr(obj, "__wrapped__"):
@@ -172,7 +181,7 @@
def pytest_sessionstart(session):
session._fixturemanager = FixtureManager(session)
[email protected]_opts(trylast=True)
[email protected](trylast=True)
def pytest_namespace():
raises.Exception = pytest.fail.Exception
return {
@@ -191,7 +200,7 @@
return request.config
[email protected]_opts(trylast=True)
[email protected](trylast=True)
def pytest_pyfunc_call(pyfuncitem):
testfunction = pyfuncitem.obj
if pyfuncitem._isyieldedfunction():
@@ -219,7 +228,7 @@
def pytest_pycollect_makemodule(path, parent):
return Module(path, parent)
[email protected]_opts(hookwrapper=True)
[email protected](hookwrapper=True)
def pytest_pycollect_makeitem(collector, name, obj):
outcome = yield
res = outcome.get_result()
@@ -604,7 +613,11 @@
if ntraceback == traceback:
ntraceback = ntraceback.cut(path=path)
if ntraceback == traceback:
- ntraceback = ntraceback.cut(excludepath=cutdir)
+ #ntraceback = ntraceback.cut(excludepath=cutdir2)
+ 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
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 _pytest/skipping.py
--- a/_pytest/skipping.py
+++ b/_pytest/skipping.py
@@ -133,7 +133,7 @@
return expl
[email protected]_opts(tryfirst=True)
[email protected](tryfirst=True)
def pytest_runtest_setup(item):
evalskip = MarkEvaluator(item, 'skipif')
if evalskip.istrue():
@@ -151,7 +151,7 @@
if not evalxfail.get('run', True):
pytest.xfail("[NOTRUN] " + evalxfail.getexplanation())
[email protected]_opts(hookwrapper=True)
[email protected](hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
rep = outcome.get_result()
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 _pytest/terminal.py
--- a/_pytest/terminal.py
+++ b/_pytest/terminal.py
@@ -3,6 +3,7 @@
This is a good source for looking at the various reporting hooks.
"""
import pytest
+import pluggy
import py
import sys
import time
@@ -267,7 +268,7 @@
def pytest_collection_modifyitems(self):
self.report_collect(True)
- @pytest.hookimpl_opts(trylast=True)
+ @pytest.hookimpl(trylast=True)
def pytest_sessionstart(self, session):
self._sessionstarttime = time.time()
if not self.showheader:
@@ -278,7 +279,8 @@
if hasattr(sys, 'pypy_version_info'):
verinfo = ".".join(map(str, sys.pypy_version_info[:3]))
msg += "[pypy-%s-%s]" % (verinfo, sys.pypy_version_info[3])
- msg += " -- py-%s -- pytest-%s" % (py.__version__, pytest.__version__)
+ msg += ", pytest-%s, py-%s, pluggy-%s" % (
+ pytest.__version__, py.__version__, pluggy.__version__)
if self.verbosity > 0 or self.config.option.debug or \
getattr(self.config.option, 'pastebin', None):
msg += " -- " + str(sys.executable)
@@ -294,10 +296,11 @@
if config.inifile:
inifile = config.rootdir.bestrelpath(config.inifile)
lines = ["rootdir: %s, inifile: %s" %(config.rootdir, inifile)]
- plugininfo = config.pluginmanager._plugin_distinfo
+
+ plugininfo = config.pluginmanager.list_plugin_distinfo()
if plugininfo:
l = []
- for dist, plugin in plugininfo:
+ for plugin, dist in plugininfo:
name = dist.project_name
if name.startswith("pytest-"):
name = name[7:]
@@ -352,7 +355,7 @@
indent = (len(stack) - 1) * " "
self._tw.line("%s%s" % (indent, col))
- @pytest.hookimpl_opts(hookwrapper=True)
+ @pytest.hookimpl(hookwrapper=True)
def pytest_sessionfinish(self, exitstatus):
outcome = yield
outcome.get_result()
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 _pytest/unittest.py
--- a/_pytest/unittest.py
+++ b/_pytest/unittest.py
@@ -140,7 +140,7 @@
if traceback:
excinfo.traceback = traceback
[email protected]_opts(tryfirst=True)
[email protected](tryfirst=True)
def pytest_runtest_makereport(item, call):
if isinstance(item, TestCaseFunction):
if item._excinfo:
@@ -152,7 +152,7 @@
# twisted trial support
[email protected]_opts(hookwrapper=True)
[email protected](hookwrapper=True)
def pytest_runtest_protocol(item):
if isinstance(item, TestCaseFunction) and \
'twisted.trial.unittest' in sys.modules:
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 doc/en/example/markers.txt
--- a/doc/en/example/markers.txt
+++ b/doc/en/example/markers.txt
@@ -201,9 +201,9 @@
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as
needing all of the specified fixtures. see
http://pytest.org/latest/fixture.html#usefixtures
- @pytest.hookimpl_opts(tryfirst=True): mark a hook implementation function
such that the plugin machinery will try to call it first/as early as possible.
+ @pytest.hookimpl(tryfirst=True): mark a hook implementation function such
that the plugin machinery will try to call it first/as early as possible.
- @pytest.hookimpl_opts(trylast=True): mark a hook implementation function
such that the plugin machinery will try to call it last/as late as possible.
+ @pytest.hookimpl(trylast=True): mark a hook implementation function such
that the plugin machinery will try to call it last/as late as possible.
For an example on how to add and work with markers from a plugin, see
@@ -375,9 +375,9 @@
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as
needing all of the specified fixtures. see
http://pytest.org/latest/fixture.html#usefixtures
- @pytest.hookimpl_opts(tryfirst=True): mark a hook implementation function
such that the plugin machinery will try to call it first/as early as possible.
+ @pytest.hookimpl(tryfirst=True): mark a hook implementation function such
that the plugin machinery will try to call it first/as early as possible.
- @pytest.hookimpl_opts(trylast=True): mark a hook implementation function
such that the plugin machinery will try to call it last/as late as possible.
+ @pytest.hookimpl(trylast=True): mark a hook implementation function such
that the plugin machinery will try to call it last/as late as possible.
Reading markers which were set from multiple places
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 doc/en/example/simple.txt
--- a/doc/en/example/simple.txt
+++ b/doc/en/example/simple.txt
@@ -534,7 +534,7 @@
import pytest
import os.path
- @pytest.hookimpl_opts(tryfirst=True)
+ @pytest.hookimpl(tryfirst=True)
def pytest_runtest_makereport(item, call, __multicall__):
# execute all other hooks to obtain the report object
rep = __multicall__.execute()
@@ -607,7 +607,7 @@
import pytest
- @pytest.hookimpl_opts(tryfirst=True)
+ @pytest.hookimpl(tryfirst=True)
def pytest_runtest_makereport(item, call, __multicall__):
# execute all other hooks to obtain the report object
rep = __multicall__.execute()
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 doc/en/goodpractises.txt
--- a/doc/en/goodpractises.txt
+++ b/doc/en/goodpractises.txt
@@ -169,6 +169,14 @@
python runtests.py
+.. note::
+
+ You must have pytest and its dependencies installed as an sdist, not
+ as wheels because genscript need the source code for generating a
+ standalone script.
+
+
+
Integrating with distutils / ``python setup.py test``
--------------------------------------------------------
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 doc/en/writing_plugins.txt
--- a/doc/en/writing_plugins.txt
+++ b/doc/en/writing_plugins.txt
@@ -292,7 +292,7 @@
import pytest
- @pytest.hookimpl_opts(hookwrapper=True)
+ @pytest.hookimpl(hookwrapper=True)
def pytest_pyfunc_call(pyfuncitem):
# do whatever you want before the next hook executes
@@ -305,8 +305,7 @@
Note that hook wrappers don't return results themselves, they merely
perform tracing or other side effects around the actual hook implementations.
If the result of the underlying hook is a mutable object, they may modify
-that result, however.
-
+that result but it's probably better to avoid it.
Hook function ordering / call example
@@ -338,16 +337,24 @@
Here is the order of execution:
1. Plugin3's pytest_collection_modifyitems called until the yield point
-2. Plugin1's pytest_collection_modifyitems is called
-3. Plugin2's pytest_collection_modifyitems is called
-4. Plugin3's pytest_collection_modifyitems called for executing after the yield
- The yield receives a :py:class:`CallOutcome` instance which encapsulates
- the result from calling the non-wrappers. Wrappers cannot modify the
result.
+ because it is a hook wrapper.
+
+2. Plugin1's pytest_collection_modifyitems is called because it is marked
+ with ``tryfirst=True``.
+
+3. Plugin2's pytest_collection_modifyitems is called because it is marked
+ with ``trylast=True`` (but even without this mark it would come after
+ Plugin1).
+
+4. Plugin3's pytest_collection_modifyitems then executing the code after the
yield
+ point. The yield receives a :py:class:`CallOutcome` instance which
encapsulates
+ the result from calling the non-wrappers. Wrappers shall not modify the
result.
It's possible to use ``tryfirst`` and ``trylast`` also in conjunction with
``hookwrapper=True`` in which case it will influence the ordering of
hookwrappers
among each other.
+
Declaring new hooks
------------------------
@@ -368,11 +375,11 @@
.. _`newhooks.py`:
https://bitbucket.org/pytest-dev/pytest-xdist/src/52082f70e7dd04b00361091b8af906c60fd6700f/xdist/newhooks.py?at=default
-Using hooks from 3rd party plugins
--------------------------------------
+Optionally using hooks from 3rd party plugins
+---------------------------------------------
Using new hooks from plugins as explained above might be a little tricky
-because the standard :ref:`validation mechanism <validation>`:
+because of the standard :ref:`validation mechanism <validation>`:
if you depend on a plugin that is not installed, validation will fail and
the error message will not make much sense to your users.
@@ -395,7 +402,6 @@
This has the added benefit of allowing you to conditionally install hooks
depending on which plugins are installed.
-
.. _`well specified hooks`:
.. currentmodule:: _pytest.hookspec
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 pytest.py
--- a/pytest.py
+++ b/pytest.py
@@ -11,8 +11,10 @@
# else we are imported
-from _pytest.config import main, UsageError, _preloadplugins, cmdline
-from _pytest.core import hookspec_opts, hookimpl_opts
+from _pytest.config import (
+ main, UsageError, _preloadplugins, cmdline,
+ hookspec, hookimpl
+)
from _pytest import __version__
_preloadplugins() # to populate pytest.* namespace so help(pytest) works
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 setup.py
--- a/setup.py
+++ b/setup.py
@@ -48,7 +48,7 @@
def main():
- install_requires = ['py>=1.4.27.dev2']
+ install_requires = ['py>=1.4.27.dev2', 'pluggy>=0.2.0,<0.3.0']
extras_require = {}
if has_environment_marker_support():
extras_require[':python_version=="2.6" or python_version=="3.0" or
python_version=="3.1"'] = ['argparse']
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 testing/python/collect.py
--- a/testing/python/collect.py
+++ b/testing/python/collect.py
@@ -559,7 +559,7 @@
b = testdir.mkdir("a").mkdir("b")
b.join("conftest.py").write(py.code.Source("""
import pytest
- @pytest.hookimpl_opts(hookwrapper=True)
+ @pytest.hookimpl(hookwrapper=True)
def pytest_pycollect_makeitem():
outcome = yield
if outcome.excinfo is None:
diff -r e14e1c5215fc9f2022ba103c52929f9ddfb15d81 -r
af98ffcfb0fb1d0116bfc4b46c49dfa31fdfc7c2 testing/test_config.py
--- a/testing/test_config.py
+++ b/testing/test_config.py
@@ -357,9 +357,9 @@
pm.register(m)
hc = pm.hook.pytest_load_initial_conftests
l = hc._nonwrappers + hc._wrappers
- assert l[-1].__module__ == "_pytest.capture"
- assert l[-2] == m.pytest_load_initial_conftests
- assert l[-3].__module__ == "_pytest.config"
+ assert l[-1].function.__module__ == "_pytest.capture"
+ assert l[-2].function == m.pytest_load_initial_conftests
+ assert l[-3].function.__module__ == "_pytest.config"
class TestWarning:
def test_warn_config(self, testdir):
This diff is so big that we needed to truncate the remainder.
Repository URL: https://bitbucket.org/pytest-dev/pytest/
--
This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.
_______________________________________________
pytest-commit mailing list
[email protected]
https://mail.python.org/mailman/listinfo/pytest-commit