1 new commit in pytest:

https://bitbucket.org/pytest-dev/pytest/commits/24f4d48abeeb/
Changeset:   24f4d48abeeb
User:        flub
Date:        2015-04-27 12:17:40+00:00
Summary:     Merged in hpk42/pytest-patches/more_plugin (pull request #282)

another major pluginmanager refactor and docs
Affected #:  27 files

diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -26,6 +26,23 @@
   change but it might still break 3rd party plugins which relied on 
   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 
+  specific parameters.  This substitutes the previous 
+  now deprecated use of ``pytest.mark`` which is meant to 
+  contain markers for test functions only.  
+
+- write/refine docs for "writing plugins" which now have their
+  own page and are separate from the "using/installing plugins`` page.
+
+- fix issue732: properly unregister plugins from any hook calling
+  sites allowing to have temporary plugins during test execution.
+
+- deprecate and warn about ``__multicall__`` argument in hook 
+  implementations.  Use the ``hookwrapper`` mechanism instead already 
+  introduced with pytest-2.7.
+
  
 2.7.1.dev (compared to 2.7.0)
 -----------------------------

diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/capture.py
--- a/_pytest/capture.py
+++ b/_pytest/capture.py
@@ -29,7 +29,7 @@
         help="shortcut for --capture=no.")
 
 
-@pytest.mark.hookwrapper
+@pytest.hookimpl_opts(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.mark.hookwrapper
+    @pytest.hookimpl_opts(hookwrapper=True)
     def pytest_make_collect_report(self, collector):
         if isinstance(collector, pytest.File):
             self.resumecapture()
@@ -115,13 +115,13 @@
         else:
             yield
 
-    @pytest.mark.hookwrapper
+    @pytest.hookimpl_opts(hookwrapper=True)
     def pytest_runtest_setup(self, item):
         self.resumecapture()
         yield
         self.suspendcapture_item(item, "setup")
 
-    @pytest.mark.hookwrapper
+    @pytest.hookimpl_opts(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.mark.hookwrapper
+    @pytest.hookimpl_opts(hookwrapper=True)
     def pytest_runtest_teardown(self, item):
         self.resumecapture()
         yield
         self.suspendcapture_item(item, "teardown")
 
-    @pytest.mark.tryfirst
+    @pytest.hookimpl_opts(tryfirst=True)
     def pytest_keyboard_interrupt(self, excinfo):
         self.reset_capturings()
 
-    @pytest.mark.tryfirst
+    @pytest.hookimpl_opts(tryfirst=True)
     def pytest_internalerror(self, excinfo):
         self.reset_capturings()
 

diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/config.py
--- a/_pytest/config.py
+++ b/_pytest/config.py
@@ -9,7 +9,7 @@
 # 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
+from _pytest.core import PluginManager, hookimpl_opts, varnames
 
 # pytest startup
 #
@@ -38,6 +38,7 @@
         tw.line("ERROR: could not load %s\n" % (e.path), red=True)
         return 4
     else:
+        config.pluginmanager.check_pending()
         return config.hook.pytest_cmdline_main(config=config)
 
 class cmdline:  # compatibility namespace
@@ -59,17 +60,17 @@
 
 def _preloadplugins():
     assert not _preinit
-    _preinit.append(get_plugin_manager())
+    _preinit.append(get_config())
 
-def get_plugin_manager():
+def get_config():
     if _preinit:
         return _preinit.pop(0)
     # subsequent calls to main will create a fresh instance
     pluginmanager = PytestPluginManager()
-    pluginmanager.config = Config(pluginmanager) # XXX attr needed?
+    config = Config(pluginmanager)
     for spec in default_plugins:
         pluginmanager.import_plugin(spec)
-    return pluginmanager
+    return config
 
 def _prepareconfig(args=None, plugins=None):
     if args is None:
@@ -80,7 +81,7 @@
         if not isinstance(args, str):
             raise ValueError("not a string or argument list: %r" % (args,))
         args = shlex.split(args)
-    pluginmanager = get_plugin_manager()
+    pluginmanager = get_config().pluginmanager
     if plugins:
         for plugin in plugins:
             pluginmanager.register(plugin)
@@ -97,8 +98,7 @@
         super(PytestPluginManager, self).__init__(prefix="pytest_",
                                                   
excludefunc=exclude_pytest_names)
         self._warnings = []
-        self._plugin_distinfo = []
-        self._globalplugins = []
+        self._conftest_plugins = set()
 
         # state related to local conftest plugins
         self._path2confmods = {}
@@ -114,28 +114,35 @@
                 err = py.io.dupfile(err, encoding=encoding)
             except Exception:
                 pass
-            self.set_tracing(err.write)
+            self.trace.root.setwriter(err.write)
+            self.enable_tracing()
 
-    def register(self, plugin, name=None, conftest=False):
+
+    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)
+            warning = dict(code="I1",
+                           fslocation=fslineno,
+                           message="%r hook uses deprecated __multicall__ "
+                                   "argument" % (hook.name))
+            self._warnings.append(warning)
+
+    def register(self, plugin, name=None):
         ret = super(PytestPluginManager, self).register(plugin, name)
-        if ret and not conftest:
-            self._globalplugins.append(plugin)
+        if ret:
+            self.hook.pytest_plugin_registered.call_historic(
+                      kwargs=dict(plugin=plugin, manager=self))
         return ret
 
-    def _do_register(self, plugin, name):
-        # called from core PluginManager class
-        if hasattr(self, "config"):
-            self.config._register_plugin(plugin, name)
-        return super(PytestPluginManager, self)._do_register(plugin, name)
-
-    def unregister(self, plugin):
-        super(PytestPluginManager, self).unregister(plugin)
-        try:
-            self._globalplugins.remove(plugin)
-        except ValueError:
-            pass
+    def getplugin(self, name):
+        # support deprecated naming because plugins (xdist e.g.) use it
+        return self.get_plugin(name)
 
     def pytest_configure(self, config):
+        # XXX now that the pluginmanager exposes hookimpl_opts(tryfirst...)
+        # we should remove tryfirst/trylast as markers
         config.addinivalue_line("markers",
             "tryfirst: mark a hook implementation function such that the "
             "plugin machinery will try to call it first/as early as possible.")
@@ -143,7 +150,10 @@
             "trylast: mark a hook implementation function such that the "
             "plugin machinery will try to call it last/as late as possible.")
         for warning in self._warnings:
