pax_global_header 0000666 0000000 0000000 00000000064 12361210771 0014512 g ustar 00root root 0000000 0000000 52 comment=87936b7945cc899ef4198104b0ac54617681fd49
manuel-1.8.0/ 0000775 0000000 0000000 00000000000 12361210771 0013001 5 ustar 00root root 0000000 0000000 manuel-1.8.0/.gitignore 0000664 0000000 0000000 00000000143 12361210771 0014767 0 ustar 00root root 0000000 0000000 .installed.cfg
bin/
develop-eggs/
dist/
docs/
src/manuel.egg-info/
parts/
*.pyc
__pycache__/
.tox/
manuel-1.8.0/.travis.yml 0000664 0000000 0000000 00000000350 12361210771 0015110 0 ustar 00root root 0000000 0000000 language: python
python:
- 2.6
- 2.7
- 3.3
- 3.4
- pypy
install:
- travis_retry pip install .
- travis_retry pip install zope.testing
script:
- python setup.py test -q
notifications:
email: false
manuel-1.8.0/CHANGES.rst 0000664 0000000 0000000 00000010603 12361210771 0014603 0 ustar 00root root 0000000 0000000 CHANGES
=======
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.8.0/COPYRIGHT.rst 0000664 0000000 0000000 00000000572 12361210771 0015107 0 ustar 00root root 0000000 0000000 Copyright Benji York and Contributors.
All Rights Reserved.
This software is subject to the provisions of the Apache License, Version
2.0.
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.
manuel-1.8.0/LICENSE.rst 0000664 0000000 0000000 00000026136 12361210771 0014625 0 ustar 00root root 0000000 0000000
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
manuel-1.8.0/MANIFEST.in 0000664 0000000 0000000 00000000233 12361210771 0014535 0 ustar 00root root 0000000 0000000 recursive-include src *.txt
recursive-include docs *
recursive-include sphinx *.py
include *.rst
include tox.ini
include bootstrap.py
include buildout.cfg
manuel-1.8.0/README.rst 0000664 0000000 0000000 00000000305 12361210771 0014466 0 ustar 00root root 0000000 0000000 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.8.0/bootstrap.py 0000664 0000000 0000000 00000014016 12361210771 0015372 0 ustar 00root root 0000000 0000000 ##############################################################################
#
# 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.8.0/buildout.cfg 0000664 0000000 0000000 00000002273 12361210771 0015315 0 ustar 00root root 0000000 0000000 [buildout]
develop = .
parts = test interpreter sphinx-docs-html build-docs
#allow-picked-versions = false
use-dependency-links = false
[test]
recipe = zc.recipe.testrunner
eggs = manuel [tests]
defaults = '--tests-pattern tests --exit-with-status -1 --auto-color'.split()
working-directory = ${buildout:directory}
[interpreter]
recipe = zc.recipe.egg
eggs = manuel
interpreter = py
# generate a script that will build the user docs (HTML)
[sphinx-docs-html]
recipe = zc.recipe.egg:script
eggs =
docutils
Sphinx
scripts = sphinx-build=docs
base-sphinx-args = ('-N -c ${buildout:directory}/sphinx ${buildout:directory}/src/manuel ${buildout:directory}/docs'.split())
arguments = sys.argv + ${sphinx-docs-html:base-sphinx-args}
initialization =
# build the (HTML) user docs each time the buildout is run
[build-docs]
recipe = iw.recipe.cmd
on_install = true
on_update = true
cmds = ${buildout:directory}/bin/docs
[versions]
Jinja2 = 2.6
Pygments = 1.6
Sphinx = 1.1.3
distribute = 0.6.35
docutils = 0.10
iw.recipe.cmd = 0.3
six = 1.2.0
zc.buildout = 2.0.1
zc.recipe.egg = 2.0.0a3
zc.recipe.testrunner = 2.0.0
zope.exceptions = 4.0.6
zope.interface = 4.0.5
zope.testing = 4.1.2
zope.testrunner = 4.3.3
manuel-1.8.0/setup.py 0000664 0000000 0000000 00000003671 12361210771 0014522 0 ustar 00root root 0000000 0000000 ##############################################################################
#
# Copyright Benji York and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Apache License, Version
# 2.0.
#
# 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.
#
##############################################################################
"""Setup for manuel package."""
from setuptools import setup, find_packages
with open('README.rst') as readme:
with open('CHANGES.rst') as changes:
long_description = readme.read() + '\n\n' + changes.read()
tests_require = ['zope.testing']
setup(
name='manuel',
version='1.8.0',
url='http://pypi.python.org/pypi/manuel',
packages=find_packages('src'),
package_dir={'': 'src'},
zip_safe=False,
author='Benji York',
author_email='benji@benjiyork.com',
description='Manuel lets you build tested documentation.',
classifiers=[
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
'License :: OSI Approved :: Apache Software License',
],
license='Apache Software License, Version 2.0',
extras_require={
'tests': tests_require,
},
tests_require=tests_require,
test_suite='manuel.tests.test_suite',
install_requires=[
'setuptools',
'six',
],
include_package_data=True,
long_description=long_description,
)
manuel-1.8.0/sphinx/ 0000775 0000000 0000000 00000000000 12361210771 0014312 5 ustar 00root root 0000000 0000000 manuel-1.8.0/sphinx/conf.py 0000664 0000000 0000000 00000000521 12361210771 0015607 0 ustar 00root root 0000000 0000000 source_suffix = '.txt'
master_doc = 'index'
project = 'Manuel'
copyright = 'Benji York'
version = '1'
release = '1'
today_fmt = '%Y-%m-%d'
pygments_style = 'sphinx'
html_last_updated_fmt = '%Y-%m-%d'
html_title = 'Manuel Documentation'
todo_include_todos = False
exclude_dirnames = ['manuel.egg-info']
unused_docs = ['manuel/capture']
manuel-1.8.0/src/ 0000775 0000000 0000000 00000000000 12361210771 0013570 5 ustar 00root root 0000000 0000000 manuel-1.8.0/src/manuel/ 0000775 0000000 0000000 00000000000 12361210771 0015051 5 ustar 00root root 0000000 0000000 manuel-1.8.0/src/manuel/README.txt 0000664 0000000 0000000 00000046513 12361210771 0016560 0 ustar 00root root 0000000 0000000 .. _theory-of-operation:
Theory of Operation
===================
.. XXX this really wants to be a "How To Write a Plug-in" tutorial.
Manuel parses documents (tests), evaluates their contents, then formats the
result of the evaluation. The functionality is accessed via the :mod:`manuel`
package.
>>> import manuel
Parsing
-------
Manuel operates on Documents. Each Document is created from a string
containing one or more lines.
>>> source = """\
... This is our document, it has several lines.
... one: 1, 2, 3
... two: 4, 5, 7
... three: 3, 5, 1
... """
>>> document = manuel.Document(source)
For example purposes we will create a type of test that consists of a sequence
of numbers. Lets create a NumbersTest object to represent the parsed list.
>>> class NumbersTest(object):
... def __init__(self, description, numbers):
... self.description = description
... self.numbers = numbers
The Document is divided into one or more regions. Each region is a distinct
"chunk" of the document and will be acted uppon in later (post-parsing) phases.
Initially the Document is made up of a single element, the source string.
>>> [region.source for region in document]
['This is our document, it has several lines.\none: 1, 2, 3\ntwo: 4, 5, 7\nthree: 3, 5, 1\n']
The Document offers a "find_regions" method to assist in locating the portions
of the document a particular parser is interested in. Given a regular
expression (either as a string, or compiled), it will return "region" objects
that contain the matched source text, the line number (1 based) the region
begins at, as well as the associated re.Match object.
>>> import re
>>> numbers_test_finder = re.compile(
... r'^(?P.*?): (?P(\d+,?[ ]?)+)$', re.MULTILINE)
>>> regions = document.find_regions(numbers_test_finder)
>>> regions
[,
,
]
>>> regions[0].lineno
2
>>> regions[0].source
'one: 1, 2, 3\n'
>>> regions[0].start_match.group('description')
'one'
>>> regions[0].start_match.group('numbers')
'1, 2, 3'
If given two regular expressions find_regions will use the first to identify
the begining of a region and the second to identify the end.
>>> region = document.find_regions(
... re.compile('^one:.*$', re.MULTILINE),
... re.compile('^three:.*$', re.MULTILINE),
... )[0]
>>> region.lineno
2
>>> six.print_(region.source)
one: 1, 2, 3
two: 4, 5, 7
three: 3, 5, 1
Also, instead of just a "start_match" attribute, the region will have
start_match and end_match attributes.
>>> region.start_match
<_sre.SRE_Match object...>
>>> region.end_match
<_sre.SRE_Match object...>
Regions must always consist of whole lines.
>>> document.find_regions('1, 2, 3')
Traceback (most recent call last):
...
ValueError: Regions must start at the begining of a line.
.. more "whole-line" tests.
>>> document.find_regions(
... re.compile('ne:.*$', re.MULTILINE),
... re.compile('^one:.*$', re.MULTILINE),
... )
Traceback (most recent call last):
...
ValueError: Regions must start at the begining of a line.
Now we can register a parser that will identify the regions we're interested in
and create NumbersTest objects from the source text.
>>> def parse(document):
... for region in document.find_regions(numbers_test_finder):
... description = region.start_match.group('description')
... numbers = list(map(
... int, region.start_match.group('numbers').split(',')))
... test = NumbersTest(description, numbers)
... document.claim_region(region)
... region.parsed = test
>>> parse(document)
>>> [region.source for region in document]
['This is our document, it has several lines.\n',
'one: 1, 2, 3\n',
'two: 4, 5, 7\n',
'three: 3, 5, 1\n']
>>> [region.parsed for region in document]
[None,
,
,
]
Evaluation
----------
After a document has been parsed the resulting tests are evaluated. Unlike
parsing and formatting, evaluation is done one region at a time, in the order
that the regions appear in the document. Lets define a function to evaluate
NumberTests. The function determines whether or not the numbers are in sorted
order and records the result along with the description of the list of numbers.
.. code-block:: python
class NumbersResult(object):
def __init__(self, test, passed):
self.test = test
self.passed = passed
def evaluate(region, document, globs):
if not isinstance(region.parsed, NumbersTest):
return
test = region.parsed
passed = sorted(test.numbers) == test.numbers
region.evaluated = NumbersResult(test, passed)
.. a test of the above
>>> for region in document:
... evaluate(region, document, {})
>>> [region.evaluated for region in document]
[None,
,
,
]
Formatting
----------
Once the evaluation phase is completed the results are formatted. You guessed
it: Manuel provides a method for formatting results. We'll build one to format
a message about whether or not our lists of numbers are sorted properly. A
formatting function returns None when it has no output, or a string otherwise.
.. code-block:: python
def format(document):
for region in document:
if not isinstance(region.evaluated, NumbersResult):
continue
result = region.evaluated
if not result.passed:
region.formatted = (
"the numbers aren't in sorted order: %s\n"
% ', '.join(map(str, result.test.numbers)))
Since one of the test cases failed we get an appropriate message out of the
formatter.
>>> format(document)
>>> [region.formatted for region in document]
[None, None, None, "the numbers aren't in sorted order: 3, 5, 1\n"]
Manuel Objects
--------------
We'll want to use these parse, evaluate, and format functions later, so we
bundle them together into a Manuel object.
>>> sorted_numbers_manuel = manuel.Manuel(
... parsers=[parse], evaluaters=[evaluate], formatters=[format])
Doctests
--------
We can use Manuel to run doctests. Let's create a simple doctest to
demonstrate with.
>>> source = """This is my
... doctest.
...
... >>> 1 + 1
... 2
... """
>>> document = manuel.Document(source)
The :mod:`manuel.doctest` module has handlers for the various phases. First
we'll look at parsing.
>>> import manuel.doctest
>>> m = manuel.doctest.Manuel()
>>> document.parse_with(m)
>>> for region in document:
... print((region.lineno, region.parsed or region.source))
(1, 'This is my\ndoctest.\n\n')
(4, )
Now we can evaluate the examples.
>>> document.evaluate_with(m, globs={})
>>> for region in document:
... print((region.lineno, region.evaluated or region.source))
(1, 'This is my\ndoctest.\n\n')
(4, )
And format the results.
>>> document.format_with(m)
>>> document.formatted()
''
Oh, we didn't have any failing tests, so we got no output. Let's try again
with a failing test. This time we'll use the "process_with" function to
simplify things.
>>> document = manuel.Document("""This is my
... doctest.
...
... >>> 1 + 1
... 42
... """)
>>> document.process_with(m, globs={})
>>> six.print_(document.formatted(), end='')
File "", line 4, in
Failed example:
1 + 1
Expected:
42
Got:
2
Alternate doctest parsers
~~~~~~~~~~~~~~~~~~~~~~~~~
You can pass an alternate doctest parser to manuel.doctest.Manuel to
customize how examples are parsed. Here's an example that changes the
example start string from ">>>" to "py>":
>>> import doctest
>>> class DocTestPyParser(doctest.DocTestParser):
... _EXAMPLE_RE = re.compile(r'''
... (?P
... (?:^(?P [ ]*) py> .*) # PS1 line
... (?:\n [ ]* \.\.\. .*)*) # PS2 lines
... \n?
... (?P (?:(?![ ]*$) # Not a blank line
... (?![ ]*py>) # Not a line starting with PS1
... .*$\n? # But any other line
... )*)
... ''', re.MULTILINE | re.VERBOSE)
>>> m = manuel.doctest.Manuel(parser=DocTestPyParser())
>>> document = manuel.Document("""This is my
... doctest.
...
... py> 1 + 1
... 42
... """)
>>> document.process_with(m, globs={})
>>> six.print_(document.formatted(), end='')
File "", line 4, in
Failed example:
1 + 1
Expected:
42
Got:
2
Multiple doctest parsers
~~~~~~~~~~~~~~~~~~~~~~~~
You may use several doctest parsers in the same session, for example,
to support shell commands and Python code in the same document.
>>> m = (manuel.doctest.Manuel(parser=DocTestPyParser()) +
... manuel.doctest.Manuel())
>>> document = manuel.Document("""
...
... py> i = 0
... py> i += 1
... py> i
... 1
...
... >>> j = 0
... >>> j += 1
... >>> j
... 1
...
... """)
>>> document.process_with(m, globs={})
>>> six.print_(document.formatted(), end='')
Globals
-------
Even though each region is parsed into its own object, state is still shared
between them. Each region of the document is executed in order so state
changes made by earlier evaluaters are available to the current evaluator.
>>> document = manuel.Document("""
... >>> x = 1
...
... A little prose to separate the examples.
...
... >>> x
... 1
... """)
>>> document.process_with(m, globs={})
>>> six.print_(document.formatted(), end='')
Imported modules are added to the global namespace as well.
>>> document = manuel.Document("""
... >>> import string
...
... A little prose to separate the examples.
...
... >>> string.digits
... '0123456789'
...
... """)
>>> document.process_with(m, globs={})
>>> six.print_(document.formatted(), end='')
Combining Test Types
--------------------
Now that we have both doctests and the silly "sorted numbers" tests, let's
create a single document that has both.
>>> document = manuel.Document("""
... We can have a list of numbers...
...
... a very nice list: 3, 6, 2
...
... ... and we can test Python.
...
... >>> 1 + 1
... 42
...
... """)
Obviously both of those tests will fail, but first we have to configure Manuel
to understand both test types. We'll start with a doctest configuration and add
the number list testing on top.
>>> m = manuel.doctest.Manuel()
Since we already have a Manuel instance configured for our "sorted numbers"
tests, we can extend the built-in doctest configuration with it.
>>> m += sorted_numbers_manuel
Now we can process our source that combines both types of tests and see what
we get.
>>> document.process_with(m, globs={})
The document was parsed and has a mixture of prose and parsed doctests and
number tests.
>>> for region in document:
... print((region.lineno, region.parsed or region.source))
(1, '\nWe can have a list of numbers...\n\n')
(4, )
(5, '\n... and we can test Python.\n\n')
(8, )
(10, '\n')
We can look at the formatted output to see that each of the two tests failed.
>>> for region in document:
... if region.formatted:
... six.print_('-'*70)
... six.print_(region.formatted, end='')
----------------------------------------------------------------------
the numbers aren't in sorted order: 3, 6, 2
----------------------------------------------------------------------
File "", line 8, in
Failed example:
1 + 1
Expected:
42
Got:
2
Priorities
----------
Some functionality requires that code be called early or late in a phase. The
"timing" decorator allows either EARLY or LATE to be specified.
Early functions are run first (in arbitrary order), then functions with no
specified timing, then the late functions are called (again in arbitrary
order). This function also demonstrates the "copy" method of Region objects
and the "insert_region_before" and "insert_region_after" methods of Documents.
>>> @manuel.timing(manuel.LATE)
... def cloning_parser(document):
... to_be_cloned = None
... # find the region to clone
... document_iter = iter(document)
... for region in document_iter:
... if region.parsed:
... continue
... if region.source.strip().endswith('my clone:'):
... to_be_cloned = six.advance_iterator(document_iter).copy()
... break
... # if we found the region to cloned, do so
... if to_be_cloned:
... # make a copy since we'll be mutating the document
... for region in list(document):
... if region.parsed:
... continue
... if 'clone before *here*' in region.source:
... clone = to_be_cloned.copy()
... clone.provenance = 'cloned to go before'
... document.insert_region_before(region, clone)
... if 'clone after *here*' in region.source:
... clone = to_be_cloned.copy()
... clone.provenance = 'cloned to go after'
... document.insert_region_after(region, clone)
>>> m.add_parser(cloning_parser)
>>> source = """\
... This is my clone:
...
... clone: 1, 2, 3
...
... I want some copies of my clone.
...
... For example, I'd like a clone before *here*.
...
... I'd also like a clone after *here*.
... """
>>> document = manuel.Document(source)
>>> document.process_with(m, globs={})
>>> [(r.source, r.provenance) for r in document]
[('This is my clone:\n\n', None),
('clone: 1, 2, 3\n', None),
('clone: 1, 2, 3\n', 'cloned to go before'),
("\nI want some copies of my clone.\n\nFor example, I'd like a clone before *here*.\n\nI'd also like a clone after *here*.\n", None),
('clone: 1, 2, 3\n', 'cloned to go after')]
Enhancing Existing Manuels
--------------------------
Lets say that you'd like failed doctest examples to give more information about
what went wrong.
First we'll create an evaluater that includes pertinant variable binding
information on failures.
.. code-block:: python
import doctest
def informative_evaluater(region, document, globs):
if not isinstance(region.parsed, doctest.Example):
return
if region.evaluated.getvalue():
info = ''
for name in sorted(globs):
if name in region.parsed.source:
info += '\n ' + name + ' = ' + repr(globs[name])
if info:
region.evaluated.write('Additional Information:')
region.evaluated.write(info)
To do that we'll start with an instance of :mod:`manuel.doctest.Manuel` and add
in our additional functionality.
>>> m = manuel.doctest.Manuel()
>>> m.add_evaluater(informative_evaluater)
Now we'll create a document that includes a failing test.
>>> document = manuel.Document("""
... Set up some variable bindings:
...
... >>> a = 1
... >>> b = 2
... >>> c = 3
...
... Make an assertion:
...
... >>> a + b
... 5
... """)
When we run the document through our Manuel instance, we see the additional
information.
>>> document.process_with(m, globs={})
>>> six.print_(document.formatted(), end='')
File "", line 10, in
Failed example:
a + b
Expected:
5
Got:
3
Additional Information:
a = 1
b = 2
Note how only the referenced variable bindings are displayed (i.e., "c" is not
listed). That's pretty nice, but the way interesting variables are identified
is a bit of a hack. For example, if a variable's name just happens to appear
in the source (in a comment for example), it will be included in the output:
>>> document = manuel.Document("""
... Set up some variable bindings:
...
... >>> a = 1
... >>> b = 2
... >>> c = 3
...
... Make an assertion:
...
... >>> a + b # doesn't mention "c"
... 5
... """)
>>> document.process_with(m, globs={})
>>> six.print_(document.formatted(), end='')
File "", line 10, in
Failed example:
a + b # doesn't mention "c"
Expected:
5
Got:
3
Additional Information:
a = 1
b = 2
c = 3
Instead of a text-based apprach, let's use the built-in tokenize module to more
robustly identify referenced variables.
>>> from six import StringIO
>>> import token
>>> import tokenize
>>> def informative_evaluater_2(region, document, globs):
... if not isinstance(region.parsed, doctest.Example):
... return
...
... if region.evaluated.getvalue():
... vars = set()
... reader = StringIO(region.source).readline
... for ttype, tval, _, _, _ in tokenize.generate_tokens(reader):
... if ttype == token.NAME:
... vars.add(tval)
...
... info = ''
... for name in sorted(globs):
... if name in vars:
... info += '\n ' + name + ' = ' + repr(globs[name])
...
... if info:
... region.evaluated.write('Additional Information:')
... region.evaluated.write(info)
>>> m = manuel.doctest.Manuel()
>>> m.add_evaluater(informative_evaluater_2)
Now when we have a failure, only the genuinely referenced variables will be
included in the debugging information.
>>> document = manuel.Document(document.source)
>>> document.process_with(m, globs={})
>>> six.print_(document.formatted(), end='')
File "", line 10, in
Failed example:
a + b # doesn't mention "c"
Expected:
5
Got:
3
Additional Information:
a = 1
b = 2
Defining Test Cases
-------------------
If you want parts of a document to be accessable individually as test cases (to
be able to run just a particular part of a document, for example), a parser can
create a region that marks the beginning of a new test case.
.. code-block:: python
new_test_case_regex = re.compile(r'^.. new-test-case: \w+', re.MULTILINE)
def parse(document):
for region in document.find_regions(new_test_case_regex):
document.claim_region(region)
id = region.start_match.group(1)
region.parsed = manuel.testing.TestCaseMarker(id)
XXX finish this section
manuel-1.8.0/src/manuel/__init__.py 0000664 0000000 0000000 00000025113 12361210771 0017164 0 ustar 00root root 0000000 0000000 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.8.0/src/manuel/bugs.txt 0000664 0000000 0000000 00000013777 12361210771 0016571 0 ustar 00root root 0000000 0000000 Fixed Bugs
==========
Here are demonstrations of various bugs that have been fixed in Manuel. If you
encounter a bug in a previous version of Manuel, check here in the newest
version to see if your bug has been addressed.
Start and End Coinciding
------------------------
If a line of text matches both a "start" and "end" regular expression, no
exception should be raised.
>>> source = """\
... Blah, blah.
...
... xxx
... some text
... xxx
...
... """
>>> import manuel
>>> document = manuel.Document(source)
>>> import re
>>> start = end = re.compile(r'^xxx$', re.MULTILINE)
>>> document.find_regions(start, end)
[ source
.. code-block:: python
import manuel.codeblock
m = manuel.codeblock.Manuel()
manuel.Document(source).parse_with(m)
Code-block options with hyphens
-------------------------------
The code-block handler reST option parsing used to not allow for options with
hyphens in their name, so blocks like this one would generate a syntax error:
.. code:: python
:number-lines:
class Foo(object):
pass
.. -> source
.. code-block:: python
import manuel.codeblock
m = manuel.codeblock.Manuel()
manuel.Document(source).parse_with(m)
Empty documents
---------------
While empty documents aren't useful, they are still documents containing
no tests, and shouldn't break the test suite.
>>> document = manuel.Document('')
>>> document.source
'\n'
Glob lifecycle
--------------
Anything put into the globs during a doctest run should still be in there
afterward.
>>> a
1
>>> b = 2
.. -> source
.. code-block:: python
import manuel.doctest
m = manuel.doctest.Manuel()
globs = {'a': 1}
document = manuel.Document(source)
document.process_with(m, globs=globs)
The doctest in the `source` variable ran with no errors.
>>> six.print_(document.formatted())
And now the globs dictionary reflects the changes made when the doctest ran.
>>> globs['b']
2
zope.testing.module
-------------------
At one point, because of the way manuel.doctest handles glob dictionaries,
zope.testing.module didn't work.
We need a globs dictionary.
>>> globs = {'foo': 1}
To call the setUp and tearDown functions, we need to set up a fake test
object that uses our globs dict from above.
.. code-block:: python
class FakeTest(object):
def __init__(self):
self.globs = globs
test = FakeTest()
Now we will use the globs as a module.
>>> import zope.testing.module
>>> zope.testing.module.setUp(test, 'fake')
Now if we run this test through Manuel, the fake module machinery works.
The items put into the globs before the test are here.
>>> import fake
>>> fake.foo
1
And if we create new bindings, they appear in the module too.
>>> bar = 2
>>> fake.bar
2
.. -> source
.. code-block:: python
import manuel.doctest
m = manuel.doctest.Manuel()
document = manuel.Document(source)
document.process_with(m, globs=globs)
The doctest in the `source` variable ran with no errors.
>>> six.print_(document.formatted())
We should clean up now.
>>> import zope.testing.module
>>> zope.testing.module.tearDown(test)
Debug flag and adding instances
-------------------------------
The unittest integration (manuel.testing) sets the debug attribute on Manuel
objects. Manuel instances that result from adding instances together need to
have the debug value passed to each Manuel instances that was added together.
>>> m1 = manuel.Manuel()
>>> m2 = manuel.Manuel()
The debug flag starts off false...
>>> m1.debug
False
>>> m2.debug
False
...but if we set it add the two instances together and set the flag on on the
resulting instance, the other one gets the value too.
>>> m3 = m1 + m2
>>> m3.debug = True
>>> m1.debug
True
>>> m2.debug
True
>>> m3.debug
True
TestCase id methods
-------------------
Twisted's testrunner, trial, makes use of the id method of TestCase instances
in a way that requires it to be a meaningful string.
For manuel.testing.TestCase instances, this used to return None. As you can
see below, the manuel.testing.TestCase.shortDescription is now returned
instead:
>>> from manuel.testing import TestCase
>>> m = manuel.Manuel()
>>> six.print_(TestCase(m, manuel.RegionContainer(), None).id())
DocTestRunner peaks at sys.argv
-------------------------------
A (bad) feature of DocTestRunner (and its subclass DebugRunner) is that it
will turn on "verbose" mode if sys.argv contains "-v". This means that if you
pass -v to a test runner that then invokes Manuel, all tests would fail
because extra junk was inserted into the doctest output. That is, before I
fixed it. Now, manuel.doctest.Manuel passes "verbose = False" to the
DocTestRunner constructor which disables the functionality.
We can ensure that the verbose mode is always disabled by creating test
standins for DocTestRunner and DebugRunner that capture their constructor
arguments.
.. code-block:: python
import doctest
import manuel.doctest
class FauxDocTestRunner(object):
def __init__(self, **kws):
self.kws = kws
try:
manuel.doctest.DocTestRunner = FauxDocTestRunner
manuel.doctest.DebugRunner = FauxDocTestRunner
m = manuel.doctest.Manuel()
finally:
manuel.doctest.DocTestRunner = doctest.DocTestRunner
manuel.doctest.DebugRunner = doctest.DebugRunner
Now, with the Manuel object instantiated we can verify that verbose is off for
both test runners.
>>> m.runner.kws['verbose']
False
>>> m.debug_runner.kws['verbose']
False
manuel-1.8.0/src/manuel/capture.py 0000664 0000000 0000000 00000006075 12361210771 0017076 0 ustar 00root root 0000000 0000000 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.8.0/src/manuel/capture.txt 0000664 0000000 0000000 00000003362 12361210771 0017261 0 ustar 00root root 0000000 0000000 manuel.capture
==============
This document explores the edge cases and boundry conditions of the
manuel.capture module. It is not meant as end-user documentation, but is
rather a set of tests.
Respecting indentation
----------------------
The text captured is determined by the indentation of the capture directive.
::
First level of indentation.
Second level of indentation.
Third level of indentation.
.. -> foo
.. -> source
>>> import manuel
>>> document = manuel.Document(source)
>>> import manuel.capture
>>> manuel.capture.find_captures(document)
>>> [r.parsed.block for r in document if r.parsed]
['Third level of indentation.\n']
Nested directives
-----------------
If two capture directives are nested, the outer one is effective.
::
First level of indentation.
Second level of indentation.
Third level of indentation.
.. -> foo
.. -> bar
.. -> source
>>> import manuel
>>> document = manuel.Document(source)
>>> import manuel.capture
>>> manuel.capture.find_captures(document)
>>> [r.parsed.block for r in document if r.parsed]
['Second level of indentation.\n\n Third level of indentation.\n\n.. -> foo\n']
Error reporting
---------------
If the capture directive is accidentally indented, a (reasonable) error will be
generated.
::
This is a block that will be captured::
Block
.. -> foo
.. -> source
>>> import manuel
>>> document = manuel.Document(source)
>>> import manuel.capture
>>> manuel.capture.find_captures(document)
Traceback (most recent call last):
...
RuntimeError: couldn't find the start of the block; improper indentation of capture directive?
manuel-1.8.0/src/manuel/codeblock.py 0000664 0000000 0000000 00000002174 12361210771 0017354 0 ustar 00root root 0000000 0000000 import re
import manuel
import textwrap
CODEBLOCK_START = re.compile(
r'(^\.\.\s*(invisible-)?code(-block)?::?\s*python\b(?:\s*\:[\w-]+\:.*\n)*)',
re.MULTILINE)
CODEBLOCK_END = re.compile(r'(\n\Z|\n(?=\S))')
class CodeBlock(object):
def __init__(self, code, source):
self.code = code
self.source = source
def find_code_blocks(document):
for region in document.find_regions(CODEBLOCK_START, CODEBLOCK_END):
start_end = CODEBLOCK_START.search(region.source).end()
source = textwrap.dedent(region.source[start_end:])
source_location = '%s:%d' % (document.location, region.lineno)
code = compile(source, source_location, 'exec', 0, True)
document.claim_region(region)
region.parsed = CodeBlock(code, source)
def execute_code_block(region, document, globs):
if not isinstance(region.parsed, CodeBlock):
return
exec(region.parsed.code, globs)
del globs['__builtins__'] # exec adds __builtins__, we don't want it
class Manuel(manuel.Manuel):
def __init__(self):
manuel.Manuel.__init__(self, [find_code_blocks], [execute_code_block])
manuel-1.8.0/src/manuel/doctest.py 0000664 0000000 0000000 00000007021 12361210771 0017070 0 ustar 00root root 0000000 0000000 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.8.0/src/manuel/footnote.py 0000664 0000000 0000000 00000004753 12361210771 0017271 0 ustar 00root root 0000000 0000000 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.8.0/src/manuel/ignore.py 0000664 0000000 0000000 00000000774 12361210771 0016716 0 ustar 00root root 0000000 0000000 import re
import manuel
import textwrap
IGNORE_START = re.compile(r'^\.\.\s*ignore-next-block\s*$', re.MULTILINE)
IGNORE_END = re.compile(r'(?