diff options
Diffstat (limited to 'third_party/python/pluggy')
18 files changed, 2663 insertions, 0 deletions
diff --git a/third_party/python/pluggy/LICENSE b/third_party/python/pluggy/LICENSE new file mode 100644 index 0000000000..121017d086 --- /dev/null +++ b/third_party/python/pluggy/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 holger krekel (rather uses bitbucket/hpk42) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/third_party/python/pluggy/MANIFEST.in b/third_party/python/pluggy/MANIFEST.in new file mode 100644 index 0000000000..0cf8f3e088 --- /dev/null +++ b/third_party/python/pluggy/MANIFEST.in @@ -0,0 +1,7 @@ +include CHANGELOG +include README.rst +include setup.py +include tox.ini +include LICENSE +graft testing +recursive-exclude * *.pyc *.pyo diff --git a/third_party/python/pluggy/PKG-INFO b/third_party/python/pluggy/PKG-INFO new file mode 100644 index 0000000000..6e2f59f1d3 --- /dev/null +++ b/third_party/python/pluggy/PKG-INFO @@ -0,0 +1,112 @@ +Metadata-Version: 1.2 +Name: pluggy +Version: 0.6.0 +Summary: plugin and hook calling mechanisms for python +Home-page: https://github.com/pytest-dev/pluggy +Author: Holger Krekel +Author-email: holger@merlinux.eu +License: MIT license +Description-Content-Type: UNKNOWN +Description: pluggy - A minimalist production ready plugin system + ==================================================== + |pypi| |anaconda| |versions| |travis| |appveyor| + + + This is the core framework used by the `pytest`_, `tox`_, and `devpi`_ projects. + + Please `read the docs`_ to learn more! + + A definitive example + ******************** + .. code-block:: python + + import pluggy + + hookspec = pluggy.HookspecMarker("myproject") + hookimpl = pluggy.HookimplMarker("myproject") + + + class MySpec(object): + """A hook specification namespace. + """ + @hookspec + def myhook(self, arg1, arg2): + """My special little hook that you can customize. + """ + + + class Plugin_1(object): + """A hook implementation namespace. + """ + @hookimpl + def myhook(self, arg1, arg2): + print("inside Plugin_1.myhook()") + return arg1 + arg2 + + + class Plugin_2(object): + """A 2nd hook implementation namespace. + """ + @hookimpl + def myhook(self, arg1, arg2): + print("inside Plugin_2.myhook()") + return arg1 - arg2 + + + # create a manager and add the spec + pm = pluggy.PluginManager("myproject") + pm.add_hookspecs(MySpec) + + # register plugins + pm.register(Plugin_1()) + pm.register(Plugin_2()) + + # call our `myhook` hook + results = pm.hook.myhook(arg1=1, arg2=2) + print(results) + + + .. badges + .. |pypi| image:: https://img.shields.io/pypi/v/pluggy.svg + :target: https://pypi.python.org/pypi/pluggy + .. |versions| image:: https://img.shields.io/pypi/pyversions/pluggy.svg + :target: https://pypi.python.org/pypi/pluggy + .. |travis| image:: https://img.shields.io/travis/pytest-dev/pluggy/master.svg + :target: https://travis-ci.org/pytest-dev/pluggy + .. |appveyor| image:: https://img.shields.io/appveyor/ci/pytestbot/pluggy/master.svg + :target: https://ci.appveyor.com/project/pytestbot/pluggy + .. |anaconda| image:: https://anaconda.org/conda-forge/pluggy/badges/version.svg + :target: https://anaconda.org/conda-forge/pluggy + + .. links + .. _pytest: + http://pytest.org + .. _tox: + https://tox.readthedocs.org + .. _devpi: + http://doc.devpi.net + .. _read the docs: + https://pluggy.readthedocs.io/en/latest/ + +Platform: unix +Platform: linux +Platform: osx +Platform: win32 +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: POSIX +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Topic :: Software Development :: Testing +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Utilities +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* diff --git a/third_party/python/pluggy/README.rst b/third_party/python/pluggy/README.rst new file mode 100644 index 0000000000..3636b6ec64 --- /dev/null +++ b/third_party/python/pluggy/README.rst @@ -0,0 +1,80 @@ +pluggy - A minimalist production ready plugin system +==================================================== +|pypi| |anaconda| |versions| |travis| |appveyor| + + +This is the core framework used by the `pytest`_, `tox`_, and `devpi`_ projects. + +Please `read the docs`_ to learn more! + +A definitive example +******************** +.. code-block:: python + + import pluggy + + hookspec = pluggy.HookspecMarker("myproject") + hookimpl = pluggy.HookimplMarker("myproject") + + + class MySpec(object): + """A hook specification namespace. + """ + @hookspec + def myhook(self, arg1, arg2): + """My special little hook that you can customize. + """ + + + class Plugin_1(object): + """A hook implementation namespace. + """ + @hookimpl + def myhook(self, arg1, arg2): + print("inside Plugin_1.myhook()") + return arg1 + arg2 + + + class Plugin_2(object): + """A 2nd hook implementation namespace. + """ + @hookimpl + def myhook(self, arg1, arg2): + print("inside Plugin_2.myhook()") + return arg1 - arg2 + + + # create a manager and add the spec + pm = pluggy.PluginManager("myproject") + pm.add_hookspecs(MySpec) + + # register plugins + pm.register(Plugin_1()) + pm.register(Plugin_2()) + + # call our `myhook` hook + results = pm.hook.myhook(arg1=1, arg2=2) + print(results) + + +.. badges +.. |pypi| image:: https://img.shields.io/pypi/v/pluggy.svg + :target: https://pypi.python.org/pypi/pluggy +.. |versions| image:: https://img.shields.io/pypi/pyversions/pluggy.svg + :target: https://pypi.python.org/pypi/pluggy +.. |travis| image:: https://img.shields.io/travis/pytest-dev/pluggy/master.svg + :target: https://travis-ci.org/pytest-dev/pluggy +.. |appveyor| image:: https://img.shields.io/appveyor/ci/pytestbot/pluggy/master.svg + :target: https://ci.appveyor.com/project/pytestbot/pluggy +.. |anaconda| image:: https://anaconda.org/conda-forge/pluggy/badges/version.svg + :target: https://anaconda.org/conda-forge/pluggy + +.. links +.. _pytest: + http://pytest.org +.. _tox: + https://tox.readthedocs.org +.. _devpi: + http://doc.devpi.net +.. _read the docs: + https://pluggy.readthedocs.io/en/latest/ diff --git a/third_party/python/pluggy/pluggy/__init__.py b/third_party/python/pluggy/pluggy/__init__.py new file mode 100644 index 0000000000..42d2220be4 --- /dev/null +++ b/third_party/python/pluggy/pluggy/__init__.py @@ -0,0 +1,684 @@ +import inspect +import warnings +from .callers import _multicall, HookCallError, _Result, _legacymulticall + +__version__ = '0.6.0' + +__all__ = ["PluginManager", "PluginValidationError", "HookCallError", + "HookspecMarker", "HookimplMarker"] + + +class PluginValidationError(Exception): + """ plugin failed validation. """ + + +class HookspecMarker(object): + """ Decorator helper class for marking functions as hook specifications. + + You can instantiate it with a project_name to get a decorator. + Calling PluginManager.add_hookspecs later will discover all marked functions + if the PluginManager uses the same project_name. + """ + + def __init__(self, project_name): + self.project_name = project_name + + def __call__(self, function=None, firstresult=False, historic=False): + """ if passed a function, directly sets attributes on the function + which will make it discoverable to add_hookspecs(). If passed no + function, returns a decorator which can be applied to a function + later using the attributes supplied. + + 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") + setattr(func, self.project_name + "_spec", + dict(firstresult=firstresult, historic=historic)) + return func + + if function is not None: + return setattr_hookspec_opts(function) + else: + return setattr_hookspec_opts + + +class HookimplMarker(object): + """ Decorator helper class for marking functions as hook implementations. + + You can instantiate with a project_name to get a decorator. + Calling PluginManager.register later will discover all marked functions + if the PluginManager uses the same project_name. + """ + def __init__(self, project_name): + self.project_name = project_name + + def __call__(self, function=None, hookwrapper=False, optionalhook=False, + tryfirst=False, trylast=False): + + """ if passed a function, directly sets attributes on the function + which will make it discoverable to register(). If passed no function, + returns a decorator which can be applied to a function later using + the attributes supplied. + + 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 a ``_Result`` object representing + the exception or result outcome of the inner calls (including other hookwrapper + calls). + + """ + def setattr_hookimpl_opts(func): + setattr(func, self.project_name + "_impl", + dict(hookwrapper=hookwrapper, optionalhook=optionalhook, + tryfirst=tryfirst, trylast=trylast)) + return func + + if function is None: + return setattr_hookimpl_opts + else: + return setattr_hookimpl_opts(function) + + +def normalize_hookimpl_opts(opts): + opts.setdefault("tryfirst", False) + opts.setdefault("trylast", False) + opts.setdefault("hookwrapper", False) + opts.setdefault("optionalhook", False) + + +class _TagTracer(object): + def __init__(self): + self._tag2proc = {} + self.writer = None + self.indent = 0 + + def get(self, name): + return _TagTracerSub(self, (name,)) + + def format_message(self, tags, args): + if isinstance(args[-1], dict): + extra = args[-1] + args = args[:-1] + else: + extra = {} + + content = " ".join(map(str, args)) + indent = " " * self.indent + + lines = [ + "%s%s [%s]\n" % (indent, content, ":".join(tags)) + ] + + for name, value in extra.items(): + lines.append("%s %s: %s\n" % (indent, name, value)) + return lines + + def processmessage(self, tags, args): + if self.writer is not None and args: + lines = self.format_message(tags, args) + self.writer(''.join(lines)) + try: + self._tag2proc[tags](tags, args) + except KeyError: + pass + + def setwriter(self, writer): + self.writer = writer + + def setprocessor(self, tags, processor): + if isinstance(tags, str): + tags = tuple(tags.split(":")) + else: + assert isinstance(tags, tuple) + self._tag2proc[tags] = processor + + +class _TagTracerSub(object): + 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,)) + + +class _TracedHookExecution(object): + 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, hook_impls, kwargs): + self.before(hook.name, hook_impls, kwargs) + outcome = _Result.from_call(lambda: self.oldcall(hook, hook_impls, kwargs)) + self.after(outcome, hook.name, hook_impls, kwargs) + return outcome.get_result() + + def undo(self): + self.pluginmanager._inner_hookexec = self.oldcall + + +class PluginManager(object): + """ Core Pluginmanager class which manages registration + of plugin objects and 1:N hook calling. + + You can register new hooks by calling ``add_hookspec(module_or_class)``. + You can register plugin objects (which contain hooks) by calling + ``register(plugin)``. The Pluginmanager is initialized with a + prefix that is searched for in the names of the dict of registered + plugin objects. An optional excludefunc allows to blacklist names which + are not considered as hooks despite a matching prefix. + + For debugging purposes you can call ``enable_tracing()`` + which will subsequently send debug information to the trace helper. + """ + + def __init__(self, project_name, implprefix=None): + """ if implprefix is given implementation functions + will be recognized if their name matches the implprefix. """ + self.project_name = project_name + self._name2plugin = {} + self._plugin2hookcallers = {} + self._plugin_distinfo = [] + self.trace = _TagTracer().get("pluginmanage") + self.hook = _HookRelay(self.trace.root.get("hook")) + self._implprefix = implprefix + self._inner_hookexec = lambda hook, methods, kwargs: \ + hook.multicall( + methods, kwargs, + firstresult=hook.spec_opts.get('firstresult'), + ) + + def _hookexec(self, hook, methods, kwargs): + # called from all hookcaller instances. + # enable_tracing will set its own wrapping function at self._inner_hookexec + return self._inner_hookexec(hook, methods, kwargs) + + def register(self, plugin, name=None): + """ Register a plugin and return its canonical name or None if the name + is blocked from registering. Raise a ValueError if the plugin is already + registered. """ + plugin_name = name or self.get_canonical_name(plugin) + + if plugin_name in self._name2plugin or plugin in self._plugin2hookcallers: + if self._name2plugin.get(plugin_name, -1) is None: + return # blocked plugin, return None to indicate no registration + raise ValueError("Plugin already registered: %s=%s\n%s" % + (plugin_name, plugin, self._name2plugin)) + + # XXX if an error happens we should make sure no state has been + # changed at point of return + self._name2plugin[plugin_name] = plugin + + # register matching hook implementations of the plugin + self._plugin2hookcallers[plugin] = hookcallers = [] + for name in dir(plugin): + hookimpl_opts = self.parse_hookimpl_opts(plugin, name) + if hookimpl_opts is not None: + normalize_hookimpl_opts(hookimpl_opts) + method = getattr(plugin, name) + hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts) + hook = getattr(self.hook, name, None) + if hook is None: + hook = _HookCaller(name, self._hookexec) + setattr(self.hook, name, hook) + elif hook.has_spec(): + self._verify_hook(hook, hookimpl) + hook._maybe_apply_history(hookimpl) + hook._add_hookimpl(hookimpl) + hookcallers.append(hook) + return plugin_name + + def parse_hookimpl_opts(self, plugin, name): + method = getattr(plugin, name) + if not inspect.isroutine(method): + return + try: + res = getattr(method, self.project_name + "_impl", None) + except Exception: + res = {} + if res is not None and not isinstance(res, dict): + # false positive + res = None + elif res is None and self._implprefix and name.startswith(self._implprefix): + res = {} + return res + + def unregister(self, plugin=None, name=None): + """ unregister a plugin object and all its contained hook implementations + from internal data structures. """ + if name is None: + assert plugin is not None, "one of name or plugin needs to be specified" + name = self.get_name(plugin) + + if plugin is None: + plugin = self.get_plugin(name) + + # if self._name2plugin[name] == None registration was blocked: ignore + if self._name2plugin.get(name): + del self._name2plugin[name] + + for hookcaller in self._plugin2hookcallers.pop(plugin, []): + hookcaller._remove_plugin(plugin) + + return plugin + + def set_blocked(self, name): + """ block registrations of the given name, unregister if already registered. """ + self.unregister(name=name) + self._name2plugin[name] = None + + def is_blocked(self, name): + """ return True if the name blogs registering plugins of that name. """ + return name in self._name2plugin and self._name2plugin[name] is None + + def add_hookspecs(self, module_or_class): + """ add new hook specifications defined in the given module_or_class. + Functions are recognized if they have been decorated accordingly. """ + names = [] + for name in dir(module_or_class): + spec_opts = self.parse_hookspec_opts(module_or_class, name) + if spec_opts is not None: + hc = getattr(self.hook, name, None) + if hc is None: + hc = _HookCaller(name, self._hookexec, module_or_class, spec_opts) + setattr(self.hook, name, hc) + else: + # plugins registered this hook without knowing the spec + hc.set_specification(module_or_class, spec_opts) + for hookfunction in (hc._wrappers + hc._nonwrappers): + self._verify_hook(hc, hookfunction) + names.append(name) + + if not names: + raise ValueError("did not find any %r hooks in %r" % + (self.project_name, module_or_class)) + + def parse_hookspec_opts(self, module_or_class, name): + method = getattr(module_or_class, name) + return getattr(method, self.project_name + "_spec", None) + + def get_plugins(self): + """ return the set of registered plugins. """ + return set(self._plugin2hookcallers) + + def is_registered(self, plugin): + """ Return True if the plugin is already registered. """ + return plugin in self._plugin2hookcallers + + def get_canonical_name(self, plugin): + """ Return canonical name for a plugin object. Note that a plugin + may be registered under a different name which was specified + by the caller of register(plugin, name). To obtain the name + of an registered plugin use ``get_name(plugin)`` instead.""" + return getattr(plugin, "__name__", None) or str(id(plugin)) + + def get_plugin(self, name): + """ Return a plugin or None for the given name. """ + return self._name2plugin.get(name) + + def has_plugin(self, name): + """ Return True if a plugin with the given name is registered. """ + return self.get_plugin(name) is not None + + 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, hookimpl): + if hook.is_historic() and hookimpl.hookwrapper: + raise PluginValidationError( + "Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" % + (hookimpl.plugin_name, hook.name)) + + # positional arg checking + notinspec = set(hookimpl.argnames) - set(hook.argnames) + if notinspec: + raise PluginValidationError( + "Plugin %r for hook %r\nhookimpl definition: %s\n" + "Argument(s) %s are declared in the hookimpl but " + "can not be found in the hookspec" % + (hookimpl.plugin_name, hook.name, + _formatdef(hookimpl.function), notinspec) + ) + + 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[0] != "_": + hook = getattr(self.hook, name) + if not hook.has_spec(): + for hookimpl in (hook._wrappers + hook._nonwrappers): + if not hookimpl.optionalhook: + raise PluginValidationError( + "unknown hook %r in plugin %r" % + (name, hookimpl.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, + VersionConflict) + for ep in iter_entry_points(entrypoint_name): + # is the plugin registered or blocked? + if self.get_plugin(ep.name) or self.is_blocked(ep.name): + continue + try: + plugin = ep.load() + except DistributionNotFound: + continue + except VersionConflict as e: + raise PluginValidationError( + "Plugin %r could not be loaded: %s!" % (ep.name, e)) + self.register(plugin, name=ep.name) + self._plugin_distinfo.append((plugin, ep.dist)) + return len(self._plugin_distinfo) + + def list_plugin_distinfo(self): + """ return list of distinfo/plugin tuples for all setuptools registered + plugins. """ + return list(self._plugin_distinfo) + + def list_name_plugin(self): + """ return list of name/plugin pairs. """ + return list(self._name2plugin.items()) + + def get_hookcallers(self, plugin): + """ get all hook callers for the specified plugin. """ + return self._plugin2hookcallers.get(plugin) + + def add_hookcall_monitoring(self, before, after): + """ add before/after tracing functions for all hooks + and return an undo function which, when called, + will remove the added tracers. + + ``before(hook_name, hook_impls, kwargs)`` will be called ahead + of all hook calls and receive a hookcaller instance, a list + of HookImpl instances and the keyword arguments for the hook call. + + ``after(outcome, hook_name, hook_impls, kwargs)`` receives the + same arguments as ``before`` but also a :py:class:`_Result`` object + which represents the result of the overall hook call. + """ + return _TracedHookExecution(self, before, after).undo + + def enable_tracing(self): + """ enable tracing of hook calls and return an undo function. """ + hooktrace = self.hook._trace + + def before(hook_name, methods, kwargs): + hooktrace.root.indent += 1 + hooktrace(hook_name, kwargs) + + def after(outcome, hook_name, methods, kwargs): + if outcome.excinfo is None: + hooktrace("finish", hook_name, "-->", outcome.get_result()) + hooktrace.root.indent -= 1 + + return self.add_hookcall_monitoring(before, after) + + 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 = [plug for plug in remove_plugins if hasattr(plug, name)] + if plugins_to_remove: + hc = _HookCaller(orig.name, orig._hookexec, orig._specmodule_or_class, + orig.spec_opts) + for hookimpl in (orig._wrappers + orig._nonwrappers): + plugin = hookimpl.plugin + if plugin not in plugins_to_remove: + hc._add_hookimpl(hookimpl) + # 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 varnames(func): + """Return tuple of positional and keywrord argument names for a function, + method, class or callable. + + In case of a class, its ``__init__`` method is considered. + For methods the ``self`` parameter is not included. + """ + cache = getattr(func, "__dict__", {}) + try: + return cache["_varnames"] + except KeyError: + pass + + if inspect.isclass(func): + try: + func = func.__init__ + except AttributeError: + return (), () + elif not inspect.isroutine(func): # callable object? + try: + func = getattr(func, '__call__', func) + except Exception: + return () + + try: # func MUST be a function or method here or we won't parse any args + spec = _getargspec(func) + except TypeError: + return (), () + + args, defaults = tuple(spec.args), spec.defaults + if defaults: + index = -len(defaults) + args, defaults = args[:index], tuple(args[index:]) + else: + defaults = () + + # strip any implicit instance arg + if args: + if inspect.ismethod(func) or ( + '.' in getattr(func, '__qualname__', ()) and args[0] == 'self' + ): + args = args[1:] + + assert "self" not in args # best naming practises check? + try: + cache["_varnames"] = args, defaults + except TypeError: + pass + return args, defaults + + +class _HookRelay(object): + """ hook holder object for performing 1:N hook calls where N is the number + of registered plugins. + + """ + + def __init__(self, trace): + self._trace = trace + + +class _HookCaller(object): + def __init__(self, name, hook_execute, specmodule_or_class=None, + spec_opts=None): + self.name = name + self._wrappers = [] + self._nonwrappers = [] + self._hookexec = hook_execute + self._specmodule_or_class = None + self.argnames = None + self.kwargnames = None + self.multicall = _multicall + self.spec_opts = spec_opts or {} + if specmodule_or_class is not None: + self.set_specification(specmodule_or_class, spec_opts) + + def has_spec(self): + return self._specmodule_or_class is not None + + def set_specification(self, specmodule_or_class, spec_opts): + assert not self.has_spec() + self._specmodule_or_class = specmodule_or_class + specfunc = getattr(specmodule_or_class, self.name) + # get spec arg signature + argnames, self.kwargnames = varnames(specfunc) + self.argnames = ["__multicall__"] + list(argnames) + self.spec_opts.update(spec_opts) + if spec_opts.get("historic"): + self._call_history = [] + + def is_historic(self): + return hasattr(self, "_call_history") + + def _remove_plugin(self, plugin): + def remove(wrappers): + for i, method in enumerate(wrappers): + if method.plugin == plugin: + del wrappers[i] + return True + if remove(self._wrappers) is None: + if remove(self._nonwrappers) is None: + raise ValueError("plugin %r not found" % (plugin,)) + + def _add_hookimpl(self, hookimpl): + """A an implementation to the callback chain. + """ + if hookimpl.hookwrapper: + methods = self._wrappers + else: + methods = self._nonwrappers + + if hookimpl.trylast: + methods.insert(0, hookimpl) + elif hookimpl.tryfirst: + methods.append(hookimpl) + else: + # find last non-tryfirst method + i = len(methods) - 1 + while i >= 0 and methods[i].tryfirst: + i -= 1 + methods.insert(i + 1, hookimpl) + + if '__multicall__' in hookimpl.argnames: + warnings.warn( + "Support for __multicall__ is now deprecated and will be" + "removed in an upcoming release.", + DeprecationWarning + ) + self.multicall = _legacymulticall + + def __repr__(self): + return "<_HookCaller %r>" % (self.name,) + + def __call__(self, *args, **kwargs): + if args: + raise TypeError("hook calling supports only keyword arguments") + assert not self.is_historic() + if self.argnames: + notincall = set(self.argnames) - set(['__multicall__']) - set( + kwargs.keys()) + if notincall: + warnings.warn( + "Argument(s) {} which are declared in the hookspec " + "can not be found in this hook call" + .format(tuple(notincall)), + stacklevel=2, + ) + return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs) + + def call_historic(self, proc=None, kwargs=None): + """ call the hook with given ``kwargs`` for all registered plugins and + for all plugins which will be registered afterwards. + + If ``proc`` is not None it will be called for for each non-None result + obtained from a hook implementation. + """ + self._call_history.append((kwargs or {}, proc)) + # historizing hooks don't return results + res = self._hookexec(self, self._nonwrappers + self._wrappers, kwargs) + for x in res or []: + proc(x) + + 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: + opts = dict(hookwrapper=False, trylast=False, tryfirst=False) + hookimpl = HookImpl(None, "<temp>", method, opts) + self._add_hookimpl(hookimpl) + try: + return self(**kwargs) + finally: + self._nonwrappers, self._wrappers = old + + def _maybe_apply_history(self, method): + """Apply call history to a new hookimpl if it is marked as historic. + """ + 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 HookImpl(object): + def __init__(self, plugin, plugin_name, function, hook_impl_opts): + self.function = function + self.argnames, self.kwargnames = varnames(self.function) + self.plugin = plugin + self.opts = hook_impl_opts + self.plugin_name = plugin_name + self.__dict__.update(hook_impl_opts) + + +if hasattr(inspect, 'getfullargspec'): + def _getargspec(func): + return inspect.getfullargspec(func) +else: + def _getargspec(func): + return inspect.getargspec(func) + + +if hasattr(inspect, 'signature'): + def _formatdef(func): + return "%s%s" % ( + func.__name__, + str(inspect.signature(func)) + ) +else: + def _formatdef(func): + return "%s%s" % ( + func.__name__, + inspect.formatargspec(*inspect.getargspec(func)) + ) diff --git a/third_party/python/pluggy/pluggy/callers.py b/third_party/python/pluggy/pluggy/callers.py new file mode 100644 index 0000000000..3ff67becff --- /dev/null +++ b/third_party/python/pluggy/pluggy/callers.py @@ -0,0 +1,201 @@ +''' +Call loop machinery +''' +import sys +import warnings + +_py3 = sys.version_info > (3, 0) + + +if not _py3: + exec(""" +def _reraise(cls, val, tb): + raise cls, val, tb +""") + + +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)) + + +class HookCallError(Exception): + """ Hook was called wrongly. """ + + +class _Result(object): + def __init__(self, result, excinfo): + self._result = result + self._excinfo = excinfo + + @property + def excinfo(self): + return self._excinfo + + @property + def result(self): + """Get the result(s) for this hook call (DEPRECATED in favor of ``get_result()``).""" + msg = 'Use get_result() which forces correct exception handling' + warnings.warn(DeprecationWarning(msg), stacklevel=2) + return self._result + + @classmethod + def from_call(cls, func): + __tracebackhide__ = True + result = excinfo = None + try: + result = func() + except BaseException: + excinfo = sys.exc_info() + + return cls(result, excinfo) + + def force_result(self, result): + """Force the result(s) to ``result``. + + If the hook was marked as a ``firstresult`` a single value should + be set otherwise set a (modified) list of results. Any exceptions + found during invocation will be deleted. + """ + self._result = result + self._excinfo = None + + def get_result(self): + """Get the result(s) for this hook call. + + If the hook was marked as a ``firstresult`` only a single value + will be returned otherwise a list of results. + """ + __tracebackhide__ = True + if self._excinfo is None: + return self._result + else: + ex = self._excinfo + if _py3: + raise ex[1].with_traceback(ex[2]) + _reraise(*ex) # noqa + + +def _wrapped_call(wrap_controller, func): + """ Wrap calling to a function with a generator which needs to yield + exactly once. The yield point will trigger calling the wrapped function + and return its ``_Result`` to the yield point. The generator then needs + to finish (raise StopIteration) in order for the wrapped call to complete. + """ + try: + next(wrap_controller) # first yield + except StopIteration: + _raise_wrapfail(wrap_controller, "did not yield") + call_outcome = _Result.from_call(func) + try: + wrap_controller.send(call_outcome) + _raise_wrapfail(wrap_controller, "has second yield") + except StopIteration: + pass + return call_outcome.get_result() + + +class _LegacyMultiCall(object): + """ 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, hook_impls, kwargs, firstresult=False): + self.hook_impls = hook_impls + self.caller_kwargs = kwargs # come from _HookCaller.__call__() + self.caller_kwargs["__multicall__"] = self + self.firstresult = firstresult + + def execute(self): + caller_kwargs = self.caller_kwargs + self.results = results = [] + firstresult = self.firstresult + + while self.hook_impls: + hook_impl = self.hook_impls.pop() + try: + args = [caller_kwargs[argname] for argname in hook_impl.argnames] + except KeyError: + for argname in hook_impl.argnames: + if argname not in caller_kwargs: + raise HookCallError( + "hook call must provide argument %r" % (argname,)) + if hook_impl.hookwrapper: + return _wrapped_call(hook_impl.function(*args), self.execute) + res = hook_impl.function(*args) + if res is not None: + if firstresult: + return res + results.append(res) + + if not firstresult: + return results + + def __repr__(self): + status = "%d meths" % (len(self.hook_impls),) + if hasattr(self, "results"): + status = ("%d results, " % len(self.results)) + status + return "<_MultiCall %s, kwargs=%r>" % (status, self.caller_kwargs) + + +def _legacymulticall(hook_impls, caller_kwargs, firstresult=False): + return _LegacyMultiCall( + hook_impls, caller_kwargs, firstresult=firstresult).execute() + + +def _multicall(hook_impls, caller_kwargs, firstresult=False): + """Execute a call into multiple python functions/methods and return the + result(s). + + ``caller_kwargs`` comes from _HookCaller.__call__(). + """ + __tracebackhide__ = True + results = [] + excinfo = None + try: # run impl and wrapper setup functions in a loop + teardowns = [] + try: + for hook_impl in reversed(hook_impls): + try: + args = [caller_kwargs[argname] for argname in hook_impl.argnames] + except KeyError: + for argname in hook_impl.argnames: + if argname not in caller_kwargs: + raise HookCallError( + "hook call must provide argument %r" % (argname,)) + + if hook_impl.hookwrapper: + try: + gen = hook_impl.function(*args) + next(gen) # first yield + teardowns.append(gen) + except StopIteration: + _raise_wrapfail(gen, "did not yield") + else: + res = hook_impl.function(*args) + if res is not None: + results.append(res) + if firstresult: # halt further impl calls + break + except BaseException: + excinfo = sys.exc_info() + finally: + if firstresult: # first result hooks return a single value + outcome = _Result(results[0] if results else None, excinfo) + else: + outcome = _Result(results, excinfo) + + # run all wrapper post-yield blocks + for gen in reversed(teardowns): + try: + gen.send(outcome) + _raise_wrapfail(gen, "has second yield") + except StopIteration: + pass + + return outcome.get_result() diff --git a/third_party/python/pluggy/setup.cfg b/third_party/python/pluggy/setup.cfg new file mode 100644 index 0000000000..ead73f887a --- /dev/null +++ b/third_party/python/pluggy/setup.cfg @@ -0,0 +1,13 @@ +[bdist_wheel] +universal = 1 + +[metadata] +license_file = LICENSE + +[devpi:upload] +formats = sdist.tgz,bdist_wheel + +[egg_info] +tag_build = +tag_date = 0 + diff --git a/third_party/python/pluggy/setup.py b/third_party/python/pluggy/setup.py new file mode 100644 index 0000000000..b7c0f69712 --- /dev/null +++ b/third_party/python/pluggy/setup.py @@ -0,0 +1,51 @@ +import os +from setuptools import setup + +classifiers = [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: POSIX', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: MacOS :: MacOS X', + 'Topic :: Software Development :: Testing', + 'Topic :: Software Development :: Libraries', + 'Topic :: Utilities', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy'] + [ + ('Programming Language :: Python :: %s' % x) for x in + '2 2.7 3 3.4 3.5 3.6'.split()] + +with open('README.rst') as fd: + long_description = fd.read() + + +def get_version(): + p = os.path.join(os.path.dirname( + os.path.abspath(__file__)), "pluggy/__init__.py") + with open(p) as f: + for line in f.readlines(): + if "__version__" in line: + return line.strip().split("=")[-1].strip(" '") + raise ValueError("could not read version") + + +def main(): + setup( + name='pluggy', + description='plugin and hook calling mechanisms for python', + long_description=long_description, + version=get_version(), + license='MIT license', + platforms=['unix', 'linux', 'osx', 'win32'], + author='Holger Krekel', + author_email='holger@merlinux.eu', + url='https://github.com/pytest-dev/pluggy', + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', + classifiers=classifiers, + packages=['pluggy'], + ) + + +if __name__ == '__main__': + main() diff --git a/third_party/python/pluggy/testing/benchmark.py b/third_party/python/pluggy/testing/benchmark.py new file mode 100644 index 0000000000..5a913e9d41 --- /dev/null +++ b/third_party/python/pluggy/testing/benchmark.py @@ -0,0 +1,59 @@ +""" +Benchmarking and performance tests. +""" +import pytest +from pluggy import (_multicall, _legacymulticall, HookImpl, HookspecMarker, + HookimplMarker) + +hookspec = HookspecMarker("example") +hookimpl = HookimplMarker("example") + + +def MC(methods, kwargs, callertype, firstresult=False): + hookfuncs = [] + for method in methods: + f = HookImpl(None, "<temp>", method, method.example_impl) + hookfuncs.append(f) + return callertype(hookfuncs, kwargs, {"firstresult": firstresult}) + + +@hookimpl +def hook(arg1, arg2, arg3): + return arg1, arg2, arg3 + + +@hookimpl(hookwrapper=True) +def wrapper(arg1, arg2, arg3): + yield + + +@pytest.fixture( + params=[10, 100], + ids="hooks={}".format, +) +def hooks(request): + return [hook for i in range(request.param)] + + +@pytest.fixture( + params=[10, 100], + ids="wrappers={}".format, +) +def wrappers(request): + return [wrapper for i in range(request.param)] + + +@pytest.fixture( + params=[_multicall, _legacymulticall], + ids=lambda item: item.__name__ +) +def callertype(request): + return request.param + + +def inner_exec(methods, callertype): + return MC(methods, {'arg1': 1, 'arg2': 2, 'arg3': 3}, callertype) + + +def test_hook_and_wrappers_speed(benchmark, hooks, wrappers, callertype): + benchmark(inner_exec, hooks + wrappers, callertype) diff --git a/third_party/python/pluggy/testing/conftest.py b/third_party/python/pluggy/testing/conftest.py new file mode 100644 index 0000000000..3d61a349c8 --- /dev/null +++ b/third_party/python/pluggy/testing/conftest.py @@ -0,0 +1,30 @@ +import pytest + + +@pytest.fixture( + params=[ + lambda spec: spec, + lambda spec: spec() + ], + ids=[ + "spec-is-class", + "spec-is-instance" + ], +) +def he_pm(request, pm): + from pluggy import HookspecMarker + hookspec = HookspecMarker("example") + + class Hooks(object): + @hookspec + def he_method1(self, arg): + return arg + 1 + + pm.add_hookspecs(request.param(Hooks)) + return pm + + +@pytest.fixture +def pm(): + from pluggy import PluginManager + return PluginManager("example") diff --git a/third_party/python/pluggy/testing/test_details.py b/third_party/python/pluggy/testing/test_details.py new file mode 100644 index 0000000000..2fad198d95 --- /dev/null +++ b/third_party/python/pluggy/testing/test_details.py @@ -0,0 +1,103 @@ +import warnings + +import pytest + +from pluggy import PluginManager, HookimplMarker, HookspecMarker, _Result + +hookspec = HookspecMarker("example") +hookimpl = HookimplMarker("example") + + +def test_parse_hookimpl_override(): + class MyPluginManager(PluginManager): + def parse_hookimpl_opts(self, module_or_class, name): + opts = PluginManager.parse_hookimpl_opts( + self, module_or_class, name) + if opts is None: + if name.startswith("x1"): + opts = {} + return opts + + class Plugin(object): + def x1meth(self): + pass + + @hookimpl(hookwrapper=True, tryfirst=True) + def x1meth2(self): + pass + + class Spec(object): + @hookspec + def x1meth(self): + pass + + @hookspec + def x1meth2(self): + pass + + pm = MyPluginManager(hookspec.project_name) + pm.register(Plugin()) + pm.add_hookspecs(Spec) + assert not pm.hook.x1meth._nonwrappers[0].hookwrapper + assert not pm.hook.x1meth._nonwrappers[0].tryfirst + assert not pm.hook.x1meth._nonwrappers[0].trylast + assert not pm.hook.x1meth._nonwrappers[0].optionalhook + + assert pm.hook.x1meth2._wrappers[0].tryfirst + assert pm.hook.x1meth2._wrappers[0].hookwrapper + + +def test_plugin_getattr_raises_errors(): + """Pluggy must be able to handle plugins which raise weird exceptions + when getattr() gets called (#11). + """ + class DontTouchMe(object): + def __getattr__(self, x): + raise Exception('cant touch me') + + class Module(object): + pass + + module = Module() + module.x = DontTouchMe() + + pm = PluginManager(hookspec.project_name) + # register() would raise an error + pm.register(module, 'donttouch') + assert pm.get_plugin('donttouch') is module + + +def test_warning_on_call_vs_hookspec_arg_mismatch(): + """Verify that is a hook is called with less arguments then defined in the + spec that a warning is emitted. + """ + class Spec: + @hookspec + def myhook(self, arg1, arg2): + pass + + class Plugin: + @hookimpl + def myhook(self, arg1): + pass + + pm = PluginManager(hookspec.project_name) + pm.register(Plugin()) + pm.add_hookspecs(Spec()) + + with warnings.catch_warnings(record=True) as warns: + warnings.simplefilter('always') + + # calling should trigger a warning + pm.hook.myhook(arg1=1) + + assert len(warns) == 1 + warning = warns[-1] + assert issubclass(warning.category, Warning) + assert "Argument(s) ('arg2',)" in str(warning.message) + + +def test_result_deprecated(): + r = _Result(10, None) + with pytest.deprecated_call(): + assert r.result == 10 diff --git a/third_party/python/pluggy/testing/test_helpers.py b/third_party/python/pluggy/testing/test_helpers.py new file mode 100644 index 0000000000..b178096844 --- /dev/null +++ b/third_party/python/pluggy/testing/test_helpers.py @@ -0,0 +1,68 @@ +from pluggy import _formatdef, varnames + + +def test_varnames(): + def f(x): + i = 3 # noqa + + class A(object): + def f(self, y): + pass + + class B(object): + def __call__(self, z): + pass + + assert varnames(f) == (("x",), ()) + assert varnames(A().f) == (('y',), ()) + assert varnames(B()) == (('z',), ()) + + +def test_varnames_default(): + def f(x, y=3): + pass + + assert varnames(f) == (("x",), ("y",)) + + +def test_varnames_class(): + class C(object): + def __init__(self, x): + pass + + class D(object): + pass + + class E(object): + def __init__(self, x): + pass + + class F(object): + pass + + assert varnames(C) == (("x",), ()) + assert varnames(D) == ((), ()) + assert varnames(E) == (("x",), ()) + assert varnames(F) == ((), ()) + + +def test_formatdef(): + def function1(): + pass + + assert _formatdef(function1) == 'function1()' + + def function2(arg1): + pass + + assert _formatdef(function2) == "function2(arg1)" + + def function3(arg1, arg2="qwe"): + pass + + assert _formatdef(function3) == "function3(arg1, arg2='qwe')" + + def function4(arg1, *args, **kwargs): + pass + + assert _formatdef(function4) == "function4(arg1, *args, **kwargs)" diff --git a/third_party/python/pluggy/testing/test_hookrelay.py b/third_party/python/pluggy/testing/test_hookrelay.py new file mode 100644 index 0000000000..5e7821bed8 --- /dev/null +++ b/third_party/python/pluggy/testing/test_hookrelay.py @@ -0,0 +1,210 @@ +import pytest +from pluggy import PluginValidationError, HookimplMarker, HookspecMarker + + +hookspec = HookspecMarker("example") +hookimpl = HookimplMarker("example") + + +def test_happypath(pm): + class Api(object): + @hookspec + def hello(self, arg): + "api hook 1" + + pm.add_hookspecs(Api) + hook = pm.hook + assert hasattr(hook, 'hello') + assert repr(hook.hello).find("hello") != -1 + + class Plugin(object): + @hookimpl + def hello(self, arg): + return arg + 1 + + plugin = Plugin() + pm.register(plugin) + out = hook.hello(arg=3) + assert out == [4] + assert not hasattr(hook, 'world') + pm.unregister(plugin) + assert hook.hello(arg=3) == [] + + +def test_argmismatch(pm): + class Api(object): + @hookspec + def hello(self, arg): + "api hook 1" + + pm.add_hookspecs(Api) + + class Plugin(object): + @hookimpl + def hello(self, argwrong): + pass + + with pytest.raises(PluginValidationError) as exc: + pm.register(Plugin()) + + assert "argwrong" in str(exc.value) + + +def test_only_kwargs(pm): + class Api(object): + @hookspec + def hello(self, arg): + "api hook 1" + + pm.add_hookspecs(Api) + with pytest.raises(TypeError) as exc: + pm.hook.hello(3) + + comprehensible = "hook calling supports only keyword arguments" + assert comprehensible in str(exc.value) + + +def test_call_order(pm): + class Api(object): + @hookspec + def hello(self, arg): + "api hook 1" + + pm.add_hookspecs(Api) + + class Plugin1(object): + @hookimpl + def hello(self, arg): + return 1 + + class Plugin2(object): + @hookimpl + def hello(self, arg): + return 2 + + class Plugin3(object): + @hookimpl + def hello(self, arg): + return 3 + + class Plugin4(object): + @hookimpl(hookwrapper=True) + def hello(self, arg): + assert arg == 0 + outcome = yield + assert outcome.get_result() == [3, 2, 1] + + pm.register(Plugin1()) + pm.register(Plugin2()) + pm.register(Plugin3()) + pm.register(Plugin4()) # hookwrapper should get same list result + res = pm.hook.hello(arg=0) + assert res == [3, 2, 1] + + +def test_firstresult_definition(pm): + class Api(object): + @hookspec(firstresult=True) + def hello(self, arg): + "api hook 1" + + pm.add_hookspecs(Api) + + class Plugin1(object): + @hookimpl + def hello(self, arg): + return arg + 1 + + class Plugin2(object): + @hookimpl + def hello(self, arg): + return arg - 1 + + class Plugin3(object): + @hookimpl + def hello(self, arg): + return None + + class Plugin4(object): + @hookimpl(hookwrapper=True) + def hello(self, arg): + assert arg == 3 + outcome = yield + assert outcome.get_result() == 2 + + pm.register(Plugin1()) # discarded - not the last registered plugin + pm.register(Plugin2()) # used as result + pm.register(Plugin3()) # None result is ignored + pm.register(Plugin4()) # hookwrapper should get same non-list result + res = pm.hook.hello(arg=3) + assert res == 2 + + +def test_firstresult_force_result(pm): + """Verify forcing a result in a wrapper. + """ + class Api(object): + @hookspec(firstresult=True) + def hello(self, arg): + "api hook 1" + + pm.add_hookspecs(Api) + + class Plugin1(object): + @hookimpl + def hello(self, arg): + return arg + 1 + + class Plugin2(object): + @hookimpl(hookwrapper=True) + def hello(self, arg): + assert arg == 3 + outcome = yield + assert outcome.get_result() == 4 + outcome.force_result(0) + + class Plugin3(object): + @hookimpl + def hello(self, arg): + return None + + pm.register(Plugin1()) + pm.register(Plugin2()) # wrapper + pm.register(Plugin3()) # ignored since returns None + res = pm.hook.hello(arg=3) + assert res == 0 # this result is forced and not a list + + +def test_firstresult_returns_none(pm): + """If None results are returned by underlying implementations ensure + the multi-call loop returns a None value. + """ + class Api(object): + @hookspec(firstresult=True) + def hello(self, arg): + "api hook 1" + + pm.add_hookspecs(Api) + + class Plugin1(object): + @hookimpl + def hello(self, arg): + return None + + pm.register(Plugin1()) + res = pm.hook.hello(arg=3) + assert res is None + + +def test_firstresult_no_plugin(pm): + """If no implementations/plugins have been registered for a firstresult + hook the multi-call loop should return a None value. + """ + class Api(object): + @hookspec(firstresult=True) + def hello(self, arg): + "api hook 1" + + pm.add_hookspecs(Api) + res = pm.hook.hello(arg=3) + assert res is None diff --git a/third_party/python/pluggy/testing/test_method_ordering.py b/third_party/python/pluggy/testing/test_method_ordering.py new file mode 100644 index 0000000000..9584a0ae5a --- /dev/null +++ b/third_party/python/pluggy/testing/test_method_ordering.py @@ -0,0 +1,322 @@ +import pytest + + +import sys +import types + +from pluggy import PluginManager, HookImpl, HookimplMarker, HookspecMarker + +hookspec = HookspecMarker("example") +hookimpl = HookimplMarker("example") + + +@pytest.fixture +def hc(pm): + class Hooks(object): + @hookspec + def he_method1(self, arg): + pass + pm.add_hookspecs(Hooks) + return pm.hook.he_method1 + + +@pytest.fixture +def addmeth(hc): + def addmeth(tryfirst=False, trylast=False, hookwrapper=False): + def wrap(func): + hookimpl(tryfirst=tryfirst, trylast=trylast, + hookwrapper=hookwrapper)(func) + hc._add_hookimpl(HookImpl(None, "<temp>", func, func.example_impl)) + return func + return wrap + return addmeth + + +def funcs(hookmethods): + return [hookmethod.function for hookmethod in hookmethods] + + +def test_adding_nonwrappers(hc, addmeth): + @addmeth() + def he_method1(): + pass + + @addmeth() + def he_method2(): + pass + + @addmeth() + def he_method3(): + pass + assert funcs(hc._nonwrappers) == [he_method1, he_method2, he_method3] + + +def test_adding_nonwrappers_trylast(hc, addmeth): + @addmeth() + def he_method1_middle(): + pass + + @addmeth(trylast=True) + def he_method1(): + pass + + @addmeth() + def he_method1_b(): + pass + assert funcs(hc._nonwrappers) == [he_method1, he_method1_middle, he_method1_b] + + +def test_adding_nonwrappers_trylast3(hc, addmeth): + @addmeth() + def he_method1_a(): + pass + + @addmeth(trylast=True) + def he_method1_b(): + pass + + @addmeth() + def he_method1_c(): + pass + + @addmeth(trylast=True) + def he_method1_d(): + pass + assert funcs(hc._nonwrappers) == \ + [he_method1_d, he_method1_b, he_method1_a, he_method1_c] + + +def test_adding_nonwrappers_trylast2(hc, addmeth): + @addmeth() + def he_method1_middle(): + pass + + @addmeth() + def he_method1_b(): + pass + + @addmeth(trylast=True) + def he_method1(): + pass + assert funcs(hc._nonwrappers) == \ + [he_method1, he_method1_middle, he_method1_b] + + +def test_adding_nonwrappers_tryfirst(hc, addmeth): + @addmeth(tryfirst=True) + def he_method1(): + pass + + @addmeth() + def he_method1_middle(): + pass + + @addmeth() + def he_method1_b(): + pass + assert funcs(hc._nonwrappers) == [ + he_method1_middle, he_method1_b, he_method1] + + +def test_adding_wrappers_ordering(hc, addmeth): + @addmeth(hookwrapper=True) + def he_method1(): + pass + + @addmeth() + def he_method1_middle(): + pass + + @addmeth(hookwrapper=True) + def he_method3(): + pass + + assert funcs(hc._nonwrappers) == [he_method1_middle] + assert funcs(hc._wrappers) == [he_method1, he_method3] + + +def test_adding_wrappers_ordering_tryfirst(hc, addmeth): + @addmeth(hookwrapper=True, tryfirst=True) + def he_method1(): + pass + + @addmeth(hookwrapper=True) + def he_method2(): + pass + + assert hc._nonwrappers == [] + assert funcs(hc._wrappers) == [he_method2, he_method1] + + +def test_hookspec(pm): + class HookSpec(object): + @hookspec() + def he_myhook1(arg1): + pass + + @hookspec(firstresult=True) + def he_myhook2(arg1): + pass + + @hookspec(firstresult=False) + def he_myhook3(arg1): + pass + + pm.add_hookspecs(HookSpec) + assert not pm.hook.he_myhook1.spec_opts["firstresult"] + assert pm.hook.he_myhook2.spec_opts["firstresult"] + assert not pm.hook.he_myhook3.spec_opts["firstresult"] + + +@pytest.mark.parametrize('name', ["hookwrapper", "optionalhook", "tryfirst", "trylast"]) +@pytest.mark.parametrize('val', [True, False]) +def test_hookimpl(name, val): + @hookimpl(**{name: val}) + def he_myhook1(arg1): + pass + if val: + assert he_myhook1.example_impl.get(name) + else: + assert not hasattr(he_myhook1, name) + + +def test_load_setuptools_instantiation(monkeypatch, pm): + pkg_resources = pytest.importorskip("pkg_resources") + + def my_iter(name): + assert name == "hello" + + class EntryPoint(object): + name = "myname" + dist = None + + def load(self): + class PseudoPlugin(object): + x = 42 + return PseudoPlugin() + + return iter([EntryPoint()]) + + monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) + num = pm.load_setuptools_entrypoints("hello") + assert num == 1 + plugin = pm.get_plugin("myname") + assert plugin.x == 42 + assert pm.list_plugin_distinfo() == [(plugin, None)] + + +def test_load_setuptools_not_installed(monkeypatch, pm): + monkeypatch.setitem( + sys.modules, 'pkg_resources', + types.ModuleType("pkg_resources")) + + with pytest.raises(ImportError): + pm.load_setuptools_entrypoints("qwe") + + +def test_add_tracefuncs(he_pm): + out = [] + + class api1(object): + @hookimpl + def he_method1(self): + out.append("he_method1-api1") + + class api2(object): + @hookimpl + def he_method1(self): + out.append("he_method1-api2") + + he_pm.register(api1()) + he_pm.register(api2()) + + def before(hook_name, hook_impls, kwargs): + out.append((hook_name, list(hook_impls), kwargs)) + + def after(outcome, hook_name, hook_impls, kwargs): + out.append((outcome, hook_name, list(hook_impls), kwargs)) + + undo = he_pm.add_hookcall_monitoring(before, after) + + he_pm.hook.he_method1(arg=1) + assert len(out) == 4 + assert out[0][0] == "he_method1" + assert len(out[0][1]) == 2 + assert isinstance(out[0][2], dict) + assert out[1] == "he_method1-api2" + assert out[2] == "he_method1-api1" + assert len(out[3]) == 4 + assert out[3][1] == out[0][0] + + undo() + he_pm.hook.he_method1(arg=1) + assert len(out) == 4 + 2 + + +def test_hook_tracing(he_pm): + saveindent = [] + + class api1(object): + @hookimpl + def he_method1(self): + saveindent.append(he_pm.trace.root.indent) + + class api2(object): + @hookimpl + def he_method1(self): + saveindent.append(he_pm.trace.root.indent) + raise ValueError() + + he_pm.register(api1()) + out = [] + he_pm.trace.root.setwriter(out.append) + undo = he_pm.enable_tracing() + try: + indent = he_pm.trace.root.indent + he_pm.hook.he_method1(arg=1) + assert indent == he_pm.trace.root.indent + assert len(out) == 2 + assert 'he_method1' in out[0] + assert 'finish' in out[1] + + out[:] = [] + he_pm.register(api2()) + + with pytest.raises(ValueError): + he_pm.hook.he_method1(arg=1) + assert he_pm.trace.root.indent == indent + assert saveindent[0] > indent + finally: + undo() + + +@pytest.mark.parametrize('include_hookspec', [True, False]) +def test_prefix_hookimpl(include_hookspec): + pm = PluginManager(hookspec.project_name, "hello_") + + if include_hookspec: + class HookSpec(object): + @hookspec + def hello_myhook(self, arg1): + """ add to arg1 """ + + pm.add_hookspecs(HookSpec) + + class Plugin(object): + def hello_myhook(self, arg1): + return arg1 + 1 + + pm.register(Plugin()) + pm.register(Plugin()) + results = pm.hook.hello_myhook(arg1=17) + assert results == [18, 18] + + +def test_prefix_hookimpl_dontmatch_module(): + pm = PluginManager(hookspec.project_name, "hello_") + + class BadPlugin(object): + hello_module = __import__('email') + + pm.register(BadPlugin()) + pm.check_pending() diff --git a/third_party/python/pluggy/testing/test_multicall.py b/third_party/python/pluggy/testing/test_multicall.py new file mode 100644 index 0000000000..860a209b66 --- /dev/null +++ b/third_party/python/pluggy/testing/test_multicall.py @@ -0,0 +1,194 @@ +import pytest + +from pluggy import _multicall, _legacymulticall, HookImpl, HookCallError +from pluggy.callers import _LegacyMultiCall +from pluggy import HookspecMarker, HookimplMarker + + +hookspec = HookspecMarker("example") +hookimpl = HookimplMarker("example") + + +def test_uses_copy_of_methods(): + out = [lambda: 42] + mc = _LegacyMultiCall(out, {}) + repr(mc) + out[:] = [] + res = mc.execute() + return res == 42 + + +def MC(methods, kwargs, firstresult=False): + caller = _multicall + hookfuncs = [] + for method in methods: + f = HookImpl(None, "<temp>", method, method.example_impl) + hookfuncs.append(f) + if '__multicall__' in f.argnames: + caller = _legacymulticall + return caller(hookfuncs, kwargs, firstresult=firstresult) + + +def test_call_passing(): + class P1(object): + @hookimpl + def m(self, __multicall__, x): + assert len(__multicall__.results) == 1 + assert not __multicall__.hook_impls + return 17 + + class P2(object): + @hookimpl + def m(self, __multicall__, x): + assert __multicall__.results == [] + assert __multicall__.hook_impls + return 23 + + p1 = P1() + p2 = P2() + reslist = MC([p1.m, p2.m], {"x": 23}) + assert len(reslist) == 2 + # ensure reversed order + assert reslist == [23, 17] + + +def test_keyword_args(): + @hookimpl + def f(x): + return x + 1 + + class A(object): + @hookimpl + def f(self, x, y): + return x + y + + reslist = MC([f, A().f], dict(x=23, y=24)) + assert reslist == [24 + 23, 24] + + +def test_keyword_args_with_defaultargs(): + @hookimpl + def f(x, z=1): + return x + z + reslist = MC([f], dict(x=23, y=24)) + assert reslist == [24] + + +def test_tags_call_error(): + @hookimpl + def f(x): + return x + with pytest.raises(HookCallError): + MC([f], {}) + + +def test_call_subexecute(): + @hookimpl + def m(__multicall__): + subresult = __multicall__.execute() + return subresult + 1 + + @hookimpl + def n(): + return 1 + + res = MC([n, m], {}, firstresult=True) + assert res == 2 + + +def test_call_none_is_no_result(): + @hookimpl + def m1(): + return 1 + + @hookimpl + def m2(): + return None + + res = MC([m1, m2], {}, firstresult=True) + assert res == 1 + res = MC([m1, m2], {}, {}) + assert res == [1] + + +def test_hookwrapper(): + out = [] + + @hookimpl(hookwrapper=True) + def m1(): + out.append("m1 init") + yield None + out.append("m1 finish") + + @hookimpl + def m2(): + out.append("m2") + return 2 + + res = MC([m2, m1], {}) + assert res == [2] + assert out == ["m1 init", "m2", "m1 finish"] + out[:] = [] + res = MC([m2, m1], {}, firstresult=True) + assert res == 2 + assert out == ["m1 init", "m2", "m1 finish"] + + +def test_hookwrapper_order(): + out = [] + + @hookimpl(hookwrapper=True) + def m1(): + out.append("m1 init") + yield 1 + out.append("m1 finish") + + @hookimpl(hookwrapper=True) + def m2(): + out.append("m2 init") + yield 2 + out.append("m2 finish") + + res = MC([m2, m1], {}) + assert res == [] + assert out == ["m1 init", "m2 init", "m2 finish", "m1 finish"] + + +def test_hookwrapper_not_yield(): + @hookimpl(hookwrapper=True) + def m1(): + pass + + with pytest.raises(TypeError): + MC([m1], {}) + + +def test_hookwrapper_too_many_yield(): + @hookimpl(hookwrapper=True) + def m1(): + yield 1 + yield 2 + + with pytest.raises(RuntimeError) as ex: + MC([m1], {}) + assert "m1" in str(ex.value) + assert (__file__ + ':') in str(ex.value) + + +@pytest.mark.parametrize("exc", [ValueError, SystemExit]) +def test_hookwrapper_exception(exc): + out = [] + + @hookimpl(hookwrapper=True) + def m1(): + out.append("m1 init") + yield None + out.append("m1 finish") + + @hookimpl + def m2(): + raise exc + + with pytest.raises(exc): + MC([m2, m1], {}) + assert out == ["m1 init", "m1 finish"] diff --git a/third_party/python/pluggy/testing/test_pluginmanager.py b/third_party/python/pluggy/testing/test_pluginmanager.py new file mode 100644 index 0000000000..e2c86cc644 --- /dev/null +++ b/third_party/python/pluggy/testing/test_pluginmanager.py @@ -0,0 +1,374 @@ +import pytest +import types + +from pluggy import (PluginValidationError, + HookCallError, HookimplMarker, HookspecMarker) + + +hookspec = HookspecMarker("example") +hookimpl = HookimplMarker("example") + + +def test_plugin_double_register(pm): + pm.register(42, name="abc") + with pytest.raises(ValueError): + pm.register(42, name="abc") + with pytest.raises(ValueError): + pm.register(42, name="def") + + +def test_pm(pm): + class A(object): + pass + + a1, a2 = A(), A() + pm.register(a1) + assert pm.is_registered(a1) + pm.register(a2, "hello") + assert pm.is_registered(a2) + out = pm.get_plugins() + assert a1 in out + assert a2 in out + assert pm.get_plugin('hello') == a2 + assert pm.unregister(a1) == a1 + assert not pm.is_registered(a1) + + out = pm.list_name_plugin() + assert len(out) == 1 + assert out == [("hello", a2)] + + +def test_has_plugin(pm): + class A(object): + pass + + a1 = A() + pm.register(a1, 'hello') + assert pm.is_registered(a1) + assert pm.has_plugin('hello') + + +def test_register_dynamic_attr(he_pm): + class A(object): + def __getattr__(self, name): + if name[0] != "_": + return 42 + raise AttributeError() + + a = A() + he_pm.register(a) + assert not he_pm.get_hookcallers(a) + + +def test_pm_name(pm): + class A(object): + pass + + a1 = A() + name = pm.register(a1, name="hello") + assert name == "hello" + pm.unregister(a1) + assert pm.get_plugin(a1) is None + assert not pm.is_registered(a1) + assert not pm.get_plugins() + name2 = pm.register(a1, name="hello") + assert name2 == name + pm.unregister(name="hello") + assert pm.get_plugin(a1) is None + assert not pm.is_registered(a1) + assert not pm.get_plugins() + + +def test_set_blocked(pm): + class A(object): + pass + + a1 = A() + name = pm.register(a1) + assert pm.is_registered(a1) + assert not pm.is_blocked(name) + pm.set_blocked(name) + assert pm.is_blocked(name) + assert not pm.is_registered(a1) + + pm.set_blocked("somename") + assert pm.is_blocked("somename") + assert not pm.register(A(), "somename") + pm.unregister(name="somename") + assert pm.is_blocked("somename") + + +def test_register_mismatch_method(he_pm): + class hello(object): + @hookimpl + def he_method_notexists(self): + pass + + he_pm.register(hello()) + with pytest.raises(PluginValidationError): + he_pm.check_pending() + + +def test_register_mismatch_arg(he_pm): + class hello(object): + @hookimpl + def he_method1(self, qlwkje): + pass + + with pytest.raises(PluginValidationError): + he_pm.register(hello()) + + +def test_register(pm): + class MyPlugin(object): + pass + my = MyPlugin() + pm.register(my) + assert my in pm.get_plugins() + my2 = MyPlugin() + pm.register(my2) + assert set([my, my2]).issubset(pm.get_plugins()) + + assert pm.is_registered(my) + assert pm.is_registered(my2) + pm.unregister(my) + assert not pm.is_registered(my) + assert my not in pm.get_plugins() + + +def test_register_unknown_hooks(pm): + class Plugin1(object): + @hookimpl + def he_method1(self, arg): + return arg + 1 + + pname = pm.register(Plugin1()) + + class Hooks(object): + @hookspec + def he_method1(self, arg): + pass + + pm.add_hookspecs(Hooks) + # assert not pm._unverified_hooks + assert pm.hook.he_method1(arg=1) == [2] + assert len(pm.get_hookcallers(pm.get_plugin(pname))) == 1 + + +def test_register_historic(pm): + class Hooks(object): + @hookspec(historic=True) + def he_method1(self, arg): + pass + pm.add_hookspecs(Hooks) + + pm.hook.he_method1.call_historic(kwargs=dict(arg=1)) + out = [] + + class Plugin(object): + @hookimpl + def he_method1(self, arg): + out.append(arg) + + pm.register(Plugin()) + assert out == [1] + + class Plugin2(object): + @hookimpl + def he_method1(self, arg): + out.append(arg * 10) + + pm.register(Plugin2()) + assert out == [1, 10] + pm.hook.he_method1.call_historic(kwargs=dict(arg=12)) + assert out == [1, 10, 120, 12] + + +def test_with_result_memorized(pm): + class Hooks(object): + @hookspec(historic=True) + def he_method1(self, arg): + pass + pm.add_hookspecs(Hooks) + + he_method1 = pm.hook.he_method1 + he_method1.call_historic(lambda res: out.append(res), dict(arg=1)) + out = [] + + class Plugin(object): + @hookimpl + def he_method1(self, arg): + return arg * 10 + + pm.register(Plugin()) + assert out == [10] + + +def test_with_callbacks_immediately_executed(pm): + class Hooks(object): + @hookspec(historic=True) + def he_method1(self, arg): + pass + pm.add_hookspecs(Hooks) + + class Plugin1(object): + @hookimpl + def he_method1(self, arg): + return arg * 10 + + class Plugin2(object): + @hookimpl + def he_method1(self, arg): + return arg * 20 + + class Plugin3(object): + @hookimpl + def he_method1(self, arg): + return arg * 30 + + out = [] + pm.register(Plugin1()) + pm.register(Plugin2()) + + he_method1 = pm.hook.he_method1 + he_method1.call_historic(lambda res: out.append(res), dict(arg=1)) + assert out == [20, 10] + pm.register(Plugin3()) + assert out == [20, 10, 30] + + +def test_register_historic_incompat_hookwrapper(pm): + class Hooks(object): + @hookspec(historic=True) + def he_method1(self, arg): + pass + + pm.add_hookspecs(Hooks) + + out = [] + + class Plugin(object): + @hookimpl(hookwrapper=True) + def he_method1(self, arg): + out.append(arg) + + with pytest.raises(PluginValidationError): + pm.register(Plugin()) + + +def test_call_extra(pm): + class Hooks(object): + @hookspec + def he_method1(self, arg): + pass + + pm.add_hookspecs(Hooks) + + def he_method1(arg): + return arg * 10 + + out = pm.hook.he_method1.call_extra([he_method1], dict(arg=1)) + assert out == [10] + + +def test_call_with_too_few_args(pm): + class Hooks(object): + @hookspec + def he_method1(self, arg): + pass + + pm.add_hookspecs(Hooks) + + class Plugin1(object): + @hookimpl + def he_method1(self, arg): + 0 / 0 + pm.register(Plugin1()) + with pytest.raises(HookCallError): + with pytest.warns(UserWarning): + pm.hook.he_method1() + + +def test_subset_hook_caller(pm): + class Hooks(object): + @hookspec + def he_method1(self, arg): + pass + + pm.add_hookspecs(Hooks) + + out = [] + + class Plugin1(object): + @hookimpl + def he_method1(self, arg): + out.append(arg) + + class Plugin2(object): + @hookimpl + def he_method1(self, arg): + out.append(arg * 10) + + class PluginNo(object): + pass + + plugin1, plugin2, plugin3 = Plugin1(), Plugin2(), PluginNo() + pm.register(plugin1) + pm.register(plugin2) + pm.register(plugin3) + pm.hook.he_method1(arg=1) + assert out == [10, 1] + out[:] = [] + + hc = pm.subset_hook_caller("he_method1", [plugin1]) + hc(arg=2) + assert out == [20] + out[:] = [] + + hc = pm.subset_hook_caller("he_method1", [plugin2]) + hc(arg=2) + assert out == [2] + out[:] = [] + + pm.unregister(plugin1) + hc(arg=2) + assert out == [] + out[:] = [] + + pm.hook.he_method1(arg=1) + assert out == [10] + + +def test_multicall_deprecated(pm): + class P1(object): + @hookimpl + def m(self, __multicall__, x): + pass + + pytest.deprecated_call(pm.register, P1()) + + +def test_add_hookspecs_nohooks(pm): + with pytest.raises(ValueError): + pm.add_hookspecs(10) + + +def test_reject_prefixed_module(pm): + """Verify that a module type attribute that contains the project + prefix in its name (in this case `'example_*'` isn't collected + when registering a module which imports it. + """ + pm._implprefix = 'example' + conftest = types.ModuleType("conftest") + src = (""" +def example_hook(): + pass +""") + exec(src, conftest.__dict__) + conftest.example_blah = types.ModuleType("example_blah") + name = pm.register(conftest) + assert name == 'conftest' + assert getattr(pm.hook, 'example_blah', None) is None + assert getattr(pm.hook, 'example_hook', None) # conftest.example_hook should be collected + assert pm.parse_hookimpl_opts(conftest, 'example_blah') is None + assert pm.parse_hookimpl_opts(conftest, 'example_hook') == {} diff --git a/third_party/python/pluggy/testing/test_tracer.py b/third_party/python/pluggy/testing/test_tracer.py new file mode 100644 index 0000000000..4a3e16cec4 --- /dev/null +++ b/third_party/python/pluggy/testing/test_tracer.py @@ -0,0 +1,89 @@ + +from pluggy import _TagTracer + + +def test_simple(): + rootlogger = _TagTracer() + log = rootlogger.get("pytest") + log("hello") + out = [] + rootlogger.setwriter(out.append) + log("world") + assert len(out) == 1 + assert out[0] == "world [pytest]\n" + sublog = log.get("collection") + sublog("hello") + assert out[1] == "hello [pytest:collection]\n" + + +def test_indent(): + rootlogger = _TagTracer() + log = rootlogger.get("1") + out = [] + log.root.setwriter(lambda arg: out.append(arg)) + log("hello") + log.root.indent += 1 + log("line1") + log("line2") + log.root.indent += 1 + log("line3") + log("line4") + log.root.indent -= 1 + log("line5") + log.root.indent -= 1 + log("last") + assert len(out) == 7 + names = [x[:x.rfind(' [')] for x in out] + assert names == [ + 'hello', ' line1', ' line2', + ' line3', ' line4', ' line5', 'last'] + + +def test_readable_output_dictargs(): + rootlogger = _TagTracer() + + out = rootlogger.format_message(['test'], [1]) + assert out == ['1 [test]\n'] + + out2 = rootlogger.format_message(['test'], ['test', {'a': 1}]) + assert out2 == [ + 'test [test]\n', + ' a: 1\n' + ] + + +def test_setprocessor(): + rootlogger = _TagTracer() + log = rootlogger.get("1") + log2 = log.get("2") + assert log2.tags == tuple("12") + out = [] + rootlogger.setprocessor(tuple("12"), lambda *args: out.append(args)) + log("not seen") + log2("seen") + assert len(out) == 1 + tags, args = out[0] + assert "1" in tags + assert "2" in tags + assert args == ("seen",) + l2 = [] + rootlogger.setprocessor("1:2", lambda *args: l2.append(args)) + log2("seen") + tags, args = l2[0] + assert args == ("seen",) + + +def test_setmyprocessor(): + rootlogger = _TagTracer() + log = rootlogger.get("1") + log2 = log.get("2") + out = [] + log2.setmyprocessor(lambda *args: out.append(args)) + log("not seen") + assert not out + log2(42) + assert len(out) == 1 + tags, args = out[0] + assert "1" in tags + assert "2" in tags + assert args == (42,) diff --git a/third_party/python/pluggy/tox.ini b/third_party/python/pluggy/tox.ini new file mode 100644 index 0000000000..89d44e352d --- /dev/null +++ b/third_party/python/pluggy/tox.ini @@ -0,0 +1,44 @@ +[tox] +envlist=check,docs,py{27,34,35,36,py}-pytestrelease,py{27,36}-pytest{master,features} + +[testenv] +commands=py.test {posargs:testing/} +setenv= + _PYTEST_SETUP_SKIP_PLUGGY_DEP=1 +deps= + pytestrelease: pytest + pytestmaster: git+https://github.com/pytest-dev/pytest.git@master + pytestfeatures: git+https://github.com/pytest-dev/pytest.git@features + +[testenv:benchmark] +commands=py.test {posargs:testing/benchmark.py} +deps= + pytest + pytest-benchmark + +[testenv:check] +deps = + flake8 + restructuredtext_lint + pygments +commands = + flake8 pluggy.py setup.py testing + rst-lint CHANGELOG.rst README.rst + +[testenv:docs] +deps = + sphinx + pygments +commands = + sphinx-build -b html {toxinidir}/docs {toxinidir}/build/html-docs + +[pytest] +minversion=2.0 +#--pyargs --doctest-modules --ignore=.tox +addopts=-rxsX +norecursedirs=.tox ja .hg .env* +filterwarnings = + error + +[flake8] +max-line-length=99 |