diff options
author | holger krekel <holger@merlinux.eu> | 2015-05-08 13:15:14 +0200 |
---|---|---|
committer | holger krekel <holger@merlinux.eu> | 2015-05-08 13:15:14 +0200 |
commit | 6e2e1a62b903a29bfeca35c680789a8e959786fa (patch) | |
tree | 10fbc8da4a1c4bed91cd36adc8ca66806f3050f1 | |
parent | ecdad82f66a6e6678276d7101f7d0457de27ecd7 (diff) | |
download | tox-6e2e1a62b903a29bfeca35c680789a8e959786fa.tar.gz |
introduce little plugin system based on pluggy
and refactor/streamline some code with relation to
getting executables
-rw-r--r-- | doc/conf.py | 4 | ||||
-rw-r--r-- | doc/index.txt | 14 | ||||
-rw-r--r-- | doc/plugins.txt | 69 | ||||
-rw-r--r-- | setup.py | 2 | ||||
-rw-r--r-- | tests/test_config.py | 36 | ||||
-rw-r--r-- | tests/test_interpreters.py | 12 | ||||
-rw-r--r-- | tests/test_venv.py | 2 | ||||
-rw-r--r-- | tox.ini | 4 | ||||
-rw-r--r-- | tox/__init__.py | 2 | ||||
-rw-r--r-- | tox/_cmdline.py | 5 | ||||
-rw-r--r-- | tox/_config.py | 66 | ||||
-rw-r--r-- | tox/_venv.py | 2 | ||||
-rw-r--r-- | tox/interpreters.py | 55 |
13 files changed, 186 insertions, 87 deletions
diff --git a/doc/conf.py b/doc/conf.py index 9c573d7..73f5168 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -48,8 +48,8 @@ copyright = u'2013, holger krekel and others' # built documents. # # The short X.Y version. -release = "1.9" -version = "1.9.0" +release = "2.0" +version = "2.0.0" # The full version, including alpha/beta/rc tags. # The language for content autogenerated by Sphinx. Refer to documentation diff --git a/doc/index.txt b/doc/index.txt index 17d063d..ed594cd 100644 --- a/doc/index.txt +++ b/doc/index.txt @@ -5,7 +5,7 @@ vision: standardize testing in Python --------------------------------------------- ``tox`` aims to automate and standardize testing in Python. It is part -of a larger vision of easing the packaging, testing and release process +of a larger vision of easing the packaging, testing and release process of Python software. What is Tox? @@ -21,6 +21,7 @@ Tox is a generic virtualenv_ management and test command line tool you can use f * acting as a frontend to Continuous Integration servers, greatly reducing boilerplate and merging CI and shell-based testing. + Basic example ----------------- @@ -62,10 +63,10 @@ Current features - test-tool agnostic: runs py.test, nose or unittests in a uniform manner -* supports :ref:`using different / multiple PyPI index servers <multiindex>` +* :doc:`(new in 2.0) plugin system <plugins>` to modify tox execution with simple hooks. * uses pip_ and setuptools_ by default. Experimental - support for configuring the installer command + support for configuring the installer command through :confval:`install_command=ARGV`. * **cross-Python compatible**: CPython-2.6, 2.7, 3.2 and higher, @@ -74,11 +75,11 @@ Current features * **cross-platform**: Windows and Unix style environments * **integrates with continuous integration servers** like Jenkins_ - (formerly known as Hudson) and helps you to avoid boilerplatish + (formerly known as Hudson) and helps you to avoid boilerplatish and platform-specific build-step hacks. * **full interoperability with devpi**: is integrated with and - is used for testing in the devpi_ system, a versatile pypi + is used for testing in the devpi_ system, a versatile pypi index server and release managing tool. * **driven by a simple ini-style config file** @@ -89,6 +90,9 @@ Current features * **professionally** :doc:`supported <support>` +* supports :ref:`using different / multiple PyPI index servers <multiindex>` + + .. _pypy: http://pypy.org .. _`tox.ini`: :doc:configfile diff --git a/doc/plugins.txt b/doc/plugins.txt new file mode 100644 index 0000000..61e4408 --- /dev/null +++ b/doc/plugins.txt @@ -0,0 +1,69 @@ +.. be in -*- rst -*- mode! + +tox plugins +=========== + +.. versionadded:: 2.0 + +With tox-2.0 a few aspects of tox running can be experimentally modified +by writing hook functions. We expect the list of hook function to grow +over time. + +writing a setuptools entrypoints plugin +--------------------------------------- + +If you have a ``tox_MYPLUGIN.py`` module you could use the following +rough ``setup.py`` to make it into a package which you can upload to the +Python packaging index:: + + # content of setup.py + from setuptools import setup + + if __name__ == "__main__": + setup( + name='tox-MYPLUGIN', + description='tox plugin decsription', + license="MIT license", + version='0.1', + py_modules=['tox_MYPLUGIN'], + entry_points={'tox': ['MYPLUGIN = tox_MYPLUGIN']}, + install_requires=['tox>=2.0'], + ) + +You can then install the plugin to develop it via:: + + pip install -e . + +and later publish it. + +The ``entry_points`` part allows tox to see your plugin during startup. + + +Writing hook implementations +---------------------------- + +A plugin module needs can define one or more hook implementation functions:: + + from tox import hookimpl + + @hookimpl + def tox_addoption(parser): + # add your own command line options + + + @hookimpl + def tox_configure(config): + # post process tox configuration after cmdline/ini file have + # been parsed + +If you put this into a module and make it pypi-installable with the ``tox`` +entry point you'll get your code executed as part of a tox run. + + + +tox hook specifications +---------------------------- + +.. automodule:: tox.hookspecs + :members: + @@ -18,7 +18,7 @@ class Tox(TestCommand): def main(): version = sys.version_info[:2] - install_requires = ['virtualenv>=1.11.2', 'py>=1.4.17', ] + install_requires = ['virtualenv>=1.11.2', 'py>=1.4.17', 'pluggy>=0.3.0,<0.4.0'] if version < (2, 7): install_requires += ['argparse'] setup( diff --git a/tests/test_config.py b/tests/test_config.py index 79c98bd..522a22f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -6,7 +6,6 @@ import pytest import tox import tox._config from tox._config import * # noqa -from tox._config import _split_env from tox._venv import VirtualEnv @@ -1561,31 +1560,16 @@ class TestCmdInvocation: ]) -class TestArgumentParser: - - def test_dash_e_single_1(self): - parser = prepare_parse('testpkg') - args = parser.parse_args('-e py26'.split()) - envlist = _split_env(args.env) - assert envlist == ['py26'] - - def test_dash_e_single_2(self): - parser = prepare_parse('testpkg') - args = parser.parse_args('-e py26,py33'.split()) - envlist = _split_env(args.env) - assert envlist == ['py26', 'py33'] - - def test_dash_e_same(self): - parser = prepare_parse('testpkg') - args = parser.parse_args('-e py26,py26'.split()) - envlist = _split_env(args.env) - assert envlist == ['py26', 'py26'] - - def test_dash_e_combine(self): - parser = prepare_parse('testpkg') - args = parser.parse_args('-e py26,py25,py33 -e py33,py27'.split()) - envlist = _split_env(args.env) - assert envlist == ['py26', 'py25', 'py33', 'py33', 'py27'] +@pytest.mark.parametrize("cmdline,envlist", [ + ("-e py26", ['py26']), + ("-e py26,py33", ['py26', 'py33']), + ("-e py26,py26", ['py26', 'py26']), + ("-e py26,py33 -e py33,py27", ['py26', 'py33', 'py33', 'py27']) +]) +def test_env_spec(cmdline, envlist): + args = cmdline.split() + config = parseconfig(args) + assert config.envlist == envlist class TestCommandParser: diff --git a/tests/test_interpreters.py b/tests/test_interpreters.py index 1c5a77d..a6997e6 100644 --- a/tests/test_interpreters.py +++ b/tests/test_interpreters.py @@ -3,11 +3,13 @@ import os import pytest from tox.interpreters import * # noqa +from tox._config import get_plugin_manager @pytest.fixture def interpreters(): - return Interpreters() + pm = get_plugin_manager() + return Interpreters(hook=pm.hook) @pytest.mark.skipif("sys.platform != 'win32'") @@ -28,8 +30,8 @@ def test_locate_via_py(monkeypatch): assert locate_via_py('3', '2') == sys.executable -def test_find_executable(): - p = find_executable(sys.executable) +def test_tox_get_python_executable(): + p = tox_get_python_executable(sys.executable) assert p == py.path.local(sys.executable) for ver in [""] + "2.4 2.5 2.6 2.7 3.0 3.1 3.2 3.3".split(): name = "python%s" % ver @@ -42,7 +44,7 @@ def test_find_executable(): else: if not py.path.local.sysfind(name): continue - p = find_executable(name) + p = tox_get_python_executable(name) assert p popen = py.std.subprocess.Popen([str(p), '-V'], stderr=py.std.subprocess.PIPE) @@ -55,7 +57,7 @@ def test_find_executable_extra(monkeypatch): def sysfind(x): return "hello" monkeypatch.setattr(py.path.local, "sysfind", sysfind) - t = find_executable("qweqwe") + t = tox_get_python_executable("qweqwe") assert t == "hello" diff --git a/tests/test_venv.py b/tests/test_venv.py index 538b7e5..80ec519 100644 --- a/tests/test_venv.py +++ b/tests/test_venv.py @@ -65,7 +65,7 @@ def test_create(monkeypatch, mocksession, newconfig): # assert Envconfig.toxworkdir in args assert venv.getcommandpath("easy_install", cwd=py.path.local()) interp = venv._getliveconfig().python - assert interp == venv.envconfig._basepython_info.executable + assert interp == venv.envconfig.python_info.executable assert venv.path_config.check(exists=False) @@ -22,7 +22,9 @@ commands= deps = pytest-flakes>=0.2 pytest-pep8 -commands = py.test -x --flakes --pep8 tox tests +commands = + py.test --flakes -m flakes tox tests + py.test --pep8 -m pep8 tox tests [testenv:dev] # required to make looponfail reload on every source code change diff --git a/tox/__init__.py b/tox/__init__.py index 7869fcb..5f3f3ad 100644 --- a/tox/__init__.py +++ b/tox/__init__.py @@ -1,6 +1,8 @@ # __version__ = '2.0.0.dev1' +from .hookspecs import hookspec, hookimpl # noqa + class exception: class Error(Exception): diff --git a/tox/_cmdline.py b/tox/_cmdline.py index 0236d17..3dd9582 100644 --- a/tox/_cmdline.py +++ b/tox/_cmdline.py @@ -24,7 +24,7 @@ def now(): def main(args=None): try: - config = parseconfig(args, 'tox') + config = parseconfig(args) retcode = Session(config).runcommand() raise SystemExit(retcode) except KeyboardInterrupt: @@ -551,8 +551,7 @@ class Session: for envconfig in self.config.envconfigs.values(): self.report.line("[testenv:%s]" % envconfig.envname, bold=True) self.report.line(" basepython=%s" % envconfig.basepython) - self.report.line(" _basepython_info=%s" % - envconfig._basepython_info) + self.report.line(" pythoninfo=%s" % (envconfig.python_info,)) self.report.line(" envpython=%s" % envconfig.envpython) self.report.line(" envtmpdir=%s" % envconfig.envtmpdir) self.report.line(" envbindir=%s" % envconfig.envbindir) diff --git a/tox/_config.py b/tox/_config.py index 4468d86..d5e8a31 100644 --- a/tox/_config.py +++ b/tox/_config.py @@ -8,8 +8,10 @@ import shlex import string import pkg_resources import itertools +import pluggy -from tox.interpreters import Interpreters +import tox.interpreters +from tox import hookspecs import py @@ -22,20 +24,43 @@ default_factors = {'jython': 'jython', 'pypy': 'pypy', 'pypy3': 'pypy3', for version in '24,25,26,27,30,31,32,33,34,35'.split(','): default_factors['py' + version] = 'python%s.%s' % tuple(version) +hookimpl = pluggy.HookimplMarker("tox") -def parseconfig(args=None, pkg=None): + +def get_plugin_manager(): + # initialize plugin manager + pm = pluggy.PluginManager("tox") + pm.add_hookspecs(hookspecs) + pm.register(tox._config) + pm.register(tox.interpreters) + pm.load_setuptools_entrypoints("tox") + pm.check_pending() + return pm + + +def parseconfig(args=None): """ :param list[str] args: Optional list of arguments. :type pkg: str :rtype: :class:`Config` :raise SystemExit: toxinit file is not found """ + + pm = get_plugin_manager() + if args is None: args = sys.argv[1:] - parser = prepare_parse(pkg) - opts = parser.parse_args(args) - config = Config() - config.option = opts + + # prepare command line options + parser = argparse.ArgumentParser(description=__doc__) + pm.hook.tox_addoption(parser=parser) + + # parse command line options + option = parser.parse_args(args) + interpreters = tox.interpreters.Interpreters(hook=pm.hook) + config = Config(pluginmanager=pm, option=option, interpreters=interpreters) + + # parse ini file basename = config.option.configfile if os.path.isabs(basename): inipath = py.path.local(basename) @@ -52,6 +77,10 @@ def parseconfig(args=None, pkg=None): exn = sys.exc_info()[1] # Use stdout to match test expectations py.builtin.print_("ERROR: " + str(exn)) + + # post process config object + pm.hook.tox_configure(config=config) + return config @@ -63,10 +92,8 @@ def feedback(msg, sysexit=False): class VersionAction(argparse.Action): def __call__(self, argparser, *args, **kwargs): - name = argparser.pkgname - mod = __import__(name) - version = mod.__version__ - py.builtin.print_("%s imported from %s" % (version, mod.__file__)) + version = tox.__version__ + py.builtin.print_("%s imported from %s" % (version, tox.__file__)) raise SystemExit(0) @@ -78,10 +105,9 @@ class CountAction(argparse.Action): setattr(namespace, self.dest, 0) -def prepare_parse(pkgname): - parser = argparse.ArgumentParser(description=__doc__,) +@hookimpl +def tox_addoption(parser): # formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.pkgname = pkgname parser.add_argument("--version", nargs=0, action=VersionAction, dest="version", help="report version information to stdout.") @@ -153,10 +179,12 @@ def prepare_parse(pkgname): class Config(object): - def __init__(self): + def __init__(self, pluginmanager, option, interpreters): self.envconfigs = {} self.invocationcwd = py.path.local() - self.interpreters = Interpreters() + self.interpreters = interpreters + self.pluginmanager = pluginmanager + self.option = option @property def homedir(self): @@ -192,10 +220,14 @@ class VenvConfig: def envsitepackagesdir(self): self.getsupportedinterpreter() # for throwing exceptions x = self.config.interpreters.get_sitepackagesdir( - info=self._basepython_info, + info=self.python_info, envdir=self.envdir) return x + @property + def python_info(self): + return self.config.interpreters.get_info(self.basepython) + def getsupportedinterpreter(self): if sys.platform == "win32" and self.basepython and \ "jython" in self.basepython: @@ -356,7 +388,7 @@ class parseini: bp = next((default_factors[f] for f in factors if f in default_factors), sys.executable) vc.basepython = reader.getdefault(section, "basepython", bp) - vc._basepython_info = config.interpreters.get_info(vc.basepython) + reader.addsubstitutions(envdir=vc.envdir, envname=vc.envname, envbindir=vc.envbindir, envpython=vc.envpython, envsitepackagesdir=vc.envsitepackagesdir) diff --git a/tox/_venv.py b/tox/_venv.py index 9114e69..58587e4 100644 --- a/tox/_venv.py +++ b/tox/_venv.py @@ -143,7 +143,7 @@ class VirtualEnv(object): self.envconfig.deps, v) def _getliveconfig(self): - python = self.envconfig._basepython_info.executable + python = self.envconfig.python_info.executable md5 = getdigest(python) version = tox.__version__ sitepackages = self.envconfig.sitepackages diff --git a/tox/interpreters.py b/tox/interpreters.py index 76075b8..98a5c40 100644 --- a/tox/interpreters.py +++ b/tox/interpreters.py @@ -2,12 +2,14 @@ import sys import py import re import inspect +from tox import hookimpl class Interpreters: - def __init__(self): + def __init__(self, hook): self.name2executable = {} self.executable2info = {} + self.hook = hook def get_executable(self, name): """ return path object to the executable for the given @@ -18,8 +20,9 @@ class Interpreters: try: return self.name2executable[name] except KeyError: - self.name2executable[name] = e = find_executable(name) - return e + exe = self.hook.tox_get_python_executable(name=name) + self.name2executable[name] = exe + return exe def get_info(self, name=None, executable=None): if name is None and executable is None: @@ -125,31 +128,13 @@ class NoInterpreterInfo: return "<executable not found for: %s>" % self.name if sys.platform != "win32": - def find_executable(name): + @hookimpl + def tox_get_python_executable(name): return py.path.local.sysfind(name) else: - # Exceptions to the usual windows mapping - win32map = { - 'python': sys.executable, - 'jython': "c:\jython2.5.1\jython.bat", - } - - def locate_via_py(v_maj, v_min): - ver = "-%s.%s" % (v_maj, v_min) - script = "import sys; print(sys.executable)" - py_exe = py.path.local.sysfind('py') - if py_exe: - try: - exe = py_exe.sysexec(ver, '-c', script).strip() - except py.process.cmdexec.Error: - exe = None - if exe: - exe = py.path.local(exe) - if exe.check(): - return exe - - def find_executable(name): + @hookimpl + def tox_get_python_executable(name): p = py.path.local.sysfind(name) if p: return p @@ -170,6 +155,26 @@ else: if m: return locate_via_py(*m.groups()) + # Exceptions to the usual windows mapping + win32map = { + 'python': sys.executable, + 'jython': "c:\jython2.5.1\jython.bat", + } + + def locate_via_py(v_maj, v_min): + ver = "-%s.%s" % (v_maj, v_min) + script = "import sys; print(sys.executable)" + py_exe = py.path.local.sysfind('py') + if py_exe: + try: + exe = py_exe.sysexec(ver, '-c', script).strip() + except py.process.cmdexec.Error: + exe = None + if exe: + exe = py.path.local(exe) + if exe.check(): + return exe + def pyinfo(): import sys |