-            config.warn(code="I1", message=warning)
+            if isinstance(warning, dict):
+                config.warn(**warning)
+            else:
+                config.warn(code="I1", message=warning)
 
     #
     # internal API for local conftest plugin handling
@@ -186,14 +196,21 @@
         try:
             return self._path2confmods[path]
         except KeyError:
-            clist = []
-            for parent in path.parts():
-                if self._confcutdir and self._confcutdir.relto(parent):
-                    continue
-                conftestpath = parent.join("conftest.py")
-                if conftestpath.check(file=1):
-                    mod = self._importconftest(conftestpath)
-                    clist.append(mod)
+            if path.isfile():
+                clist = self._getconftestmodules(path.dirpath())
+            else:
+                # XXX these days we may rather want to use config.rootdir
+                # and allow users to opt into looking into the rootdir parent
+                # directories instead of requiring to specify confcutdir
+                clist = []
+                for parent in path.parts():
+                    if self._confcutdir and self._confcutdir.relto(parent):
+                        continue
+                    conftestpath = parent.join("conftest.py")
+                    if conftestpath.isfile():
+                        mod = self._importconftest(conftestpath)
+                        clist.append(mod)
+
             self._path2confmods[path] = clist
             return clist
 
@@ -217,6 +234,8 @@
                 mod = conftestpath.pyimport()
             except Exception:
                 raise ConftestImportFailure(conftestpath, sys.exc_info())
+
+            self._conftest_plugins.add(mod)
             self._conftestpath2mod[conftestpath] = mod
             dirpath = conftestpath.dirpath()
             if dirpath in self._path2confmods:
@@ -233,24 +252,6 @@
     #
     #
 
-    def consider_setuptools_entrypoints(self):
-        try:
-            from pkg_resources import iter_entry_points, DistributionNotFound
-        except ImportError:
-            return # XXX issue a warning
-        for ep in iter_entry_points('pytest11'):
-            name = ep.name
-            if name.startswith("pytest_"):
-                name = name[7:]
-            if ep.name in self._name2plugin or name in self._name2plugin:
-                continue
-            try:
-                plugin = ep.load()
-            except DistributionNotFound:
-                continue
-            self._plugin_distinfo.append((ep.dist, plugin))
-            self.register(plugin, name=name)
-
     def consider_preparse(self, args):
         for opt1,opt2 in zip(args, args[1:]):
             if opt1 == "-p":
@@ -258,18 +259,12 @@
 
     def consider_pluginarg(self, arg):
         if arg.startswith("no:"):
-            name = arg[3:]
-            plugin = self.getplugin(name)
-            if plugin is not None:
-                self.unregister(plugin)
-            self._name2plugin[name] = -1
+            self.set_blocked(arg[3:])
         else:
-            if self.getplugin(arg) is None:
-                self.import_plugin(arg)
+            self.import_plugin(arg)
 
     def consider_conftest(self, conftestmodule):
-        if self.register(conftestmodule, name=conftestmodule.__file__,
-                         conftest=True):
+        if self.register(conftestmodule, name=conftestmodule.__file__):
             self.consider_module(conftestmodule)
 
     def consider_env(self):
@@ -291,7 +286,7 @@
         # basename for historic purposes but must be imported with the
         # _pytest prefix.
         assert isinstance(modname, str)
-        if self.getplugin(modname) is not None:
+        if self.get_plugin(modname) is not None:
             return
         if modname in builtin_plugins:
             importspec = "_pytest." + modname
@@ -685,6 +680,7 @@
 
 notset = Notset()
 FILE_OR_DIR = 'file_or_dir'
+
 class Config(object):
     """ access to configuration values, pluginmanager and plugin hooks.  """
 
@@ -706,20 +702,11 @@
         self._cleanup = []
         self.pluginmanager.register(self, "pytestconfig")
         self._configured = False
-
-    def _register_plugin(self, plugin, name):
-        call_plugin = self.pluginmanager.call_plugin
-        call_plugin(plugin, "pytest_addhooks",
-                    {'pluginmanager': self.pluginmanager})
-        self.hook.pytest_plugin_registered(plugin=plugin,
-                                           manager=self.pluginmanager)
-        dic = call_plugin(plugin, "pytest_namespace", {}) or {}
-        if dic:
+        def do_setns(dic):
             import pytest
             setns(pytest, dic)
