manuel-1.10.1/ 0000755 0001750 0001750 00000000000 13373300442 012032 5 ustar james james manuel-1.10.1/.circleci/ 0000755 0001750 0001750 00000000000 13373300442 013665 5 ustar james james manuel-1.10.1/.circleci/config.yml 0000644 0001750 0001750 00000000533 13373300442 015656 0 ustar james james # Python CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-python/ for more details
#
version: 2
jobs:
build:
working_directory: ~/repo
docker:
- image: circleci/python:3.6.4
steps:
- checkout
- run:
name: Run Tests
command: |
python setup.py test
manuel-1.10.1/CHANGES.rst 0000644 0001750 0001750 00000011712 13373300442 013636 0 ustar james james CHANGES
=======
1.10.1 (2018-11-15)
-------------------
- Add support for PyPy3.
1.10.0 (2018-11-14)
-------------------
- Fix DeprecationWarning about 'U' mode under Python 3.
- Drop Python 2.7 and 3.3 support. Add testing and support for Python 3.6 and
3.7.
1.9.0 (2017-11-20)
------------------
- You can now use Manuel with the `nose
`_ and `pytest
`_ test runners by defining
Manuel tests inside `unittest.TestCase` classes.
- Added support for Python 3.5 and Python 3.6.
- Dropped support for Python 2.6
1.8.0 (2014-07-15)
------------------
- Fixed ResourceWarnings under Python 3.
- Added support for PyPy and Python 3.4.
- Drop official support for Python 3.1 and 3.2.
- Fix odd ImportError problems when used with tox and coverage.
- Fix parsing of reST codeblock options with hyphens.
1.7.2 (2013-03-16)
------------------
- Fixed release issues.
- Updated copyright and license to reflect recent Zope Foundation release of
claim on the project.
1.7.1 (2013-02-13)
------------------
- Fix brown-bag release.
1.7.0 (2013-02-13)
------------------
- Added support for docutils-style code blocks and options there-of.
1.6.1 (2013-01-24)
------------------
- Fixed a bug that made doctests fail if sys.argv contained the string "-v".
1.6.0 (2012-04-16)
------------------
- Ported to Python 3, still works in 2.6 and up.
1.5.0 (2011-03-08)
------------------
- Removed the dependency on zope.testrunner
- Added the ability to run the tests using "setup.py test".
1.4.1 (2011-01-25)
------------------
- Fixed a bug that caused extra example evaluation if multiple doctest
manuels were used at once (e.g. to execute Python and shell code in
the same document).
1.4.0 (2011-01-11)
------------------
- Added a ``parser`` keyword argument to manuel.doctest.Manuel to
allow a custom doctest parser to be passed in. This allows easily
adding support for other languages or other (but similar) example
syntaxes.
1.3.0 (2010-09-02)
------------------
- Respect test runner reporting switches (e.g., zope.testrunner's --ndiff
switch)
- Fixed a bug that caused post-mortem debugging to not work most of the
time.
- Made manuel.testing.TestCase.id return a sensible textual value
at all times. This keeps Twisted's trial testrunner happy.
1.2.0 (2010-06-10)
------------------
- Conform to repository policy.
- Switch to using zope.testrunner instead of zope.testing due to API changes.
zope.testing is now only required for testing.
1.1.1 (2010-05-20)
------------------
- fix the way globs are handled; fixes
https://bugs.launchpad.net/manuel/+bug/582482
1.1.0 (2010-05-18)
------------------
- fix a SyntaxError when running the tests under Python 2.5
- improved error message for improperly indented capture directive
- Manuel no longer uses the now depricated zope.testing.doctest (requires
zope.testing 3.9.1 or newer)
1.0.5 (2010-01-29)
------------------
- fix a bug that caused Manuel to choke on empty documents (patch submitted by
Bjorn Tillenius)
- add a pointer to Manuel's Subversion repo on the PyPI page
- add an optional parameter that allows a custom TestCase class to be passed to
TestSuite() (patch submitted by Bjorn Tillenius)
1.0.4 (2010-01-06)
------------------
- use newer setuptools (one compatible with Subversion 1.6) so built
distributions include all files
1.0.3 (2010-01-06)
------------------
- fix a small doc thinko
- fix the code-block handler to allow :linenos:
- open files in universal newlines mode
1.0.2 (2009-12-07)
------------------
- fix a bug that caused instances of zope.testing.doctest.Example (and
instances of subclasses of the same) to be silently ignored.
1.0.1 (2009-08-31)
------------------
- fix line number reporting for test failures
1.0.0 (2009-08-09)
------------------
- Python 2.4 compatability fix
1.0.0b2 (2009-07-10)
--------------------
- add the ability to identify and run subsets of documents (using the -t switch
of zope.testing's testrunner for example)
1.0.0b1 (2009-06-24)
--------------------
- major docs improvements
- added several new plug-ins
1.0.0a8 (2009-05-01)
--------------------
- add a larger example of using Manuel (table-example.txt)
- make the test suite factory function try harder to find the calling
module
- fix a bug in the order regions are evaluated
- add a Manuel object that can evaluate Python code in
".. code-block:: python" regions of a reST document
1.0.0a4 (2009-05-01)
--------------------
- make the global state ("globs") shared between all evaluators, not just
doctest
1.0.0a3 (2009-05-01)
--------------------
- make zope.testing's testrunner recognized the enhanced, doctest-style
errors generated by Manuel
- rework the evaluaters to work region-by-region instead of on the
entire document
- switch to using regular Python classes for Manuel objects instead of
previous prototype-y style
1.0.0a2 (2008-10-17)
--------------------
- first release
manuel-1.10.1/MANIFEST.in 0000644 0001750 0001750 00000000355 13373300442 013573 0 ustar james james recursive-include src *.ex
recursive-include src *.txt
recursive-include docs *
recursive-include sphinx *.py
include *.rst
include tox.ini
include bootstrap.py
include buildout.cfg
include *.yaml
include .coveragerc
include .circleci/*
manuel-1.10.1/README.rst 0000644 0001750 0001750 00000001265 13373300442 013525 0 ustar james james .. image:: https://travis-ci.org/benji-york/manuel.png?branch=master
:target: https://travis-ci.org/benji-york/manuel
.. image:: https://coveralls.io/repos/github/benji-york/manuel/badge.svg?branch=master
:target: https://coveralls.io/github/benji-york/manuel?branch=master
.. image:: https://img.shields.io/pypi/v/manuel.svg
:target: https://pypi.python.org/pypi/manuel
.. image:: https://img.shields.io/pypi/pyversions/manuel.svg
:target: https://pypi.python.org/pypi/manuel/
Documentation, a full list of included plug-ins, and examples are available at
``_.
Source code and issues are managed at https://github.com/benji-york/manuel.
manuel-1.10.1/bootstrap.py 0000644 0001750 0001750 00000014016 13373300442 014423 0 ustar james james ##############################################################################
#
# Copyright (c) 2006 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Bootstrap a buildout-based project
Simply run this script in a directory containing a buildout.cfg.
The script accepts buildout command-line options, so you can
use the -c option to specify an alternate configuration file.
"""
import os
import shutil
import sys
import tempfile
from optparse import OptionParser
tmpeggs = tempfile.mkdtemp()
usage = '''\
[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
Bootstraps a buildout-based project.
Simply run this script in a directory containing a buildout.cfg, using the
Python that you want bin/buildout to use.
Note that by using --find-links to point to local resources, you can keep
this script from going over the network.
'''
parser = OptionParser(usage=usage)
parser.add_option("-v", "--version", help="use a specific zc.buildout version")
parser.add_option("-t", "--accept-buildout-test-releases",
dest='accept_buildout_test_releases',
action="store_true", default=False,
help=("Normally, if you do not specify a --version, the "
"bootstrap script and buildout gets the newest "
"*final* versions of zc.buildout and its recipes and "
"extensions for you. If you use this flag, "
"bootstrap and buildout will get the newest releases "
"even if they are alphas or betas."))
parser.add_option("-c", "--config-file",
help=("Specify the path to the buildout configuration "
"file to be used."))
parser.add_option("-f", "--find-links",
help=("Specify a URL to search for buildout releases"))
parser.add_option("--allow-site-packages",
action="store_true", default=False,
help=("Let bootstrap.py use existing site packages"))
options, args = parser.parse_args()
######################################################################
# load/install setuptools
try:
if options.allow_site_packages:
import setuptools
import pkg_resources
from urllib.request import urlopen
except ImportError:
from urllib2 import urlopen
ez = {}
exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez)
if not options.allow_site_packages:
# ez_setup imports site, which adds site packages
# this will remove them from the path to ensure that incompatible versions
# of setuptools are not in the path
import site
# inside a virtualenv, there is no 'getsitepackages'.
# We can't remove these reliably
if hasattr(site, 'getsitepackages'):
for sitepackage_path in site.getsitepackages():
sys.path[:] = [x for x in sys.path if sitepackage_path not in x]
setup_args = dict(to_dir=tmpeggs, download_delay=0)
ez['use_setuptools'](**setup_args)
import setuptools
import pkg_resources
# This does not (always?) update the default working set. We will
# do it.
for path in sys.path:
if path not in pkg_resources.working_set.entries:
pkg_resources.working_set.add_entry(path)
######################################################################
# Install buildout
ws = pkg_resources.working_set
cmd = [sys.executable, '-c',
'from setuptools.command.easy_install import main; main()',
'-mZqNxd', tmpeggs]
find_links = os.environ.get(
'bootstrap-testing-find-links',
options.find_links or
('http://downloads.buildout.org/'
if options.accept_buildout_test_releases else None)
)
if find_links:
cmd.extend(['-f', find_links])
setuptools_path = ws.find(
pkg_resources.Requirement.parse('setuptools')).location
requirement = 'zc.buildout'
version = options.version
if version is None and not options.accept_buildout_test_releases:
# Figure out the most recent final version of zc.buildout.
import setuptools.package_index
_final_parts = '*final-', '*final'
def _final_version(parsed_version):
for part in parsed_version:
if (part[:1] == '*') and (part not in _final_parts):
return False
return True
index = setuptools.package_index.PackageIndex(
search_path=[setuptools_path])
if find_links:
index.add_find_links((find_links,))
req = pkg_resources.Requirement.parse(requirement)
if index.obtain(req) is not None:
best = []
bestv = None
for dist in index[req.project_name]:
distv = dist.parsed_version
if _final_version(distv):
if bestv is None or distv > bestv:
best = [dist]
bestv = distv
elif distv == bestv:
best.append(dist)
if best:
best.sort()
version = best[-1].version
if version:
requirement = '=='.join((requirement, version))
cmd.append(requirement)
import subprocess
if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0:
raise Exception(
"Failed to execute command:\n%s" % repr(cmd)[1:-1])
######################################################################
# Import and run buildout
ws.add_entry(tmpeggs)
ws.require(requirement)
import zc.buildout.buildout
if not [a for a in args if '=' not in a]:
args.append('bootstrap')
# if -c was provided, we push it back into args for buildout' main function
if options.config_file is not None:
args[0:0] = ['-c', options.config_file]
zc.buildout.buildout.main(args)
shutil.rmtree(tmpeggs)
manuel-1.10.1/src/ 0000755 0001750 0001750 00000000000 13373300442 012621 5 ustar james james manuel-1.10.1/src/manuel/ 0000755 0001750 0001750 00000000000 13373300442 014102 5 ustar james james manuel-1.10.1/src/manuel/footnote.py 0000644 0001750 0001750 00000004753 13373300442 016322 0 ustar james james import re
import manuel
FOOTNOTE_REFERENCE_LINE_RE = re.compile(r'^.*\[([^\]]+)]_.*$', re.MULTILINE)
FOOTNOTE_REFERENCE_RE = re.compile(r'\[([^\]]+)]_')
FOOTNOTE_DEFINITION_RE = re.compile(
r'^\.\.\s*\[\s*([^\]]+)\s*\].*$', re.MULTILINE)
END_OF_FOOTNOTE_RE = re.compile(r'^\S.*$', re.MULTILINE)
class FootnoteReference(object):
def __init__(self, names):
self.names = names
class FootnoteDefinition(object):
def __init__(self, name):
self.name = name
@manuel.timing(manuel.EARLY)
def find_footnote_references(document):
# find the markers that show where footnotes have been defined.
footnote_names = []
for region in document.find_regions(FOOTNOTE_DEFINITION_RE):
name = region.start_match.group(1)
document.claim_region(region)
region.parsed = FootnoteDefinition(name)
footnote_names.append(name)
# find the markers that show where footnotes have been referenced.
for region in document.find_regions(FOOTNOTE_REFERENCE_LINE_RE):
assert region.source.count('\n') == 1
names = FOOTNOTE_REFERENCE_RE.findall(region.source)
for name in names:
if name not in footnote_names:
raise RuntimeError('Unknown footnote: %r' % name)
assert names
document.claim_region(region)
region.parsed = FootnoteReference(names)
@manuel.timing(manuel.LATE)
def do_footnotes(document):
"""Copy footnoted items into their appropriate position.
"""
# first find all the regions that are in footnotes
footnotes = {}
name = None
for region in list(document):
if isinstance(region.parsed, FootnoteDefinition):
name = region.parsed.name
footnotes[name] = []
document.remove_region(region)
continue
if END_OF_FOOTNOTE_RE.search(region.source):
name = None
if name is not None:
footnotes[name].append(region)
document.remove_region(region)
# now make copies of the footnotes in the right places
for region in list(document):
if not isinstance(region.parsed, FootnoteReference):
continue
names = region.parsed.names
for name in names:
for footnoted in footnotes[name]:
document.insert_region_before(region, footnoted.copy())
document.remove_region(region)
class Manuel(manuel.Manuel):
def __init__(self):
manuel.Manuel.__init__(self, [find_footnote_references, do_footnotes])
manuel-1.10.1/src/manuel/table-example.txt 0000644 0001750 0001750 00000022721 13373300442 017367 0 ustar james james .. _fit-table-example:
FIT Table Example
=================
Here is an example of writing a relatively complex Manuel plug-in.
Occasionally when writing a doctest, you want a better way to express a test
than doctest by itself provides.
For example, you may want to succinctly express the result of an expression for
several sets of inputs and outputs.
That's something `FIT `_ tables do a
good job of.
We can use Manuel to write a parser that can read the tables, an evaluator that
can check to see if the assertions made in the tables match reality, and a
formatter to display the results if they don't.
We'll use `reST `_ tables as the
table format. The table source will look like this::
===== ===== ======
\ A or B
--------------------
A B Result
===== ===== ======
False False False
True False True
False True True
True True True
===== ===== ======
.. -> example_table_1
When rendered to HTML, it will look like this:
===== ===== ======
\ A or B
--------------------
A B Result
===== ===== ======
False False False
True False True
False True True
True True True
===== ===== ======
.. -> example_table_2
>>> example_table_1 == example_table_2
True
Documents
---------
Here is an example of a source document we want our plug-in to be able to
understand::
The "or" operator
=================
Here is an example of the "or" operator in action:
===== ===== ======
\ A or B
--------------------
A B Result
===== ===== ======
False False False
True False True
False True True
True True True
===== ===== ======
.. -> source
Manuel plug-ins operate on instances of :class:`manuel.Document`.
.. code-block:: python
import manuel
document = manuel.Document(source, location='fake.txt')
Parsing
-------
We need an object to represent the tables.
.. code-block:: python
class Table(object):
def __init__(self, expression, variables, examples):
self.expression = expression
self.variables = variables
self.examples = examples
We'll also need a function to find the tables in the document, extract the
pertinent details, and instantiate Table objects.
.. code-block:: python
import re
import six
table_start = re.compile(r'(?<=\n\n)=[= ]+\n(?=[ \t]*?\S)', re.DOTALL)
table_end = re.compile(r'\n=[= ]+\n(?=\Z|\n)', re.DOTALL)
def parse_tables(document):
for region in document.find_regions(table_start, table_end):
lines = enumerate(iter(region.source.splitlines()))
six.advance_iterator(lines) # skip the first line
# grab the expression to be evaluated
expression = six.advance_iterator(lines)[1]
if expression.startswith('\\'):
expression = expression[1:]
six.advance_iterator(lines) # skip the divider line
variables = [v.strip() for v in six.advance_iterator(lines)[1].split()][:-1]
six.advance_iterator(lines) # skip the divider line
examples = []
for lineno_offset, line in lines:
if line.startswith('='):
break # we ran into the final divider, so stop
values = [eval(v.strip(), {}) for v in line.split()]
inputs = values[:-1]
output = values[-1]
examples.append((inputs, output, lineno_offset))
table = Table(expression, variables, examples)
document.claim_region(region)
region.parsed = table
If we parse the Document we can see that the table was recognized.
>>> parse_tables(document)
>>> region = list(document)[1]
>>> import six
>>> six.print_(region.source, end='')
===== ===== ======
\ A or B
--------------------
A B Result
===== ===== ======
False False False
True False True
False True True
True True True
===== ===== ======
>>> region.parsed
Evaluating
----------
Now that we can find and extract the tables from the source, we need to be able
to check them for correctness.
The parse phase decomposed the :class:`Document` into several :class:`Region`
instances. During the evaluation phase each evaluater is called once for each
region.
The evaluate_table function iterates over each set of inputs given in a single
table, evaluate the inputs with the expression and compare the result with what
was expected. Each discrepancy will be stored as a :class:`TableError` in a
:class:`TableErrors` object.
.. code-block:: python
class TableErrors(list):
pass
class TableError(object):
def __init__(self, location, lineno, expected, got):
self.location = location
self.lineno = lineno
self.expected = expected
self.got = got
def __str__(self):
return '<%s %s:%s>' % (
self.__class__.__name__, self.location, self.lineno)
def evaluate_table(region, document, globs):
if not isinstance(region.parsed, Table):
return
table = region.parsed
errors = TableErrors()
for inputs, output, lineno_offset in table.examples:
result = eval(table.expression, dict(zip(table.variables, inputs)))
if result != output:
lineno = region.lineno + lineno_offset
errors.append(
TableError(document.location, lineno, output, result))
region.evaluated = errors
Now we can use the function to evaluate our table.
>>> evaluate_table(region, document, {})
Yay! There were no errors:
>>> region.evaluated
[]
What would happen if there were errors?
::
The "or" operator
=================
Here is an (erroneous) example of the "or" operator in action:
===== ===== ======
\ A or B
--------------------
A B Result
===== ===== ======
False False True
True False True
False True False
True True True
===== ===== ======
.. -> source_with_errors
>>> document = manuel.Document(source_with_errors, location='fake.txt')
>>> parse_tables(document)
>>> region = list(document)[1]
>>> evaluate_table(region, document, {})
...the result of evaluaton would include them:
>>> region.evaluated
[]
Formatting Errors
-----------------
Now that we can parse the tables and evaluate them, we need to be able to
display the results in a readable fashion.
.. code-block:: python
def format_table_errors(document):
for region in document:
if not isinstance(region.evaluated, TableErrors):
continue
# if there were no errors, there is nothing to report
if not region.evaluated:
continue
messages = []
for error in region.evaluated:
messages.append('%s, line %d: expected %r, got %r instead.' % (
error.location, error.lineno, error.expected, error.got))
sep = '\n '
header = 'when evaluating table at %s, line %d' % (
document.location, region.lineno)
region.formatted = header + sep + sep.join(messages)
We can see how the results are formatted.
>>> format_table_errors(document)
>>> six.print_(region.formatted, end='')
when evaluating table at fake.txt, line 6
fake.txt, line 11: expected True, got False instead.
fake.txt, line 13: expected False, got True instead.
All Together Now
----------------
All the pieces (parsing, evaluating, and formatting) are available now, so we
just have to put them together into a single "Manuel" object.
.. code-block:: python
class Manuel(manuel.Manuel):
def __init__(self):
manuel.Manuel.__init__(self, [parse_tables], [evaluate_table],
[format_table_errors])
Now we can create a fresh document and tell it to do all the above steps
(parse, evaluate, format) using an instance of our plug-in.
>>> m = Manuel()
>>> document = manuel.Document(source_with_errors, location='fake.txt')
>>> document.process_with(m, globs={})
>>> six.print_(document.formatted(), end='')
when evaluating table at fake.txt, line 6
fake.txt, line 11: expected True, got False instead.
fake.txt, line 13: expected False, got True instead.
Of course, if there were no errors, nothing would be reported:
>>> document = manuel.Document(source, location='fake.txt')
>>> document.process_with(m, globs={})
>>> six.print_(document.formatted())
If we wanted to use instances of our Manuel object in a test, we would follow
the directions in :ref:`getting-started`, importing Manuel from the module
where we placed the code, just like any other Manuel plug-in.
.. this next bit is actually a reST comment, but it is run during tests anyway
(note the single colon instead of double colon)
.. invisible-code-block: python
import unittest
suite = manuel.testing.TestSuite(m, 'table-example.txt')
.. run this file through the Manuel instance constructed above to ensure it
actually works when given a real file to process
>>> suite.run(unittest.TestResult())
manuel-1.10.1/src/manuel/testing.py 0000644 0001750 0001750 00000016372 13373300442 016142 0 ustar james james from __future__ import absolute_import
import doctest as real_doctest
import functools
import inspect
import itertools
import manuel
import io
import os.path
import re
import sys
import types
import unittest
__all__ = ['TestSuite', 'TestFactory']
class TestCaseMarker(object):
def __init__(self, id=''):
self.id = id
class TestCase(unittest.TestCase):
def __init__(self, m, regions, globs, setUp=None, tearDown=None):
unittest.TestCase.__init__(self)
self.manuel = m
self.regions = regions
self.globs = globs
self.setUp_func = setUp
self.tearDown_func = tearDown
def setUp(self):
if self.setUp_func is not None:
self.setUp_func(self)
def tearDown(self):
if self.tearDown_func is not None:
self.tearDown_func(self)
def runTest(self):
self.regions.evaluate_with(self.manuel, self.globs)
self.regions.format_with(self.manuel)
results = [r.formatted for r in self.regions if r.formatted]
if results:
DIVIDER = '-'*70 + '\n'
raise real_doctest.DocTestCase.failureException(
'\n' + DIVIDER + DIVIDER.join(results))
def debug(self):
self.setUp()
self.manuel.debug = True
self.regions.evaluate_with(self.manuel, self.globs)
self.tearDown()
def countTestCases(self):
return len([r for r in self.regions if r.parsed])
def shortDescription(self):
if self.regions.id:
return self.regions.location + ':' + self.regions.id
else:
return self.regions.location
__str__ = __repr__ = id = shortDescription
def group_regions_by_test_case(document):
"""Generate groups of regions according to which testcase they belong"""
document_iter = iter(document)
marker = None
while True:
accumulated_regions = manuel.RegionContainer()
while True:
region = None # being defensive
try:
region = next(document_iter)
except StopIteration:
if not accumulated_regions:
break
else:
accumulated_regions.append(region)
if not isinstance(region.parsed, TestCaseMarker):
continue
# we just found a test case marker or hit the end of the
# document
# figure out what this test case's ID is
accumulated_regions.location = document.location
if marker is not None and marker.parsed.id:
accumulated_regions.id = marker.parsed.id
yield accumulated_regions
marker = region
break
# if there are no more regions, stop
try:
region = next(document_iter)
except StopIteration:
break
# put the region we peeked at back so the inner loop can consume it
document_iter = itertools.chain([region], document_iter)
# copied from zope.testing.doctest
def _module_relative_path(module, path):
if not inspect.ismodule(module):
raise TypeError('Expected a module: %r' % module)
if path.startswith('/'):
raise ValueError('Module-relative files may not have absolute paths')
# Find the base directory for the path.
if hasattr(module, '__file__'):
# A normal module/package
basedir = os.path.split(module.__file__)[0]
elif module.__name__ == '__main__':
# An interactive session.
if len(sys.argv)>0 and sys.argv[0] != '':
basedir = os.path.split(sys.argv[0])[0]
else:
basedir = os.curdir
else:
# A module w/o __file__ (this includes builtins)
raise ValueError("Can't resolve paths relative to the module " +
module + " (it has no __file__)")
# Combine the base directory and the path.
return os.path.join(basedir, *(path.split('/')))
def TestSuite(m, *paths, **kws):
"""A unittest suite that processes files with Manuel
The path to each document file is given as a string.
A number of options may be provided as keyword arguments:
`setUp`
A set-up function. This is called before running the tests in each file.
The setUp function will be passed a TestCase object. The setUp function
can access the test globals as the `globs` attribute of the instance
passed.
`tearDown`
A tear-down function. This is called after running the tests in each
file. The tearDown function will be passed a Manuel object. The
tearDown function can access the test globals as the `globs` attribute of
the instance passed.
`globs`
A dictionary containing initial global variables for the tests.
`TestCase`
The TestCase class to be used instead of manuel.testing.TestCase.
"""
suite = unittest.TestSuite()
globs = kws.pop('globs', {})
TestCase_class = kws.pop('TestCase', TestCase)
# walk up the stack frame to find the module that called this function
for depth in range(1, 5):
try:
calling_module = \
sys.modules[sys._getframe(depth).f_globals['__name__']]
except KeyError:
continue
else:
break
for path in paths:
if os.path.isabs(path):
abs_path = os.path.normpath(path)
else:
abs_path = \
os.path.abspath(_module_relative_path(calling_module, path))
with io.open(abs_path, 'rt', newline=None) as fp:
contents = fp.read()
if not isinstance(contents, str):
# Python 2, we read unicode, but we really need a str
contents = str.encode("utf-8")
document = manuel.Document(
contents, location=abs_path)
document.parse_with(m)
for regions in group_regions_by_test_case(document):
suite.addTest(TestCase_class(m, regions, globs, **kws))
return suite
_not_word = re.compile(r'\W')
class TestFactory:
def __init__(self, m):
self.m = m
def __call__(self, path):
base = os.path.dirname(os.path.abspath(
sys._getframe(2).f_globals['__file__']
))
path = os.path.join(base, path)
with open(path) as f:
test = f.read()
m = self.m
def test_file(self, setup=lambda i: None):
if isinstance(self, types.FunctionType):
# We're being used as a decorator. `self` is a setup method.
f = functools.wraps(self)(lambda inst: test_file(inst, self))
f.filepath = path
f.filename = os.path.basename(path)
return f
setup(self)
globs = dict(getattr(self, 'globs', ()))
globs['test'] = self
document = manuel.Document(test, location=path)
document.parse_with(m)
[regions] = group_regions_by_test_case(document)
TestCase(m, regions, globs).runTest()
test_file.filepath = path
test_file.filename = filename = os.path.basename(path)
name = _not_word.sub('_', os.path.splitext(filename)[0])
if not name.startswith('test'):
name = 'test_' + name
test_file.__name__ = name
return test_file
manuel-1.10.1/src/manuel/doc1.ex 0000644 0001750 0001750 00000000015 13373300442 015262 0 ustar james james >>> test.a
1
manuel-1.10.1/src/manuel/capture.py 0000644 0001750 0001750 00000006075 13373300442 016127 0 ustar james james import manuel
import re
import string
import textwrap
CAPTURE_DIRECTIVE = re.compile(
r'^(?P(\t| )*)\.\.\s*->\s*(?P\S+).*$',
re.MULTILINE)
class Capture(object):
def __init__(self, name, block):
self.name = name
self.block = block
def normalize_whitespace(s):
return s.replace('\t', ' '*8) # turn tabs into spaces
@manuel.timing(manuel.EARLY)
def find_captures(document):
while True:
regions = document.find_regions(CAPTURE_DIRECTIVE)
if not regions:
break
region = regions[-1]
# note that start and end have different bases, "start" is the offset
# from the begining of the region, "end" is a document line number
end = region.lineno - 2
indent = region.start_match.group('indent')
indent = normalize_whitespace(indent)
def indent_matches(line):
"""Is the indentation of a line match what we're looking for?"""
line = normalize_whitespace(line)
if not line.strip():
# the line consists entirely of whitespace (or nothing at all),
# so is not considered to be of the appropriate indentation
return False
if line.startswith(indent):
if line[len(indent)] not in string.whitespace:
return True
# if none of the above found the indentation to be a match, it is
# not a match
return False
# now that we've extracted the information we need, lets slice up the
# document's regions to match
for candidate in document:
if candidate.lineno >= region.lineno:
break
found_region = candidate
lines = found_region.source.splitlines()
if found_region.lineno + len(lines) < end:
raise RuntimeError('both start and end lines must be in the '
'same region')
start = None
for offset, line in reversed(list(enumerate(lines))):
if offset > end - found_region.lineno:
continue
if indent_matches(line):
break
start = offset + 1
if start is None:
raise RuntimeError("couldn't find the start of the block; "
"improper indentation of capture directive?")
_, temp_region = document.split_region(found_region,
found_region.lineno+start)
# there are some extra lines in the new region, trim them off
final_region, _ = document.split_region(temp_region, end+1)
document.remove_region(final_region)
name = region.start_match.group('name')
block = textwrap.dedent(final_region.source)
document.claim_region(region)
region.parsed = Capture(name, block)
def store_capture(region, document, globs):
if not isinstance(region.parsed, Capture):
return
globs[region.parsed.name] = region.parsed.block
class Manuel(manuel.Manuel):
def __init__(self):
manuel.Manuel.__init__(self, [find_captures], [store_capture])
manuel-1.10.1/src/manuel/tests.py 0000644 0001750 0001750 00000004422 13373300442 015620 0 ustar james james from __future__ import absolute_import
import doctest
import manuel
import manuel.capture
import manuel.codeblock
import manuel.doctest
import manuel.ignore
import manuel.testcase
import manuel.testing
import os.path
import re
import unittest
import zope.testing.renormalizing
here = os.path.dirname(os.path.abspath(__file__))
checker = zope.testing.renormalizing.RENormalizing([
(re.compile(r">> document = manuel.Document('''This is my doctest.
...
... >>> 2 + 2
... 5
... ''')
>>> document.process_with(manuel.doctest.Manuel(), globs={})
>>> print(document.formatted())
File "", line 3, in
Failed example:
2 + 2
Expected:
5
Got:
4
"""
def test_suite():
tests = ['index.txt', 'table-example.txt', 'README.txt', 'bugs.txt',
'capture.txt']
optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
m = manuel.ignore.Manuel()
m += manuel.doctest.Manuel(optionflags=optionflags, checker=checker)
m += manuel.codeblock.Manuel()
m += manuel.capture.Manuel()
m += manuel.testcase.SectionManuel()
# The apparently redundant "**dict()" is to make this code compatible with
# Python 2.5 -- it would generate a SyntaxError otherwise.
suite = manuel.testing.TestSuite(m, *tests, **dict(
globs={'path_to_test': os.path.join(here, 'bugs.txt')}))
return unittest.TestSuite((
suite,
doctest.DocTestSuite(),
))
manuel-1.10.1/src/manuel/doctest.py 0000644 0001750 0001750 00000007021 13373300442 016121 0 ustar james james from __future__ import absolute_import
import doctest
import six
import manuel
import os.path
DocTestRunner = doctest.DocTestRunner
DebugRunner = doctest.DebugRunner
class DocTestResult(six.StringIO):
pass
def parse(m, document, parser):
for region in list(document):
if region.parsed:
continue
region_start = region.lineno
region_end = region.lineno + region.source.count('\n')
for chunk in parser.parse(region.source):
# If the chunk contains prose (as opposed to and example), skip it.
if isinstance(chunk, str):
continue
chunk._manual = m
chunk_line_count = (chunk.source.count('\n')
+ chunk.want.count('\n'))
split_line_1 = region_start + chunk.lineno
split_line_2 = split_line_1 + chunk_line_count
# if there is some source we need to trim off the front...
if split_line_1 > region.lineno:
_, region = document.split_region(region, split_line_1)
if split_line_2 < region_end:
found, region = document.split_region(region, split_line_2)
else:
found = region
document.claim_region(found)
# Since we're treating each example as a stand-alone thing, we need
# to reset its line number to zero.
chunk.lineno = 0
found.parsed = chunk
assert region in document
class DocTest(doctest.DocTest):
def __init__(self, examples, globs, name, filename, lineno, docstring):
# do everything like regular doctests, but don't make a copy of globs
doctest.DocTest.__init__(self, examples, globs, name, filename, lineno,
docstring)
self.globs = globs
def evaluate(m, region, document, globs):
# If the parsed object is not a doctest Example then we don't need to
# handle it.
if getattr(region.parsed, '_manual', None) is not m:
return
result = DocTestResult()
test_name = os.path.split(document.location)[1]
if m.debug:
runner = m.debug_runner
out = None
else:
runner = m.runner
out = result.write
# Use the testrunner-set option flags when running these tests.
old_optionflags = runner.optionflags
runner.optionflags |= doctest._unittest_reportflags
runner.DIVIDER = '' # disable unwanted result formatting
# Here's where everything happens.
example = region.parsed
runner.run(
DocTest([example], globs, test_name,
document.location, region.lineno-1, None),
out=out, clear_globs=False)
runner.optionflags = old_optionflags # Reset the option flags.
region.evaluated = result
def format(document):
for region in document:
if not isinstance(region.evaluated, DocTestResult):
continue
region.formatted = region.evaluated.getvalue().lstrip()
class Manuel(manuel.Manuel):
def __init__(self, optionflags=0, checker=None, parser=None):
self.runner = DocTestRunner(optionflags=optionflags,
checker=checker, verbose=False)
self.debug_runner = DebugRunner(optionflags=optionflags, verbose=False)
def evaluate_closure(region, document, globs):
# capture "self"
evaluate(self, region, document, globs)
parser = parser or doctest.DocTestParser()
manuel.Manuel.__init__(
self,
[lambda document: parse(self, document, parser)],
[evaluate_closure], [format])
manuel-1.10.1/src/manuel/__init__.py 0000644 0001750 0001750 00000025113 13373300442 016215 0 ustar james james import re
# constants for use with "timing" decorator
EARLY = 'early'
LATE = 'late'
def timing(timing):
assert timing in (EARLY, LATE)
def decorate(func):
func.manuel_timing = timing
return func
return decorate
def newlineify(s):
if s == '' or s[-1] != '\n':
s += '\n'
return s
class Region(object):
"""A portion of source found via regular expression."""
parsed = None
evaluated = None
formatted = None
def __init__(self, lineno, source, start_match=None, end_match=None,
provenance=None):
self.lineno = lineno
self.source = newlineify(source)
self.start_match = start_match
self.end_match = end_match
self.provenance = provenance
def copy(self):
"""Private utility function to make a copy of this region.
"""
copy = Region(self.lineno, self.source, provenance=self.provenance)
copy.parsed = self.parsed
copy.evaluated = self.evaluated
copy.formatted = self.formatted
return copy
def find_line(region, index):
return region[:index].count('\n') + 1
def check_region_start(region, match):
if match.start() != 0 \
and region.source[match.start()-1] != '\n':
raise ValueError(
'Regions must start at the begining of a line.')
def check_region_end(region, match):
if match.end() != len(region.source) \
and region.source[match.end()-1] != '\n':
raise ValueError(
'Regions must end at the ending of a line.')
def lines_to_string(lines):
return '\n'.join(lines) + '\n'
def make_string_into_lines(s):
lines = newlineify(s).split('\n')
assert lines[-1] == ''
del lines[-1]
return lines
def break_up_region(original, new):
assert original.parsed is None
lines = make_string_into_lines(original.source)
new_regions = []
# figure out if there are any lines before the given region
before_lines = lines[:new.lineno-original.lineno]
if before_lines:
new_regions.append(
Region(original.lineno, lines_to_string(before_lines)))
# put in the parsed
new_regions.append(new)
# figure out if there are any lines after the given region
assert new.source[-1] == '\n', 'all lines must end with a newline'
lines_in_new = new.source.count('\n')
after_lines = lines[len(before_lines)+lines_in_new:]
if after_lines:
first_line_after_new = new.lineno + lines_in_new
new_regions.append(
Region(first_line_after_new, lines_to_string(after_lines)))
assert original.source.count('\n') == \
sum(r.source.count('\n') for r in new_regions)
return new_regions
def sort_handlers(handlers):
def key(f):
# "j" was chosen because it sorts between "early" and "late"
return getattr(f, 'manuel_timing', 'j')
return sorted(handlers, key=key)
def find_end_of_line(s):
end = 0
while len(s) < end and s[end] != '\n':
end += 1
return end
class RegionContainer(object):
location = ''
id = None
def __init__(self):
self.regions = []
def parse_with(self, m):
for parser in sort_handlers(m.parsers):
parser(self)
def evaluate_with(self, m, globs):
for region in list(self):
for evaluater in sort_handlers(m.evaluaters):
evaluater(region, self, globs)
def format_with(self, m):
for formatter in sort_handlers(m.formatters):
formatter(self)
def process_with(self, m, globs):
"""Run all phases of document processing using a Manuel instance.
"""
self.parse_with(m)
self.evaluate_with(m, globs)
self.format_with(m)
def formatted(self):
"""Return a string of all non-boolean-false formatted regions.
"""
return ''.join(region.formatted for region in self if region.formatted)
def append(self, region):
self.regions.append(region)
def __iter__(self):
"""Iterate over all regions of the document.
"""
return iter(self.regions)
def __bool__(self):
return bool(self.regions)
class Document(RegionContainer):
def __init__(self, source, location=None):
RegionContainer.__init__(self)
if location is not None:
self.location = location
self.source = newlineify(source)
self.append(Region(lineno=1, source=source))
self.shadow_regions = []
def find_regions(self, start, end=None):
def compile(regex):
if regex is not None and isinstance(regex, str):
regex = re.compile(regex)
return regex
start = compile(start)
end = compile(end)
results = []
for region in self.regions:
# can't parse things that have already been parsed
if region.parsed:
continue
for start_match in re.finditer(start, region.source):
first_lineno = region.lineno + find_line(
region.source, start_match.start()) - 1
check_region_start(region, start_match)
if end is None:
end_match = None
text = start_match.group()
else:
end_match = end.search(region.source, start_match.end())
# couldn't find a match for the end re, try again
if end_match is None:
continue
end_position = end_match.end() + \
find_end_of_line(region.source[end_match.end():])
text = region.source[start_match.start():end_position]
if text[-1] != '\n':
text += '\n'
new_region = Region(first_lineno, text, start_match, end_match)
self.shadow_regions.append(new_region)
results.append(new_region)
return results
def split_region(self, region, lineno):
lineno -= region.lineno
assert lineno > 0
assert region in self.regions
assert region.parsed == region.evaluated == region.formatted == None
lines = make_string_into_lines(region.source)
source1 = lines_to_string(lines[:lineno])
source2 = lines_to_string(lines[lineno:])
region_index = self.regions.index(region)
del self.regions[region_index]
lines_in_source1 = source1.count('\n')
region1 = Region(region.lineno, source1)
region2 = Region(region.lineno+lines_in_source1, source2)
self.regions.insert(region_index, region2)
self.regions.insert(region_index, region1)
if not region.source == source1 + source2:
raise RuntimeError('when splitting a region, combined results do '
'not equal the input')
return region1, region2
def claim_region(self, to_be_replaced):
new_regions = []
old_regions = list(self.regions)
while old_regions:
region = old_regions.pop(0)
if region.lineno == to_be_replaced.lineno:
assert not region.parsed
new_regions.extend(break_up_region(region, to_be_replaced))
break
elif region.lineno > to_be_replaced.lineno: # we "overshot"
assert not new_regions[-1].parsed
to_be_broken = new_regions[-1]
del new_regions[-1]
new_regions.extend(break_up_region(
to_be_broken, to_be_replaced))
new_regions.append(region)
break
new_regions.append(region)
else:
# we didn't make any replacements, so the parsed data must be for
# the very last region, which also must not have been parsed yet
assert not region.parsed
del new_regions[-1]
new_regions.extend(break_up_region(region, to_be_replaced))
new_regions.extend(old_regions)
self.regions = new_regions
def insert_region(self, where, marker_region, new_region):
if new_region in self.regions:
raise ValueError(
'Only regions not already in the document may be inserted.')
if new_region in self.shadow_regions:
raise ValueError(
'Regions returned by "find_regions" can not be directly '
'inserted into a document. Use "claim_region" instead.')
for index, region in enumerate(self.regions):
if region is marker_region:
if where == 'after':
index += 1
self.regions.insert(index, new_region)
break
def remove_region(self, region):
self.regions.remove(region)
def insert_region_before(self, marker_region, new_region):
self.insert_region('before', marker_region, new_region)
def insert_region_after(self, marker_region, new_region):
self.insert_region('after', marker_region, new_region)
def call(func):
return func()
class Manuel(object):
_debug = False
def __init__(self, parsers=None, evaluaters=None, formatters=None):
if parsers is not None:
self.parsers = parsers
else:
self.parsers = []
if evaluaters is not None:
self.evaluaters = evaluaters
else:
self.evaluaters = []
if formatters is not None:
self.formatters = formatters
else:
self.formatters = []
# other instances that this one has been extended with
self.others = []
def add_parser(self, parser):
self.parsers.append(parser)
def add_evaluater(self, evaluater):
self.evaluaters.append(evaluater)
def add_formatter(self, formatter):
self.formatters.append(formatter)
def __extend(self, other):
self.others.append(other)
self.debug = max(self.debug, other.debug)
self.parsers.extend(other.parsers)
self.evaluaters.extend(other.evaluaters)
self.formatters.extend(other.formatters)
# the testing integration (manuel.testing) sets this flag when needed
@call
def debug():
def getter(self):
debug = self._debug
if self.others:
debug = max(debug, max(m.debug for m in self.others))
return debug
def setter(self, value):
self._debug = value
for m in self.others:
m.debug = value
return property(getter, setter)
def __add__(self, other):
m = Manuel()
m.__extend(self)
m.__extend(other)
return m
manuel-1.10.1/src/manuel/isolation.py 0000644 0001750 0001750 00000002142 13373300442 016454 0 ustar james james import re
import manuel
import textwrap
RESET = re.compile(r'^\.\.\s*reset-globs\s*$', re.MULTILINE)
CAPTURE = re.compile(r'^\.\.\s*capture-globs\s*$', re.MULTILINE)
baseline = {}
class Reset(object):
pass
def find_reset(document):
for region in document.find_regions(RESET):
document.claim_region(region)
region.parsed = Reset()
def execute_reset(region, document, globs):
if not isinstance(region.parsed, Reset):
return
globs.clear()
globs.update(baseline)
class Capture(object):
pass
def find_baseline(document):
# clear the baseline globs at the begining of a run (a bit of a hack)
baseline.clear()
for region in document.find_regions(CAPTURE):
document.claim_region(region)
region.parsed = Capture()
def execute_baseline(region, document, globs):
if not isinstance(region.parsed, Capture):
return
baseline.clear()
baseline.update(globs)
class Manuel(manuel.Manuel):
def __init__(self):
manuel.Manuel.__init__(self, [find_reset, find_baseline],
[execute_reset, execute_baseline])
manuel-1.10.1/src/manuel/index.txt 0000644 0001750 0001750 00000067314 13373300442 015765 0 ustar james james ======
Manuel
======
Manuel lets you mix and match traditional doctests with custom test syntax.
Several plug-ins are included that provide new test syntax (see
:ref:`functionality`). You can also create your own plug-ins.
For example, if you've ever wanted to include a large chunk of Python in a
doctest but were irritated by all the ">>>" and "..." prompts required, you'd
like the :mod:`manuel.codeblock` module. It lets you execute code using
Sphinx-style ".. code-block:: python" directives. The markup looks like
this::
.. code-block:: python
import foo
def my_func(bar):
return foo.baz(bar)
Incidentally, the implementation of :mod:`manuel.codeblock` is only 23 lines of
code.
The plug-ins included in Manuel make good examples while being quite useful in
their own right. The Manuel documentation makes extensive use of them as well.
Follow the "Show Source" link to the left to see the `reST
`_ source of this document.
For a large example of creating test syntax, take a look at the
:ref:`fit-table-example` or for all the details, :ref:`theory-of-operation`.
To see how to get Manuel wired up see :ref:`getting-started`.
.. contents::
.. reset-globs
.. _functionality:
Included Functionality
======================
Manuel includes several plug-ins out of the box:
:ref:`manuel.capture `
stores regions of a document in variables for later processing
:ref:`manuel.codeblock `
executes code in ".. code-block:: python" blocks
:ref:`manuel.doctest `
provides traditional doctest processing as a Manuel plug-in
:ref:`manuel.footnote `
executes code in reST-style footnodes each time they're referenced (good
for getting incidental code out of the main flow of a document)
:ref:`manuel.ignore `
ignores parts of a document while running tests
:ref:`manuel.isolation `
makes it easier to have test isolation in doctests
:ref:`manuel.testcase `
identify parts of tests as individual test cases so they can be run
independently
.. reset-globs
.. _getting-started:
Getting Started
===============
The plug-ins used for a test are composed together using the "+" operator.
Let's say you wanted a test that used doctest syntax as well as footnotes. You
would create a Manuel instance to use like this:
.. code-block:: python
import manuel.doctest
import manuel.footnote
m = manuel.doctest.Manuel()
m += manuel.footnote.Manuel()
You would then pass the Manuel instance to a :class:`manuel.testing.TestSuite`,
including the names of documents you want to process:
.. ignore-next-block
.. code-block:: python
manuel.testing.TestSuite(m, 'test-one.txt', 'test-two.txt')
Using unittest
--------------
The simplest way to get started with Manuel is to use :mod:`unittest` to run
your tests:
.. code-block:: python
import manuel.codeblock
import manuel.doctest
import manuel.testing
import unittest
def test_suite():
m = manuel.doctest.Manuel()
m += manuel.codeblock.Manuel()
return manuel.testing.TestSuite(m, 'test-one.txt', 'test-two.txt')
if __name__ == '__main__':
unittest.TextTestRunner().run(test_suite())
Using zope.testing
------------------
If you want to use a more featureful test runner you can use zope.testing's
test runner (usable stand-alone -- it isn't dependent on the Zope application
server). Create a file named :file:`tests.py` with a :func:`test_suite`
function that returns a test suite.
The suite can be either a :class:`manuel.testing.TestSuite` object or a
:class:`unittest.TestSuite` as demonstrated below.
.. code-block:: python
import manuel.codeblock
import manuel.doctest
import manuel.testing
def test_suite():
suite = unittest.TestSuite()
# here you add your other tests to the suite...
# now you can add the Manuel tests
m = manuel.doctest.Manuel()
m += manuel.codeblock.Manuel()
suite.addTest(manuel.testing.TestSuite(m,
'test-one.txt', 'test-two.txt'))
return suite
Others
------
To use another test runner, like nose or pytest:
.. setup __name__ and __file__
>>> import manuel
>>> __name__ = 'tests'
>>> __file__ = manuel.__file__
.. code-block:: python
import manuel.codeblock
import manuel.doctest
import manuel.testing
m = manuel.doctest.Manuel()
m += manuel.codeblock.Manuel()
manueltest = manuel.testing.TestFactory(m)
class MyTest(unittest.TestCase):
def setUp(self):
self.a = 1
self.globs = dict(c=9)
test1 = manueltest('doc1.ex')
@manueltest('doc2.ex')
def test2(self):
self.x = 5
test3 = manueltest('doc3.ex')
Here, we instantiated `TestFactory` with a `Manuel` instance to
create `manueltest`, which is a factory for creating Manuel-based
tests using on the given Manuel instance. We then used that to create
3 tests.
The first and third tests just execute tests in the named files,
`doc1.ex` and `doc3.ex`. The class' `setUp` method is used to set up
the test.
The second test also executes tests in a named file, `doc2.ex`, but it
decorates a function that provides additional setup code that runs
after the class setup code.
When tests are run this way:
- The test globals contain the test instance in the `test` variable.
- If a test case defines a `globs` attribute, it must be a dictionary
and it's contents are added to the test globals.
.. We can run these tests with the ``unittest`` test runner.
>>> loader = unittest.TestLoader()
>>> import sys
>>> sys.stdout.writeln = lambda s: sys.stdout.write(s+'\n')
>>> suite = loader.loadTestsFromTestCase(MyTest)
>>> result = suite.run(unittest.TextTestResult(sys.stdout, True, 3))
test1 (tests.MyTest) ... ok
test2 (tests.MyTest) ... ok
test3 (tests.MyTest) ... FAIL
>>> for _, e in result.errors:
... print(e); print
>>> for c, e in result.failures:
... print(e) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
Traceback (most recent call last):
...
----------------------------------------------------------------------
File "...doc3.ex", line 1, in doc3.ex
Failed example:
1
Expected:
2
Got:
1
Check meta data:
>>> MyTest.test1.__name__
'test_doc1'
>>> import os, manuel
>>> (MyTest.test1.filepath ==
... os.path.join(os.path.dirname(manuel.__file__), 'doc1.ex'))
True
>>> MyTest.test1.filename
'doc1.ex'
>>> (MyTest.test2.filepath ==
... os.path.join(os.path.dirname(manuel.__file__), 'doc2.ex'))
True
>>> MyTest.test2.filename
'doc2.ex'
Having __name__ around breaks other manuel tests, for some reason.
>>> del __name__
Customizing the TestCase class
------------------------------
Manuel has its own :class:`manuel.testing.TestClass` class that
:class:`manuel.testing.TestSuite` uses. If you want to customize it, you
can pass in your own class to `TestSuite`.
.. code-block:: python
import os.path
import manuel.testing
class StripDirsTestCase(manuel.testing.TestCase):
def shortDescription(self):
return os.path.basename(str(self))
suite = manuel.testing.TestSuite(
m, path_to_test, TestCase=StripDirsTestCase)
>>> list(suite)[0].shortDescription()
'bugs.txt'
.. reset-globs
.. _doctest:
Doctests
========
Manuel is all about making testable documents and well-documented tests. Of
course, Python's doctest module is a long-standing fixture in that space, so it
only makes sense for Manuel to support doctest syntax.
Handling doctests is easy:
.. ignore-next-block
.. code-block:: python
import manuel.doctest
m = manuel.doctest.Manuel()
suite = manuel.testing.TestSuite(m, 'my-doctest.txt')
Of course you can mix in other Manuel syntax plug-ins as well (including ones
you write yourself).
.. ignore-next-block
.. code-block:: python
import manuel.doctest
import manuel.codeblock
m = manuel.doctest.Manuel()
m += manuel.codeblock.Manuel()
suite = manuel.testing.TestSuite(m, 'my-doctest-with-code-blocks.txt')
The :class:`manuel.doctest.Manuel` constructor also takes :data:`optionflags`
and :data:`checker` arguments.
.. ignore-next-block
.. code-block:: python
m = manuel.doctest.Manuel(optionflags=optionflags, checker=checker)
See the `doctest documentation `_
for more information about the `available options
`_ and `output
checkers `_
.. note::
:mod:`zope.testing.renormalizing` provides an :class:`OutputChecker`
for smoothing out differences between actual and expected output for things
that are hard to control (like memory addresses and time). See the
`module's doctests `_
for more information on how it works. Here's a short example that
smoothes over the differences between CPython's and PyPy's NameError
messages:
.. code-block:: python
import re
import zope.testing.renormalizing
checker = zope.testing.renormalizing.RENormalizing([
(re.compile(r"NameError: global name '([a-zA-Z0-9_]+)' is not defined"),
r"NameError: name '\1' is not defined"),
])
.. reset-globs
.. _capture:
Capturing Blocks
================
When writing documentation the need often arises to describe the contents of
files or other non-Python information. You may also want to put that
information under test. :mod:`manuel.capture` helps with that.
For example, if you were writing the problems for a programming contest, you
might want to describe the input and output files for each challenge, but you
want to be sure that your examples are correct.
To do that you might write your document like this:
::
Challenge 1
===========
Write a program that sorts the numbers in a file.
Example
-------
Given this example input file::
6
1
8
20
11
65
2
.. -> input
Your program should generate this output file::
1
2
6
8
11
20
65
.. -> output
>>> input_lines = input.splitlines()
>>> correct = '\n'.join(map(str, sorted(map(int, input_lines)))) + '\n'
>>> output == correct
True
.. -> source
>>> import manuel
>>> document = manuel.Document(source)
>>> import manuel.capture
>>> m = manuel.capture.Manuel()
>>> import manuel.doctest
>>> m += manuel.doctest.Manuel()
>>> document.process_with(m, globs={})
>>> print(document.formatted())
This uses the syntax implemented in :mod:`manuel.capture` to capture a block of
text into a variable (the one named after "->").
Whenever a line of the structure ".. -> VAR" is detected, the text of the
*previous* block will be stored in the given variable.
.. the paragraph below could be phrased better
Of course, lines that start with ".. " are reST comments, so when the document
is rendered with docutils or Sphinx, the tests will dissapear and only the
intended document contents will remain. Like so::
Challenge 1
===========
Write a program that sorts the numbers in a file.
Example
-------
Given this example input file::
6
1
8
20
11
65
2
Your program should generate this output file::
1
2
6
8
11
20
65
.. reset-globs
.. _code-blocks:
Code Blocks
===========
`Sphinx `_ and other docutils `extensions
`_
provide a `"code-block" directive `_,
which allows inlined snippets of code in reST documents.
The :mod:`manuel.codeblock` module provides the ability to execute the contents
of Python code-blocks. For example::
.. code-block:: python
print('hello')
.. Let's create a reST document with a code block.
>>> import manuel.codeblock
>>> document = manuel.Document("""
... Here is a code-block:
...
... .. code-block:: python
...
... x = 'hello'
...
... A little prose to separate the examples.
...
... >>> print(x)
... hello
...
... """)
.. Since the above document mixes code-blocks and doctests, we'll mix in the
doctest handler.
>>> import manuel.doctest
>>> m = manuel.codeblock.Manuel()
>>> m += manuel.doctest.Manuel()
>>> document.process_with(m, globs={})
Both code blocks were found (for a total of five regions -- text, block,
text, block, and text):
>>> len(list(document))
5
We can see that none of the tests in the document failed:
>>> print(document.formatted())
If the code-block generates some sort of error...
.. code-block:: python
.. code-block:: python
print(does_not_exist)
.. -> source
>>> document = manuel.Document(source, location='fake.txt')
.. the document above was specially formulated to have nothing before or after
the code-block
>>> document.source.startswith('.. code-block')
True
>>> document.source.endswith('print(does_not_exist)\n')
True
...that error will be reported:
>>> document.process_with(m, globs={})
Traceback (most recent call last):
...
NameError: name 'does_not_exist' is not defined
If you find that you want to include a code-block in a document but don't want
Manuel to execute it, use :ref:`manuel.ignore ` to ignore that
particular block.
.. reset-globs
Docutils Code Blocks
--------------------
Sphinx and docutils have different ideas of how code blocks should be spelled.
Manuel supports the docutils-style code blocks too.
::
.. code:: python
a = 1
.. -> source
>>> import manuel
>>> document = manuel.Document(source)
>>> import manuel.codeblock
>>> m = manuel.codeblock.Manuel()
>>> document.parse_with(m)
>>> for region in document:
... print((region.lineno, region.parsed or region.source))
(1, )
Docutils options after the opening of the code block are also allowed::
.. code:: python
:class: hidden
a = 1
.. -> source
>>> import manuel
>>> document = manuel.Document(source)
>>> import manuel.codeblock
>>> m = manuel.codeblock.Manuel()
>>> document.parse_with(m)
>>> for region in document:
... print((region.lineno, region.parsed, region.parsed.source))
(1, , '\na = 1\n')
Invisible Code Blocks
---------------------
At times you'll want to have a block of code that is executed but not displayed
in the rendered document (like some setup for later examples).
When using doctest's native format (">>>") that's easy to do, you just put the
code in a reST comment, like so:
::
.. this is some setup, it is hidden in a reST comment
>>> a = 5
>>> b = a + 3
However, if you want to include a relatively large chunk of Python, you'd
rather use a code-block, but that means that it will be included in the
rendered document. Instead, :mod:`manuel.codeblock` also understands a variant
of the code-block directive that is actually a reST comment: "..
invisible-code-block:: python"::
.. invisible-code-block:: python
a = 5
b = a + 3
.. -> source
>>> import manuel
>>> document = manuel.Document(source)
>>> document.process_with(m, globs={})
>>> print(document.formatted())
.. note:: The "invisible-code-block" directive will work with either one or two
colons. The reason is that reST processers (like docutils and Sphinx) will
generate an error for unrecognized directives (like invisible-code-block).
Therefore you can use a single colon and the line will be interpreted as a
comment instead.
.. the single-colon variant works too
>>> document = manuel.Document("""
...
... .. invisible-code-block: python
...
... raise RuntimeError('it worked!')
...
... """)
>>> document.process_with(m, globs={})
Traceback (most recent call last):
...
RuntimeError: it worked!
.. reset-globs
.. _footnotes:
Footnotes
=========
The :mod:`manuel.footnote` module provides an implementation of reST footnote
handling, but instead of just plain text, the footnotes can contain any syntax
Manuel can interpret including doctests.
>>> import manuel.footnote
>>> m = manuel.footnote.Manuel()
Here's an example of combining footnotes with doctests:
.. so we also need the doctest Manuel plug-in
>>> import manuel.doctest
>>> m += manuel.doctest.Manuel()
::
Here we reference a footnote. [1]_
>>> x
42
Here we reference another. [2]_
>>> x
100
.. [1] This is a test footnote definition.
>>> x = 42
.. [2] This is another test footnote definition.
>>> x = 100
.. [3] This is a footnote that will never be executed.
>>> raise RuntimeError('nooooo!')
.. -> source
>>> import manuel
>>> document = manuel.Document(source)
>>> document.process_with(m, globs={})
>>> print(document.formatted())
.. The order of examples in footnotes is preserved. If not, the document below
would generate an error because "a" won't be defined when "b = a + 1" is
evaluated.
>>> document = manuel.Document("""
... Here we want some imports to be done. [foo]_
...
... >>> a + b
... 3
...
... A little prose to separate the examples.
...
... .. [foo] Do something
...
... >>> a = 1
...
... >>> b = a + 1
...
... """)
>>> document.process_with(m, globs={})
>>> print(document.formatted())
It is also possible to reference more than one footnote on a single line.
::
This line has several footnotes on it. [1]_ [2]_ [3]_
>>> z
105
A little prose to separate the examples.
.. [1] Do something
>>> w = 3
.. [2] Do something
>>> x = 5
.. [3] Do something
>>> y = 7
>>> z = w * x * y
.. -> source2
>>> document = manuel.Document(source)
>>> document.process_with(m, globs={})
>>> print(document.formatted())
.. reset-globs
.. _ignore:
Ignoring Blocks
===============
.. reset-globs
Occasionally the need arises to ignore a block of markup that would otherwise
be parsed by a Manuel plug-in.
For example, this document has a code-block that will generate a syntax error::
The following is invalid Python.
.. code-block:: python
def foo:
pass
.. -> source
>>> import manuel
>>> document = manuel.Document(source)
>>> import manuel.codeblock
>>> m = manuel.codeblock.Manuel()
We can see that when executed, the SyntaxError escapes.
>>> import manuel.codeblock
>>> m = manuel.codeblock.Manuel()
>>> document.process_with(m, globs={})
Traceback (most recent call last):
...
File ":4", line 2
def foo:
^
SyntaxError: invalid syntax...
The :mod:`manuel.ignore` module provides a way to ignore parts of a document
using a directive ".. ignore-next-block".
Because Manuel plug-ins are executed in the order they are accumulated, we want
:mod:`manuel.ignore` to be the base Manuel object, with any additional plug-ins
added to it.
.. code-block:: python
import manuel.ignore
import manuel.doctest
m = manuel.ignore.Manuel()
m += manuel.codeblock.Manuel()
m += manuel.doctest.Manuel()
If we add an ignore marker to the block we don't want processed...
.. code-block:: python
The following is invalid Python.
.. ignore-next-block
.. code-block:: python
def foo:
pass
.. -> source
>>> document = manuel.Document(source)
...the error goes away.
>>> document.process_with(m, globs={})
>>> print(document.formatted())
Ignoring Literal Blocks
-----------------------
Ignoring literal blocks is a little more involved::
Here is some invalid Python:
.. ignore-next-block
::
>>> lambda: x=1
.. -> source
>>> document = manuel.Document(source)
>>> document.process_with(m, globs={})
>>> print(document.formatted())
.. we want to be very sure that the above example without the ignore actually
generates an error:
>>> document = manuel.Document(document.source.replace(
... '.. ignore-next-block', ''))
>>> document.process_with(m, globs={})
>>> print(document.formatted())
File ""...
Exception raised:
...
SyntaxError: ...
.. reset-globs
.. _isolation:
Test Isolation
==============
One of the advantages of unittest over doctest is that the individual tests are
isolated from one-another.
In large doctests (like this one) you may want to keep later tests from
depending on incidental details of earlier tests, preventing the tests from
becoming brittle and harder to change.
Test isolation is one approach to reducing this intra-doctest coupling. The
:mod:`manuel.isolation` module provides a plug-in to help.
The ".. reset-globs" directive resets the globals in the test::
We define a variable.
>>> x = 'hello'
It is still defined.
>>> print(x)
hello
Now we can reset the globals...
.. reset-globs
...and the name binding will be gone:
>>> print(x)
Traceback (most recent call last):
...
NameError: name 'x' is not defined
.. -> source
>>> import manuel
>>> document = manuel.Document(source)
>>> import manuel.isolation
>>> import manuel.doctest
>>> m = manuel.isolation.Manuel()
>>> m += manuel.doctest.Manuel(checker=checker)
We can see that after the globals have been reset, the second "print(x)" line
raises an error.
Of course, resetting to an empty set of global variables isn't always what's
wanted. In that case there is a ".. capture-globs" directive that saves a
baseline set of globals that will be restored at each reset.
::
We define a variable.
>>> x = 'hello'
It is still defined.
>>> print(x)
hello
We can capture the currently defined globals:
.. capture-globs
Of course capturing the globals doesn't disturb them.
>>> print(x)
hello
Now if we define a new global...
>>> y = 'goodbye'
>>> print(y)
goodbye
.. reset-globs
...it will disappear after a reset.
>>> print(y)
Traceback (most recent call last):
...
NameError: name 'y' is not defined
But the captured globals will still be defined.
>>> print(x)
hello
.. -> source
>>> import manuel
>>> document = manuel.Document(source)
>>> document.process_with(m, globs={})
>>> print(document.formatted())
.. reset-globs
.. _testcase:
Identifying Test Cases
======================
If you want parts of a document to be individually accessible as test cases (to
be able to run just a particular subset of them, for example), a parser can
create a region that marks the beginning of a new test case.
Two ways of identifying test cases are included in :mod:`manuel.testcase`:
1. by section headings
2. by explicit ".. test-case: NAME" markers.
Grouping Tests by Heading
-------------------------
::
First Section
=============
Some prose.
>>> print('first test case')
Some more prose.
>>> print('still in the first test case')
Second Section
==============
Even more prose.
>>> print('second test case')
.. -> source
>>> import manuel
>>> import manuel.testcase
>>> document = manuel.Document(source)
>>> m = manuel.testcase.SectionManuel()
>>> m += manuel.doctest.Manuel()
>>> document.process_with(m, globs={})
>>> print(document.formatted())
File ""...
Failed example:
print('first test case')
Expected nothing
Got:
first test case
File ""...
Failed example:
print('still in the first test case')
Expected nothing
Got:
still in the first test case
File ""...
Failed example:
print('second test case')
Expected nothing
Got:
second test case
.. now lets see if the regions are grouped as we expect
>>> import manuel.testing
>>> for regions in manuel.testing.group_regions_by_test_case(document):
... print((regions.location, regions.id))
('', None)
('', 'First Section')
('', 'Second Section')
Given the above document, if you're using zope.testing's testrunner (located in bin/test), you could run just the tests in the second section with this command::
bin/test -t "file-name.txt:Second Section"
Or, exploiting the fact that -t does a regex search (as opposed to a match)::
bin/test -t file-name.txt:Second
Grouping Tests Explicitly
-------------------------
If you would like to identify test cases separately from sections, you can
identify them with a marker::
First Section
=============
The following test will be in a test case that is not individually
identifiable.
>>> print('first test case (unidentified)')
Some more prose.
.. test-case: first-named-test-case
>>> print('first identified test case')
Second Section
==============
The test case markers don't have to immediately proceed a test.
.. test-case: second-named-test-case
Even more prose.
>>> print('second identified test case')
.. -> source
>>> document = manuel.Document(source)
>>> m = manuel.testcase.MarkerManuel()
>>> m += manuel.doctest.Manuel()
>>> document.parse_with(m)
>>> import six
>>> for regions in manuel.testing.group_regions_by_test_case(document):
... six.print_(regions.location, regions.id)
None
first-named-test-case
second-named-test-case
Again, given the above document and zope.testing, you could run just the second
set of tests with this command::
bin/test -t file-name.txt:second-named-test-case
Or, exploiting the fact that -t does a regex search again::
bin/test -t file-name.txt:second
Even though the tests are individually accessable doesn't mean that they can't
all be run at the same time::
bin/test -t file-name.txt
Also, if you create a hierarchy of names, you can run groups of tests at a
time. For example, lets say that you append "-important" to all your really
important tests, you could then run the important tests for a single document
like so::
bin/test -t 'file-name.txt:.*-important$'
or all the "important" tests no matter what file they are in::
bin/test -t '-important$'
Both Methods
------------
You can also combine more than one test case identification method if you want.
Here's an example of building a Manuel stack that has doctests and both flavors
of test case identification:
.. code-block:: python
import manuel.doctest
import manuel.testcase
m = manuel.doctest.Manuel()
m += manuel.testcase.SectionManuel()
m += manuel.testcase.MarkerManuel()
.. make sure above finds all the test cases appropriately
>>> document.parse_with(m)
>>> import six
>>> for regions in manuel.testing.group_regions_by_test_case(document):
... six.print_(regions.location, regions.id)
None
First Section
first-named-test-case
Second Section
second-named-test-case
Further Reading
===============
.. toctree::
:maxdepth: 1
README.txt
table-example.txt
bugs.txt
manuel-1.10.1/src/manuel/ignore.py 0000644 0001750 0001750 00000000774 13373300442 015747 0 ustar james james import re
import manuel
import textwrap
IGNORE_START = re.compile(r'^\.\.\s*ignore-next-block\s*$', re.MULTILINE)
IGNORE_END = re.compile(r'(?