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