-        call_plugin(plugin, "pytest_addoption", {'parser': self._parser})
-        if self._configured:
-            call_plugin(plugin, "pytest_configure", {'config': self})
+        self.hook.pytest_namespace.call_historic(do_setns, {})
+        
self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser))
 
     def add_cleanup(self, func):
         """ Add a function to be called when the config object gets out of
@@ -729,26 +716,27 @@
     def _do_configure(self):
         assert not self._configured
         self._configured = True
-        self.hook.pytest_configure(config=self)
+        self.hook.pytest_configure.call_historic(kwargs=dict(config=self))
 
     def _ensure_unconfigure(self):
         if self._configured:
             self._configured = False
             self.hook.pytest_unconfigure(config=self)
+            self.hook.pytest_configure._call_history = []
         while self._cleanup:
             fin = self._cleanup.pop()
             fin()
 
-    def warn(self, code, message):
+    def warn(self, code, message, fslocation=None):
         """ generate a warning for this test session. """
         self.hook.pytest_logwarning(code=code, message=message,
-                                    fslocation=None, nodeid=None)
+                                    fslocation=fslocation, nodeid=None)
 
     def get_terminal_writer(self):
-        return self.pluginmanager.getplugin("terminalreporter")._tw
+        return self.pluginmanager.get_plugin("terminalreporter")._tw
 
     def pytest_cmdline_parse(self, pluginmanager, args):
-        assert self == pluginmanager.config, (self, pluginmanager.config)
+        # REF1 assert self == pluginmanager.config, (self, 
pluginmanager.config)
         self.parse(args)
         return self
 
@@ -778,8 +766,7 @@
     @classmethod
     def fromdictargs(cls, option_dict, args):
         """ constructor useable for subprocesses. """
-        pluginmanager = get_plugin_manager()
-        config = pluginmanager.config
+        config = get_config()
         config._preparse(args, addopts=False)
         config.option.__dict__.update(option_dict)
         for x in config.option.plugins:
@@ -794,13 +781,9 @@
             if not hasattr(self.option, opt.dest):
                 setattr(self.option, opt.dest, opt.default)
 
-    def _getmatchingplugins(self, fspath):
-        return self.pluginmanager._globalplugins + \
-               self.pluginmanager._getconftestmodules(fspath)
-
+    @hookimpl_opts(trylast=True)
     def pytest_load_initial_conftests(self, early_config):
         
self.pluginmanager._set_initial_conftests(early_config.known_args_namespace)
-    pytest_load_initial_conftests.trylast = True
 
     def _initini(self, args):
         parsed_args = self._parser.parse_known_args(args)
@@ -817,7 +800,10 @@
             args[:] = self.getini("addopts") + args
         self._checkversion()
         self.pluginmanager.consider_preparse(args)
-        self.pluginmanager.consider_setuptools_entrypoints()
+        try:
+            self.pluginmanager.load_setuptools_entrypoints("pytest11")
+        except ImportError as e:
+            self.warn("I2", "could not load setuptools entry import: %s" % 
(e,))
         self.pluginmanager.consider_env()
         self.known_args_namespace = ns = self._parser.parse_known_args(args)
         try:
@@ -850,6 +836,8 @@
         assert not hasattr(self, 'args'), (
                 "can only parse cmdline args at most once per Config object")
         self._origargs = args
+        self.hook.pytest_addhooks.call_historic(
+                                  
kwargs=dict(pluginmanager=self.pluginmanager))
         self._preparse(args)
         # XXX deprecated hook:
         self.hook.pytest_cmdline_preparse(config=self, args=args)

diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/core.py
--- a/_pytest/core.py
+++ b/_pytest/core.py
@@ -2,11 +2,65 @@
 PluginManager, basic initialization and tracing.
 """
 import sys
-import inspect
+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 = {}
@@ -53,42 +107,28 @@
             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 add_method_wrapper(cls, wrapper_func):
-    """ Substitute the function named "wrapperfunc.__name__" at class
-    "cls" with a function that wraps the call to the original function.
-    Return an undo function which can be called to reset the class to use
-    the old method again.
-
-    wrapper_func is called with the same arguments as the method
-    it wraps and its result is used as a wrap_controller for
-    calling the original function.
-    """
-    name = wrapper_func.__name__
-    oldcall = getattr(cls, name)
-    def wrap_exec(*args, **kwargs):
-        gen = wrapper_func(*args, **kwargs)
-        return wrapped_call(gen, lambda: oldcall(*args, **kwargs))
-
-    setattr(cls, name, wrap_exec)
-    return lambda: setattr(cls, name, oldcall)
-
 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
@@ -133,6 +173,25 @@
             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.
@@ -144,197 +203,228 @@
     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 ``set_tracing(writer)``
-    which will subsequently send debug information to the specified
-    write function.
+    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._plugins = []
         self._plugin2hookcallers = {}
+        self._plugin_distinfo = []
         self.trace = TagTracer().get("pluginmanage")
-        self.hook = HookRelay(pm=self)
+        self.hook = HookRelay(self.trace.root.get("hook"))
+        self._inner_hookexec = lambda hook, methods, kwargs: \
+                               MultiCall(methods, kwargs, 
hook.firstresult).execute()
 
-    def set_tracing(self, writer):
-        """ turn on tracing to the given writer method and
-        return an undo function. """
-        self.trace.root.setwriter(writer)
-        # reconfigure HookCalling to perform tracing
-        assert not hasattr(self, "_wrapping")
-        self._wrapping = True
+    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)
 
-        hooktrace = self.hook.trace
+    def enable_tracing(self):
+        """ enable tracing of hook calls and return an undo function. """
+        hooktrace = self.hook._trace
 
-        def _docall(self, methods, kwargs):
+        def before(hook, methods, kwargs):
             hooktrace.root.indent += 1
