From d10823509537b011f1dc59413f547a8cc061a513 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 5 Nov 2010 23:37:31 +0100 Subject: [PATCH] implement and document new invocation mechanisms, see doc/usage.txt also rename pytest._core to pytest.main for convenience. --- CHANGELOG | 3 + doc/apiref.txt | 1 + doc/builtin.txt | 28 ++-- doc/customize.txt | 6 +- doc/features.txt | 6 +- doc/goodpractises.txt | 17 ++- doc/index.txt | 9 +- doc/overview.txt | 2 +- doc/test/plugin/cov.txt | 2 +- doc/{cmdline.txt => usage.txt} | 62 ++++++++- pytest/__init__.py | 6 +- pytest/__main__.py | 4 + pytest/hookspec.py | 2 +- pytest/{_core.py => main.py} | 29 +++- pytest/plugin/config.py | 2 +- pytest/plugin/pytester.py | 8 +- pytest/plugin/python.py | 2 +- setup.py | 4 +- testing/acceptance_test.py | 125 ++++++++++++------ testing/plugin/conftest.py | 2 +- testing/plugin/test_pytester.py | 4 +- .../{test_pluginmanager.py => test_main.py} | 10 +- 22 files changed, 228 insertions(+), 106 deletions(-) rename doc/{cmdline.txt => usage.txt} (71%) create mode 100644 pytest/__main__.py rename pytest/{_core.py => main.py} (94%) rename testing/{test_pluginmanager.py => test_main.py} (98%) diff --git a/CHANGELOG b/CHANGELOG index bbc78490819..71870b4844e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,9 @@ Changes between 1.3.4 and 2.0.0dev0 ---------------------------------------------- - pytest-2.0 is now its own package and depends on pylib-2.0 +- new ability: python -m pytest / python -m pytest.main ability +- new python invcation: pytest.main(args, plugins) to load + some custom plugins early. - try harder to run unittest test suites in a more compatible manner by deferring setup/teardown semantics to the unittest package. - introduce a new way to set config options via ini-style files, diff --git a/doc/apiref.txt b/doc/apiref.txt index b9623e67d54..52af57de454 100644 --- a/doc/apiref.txt +++ b/doc/apiref.txt @@ -4,6 +4,7 @@ py.test reference documentation ================================================ + .. toctree:: :maxdepth: 2 diff --git a/doc/builtin.txt b/doc/builtin.txt index 26764ec02d7..017cc8cc884 100644 --- a/doc/builtin.txt +++ b/doc/builtin.txt @@ -1,7 +1,19 @@ -pytest builtin helpers +py.test builtin helpers ================================================ +builtin py.test.* helpers +----------------------------------------------------- + +You can always use an interactive Python prompt and type:: + + import pytest + help(pytest) + +to get an overview on available globally available helpers. + +.. automodule:: pytest + :members: builtin function arguments ----------------------------------------------------- @@ -54,17 +66,3 @@ You can ask for available builtin or project-custom * ``pop(category=None)``: return last warning matching the category. * ``clear()``: clear list of warnings - -builtin py.test.* helpers ------------------------------------------------------ - -You can always use an interactive Python prompt and type:: - - import pytest - help(pytest) - -to get an overview on available globally available helpers. - -.. automodule:: pytest - :members: - diff --git a/doc/customize.txt b/doc/customize.txt index ef51662ce5b..121377e9212 100644 --- a/doc/customize.txt +++ b/doc/customize.txt @@ -58,8 +58,8 @@ builtin configuration file options .. confval:: norecursedirs Set the directory basename patterns to avoid when recursing - for test discovery. The individual (fnmatch-style) patterns are - applied to the basename of a directory to decide if to recurse into it. + for test discovery. The individual (fnmatch-style) patterns are + applied to the basename of a directory to decide if to recurse into it. Pattern matching characters:: * matches everything @@ -68,7 +68,7 @@ builtin configuration file options [!seq] matches any char not in seq Default patterns are ``.* _* CVS {args}``. Setting a ``norecurse`` - replaces the default. Here is a customizing example for avoiding + replaces the default. Here is a customizing example for avoiding a different set of directories:: # content of setup.cfg diff --git a/doc/features.txt b/doc/features.txt index e8f4b6b20e3..b3cb48e1bb4 100644 --- a/doc/features.txt +++ b/doc/features.txt @@ -5,8 +5,8 @@ no-boilerplate testing with Python ---------------------------------- - automatic, fully customizable Python test discovery -- :pep:`8` consistent testing style -- allows simple test functions +- allows fully :pep:`8` compliant coding style +- write simple test functions and freely group tests - ``assert`` statement for your assertions - powerful parametrization of test functions - rely on powerful traceback and assertion reporting @@ -25,8 +25,8 @@ extensive plugin and customization system mature command line testing tool -------------------------------------- +- powerful :ref:`usage` possibilities - used in many projects, ranging from 10 to 10K tests -- autodiscovery of tests - simple well sorted command line options - runs on Unix, Windows from Python 2.4 up to Python 3.1 and 3.2 - is itself tested extensively on a CI server diff --git a/doc/goodpractises.txt b/doc/goodpractises.txt index 44b72a9bd67..1f82c1b0bb1 100644 --- a/doc/goodpractises.txt +++ b/doc/goodpractises.txt @@ -76,16 +76,21 @@ You can always run your tests by pointing to it:: ... .. _`package name`: - -.. note:: + +.. note:: Test modules are imported under their fully qualified name as follows: - * ``basedir`` = first upward directory not containing an ``__init__.py`` + * find ``basedir`` -- this is the first "upward" directory not + containing an ``__init__.py`` - * perform ``sys.path.insert(0, basedir)``. + * perform ``sys.path.insert(0, basedir)`` to make the fully + qualified test module path importable. - * ``import path.to.test_module`` + * ``import path.to.test_module`` where the path is determined + by converting path separators into "." files. This means + you must follow the convention of having directory and file + names map to the import names. .. _standalone: .. _`genscript method`: @@ -94,7 +99,7 @@ Generating a py.test standalone Script ------------------------------------------- If you are a maintainer or application developer and want others -to easily run tests you can generate a completely standalone "py.test" +to easily run tests you can generate a completely standalone "py.test" script:: py.test --genscript=runtests.py diff --git a/doc/index.txt b/doc/index.txt index 4edf17006ad..d50d4c7a83c 100644 --- a/doc/index.txt +++ b/doc/index.txt @@ -3,10 +3,14 @@ py.test: no-boilerplate testing with Python .. todolist:: - +.. note:: + version 2.0 introduces ``pytest`` as the main Python import name + but for historic reasons ``py.test`` remains fully valid and + represents the same package. + Welcome to ``py.test`` documentation: -.. toctree:: +.. toctree:: :maxdepth: 2 overview @@ -27,4 +31,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/doc/overview.txt b/doc/overview.txt index ecb50cc94ba..11a7f28d3c9 100644 --- a/doc/overview.txt +++ b/doc/overview.txt @@ -7,7 +7,7 @@ Overview and Introduction features.txt getting-started.txt - cmdline.txt + usage.txt goodpractises.txt faq.txt diff --git a/doc/test/plugin/cov.txt b/doc/test/plugin/cov.txt index 3a2548b600c..e24a2e15a28 100644 --- a/doc/test/plugin/cov.txt +++ b/doc/test/plugin/cov.txt @@ -35,7 +35,7 @@ However easy_install does not provide an uninstall facility. .. IMPORTANT:: - Ensure that you manually delete the init_cov_core.pth file in your site-packages directory. + Ensure that you manually delete the init_covmain.pth file in your site-packages directory. This file starts coverage collection of subprocesses if appropriate during site initialisation at python startup. diff --git a/doc/cmdline.txt b/doc/usage.txt similarity index 71% rename from doc/cmdline.txt rename to doc/usage.txt index 9a71b70d026..38a0f3687ff 100644 --- a/doc/cmdline.txt +++ b/doc/usage.txt @@ -1,8 +1,11 @@ -.. _cmdline: +.. _usage: + +Usage and Invocations +========================================== -Using the interactive command line -=============================================== + +.. _cmdline: Getting help on version, option names, environment vars ----------------------------------------------------------- @@ -22,6 +25,57 @@ To stop the testing process after the first (N) failures:: py.test -x # stop after first failure py.test -maxfail=2 # stop after two failures +calling pytest from Python code +---------------------------------------------------- + +.. versionadded: 2.0 + +You can invoke ``py.test`` from Python code directly:: + + pytest.main() + +this acts as if you would call "py.test" from the command line. +It will not raise ``SystemExit`` but return the exitcode instead. +You can pass in options and arguments:: + + pytest.main(['x', 'mytestdir']) + +or pass in a string:: + + pytest.main("-x mytestdir") + +You can specify additional plugins to ``pytest.main``:: + + # content of myinvoke.py + import pytest + class MyPlugin: + def pytest_addoption(self, parser): + raise pytest.UsageError("hi from our plugin") + + pytest.main(plugins=[MyPlugin()]) + +Running it will exit quickly:: + + $ python myinvoke.py + ERROR: hi from our plugin + +calling pytest through ``python -m pytest`` +----------------------------------------------------- + +.. versionadded: 2.0 + +You can invoke testing through the Python interpreter from the command line:: + + python -m pytest.main [...] + +Python2.7 and Python3 introduced specifying packages to "-m" so there +you can also type:: + + python -m pytest [...] + +All of these invocations are equivalent to the ``py.test [...]`` command line invocation. + + Modifying Python traceback printing ---------------------------------------------- @@ -40,7 +94,7 @@ Dropping to PDB (Python Debugger) on failures .. _PDB: http://docs.python.org/library/pdb.html -Python comes with a builtin Python debugger called PDB_. ``py.test`` +Python comes with a builtin Python debugger called PDB_. ``py.test`` allows to drop into the PDB prompt via a command line option:: py.test --pdb diff --git a/pytest/__init__.py b/pytest/__init__.py index 546dce32bac..bef0c9c2422 100644 --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -9,8 +9,6 @@ __all__ = ['config', 'cmdline'] -from pytest import _core as cmdline +from pytest import main as cmdline UsageError = cmdline.UsageError - -def __main__(): - raise SystemExit(cmdline.main()) +main = cmdline.main diff --git a/pytest/__main__.py b/pytest/__main__.py new file mode 100644 index 00000000000..6dc62cd262f --- /dev/null +++ b/pytest/__main__.py @@ -0,0 +1,4 @@ +import pytest + +if __name__ == '__main__': + raise SystemExit(pytest.main()) diff --git a/pytest/hookspec.py b/pytest/hookspec.py index f27728baa09..a5213a93770 100644 --- a/pytest/hookspec.py +++ b/pytest/hookspec.py @@ -1,4 +1,4 @@ -""" hook specifications for pytest plugins, invoked from _core.py and builtin plugins. """ +""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ # ------------------------------------------------------------------------- # Initialization diff --git a/pytest/_core.py b/pytest/main.py similarity index 94% rename from pytest/_core.py rename to pytest/main.py index 79c421ec645..2b08a240f71 100644 --- a/pytest/_core.py +++ b/pytest/main.py @@ -1,7 +1,12 @@ +""" +pytest PluginManager, basic initialization and tracing. +All else is in pytest/plugin. +(c) Holger Krekel 2004-2010 +""" import sys, os import inspect import py -from pytest import hookspec +from pytest import hookspec # the extension point definitions assert py.__version__.split(".")[:2] >= ['2', '0'], ("installation problem: " "%s is too old, remove or upgrade 'py'" % (py.__version__)) @@ -375,23 +380,33 @@ def pcall(self, plugins, **kwargs): mc = MultiCall(methods, kwargs, firstresult=self.firstresult) return mc.execute() -pluginmanager = PluginManager(load=True) # will trigger default plugin importing +_preinit = [PluginManager(load=True)] # triggers default plugin importing -def main(args=None): - global pluginmanager +def main(args=None, plugins=None): if args is None: args = sys.argv[1:] - hook = pluginmanager.hook + elif not isinstance(args, (tuple, list)): + args = py.std.shlex.split(str(args)) + if _preinit: + _pluginmanager = _preinit.pop(0) + else: # subsequent calls to main will create a fresh instance + _pluginmanager = PluginManager(load=True) + hook = _pluginmanager.hook try: + if plugins: + for plugin in plugins: + _pluginmanager.register(plugin) config = hook.pytest_cmdline_parse( - pluginmanager=pluginmanager, args=args) + pluginmanager=_pluginmanager, args=args) exitstatus = hook.pytest_cmdline_main(config=config) except UsageError: e = sys.exc_info()[1] sys.stderr.write("ERROR: %s\n" %(e.args[0],)) exitstatus = 3 - pluginmanager = PluginManager(load=True) return exitstatus class UsageError(Exception): """ error in py.test usage or invocation""" + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/pytest/plugin/config.py b/pytest/plugin/config.py index b824056c93c..77d5fc1f629 100644 --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -1,7 +1,7 @@ import py import sys, os -from pytest._core import PluginManager +from pytest.main import PluginManager import pytest diff --git a/pytest/plugin/pytester.py b/pytest/plugin/pytester.py index b438afcbd08..8162aa5cfd3 100644 --- a/pytest/plugin/pytester.py +++ b/pytest/plugin/pytester.py @@ -2,7 +2,7 @@ funcargs and support code for testing py.test's own functionality. """ -import py +import py, pytest import sys, os import re import inspect @@ -10,7 +10,7 @@ from fnmatch import fnmatch from pytest.plugin.session import Collection from py.builtin import print_ -from pytest._core import HookRelay +from pytest.main import HookRelay def pytest_addoption(parser): group = parser.getgroup("pylib") @@ -401,6 +401,10 @@ def popen(self, cmdargs, stdout, stderr, **kw): #print "env", env return py.std.subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw) + def pytestmain(self, *args, **kwargs): + ret = pytest.main(*args, **kwargs) + if ret == 2: + raise KeyboardInterrupt() def run(self, *cmdargs): return self._run(*cmdargs) diff --git a/pytest/plugin/python.py b/pytest/plugin/python.py index fcad1962d34..7c510b29d02 100644 --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -461,7 +461,7 @@ def hasinit(obj): def getfuncargnames(function): - # XXX merge with _core.py's varnames + # XXX merge with main.py's varnames argnames = py.std.inspect.getargs(py.code.getrawcode(function))[0] startindex = py.std.inspect.ismethod(function) and 1 or 0 defaults = getattr(function, 'func_defaults', diff --git a/setup.py b/setup.py index c4deb75bd5d..984623dda80 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ def main(): ) def cmdline_entrypoints(versioninfo, platform, basename): - target = 'pytest:__main__' + target = 'pytest:main' if platform.startswith('java'): points = {'py.test-jython': target} else: @@ -66,4 +66,4 @@ def make_entry_points(): return {'console_scripts': l} if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 572290d05dd..c24eb1c176a 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1,4 +1,4 @@ -import sys, py +import sys, py, pytest class TestGeneralUsage: def test_config_error(self, testdir): @@ -82,36 +82,6 @@ def test_not_collectable_arguments(self, testdir): ]) - def test_earlyinit(self, testdir): - p = testdir.makepyfile(""" - import py - assert hasattr(py.test, 'mark') - """) - result = testdir.runpython(p) - assert result.ret == 0 - - def test_pydoc(self, testdir): - result = testdir.runpython_c("import py;help(py.test)") - assert result.ret == 0 - s = result.stdout.str() - assert 'MarkGenerator' in s - - def test_double_pytestcmdline(self, testdir): - p = testdir.makepyfile(run=""" - import py - py.test.cmdline.main() - py.test.cmdline.main() - """) - testdir.makepyfile(""" - def test_hello(): - pass - """) - result = testdir.runpython(p) - result.stdout.fnmatch_lines([ - "*1 passed*", - "*1 passed*", - ]) - @py.test.mark.xfail def test_early_skip(self, testdir): @@ -225,19 +195,6 @@ def pytest_collect_file(path, parent): "*1 pass*", ]) - - @py.test.mark.skipif("sys.version_info < (2,5)") - def test_python_minus_m_invocation_ok(self, testdir): - p1 = testdir.makepyfile("def test_hello(): pass") - res = testdir.run(py.std.sys.executable, "-m", "py.test", str(p1)) - assert res.ret == 0 - - @py.test.mark.skipif("sys.version_info < (2,5)") - def test_python_minus_m_invocation_fail(self, testdir): - p1 = testdir.makepyfile("def test_fail(): 0/0") - res = testdir.run(py.std.sys.executable, "-m", "py.test", str(p1)) - assert res.ret == 1 - def test_skip_on_generated_funcarg_id(self, testdir): testdir.makeconftest(""" import py @@ -253,3 +210,83 @@ def pytest_runtest_setup(item): res = testdir.runpytest(p) assert res.ret == 0 res.stdout.fnmatch_lines(["*1 skipped*"]) + +class TestInvocationVariants: + def test_earlyinit(self, testdir): + p = testdir.makepyfile(""" + import py + assert hasattr(py.test, 'mark') + """) + result = testdir.runpython(p) + assert result.ret == 0 + + def test_pydoc(self, testdir): + result = testdir.runpython_c("import py;help(py.test)") + assert result.ret == 0 + s = result.stdout.str() + assert 'MarkGenerator' in s + + def test_double_pytestcmdline(self, testdir): + p = testdir.makepyfile(run=""" + import py + py.test.cmdline.main() + py.test.cmdline.main() + """) + testdir.makepyfile(""" + def test_hello(): + pass + """) + result = testdir.runpython(p) + result.stdout.fnmatch_lines([ + "*1 passed*", + "*1 passed*", + ]) + + @py.test.mark.skipif("sys.version_info < (2,5)") + def test_python_minus_m_invocation_ok(self, testdir): + p1 = testdir.makepyfile("def test_hello(): pass") + res = testdir.run(py.std.sys.executable, "-m", "py.test", str(p1)) + assert res.ret == 0 + + @py.test.mark.skipif("sys.version_info < (2,5)") + def test_python_minus_m_invocation_fail(self, testdir): + p1 = testdir.makepyfile("def test_fail(): 0/0") + res = testdir.run(py.std.sys.executable, "-m", "py.test", str(p1)) + assert res.ret == 1 + + def test_python_pytest_main(self, testdir): + p1 = testdir.makepyfile("def test_pass(): pass") + res = testdir.run(py.std.sys.executable, "-m", "pytest.main", str(p1)) + assert res.ret == 0 + res.stdout.fnmatch_lines(["*1 passed*"]) + + @py.test.mark.skipif("sys.version_info < (2,7)") + def test_python_pytest_package(self, testdir): + p1 = testdir.makepyfile("def test_pass(): pass") + res = testdir.run(py.std.sys.executable, "-m", "pytest", str(p1)) + assert res.ret == 0 + res.stdout.fnmatch_lines(["*1 passed*"]) + + def test_equivalence_pytest_pytest(self): + assert pytest.main == py.test.cmdline.main + + def test_invoke_with_string(self, capsys): + retcode = pytest.main("-h") + assert not retcode + out, err = capsys.readouterr() + assert "--help" in out + + def test_invoke_with_path(self, testdir, capsys): + retcode = testdir.pytestmain(testdir.tmpdir) + assert not retcode + out, err = capsys.readouterr() + + def test_invoke_plugin_api(self, capsys): + class MyPlugin: + def pytest_addoption(self, parser): + parser.addoption("--myopt") + + pytest.main(["-h"], plugins=[MyPlugin()]) + out, err = capsys.readouterr() + assert "--myopt" in out + diff --git a/testing/plugin/conftest.py b/testing/plugin/conftest.py index 57274273b5a..8df9abe4c00 100644 --- a/testing/plugin/conftest.py +++ b/testing/plugin/conftest.py @@ -2,7 +2,7 @@ import pytest.plugin plugindir = py.path.local(pytest.plugin.__file__).dirpath() -from pytest._core import default_plugins +from pytest.main import default_plugins def pytest_collect_file(path, parent): if path.basename.startswith("pytest_") and path.ext == ".py": diff --git a/testing/plugin/test_pytester.py b/testing/plugin/test_pytester.py index 17701221e5b..2d956360a01 100644 --- a/testing/plugin/test_pytester.py +++ b/testing/plugin/test_pytester.py @@ -1,7 +1,7 @@ import py import os, sys from pytest.plugin.pytester import LineMatcher, LineComp, HookRecorder -from pytest._core import PluginManager +from pytest.main import PluginManager def test_reportrecorder(testdir): item = testdir.getitem("def test_func(): pass") @@ -97,7 +97,7 @@ def pytest_xyz(): def test_functional(testdir, linecomp): reprec = testdir.inline_runsource(""" import py - from pytest._core import HookRelay, PluginManager + from pytest.main import HookRelay, PluginManager pytest_plugins="pytester" def test_func(_pytest): class ApiClass: diff --git a/testing/test_pluginmanager.py b/testing/test_main.py similarity index 98% rename from testing/test_pluginmanager.py rename to testing/test_main.py index f7c101e98d7..4f8a754d7f7 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_main.py @@ -1,6 +1,6 @@ import py, os -from pytest._core import PluginManager, canonical_importname -from pytest._core import MultiCall, HookRelay, varnames +from pytest.main import PluginManager, canonical_importname +from pytest.main import MultiCall, HookRelay, varnames class TestBootstrapping: @@ -552,7 +552,7 @@ def hello(self, arg): class TestTracer: def test_simple(self): - from pytest._core import TagTracer + from pytest.main import TagTracer rootlogger = TagTracer() log = rootlogger.get("pytest") log("hello") @@ -566,7 +566,7 @@ def test_simple(self): assert l[1] == "[pytest:collection] hello\n" def test_setprocessor(self): - from pytest._core import TagTracer + from pytest.main import TagTracer rootlogger = TagTracer() log = rootlogger.get("1") log2 = log.get("2") @@ -588,7 +588,7 @@ def test_setprocessor(self): def test_setmyprocessor(self): - from pytest._core import TagTracer + from pytest.main import TagTracer rootlogger = TagTracer() log = rootlogger.get("1") log2 = log.get("2")