implement and document new invocation mechanisms, see doc/usage.txt

also rename pytest._core to pytest.main for convenience.
This commit is contained in:
holger krekel 2010-11-05 23:37:31 +01:00
parent 6a734efe44
commit d108235095
22 changed files with 228 additions and 106 deletions

View File

@ -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 - 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 - try harder to run unittest test suites in a more compatible manner
by deferring setup/teardown semantics to the unittest package. by deferring setup/teardown semantics to the unittest package.
- introduce a new way to set config options via ini-style files, - introduce a new way to set config options via ini-style files,

View File

@ -4,6 +4,7 @@
py.test reference documentation py.test reference documentation
================================================ ================================================
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2

View File

@ -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 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. * ``pop(category=None)``: return last warning matching the category.
* ``clear()``: clear list of warnings * ``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:

View File

@ -5,8 +5,8 @@ no-boilerplate testing with Python
---------------------------------- ----------------------------------
- automatic, fully customizable Python test discovery - automatic, fully customizable Python test discovery
- :pep:`8` consistent testing style - allows fully :pep:`8` compliant coding style
- allows simple test functions - write simple test functions and freely group tests
- ``assert`` statement for your assertions - ``assert`` statement for your assertions
- powerful parametrization of test functions - powerful parametrization of test functions
- rely on powerful traceback and assertion reporting - rely on powerful traceback and assertion reporting
@ -25,8 +25,8 @@ extensive plugin and customization system
mature command line testing tool mature command line testing tool
-------------------------------------- --------------------------------------
- powerful :ref:`usage` possibilities
- used in many projects, ranging from 10 to 10K tests - used in many projects, ranging from 10 to 10K tests
- autodiscovery of tests
- simple well sorted command line options - simple well sorted command line options
- runs on Unix, Windows from Python 2.4 up to Python 3.1 and 3.2 - runs on Unix, Windows from Python 2.4 up to Python 3.1 and 3.2
- is itself tested extensively on a CI server - is itself tested extensively on a CI server

View File

@ -81,11 +81,16 @@ You can always run your tests by pointing to it::
Test modules are imported under their fully qualified name as follows: 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: .. _standalone:
.. _`genscript method`: .. _`genscript method`:

View File

@ -3,6 +3,10 @@ py.test: no-boilerplate testing with Python
.. todolist:: .. 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: Welcome to ``py.test`` documentation:
@ -27,4 +31,3 @@ Indices and tables
* :ref:`genindex` * :ref:`genindex`
* :ref:`modindex` * :ref:`modindex`
* :ref:`search` * :ref:`search`

View File

@ -7,7 +7,7 @@ Overview and Introduction
features.txt features.txt
getting-started.txt getting-started.txt
cmdline.txt usage.txt
goodpractises.txt goodpractises.txt
faq.txt faq.txt

View File

@ -35,7 +35,7 @@ However easy_install does not provide an uninstall facility.
.. IMPORTANT:: .. 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 This file starts coverage collection of subprocesses if appropriate during site initialisation
at python startup. at python startup.

View File

@ -1,9 +1,12 @@
.. _usage:
Usage and Invocations
==========================================
.. _cmdline: .. _cmdline:
Using the interactive command line
===============================================
Getting help on version, option names, environment vars 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 -x # stop after first failure
py.test -maxfail=2 # stop after two failures 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 Modifying Python traceback printing
---------------------------------------------- ----------------------------------------------

View File

@ -9,8 +9,6 @@ __version__ = '2.0.0.dev18'
__all__ = ['config', 'cmdline'] __all__ = ['config', 'cmdline']
from pytest import _core as cmdline from pytest import main as cmdline
UsageError = cmdline.UsageError UsageError = cmdline.UsageError
main = cmdline.main
def __main__():
raise SystemExit(cmdline.main())

4
pytest/__main__.py Normal file
View File

@ -0,0 +1,4 @@
import pytest
if __name__ == '__main__':
raise SystemExit(pytest.main())

View File

@ -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 # Initialization

View File

@ -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 sys, os
import inspect import inspect
import py import py
from pytest import hookspec from pytest import hookspec # the extension point definitions
assert py.__version__.split(".")[:2] >= ['2', '0'], ("installation problem: " assert py.__version__.split(".")[:2] >= ['2', '0'], ("installation problem: "
"%s is too old, remove or upgrade 'py'" % (py.__version__)) "%s is too old, remove or upgrade 'py'" % (py.__version__))
@ -375,23 +380,33 @@ class HookCaller:
mc = MultiCall(methods, kwargs, firstresult=self.firstresult) mc = MultiCall(methods, kwargs, firstresult=self.firstresult)
return mc.execute() return mc.execute()
pluginmanager = PluginManager(load=True) # will trigger default plugin importing _preinit = [PluginManager(load=True)] # triggers default plugin importing
def main(args=None): def main(args=None, plugins=None):
global pluginmanager
if args is None: if args is None:
args = sys.argv[1:] 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: try:
if plugins:
for plugin in plugins:
_pluginmanager.register(plugin)
config = hook.pytest_cmdline_parse( config = hook.pytest_cmdline_parse(
pluginmanager=pluginmanager, args=args) pluginmanager=_pluginmanager, args=args)
exitstatus = hook.pytest_cmdline_main(config=config) exitstatus = hook.pytest_cmdline_main(config=config)
except UsageError: except UsageError:
e = sys.exc_info()[1] e = sys.exc_info()[1]
sys.stderr.write("ERROR: %s\n" %(e.args[0],)) sys.stderr.write("ERROR: %s\n" %(e.args[0],))
exitstatus = 3 exitstatus = 3
pluginmanager = PluginManager(load=True)
return exitstatus return exitstatus
class UsageError(Exception): class UsageError(Exception):
""" error in py.test usage or invocation""" """ error in py.test usage or invocation"""
if __name__ == '__main__':
raise SystemExit(main())