-            hooktrace(self.name, kwargs)
-            box = yield
-            if box.excinfo is None:
-                hooktrace("finish", self.name, "-->", box.result)
+            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 add_method_wrapper(HookCaller, _docall)
+        return TracedHookExecution(self, before, after).undo
 
-    def make_hook_caller(self, name, plugins):
-        caller = getattr(self.hook, name)
-        methods = self.listattr(name, plugins=plugins)
-        return HookCaller(caller.name, caller.firstresult,
-                          argnames=caller.argnames, methods=methods)
+    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 with the given name and ensure that all its
-        hook implementations are integrated.  If the name is not specified
-        we use the ``__name__`` attribute of the plugin object or, if that
-        doesn't exist, the id of the plugin.  This method will raise a
-        ValueError if the eventual name is already registered. """
-        name = name or self._get_canonical_name(plugin)
-        if self._name2plugin.get(name, None) == -1:
-            return
-        if self.hasplugin(name):
+        """ 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" %(
-                              name, plugin, self._name2plugin))
-        #self.trace("registering", name, plugin)
-        # allow subclasses to intercept here by calling a helper
-        return self._do_register(plugin, name)
+                              plugin_name, plugin, self._name2plugin))
 
-    def _do_register(self, plugin, name):
-        hookcallers = list(self._scan_plugin(plugin))
-        self._plugin2hookcallers[plugin] = hookcallers
-        self._name2plugin[name] = plugin
-        self._plugins.append(plugin)
-        # rescan all methods for the hookcallers we found
-        for hookcaller in hookcallers:
-            self._scan_methods(hookcaller)
-        return True
+        self._name2plugin[plugin_name] = plugin
 
-    def unregister(self, plugin):
-        """ unregister the plugin object and all its contained hook 
implementations
+        # 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. """
-        self._plugins.remove(plugin)
-        for name, value in list(self._name2plugin.items()):
-            if value == plugin:
-                del self._name2plugin[name]
-        hookcallers = self._plugin2hookcallers.pop(plugin)
-        for hookcaller in hookcallers:
-            self._scan_methods(hookcaller)
+        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. 
"""
-        isclass = int(inspect.isclass(module_or_class))
         names = []
         for name in dir(module_or_class):
             if name.startswith(self._prefix):
-                method = module_or_class.__dict__[name]
-                firstresult = getattr(method, 'firstresult', False)
-                hc = HookCaller(name, firstresult=firstresult,
-                                argnames=varnames(method, startindex=isclass))
-                setattr(self.hook, name, hc)
+                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 getplugins(self):
-        """ return the complete list of registered plugins. NOTE that
-        you will get the internal list and need to make a copy if you
-        modify the list."""
-        return self._plugins
+    def get_plugins(self):
+        """ return the set of registered plugins. """
+        return set(self._plugin2hookcallers)
 
-    def isregistered(self, plugin):
-        """ Return True if the plugin is already registered under its
-        canonical name. """
-        return self.hasplugin(self._get_canonical_name(plugin)) or \
-               plugin in self._plugins
+    def is_registered(self, plugin):
+        """ Return True if the plugin is already registered. """
+        return plugin in self._plugin2hookcallers
 
-    def hasplugin(self, name):
-        """ Return True if there is a registered with the given name. """
-        return name in self._name2plugin
+    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 getplugin(self, name):
+    def get_plugin(self, name):
         """ Return a plugin or None for the given name. """
         return self._name2plugin.get(name)
 
-    def listattr(self, attrname, plugins=None):
-        if plugins is None:
-            plugins = self._plugins
-        l = []
-        last = []
-        wrappers = []
-        for plugin in plugins:
+    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:
-                meth = getattr(plugin, attrname)
-            except AttributeError:
+                plugin = ep.load()
+            except DistributionNotFound:
                 continue
-            if hasattr(meth, 'hookwrapper'):
-                wrappers.append(meth)
-            elif hasattr(meth, 'tryfirst'):
-                last.append(meth)
-            elif hasattr(meth, 'trylast'):
-                l.insert(0, meth)
-            else:
-                l.append(meth)
-        l.extend(last)
-        l.extend(wrappers)
-        return l
-
-    def _scan_methods(self, hookcaller):
-        hookcaller.methods = self.listattr(hookcaller.name)
-
-    def call_plugin(self, plugin, methname, kwargs):
-        return MultiCall(methods=self.listattr(methname, plugins=[plugin]),
-                kwargs=kwargs, firstresult=True).execute()
-
-
-    def _scan_plugin(self, plugin):
-        def fail(msg, *args):
-            name = getattr(plugin, '__name__', plugin)
-            raise PluginValidationError("plugin %r\n%s" %(name, msg % args))
-
-        for name in dir(plugin):
-            if name[0] == "_" or not name.startswith(self._prefix):
-                continue
-            hook = getattr(self.hook, name, None)
-            method = getattr(plugin, name)
-            if hook is None:
-                if self._excludefunc is not None and self._excludefunc(name):
-                    continue
-                if getattr(method, 'optionalhook', False):
-                    continue
-                fail("found unknown hook: %r", name)
-            for arg in varnames(method):
-                if arg not in hook.argnames:
-                    fail("argument %r not available\n"
-                         "actual definition: %s\n"
-                         "available hookargs: %s",
-                         arg, formatdef(method),
-                           ", ".join(hook.argnames))
-            yield hook
-
-    def _get_canonical_name(self, plugin):
-        return getattr(plugin, "__name__", None) or str(id(plugin))
-
+            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 = list(methods)
+        self.methods = methods
         self.kwargs = kwargs
         self.kwargs["__multicall__"] = self
-        self.results = []
         self.firstresult = firstresult
 
-    def __repr__(self):
-        status = "%d results, %d meths" % (len(self.results), 
len(self.methods))
-        return "<MultiCall %s, kwargs=%r>" %(status, self.kwargs)
-
     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)]
@@ -342,11 +432,19 @@
                 return wrapped_call(method(*args), self.execute)
             res = method(*args)
             if res is not None:
-                self.results.append(res)
-                if self.firstresult:
+                if firstresult:
                     return res
-        if not self.firstresult:
-            return self.results
+                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):
@@ -361,17 +459,17 @@
         return cache["_varnames"]
     except KeyError:
         pass
-    if inspect.isclass(func):
+    if isclass(func):
         try:
             func = func.__init__
         except AttributeError:
             return ()
         startindex = 1
     else:
-        if not inspect.isfunction(func) and not inspect.ismethod(func):
+        if not isfunction(func) and not ismethod(func):
             func = getattr(func, '__call__', func)
         if startindex is None:
-            startindex = int(inspect.ismethod(func))
+            startindex = int(ismethod(func))
 
     rawcode = py.code.getrawcode(func)
     try:
@@ -390,32 +488,95 @@
 
 
 class HookRelay:
-    def __init__(self, pm):
-        self._pm = pm
-        self.trace = pm.trace.root.get("hook")
+    def __init__(self, trace):
+        self._trace = trace
 
 
-class HookCaller:
-    def __init__(self, name, firstresult, argnames, methods=()):
+class HookCaller(object):
+    def __init__(self, name, hook_execute, specmodule_or_class=None):
         self.name = name
-        self.firstresult = firstresult
-        self.argnames = ["__multicall__"]
-        self.argnames.extend(argnames)
+        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.methods = methods
+        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):
-        return self._docall(self.methods, kwargs)
+        assert not self.is_historic()
+        return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
 
-    def callextra(self, methods, **kwargs):
-        return self._docall(self.methods + methods, 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 _docall(self, methods, kwargs):
-        return MultiCall(methods, kwargs,
-                         firstresult=self.firstresult).execute()
+    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):
@@ -425,5 +586,5 @@
 def formatdef(func):
     return "%s%s" % (
         func.__name__,
-        inspect.formatargspec(*inspect.getargspec(func))
+        formatargspec(*getargspec(func))
     )

diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/helpconfig.py
--- a/_pytest/helpconfig.py
+++ b/_pytest/helpconfig.py
@@ -22,7 +22,7 @@
                help="store internal tracing debug information in 
'pytestdebug.log'.")
 
 
-@pytest.mark.hookwrapper
+@pytest.hookimpl_opts(hookwrapper=True)
 def pytest_cmdline_parse():
     outcome = yield
     config = outcome.get_result()
@@ -34,13 +34,15 @@
             pytest.__version__, py.__version__,
             ".".join(map(str, sys.version_info)),
             os.getcwd(), config._origargs))
-        config.pluginmanager.set_tracing(debugfile.write)
+        config.trace.root.setwriter(debugfile.write)
+        undo_tracing = config.pluginmanager.enable_tracing()
         sys.stderr.write("writing pytestdebug information to %s\n" % path)
         def unset_tracing():
             debugfile.close()
             sys.stderr.write("wrote pytestdebug information to %s\n" %
                              debugfile.name)
             config.trace.root.setwriter(None)
+            undo_tracing()
         config.add_cleanup(unset_tracing)
 
 def pytest_cmdline_main(config):

diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/hookspec.py
--- a/_pytest/hookspec.py
+++ b/_pytest/hookspec.py
@@ -1,27 +1,30 @@
 """ hook specifications for pytest plugins, invoked from main.py and builtin 
