1 new commit in pytest:
https://bitbucket.org/pytest-dev/pytest/commits/65a7f821cb6e/
Changeset: 65a7f821cb6e
User: flub
Date: 2015-04-11 16:07:37+00:00
Summary: Some docstrings for the pytester plugin
These aren't quite complete but are a jolly good start anyway. It
seems better to commit this now then leave it lingering until it
gets lost.
Affected #: 1 file
diff -r 45921b2e640011d8f169a7f13fd79218f88c7495 -r
65a7f821cb6e582ef5b295e458dae90508e7a4c8 _pytest/pytester.py
--- a/_pytest/pytester.py
+++ b/_pytest/pytester.py
@@ -50,6 +50,13 @@
class HookRecorder:
+ """Record all hooks called in a plugin manager.
+
+ This wraps all the hook calls in the plugin manager, recording
+ each call before propagating the normal calls.
+
+ """
+
def __init__(self, pluginmanager):
self._pluginmanager = pluginmanager
self.calls = []
@@ -180,6 +187,20 @@
rex_outcome = re.compile("(\d+) (\w+)")
class RunResult:
+ """The result of running a command.
+
+ Attributes:
+
+ :ret: The return value.
+ :outlines: List of lines captured from stdout.
+ :errlines: List of lines captures from stderr.
+ :stdout: LineMatcher of stdout, use ``stdout.str()`` to
+ reconstruct stdout or the commonly used
+ ``stdout.fnmatch_lines()`` method.
+ :stderrr: LineMatcher of stderr.
+ :duration: Duration in seconds.
+
+ """
def __init__(self, ret, outlines, errlines, duration):
self.ret = ret
self.outlines = outlines
@@ -199,6 +220,26 @@
return d
class TmpTestdir:
+ """Temporary test directory with tools to test/run py.test itself.
+
+ This is based on the ``tmpdir`` fixture but provides a number of
+ methods which aid with testing py.test itself. Unless
+ :py:meth:`chdir` is used all methods will use :py:attr:`tmpdir` as
+ current working directory.
+
+ Attributes:
+
+ :tmpdir: The :py:class:`py.path.local` instance of the temporary
+ directory.
+
+ :plugins: A list of plugins to use with :py:meth:`parseconfig` and
+ :py:meth:`runpytest`. Initially this is an empty list but
+ plugins can be added to the list. The type of items to add to
+ the list depend on the method which uses them so refer to them
+ for details.
+
+ """
+
def __init__(self, request):
self.request = request
self.Config = request.config.__class__
@@ -221,6 +262,14 @@
return "<TmpTestdir %r>" % (self.tmpdir,)
def finalize(self):
+ """Clean up global state artifacts.
+
+ Some methods modify the global interpreter state and this
+ tries to clean this up. It does not remove the temporary
+ directlry however so it can be looked at after the test run
+ has finished.
+
+ """
for p in self._syspathremove:
sys.path.remove(p)
if hasattr(self, '_olddir'):
@@ -233,12 +282,18 @@
del sys.modules[name]
def make_hook_recorder(self, pluginmanager):
+ """Create a new :py:class:`HookRecorder` for a PluginManager."""
assert not hasattr(pluginmanager, "reprec")
pluginmanager.reprec = reprec = HookRecorder(pluginmanager)
self.request.addfinalizer(reprec.finish_recording)
return reprec
def chdir(self):
+ """Cd into the temporary directory.
+
+ This is done automatically upon instantiation.
+
+ """
old = self.tmpdir.chdir()
if not hasattr(self, '_olddir'):
self._olddir = old
@@ -267,42 +322,82 @@
ret = p
return ret
+ def makefile(self, ext, *args, **kwargs):
+ """Create a new file in the testdir.
- def makefile(self, ext, *args, **kwargs):
+ ext: The extension the file should use, including the dot.
+ E.g. ".py".
+
+ args: All args will be treated as strings and joined using
+ newlines. The result will be written as contents to the
+ file. The name of the file will be based on the test
+ function requesting this fixture.
+ E.g. "testdir.makefile('.txt', 'line1', 'line2')"
+
+ kwargs: Each keyword is the name of a file, while the value of
+ it will be written as contents of the file.
+ E.g. "testdir.makefile('.ini', pytest='[pytest]\naddopts=-rs\n')"
+
+ """
return self._makefile(ext, args, kwargs)
def makeconftest(self, source):
+ """Write a contest.py file with 'source' as contents."""
return self.makepyfile(conftest=source)
def makeini(self, source):
+ """Write a tox.ini file with 'source' as contents."""
return self.makefile('.ini', tox=source)
def getinicfg(self, source):
+ """Return the pytest section from the tox.ini config file."""
p = self.makeini(source)
return py.iniconfig.IniConfig(p)['pytest']
def makepyfile(self, *args, **kwargs):
+ """Shortcut for .makefile() with a .py extension."""
return self._makefile('.py', args, kwargs)
def maketxtfile(self, *args, **kwargs):
+ """Shortcut for .makefile() with a .txt extension."""
return self._makefile('.txt', args, kwargs)
def syspathinsert(self, path=None):
+ """Prepend a directory to sys.path, defaults to :py:attr:`tmpdir`.
+
+ This is undone automatically after the test.
+ """
if path is None:
path = self.tmpdir
sys.path.insert(0, str(path))
self._syspathremove.append(str(path))
def mkdir(self, name):
+ """Create a new (sub)directory."""
return self.tmpdir.mkdir(name)
def mkpydir(self, name):
+ """Create a new python package.
+
+ This creates a (sub)direcotry with an empty ``__init__.py``
+ file so that is recognised as a python package.
+
+ """
p = self.mkdir(name)
p.ensure("__init__.py")
return p
Session = Session
def getnode(self, config, arg):
+ """Return the collection node of a file.
+
+ :param config: :py:class:`_pytest.config.Config` instance, see
+ :py:meth:`parseconfig` and :py:meth:`parseconfigure` to
+ create the configuration.
+
+ :param arg: A :py:class:`py.path.local` instance of the file.
+
+ """
session = Session(config)
assert '::' not in str(arg)
p = py.path.local(arg)
@@ -312,6 +407,15 @@
return res
def getpathnode(self, path):
+ """Return the collection node of a file.
+
+ This is like :py:meth:`getnode` but uses
+ :py:meth:`parseconfigure` to create the (configured) py.test
+ Config instance.
+
+ :param path: A :py:class:`py.path.local` instance of the file.
+
+ """
config = self.parseconfigure(path)
session = Session(config)
x = session.fspath.bestrelpath(path)
@@ -321,6 +425,12 @@
return res
def genitems(self, colitems):
+ """Generate all test items from a collection node.
+
+ This recurses into the collection node and returns a list of
+ all the test items contained within.
+
+ """
session = colitems[0].session
result = []
for colitem in colitems:
@@ -328,6 +438,14 @@
return result
def runitem(self, source):
+ """Run the "test_func" Item.
+
+ The calling test instance (the class which contains the test
+ method) must provide a ``.getrunner()`` method which should
+ return a runner which can run the test protocol for a single
+ item, like e.g. :py:func:`_pytest.runner.runtestprotocol`.
+
+ """
# used from runner functional tests
item = self.getitem(source)
# the test class where we are called from wants to provide the runner
@@ -336,11 +454,32 @@
return runner(item)
def inline_runsource(self, source, *cmdlineargs):
+ """Run a test module in process using ``pytest.main()``.
+
+ This run writes "source" into a temporary file and runs
+ ``pytest.main()`` on it, returning a :py:class:`HookRecorder`
+ instance for the result.
+
+ :param source: The source code of the test module.
+
+ :param cmdlineargs: Any extra command line arguments to use.
+
+ :return: :py:class:`HookRecorder` instance of the result.
+
+ """
p = self.makepyfile(source)
l = list(cmdlineargs) + [p]
return self.inline_run(*l)
def inline_runsource1(self, *args):
+ """Run a test module in process using ``pytest.main()``.
+
+ This behaves exactly like :py:meth:`inline_runsource` and
+ takes identical arguments. However the return value is a list
+ of the reports created by the pytest_runtest_logreport hook
+ during the run.
+
+ """
args = list(args)
source = args.pop()
p = self.makepyfile(source)
@@ -351,14 +490,45 @@
return reports[1]
def inline_genitems(self, *args):
+ """Run ``pytest.main(['--collectonly'])`` in-process.
+
+ Retuns a tuple of the collected items and a
+ :py:class:`HookRecorder` instance.
+
+ """
return self.inprocess_run(list(args) + ['--collectonly'])
def inprocess_run(self, args, plugins=()):
+ """Run ``pytest.main()`` in-process, return Items and a HookRecorder.
+
+ This runs the :py:func:`pytest.main` function to run all of
+ py.test inside the test process itself like
+ :py:meth:`inline_run`. However the return value is a tuple of
+ the collection items and a :py:class:`HookRecorder` instance.
+
+ """
rec = self.inline_run(*args, plugins=plugins)
items = [x.item for x in rec.getcalls("pytest_itemcollected")]
return items, rec
def inline_run(self, *args, **kwargs):
+ """Run ``pytest.main()`` in-process, returning a HookRecorder.
+
+ This runs the :py:func:`pytest.main` function to run all of
+ py.test inside the test process itself. This means it can
+ return a :py:class:`HookRecorder` instance which gives more
+ detailed results from then run then can be done by matching
+ stdout/stderr from :py:meth:`runpytest`.
+
+ :param args: Any command line arguments to pass to
+ :py:func:`pytest.main`.
+
+ :param plugin: (keyword-only) Extra plugin instances the
+ ``pytest.main()`` instance should use.
+
+ :return: A :py:class:`HookRecorder` instance.
+
+ """
rec = []
class Collect:
def pytest_configure(x, config):
@@ -372,6 +542,17 @@
return reprec
def parseconfig(self, *args):
+ """Return a new py.test Config instance from given commandline args.
+
+ This invokes the py.test bootstrapping code in _pytest.config
+ to create a new :py:class:`_pytest.core.PluginManager` and
+ call the pytest_cmdline_parse hook to create new
+ :py:class:`_pytest.config.Config` instance.
+
+ If :py:attr:`plugins` has been populated they should be plugin
+ modules which will be registered with the PluginManager.
+
+ """
args = [str(x) for x in args]
for x in args:
if str(x).startswith('--basetemp'):
@@ -392,12 +573,31 @@
return config
def parseconfigure(self, *args):
+ """Return a new py.test configured Config instance.
+
+ This returns a new :py:class:`_pytest.config.Config` instance
+ like :py:meth:`parseconfig`, but also calls the
+ pytest_configure hook.
+
+ """
config = self.parseconfig(*args)
config.do_configure()
self.request.addfinalizer(config.do_unconfigure)
return config
def getitem(self, source, funcname="test_func"):
+ """Return the test item for a test function.
+
+ This writes the source to a python file and runs py.test's
+ collection on the resulting module, returning the test item
+ for the requested function name.
+
+ :param source: The module source.
+
+ :param funcname: The name of the test function for which the
+ Item must be returned.
+
+ """
items = self.getitems(source)
for item in items:
if item.name == funcname:
@@ -406,10 +606,32 @@
funcname, source, items)
def getitems(self, source):
+ """Return all test items collected from the module.
+
+ This writes the source to a python file and runs py.test's
+ collection on the resulting module, returning all test items
+ contained within.
+
+ """
modcol = self.getmodulecol(source)
return self.genitems([modcol])
def getmodulecol(self, source, configargs=(), withinit=False):
+ """Return the module collection node for ``source``.
+
+ This writes ``source`` to a file using :py:meth:`makepyfile`
+ and then runs the py.test collection on it, returning the
+ collection node for the test module.
+
+ :param source: The source code of the module to collect.
+
+ :param configargs: Any extra arguments to pass to
+ :py:meth:`parseconfigure`.
+
+ :param withinit: Whether to also write a ``__init__.py`` file
+ to the temporarly directory to ensure it is a package.
+
+ """
kw = {self.request.function.__name__: py.code.Source(source).strip()}
path = self.makepyfile(**kw)
if withinit:
@@ -419,11 +641,30 @@
return node
def collect_by_name(self, modcol, name):
+ """Return the collection node for name from the module collection.
+
+ This will search a module collection node for a collection
+ node matching the given name.
+
+ :param modcol: A module collection node, see
+ :py:meth:`getmodulecol`.
+
+ :param name: The name of the node to return.
+
+ """
for colitem in modcol._memocollect():
if colitem.name == name:
return colitem
def popen(self, cmdargs, stdout, stderr, **kw):
+ """Invoke subprocess.Popen.
+
+ This calls subprocess.Popen making sure the current working
+ directory is the PYTHONPATH.
+
+ You probably want to use :py:meth:`run` instead.
+
+ """
env = os.environ.copy()
env['PYTHONPATH'] = os.pathsep.join(filter(None, [
str(os.getcwd()), env.get('PYTHONPATH', '')]))
@@ -432,6 +673,14 @@
stdout=stdout, stderr=stderr, **kw)
def run(self, *cmdargs):
+ """Run a command with arguments.
+
+ Run a process using subprocess.Popen saving the stdout and
+ stderr.
+
+ Returns a :py:class:`RunResult`.
+
+ """
return self._run(*cmdargs)
def _run(self, *cmdargs):
@@ -469,6 +718,14 @@
print("couldn't print to %s because of encoding" % (fp,))
def runpybin(self, scriptname, *args):
+ """Run a py.* tool with arguments.
+
+ This can realy only be used to run py.test, you probably want
+ :py:meth:`runpytest` instead.
+
+ Returns a :py:class:`RunResult`.
+
+ """
fullargs = self._getpybinargs(scriptname) + args
return self.run(*fullargs)
@@ -482,6 +739,16 @@
pytest.skip("cannot run %r with --no-tools-on-path" % scriptname)
def runpython(self, script, prepend=True):
+ """Run a python script.
+
+ If ``prepend`` is True then the directory from which the py
+ package has been imported will be prepended to sys.path.
+
+ Returns a :py:class:`RunResult`.
+
+ """
+ # XXX The prepend feature is probably not very useful since the
+ # split of py and pytest.
if prepend:
s = self._getsysprepend()
if s:
@@ -496,10 +763,23 @@
return s
def runpython_c(self, command):
+ """Run python -c "command", return a :py:class:`RunResult`."""
command = self._getsysprepend() + command
return self.run(sys.executable, "-c", command)
def runpytest(self, *args):
+ """Run py.test as a subprocess with given arguments.
+
+ Any plugins added to the :py:attr:`plugins` list will added
+ using the ``-p`` command line option. Addtionally
+ ``--basetemp`` is used put any temporary files and directories
+ in a numbered directory prefixed with "runpytest-" so they do
+ not conflict with the normal numberd pytest location for
+ temporary files and directories.
+
+ Returns a :py:class:`RunResult`.
+
+ """
p = py.path.local.make_numbered_dir(prefix="runpytest-",
keep=None, rootdir=self.tmpdir)
args = ('--basetemp=%s' % p, ) + args
@@ -515,6 +795,14 @@
return self.runpybin("py.test", *args)
def spawn_pytest(self, string, expect_timeout=10.0):
+ """Run py.test using pexpect.
+
+ This makes sure to use the right py.test and sets up the
+ temporary directory locations.
+
+ The pexpect child is returned.
+
+ """
if self.request.config.getvalue("notoolsonpath"):
pytest.skip("--no-tools-on-path prevents running pexpect-spawn
tests")
basetemp = self.tmpdir.mkdir("pexpect")
@@ -523,6 +811,10 @@
return self.spawn(cmd, expect_timeout=expect_timeout)
def spawn(self, cmd, expect_timeout=10.0):
+ """Run a command using pexpect.
+
+ The pexpect child is returned.
+ """
pexpect = pytest.importorskip("pexpect", "3.0")
if hasattr(sys, 'pypy_version_info') and '64' in platform.machine():
pytest.skip("pypy-64 bit not supported")
@@ -560,10 +852,21 @@
return LineMatcher(lines1).fnmatch_lines(lines2)
class LineMatcher:
+ """Flexible matching of text.
+
+ This is a convenience class to test large texts like the output of
+ commands.
+
+ The constructor takes a list of lines without their trailing
+ newlines, i.e. ``text.splitlines()``.
+
+ """
+
def __init__(self, lines):
self.lines = lines
def str(self):
+ """Return the entire original text."""
return "\n".join(self.lines)
def _getlines(self, lines2):
@@ -574,6 +877,12 @@
return lines2
def fnmatch_lines_random(self, lines2):
+ """Check lines exist in the output.
+
+ The argument is a list of lines which have to occur in the
+ output, in any order. Each line can contain glob whildcards.
+
+ """
lines2 = self._getlines(lines2)
for line in lines2:
for x in self.lines:
@@ -584,12 +893,24 @@
raise ValueError("line %r not found in output" % line)
def get_lines_after(self, fnline):
+ """Return all lines following the given line in the text.
+
+ The given line can contain glob wildcards.
+ """
for i, line in enumerate(self.lines):
if fnline == line or fnmatch(line, fnline):
return self.lines[i+1:]
raise ValueError("line %r not found in output" % fnline)
def fnmatch_lines(self, lines2):
+ """Search the text for matching lines.
+
+ The argument is a list of lines which have to match and can
+ use glob wildcards. If they do not match an pytest.fail() is
+ called. The matches and non-matches are also printed on
+ stdout.
+
+ """
def show(arg1, arg2):
py.builtin.print_(arg1, arg2, file=sys.stderr)
lines2 = self._getlines(lines2)
Repository URL: https://bitbucket.org/pytest-dev/pytest/
--
This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.
_______________________________________________
pytest-commit mailing list
[email protected]
https://mail.python.org/mailman/listinfo/pytest-commit