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