plugins.  """
 
+from _pytest.core import hookspec_opts
+
 # -------------------------------------------------------------------------
-# Initialization
+# Initialization hooks called for every plugin
 # -------------------------------------------------------------------------
 
+@hookspec_opts(historic=True)
 def pytest_addhooks(pluginmanager):
-    """called at plugin load time to allow adding new hooks via a call to
+    """called at plugin registration time to allow adding new hooks via a call 
to
     pluginmanager.addhooks(module_or_class, prefix)."""
 
 
+@hookspec_opts(historic=True)
 def pytest_namespace():
     """return dict of name->object to be made globally available in
-    the pytest namespace.  This hook is called before command line options
-    are parsed.
+    the pytest namespace.  This hook is called at plugin registration
+    time.
     """
 
-def pytest_cmdline_parse(pluginmanager, args):
-    """return initialized config object, parsing the specified args. """
-pytest_cmdline_parse.firstresult = True
+@hookspec_opts(historic=True)
+def pytest_plugin_registered(plugin, manager):
+    """ a new pytest plugin got registered. """
 
-def pytest_cmdline_preparse(config, args):
-    """(deprecated) modify command line arguments before option parsing. """
 
+@hookspec_opts(historic=True)
 def pytest_addoption(parser):
     """register argparse-style options and ini-style config values.
 
@@ -47,35 +50,43 @@
     via (deprecated) ``pytest.config``.
     """
 
+@hookspec_opts(historic=True)
+def pytest_configure(config):
+    """ called after command line options have been parsed
+    and all plugins and initial conftest files been loaded.
+    This hook is called for every plugin.
+    """
+
+# -------------------------------------------------------------------------
+# Bootstrapping hooks called for plugins registered early enough:
+# internal and 3rd party plugins as well as directly
+# discoverable conftest.py local plugins.
+# -------------------------------------------------------------------------
+
+@hookspec_opts(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)
 def pytest_cmdline_main(config):
     """ called for performing the main command line action. The default
     implementation will invoke the configure hooks and runtest_mainloop. """
