[py-svn] r56913 - in py/branch/event/py: . test2 test2/testing
hpk at codespeak.net
hpk at codespeak.net
Sat Aug 2 10:52:32 CEST 2008
Author: hpk
Date: Sat Aug 2 10:52:28 2008
New Revision: 56913
Added:
py/branch/event/py/test2/pycollect.py
- copied, changed from r56912, py/branch/event/py/test2/collect.py
Removed:
py/branch/event/py/test2/doctest.py
Modified:
py/branch/event/py/__init__.py
py/branch/event/py/test2/collect.py
py/branch/event/py/test2/item.py
py/branch/event/py/test2/testing/test_collect.py
py/branch/event/py/test2/testing/test_doctest.py
Log:
split python and non-python code realted collection nodes into two files,
merge small item and doctest files, so that now there is collect.py and pycollect.py
Modified: py/branch/event/py/__init__.py
==============================================================================
--- py/branch/event/py/__init__.py (original)
+++ py/branch/event/py/__init__.py Sat Aug 2 10:52:28 2008
@@ -50,23 +50,23 @@
# for customization of collecting/running tests
'test.collect.Collector' : ('./test/collect.py', 'Collector'),
- 'test2.collect.Collector' : ('./test2/collect.py', 'Collector'),
'test.collect.Directory' : ('./test/collect.py', 'Directory'),
- 'test2.collect.Directory' : ('./test2/collect.py', 'Directory'),
'test.collect.Module' : ('./test/collect.py', 'Module'),
- 'test2.collect.Module' : ('./test2/collect.py', 'Module'),
'test.collect.DoctestFile' : ('./test/collect.py', 'DoctestFile'),
- 'test2.collect.DoctestFile' : ('./test2/collect.py', 'DoctestFile'),
'test.collect.Class' : ('./test/collect.py', 'Class'),
- 'test2.collect.Class' : ('./test2/collect.py', 'Class'),
'test.collect.Instance' : ('./test/collect.py', 'Instance'),
- 'test2.collect.Instance' : ('./test2/collect.py', 'Instance'),
'test.collect.Generator' : ('./test/collect.py', 'Generator'),
- 'test2.collect.Generator' : ('./test2/collect.py', 'Generator'),
'test.collect.Item' : ('./test/item.py', 'Item'),
- 'test2.collect.Item' : ('./test2/item.py', 'Item'),
'test.collect.Function' : ('./test/item.py', 'Function'),
- 'test2.collect.Function' : ('./test2/item.py', 'Function'),
+ 'test2.collect.Collector' : ('./test2/collect.py', 'Collector'),
+ 'test2.collect.Directory' : ('./test2/collect.py', 'Directory'),
+ 'test2.collect.Module' : ('./test2/pycollect.py', 'Module'),
+ 'test2.collect.DoctestFile' : ('./test2/pycollect.py', 'DoctestFile'),
+ 'test2.collect.Class' : ('./test2/pycollect.py', 'Class'),
+ 'test2.collect.Instance' : ('./test2/pycollect.py', 'Instance'),
+ 'test2.collect.Generator' : ('./test2/pycollect.py', 'Generator'),
+ 'test2.collect.Item' : ('./test2/collect.py', 'Item'),
+ 'test2.collect.Function' : ('./test2/pycollect.py', 'Function'),
# thread related API (still in early design phase)
'_thread.WorkerPool' : ('./thread/pool.py', 'WorkerPool'),
Modified: py/branch/event/py/test2/collect.py
==============================================================================
--- py/branch/event/py/test2/collect.py (original)
+++ py/branch/event/py/test2/collect.py Sat Aug 2 10:52:28 2008
@@ -23,9 +23,7 @@
...
"""
-from __future__ import generators
import py
-from py.__.test2 import pypresent
def configproperty(name):
def fget(self):
@@ -279,249 +277,24 @@
name2items[name] = res
return res
-class PyobjMixin(object):
- def obj():
- def fget(self):
- try:
- return self._obj
- except AttributeError:
- self._obj = obj = self._getobj()
- return obj
- def fset(self, value):
- self._obj = value
- return property(fget, fset, None, "underlying python object")
- obj = obj()
-
- def _getobj(self):
- return getattr(self.parent.obj, self.name)
-
- def repr_path(self):
- fspath = pypresent.getrelpath(self._config.topdir, self.fspath)
- modpath = pypresent.getmodpath(self)
- return (fspath, modpath)
-
-
-class PyCollectorMixin(PyobjMixin, Collector):
- def funcnamefilter(self, name):
- return name.startswith('test')
- def classnamefilter(self, name):
- return name.startswith('Test')
-
- def _buildname2items(self):
- # NB. we avoid random getattrs and peek in the __dict__ instead
- d = {}
- dicts = [getattr(self.obj, '__dict__', {})]
- for basecls in py.std.inspect.getmro(self.obj.__class__):
- dicts.append(basecls.__dict__)
- seen = {}
- for dic in dicts:
- for name, obj in dic.items():
- if name in seen:
- continue
- seen[name] = True
- res = self.makeitem(name, obj)
- if res is not None:
- d[name] = res
- return d
-
- def makeitem(self, name, obj, usefilters=True):
- if (not usefilters or self.classnamefilter(name)) and \
- py.std.inspect.isclass(obj):
- return self.Class(name, parent=self)
- elif (not usefilters or self.funcnamefilter(name)) and callable(obj):
- if obj.func_code.co_flags & 32: # generator function
- return self.Generator(name, parent=self)
- else:
- return self.Function(name, parent=self)
-
- def _prepare(self):
- if not hasattr(self, '_name2items'):
- ex = getattr(self, '_name2items_exception', None)
- if ex is not None:
- raise ex[0], ex[1], ex[2]
- try:
- self._name2items = self._buildname2items()
- except (SystemExit, KeyboardInterrupt):
- raise
- except:
- self._name2items_exception = py.std.sys.exc_info()
- raise
-
- def listdir(self):
- self._prepare()
- itemlist = self._name2items.values()
- itemlist.sort()
- return [x.name for x in itemlist]
-
- def join(self, name):
- self._prepare()
- return self._name2items.get(name, None)
-
-class Module(FSCollector, PyCollectorMixin):
- _stickyfailure = None
-
+from py.__.test2.runner import basic_run_report, forked_run_report
+class Item(Node):
+ """ a basic test item. """
def repr_path(self):
- return (self.fspath, None)
-
- def listdir(self):
- if getattr(self.obj, 'disabled', 0):
- return []
- return super(Module, self).listdir()
-
- def join(self, name):
- res = super(Module, self).join(name)
- if res is None:
- attr = getattr(self.obj, name, None)
- if attr is not None:
- res = self.makeitem(name, attr, usefilters=False)
- return res
+ """ return (filepath, testpath) tuple with
+ filepath: a fspath representation (None if not applicable)
+ testpath: a path representation of the test path.
- def _getobj(self):
- failure = self._stickyfailure
- if failure is not None:
- raise failure[0], failure[1], failure[2]
- try:
- self._obj = obj = self.fspath.pyimport()
- except KeyboardInterrupt:
- raise
- except:
- self._stickyfailure = py.std.sys.exc_info()
- raise
- return self._obj
-
- def setup(self):
- if hasattr(self.obj, 'setup_module'):
- self.obj.setup_module(self.obj)
-
- def teardown(self):
- if hasattr(self.obj, 'teardown_module'):
- self.obj.teardown_module(self.obj)
-
-
-class Class(PyCollectorMixin, Collector):
-
- def listdir(self):
- if getattr(self.obj, 'disabled', 0):
- return []
- return ["()"]
-
- def join(self, name):
- assert name == '()'
- return self.Instance(name, self)
-
- def setup(self):
- setup_class = getattr(self.obj, 'setup_class', None)
- if setup_class is not None:
- setup_class = getattr(setup_class, 'im_func', setup_class)
- setup_class(self.obj)
-
- def teardown(self):
- teardown_class = getattr(self.obj, 'teardown_class', None)
- if teardown_class is not None:
- teardown_class = getattr(teardown_class, 'im_func', teardown_class)
- teardown_class(self.obj)
-
- def _getsortvalue(self):
- # try to locate the class in the source - not nice, but probably
- # the most useful "solution" that we have
- try:
- file = (py.std.inspect.getsourcefile(self.obj) or
- py.std.inspect.getfile(self.obj))
- if not file:
- raise IOError
- lines, lineno = py.std.inspect.findsource(self.obj)
- return py.path.local(file), lineno
- except IOError:
- pass
-
-class Instance(PyCollectorMixin, Collector):
- def _getobj(self):
- return self.parent.obj()
- def Function(self):
- return getattr(self.obj, 'Function',
- Collector.Function.__get__(self)) # XXX for python 2.2
- def _keywords(self):
- return []
- Function = property(Function)
-
-
-class FunctionMixin(PyobjMixin):
- """ mixin for the code common to Function and Generator.
- """
- _sortvalue = None
- def _getsortvalue(self):
- if self._sortvalue is None:
- code = py.code.Code(self.obj)
- self._sortvalue = code.path, code.firstlineno
- return self._sortvalue
-
- def setup(self):
- """ perform setup for this test function. """
- if hasattr(self.obj, 'im_self'):
- name = 'setup_method'
- else:
- name = 'setup_function'
- obj = self.parent.obj
- setup_func_or_method = getattr(obj, name, None)
- if setup_func_or_method is not None:
- return setup_func_or_method(self.obj)
-
- def teardown(self):
- """ perform teardown for this test function. """
- if hasattr(self.obj, 'im_self'):
- name = 'teardown_method'
- else:
- name = 'teardown_function'
- obj = self.parent.obj
- teardown_func_or_meth = getattr(obj, name, None)
- if teardown_func_or_meth is not None:
- return teardown_func_or_meth(self.obj)
-
- def prunetraceback(self, traceback):
- if not self._config.option.fulltrace:
- code = py.code.Code(self.obj)
- path, firstlineno = code.path, code.firstlineno
- ntraceback = traceback.cut(path=path, firstlineno=firstlineno)
- if ntraceback == traceback:
- ntraceback = ntraceback.cut(path=path)
- traceback = ntraceback.filter()
- return traceback
-
-class Generator(FunctionMixin, PyCollectorMixin, Collector):
- def listdir(self):
- self._prepare()
- itemlist = self._name2items
- return [itemlist["[%d]" % num].name for num in xrange(len(itemlist))]
-
- def _buildname2items(self):
- d = {}
- # XXX test generators are collectors yet participate in
- # the test-item setup and teardown protocol
- # if not for this we could probably avoid global setupstate
- self._setupstate.prepare(self)
- for i, x in py.builtin.enumerate(self.obj()):
- call, args = self.getcallargs(x)
- if not callable(call):
- raise TypeError("yielded test %r not callable" %(call,))
- name = "[%d]" % i
- d[name] = self.Function(name, self, args, callobj=call)
- return d
-
- def getcallargs(self, obj):
- if isinstance(obj, (tuple, list)):
- call, args = obj[0], obj[1:]
- else:
- call, args = obj, ()
- return call, args
-
-class DoctestFile(PyCollectorMixin, FSCollector):
- def listdir(self):
- return [self.fspath.basename]
+ this is used by reporters.
+ """
+ xxx
- def join(self, name):
- from py.__.test2.doctest import DoctestText
- if name == self.fspath.basename:
- item = DoctestText(self.fspath.basename, parent=self)
- item._content = self.fspath.read()
- return item
+ def repr_run(self, excinfo):
+ """ return string failure represenation for this item.
+ """
+ xxx
+ def _getrunner(self):
+ if self._config.option.boxed:
+ return forked_run_report
+ return basic_run_report
Deleted: /py/branch/event/py/test2/doctest.py
==============================================================================
--- /py/branch/event/py/test2/doctest.py Sat Aug 2 10:52:28 2008
+++ (empty file)
@@ -1,33 +0,0 @@
-import py
-
-class DoctestText(py.test2.collect.Item):
-
- def _setcontent(self, content):
- self._content = content
-
- #def buildname2items(self):
- # parser = py.compat.doctest.DoctestParser()
- # l = parser.get_examples(self._content)
- # d = {}
- # globs = {}
- # locs
- # for i, example in py.builtin.enumerate(l):
- # ex = ExampleItem(example)
- # d[str(i)] = ex
-
- def run(self):
- mod = py.std.types.ModuleType(self.name)
- #for line in s.split('\n'):
- # if line.startswith(prefix):
- # exec py.code.Source(line[len(prefix):]).compile() in mod.__dict__
- # line = ""
- # else:
- # l.append(line)
- self.execute(mod, self._content)
-
- def execute(self, mod, docstring):
- mod.__doc__ = docstring
- failed, tot = py.compat.doctest.testmod(mod, verbose=1)
- if failed:
- py.test2.fail("doctest %s: %s failed out of %s" %(
- self.fspath, failed, tot))
Modified: py/branch/event/py/test2/item.py
==============================================================================
--- py/branch/event/py/test2/item.py (original)
+++ py/branch/event/py/test2/item.py Sat Aug 2 10:52:28 2008
@@ -1,45 +1,4 @@
import py
from py.__.test2.collect import FunctionMixin, Node
-from py.__.test2.runner import basic_run_report, forked_run_report
from py.__.test2 import pypresent
-_dummy = object()
-
-class Item(Node):
- def repr_path(self):
- """ return (filepath, testpath) tuple with
- filepath: a fspath representation (None if not applicable)
- testpath: a path representation of the test path.
-
- this is used by reporters.
- """
- xxx
-
- def repr_run(self, excinfo):
- """ return string failure represenation for this item.
- """
- xxx
-
- def _getrunner(self):
- if self._config.option.boxed:
- return forked_run_report
- return basic_run_report
-
-class Function(FunctionMixin, Item):
- """ a Function Item is responsible for setting up
- and executing a Python callable test object.
- """
- def __init__(self, name, parent, args=(), callobj=_dummy):
- super(Function, self).__init__(name, parent)
- self._args = args
- if callobj is not _dummy:
- self._obj = callobj
-
- def execute(self):
- """ execute the given test function. """
- self.obj(*self._args)
-
- def repr_run(self, runinfo):
- """ return a textual representation of run info. """
- return pypresent.python_repr_run(runinfo)
-
Copied: py/branch/event/py/test2/pycollect.py (from r56912, py/branch/event/py/test2/collect.py)
==============================================================================
--- py/branch/event/py/test2/collect.py (original)
+++ py/branch/event/py/test2/pycollect.py Sat Aug 2 10:52:28 2008
@@ -1,283 +1,9 @@
"""
-Collect test items at filesystem and python module levels.
-
-Collectors and test items form a tree. The difference
-between a collector and a test item as seen from the session
-is smalll. Collectors usually return a list of child
-collectors/items whereas items usually return None
-indicating a successful test run.
-
-The is a schematic example of a tree of collectors and test items::
-
- Directory
- Module
- Class
- Instance
- Function
- Generator
- ...
- Function
- Generator
- Function
- Directory
- ...
-
+Python related Collect nodes.
"""
-from __future__ import generators
import py
-from py.__.test2 import pypresent
-
-def configproperty(name):
- def fget(self):
- #print "retrieving %r property from %s" %(name, self.fspath)
- return self._config.getvalue(name, self.fspath)
- return property(fget)
-
-class SetupState(object):
- """ shared state for setting up/tearing down tests. """
- def __init__(self):
- self.stack = []
-
- def teardown_all(self):
- while self.stack:
- col = self.stack.pop()
- col.teardown()
-
- def teardown_exact(self, item):
- if self.stack[-1] == item:
- col = self.stack.pop()
- col.teardown()
-
- def prepare(self, colitem):
- """ setup objects along the collector chain to the test-method
- Teardown any unneccessary previously setup objects."""
-
- needed_collectors = colitem.listchain()
- while self.stack:
- if self.stack == needed_collectors[:len(self.stack)]:
- break
- col = self.stack.pop()
- col.teardown()
- for col in needed_collectors[len(self.stack):]:
- #print "setting up", col
- col.setup()
- self.stack.append(col)
-
-class Node(object):
- """ base class for Nodes in the collection tree.
- Nodes with Children are "Collectors" and
- leaves are runnable Test Items.
- """
- _setupstate = SetupState()
- def __init__(self, name, parent=None, config=None):
- self.name = name
- self.parent = parent
- if config is None:
- config = getattr(parent, '_config')
- self._config = config
- self.fspath = getattr(parent, 'fspath', None)
-
- def __getstate__(self):
- config = self._config
- return (config, config.get_collector_trail(self))
- def __setstate__(self, (config, trail)):
- newnode = config._getcollector(trail)
- self.__dict__.update(newnode.__dict__)
-
- def __repr__(self):
- return "<%s %r>" %(self.__class__.__name__, self.name)
-
- # methods for ordering nodes
-
- def __eq__(self, other):
- if not isinstance(other, Node):
- return False
- return self.name == other.name and self.parent == other.parent
-
- def __ne__(self, other):
- return not self == other
-
- def __hash__(self):
- return hash((self.name, self.parent))
-
- def __cmp__(self, other):
- if not isinstance(other, Node):
- return -1
- s1 = self._getsortvalue()
- s2 = other._getsortvalue()
- return cmp(s1, s2)
-
- def setup(self):
- pass
-
- def teardown(self):
- pass
-
- def listchain(self):
- """ return list of all parent collectors up to self. """
- l = [self]
- while 1:
- x = l[-1]
- if x.parent is not None:
- l.append(x.parent)
- else:
- l.reverse()
- return l
-
- def listnames(self):
- return [x.name for x in self.listchain()]
-
- def _getitembynames(self, namelist):
- cur = self
- for name in namelist:
- if name:
- next = cur.join(name)
- if next is None:
- existingnames = cur.listdir()
- msg = ("Collector %r does not have name %r "
- "existing names are: %s" %
- (cur, name, existingnames))
- raise AssertionError(msg)
- cur = next
- return cur
-
- def _keywords(self):
- return [self.name]
-
- def _skipbykeyword(self, keywordexpr):
- """ return True if they given keyword expression means to
- skip this collector/item.
- """
- if not keywordexpr:
- return
- chain = self.listchain()
- for key in filter(None, keywordexpr.split()):
- eor = key[:1] == '-'
- if eor:
- key = key[1:]
- if not (eor ^ self._matchonekeyword(key, chain)):
- return True
-
- def _matchonekeyword(self, key, chain):
- elems = key.split(".")
- # XXX O(n^2), anyone cares?
- chain = [item._keywords() for item in chain if item._keywords()]
- for start, _ in enumerate(chain):
- if start + len(elems) > len(chain):
- return False
- for num, elem in enumerate(elems):
- for keyword in chain[num + start]:
- ok = False
- if elem in keyword:
- ok = True
- break
- if not ok:
- break
- if num == len(elems) - 1 and ok:
- return True
- return False
-
- def _getsortvalue(self):
- return self.name
-
- def _get_collector_trail(self):
- """ Shortcut
- """
- return self._config.get_collector_trail(self)
-
- def prunetraceback(self, traceback):
- return traceback
-
-class Collector(Node):
- """
- Collector instances generate children through
- their listdir() and join() methods and thus
- form a tree. attributes::
-
- parent: attribute pointing to the parent collector
- (or None if this is the root collector)
- name: basename of this collector object
- """
- Module = configproperty('Module')
- DoctestFile = configproperty('DoctestFile')
- Directory = configproperty('Directory')
- Class = configproperty('Class')
- Instance = configproperty('Instance')
- Function = configproperty('Function')
- Generator = configproperty('Generator')
-
- def run(self):
- """ deprecated: use listdir(). """
- py.std.warnings.warn("deprecated: use listdir()", category=DeprecationWarning)
- return self.listdir()
-
- def multijoin(self, namelist):
- """ return a list of colitems for the given namelist. """
- return [self.join(name) for name in namelist]
-
- def listdir(self):
- """ returns a list of names available from this collector.
- You can return an empty list. Callers of this method
- must take care to catch exceptions properly.
- """
- raise NotImplementedError("abstract")
-
- def join(self, name):
- """ return a child collector or item for the given name.
- If the return value is None there is no such child.
- """
- raise NotImplementedError("abstract")
-
-class FSCollector(Collector):
- def __init__(self, fspath, parent=None, config=None):
- fspath = py.path.local(fspath)
- super(FSCollector, self).__init__(fspath.basename, parent, config=config)
- self.fspath = fspath
-
-class Directory(FSCollector):
- def filefilter(self, path):
- if path.check(file=1):
- b = path.purebasename
- ext = path.ext
- return (b.startswith('test_') or
- b.endswith('_test')) and ext in ('.txt', '.py')
-
- def recfilter(self, path):
- if path.check(dir=1, dotfile=0):
- return path.basename not in ('CVS', '_darcs', '{arch}')
-
- def repr_path(self):
- return (self.fspath, None)
-
- def listdir(self):
- files = []
- dirs = []
- for p in self.fspath.listdir():
- if self.filefilter(p):
- files.append(p.basename)
- elif self.recfilter(p):
- dirs.append(p.basename)
- files.sort()
- dirs.sort()
- return files + dirs
-
- def join(self, name):
- name2items = self.__dict__.setdefault('_name2items', {})
- try:
- res = name2items[name]
- except KeyError:
- p = self.fspath.join(name)
- res = None
- if p.check(file=1):
- if p.ext == '.py':
- res = self.Module(p, parent=self)
- elif p.ext == '.txt':
- res = self.DoctestFile(p, parent=self)
- elif p.check(dir=1):
- Directory = py.test2.config.getvalue('Directory', p)
- res = Directory(p, parent=self)
- name2items[name] = res
- return res
+from py.__.test2.collect import Collector, FSCollector, Item
+from py.__.test2 import pypresent
class PyobjMixin(object):
def obj():
@@ -300,7 +26,6 @@
modpath = pypresent.getmodpath(self)
return (fspath, modpath)
-
class PyCollectorMixin(PyobjMixin, Collector):
def funcnamefilter(self, name):
return name.startswith('test')
@@ -514,14 +239,63 @@
call, args = obj, ()
return call, args
+_dummy = object()
+class Function(FunctionMixin, Item):
+ """ a Function Item is responsible for setting up
+ and executing a Python callable test object.
+ """
+ def __init__(self, name, parent, args=(), callobj=_dummy):
+ super(Function, self).__init__(name, parent)
+ self._args = args
+ if callobj is not _dummy:
+ self._obj = callobj
+
+ def execute(self):
+ """ execute the given test function. """
+ self.obj(*self._args)
+
+ def repr_run(self, runinfo):
+ """ return a textual representation of run info. """
+ return pypresent.python_repr_run(runinfo)
+
class DoctestFile(PyCollectorMixin, FSCollector):
def listdir(self):
return [self.fspath.basename]
def join(self, name):
- from py.__.test2.doctest import DoctestText
if name == self.fspath.basename:
item = DoctestText(self.fspath.basename, parent=self)
item._content = self.fspath.read()
return item
+class DoctestText(Item):
+ def _setcontent(self, content):
+ self._content = content
+
+ #def buildname2items(self):
+ # parser = py.compat.doctest.DoctestParser()
+ # l = parser.get_examples(self._content)
+ # d = {}
+ # globs = {}
+ # locs
+ # for i, example in py.builtin.enumerate(l):
+ # ex = ExampleItem(example)
+ # d[str(i)] = ex
+
+ def run(self):
+ mod = py.std.types.ModuleType(self.name)
+ #for line in s.split('\n'):
+ # if line.startswith(prefix):
+ # exec py.code.Source(line[len(prefix):]).compile() in mod.__dict__
+ # line = ""
+ # else:
+ # l.append(line)
+ self.execute(mod, self._content)
+
+ def execute(self, mod, docstring):
+ mod.__doc__ = docstring
+ failed, tot = py.compat.doctest.testmod(mod, verbose=1)
+ if failed:
+ py.test2.fail("doctest %s: %s failed out of %s" %(
+ self.fspath, failed, tot))
+
Modified: py/branch/event/py/test2/testing/test_collect.py
==============================================================================
--- py/branch/event/py/test2/testing/test_collect.py (original)
+++ py/branch/event/py/test2/testing/test_collect.py Sat Aug 2 10:52:28 2008
@@ -1,10 +1,10 @@
from __future__ import generators
import py
from py.__.test2 import repevent, outcome
-from py.__.test2.doctest import DoctestText
import setupdata, suptest
from py.__.test2.conftesthandle import Conftest
from py.__.test2.collect import SetupState
+from py.__.test2.pycollect import DoctestText
class DummyConfig:
def __init__(self):
Modified: py/branch/event/py/test2/testing/test_doctest.py
==============================================================================
--- py/branch/event/py/test2/testing/test_doctest.py (original)
+++ py/branch/event/py/test2/testing/test_doctest.py Sat Aug 2 10:52:28 2008
@@ -1,6 +1,6 @@
import py
-from py.__.test2.doctest import DoctestText
+from py.__.test2.pycollect import DoctestText
from py.__.test2.outcome import Skipped, Failed, Passed, Outcome
def test_simple_docteststring():
More information about the pytest-commit
mailing list