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:
holger krekel 2009-12-29 16:29:48 +01:00
parent 080fd2880e
commit db21cac694
13 changed files with 98 additions and 36 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +1 @@
""" versatile unit-testing tool + libraries """
""" assertion and py.test helper API."""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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