View File

@ -1,7 +1,7 @@
import py import py
import sys, os import sys, os
from pytest._core import PluginManager from pytest.main import PluginManager
import pytest import pytest

View File

@ -2,7 +2,7 @@
funcargs and support code for testing py.test's own functionality. funcargs and support code for testing py.test's own functionality.
""" """
import py import py, pytest
import sys, os import sys, os
import re import re
import inspect import inspect
@ -10,7 +10,7 @@ import time
from fnmatch import fnmatch from fnmatch import fnmatch
from pytest.plugin.session import Collection from pytest.plugin.session import Collection
from py.builtin import print_ from py.builtin import print_
from pytest._core import HookRelay from pytest.main import HookRelay
def pytest_addoption(parser): def pytest_addoption(parser):
group = parser.getgroup("pylib") group = parser.getgroup("pylib")
@ -401,6 +401,10 @@ class TmpTestdir:
#print "env", env #print "env", env
return py.std.subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw) 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): def run(self, *cmdargs):
return self._run(*cmdargs) return self._run(*cmdargs)

View File

@ -461,7 +461,7 @@ def hasinit(obj):
def getfuncargnames(function): 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] argnames = py.std.inspect.getargs(py.code.getrawcode(function))[0]
startindex = py.std.inspect.ismethod(function) and 1 or 0 startindex = py.std.inspect.ismethod(function) and 1 or 0
defaults = getattr(function, 'func_defaults', defaults = getattr(function, 'func_defaults',

View File

@ -46,7 +46,7 @@ def main():
) )
def cmdline_entrypoints(versioninfo, platform, basename): def cmdline_entrypoints(versioninfo, platform, basename):
target = 'pytest:__main__' target = 'pytest:main'
if platform.startswith('java'): if platform.startswith('java'):
points = {'py.test-jython': target} points = {'py.test-jython': target}
else: else:

View File

@ -1,4 +1,4 @@
import sys, py import sys, py, pytest
class TestGeneralUsage: class TestGeneralUsage:
def test_config_error(self, testdir): def test_config_error(self, testdir):
@ -82,36 +82,6 @@ class TestGeneralUsage:
]) ])
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 @py.test.mark.xfail
def test_early_skip(self, testdir): def test_early_skip(self, testdir):
@ -225,19 +195,6 @@ class TestGeneralUsage:
"*1 pass*", "*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): def test_skip_on_generated_funcarg_id(self, testdir):
testdir.makeconftest(""" testdir.makeconftest("""
import py import py
@ -253,3 +210,83 @@ class TestGeneralUsage:
res = testdir.runpytest(p) res = testdir.runpytest(p)
assert res.ret == 0 assert res.ret == 0
res.stdout.fnmatch_lines(["*1 skipped*"]) 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

View File

@ -2,7 +2,7 @@ import py
import pytest.plugin import pytest.plugin
plugindir = py.path.local(pytest.plugin.__file__).dirpath() 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): def pytest_collect_file(path, parent):
if path.basename.startswith("pytest_") and path.ext == ".py": if path.basename.startswith("pytest_") and path.ext == ".py":

View File

@ -1,7 +1,7 @@
import py import py
import os, sys import os, sys
from pytest.plugin.pytester import LineMatcher, LineComp, HookRecorder from pytest.plugin.pytester import LineMatcher, LineComp, HookRecorder
from pytest._core import PluginManager from pytest.main import PluginManager
def test_reportrecorder(testdir): def test_reportrecorder(testdir):
item = testdir.getitem("def test_func(): pass") item = testdir.getitem("def test_func(): pass")
@ -97,7 +97,7 @@ def test_hookrecorder_basic_no_args_hook():
def test_functional(testdir, linecomp): def test_functional(testdir, linecomp):
reprec = testdir.inline_runsource(""" reprec = testdir.inline_runsource("""
import py import py
from pytest._core import HookRelay, PluginManager from pytest.main import HookRelay, PluginManager
pytest_plugins="pytester" pytest_plugins="pytester"
def test_func(_pytest): def test_func(_pytest):
class ApiClass: class ApiClass:

View File

@ -1,6 +1,6 @@
import py, os import py, os
from pytest._core import PluginManager, canonical_importname from pytest.main import PluginManager, canonical_importname
from pytest._core import MultiCall, HookRelay, varnames from pytest.main import MultiCall, HookRelay, varnames
class TestBootstrapping: class TestBootstrapping:
@ -552,7 +552,7 @@ class TestHookRelay:
class TestTracer: class TestTracer:
def test_simple(self): def test_simple(self):
from pytest._core import TagTracer from pytest.main import TagTracer
rootlogger = TagTracer() rootlogger = TagTracer()
log = rootlogger.get("pytest") log = rootlogger.get("pytest")
log("hello") log("hello")
@ -566,7 +566,7 @@ class TestTracer:
assert l[1] == "[pytest:collection] hello\n" assert l[1] == "[pytest:collection] hello\n"
def test_setprocessor(self): def test_setprocessor(self):
from pytest._core import TagTracer from pytest.main import TagTracer
rootlogger = TagTracer() rootlogger = TagTracer()
log = rootlogger.get("1") log = rootlogger.get("1")
log2 = log.get("2") log2 = log.get("2")
@ -588,7 +588,7 @@ class TestTracer:
def test_setmyprocessor(self): def test_setmyprocessor(self):
from pytest._core import TagTracer from pytest.main import TagTracer
rootlogger = TagTracer() rootlogger = TagTracer()
log = rootlogger.get("1") log = rootlogger.get("1")
log2 = log.get("2") log2 = log.get("2")