cleanup py.test.* namespace, docstrings for improved pydoc and interactive usage.
use new apipkg __onfirstaccess__ feature to initialize the py.test namespace with the default plugins. This, besides other good implications, means that you can now type: pydoc py.test or help(py.test) --HG-- branch : trunk
This commit is contained in:
parent
080fd2880e
commit
db21cac694
|
@ -1,4 +1,4 @@
|
||||||
Changes between 1.1.2 and 1.1.1
|
Changes between 1.X and 1.1.1
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
- new option: --ignore will prevent specified path from collection.
|
- new option: --ignore will prevent specified path from collection.
|
||||||
|
@ -7,6 +7,12 @@ Changes between 1.1.2 and 1.1.1
|
||||||
- install 'py.test' and `py.which` with a ``-$VERSION`` suffix to
|
- install 'py.test' and `py.which` with a ``-$VERSION`` suffix to
|
||||||
disambiguate between Python3, python2.X, Jython and PyPy installed versions.
|
disambiguate between Python3, python2.X, Jython and PyPy installed versions.
|
||||||
|
|
||||||
|
- make py.test.* helpers provided by default plugins visible early -
|
||||||
|
works transparently both for pydoc and for interactive sessions
|
||||||
|
which will regularly see e.g. py.test.mark and py.test.importorskip.
|
||||||
|
|
||||||
|
- simplify internal plugin manager machinery
|
||||||
|
|
||||||
- fix assert reinterpreation that sees a call containing "keyword=..."
|
- fix assert reinterpreation that sees a call containing "keyword=..."
|
||||||
|
|
||||||
- skip some install-tests if no execnet is available
|
- skip some install-tests if no execnet is available
|
||||||
|
|
|
@ -14,5 +14,4 @@ def pytest(argv=None):
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
pass
|
pass
|
||||||
# we need to reset the global py.test.config object
|
# we need to reset the global py.test.config object
|
||||||
py.test.config = py.test.config.__class__(
|
py.test.config = py.test.config.__class__()
|
||||||
pluginmanager=py.test._PluginManager())
|
|
||||||
|
|
|
@ -40,8 +40,8 @@ py.apipkg.initpkg(__name__, dict(
|
||||||
|
|
||||||
test = {
|
test = {
|
||||||
# helpers for use from test functions or collectors
|
# helpers for use from test functions or collectors
|
||||||
|
'__onfirstaccess__' : '.impl.test.config:onpytestaccess',
|
||||||
'__doc__' : '.impl.test:__doc__',
|
'__doc__' : '.impl.test:__doc__',
|
||||||
'_PluginManager' : '.impl.test.pluginmanager:PluginManager',
|
|
||||||
'raises' : '.impl.test.outcome:raises',
|
'raises' : '.impl.test.outcome:raises',
|
||||||
'skip' : '.impl.test.outcome:skip',
|
'skip' : '.impl.test.outcome:skip',
|
||||||
'importorskip' : '.impl.test.outcome:importorskip',
|
'importorskip' : '.impl.test.outcome:importorskip',
|
||||||
|
|
19
py/apipkg.py
19
py/apipkg.py
|
@ -8,7 +8,7 @@ see http://pypi.python.org/pypi/apipkg
|
||||||
import sys
|
import sys
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
|
|
||||||
__version__ = "1.0b2"
|
__version__ = "1.0b3"
|
||||||
|
|
||||||
def initpkg(pkgname, exportdefs):
|
def initpkg(pkgname, exportdefs):
|
||||||
""" initialize given package from the export definitions. """
|
""" initialize given package from the export definitions. """
|
||||||
|
@ -26,7 +26,7 @@ def importobj(modpath, attrname):
|
||||||
class ApiModule(ModuleType):
|
class ApiModule(ModuleType):
|
||||||
def __init__(self, name, importspec, implprefix=None):
|
def __init__(self, name, importspec, implprefix=None):
|
||||||
self.__name__ = name
|
self.__name__ = name
|
||||||
self.__all__ = list(importspec)
|
self.__all__ = [x for x in importspec if x != '__onfirstaccess__']
|
||||||
self.__map__ = {}
|
self.__map__ = {}
|
||||||
self.__implprefix__ = implprefix or name
|
self.__implprefix__ = implprefix or name
|
||||||
for name, importspec in importspec.items():
|
for name, importspec in importspec.items():
|
||||||
|
@ -45,12 +45,26 @@ class ApiModule(ModuleType):
|
||||||
self.__map__[name] = (modpath, attrname)
|
self.__map__[name] = (modpath, attrname)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
l = []
|
||||||
|
if hasattr(self, '__version__'):
|
||||||
|
l.append("version=" + repr(self.__version__))
|
||||||
|
if hasattr(self, '__file__'):
|
||||||
|
l.append('from ' + repr(self.__file__))
|
||||||
|
if l:
|
||||||
|
return '<ApiModule %r %s>' % (self.__name__, " ".join(l))
|
||||||
return '<ApiModule %r>' % (self.__name__,)
|
return '<ApiModule %r>' % (self.__name__,)
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
|
target = None
|
||||||
|
if '__onfirstaccess__' in self.__map__:
|
||||||
|
target = self.__map__.pop('__onfirstaccess__')
|
||||||
|
importobj(*target)()
|
||||||
try:
|
try:
|
||||||
modpath, attrname = self.__map__[name]
|
modpath, attrname = self.__map__[name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
if target is not None and name != '__onfirstaccess__':
|
||||||
|
# retry, onfirstaccess might have set attrs
|
||||||
|
return getattr(self, name)
|
||||||
raise AttributeError(name)
|
raise AttributeError(name)
|
||||||
else:
|
else:
|
||||||
result = importobj(modpath, attrname)
|
result = importobj(modpath, attrname)
|
||||||
|
@ -63,6 +77,7 @@ class ApiModule(ModuleType):
|
||||||
dictdescr = ModuleType.__dict__['__dict__']
|
dictdescr = ModuleType.__dict__['__dict__']
|
||||||
dict = dictdescr.__get__(self)
|
dict = dictdescr.__get__(self)
|
||||||
if dict is not None:
|
if dict is not None:
|
||||||
|
hasattr(self, 'some')
|
||||||
for name in self.__all__:
|
for name in self.__all__:
|
||||||
hasattr(self, name) # force attribute load, ignore errors
|
hasattr(self, name) # force attribute load, ignore errors
|
||||||
return dict
|
return dict
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
""" versatile unit-testing tool + libraries """
|
""" assertion and py.test helper API."""
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import py, os
|
import py, os
|
||||||
from py.impl.test.conftesthandle import Conftest
|
from py.impl.test.conftesthandle import Conftest
|
||||||
|
from py.impl.test.pluginmanager import PluginManager
|
||||||
from py.impl.test import parseopt
|
from py.impl.test import parseopt
|
||||||
|
|
||||||
def ensuretemp(string, dir=1):
|
def ensuretemp(string, dir=1):
|
||||||
""" return temporary directory path with
|
""" (deprecated) return temporary directory path with
|
||||||
the given string as the trailing part.
|
the given string as the trailing part. It is usually
|
||||||
|
better to use the 'tmpdir' function argument which will
|
||||||
|
take care to provide empty unique directories for each
|
||||||
|
test call even if the test is called multiple times.
|
||||||
"""
|
"""
|
||||||
return py.test.config.ensuretemp(string, dir=dir)
|
return py.test.config.ensuretemp(string, dir=dir)
|
||||||
|
|
||||||
|
@ -33,7 +36,7 @@ class Config(object):
|
||||||
usage="usage: %prog [options] [file_or_dir] [file_or_dir] [...]",
|
usage="usage: %prog [options] [file_or_dir] [file_or_dir] [...]",
|
||||||
processopt=self._processopt,
|
processopt=self._processopt,
|
||||||
)
|
)
|
||||||
self.pluginmanager = py.test._PluginManager()
|
self.pluginmanager = PluginManager()
|
||||||
self._conftest = Conftest(onimport=self._onimportconftest)
|
self._conftest = Conftest(onimport=self._onimportconftest)
|
||||||
self.hook = self.pluginmanager.hook
|
self.hook = self.pluginmanager.hook
|
||||||
|
|
||||||
|
@ -178,7 +181,7 @@ class Config(object):
|
||||||
This function gets invoked during testing session initialization.
|
This function gets invoked during testing session initialization.
|
||||||
"""
|
"""
|
||||||
py.log._apiwarn("1.0", "define plugins to add options", stacklevel=2)
|
py.log._apiwarn("1.0", "define plugins to add options", stacklevel=2)
|
||||||
group = self._parser.addgroup(groupname)
|
group = self._parser.getgroup(groupname)
|
||||||
for opt in specs:
|
for opt in specs:
|
||||||
group._addoption_instance(opt)
|
group._addoption_instance(opt)
|
||||||
return self.option
|
return self.option
|
||||||
|
@ -296,6 +299,11 @@ def gettopdir(args):
|
||||||
else:
|
else:
|
||||||
return pkgdir.dirpath()
|
return pkgdir.dirpath()
|
||||||
|
|
||||||
|
def onpytestaccess():
|
||||||
|
# it's enough to have our containing module loaded as
|
||||||
|
# it initializes a per-process config instance
|
||||||
|
# which loads default plugins which add to py.test.*
|
||||||
|
pass
|
||||||
|
|
||||||
# this is default per-process instance of py.test configuration
|
# a default per-process instance of py.test configuration
|
||||||
config_per_process = Config()
|
config_per_process = Config()
|
||||||
|
|
|
@ -47,23 +47,33 @@ class Exit(KeyboardInterrupt):
|
||||||
# exposed helper methods
|
# exposed helper methods
|
||||||
|
|
||||||
def exit(msg):
|
def exit(msg):
|
||||||
""" exit testing process immediately. """
|
""" exit testing process as if KeyboardInterrupt was triggered. """
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
raise Exit(msg)
|
raise Exit(msg)
|
||||||
|
|
||||||
def skip(msg=""):
|
def skip(msg=""):
|
||||||
""" skip with the given message. """
|
""" skip an executing test with the given message. Note: it's usually
|
||||||
|
better use the py.test.mark.skipif marker to declare a test to be
|
||||||
|
skipped under certain conditions like mismatching platforms or
|
||||||
|
dependencies. See the pytest_skipping plugin for details.
|
||||||
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
raise Skipped(msg=msg)
|
raise Skipped(msg=msg)
|
||||||
|
|
||||||
def fail(msg="unknown failure"):
|
def fail(msg=""):
|
||||||
""" fail with the given Message. """
|
""" explicitely fail this executing test with the given Message. """
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
raise Failed(msg=msg)
|
raise Failed(msg=msg)
|
||||||
|
|
||||||
def raises(ExpectedException, *args, **kwargs):
|
def raises(ExpectedException, *args, **kwargs):
|
||||||
""" raise AssertionError, if target code does not raise the expected
|
""" if args[0] is callable: raise AssertionError if calling it with
|
||||||
exception.
|
the remaining arguments does not raise the expected exception.
|
||||||
|
if args[0] is a string: raise AssertionError if executing the
|
||||||
|
the string in the calling scope does not raise expected exception.
|
||||||
|
for examples:
|
||||||
|
x = 5
|
||||||
|
raises(TypeError, lambda x: x + 'hello', x=x)
|
||||||
|
raises(TypeError, "x + 'hello'")
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
assert args
|
assert args
|
||||||
|
@ -95,7 +105,10 @@ def raises(ExpectedException, *args, **kwargs):
|
||||||
expr=args, expected=ExpectedException)
|
expr=args, expected=ExpectedException)
|
||||||
|
|
||||||
def importorskip(modname, minversion=None):
|
def importorskip(modname, minversion=None):
|
||||||
""" return imported module or perform a dynamic skip() """
|
""" return imported module if it has a higher __version__ than the
|
||||||
|
optionally specified 'minversion' - otherwise call py.test.skip()
|
||||||
|
with a message detailing the mismatch.
|
||||||
|
"""
|
||||||
compile(modname, '', 'eval') # to catch syntaxerrors
|
compile(modname, '', 'eval') # to catch syntaxerrors
|
||||||
try:
|
try:
|
||||||
mod = __import__(modname, None, None, ['__doc__'])
|
mod = __import__(modname, None, None, ['__doc__'])
|
||||||
|
@ -114,6 +127,7 @@ def importorskip(modname, minversion=None):
|
||||||
return mod
|
return mod
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# exitcodes for the command line
|
# exitcodes for the command line
|
||||||
EXIT_OK = 0
|
EXIT_OK = 0
|
||||||
EXIT_TESTSFAILED = 1
|
EXIT_TESTSFAILED = 1
|
||||||
|
|
|
@ -15,8 +15,6 @@ def check_old_use(mod, modname):
|
||||||
assert not hasattr(mod, clsname), (mod, clsname)
|
assert not hasattr(mod, clsname), (mod, clsname)
|
||||||
|
|
||||||
class PluginManager(object):
|
class PluginManager(object):
|
||||||
class Error(Exception):
|
|
||||||
"""signals a plugin specific error."""
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.registry = Registry()
|
self.registry = Registry()
|
||||||
self._name2plugin = {}
|
self._name2plugin = {}
|
||||||
|
@ -157,6 +155,7 @@ class PluginManager(object):
|
||||||
dic = self.call_plugin(plugin, "pytest_namespace", {}) or {}
|
dic = self.call_plugin(plugin, "pytest_namespace", {}) or {}
|
||||||
for name, value in dic.items():
|
for name, value in dic.items():
|
||||||
setattr(py.test, name, value)
|
setattr(py.test, name, value)
|
||||||
|
py.test.__all__.append(name)
|
||||||
if hasattr(self, '_config'):
|
if hasattr(self, '_config'):
|
||||||
self.call_plugin(plugin, "pytest_addoption",
|
self.call_plugin(plugin, "pytest_addoption",
|
||||||
{'parser': self._config._parser})
|
{'parser': self._config._parser})
|
||||||
|
|
|
@ -78,16 +78,18 @@ tests::
|
||||||
import py
|
import py
|
||||||
|
|
||||||
def pytest_namespace():
|
def pytest_namespace():
|
||||||
return {'mark': Mark()}
|
return {'mark': MarkGenerator()}
|
||||||
|
|
||||||
|
class MarkGenerator:
|
||||||
class Mark(object):
|
""" non-underscore attributes of this object can be used as decorators for
|
||||||
|
marking test functions. Example: @py.test.mark.slowtest in front of a
|
||||||
|
function will set the 'slowtest' marker object on it. """
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
if name[0] == "_":
|
if name[0] == "_":
|
||||||
raise AttributeError(name)
|
raise AttributeError(name)
|
||||||
return MarkerDecorator(name)
|
return MarkDecorator(name)
|
||||||
|
|
||||||
class MarkerDecorator:
|
class MarkDecorator:
|
||||||
""" decorator for setting function attributes. """
|
""" decorator for setting function attributes. """
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
self.markname = name
|
self.markname = name
|
||||||
|
@ -97,15 +99,17 @@ class MarkerDecorator:
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
d = self.__dict__.copy()
|
d = self.__dict__.copy()
|
||||||
name = d.pop('markname')
|
name = d.pop('markname')
|
||||||
return "<MarkerDecorator %r %r>" %(name, d)
|
return "<MarkDecorator %r %r>" %(name, d)
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
|
""" if passed a single callable argument: decorate it with mark info.
|
||||||
|
otherwise add *args/**kwargs in-place to mark information. """
|
||||||
if args:
|
if args:
|
||||||
if len(args) == 1 and hasattr(args[0], '__call__'):
|
if len(args) == 1 and hasattr(args[0], '__call__'):
|
||||||
func = args[0]
|
func = args[0]
|
||||||
holder = getattr(func, self.markname, None)
|
holder = getattr(func, self.markname, None)
|
||||||
if holder is None:
|
if holder is None:
|
||||||
holder = Marker(self.markname, self.args, self.kwargs)
|
holder = MarkInfo(self.markname, self.args, self.kwargs)
|
||||||
setattr(func, self.markname, holder)
|
setattr(func, self.markname, holder)
|
||||||
else:
|
else:
|
||||||
holder.kwargs.update(self.kwargs)
|
holder.kwargs.update(self.kwargs)
|
||||||
|
@ -116,7 +120,7 @@ class MarkerDecorator:
|
||||||
self.kwargs.update(kwargs)
|
self.kwargs.update(kwargs)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
class Marker:
|
class MarkInfo:
|
||||||
def __init__(self, name, args, kwargs):
|
def __init__(self, name, args, kwargs):
|
||||||
self._name = name
|
self._name = name
|
||||||
self.args = args
|
self.args = args
|
||||||
|
@ -129,7 +133,7 @@ class Marker:
|
||||||
raise AttributeError(name)
|
raise AttributeError(name)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Marker %r args=%r kwargs=%r>" % (
|
return "<MarkInfo %r args=%r kwargs=%r>" % (
|
||||||
self._name, self.args, self.kwargs)
|
self._name, self.args, self.kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -143,6 +147,6 @@ def pytest_pycollect_makeitem(__multicall__, collector, name, obj):
|
||||||
func = getattr(func, 'im_func', func) # py2
|
func = getattr(func, 'im_func', func) # py2
|
||||||
for parent in [x for x in (mod, cls) if x]:
|
for parent in [x for x in (mod, cls) if x]:
|
||||||
marker = getattr(parent.obj, 'pytestmark', None)
|
marker = getattr(parent.obj, 'pytestmark', None)
|
||||||
if isinstance(marker, MarkerDecorator):
|
if isinstance(marker, MarkDecorator):
|
||||||
marker(func)
|
marker(func)
|
||||||
return item
|
return item
|
||||||
|
|
|
@ -3,7 +3,7 @@ advanced skipping for python test functions, classes or modules.
|
||||||
|
|
||||||
With this plugin you can mark test functions for conditional skipping
|
With this plugin you can mark test functions for conditional skipping
|
||||||
or as "xfail", expected-to-fail. Skipping a test will avoid running it
|
or as "xfail", expected-to-fail. Skipping a test will avoid running it
|
||||||
at all while xfail-marked tests will run and result in an inverted outcome:
|
while xfail-marked tests will run and result in an inverted outcome:
|
||||||
a pass becomes a failure and a fail becomes a semi-passing one.
|
a pass becomes a failure and a fail becomes a semi-passing one.
|
||||||
|
|
||||||
The need for skipping a test is usually connected to a condition.
|
The need for skipping a test is usually connected to a condition.
|
||||||
|
@ -121,6 +121,7 @@ within test or setup code. Example::
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
|
||||||
|
|
||||||
def pytest_runtest_setup(item):
|
def pytest_runtest_setup(item):
|
||||||
expr, result = evalexpression(item, 'skipif')
|
expr, result = evalexpression(item, 'skipif')
|
||||||
if result:
|
if result:
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import py
|
import py
|
||||||
from py.plugin.pytest_mark import Mark
|
from py.plugin.pytest_mark import MarkGenerator as Mark
|
||||||
|
|
||||||
class TestMark:
|
class TestMark:
|
||||||
def test_pytest_mark_notcallable(self):
|
def test_pytest_mark_notcallable(self):
|
||||||
mark = Mark()
|
mark = Mark()
|
||||||
py.test.raises(TypeError, "mark()")
|
py.test.raises((AttributeError, TypeError), "mark()")
|
||||||
|
|
||||||
def test_pytest_mark_bare(self):
|
def test_pytest_mark_bare(self):
|
||||||
mark = Mark()
|
mark = Mark()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import py
|
import sys, py
|
||||||
|
|
||||||
class TestGeneralUsage:
|
class TestGeneralUsage:
|
||||||
def test_config_error(self, testdir):
|
def test_config_error(self, testdir):
|
||||||
|
@ -74,3 +74,18 @@ class TestGeneralUsage:
|
||||||
assert result.stderr.fnmatch_lines([
|
assert result.stderr.fnmatch_lines([
|
||||||
"*ERROR: can't collect: %s" %(p2,)
|
"*ERROR: can't collect: %s" %(p2,)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def test_earlyinit(self, testdir):
|
||||||
|
p = testdir.makepyfile("""
|
||||||
|
import py
|
||||||
|
assert hasattr(py.test, 'mark')
|
||||||
|
""")
|
||||||
|
result = testdir._run(sys.executable, p)
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
def test_pydoc(self, testdir):
|
||||||
|
result = testdir._run(sys.executable, "-c", "import py ; help(py.test)")
|
||||||
|
assert result.ret == 0
|
||||||
|
s = result.stdout.str()
|
||||||
|
assert 'MarkGenerator' in s
|
||||||
|
|
|
@ -223,6 +223,7 @@ class TestPytestPluginInteractions:
|
||||||
import py
|
import py
|
||||||
def test_hello():
|
def test_hello():
|
||||||
assert hello == "world"
|
assert hello == "world"
|
||||||
|
assert 'hello' in py.test.__all__
|
||||||
""")
|
""")
|
||||||
result = testdir.runpytest(p)
|
result = testdir.runpytest(p)
|
||||||
result.stdout.fnmatch_lines([
|
result.stdout.fnmatch_lines([
|
||||||
|
|
Loading…
Reference in New Issue