From ed03eef81b220199a819632a27f9c452b8e1fb81 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 23 Nov 2009 17:20:36 +0100 Subject: [PATCH] introduce plugin discovery through setuptools "pytest11" entrypoints and refine execnet dependency handling. Prepare 1.1 release --HG-- branch : trunk --- CHANGELOG | 5 ++- bin-for-dist/test_install.py | 33 +++++++++++++- doc/announce/release-1.1.1.txt | 15 ++++--- doc/test/customize.txt | 65 ++++++++++++++++++++++++---- py/impl/test/config.py | 1 + py/impl/test/pluginmanager.py | 8 ++++ testing/pytest/test_pluginmanager.py | 18 ++++++++ 7 files changed, 128 insertions(+), 17 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 96f99a57e..2141313bc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,10 @@ Changes between 1.1.1 and 1.1.0 ===================================== -- fix py.test dist-testing to work with execnet >= 1.0.0b4 (required) +- introduce automatic lookup of 'pytest11' entrypoints + via setuptools' pkg_resources.iter_entry_points + +- fix py.test dist-testing to work with execnet >= 1.0.0b4 - re-introduce py.test.cmdline.main() for better backward compatibility diff --git a/bin-for-dist/test_install.py b/bin-for-dist/test_install.py index 52d30c379..e7ed972d8 100644 --- a/bin-for-dist/test_install.py +++ b/bin-for-dist/test_install.py @@ -88,6 +88,16 @@ class VirtualEnv(object): ] + list(args), **kw) + def pytest_getouterr(self, *args): + self.ensure() + args = [self._cmd("python"), self._cmd("py.test")] + list(args) + popen = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + out, err = popen.communicate() + return out + + def setup_develop(self): + self.ensure() + return self.pcall("python", "setup.py", "develop") def easy_install(self, *packages, **kw): args = [] @@ -110,4 +120,25 @@ def test_make_sdist_and_run_it(py_setup, venv): ch = gw.remote_exec("import py ; channel.send(py.__version__)") version = ch.receive() assert version == py.__version__ - ch = gw.remote_exec("import py ; channel.send(py.__version__)") + +def test_plugin_setuptools_entry_point_integration(py_setup, venv, tmpdir): + sdist = py_setup.make_sdist(venv.path) + venv.easy_install(str(sdist)) + # create a sample plugin + basedir = tmpdir.mkdir("testplugin") + basedir.join("setup.py").write("""if 1: + from setuptools import setup + setup(name="testplugin", + entry_points = {'pytest11': ['testplugin=tp1']}, + py_modules = ['tp1'], + ) + """) + basedir.join("tp1.py").write(py.code.Source(""" + def pytest_addoption(parser): + parser.addoption("--testpluginopt", action="store_true") + """)) + basedir.chdir() + print ("created sample plugin in %s" %basedir) + venv.setup_develop() + out = venv.pytest_getouterr("-h") + assert "testpluginopt" in out diff --git a/doc/announce/release-1.1.1.txt b/doc/announce/release-1.1.1.txt index 78fd8d9a7..b9f3b089d 100644 --- a/doc/announce/release-1.1.1.txt +++ b/doc/announce/release-1.1.1.txt @@ -1,10 +1,10 @@ -py.test/pylib 1.1.1: bugfix release, improved 1.0.x backward compat +py.test/pylib 1.1.1: bugfix release, setuptools plugin registration -------------------------------------------------------------------------------- This is a compatibility fixing release of pylib/py.test to work -better with previous 1.0.x code bases. It also contains fixes -and changes to work with `execnet>=1.0.0b4`_ which is now required -(but is not installed automatically, issue "easy_install -U execnet"). +better with previous 1.0.x test code bases. It also contains fixes +and changes to work with `execnet>=1.0.0b4`_. 1.1.1 also introduces +a new mechanism for registering plugins via setuptools. Last but not least, documentation has been improved. What is pylib/py.test? @@ -17,7 +17,7 @@ existing common Python test suites without modification. Moreover, it offers some unique features not found in other testing tools. See http://pytest.org for more info. -The pylib contains a localpath and svnpath implementation +The pylib also contains a localpath and svnpath implementation and some developer-oriented command line tools. See http://pylib.org for more info. @@ -31,7 +31,10 @@ holger (http://twitter.com/hpk42) Changes between 1.1.1 and 1.1.0 ===================================== -- fix py.test dist-testing to work with execnet >= 1.0.0b4 (required) +- introduce automatic lookup of 'pytest11' entrypoints + via setuptools' pkg_resources.iter_entry_points + +- fix py.test dist-testing to work with execnet >= 1.0.0b4 - re-introduce py.test.cmdline.main() for better backward compatibility diff --git a/doc/test/customize.txt b/doc/test/customize.txt index 707811089..47908599b 100644 --- a/doc/test/customize.txt +++ b/doc/test/customize.txt @@ -125,6 +125,8 @@ Plugin discovery at tool startup py.test loads plugin modules at tool startup in the following way: +* by loading all plugins registered through `setuptools entry points`_. + * by reading the ``PYTEST_PLUGINS`` environment variable and importing the comma-separated list of named plugins. @@ -132,17 +134,13 @@ py.test loads plugin modules at tool startup in the following way: and loading the specified plugin before actual command line parsing. * by loading all `conftest.py plugin`_ files as inferred by the command line - invocation + invocation (test files and all of its parent directories). + Note that ``conftest.py`` files from sub directories are loaded + during test collection and not at tool startup. * by recursively loading all plugins specified by the ``pytest_plugins`` variable in a ``conftest.py`` file -Note that at tool startup only ``conftest.py`` files in -the directory of the specified test modules (or the current dir if None) -or any of the parent directories are found. There is no try to -pre-scan all subdirectories to find ``conftest.py`` files or test -modules. - Specifying plugins in a test module or plugin ----------------------------------------------- @@ -160,8 +158,8 @@ must be lowercase. .. _`conftest.py plugin`: .. _`conftestplugin`: -conftest.py as anonymous per-project plugins --------------------------------------------------- +Writing per-project plugins (conftest.py) +------------------------------------------------------ The purpose of ``conftest.py`` files is to allow `project-specific test configuration`_. They thus make for a good place to implement @@ -181,6 +179,55 @@ by defining the following hook in a ``conftest.py`` file: if config.getvalue("runall"): collect_ignore[:] = [] +.. _`setuptools entry points`: + +Writing setuptools-registered plugins +------------------------------------------------------ + +.. _`Distribute`: http://pypi.python.org/pypi/distribute +.. _`setuptools`: http://pypi.python.org/pypi/setuptools + +If you want to make your plugin publically available, you +can use `setuptools`_ or `Distribute`_ which both allow +to register an entry point. ``py.test`` will register +all objects with the ``pytest11`` entry point. +To make your plugin available you may insert the following +lines in your setuptools/distribute-based setup-invocation: + +.. sourcecode:: python + + # sample ./setup.py file + from setuptools import setup + + setup( + name="myproject", + packages = ['myproject'] + + # the following makes a plugin available to py.test + entry_points = { + 'pytest11': [ + 'name_of_plugin = myproject.pluginmodule', + ] + }, + ) + +If a package is installed with this setup, py.test will load +``myproject.pluginmodule`` under the ``name_of_plugin`` name +and use it as a plugin. + +Accessing another plugin by name +-------------------------------------------- + +If a plugin wants to collaborate with code from +another plugin it can obtain a reference through +the plugin manager like this: + +.. sourcecode:: python + + plugin = config.pluginmanager.getplugin("name_of_plugin") + +If you want to look at the names of existing plugins, use +the ``--traceconfig`` option. .. _`well specified hooks`: .. _`implement hooks`: diff --git a/py/impl/test/config.py b/py/impl/test/config.py index b3b49ea29..a74384ba6 100644 --- a/py/impl/test/config.py +++ b/py/impl/test/config.py @@ -74,6 +74,7 @@ class Config(object): def _preparse(self, args): self._conftest.setinitial(args) + self.pluginmanager.consider_setuptools_entrypoints() self.pluginmanager.consider_preparse(args) self.pluginmanager.consider_env() self.pluginmanager.do_addoption(self._parser) diff --git a/py/impl/test/pluginmanager.py b/py/impl/test/pluginmanager.py index 75e3fb344..85060bef7 100644 --- a/py/impl/test/pluginmanager.py +++ b/py/impl/test/pluginmanager.py @@ -77,6 +77,14 @@ class PluginManager(object): for spec in self._envlist("PYTEST_PLUGINS"): self.import_plugin(spec) + def consider_setuptools_entrypoints(self): + from pkg_resources import iter_entry_points + for ep in iter_entry_points('pytest11'): + if ep.name in self._name2plugin: + continue + plugin = ep.load() + self.register(plugin, name=ep.name) + def consider_preparse(self, args): for opt1,opt2 in zip(args, args[1:]): if opt1 == "-p": diff --git a/testing/pytest/test_pluginmanager.py b/testing/pytest/test_pluginmanager.py index cd9d330f0..9050d8451 100644 --- a/testing/pytest/test_pluginmanager.py +++ b/testing/pytest/test_pluginmanager.py @@ -42,6 +42,24 @@ class TestBootstrapping: l3 = len(pluginmanager.getplugins()) assert l2 == l3 + def test_consider_setuptools_instantiation(self, monkeypatch): + pkg_resources = py.test.importorskip("pkg_resources") + def my_iter(name): + assert name == "pytest11" + class EntryPoint: + name = "mytestplugin" + def load(self): + class PseudoPlugin: + x = 42 + return PseudoPlugin() + return iter([EntryPoint()]) + + monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) + pluginmanager = PluginManager() + pluginmanager.consider_setuptools_entrypoints() + plugin = pluginmanager.getplugin("mytestplugin") + assert plugin.x == 42 + def test_pluginmanager_ENV_startup(self, testdir, monkeypatch): x500 = testdir.makepyfile(pytest_x500="#") p = testdir.makepyfile("""