pax_global_header00006660000000000000000000000064147146455720014531gustar00rootroot0000000000000052 comment=4b4147de05072309c0c787bd49fd2f51c1c79ef4 manuel-1.13.0/000077500000000000000000000000001471464557200130745ustar00rootroot00000000000000manuel-1.13.0/.coveragerc000066400000000000000000000001661471464557200152200ustar00rootroot00000000000000[run] branch = True source = manuel omit = */test* [report] show_missing = true exclude_lines = pragma: no cover manuel-1.13.0/.coveralls.yaml000066400000000000000000000000561471464557200160310ustar00rootroot00000000000000repo_token: KSGERiTYQ51Efz265Uivz00izkUkiBMUp manuel-1.13.0/.github/000077500000000000000000000000001471464557200144345ustar00rootroot00000000000000manuel-1.13.0/.github/workflows/000077500000000000000000000000001471464557200164715ustar00rootroot00000000000000manuel-1.13.0/.github/workflows/check.yml000066400000000000000000000007361471464557200202770ustar00rootroot00000000000000name: Check project on: push jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest] python-version: ['3.11', '3.10', '3.9', '3.8'] steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - name: Build project run: make - name: Check project run: make check manuel-1.13.0/.gitignore000066400000000000000000000002021471464557200150560ustar00rootroot00000000000000.installed.cfg develop-eggs/ dist/ docs/ src/manuel.egg-info/ parts/ *.pyc __pycache__/ .coverage coverage.xml .eggs/ htmlcov ve/ manuel-1.13.0/.readthedocs.yaml000066400000000000000000000005541471464557200163270ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "3.11" # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py manuel-1.13.0/CHANGES.rst000066400000000000000000000140011471464557200146720ustar00rootroot00000000000000CHANGES ======= 1.13.0 (2024-11-12) ------------------- - Include*.md files in wheel These are needed by the test suite, which is also included in the wheel. - Fix tests on python3.11 - Update some web links - Update pyinstaller to 6.1.0 for python >= 3.11 support - doctest: use outputchecker argument also in debug mode. Fixes #38 - Remove references to old "six" module 1.12.4 (2022-06-24) ------------------- - Remove silly PyPI version badge. 1.12.3 (2022-06-24) ------------------- - Modernize internal project structure; drop tox; no user-visible changes (hopefully). - Rework coverage badge generation. - Drop Travis CI badge (the project is using GitHub for CI now) 1.11.2 (2022-05-15) ------------------- Fix missing file in release. 1.11.1 (2022-05-14) ------------------- Fix brown-bag release. 1.11.0 (2022-05-14) ------------------- - Fix test detection in Python 2 which was broken since 1.10.0. (`#20 `_) - Add Python 3.9 and 3.10 to tox config. - Add a Makefile to centeralized development activities. 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.6 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.13.0/COPYRIGHT.rst000066400000000000000000000005721471464557200152020ustar00rootroot00000000000000Copyright 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.13.0/LICENSE.rst000066400000000000000000000261361471464557200147200ustar00rootroot00000000000000 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.13.0/MANIFEST.in000066400000000000000000000003431471464557200146320ustar00rootroot00000000000000recursive-include src *.ex recursive-include src *.md recursive-include src *.txt recursive-include docs * recursive-include sphinx *.py include *.rst include Makefile include constraints.txt include *.yaml include .coveragerc manuel-1.13.0/Makefile000066400000000000000000000125171471464557200145420ustar00rootroot00000000000000SHELL := bash .SHELLFLAGS := -eux -o pipefail -c .DEFAULT_GOAL := build .DELETE_ON_ERROR: # If a recipe to build a file exits with an error, delete the file. .SUFFIXES: # Remove the default suffixes which are for compiling C projects. .NOTPARALLEL: # Disable use of parallel subprocesses. MAKEFLAGS += --warn-undefined-variables MAKEFLAGS += --no-builtin-rules export COLUMNS ?= 70 seperator ?= $(shell printf %${COLUMNS}s | tr " " "═") platform := $(shell python -c 'import sys; print(sys.platform)') PYTHON_VERSION ?= 3 export PIP_DISABLE_PIP_VERSION_CHECK=1 pip-install := ve/bin/pip --no-input install --constraint constraints.txt pip-check := ve/bin/pip show -q source_code := src isort := ve/bin/isort --multi-line=VERTICAL_HANGING_INDENT --trailing-comma --no-sections ######################################################################################## # Build targets # # It is acceptable for other targets to implicitly depend on these targets having been # run. I.e., it is ok if "make lint" generates an error before "make" has been run. .PHONY: build build: ve development-utilities ve: python$(PYTHON_VERSION) -m venv ve ve/bin/genbadge: $(pip-install) genbadge[coverage] ve/bin/%: # Install development utility "$*" $(pip-install) $* # Utilities we use during development. .PHONY: development-utilities development-utilities: ve/bin/black development-utilities: ve/bin/coverage development-utilities: ve/bin/flake8 development-utilities: ve/bin/genbadge development-utilities: ve/bin/isort development-utilities: ve/bin/mypy development-utilities: ve/bin/pydocstyle development-utilities: ve/bin/pyinstaller development-utilities: ve/bin/pylint development-utilities: ve/bin/twine development-utilities: ve/bin/wheel ######################################################################################## # Distribution targets .PHONY: assert-one-dist assert-one-dist: @if [ $$(find dist -name 'manuel-*.tar.gz' | wc -l) != 1 ]; then \ echo There must be one and only one distribution file present.; \ exit 1; \ fi .PHONY: assert-no-unreleased-changes assert-no-unreleased-changes: @if grep unreleased CHANGES.rst > /dev/null; then \ echo There must not be any unreleased changes in CHANGES.rst.; \ exit 1; \ fi .PHONY: assert-version-in-changelog assert-version-in-changelog: @if ! grep $$(ve/bin/python setup.py --version) CHANGES.rst; then \ echo The current version number must be mentioned in CHANGES.rst.; \ exit 1; \ fi .PHONY: assert-matching-versions assert-matching-versions: # verify that the top-most version in the change log matches what is in setup.py @env \ CHANGE_LOG_VERSION=$$(grep '^[^ ]\+ (20\d\d-\d\d-\d\d)' CHANGES.rst | head -n 1 | cut -d' ' -f1) \ SETUP_VERSION=$$(ve/bin/python setup.py --version) \ bash -c 'test $$CHANGE_LOG_VERSION = $$SETUP_VERSION' .PHONY: assert-no-changes assert-no-changes: @if ! output=$$(git status --porcelain) || [ -n "$$output" ]; then \ echo There must not be any ucomitted changes.; \ exit 1; \ fi .PHONY: dist dist: ve/bin/python setup.py sdist .PHONY: test-dist test-dist: # check to see if the distribution passes the tests rm -rf tmp mkdir tmp tar xzvf $$(find dist -name 'manuel-*.tar.gz') -C tmp cd tmp/manuel-* && make && make check rm -rf tmp .PHONY: upload upload: assert-one-dist ve/bin/twine upload --repository manuel $$(find dist -name 'manuel-*.tar.gz') .PHONY: badges badges: ve/bin/python bin/genbadge coverage -i coverage.xml -o badges/coverage-badge.svg .PHONY: release ifeq '$(shell git rev-parse --abbrev-ref HEAD)' 'master' release: clean-dist assert-no-unreleased-changes assert-matching-versions \ assert-version-in-changelog badges dist assert-one-dist test-dist \ assert-no-changes upload # now that a release has happened, tag the current HEAD as that release git tag $$(ve/bin/python setup.py --version) git push origin git push origin --tags else release: @echo Error: must be on master branch to do a release.; exit 1 endif ######################################################################################## # Test and lint targets .PHONY: pylint pylint: ve/bin/pylint $(source_code) --output-format=colorized .PHONY: flake8 flake8: ve/bin/flake8 $(source_code) .PHONY: pydocstyle pydocstyle: ve/bin/pydocstyle $(source_code) .PHONY: mypy mypy: ve/bin/mypy $(source_code) --strict .PHONY: black-check black-check: ve/bin/black -S $(source_code) --check .PHONY: isort-check isort-check: $(isort) $(source_code) --diff --check .PHONY: lint lint: black-check isort-check .PHONY: test test: ve/bin/python setup.py test .PHONY: coverage coverage: ve/bin/coverage run --branch setup.py test ve/bin/coverage xml # the XML output file is used by the "badges" target PYTHONWARNINGS=ignore ve/bin/coverage report --ignore-errors --fail-under=97 --show-missing --skip-empty .PHONY: check check: test lint coverage ######################################################################################## # Sorce code formatting targets .PHONY: black black: ve/bin/black -S $(source_code) .PHONY: isort isort: $(isort) $(source_code) ######################################################################################## # Cleanup targets .PHONY: clean-% clean-%: rm -rf $* .PHONY: clean-pycache clean-pycache: find . -name __pycache__ -delete .PHONY: clean clean: clean-ve clean-pycache clean-dist manuel-1.13.0/README.rst000066400000000000000000000012701471464557200145630ustar00rootroot00000000000000.. image:: https://raw.githubusercontent.com/benji-york/manuel/master/badges/coverage-badge.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. Development =========== To work on Manuel, check out the code and then run `make` to build a development environment. To run the tests, run ``make test``. To run all checks, run ``make check``. See the `Makefile` for more useful targets. manuel-1.13.0/badges/000077500000000000000000000000001471464557200143215ustar00rootroot00000000000000manuel-1.13.0/badges/coverage-badge.svg000066400000000000000000000021531471464557200176760ustar00rootroot00000000000000coverage: 97%coverage97%manuel-1.13.0/bin/000077500000000000000000000000001471464557200136445ustar00rootroot00000000000000manuel-1.13.0/bin/genbadge000066400000000000000000000014121471464557200153210ustar00rootroot00000000000000"""This is a hack to get the coverage percentage reported in whole numbers.""" import re import sys from genbadge import utils_coverage from genbadge import utils_badge def my_get_coverage_badge(cov_stats): """Generate a coverage badge they way I like it. The original included two decimal places in the percentage. I just want an integer percentage. """ color = utils_coverage.get_color(cov_stats) right_txt = '%.0f%%' % (cov_stats.total_coverage,) return utils_badge.Badge(left_txt="coverage", right_txt=right_txt, color=color) utils_coverage.get_coverage_badge = my_get_coverage_badge import genbadge.main if __name__ == '__main__': sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) sys.exit(genbadge.main.genbadge()) manuel-1.13.0/constraints.txt000066400000000000000000000014621471464557200162070ustar00rootroot00000000000000altgraph==0.17.2 astroid==2.11.2 black==22.3.0 bleach==5.0.0 certifi==2022.6.15 charset-normalizer==2.0.12 click==8.1.2 commonmark==0.9.1 coverage==6.4.1 defusedxml==0.7.1 dill==0.3.4 docutils==0.18.1 flake8==4.0.1 genbadge==1.0.6 idna==3.3 importlib-metadata==4.11.4 isort==5.10.1 keyring==23.6.0 lazy-object-proxy==1.7.1 macholib==1.16 mccabe==0.6.1 mypy==0.942 mypy-extensions==0.4.3 pathspec==0.9.0 Pillow==9.1.1 pkginfo==1.8.3 platformdirs==2.5.2 pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 Pygments==2.12.0 pyinstaller==6.1.0 pyinstaller-hooks-contrib==2022.4 pylint==2.13.5 readme-renderer==35.0 requests==2.28.0 requests-toolbelt==0.9.1 rfc3986==2.0.0 rich==12.4.4 snowballstemmer==2.2.0 tomli==2.0.1 twine==4.0.1 typing_extensions==4.2.0 urllib3==1.26.9 webencodings==0.5.1 wrapt==1.14.0 zipp==3.8.0 manuel-1.13.0/setup.py000066400000000000000000000036501471464557200146120ustar00rootroot00000000000000############################################################################## # # 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.13.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 :: 3', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', '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', ], include_package_data=True, long_description=long_description, keywords=['testing', 'documentation'], ) manuel-1.13.0/sphinx/000077500000000000000000000000001471464557200144055ustar00rootroot00000000000000manuel-1.13.0/sphinx/conf.py000066400000000000000000000007421471464557200157070ustar00rootroot00000000000000source_suffix = { '.rst': 'restructuredtext', '.txt': 'restructuredtext', '.md': 'markdown', } 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'] extensions = [ "myst_parser", "sphinx_copybutton", ] manuel-1.13.0/src/000077500000000000000000000000001471464557200136635ustar00rootroot00000000000000manuel-1.13.0/src/manuel/000077500000000000000000000000001471464557200151445ustar00rootroot00000000000000manuel-1.13.0/src/manuel/README.txt000066400000000000000000000463671471464557200166620ustar00rootroot00000000000000.. _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 >>> 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 <...Match object...> >>> region.end_match <...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={}) >>> 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={}) >>> 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={}) >>> 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={}) >>> 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={}) >>> 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: ... print('-'*70) ... 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 = next(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={}) >>> 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={}) >>> 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 io 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={}) >>> 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.13.0/src/manuel/__init__.py000066400000000000000000000250521471464557200172610ustar00rootroot00000000000000import 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.13.0/src/manuel/bugs.txt000066400000000000000000000153271471464557200166550ustar00rootroot00000000000000Fixed 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. >>> 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. >>> 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() >>> print(TestCase(m, manuel.RegionContainer(), None).id()) OutputChecker and debug ----------------------- When running in debug mode, such as when using the ``-D`` option of ``zope.testrunner``, the ``outputchecker`` of ``manuel.doctest.Manuel`` was ignored. .. code-block:: python import doctest import os.path import manuel.doctest import manuel.testing doc = os.path.join(__file__, '..', 'doc3.ex') checked_outputs = [] class CustomChecker(doctest.OutputChecker): def check_output(self, want, got, optionflags): checked_outputs.append((want, got)) return True m = manuel.doctest.Manuel(checker=CustomChecker()) >>> suite = manuel.testing.TestSuite(m, doc) >>> suite.debug() >>> checked_outputs [('2\n', '1\n')] 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.13.0/src/manuel/capture.py000066400000000000000000000061321471464557200171630ustar00rootroot00000000000000import 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.13.0/src/manuel/capture.txt000066400000000000000000000033621471464557200173540ustar00rootroot00000000000000manuel.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.13.0/src/manuel/codeblock.py000066400000000000000000000022071471464557200174440ustar00rootroot00000000000000import manuel import re import textwrap CODEBLOCK_START = re.compile( r'(^\.\.\s*(invisible-)?code(-block)?::?\s*python\b(?:\s*\:[\w-]+\:.*\n)*)', # noqa 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.13.0/src/manuel/doc1.ex000066400000000000000000000000151471464557200163240ustar00rootroot00000000000000>>> test.a 1 manuel-1.13.0/src/manuel/doc2.ex000066400000000000000000000000501471464557200163240ustar00rootroot00000000000000 >>> test.a, test.x, c (1, 5, 9) manuel-1.13.0/src/manuel/doc3.ex000066400000000000000000000000101471464557200163210ustar00rootroot00000000000000>>> 1 2 manuel-1.13.0/src/manuel/doctest.py000066400000000000000000000071161471464557200171700ustar00rootroot00000000000000import doctest import manuel import os.path from io import StringIO DocTestRunner = doctest.DocTestRunner DebugRunner = doctest.DebugRunner class DocTestResult(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, checker=checker, 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.13.0/src/manuel/footnote.py000066400000000000000000000047411471464557200173610ustar00rootroot00000000000000import manuel import re 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.13.0/src/manuel/ignore.py000066400000000000000000000007751471464557200170120ustar00rootroot00000000000000import manuel import re import textwrap IGNORE_START = re.compile(r'^\.\.\s*ignore-next-block\s*$', re.MULTILINE) IGNORE_END = re.compile(r'(?>>" 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 documentation `_ 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: ... 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) >>> for regions in manuel.testing.group_regions_by_test_case(document): ... 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) >>> for regions in manuel.testing.group_regions_by_test_case(document): ... 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 myst-markdown.md manuel-1.13.0/src/manuel/isolation.py000066400000000000000000000021551471464557200175220ustar00rootroot00000000000000import manuel import re 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.13.0/src/manuel/myst-markdown.md000066400000000000000000000040071471464557200203030ustar00rootroot00000000000000# MyST markdown Manuel was originally written for reStructuredText. Starting with the {mod}`manuel.codeblock` module, Manuel will successively be extended for MyST, a Markdown flavor. [Read about `MyST`](https://myst-parser.readthedocs.io/en/latest/). ## Code Blocks Sphinx and other docutils extensions provide a `code-block` directive, which allows inlined snippets of code in MyST documents. 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.myst.codeblock` module. It lets you execute code using MyST-style code block directives containing unpolluted Python code. The markup looks like this: ```python import foo def my_func(bar): return foo.baz(bar) ``` To get Manuel wired up, see {ref}`getting-started`. To run doctests in MyST, use {mod}`manuel.myst.codeblock`. The scope of variables spans across the complete document. ```python a = 3 # another variable b = 2 * 3 ``` The variables `a` and `b` can be used in the subsequent code block. ```python assert b == 6 ``` For better test feedback, you can use the methods of [`unittest.TestCase`](https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertEqual). ```python self.assertEqual(b, 6) ``` The output of `self.assertEqual(b, 9999)` would be the following. ```console AssertionError: 6 != 9999 ``` You can even write code in invisible code blocks. Invisible code blocks do not show up in the rendered documentation. Using MyST syntax, lines that start with `%` are comments. The markup looks like this: % invisible-code-block: python % % self.assertEqual(a+b, 9) % % self.assertEqual(7 * a, 21) % invisible-code-block: python % % self.assertEqual(a+b, 9) % % self.assertEqual(7 * a, 21) Invisible code blocks are tested like normal code blocks. Happy hacking! manuel-1.13.0/src/manuel/myst/000077500000000000000000000000001471464557200161405ustar00rootroot00000000000000manuel-1.13.0/src/manuel/myst/__init__.py000066400000000000000000000000001471464557200202370ustar00rootroot00000000000000manuel-1.13.0/src/manuel/myst/codeblock.py000066400000000000000000000016771471464557200204520ustar00rootroot00000000000000import re import textwrap from manuel import Manuel as BaseManuel from manuel.codeblock import CodeBlock, execute_code_block CODEBLOCK_START = re.compile( r"((^```python)|(^% invisible-code-block:\s+python)$)", re.MULTILINE, ) CODEBLOCK_END = re.compile(r"(\n(?=```\n))|((?:% [\S ]*)\n(?=\n))") 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:]) # MyST comments source = re.sub(r'\n%[ ]?', '\n', source) 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) class Manuel(BaseManuel): def __init__(self): BaseManuel.__init__(self, [find_code_blocks], [execute_code_block]) manuel-1.13.0/src/manuel/table-example.txt000066400000000000000000000225131471464557200204300ustar00rootroot00000000000000.. _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 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())) next(lines) # skip the first line # grab the expression to be evaluated expression = next(lines)[1] if expression.startswith('\\'): expression = expression[1:] next(lines) # skip the divider line variables = [v.strip() for v in next(lines)[1].split()][:-1] next(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] >>> 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) >>> 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={}) >>> 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={}) >>> 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.13.0/src/manuel/testcase.py000066400000000000000000000025731471464557200173400ustar00rootroot00000000000000import manuel import manuel.testing import re import string import textwrap punctuation = re.escape(string.punctuation) SECTION_TITLE = re.compile(r'^.+$', re.MULTILINE) SECTION_UNDERLINE = re.compile('^[' + punctuation + ']+\s*$', re.MULTILINE) MARKER = re.compile(r'^.. test-case: (\S+)', re.MULTILINE) def find_section_headers(document): for region in document.find_regions(SECTION_TITLE, SECTION_UNDERLINE): # regions that represent titles will have two lines if region.source.count('\n') != 2: continue title, underline = region.source.splitlines() # the underline has to be the same length as or longer than the title if len(underline) < len(title): continue # ok, this is a region we want document.claim_region(region) test_case_name = title.strip() region.parsed = manuel.testing.TestCaseMarker(test_case_name) def find_markers(document): for region in document.find_regions(MARKER): document.claim_region(region) test_case_name = region.start_match.group(1) region.parsed = manuel.testing.TestCaseMarker(test_case_name) class SectionManuel(manuel.Manuel): def __init__(self): manuel.Manuel.__init__(self, [find_section_headers]) class MarkerManuel(manuel.Manuel): def __init__(self): manuel.Manuel.__init__(self, [find_markers]) manuel-1.13.0/src/manuel/testing.py000066400000000000000000000163451471464557200172040ustar00rootroot00000000000000import doctest as real_doctest import functools import inspect import io import itertools import manuel 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 = contents.encode("utf-8") document = manuel.Document(contents, location=abs_path) document.parse_with(m) for regions in group_regions_by_test_case(document): tc = TestCase_class(m, regions, globs, **kws) tc.globs['self'] = tc suite.addTest(tc) 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.13.0/src/manuel/tests.py000066400000000000000000000064511471464557200166660ustar00rootroot00000000000000import doctest import manuel import manuel.capture import manuel.codeblock import manuel.doctest import manuel.ignore import manuel.myst.codeblock 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 suite_rst(): tests = [ 'index.txt', 'table-example.txt', 'README.txt', 'bugs.txt', 'capture.txt', 'myst-markdown.md', ] print("*** suite_rst. tests", tests) 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. return manuel.testing.TestSuite( m, *tests, **dict(globs={'path_to_test': os.path.join(here, 'bugs.txt')}) ) def suite_myst(): tests = ['myst-markdown.md'] print("*** suite_myst. tests", tests) optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS m = manuel.doctest.Manuel(optionflags=optionflags, checker=checker) m += manuel.myst.codeblock.Manuel() # The apparently redundant "**dict()" is to make this code compatible with # Python 2.5 -- it would generate a SyntaxError otherwise. return manuel.testing.TestSuite( m, *tests, **dict(globs={'path_to_test': os.path.join(here, 'bugs.txt')}) ) def test_suite(): return unittest.TestSuite( ( suite_rst(), suite_myst(), doctest.DocTestSuite(), ) )