-pytest_cmdline_main.firstresult = True
 
 def pytest_load_initial_conftests(args, early_config, parser):
     """ implements the loading of initial conftest files ahead
     of command line option parsing. """
 
-def pytest_configure(config):
-    """ called after command line options have been parsed
-        and all plugins and initial conftest files been loaded.
-    """
-
-def pytest_unconfigure(config):
-    """ called before test process is exited.  """
-
-def pytest_runtestloop(session):
-    """ called for performing the main runtest loop
-    (after collection finished). """
-pytest_runtestloop.firstresult = True
 
 # -------------------------------------------------------------------------
 # collection hooks
 # -------------------------------------------------------------------------
 
+@hookspec_opts(firstresult=True)
 def pytest_collection(session):
     """ perform the collection protocol for the given session. """
-pytest_collection.firstresult = True
 
 def pytest_collection_modifyitems(session, config, items):
     """ called after collection has been performed, may filter or re-order
@@ -84,16 +95,16 @@
 def pytest_collection_finish(session):
     """ called after collection has been performed and modified. """
 
+@hookspec_opts(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.
     """
-pytest_ignore_collect.firstresult = True
 
+@hookspec_opts(firstresult=True)
 def pytest_collect_directory(path, parent):
     """ called before traversing a directory for collection files. """
-pytest_collect_directory.firstresult = True
 
 def pytest_collect_file(path, parent):
     """ return collection Node or None for the given path. Any new node
@@ -112,29 +123,29 @@
 def pytest_deselected(items):
     """ called for test items deselected by keyword. """
 
+@hookspec_opts(firstresult=True)
 def pytest_make_collect_report(collector):
     """ perform ``collector.collect()`` and return a CollectReport. """
-pytest_make_collect_report.firstresult = True
 
 # -------------------------------------------------------------------------
 # Python test function related hooks
 # -------------------------------------------------------------------------
 
+@hookspec_opts(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.
     The pytest_collect_file hook needs to be used if you want to
     create test modules for files that do not match as a test module.
     """
-pytest_pycollect_makemodule.firstresult = True
 
+@hookspec_opts(firstresult=True)
 def pytest_pycollect_makeitem(collector, name, obj):
     """ return custom item/collector for a python object in a module, or None. 
 """
-pytest_pycollect_makeitem.firstresult = True
 
+@hookspec_opts(firstresult=True)
 def pytest_pyfunc_call(pyfuncitem):
     """ call underlying test function. """
-pytest_pyfunc_call.firstresult = True
 
 def pytest_generate_tests(metafunc):
     """ generate (multiple) parametrized calls to a test function."""
@@ -142,9 +153,16 @@
 # -------------------------------------------------------------------------
 # generic runtest related hooks
 # -------------------------------------------------------------------------
+
+@hookspec_opts(firstresult=True)
+def pytest_runtestloop(session):
+    """ called for performing the main runtest loop
+    (after collection finished). """
+
 def pytest_itemstart(item, node):
     """ (deprecated, use pytest_runtest_logstart). """
 
