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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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