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
- 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,

View File

@ -4,6 +4,7 @@
py.test reference documentation
================================================
.. toctree::
: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
-----------------------------------------------------
@ -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:

View File

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

View File

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

View File

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

View File

@ -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`

View File

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

View File

@ -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.

View File

@ -1,9 +1,12 @@
.. _usage:
Usage and Invocations
==========================================
.. _cmdline:
Using the interactive command line
===============================================
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

View File

@ -9,8 +9,6 @@ __version__ = '2.0.0.dev18'
__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

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

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 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 @@ class HookCaller:
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())

View File

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

View File

@ -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 @@ import time
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 @@ class TmpTestdir:
#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)

View File

@ -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',

View File

@ -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()
main()

View File

@ -1,4 +1,4 @@
import sys, py
import sys, py, pytest
class TestGeneralUsage:
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
def test_early_skip(self, testdir):
@ -225,19 +195,6 @@ class TestGeneralUsage:
"*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 @@ class TestGeneralUsage:
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

View File

@ -2,7 +2,7 @@ import py
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":

View File

@ -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 test_hookrecorder_basic_no_args_hook():
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:

View File

@ -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 @@ class TestHookRelay:
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 @@ class TestTracer:
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 @@ class TestTracer:
def test_setmyprocessor(self):
from pytest._core import TagTracer
from pytest.main import TagTracer
rootlogger = TagTracer()
log = rootlogger.get("1")
log2 = log.get("2")