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.
|
||||
|
@ -7,6 +7,12 @@ Changes between 1.1.2 and 1.1.1
|
|||
- install 'py.test' and `py.which` with a ``-$VERSION`` suffix to
|
||||
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=..."
|
||||
|
||||
- skip some install-tests if no execnet is available
|
||||
|
|
|
@ -14,5 +14,4 @@ def pytest(argv=None):
|
|||
except SystemExit:
|
||||
pass
|
||||
# we need to reset the global py.test.config object
|
||||
py.test.config = py.test.config.__class__(
|
||||
pluginmanager=py.test._PluginManager())
|
||||
py.test.config = py.test.config.__class__()
|
||||
|
|
|
@ -40,8 +40,8 @@ py.apipkg.initpkg(__name__, dict(
|
|||
|
||||
test = {
|
||||
# helpers for use from test functions or collectors
|
||||
'__onfirstaccess__' : '.impl.test.config:onpytestaccess',
|
||||
'__doc__' : '.impl.test:__doc__',
|
||||
'_PluginManager' : '.impl.test.pluginmanager:PluginManager',
|
||||
'raises' : '.impl.test.outcome:raises',
|
||||
'skip' : '.impl.test.outcome:skip',
|
||||
'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
|
||||
from types import ModuleType
|
||||
|
||||
__version__ = "1.0b2"
|
||||
__version__ = "1.0b3"
|
||||
|
||||
def initpkg(pkgname, exportdefs):
|
||||
""" initialize given package from the export definitions. """
|
||||
|
@ -26,7 +26,7 @@ def importobj(modpath, attrname):
|
|||
class ApiModule(ModuleType):
|
||||
def __init__(self, name, importspec, implprefix=None):
|
||||
self.__name__ = name
|
||||
self.__all__ = list(importspec)
|
||||
self.__all__ = [x for x in importspec if x != '__onfirstaccess__']
|
||||
self.__map__ = {}
|
||||
self.__implprefix__ = implprefix or name
|
||||
for name, importspec in importspec.items():
|
||||
|
@ -45,12 +45,26 @@ class ApiModule(ModuleType):
|
|||
self.__map__[name] = (modpath, attrname)
|
||||
|
||||
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__,)
|
||||
|
||||
def __getattr__(self, name):
|
||||
target = None
|
||||
if '__onfirstaccess__' in self.__map__:
|
||||
target = self.__map__.pop('__onfirstaccess__')
|
||||
importobj(*target)()
|
||||
try:
|
||||
modpath, attrname = self.__map__[name]
|
||||
except KeyError:
|
||||
if target is not None and name != '__onfirstaccess__':
|
||||
# retry, onfirstaccess might have set attrs
|
||||
return getattr(self, name)
|
||||
raise AttributeError(name)
|
||||
else:
|
||||
result = importobj(modpath, attrname)
|
||||
|
@ -63,6 +77,7 @@ class ApiModule(ModuleType):
|
|||
dictdescr = ModuleType.__dict__['__dict__']
|
||||
dict = dictdescr.__get__(self)
|
||||
if dict is not None:
|
||||
hasattr(self, 'some')
|
||||
for name in self.__all__:
|
||||
hasattr(self, name) # force attribute load, ignore errors
|
||||
return dict
|
||||
|
|
|
@ -1 +1 @@
|
|||
""" versatile unit-testing tool + libraries """
|
||||
""" assertion and py.test helper API."""
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import py, os
|
||||
from py.impl.test.conftesthandle import Conftest
|
||||
|
||||
from py.impl.test.pluginmanager import PluginManager
|
||||
from py.impl.test import parseopt
|
||||
|
||||
def ensuretemp(string, dir=1):
|
||||
""" return temporary directory path with
|
||||
the given string as the trailing part.
|
||||
""" (deprecated) return temporary directory path with
|
||||
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)
|
||||
|
||||
|
@ -33,7 +36,7 @@ class Config(object):
|
|||
usage="usage: %prog [options] [file_or_dir] [file_or_dir] [...]",
|
||||
processopt=self._processopt,
|
||||
)
|
||||
self.pluginmanager = py.test._PluginManager()
|
||||
self.pluginmanager = PluginManager()
|
||||
self._conftest = Conftest(onimport=self._onimportconftest)
|
||||
self.hook = self.pluginmanager.hook
|
||||
|
||||
|
@ -178,7 +181,7 @@ class Config(object):
|
|||
This function gets invoked during testing session initialization.
|
||||
"""
|
||||
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:
|
||||
group._addoption_instance(opt)
|
||||
return self.option
|
||||
|
@ -296,6 +299,11 @@ def gettopdir(args):
|
|||
else:
|
||||
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()
|
||||
|
|
|
@ -47,23 +47,33 @@ class Exit(KeyboardInterrupt):
|
|||
# exposed helper methods
|
||||
|
||||
def exit(msg):
|
||||
""" exit testing process immediately. """
|
||||
""" exit testing process as if KeyboardInterrupt was triggered. """
|
||||
__tracebackhide__ = True
|
||||
raise Exit(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
|
||||
raise Skipped(msg=msg)
|
||||
|
||||
def fail(msg="unknown failure"):
|
||||
""" fail with the given Message. """
|
||||
def fail(msg=""):
|
||||
""" explicitely fail this executing test with the given Message. """
|
||||
__tracebackhide__ = True
|
||||
raise Failed(msg=msg)
|
||||
|
||||
def raises(ExpectedException, *args, **kwargs):
|
||||
""" raise AssertionError, if target code does not raise the expected
|
||||
exception.
|
||||
""" if args[0] is callable: raise AssertionError if calling it with
|
||||
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
|
||||
assert args
|
||||
|
@ -95,7 +105,10 @@ def raises(ExpectedException, *args, **kwargs):
|
|||
expr=args, expected=ExpectedException)
|
||||
|
||||
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
|
||||
try:
|
||||
mod = __import__(modname, None, None, ['__doc__'])
|
||||
|
@ -114,6 +127,7 @@ def importorskip(modname, minversion=None):
|
|||
return mod
|
||||
|
||||
|
||||
|
||||
# exitcodes for the command line
|
||||
EXIT_OK = 0
|
||||
EXIT_TESTSFAILED = 1
|
||||
|
|
|
@ -15,8 +15,6 @@ def check_old_use(mod, modname):
|
|||
assert not hasattr(mod, clsname), (mod, clsname)
|
||||
|
||||
class PluginManager(object):
|
||||
class Error(Exception):
|
||||
"""signals a plugin specific error."""
|
||||
def __init__(self):
|
||||
self.registry = Registry()
|
||||
self._name2plugin = {}
|
||||
|
@ -157,6 +155,7 @@ class PluginManager(object):
|
|||
dic = self.call_plugin(plugin, "pytest_namespace", {}) or {}
|
||||
for name, value in dic.items():
|
||||
setattr(py.test, name, value)
|
||||
py.test.__all__.append(name)
|
||||
if hasattr(self, '_config'):
|
||||
self.call_plugin(plugin, "pytest_addoption",
|
||||
{'parser': self._config._parser})
|
||||
|
|
|
@ -78,16 +78,18 @@ tests::
|
|||
import py
|
||||
|
||||
def pytest_namespace():
|
||||
return {'mark': Mark()}
|
||||
return {'mark': MarkGenerator()}
|
||||
|
||||
|
||||
class Mark(object):
|
||||
class MarkGenerator:
|
||||
""" 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):
|
||||
if name[0] == "_":
|
||||
raise AttributeError(name)
|
||||
return MarkerDecorator(name)
|
||||
return MarkDecorator(name)
|
||||
|
||||
class MarkerDecorator:
|
||||
class MarkDecorator:
|
||||
""" decorator for setting function attributes. """
|
||||
def __init__(self, name):
|
||||
self.markname = name
|
||||
|
@ -97,15 +99,17 @@ class MarkerDecorator:
|
|||
def __repr__(self):
|
||||
d = self.__dict__.copy()
|
||||
name = d.pop('markname')
|
||||
return "<MarkerDecorator %r %r>" %(name, d)
|
||||
return "<MarkDecorator %r %r>" %(name, d)
|
||||
|
||||
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 len(args) == 1 and hasattr(args[0], '__call__'):
|
||||
func = args[0]
|
||||
holder = getattr(func, self.markname, 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)
|
||||
else:
|
||||
holder.kwargs.update(self.kwargs)
|
||||
|
@ -116,7 +120,7 @@ class MarkerDecorator:
|
|||
self.kwargs.update(kwargs)
|
||||
return self
|
||||
|
||||
class Marker:
|
||||
class MarkInfo:
|
||||
def __init__(self, name, args, kwargs):
|
||||
self._name = name
|
||||
self.args = args
|
||||
|
@ -129,7 +133,7 @@ class Marker:
|
|||
raise AttributeError(name)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Marker %r args=%r kwargs=%r>" % (
|
||||
return "<MarkInfo %r args=%r kwargs=%r>" % (
|
||||
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
|
||||
for parent in [x for x in (mod, cls) if x]:
|
||||
marker = getattr(parent.obj, 'pytestmark', None)
|
||||
if isinstance(marker, MarkerDecorator):
|
||||
if isinstance(marker, MarkDecorator):
|
||||
marker(func)
|
||||
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
|
||||
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.
|
||||
|
||||
The need for skipping a test is usually connected to a condition.
|
||||
|
@ -121,6 +121,7 @@ within test or setup code. Example::
|
|||
|
||||
import py
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
expr, result = evalexpression(item, 'skipif')
|
||||
if result:
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import py
|
||||
from py.plugin.pytest_mark import Mark
|
||||
from py.plugin.pytest_mark import MarkGenerator as Mark
|
||||
|
||||
class TestMark:
|
||||
def test_pytest_mark_notcallable(self):
|
||||
mark = Mark()
|
||||
py.test.raises(TypeError, "mark()")
|
||||
py.test.raises((AttributeError, TypeError), "mark()")
|
||||
|
||||
def test_pytest_mark_bare(self):
|
||||
mark = Mark()
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import py
|
||||
import sys, py
|
||||
|
||||
class TestGeneralUsage:
|
||||
def test_config_error(self, testdir):
|
||||
|
@ -74,3 +74,18 @@ class TestGeneralUsage:
|
|||
assert result.stderr.fnmatch_lines([
|
||||
"*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
|
||||
def test_hello():
|
||||
assert hello == "world"
|
||||
assert 'hello' in py.test.__all__
|
||||
""")
|
||||
result = testdir.runpytest(p)
|
||||
result.stdout.fnmatch_lines([
|
||||
|
|
Loading…
Reference in New Issue