+@hookspec_opts(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
@@ -158,7 +176,6 @@
 
     :return boolean: True if no further hook implementations should be invoked.
     """
-pytest_runtest_protocol.firstresult = True
 
 def pytest_runtest_logstart(nodeid, location):
     """ signal the start of running a single test item. """
@@ -178,12 +195,12 @@
                    so that nextitem only needs to call setup-functions.
     """
 
+@hookspec_opts(firstresult=True)
 def pytest_runtest_makereport(item, call):
     """ return a :py:class:`_pytest.runner.TestReport` object
     for the given :py:class:`pytest.Item` and
     :py:class:`_pytest.runner.CallInfo`.
     """
-pytest_runtest_makereport.firstresult = True
 
 def pytest_runtest_logreport(report):
     """ process a test setup/call/teardown report relating to
@@ -199,6 +216,9 @@
 def pytest_sessionfinish(session, exitstatus):
     """ whole test run finishes. """
 
+def pytest_unconfigure(config):
+    """ called before test process is exited.  """
+
 
 # -------------------------------------------------------------------------
 # hooks for customising the assert methods
@@ -220,9 +240,9 @@
 def pytest_report_header(config, startdir):
     """ return a string to be displayed as header info for terminal 
reporting."""
 
+@hookspec_opts(firstresult=True)
 def pytest_report_teststatus(report):
     """ return result-category, shortletter and verbose word for reporting."""
-pytest_report_teststatus.firstresult = True
 
 def pytest_terminal_summary(terminalreporter):
     """ add additional section in terminal summary reporting.  """
@@ -236,17 +256,14 @@
 # doctest hooks
 # -------------------------------------------------------------------------
 
+@hookspec_opts(firstresult=True)
 def pytest_doctest_prepare_content(content):
     """ return processed content for a given doctest"""
-pytest_doctest_prepare_content.firstresult = True
 
 # -------------------------------------------------------------------------
 # error handling and internal debugging hooks
 # -------------------------------------------------------------------------
 
-def pytest_plugin_registered(plugin, manager):
-    """ a new pytest plugin got registered. """
-
 def pytest_internalerror(excrepr, excinfo):
     """ called for internal errors. """
 

diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/main.py
--- a/_pytest/main.py
+++ b/_pytest/main.py
@@ -151,18 +151,17 @@
         ignore_paths.extend([py.path.local(x) for x in excludeopt])
     return path in ignore_paths
 
-class FSHookProxy(object):
-    def __init__(self, fspath, config):
+class FSHookProxy:
+    def __init__(self, fspath, pm, remove_mods):
         self.fspath = fspath
-        self.config = config
+        self.pm = pm
+        self.remove_mods = remove_mods
 
     def __getattr__(self, name):
-        plugins = self.config._getmatchingplugins(self.fspath)
-        x = self.config.pluginmanager.make_hook_caller(name, plugins)
+        x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
         self.__dict__[name] = x
         return x
 
-
 def compatproperty(name):
     def fget(self):
         # deprecated - use pytest.name
@@ -362,9 +361,6 @@
     def listnames(self):
         return [x.name for x in self.listchain()]
 
-    def getplugins(self):
-        return self.config._getmatchingplugins(self.fspath)
-
     def addfinalizer(self, fin):
         """ register a function to be called when this node is finalized.
 
@@ -519,12 +515,12 @@
     def _makeid(self):
         return ""
 
-    @pytest.mark.tryfirst
+    @pytest.hookimpl_opts(tryfirst=True)
     def pytest_collectstart(self):
         if self.shouldstop:
             raise self.Interrupted(self.shouldstop)
 
-    @pytest.mark.tryfirst
+    @pytest.hookimpl_opts(tryfirst=True)
     def pytest_runtest_logreport(self, report):
         if report.failed and not hasattr(report, 'wasxfail'):
             self._testsfailed += 1
@@ -541,8 +537,20 @@
         try:
             return self._fs2hookproxy[fspath]
         except KeyError:
-            self._fs2hookproxy[fspath] = x = FSHookProxy(fspath, self.config)
-            return x
+            # check if we have the common case of running
+            # hooks with all conftest.py filesall conftest.py
+            pm = self.config.pluginmanager
+            my_conftestmodules = pm._getconftestmodules(fspath)
+            remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
+            if remove_mods:
+                # one or more conftests are not in use at this fspath
+                proxy = FSHookProxy(fspath, pm, remove_mods)
+            else:
+                # all plugis are active for this fspath
+                proxy = self.config.hook
+
+            self._fs2hookproxy[fspath] = proxy
+            return proxy
 
     def perform_collect(self, args=None, genitems=True):
         hook = self.config.hook

diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/nose.py
--- a/_pytest/nose.py
+++ b/_pytest/nose.py
@@ -24,7 +24,7 @@
         call.excinfo = call2.excinfo
 
 
-@pytest.mark.trylast
+@pytest.hookimpl_opts(trylast=True)
 def pytest_runtest_setup(item):
     if is_potential_nosetest(item):
         if isinstance(item.parent, pytest.Generator):

diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _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.")
 
-@pytest.mark.trylast
+@pytest.hookimpl_opts(trylast=True)
 def pytest_configure(config):
     if config.option.pastebin == "all":
         tr = config.pluginmanager.getplugin('terminalreporter')

diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/pytester.py
--- a/_pytest/pytester.py
+++ b/_pytest/pytester.py
@@ -11,7 +11,7 @@
 import py
 import pytest
 from py.builtin import print_
-from _pytest.core import HookCaller, add_method_wrapper
+from _pytest.core import TracedHookExecution
 
 from _pytest.main import Session, EXIT_OK
 
@@ -79,12 +79,12 @@
         self._pluginmanager = pluginmanager
         self.calls = []
 
-        def _docall(hookcaller, methods, kwargs):
-            self.calls.append(ParsedCall(hookcaller.name, kwargs))
-            yield
-        self._undo_wrapping = add_method_wrapper(HookCaller, _docall)
-        #if hasattr(pluginmanager, "config"):
-        #    pluginmanager.add_shutdown(self._undo_wrapping)
+        def before(hook, method, kwargs):
+            self.calls.append(ParsedCall(hook.name, kwargs))
+        def after(outcome, hook, method, kwargs):
+            pass
+        executor = TracedHookExecution(pluginmanager, before, after)
+        self._undo_wrapping = executor.undo
 
     def finish_recording(self):
         self._undo_wrapping()

diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/python.py
--- a/_pytest/python.py
+++ b/_pytest/python.py
@@ -172,7 +172,7 @@
 def pytest_sessionstart(session):
     session._fixturemanager = FixtureManager(session)
 
-@pytest.mark.trylast
+@pytest.hookimpl_opts(trylast=True)
 def pytest_namespace():
     raises.Exception = pytest.fail.Exception
     return {
@@ -191,7 +191,7 @@
     return request.config
 
 
-@pytest.mark.trylast
+@pytest.hookimpl_opts(trylast=True)
 def pytest_pyfunc_call(pyfuncitem):
     testfunction = pyfuncitem.obj
     if pyfuncitem._isyieldedfunction():
@@ -219,7 +219,7 @@
 def pytest_pycollect_makemodule(path, parent):
     return Module(path, parent)
 
-@pytest.mark.hookwrapper
+@pytest.hookimpl_opts(hookwrapper=True)
 def pytest_pycollect_makeitem(collector, name, obj):
     outcome = yield
     res = outcome.get_result()
@@ -375,13 +375,16 @@
         fixtureinfo = fm.getfixtureinfo(self, funcobj, cls)
         metafunc = Metafunc(funcobj, fixtureinfo, self.config,
                             cls=cls, module=module)
-        try:
-            methods = [module.pytest_generate_tests]
-        except AttributeError:
-            methods = []
+        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)
-        self.ihook.pytest_generate_tests.callextra(methods, metafunc=metafunc)
+        if methods:
+            self.ihook.pytest_generate_tests.call_extra(methods,
+                                                        
dict(metafunc=metafunc))
+        else:
+            self.ihook.pytest_generate_tests(metafunc=metafunc)
 
         Function = self._getcustomclass("Function")
         if not metafunc._calls:
@@ -1621,7 +1624,6 @@
         self.session = session
         self.config = session.config
         self._arg2fixturedefs = {}
-        self._seenplugins = set()
         self._holderobjseen = set()
         self._arg2finish = {}
         self._nodeid_and_autousenames = [("", 
self.config.getini("usefixtures"))]
@@ -1646,11 +1648,7 @@
                                                               node)
         return FuncFixtureInfo(argnames, names_closure, arg2fixturedefs)
 
-    ### XXX this hook should be called for historic events like 
pytest_configure
-    ### so that we don't have to do the below pytest_configure hook
     def pytest_plugin_registered(self, plugin):
-        if plugin in self._seenplugins:
-            return
         nodeid = None
         try:
             p = py.path.local(plugin.__file__)
@@ -1665,13 +1663,6 @@
                 if p.sep != "/":
                     nodeid = nodeid.replace(p.sep, "/")
         self.parsefactories(plugin, nodeid)
-        self._seenplugins.add(plugin)
-
-    @pytest.mark.tryfirst
-    def pytest_configure(self, config):
-        plugins = config.pluginmanager.getplugins()
-        for plugin in plugins:
-            self.pytest_plugin_registered(plugin)
 
     def _getautousenames(self, nodeid):
         """ return a tuple of fixture names to be used. """

diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/skipping.py
--- a/_pytest/skipping.py
+++ b/_pytest/skipping.py
@@ -133,7 +133,7 @@
         return expl
 
 
-@pytest.mark.tryfirst
+@pytest.hookimpl_opts(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())
 
-@pytest.mark.hookwrapper
+@pytest.hookimpl_opts(hookwrapper=True)
 def pytest_runtest_makereport(item, call):
     outcome = yield
     rep = outcome.get_result()

diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/terminal.py
--- a/_pytest/terminal.py
+++ b/_pytest/terminal.py
@@ -164,6 +164,8 @@
 
     def pytest_logwarning(self, code, fslocation, message, nodeid):
         warnings = self.stats.setdefault("warnings", [])
+        if isinstance(fslocation, tuple):
+            fslocation = "%s:%d" % fslocation
         warning = WarningReport(code=code, fslocation=fslocation,
                                 message=message, nodeid=nodeid)
         warnings.append(warning)
@@ -265,7 +267,7 @@
     def pytest_collection_modifyitems(self):
         self.report_collect(True)
 
-    @pytest.mark.trylast
+    @pytest.hookimpl_opts(trylast=True)
     def pytest_sessionstart(self, session):
         self._sessionstarttime = time.time()
         if not self.showheader:
@@ -350,7 +352,7 @@
                 indent = (len(stack) - 1) * "  "
                 self._tw.line("%s%s" % (indent, col))
 
-    @pytest.mark.hookwrapper
+    @pytest.hookimpl_opts(hookwrapper=True)
     def pytest_sessionfinish(self, exitstatus):
         outcome = yield
         outcome.get_result()

diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/unittest.py
--- a/_pytest/unittest.py
+++ b/_pytest/unittest.py
@@ -140,7 +140,7 @@
         if traceback:
             excinfo.traceback = traceback
 
-@pytest.mark.tryfirst
+@pytest.hookimpl_opts(tryfirst=True)
 def pytest_runtest_makereport(item, call):
     if isinstance(item, TestCaseFunction):
         if item._excinfo:
@@ -152,7 +152,7 @@
 
 # twisted trial support
 
-@pytest.mark.hookwrapper
+@pytest.hookimpl_opts(hookwrapper=True)
 def pytest_runtest_protocol(item):
     if isinstance(item, TestCaseFunction) and \
        'twisted.trial.unittest' in sys.modules:

diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 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.mark.tryfirst: mark a hook implementation function such that the 
plugin machinery will try to call it first/as early as possible.
+    @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.mark.trylast: mark a hook implementation function such that the 
plugin machinery will try to call it last/as late 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.
     
 
 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.mark.tryfirst: mark a hook implementation function such that the 
plugin machinery will try to call it first/as early as possible.
+    @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.mark.trylast: mark a hook implementation function such that the 
plugin machinery will try to call it last/as late 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.
     
 
 Reading markers which were set from multiple places

diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 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.mark.tryfirst
+    @pytest.hookimpl_opts(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.mark.tryfirst
+    @pytest.hookimpl_opts(tryfirst=True)
     def pytest_runtest_makereport(item, call, __multicall__):
         # execute all other hooks to obtain the report object
         rep = __multicall__.execute()

diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 doc/en/index.txt
--- a/doc/en/index.txt
+++ b/doc/en/index.txt
@@ -56,6 +56,7 @@
  - all collection, reporting, running aspects are delegated to hook functions
  - customizations can be per-directory, per-project or per PyPI released plugin
  - it is easy to add command line options or customize existing behaviour
+ - :ref:`easy to write your own plugins <writing-plugins>`
 
 
 .. _`easy`: 
http://bruynooghe.blogspot.com/2009/12/skipping-slow-test-by-default-in-pytest.html

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
pytest-commit@python.org
https://mail.python.org/mailman/listinfo/pytest-commit

Reply via email to