sybil-1.2.0/0000755000000000000000000000000013461247332012577 5ustar rootroot00000000000000sybil-1.2.0/.carthorse.yml0000644000000000000000000000042213461247305015370 0ustar rootroot00000000000000carthorse: version-from: setup.py tag-format: "{version}" when: - version-not-tagged actions: - run: "sudo pip install -e .[build]" - run: "sudo python setup.py sdist bdist_wheel" - run: "twine upload -u chrisw -p $PYPI_PASS dist/*" - create-tag sybil-1.2.0/.circleci/0000755000000000000000000000000013461247332014432 5ustar rootroot00000000000000sybil-1.2.0/.circleci/config.yml0000644000000000000000000000132513461247305016423 0ustar rootroot00000000000000version: 2.1 orbs: python: cjw296/python-ci@1.2 common: &common jobs: - python/pip-run-tests: name: python27 image: circleci/python:2.7 - python/pip-run-tests: name: python37 image: circleci/python:3.7 - python/coverage: name: coverage requires: - python27 - python37 - python/release: name: release config: .carthorse.yml requires: - coverage filters: branches: only: master workflows: push: <<: *common periodic: <<: *common triggers: - schedule: cron: "0 0 * * 2" filters: branches: only: master sybil-1.2.0/.gitignore0000644000000000000000000000011313461247305014562 0ustar rootroot00000000000000/bin /*.egg-info /include /lib .coverage* _build/ .*cache/ pytestdebug.log sybil-1.2.0/.readthedocs.yml0000644000000000000000000000017013461247305015663 0ustar rootroot00000000000000version: 2 python: version: 3.7 install: - method: pip path: . extra_requirements: - build sybil-1.2.0/PKG-INFO0000644000000000000000000000167213461247332013702 0ustar rootroot00000000000000Metadata-Version: 2.1 Name: sybil Version: 1.2.0 Summary: Automated testing for the examples in your documentation. Home-page: https://github.com/cjw296/sybil Author: Chris Withers Author-email: chris@withers.org License: MIT Description: ===== sybil ===== Automated testing for the examples in your documentation. The latest documentation can be found at: http://sybil.readthedocs.org/en/latest/ Development takes place here: https://github.com/cjw296/sybil/ Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Provides-Extra: build Provides-Extra: test sybil-1.2.0/README.rst0000644000000000000000000000122213461247305014263 0ustar rootroot00000000000000|Travis|_ |Coveralls|_ |Docs|_ .. |Travis| image:: https://api.travis-ci.org/cjw296/sybil.svg?branch=master .. _Travis: https://travis-ci.org/cjw296/sybil .. |Coveralls| image:: https://coveralls.io/repos/cjw296/sybil/badge.svg?branch=master .. _Coveralls: https://coveralls.io/r/cjw296/sybil?branch=master .. |Docs| image:: https://readthedocs.org/projects/sybil/badge/?version=latest .. _Docs: http://sybil.readthedocs.org/en/latest/ Sybil ===== Automated testing for the examples in your documentation. The latest documentation can be found at: http://sybil.readthedocs.org/en/latest/ Development takes place here: https://github.com/cjw296/sybil/ sybil-1.2.0/docs/0000755000000000000000000000000013461247332013527 5ustar rootroot00000000000000sybil-1.2.0/docs/Makefile0000644000000000000000000000501713461247305015172 0ustar rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf _build/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html @echo @echo "Build finished. The HTML pages are in _build/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) _build/dirhtml @echo @echo "Build finished. The HTML pages are in _build/dirhtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) _build/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) _build/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in _build/htmlhelp." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex @echo @echo "Build finished; the LaTeX files are in _build/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes @echo @echo "The overview file is in _build/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in _build/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) _build/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in _build/doctest/output.txt." sybil-1.2.0/docs/api.rst0000644000000000000000000000055713461247305015041 0ustar rootroot00000000000000API Reference ============= .. automodule:: sybil :members: Sybil, Region .. automodule:: sybil.document :members: .. automodule:: sybil.example :members: .. automodule:: sybil.parsers.doctest :members: .. automodule:: sybil.parsers.codeblock :members: .. automodule:: sybil.parsers.capture :members: .. automodule:: sybil.parsers.skip :members: sybil-1.2.0/docs/changes.rst0000644000000000000000000000421613461247305015674 0ustar rootroot00000000000000Changes ======= 1.2.0 (28 Apr 2019) ------------------- - Only compile code in :ref:`codeblocks ` at evaluation time, giving :ref:`skip ` a chance to skip code blocks that won't compile on a particular version of Python. 1.1.0 (25 Apr 2019) ------------------- - Move to CircleCI__ and Carthorse__. __ https://circleci.com/gh/cjw296/sybil __ https://github.com/cjw296/carthorse - Add warning about the limitations of :attr:`~sybil.parsers.doctest.FIX_BYTE_UNICODE_REPR`. - Support explicit filenames to include and patterns to exclude when instantiating a :class:`~sybil.Sybil`. - Add the :ref:`skip ` parser. 1.0.9 (1 Aug 2018) ------------------ - Fix for pytest 3.7+. 1.0.8 (6 Apr 2018) ------------------ - Changes only to unit tests to support fixes in the latest release of pytest. 1.0.7 (25 January 2018) ----------------------- - Literal tabs may no longer be included in text that is parsed by the :class:`~sybil.parsers.doctest.DocTestParser`. Previously, tabs were expanded which could cause unpleasant problems. 1.0.6 (30 November 2017) ------------------------ - Fix compatibility with pytest 3.3+. Thanks to Bruno Oliveira for this fix! 1.0.5 (6 June 2017) ------------------- - Fix ordering issue that would cause some tests to fail when run on systems using tmpfs. 1.0.4 (5 June 2017) ------------------- - Fix another bug in :class:`~sybil.parsers.codeblock.CodeBlockParser` where a :rst:dir:`code-block` followed by a less-indented block would be incorrectly indented, resulting in a :class:`SyntaxError`. 1.0.3 (2 June 2017) ------------------- - Fix bug in :func:`~sybil.parsers.codeblock.CodeBlockParser` where it would incorrectly parse indented code blocks. 1.0.2 (1 June 2017) ------------------- - Fix bug in :func:`~sybil.parsers.codeblock.CodeBlockParser` where it would not find indented code blocks. 1.0.1 (30 May 2017) ------------------- - Fix bug where unicode and byte literals weren't corrected in doctest tracebacks, even when :attr:`sybil.parsers.doctest.FIX_BYTE_UNICODE_REPR` was specified. 1.0.0 (26 May 2017) ------------------- - Initial release sybil-1.2.0/docs/conf.py0000644000000000000000000000160413461247305015027 0ustar rootroot00000000000000# -*- coding: utf-8 -*- import os, pkg_resources, datetime on_rtd = os.environ.get('READTHEDOCS', None) == 'True' intersphinx_mapping = { 'https://docs.python.org/3/': None, 'http://www.sphinx-doc.org/en/stable/': None, } extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx' ] # General source_suffix = '.rst' master_doc = 'index' project = 'sybil' copyright = '2017 - %s Chris Withers' % datetime.datetime.now().year version = release = pkg_resources.get_distribution(project).version exclude_patterns = [ 'description.rst', '_build', 'example*', ] pygments_style = 'sphinx' # Options for HTML output html_theme = 'default' if on_rtd else 'classic' htmlhelp_basename = project+'doc' # Options for LaTeX output latex_documents = [ ('index',project+'.tex', project+u' Documentation', 'Chris Withers', 'manual'), ] autodoc_member_order = 'bysource' sybil-1.2.0/docs/conftest.py0000644000000000000000000000077513461247305015737 0ustar rootroot00000000000000from doctest import ELLIPSIS from sybil import Sybil from sybil.parsers.capture import parse_captures from sybil.parsers.codeblock import CodeBlockParser from sybil.parsers.doctest import DocTestParser, FIX_BYTE_UNICODE_REPR from sybil.parsers.skip import skip pytest_collect_file = Sybil( parsers=[ DocTestParser(optionflags=ELLIPSIS|FIX_BYTE_UNICODE_REPR), CodeBlockParser(future_imports=['print_function']), parse_captures, skip, ], pattern='*.rst', ).pytest() sybil-1.2.0/docs/description.rst0000644000000000000000000000034013461247305016601 0ustar rootroot00000000000000===== sybil ===== Automated testing for the examples in your documentation. The latest documentation can be found at: http://sybil.readthedocs.org/en/latest/ Development takes place here: https://github.com/cjw296/sybil/ sybil-1.2.0/docs/development.rst0000644000000000000000000000361113461247305016604 0ustar rootroot00000000000000Development =========== .. highlight:: bash This package is developed using continuous integration which can be found here: https://travis-ci.org/cjw296/sybil The latest development version of the documentation can be found here: http://sybil.readthedocs.org/en/latest/ If you wish to contribute to this project, then you should fork the repository found here: https://github.com/cjw296/sybil/ Once that has been done and you have a checkout, you can follow these instructions to perform various development tasks: Setting up a virtualenv ----------------------- The recommended way to set up a development environment is to turn your checkout into a virtualenv and then install the package in editable form as follows:: $ virtualenv . $ bin/pip install -U -e .[test,build] Running the tests ----------------- Once you've set up a virtualenv, the tests can be run as follows:: $ bin/pytest Building the documentation -------------------------- The Sphinx documentation is built by doing the following from the directory containing setup.py:: $ source bin/activate $ cd docs $ make html To check that the description that will be used on PyPI renders properly, do the following:: $ python setup.py --long-description | rst2html.py > desc.html The resulting ``desc.html`` should be checked by opening in a browser. To check that the README that will be used on GitHub renders properly, do the following:: $ cat README.rst | rst2html.py > readme.html The resulting ``readme.html`` should be checked by opening in a browser. Making a release ---------------- To make a release, just update the version in ``setup.py``, update the change log, tag it and push to https://github.com/cjw296/sybil and Travis CI should take care of the rest. Once Travis CI is done, make sure to go to https://readthedocs.org/projects/sybil/versions/ and make sure the new release is marked as an Active Version. sybil-1.2.0/docs/example/0000755000000000000000000000000013461247332015162 5ustar rootroot00000000000000sybil-1.2.0/docs/example/docs/0000755000000000000000000000000013461247332016112 5ustar rootroot00000000000000sybil-1.2.0/docs/example/docs/conftest.py0000644000000000000000000000125313461247305020312 0ustar rootroot00000000000000from os import chdir, getcwd from shutil import rmtree from tempfile import mkdtemp import pytest from sybil import Sybil from sybil.parsers.codeblock import CodeBlockParser from sybil.parsers.doctest import DocTestParser @pytest.fixture(scope="module") def tempdir(): # there are better ways to do temp directories, but it's a simple example: path = mkdtemp() cwd = getcwd() try: chdir(path) yield path finally: chdir(cwd) rmtree(path) pytest_collect_file = Sybil( parsers=[ DocTestParser(), CodeBlockParser(future_imports=['print_function']), ], pattern='*.rst', fixtures=['tempdir'] ).pytest() sybil-1.2.0/docs/example/docs/example.rst0000644000000000000000000000057113461247305020302 0ustar rootroot00000000000000Another fairly pointless function: .. code-block:: python import sys def write_message(filename, message): with open(filename, 'w') as target: target.write(message) Now we can use a doctest REPL to show it in action: >>> write_message('test.txt', 'is this thing on?') >>> with open('test.txt') as source: ... print(source.read()) is this thing on? sybil-1.2.0/docs/example/example_nose/0000755000000000000000000000000013461247332017641 5ustar rootroot00000000000000sybil-1.2.0/docs/example/example_nose/__init__.py0000644000000000000000000000000013461247305021740 0ustar rootroot00000000000000sybil-1.2.0/docs/example/example_nose/test_example_docs.py0000644000000000000000000000131413461247305023714 0ustar rootroot00000000000000from os import chdir, getcwd from shutil import rmtree from tempfile import mkdtemp from sybil import Sybil from sybil.parsers.codeblock import CodeBlockParser from sybil.parsers.doctest import DocTestParser def sybil_setup(namespace): # there are better ways to do temp directories, but it's a simple example: namespace['path'] = path = mkdtemp() namespace['cwd'] = getcwd() chdir(path) def sybil_teardown(namespace): chdir(namespace['cwd']) rmtree(namespace['path']) load_tests = Sybil( parsers=[ DocTestParser(), CodeBlockParser(future_imports=['print_function']), ], path='../docs', pattern='*.rst', setup=sybil_setup, teardown=sybil_teardown ).nose() sybil-1.2.0/docs/example/example_unittest/0000755000000000000000000000000013461247332020554 5ustar rootroot00000000000000sybil-1.2.0/docs/example/example_unittest/__init__.py0000644000000000000000000000000013461247305022653 0ustar rootroot00000000000000sybil-1.2.0/docs/example/example_unittest/test_example_docs.py0000644000000000000000000000131713461247305024632 0ustar rootroot00000000000000from os import chdir, getcwd from shutil import rmtree from tempfile import mkdtemp from sybil import Sybil from sybil.parsers.codeblock import CodeBlockParser from sybil.parsers.doctest import DocTestParser def sybil_setup(namespace): # there are better ways to do temp directories, but it's a simple example: namespace['path'] = path = mkdtemp() namespace['cwd'] = getcwd() chdir(path) def sybil_teardown(namespace): chdir(namespace['cwd']) rmtree(namespace['path']) load_tests = Sybil( parsers=[ DocTestParser(), CodeBlockParser(future_imports=['print_function']), ], path='../docs', pattern='*.rst', setup=sybil_setup, teardown=sybil_teardown ).unittest() sybil-1.2.0/docs/example/pytest.ini0000644000000000000000000000000013461247305017201 0ustar rootroot00000000000000sybil-1.2.0/docs/example-skip.rst0000644000000000000000000000046513461247305016665 0ustar rootroot00000000000000.. skip: next This would be wrong: >>> 1 == 2 True This is pseudo-code: .. skip: start >>> foo = ... >>> foo(..) .. skip: end .. invisible-code-block: python import sys This will only work on Python 3: .. skip: next if(sys.version_info < (3, 0), reason="python 3 only") >>> repr(b'foo') "b'foo'" sybil-1.2.0/docs/example.rst0000644000000000000000000000115413461247305015715 0ustar rootroot00000000000000Sample Documentation ==================== Let's put something in the Sybil document's namespace: .. invisible-code-block: python remember_me = b'see how namespaces work?' Suppose we define a function, convoluted and pointless but shows stuff nicely: .. code-block:: python import sys def prefix_and_print(message): print('prefix:', message.decode('ascii')) Now we can use a doctest REPL to show it in action: >>> prefix_and_print(remember_me) prefix: see how namespaces work? The namespace carries across from example to example, no matter what parser: >>> remember_me b'see how namespaces work?' sybil-1.2.0/docs/index.rst0000644000000000000000000000173013461247305015371 0ustar rootroot00000000000000Sybil documentation =================== This library provides a way to test examples in your documentation by parsing them from the documentation source and evaluating the parsed examples as part of your normal test run. Integration is provided for the three main Python test runners. Sybil is designed so that it's easy to provide your own parsers for your own types of example in addition to the included parsers for :mod:`doctest` and :rst:dir:`code-block` examples. .. toctree:: :maxdepth: 3 use.rst parsers.rst api.rst development.rst changes.rst license.rst Why Sybil? ========== Sybil is heavily inspired by `Manuel`__, which was named after a `Fawlty Towers`__ character, and so it seemed only right to pick another Fawtly Towers character name for this library. __ http://pythonhosted.org/manuel/index.html __ https://en.wikipedia.org/wiki/Fawlty_Towers Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` sybil-1.2.0/docs/license.rst0000644000000000000000000000211513461247305015702 0ustar rootroot00000000000000======= License ======= Copyright (c) 2017-2018 Chris Withers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sybil-1.2.0/docs/parsers.rst0000644000000000000000000002324513461247305015746 0ustar rootroot00000000000000Parsers ======= Sybil parsers are what extracts examples from documentation source files and turns them into parsed examples with evaluators that can check if they are as expected. A number of parsers are included, and it's simple enough to write your own. The included parsers are as follows: .. _doctest-parser: doctest ------- This parser extracts classic :ref:`doctest ` examples and evaluates them in the document's :attr:`~sybil.document.Document.namespace`. The parser can optionally be instantiated with :ref:`doctest option flags`. An additional option flag, :attr:`sybil.parsers.doctest.FIX_BYTE_UNICODE_REPR`, is provided. When used, this flag causes byte and unicode literals in doctest expected output to be rewritten such that they are compatible with the version of Python with which the tests are executed. If your example output includes either ``b'...'`` or ``u'...'`` and your code is expected to run under both Python 2 and Python 3, then you will likely need this option. The parser is used by instantiating :class:`sybil.parsers.doctest.DocTestParser` with the required options and passing it as an element in the list passed as the ``parsers`` parameter to :class:`~sybil.Sybil`. .. warning:: :attr:`~sybil.parsers.doctest.FIX_BYTE_UNICODE_REPR` is quite simplistic. It will catch examples but you may hit problems where, for example, ``['b', '']`` in expected output will be rewritten as ``['', '']`` on Python 2 and ``['u', '']`` as ``['', '']``. on Python 3. To work around this, either only run Sybil on Python 3 and do not use this option, or pick different example output. .. _codeblock-parser: codeblock --------- This parser extracts examples from Sphinx :rst:dir:`code-block` directives and evaluates them in the document's :attr:`~sybil.document.Document.namespace`. For example, this code block would be evaluated successfully and will define the :func:`prefix_and_print` function in the document's namespace: .. literalinclude:: example.rst :language: rest :lines: 11-18 Including all the boilerplate necessary for an example to successfully evaluate can hinder an example's usefulness as part of documentation. As a result, this parser also evaluates "invisible" code blocks such as this one: .. literalinclude:: example.rst :language: rest :lines: 5-9 These take advantage of Sphinx `comment`__ syntax so that the code block will not be rendered in your documentation but can be used to set up the document's namespace or make assertions about what the evaluation of other examples has put in that namespace. __ http://www.sphinx-doc.org/en/stable/rest.html#comments The parser is used by instantiating :class:`sybil.parsers.codeblock.CodeBlockParser` and passing it as an element in the list passed as the ``parsers`` parameter to :class:`~sybil.Sybil`. :class:`~sybil.parsers.codeblock.CodeBlockParser` takes an optional ``future_imports`` parameter that can be used to prefix all example python code found by this parser with one or or more ``from __future__ import ...`` statements. For example, to prefix all code block examples with ``from __future__ import print_function``, such that they can use Python 3 style ``print()`` calls even when testing the documentation under Python 2, you would instantiate the parser as follows: .. code-block:: python from sybil.parsers.codeblock import CodeBlockParser CodeBlockParser(future_imports=['print_function']) .. _capture-parser: capture ------- This parser takes advantage of Sphinx `comment`__ syntax to introduce a special comment that takes the preceding ReST `block`__ and inserts its raw content into the document's :attr:`~sybil.document.Document.namespace` using the name specified. __ http://www.sphinx-doc.org/en/stable/rest.html#comments __ http://www.sphinx-doc.org/en/stable/rest.html?highlight=block#source-code For example:: A simple example:: root.txt subdir/ subdir/file.txt subdir/logs/ .. -> expected_listing .. --> capture_example .. invisible-code-block: python from sybil.document import Document from sybil.parsers.capture import parse_captures document = Document(capture_example, '/the/path') (region,) = parse_captures(document) document.add(region) (example,) = document region.evaluator(example) expected_listing = document.namespace['expected_listing'] The above documentation source, when parsed by this parser and then evaluated, would mean that ``expected_listing`` could be used in other examples in the document: >>> expected_listing.split() ['root.txt', 'subdir/', 'subdir/file.txt', 'subdir/logs/'] The parser is used by including :func:`sybil.parsers.capture.parse_captures` as an element in the list passed as the ``parsers`` parameter to :class:`~sybil.Sybil`. .. _skip-parser: skip ---- This parser takes advantage of Sphinx `comment`__ syntax to introduce special comments that allow other examples in the document to be skipped. This can be useful if they include pseudo code or examples that can only be evaluated on a particular version of Python. __ http://www.sphinx-doc.org/en/stable/rest.html#comments For example: .. literalinclude:: example-skip.rst :language: rest :lines: 1-6 If you need to skip a collection of examples, this can be done as follows: .. literalinclude:: example-skip.rst :language: rest :lines: 8-15 You can also add conditions to either ``next`` or ``start`` as shown below: .. literalinclude:: example-skip.rst :language: rest :lines: 17- As you can see, any names used in the expression passed to ``if`` must be present in the document's :attr:`~sybil.document.Document.namespace`. :ref:`invisible code blocks `, :class:`setup ` methods or :ref:`fixtures ` are good ways to provide these. The parser is used by including :func:`sybil.parsers.skip.skip` as an element in the list passed as the ``parsers`` parameter to :class:`~sybil.Sybil`. .. _developing-parsers: Developing your own parsers --------------------------- Sybil parsers are callables that take a :class:`sybil.document.Document` and yield a sequence of :class:`regions `. A :class:`~sybil.Region` contains the character position of the start and end of the example in the document's :attr:`~sybil.document.Document.text`, along with a parsed version of the example and a callable evaluator. That evaluator will be called with an :class:`~sybil.example.Example` constructed from the :class:`~sybil.document.Document` and the :class:`~sybil.Region` and should either raise an exception or return a textual description in the event of the example not being as expected. Evaluators may also modify the document's :attr:`~sybil.document.Document.namespace` or :attr:`~sybil.document.Document.evaluator`. As an example, let's look at a parser suitable for evaluating bash commands in a subprocess and checking the output is as expected:: .. code-block:: bash $ echo hi there hi there .. -> bash_document_text Writing parsers quite often involves using regular expressions to extract the text for examples from the document. There's no hard requirement for this, but if you find you need to, then :meth:`~sybil.document.Document.find_region_sources` may be of help. Parsers are free to access any documented attribute of the :class:`~sybil.document.Document` although will most likely only need to work with :attr:`~sybil.document.Document.text`. The :attr:`~sybil.document.Document.namespace` attribute should **not** be modified. For the above example, the parser could be implemented as follows, with the parsed version consisting of a tuple of the command to run and the expected output: .. code-block:: python import re, textwrap from sybil import Region BASHBLOCK_START = re.compile(r'^\.\.\s*code-block::\s*bash') BASHBLOCK_END = re.compile(r'(\n\Z|\n(?=\S))') def parse_bash_blocks(document): for start_match, end_match, source in document.find_region_sources( BASHBLOCK_START, BASHBLOCK_END ): command, output = textwrap.dedent(source).strip().split('\n') assert command.startswith('$ ') parsed = command[2:].split(), output yield Region(start_match.start(), end_match.end(), parsed, evaluate_bash_block) Evaluators are generally much simpler than parsers and are called with an :class:`~sybil.example.Example`. Instances of this class are used to wrap up all the attributes you're likely to need when writing an evaluator and all documented attributes are fine to use. In particular, :attr:`~sybil.example.Example.parsed` is the parsed value provided by the parser when instantiating the :class:`~sybil.Region` and :attr:`~sybil.example.Example.namespace` is a reference to the document's namespace. Evaluators **are** free to modify the :attr:`~sybil.document.Document.namespace` if they need to. For the above example, the evaluator could be implemented as follows: .. code-block:: python from subprocess import check_output def evaluate_bash_block(example): command, expected = example.parsed actual = check_output(command).strip().decode('ascii') assert actual == expected, repr(actual) + ' != ' + repr(expected) The parser can now be used when instantiating a :class:`~sybil.Sybil`, which can then be used to integrate with your test runner: .. code-block:: python from sybil import Sybil sybil = Sybil(parsers=[parse_bash_blocks], pattern='*.rst') .. invisible-code-block: python from tempfile import NamedTemporaryFile with NamedTemporaryFile() as temp: temp.write(bash_document_text.encode('ascii')) temp.flush() document = sybil.parse(temp.name) (example,) = document example.evaluate() sybil-1.2.0/docs/use.rst0000644000000000000000000001233713461247305015063 0ustar rootroot00000000000000Usage ===== As a quick-start, here's how you would set up a ``conftest.py`` in your `Sphinx`__ source directory such that running `pytest`__ would check :ref:`doctest ` and :rst:dir:`code-block` examples in your documentation source files, taking into account the different representation of :class:`bytes` and :class:`unicode ` between Python 2 and 3, and also prefixing all :rst:dir:`code-block` examples with a ``from __future__ import print_function``: .. literalinclude:: conftest.py :lines: 1-2, 4-10, 12- __ http://www.sphinx-doc.org/ __ https://docs.pytest.org An example of a documentation source file that could be checked using the above configuration is shown below: .. literalinclude:: example.rst :language: rest Method of operation ------------------- Sybil works by discovering a series of :class:`documents ` as part of the :ref:`test runner integration `. These documents are then :doc:`parsed ` into a set of non-overlapping :class:`regions `. When the tests are run, the :ref:`test runner integration ` turns each :class:`~sybil.Region` into an :class:`~sybil.example.Example` before evaluating each :class:`~sybil.example.Example` in the document's :class:`~sybil.document.Document.namespace`. The examples are evaluated in the order in which they appear in the document. If an example does not evaluate as expected, a test failure occurs and Sybil continues on to evaluate the remaining :class:`examples ` in the :class:`~sybil.document.Document`. .. _integrations: Test runner integration ----------------------- Sybil aims to integrate with all major Python test runners. Those currently catered for explicitly are listed below, but you may find that one of these integration methods may work as required with other test runners. If not, please file an issue on GitHub. To show how the integration options work, the following documentation examples will be tested. They use :ref:`doctests `, :rst:dir:`code blocks ` and require a temporary directory: .. literalinclude:: example/docs/example.rst :language: rest .. _pytest_integration: pytest ~~~~~~ To have `pytest`__ check the examples, Sybil makes use of the ``pytest_collect_file`` hook. To use this, configuration is placed in a ``confest.py`` in your documentation source directory, as shown below. ``pytest`` should be invoked from a location that has the opportunity to recurse into that directory: __ https://docs.pytest.org .. literalinclude:: example/docs/conftest.py The file glob passed as ``pattern`` should match any documentation source files that contain examples which you would like to be checked. As you can see, if your examples require any fixtures, these can be requested by passing their names to the ``fixtures`` argument of the :class:`~sybil.Sybil` class. These will be available in the :class:`~sybil.document.Document` :class:`~sybil.document.Document.namespace` in a way that should feel natural to ``pytest`` users. The ``setup`` and ``teardown`` parameters can still be used to pass :class:`~sybil.document.Document` setup and teardown callables. The ``path`` parameter, however, is ignored. .. _unitttest_integration: unittest ~~~~~~~~ To have :ref:`unittest-test-discovery` check the example, Sybil makes use of the `load_tests protocol`__. As such, the following should be placed in a test module, say ``test_docs.py``, where the unit test discovery process can find it: __ https://docs.python.org/3/library/unittest.html#load-tests-protocol .. literalinclude:: example/example_unittest/test_example_docs.py The ``path`` parameter gives the path, relative to the file containing this code, that contains the documentation source files. The file glob passed as ``pattern`` should match any documentation source files that contain examples which you would like to be checked. Any setup or teardown necessary for your tests can be carried out in callables passed to the ``setup`` and ``teardown`` parameters, which are both called with the :class:`~sybil.document.Document` :class:`~sybil.document.Document.namespace`. The ``fixtures`` parameter, is ignored. .. _nose_integration: nose ~~~~ Sybil acts as a test loader plugin for nose that provides a ``--test-suite-func`` ooption which defaults to ``load_tests``, so making nose respect the `load_tests protocol`__. __ https://docs.python.org/3/library/unittest.html#load-tests-protocol Provided Sybil is activated as a nose plugin, the following code, when placed in a test module where nose can find it, will result in your documentation examples being checked: .. literalinclude:: example/example_nose/test_example_docs.py The ``path`` parameter gives the path, relative to the file containing this code, that contains the documentation source files. The file glob passed as ``pattern`` should match any documentation source files that contain examples which you would like to be checked. Any setup or teardown necessary for your tests can be carried out in callables passed to the ``setup`` and ``teardown`` parameters, which are both called with the :class:`~sybil.document.Document` :class:`~sybil.document.Document.namespace`. The ``fixtures`` parameter is ignored. sybil-1.2.0/setup.cfg0000644000000000000000000000022613461247332014420 0ustar rootroot00000000000000[wheel] universal = 1 [tool:pytest] addopts = --verbose --strict norecursedirs = functional .git docs/example [egg_info] tag_build = tag_date = 0 sybil-1.2.0/setup.py0000644000000000000000000000252213461247305014312 0ustar rootroot00000000000000# See docs/license.rst for license details. # Copyright (c) 2017-2018 Chris Withers import os from setuptools import setup, find_packages base_dir = os.path.dirname(__file__) setup( name='sybil', version='1.2.0', author='Chris Withers', author_email='chris@withers.org', license='MIT', description="Automated testing for the examples in your documentation.", long_description=open('docs/description.rst').read(), url='https://github.com/cjw296/sybil', classifiers=[ # 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', ], packages=find_packages(exclude=['tests', 'functional_tests']), zip_safe=False, include_package_data=True, extras_require=dict( test=[ 'nose', 'pytest>=3.5.0', 'pytest-cov', ], build=['sphinx', 'pkginfo', 'setuptools-git', 'twine', 'wheel'] ), entry_points = { 'nose.plugins.0.10': [ 'sybil = sybil.integration.nose:Plugin' ] }, ) sybil-1.2.0/sybil/0000755000000000000000000000000013461247332013721 5ustar rootroot00000000000000sybil-1.2.0/sybil/__init__.py0000644000000000000000000000006413461247305016032 0ustar rootroot00000000000000from .sybil import Sybil from .region import Region sybil-1.2.0/sybil/compat.py0000644000000000000000000000017013461247305015554 0ustar rootroot00000000000000import sys PY3 = sys.version_info[0] == 3 if PY3: from io import StringIO else: from StringIO import StringIO sybil-1.2.0/sybil/document.py0000644000000000000000000001063413461247305016115 0ustar rootroot00000000000000import re from bisect import bisect from .example import Example class Document(object): """ This is Sybil's representation of a documentation source file. It will be instantiated by Sybil and provided to each parser in turn. """ #: This can be set by :ref:`evaluators ` to affect the evaluation #: of future examples. It can be set to a callable that takes an #: :class:`~sybil.example.Example`. This callable can then do whatever it needs to do, #: including not executing the example at all, modifying it, or the #: :class:`~sybil.document.Document` or calling the original evaluator on the example. #: This last case should always take the form of ``example.region.evaluator(example)``. evaluator = None def __init__(self, text, path): #: This is the text of the documentation source file. self.text = text #: This is the absolute path of the documentation source file. self.path = path self.end = len(text) self.regions = [] #: This dictionary is the namespace in which all example parsed from #: this document will be evaluated. self.namespace = {} @classmethod def parse(cls, path, *parsers): """ Read the text from the supplied path and parse it into a document using the supplied parsers. """ with open(path) as source: text = source.read() document = cls(text, path) for parser in parsers: for region in parser(document): document.add(region) return document def line_column(self, position): """ Return a line and column location in this document based on a byte position. """ line = self.text.count('\n', 0, position)+1 col = position - self.text.rfind('\n', 0, position) return 'line {}, column {}'.format(line, col) def region_details(self, region): return '{!r} from {} to {}'.format( region, self.line_column(region.start), self.line_column(region.end) ) def raise_overlap(self, *regions): reprs = [] for region in regions: reprs.append(self.region_details(region)) raise ValueError('{} overlaps {}'.format(*reprs)) def add(self, region): if region.start < 0: raise ValueError('{} is before start of document'.format( self.region_details(region) )) if region.end > self.end: raise ValueError('{} goes beyond end of document'.format( self.region_details(region) )) entry = (region.start, region) index = bisect(self.regions, entry) if index > 0: previous = self.regions[index-1][1] if previous.end > region.start: self.raise_overlap(previous, region) if index < len(self.regions): next = self.regions[index][1] if next.start < region.end: self.raise_overlap(region, next) self.regions.insert(index, entry) def __iter__(self): line = 1 place = 0 for _, region in self.regions: line += self.text.count('\n', place, region.start) line_start = self.text.rfind('\n', place, region.start) place = region.start yield Example(self, line, region.start-line_start, region, self.namespace) def find_region_sources(self, start_pattern, end_pattern): """ This helper method can be called used to extract source text for regions based on the two :ref:`regular expressions ` provided. It will yield a tuple of ``(start_match, end_match, source)`` for each occurrence of ``start_pattern`` in the document's :attr:`~Document.text` that is followed by an occurrence of ``end_pattern``. The matches will be provided as :ref:`match objects `, while the source is provided as a string. """ for start_match in re.finditer(start_pattern, self.text): source_start = start_match.end() end_match = end_pattern.search(self.text, source_start) source_end = end_match.start() source = self.text[source_start:source_end] yield start_match, end_match, source sybil-1.2.0/sybil/example.py0000644000000000000000000000434713461247305015736 0ustar rootroot00000000000000class SybilFailure(AssertionError): def __init__(self, example, result): super(SybilFailure, self).__init__(( 'Example at {}, line {}, column {} did not evaluate as expected:\n' '{}' ).format(example.document.path, example.line, example.column, result)) self.example = example self.result = result class Example(object): """ This represents a particular example from a documentation source file. It is assembled from the :class:`~sybil.document.Document` and :class:`~sybil.Region` the example comes from and is passed to the region's evaluator. """ def __init__(self, document, line, column, region, namespace): #: The :class:`~sybil.document.Document` from which this example came. self.document = document #: The absolute path of the :class:`~sybil.document.Document`. self.path = document.path #: The line number at which this example occurs in the #: :class:`~sybil.document.Document`. self.line = line #: The column number at which this example occurs in the #: :class:`~sybil.document.Document`. self.column = column #: The :class:`~sybil.Region` from which this example came. self.region = region #: The character position at which this example starts in the #: :class:`~sybil.document.Document`. self.start = region.start #: The character position at which this example ends in the #: :class:`~sybil.document.Document`. self.end = region.end #: The version of this example provided by the parser that yielded #: the :class:`~sybil.Region` containing it. self.parsed = region.parsed #: The :attr:`~sybil.document.Document.namespace` of the document from #: which this example came. self.namespace = namespace def __repr__(self): return ''.format( self.document.path, self.line, self.column, self.region.evaluator ) def evaluate(self): evaluator = self.document.evaluator or self.region.evaluator result = evaluator(self) if result: raise SybilFailure(self, result) sybil-1.2.0/sybil/integration/0000755000000000000000000000000013461247332016244 5ustar rootroot00000000000000sybil-1.2.0/sybil/integration/__init__.py0000644000000000000000000000000013461247305020343 0ustar rootroot00000000000000sybil-1.2.0/sybil/integration/nose.py0000644000000000000000000000225013461247305017561 0ustar rootroot00000000000000from __future__ import absolute_import from nose.plugins import Plugin as NosePlugin from nose.loader import TestLoader class SybilLoader(TestLoader): def __init__(self, test_suite_func, config): super(SybilLoader, self).__init__(config) self.test_suite_func = test_suite_func def loadTestsFromModule(self, module, path=None, discovered=False): suite_func = getattr(module, self.test_suite_func, None) if suite_func is not None: return suite_func() return super(SybilLoader, self).loadTestsFromModule( module, path, discovered ) class Plugin(NosePlugin): name = 'sybil' enabled = True def options(self, parser, env): parser.add_option( "--test-suite-func", action="store", dest="test_suite_func", default='load_tests', help="A function in modules that will return a TestSuite. " "Defaults to 'load_tests'.") def configure(self, options, config): self.test_suite_func = options.test_suite_func def prepareTestLoader(self, loader): return SybilLoader(self.test_suite_func, loader.config) sybil-1.2.0/sybil/integration/pytest.py0000644000000000000000000000726613461247305020161 0ustar rootroot00000000000000from __future__ import absolute_import from inspect import getsourcefile from os.path import abspath from _pytest._code.code import TerminalRepr, Traceback from _pytest import fixtures from _pytest.fixtures import FuncFixtureInfo from _pytest.main import Session from _pytest.python import Module import pytest from ..example import SybilFailure from .. import example example_module_path = abspath(getsourcefile(example)) class SybilFailureRepr(TerminalRepr): def __init__(self, item, message): self.item = item self.message = message def toterminal(self, tw): tw.line() for line in self.message.splitlines(): tw.line(line) tw.line() tw.write(self.item.parent.name, bold=True, red=True) tw.line(":%s: SybilFailure" % self.item.example.line) class SybilItem(pytest.Item): def __init__(self, parent, sybil, example): name = 'line:{},column:{}'.format(example.line, example.column) super(SybilItem, self).__init__(name, parent) self.example = example self.request_fixtures(sybil.fixtures) def request_fixtures(self, names): # pytest fixtures dance: fm = self.session._fixturemanager closure = fm.getfixtureclosure(names, self) try: initialnames, names_closure, arg2fixturedefs = closure except ValueError: # pragma: no cover # pytest < 3.7 names_closure, arg2fixturedefs = closure fixtureinfo = FuncFixtureInfo(names, names_closure, arg2fixturedefs) else: # pyest >= 3.7 fixtureinfo = FuncFixtureInfo(names, initialnames, names_closure, arg2fixturedefs) self._fixtureinfo = fixtureinfo self.funcargs = {} self._request = fixtures.FixtureRequest(self) def reportinfo(self): info = '%s line=%i column=%i' % ( self.fspath.basename, self.example.line, self.example.column ) return self.example.document.path, self.example.line, info def getparent(self, cls): if cls is Module: return self.parent if cls is Session: return self.session def setup(self): fixtures.fillfixtures(self) for name, fixture in self.funcargs.items(): self.example.namespace[name] = fixture def runtest(self): self.example.evaluate() def _prunetraceback(self, excinfo): # Messier than it could be because slicing a list subclass in # Python 2 returns a list, not an instance of the subclass. tb = excinfo.traceback.cut(path=example_module_path) tb = tb[1] if getattr(tb, '_rawentry', None) is not None: excinfo.traceback = Traceback(tb._rawentry, excinfo) def repr_failure(self, excinfo): if isinstance(excinfo.value, SybilFailure): return SybilFailureRepr(self, str(excinfo.value)) return super(SybilItem, self).repr_failure(excinfo) class SybilFile(pytest.File): def __init__(self, path, parent, sybil): super(SybilFile, self).__init__(path, parent) self.sybil = sybil def collect(self): self.document = self.sybil.parse(self.fspath.strpath) for example in self.document: yield SybilItem(self, self.sybil, example) def setup(self): if self.sybil.setup: self.sybil.setup(self.document.namespace) def teardown(self): if self.sybil.teardown: self.sybil.teardown(self.document.namespace) def pytest_integration(sybil): def pytest_collect_file(parent, path): if sybil.should_test_filename(path.basename): return SybilFile(path, parent, sybil) return pytest_collect_file sybil-1.2.0/sybil/integration/unittest.py0000644000000000000000000000225013461247305020474 0ustar rootroot00000000000000from __future__ import absolute_import from unittest import TestCase as BaseTestCase, TestSuite class TestCase(BaseTestCase): sybil = namespace = None def __init__(self, example): BaseTestCase.__init__(self) self.example = example def runTest(self): self.example.evaluate() def id(self): return '{},line:{},column:{}'.format( self.example.document.path, self.example.line, self.example.column ) __str__ = __repr__ = id @classmethod def setUpClass(cls): if cls.sybil.setup is not None: cls.sybil.setup(cls.namespace) @classmethod def tearDownClass(cls): if cls.sybil.teardown is not None: cls.sybil.teardown(cls.namespace) def unittest_integration(sybil): def load_tests(loader=None, tests=None, pattern=None): suite = TestSuite() for document in sybil.all_documents(): case = type(document.path, (TestCase, ), dict( sybil=sybil, namespace=document.namespace, )) for example in document: suite.addTest(case(example)) return suite return load_tests sybil-1.2.0/sybil/parsers/0000755000000000000000000000000013461247332015400 5ustar rootroot00000000000000sybil-1.2.0/sybil/parsers/__init__.py0000644000000000000000000000000013461247305017477 0ustar rootroot00000000000000sybil-1.2.0/sybil/parsers/capture.py0000644000000000000000000000544413461247305017424 0ustar rootroot00000000000000import re import string from textwrap import dedent from sybil import Region from sybil.compat import StringIO CAPTURE_DIRECTIVE = re.compile( r'^(?P(\t| )*)\.\.\s*-+>\s*(?P\S+).*$' ) def evaluate_capture(example): name, text = example.parsed example.namespace[name] = text def indent_matches(line, indent): # Is the indentation of a line match what we're looking for? 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 class DocumentReverseIterator(list): def __init__(self, document): # using splitlines(keepends=True) would be more explicit # but Python 2 :-( self[:] = StringIO(document.text) self.current_line = len(self) self.current_line_end_position = len(document.text) def __iter__(self): while self.current_line > 0: self.current_line -= 1 line = self[self.current_line] self.current_line_end_position -= len(line) yield self.current_line, line def parse_captures(document): """ A parser function to be included when your documentation makes use of :ref:`capture-parser` examples. """ lines = DocumentReverseIterator(document) for end_index, line in lines: directive = CAPTURE_DIRECTIVE.match(line) if directive: region_end = lines.current_line_end_position indent = directive.group('indent') for start_index, line in lines: if indent_matches(line, indent): # don't include the preceding line in the capture start_index += 1 break else: # make it blow up start_index = end_index if end_index - start_index < 2: raise ValueError(( "couldn't find the start of the block to match" "%r on line %i of %s" ) % (directive.group(), end_index+1, document.path)) # after dedenting, we need to remove excess leading and trailing # newlines, before adding back the final newline that's strippped # off text = dedent(''.join(lines[start_index:end_index])).strip()+'\n' name = directive.group('name') parsed = name, text yield Region( lines.current_line_end_position, region_end, parsed, evaluate_capture ) sybil-1.2.0/sybil/parsers/codeblock.py0000644000000000000000000000412613461247305017702 0ustar rootroot00000000000000import re import textwrap from sybil import Region CODEBLOCK_START = re.compile( r'^(?P[ \t]*)\.\.\s*(invisible-)?code(-block)?::?\s*python\b' r'(?:\s*\:[\w-]+\:.*\n)*' r'(?:\s*\n)*', re.MULTILINE) def compile_codeblock(source, path): return compile(source, path, 'exec', dont_inherit=True) def evaluate_code_block(example): code = compile_codeblock(example.parsed, example.document.path) exec(code, example.namespace) # exec adds __builtins__, we don't want it: del example.namespace['__builtins__'] class CodeBlockParser(object): """ A class to instantiate and include when your documentation makes use of :ref:`codeblock-parser` examples. :param future_imports: An optional list of strings that will be turned into ``from __future__ import ...`` statements and prepended to the code in each of the examples found by this parser. """ def __init__(self, future_imports=()): self.future_imports = future_imports def __call__(self, document): for start_match in re.finditer(CODEBLOCK_START, document.text): source_start = start_match.end() indent = str(len(start_match.group('indent'))) end_pattern = re.compile(r'(\n\Z|\n[ \t]{0,'+indent+'}(?=\S))') end_match = end_pattern.search(document.text, source_start) source_end = end_match.start() source = textwrap.dedent(document.text[source_start:source_end]) # There must be a nicer way to get code.co_firstlineno # to be correct... line_count = document.text.count('\n', 0, source_start) if self.future_imports: line_count -= 1 source = 'from __future__ import {}\n{}'.format( ', '.join(self.future_imports), source ) line_prefix = '\n' * line_count source = line_prefix + source yield Region( start_match.start(), source_end, source, evaluate_code_block ) sybil-1.2.0/sybil/parsers/doctest.py0000644000000000000000000001012013461247305017411 0ustar rootroot00000000000000from __future__ import absolute_import import re from doctest import ( DocTest as BaseDocTest, DocTestParser as BaseDocTestParser, DocTestRunner as BaseDocTestRunner, Example as DocTestExample, _unittest_reportflags, register_optionflag ) from ..compat import PY3 from ..region import Region def make_literal(literal): return re.compile(literal+r"((['\"])[^\2]*\2)", re.MULTILINE) BYTE_LITERAL = make_literal('b') UNICODE_LITERAL = make_literal('u') #: A :ref:`doctest option flag` that #: causes byte and unicode literals in doctest expected #: output to be rewritten such that they are compatible with the version of #: Python with which the tests are executed. FIX_BYTE_UNICODE_REPR = register_optionflag('FIX_BYTE_UNICODE_REPR') class DocTest(BaseDocTest): def __init__(self, examples, globs, name, filename, lineno, docstring): # do everything like regular doctests, but don't make a copy of globs BaseDocTest.__init__(self, examples, globs, name, filename, lineno, docstring) self.globs = globs class DocTestRunner(BaseDocTestRunner): def __init__(self, optionflags=0): optionflags |= _unittest_reportflags BaseDocTestRunner.__init__(self, verbose=False, optionflags=optionflags) def _failure_header(self, test, example): return '' def fix_byte_unicode_repr(want): if PY3: pattern = UNICODE_LITERAL else: pattern = BYTE_LITERAL return pattern.sub(r"\1", want) class DocTestParser(BaseDocTestParser): """ A class to instantiate and include when your documentation makes use of :ref:`doctest-parser` examples. :param optionflags: :ref:`doctest option flags` to use when evaluating the examples found by this parser. """ def __init__(self, optionflags=0): self.runner = DocTestRunner(optionflags=optionflags) def __call__(self, document): # a cut down version of doctest.DocTestParser.parse: text = document.text first_tab = text.find('\t') if first_tab != -1: raise ValueError('tabs are not supported, first one found at '+( document.line_column(first_tab) )) # If all lines begin with the same indentation, then strip it. min_indent = self._min_indent(text) if min_indent > 0: text = '\n'.join([l[min_indent:] for l in text.split('\n')]) charno, lineno = 0, 0 # Find all doctest examples in the string: for m in self._EXAMPLE_RE.finditer(text): # Update lineno (lines before this example) lineno += text.count('\n', charno, m.start()) # Extract info from the regexp match. (source, options, want, exc_msg) = \ self._parse_example(m, document.path, lineno) if self.runner.optionflags & FIX_BYTE_UNICODE_REPR: want = fix_byte_unicode_repr(want) if exc_msg: exc_msg = fix_byte_unicode_repr(exc_msg) # Create an Example, and add it to the list. if not self._IS_BLANK_OR_COMMENT(source): yield Region( m.start(), m.end(), DocTestExample(source, want, exc_msg, lineno=lineno, indent=min_indent+len(m.group('indent')), options=options), self.evaluate ) # Update lineno (lines inside this example) lineno += text.count('\n', m.start(), m.end()) # Update charno. charno = m.end() def evaluate(self, sybil_example): example = sybil_example.parsed namespace = sybil_example.namespace output = [] self.runner.run( DocTest([example], namespace, name=None, filename=None, lineno=example.lineno, docstring=None), clear_globs=False, out=output.append ) return ''.join(output) sybil-1.2.0/sybil/parsers/skip.py0000644000000000000000000000415413461247305016724 0ustar rootroot00000000000000from unittest import SkipTest import re from sybil import Region SKIP = re.compile(r'^[ \t]*\.\.\s*skip:\s*(\w+)(?:\s+if(.+)$)?', re.MULTILINE) class If: def __init__(self, default_reason): self.default_reason = default_reason def __call__(self, condition, reason=None): if condition: return reason or self.default_reason class Skip(object): def __init__(self, original_evaluator): self.original_evaluator = original_evaluator self.restore_next = False self.reason = None def __call__(self, example): document = example.document if self.restore_next: document.evaluator = self.original_evaluator if example.region.evaluator is evaluate_skip: action, condition = example.parsed if condition: if action == 'end': raise ValueError('Cannot have condition on end') namespace = document.namespace.copy() namespace['If'] = If(condition) reason = eval('If'+condition, namespace) if reason: self.reason = SkipTest(reason) else: document.evaluator = self.original_evaluator return if action == 'next': self.restore_next = True elif action == 'start': pass elif action == 'end': document.evaluator = self.original_evaluator else: raise ValueError('Bad skip action: '+action) elif self.reason: raise self.reason def evaluate_skip(example): evaluator = example.document.evaluator if not isinstance(evaluator, Skip): example.document.evaluator = evaluator = Skip(evaluator) evaluator(example) def skip(document): """ A parser function to be included when your documentation makes use of :ref:`skipping ` examples in a document. """ for match in re.finditer(SKIP, document.text): yield Region(match.start(), match.end(), match.groups(), evaluate_skip) sybil-1.2.0/sybil/region.py0000644000000000000000000000162713461247305015564 0ustar rootroot00000000000000class Region(object): """ Parsers should yield instances of this class for each example they discover in a documentation source file. :param start: The character position at which the example starts in the :class:`~sybil.document.Document`. :param end: The character position at which the example ends in the :class:`~sybil.document.Document`. :param parsed: The parsed version of the example. :param evaluator: The callable to use to evaluate this example and check if it is as it should be. """ def __init__(self, start, end, parsed, evaluator): self.start, self.end, self.parsed, self.evaluator = ( start, end, parsed, evaluator ) def __repr__(self): return ''.format( self.start, self.end, self.evaluator ) sybil-1.2.0/sybil/sybil.py0000644000000000000000000000725113461247305015422 0ustar rootroot00000000000000import sys from fnmatch import fnmatch from glob import glob from os import listdir from os.path import join, dirname, abspath from .document import Document class FilenameFilter(object): def __init__(self, pattern, filenames, excludes): self.pattern = pattern self.filenames = filenames self.excludes = excludes def __call__(self, filename): return ( (fnmatch(filename, self.pattern) or filename in self.filenames) and not any(fnmatch(filename, e) for e in self.excludes) ) class Sybil(object): """ An object to provide test runner integration for discovering examples in documentation and ensuring they are correct. :param parsers: A sequence of callables. See :doc:`parsers`. :param path: The path in which documentation source files are found, relative to the path of the Python source file in which this class is instantiated. .. note:: This is ignored when using the :ref:`pytest integration `. :param pattern: An optional :func:`pattern ` used to match documentation source files that will be parsed for examples. :param filenames: An optional :class:`set` of source file names that will be parsed for examples. :param excludes: An optional sequence of :func:`patterns ` of source file names that will excluded when looking for examples. :param setup: An optional callable that will be called once before any examples from a :class:`~sybil.document.Document` are evaluated. If provided, it is called with the document's :attr:`~sybil.document.Document.namespace`. :param teardown: An optional callable that will be called after all the examples from a :class:`~sybil.document.Document` have been evaluated. If provided, it is called with the document's :attr:`~sybil.document.Document.namespace`. :param fixtures: An optional sequence of strings specifying the names of fixtures to be requested when using the :ref:`pytest integration `. The fixtures will be inserted into the document's :attr:`~sybil.document.Document.namespace`. All scopes of fixture are supported. """ def __init__(self, parsers, pattern='', path='.', setup=None, teardown=None, fixtures=(), filenames=(), excludes=()): self.parsers = parsers calling_filename = sys._getframe(1).f_globals.get('__file__') if calling_filename: start_path = join(dirname(calling_filename), path) else: start_path = path self.path = abspath(start_path) self.should_test_filename = FilenameFilter(pattern, filenames, excludes) self.setup = setup self.teardown = teardown self.fixtures = fixtures def parse(self, path): return Document.parse(path, *self.parsers) def all_documents(self): for filename in sorted(listdir(self.path)): if self.should_test_filename(filename): yield self.parse(join(self.path, filename)) def pytest(self): """ The helper method for when you use :ref:`pytest_integration`. """ from .integration.pytest import pytest_integration return pytest_integration(self) def unittest(self): """ The helper method for when you use either :ref:`unitttest_integration` or :ref:`nose_integration`. """ from .integration.unittest import unittest_integration return unittest_integration(self) nose = unittest sybil-1.2.0/sybil.egg-info/0000755000000000000000000000000013461247332015413 5ustar rootroot00000000000000sybil-1.2.0/sybil.egg-info/PKG-INFO0000644000000000000000000000167213461247332016516 0ustar rootroot00000000000000Metadata-Version: 2.1 Name: sybil Version: 1.2.0 Summary: Automated testing for the examples in your documentation. Home-page: https://github.com/cjw296/sybil Author: Chris Withers Author-email: chris@withers.org License: MIT Description: ===== sybil ===== Automated testing for the examples in your documentation. The latest documentation can be found at: http://sybil.readthedocs.org/en/latest/ Development takes place here: https://github.com/cjw296/sybil/ Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Provides-Extra: build Provides-Extra: test sybil-1.2.0/sybil.egg-info/SOURCES.txt0000644000000000000000000000420013461247332017273 0ustar rootroot00000000000000.carthorse.yml .gitignore .readthedocs.yml README.rst setup.cfg setup.py .circleci/config.yml docs/Makefile docs/api.rst docs/changes.rst docs/conf.py docs/conftest.py docs/description.rst docs/development.rst docs/example-skip.rst docs/example.rst docs/index.rst docs/license.rst docs/parsers.rst docs/use.rst docs/example/pytest.ini docs/example/docs/conftest.py docs/example/docs/example.rst docs/example/example_nose/__init__.py docs/example/example_nose/test_example_docs.py docs/example/example_unittest/__init__.py docs/example/example_unittest/test_example_docs.py sybil/__init__.py sybil/compat.py sybil/document.py sybil/example.py sybil/region.py sybil/sybil.py sybil.egg-info/PKG-INFO sybil.egg-info/SOURCES.txt sybil.egg-info/dependency_links.txt sybil.egg-info/entry_points.txt sybil.egg-info/not-zip-safe sybil.egg-info/requires.txt sybil.egg-info/top_level.txt sybil/integration/__init__.py sybil/integration/nose.py sybil/integration/pytest.py sybil/integration/unittest.py sybil/parsers/__init__.py sybil/parsers/capture.py sybil/parsers/codeblock.py sybil/parsers/doctest.py sybil/parsers/skip.py tests/__init__.py tests/helpers.py tests/test_capture.py tests/test_codeblock.py tests/test_doc_example.py tests/test_doctest.py tests/test_functional.py tests/test_skip.py tests/test_sybil.py tests/functional/functional_unittest/__init__.py tests/functional/functional_unittest/test_unittest.py tests/functional/nose/__init__.py tests/functional/nose/test_nose.py tests/functional/nose/test_other.py tests/functional/pytest/conftest.py tests/functional/pytest/fail.rst tests/functional/pytest/pass.rst tests/functional/pytest/pytest.ini tests/samples/capture.txt tests/samples/capture_bad_indent1.txt tests/samples/capture_bad_indent2.txt tests/samples/codeblock.txt tests/samples/codeblock_future_imports.txt tests/samples/doctest.txt tests/samples/doctest_fail.txt tests/samples/doctest_literals.txt tests/samples/doctest_min_indent.txt tests/samples/doctest_tabs.txt tests/samples/sample1.txt tests/samples/sample2.txt tests/samples/skip-conditional-bad.txt tests/samples/skip-conditional-edges.txt tests/samples/skip-conditional.txt tests/samples/skip.txtsybil-1.2.0/sybil.egg-info/dependency_links.txt0000644000000000000000000000000113461247332021461 0ustar rootroot00000000000000 sybil-1.2.0/sybil.egg-info/entry_points.txt0000644000000000000000000000007313461247332020711 0ustar rootroot00000000000000[nose.plugins.0.10] sybil = sybil.integration.nose:Plugin sybil-1.2.0/sybil.egg-info/not-zip-safe0000644000000000000000000000000113461247322017640 0ustar rootroot00000000000000 sybil-1.2.0/sybil.egg-info/requires.txt0000644000000000000000000000013113461247332020006 0ustar rootroot00000000000000 [build] sphinx pkginfo setuptools-git twine wheel [test] nose pytest>=3.5.0 pytest-cov sybil-1.2.0/sybil.egg-info/top_level.txt0000644000000000000000000000000613461247332020141 0ustar rootroot00000000000000sybil sybil-1.2.0/tests/0000755000000000000000000000000013461247332013741 5ustar rootroot00000000000000sybil-1.2.0/tests/__init__.py0000644000000000000000000000005413461247305016051 0ustar rootroot00000000000000# believe it or not, # this line is a test! sybil-1.2.0/tests/functional/0000755000000000000000000000000013461247332016103 5ustar rootroot00000000000000sybil-1.2.0/tests/functional/functional_unittest/0000755000000000000000000000000013461247332022204 5ustar rootroot00000000000000sybil-1.2.0/tests/functional/functional_unittest/__init__.py0000644000000000000000000000000013461247305024303 0ustar rootroot00000000000000sybil-1.2.0/tests/functional/functional_unittest/test_unittest.py0000644000000000000000000000205613461247305025477 0ustar rootroot00000000000000from __future__ import print_function import re from functools import partial from sybil import Sybil, Region def check(letter, example): print(example.namespace['x']) example.namespace['x'] += 1 text, expected = example.parsed actual = text.count(letter) if actual != expected: message = '{} count was {} instead of {}'.format( letter, actual, expected ) if letter=='X': raise ValueError(message) return message def parse_for(letter, document): for m in re.finditer(r'(%s+) (\d+) check' % letter, document.text): yield Region(m.start(), m.end(), (m.group(1), int(m.group(2))), partial(check, letter)) def sybil_setup(namespace): print('sybil setup') namespace['x'] = 0 def sybil_teardown(namespace): print('sybil teardown', namespace['x']) load_tests = Sybil( [partial(parse_for, 'X'), partial(parse_for, 'Y')], path='../pytest', pattern='*.rst', setup=sybil_setup, teardown=sybil_teardown ).unittest() sybil-1.2.0/tests/functional/nose/0000755000000000000000000000000013461247332017047 5ustar rootroot00000000000000sybil-1.2.0/tests/functional/nose/__init__.py0000644000000000000000000000000013461247305021146 0ustar rootroot00000000000000sybil-1.2.0/tests/functional/nose/test_nose.py0000644000000000000000000000205213461247305021423 0ustar rootroot00000000000000from __future__ import print_function import re from functools import partial from sybil import Sybil, Region def check(letter, example): print(example.namespace['x']) example.namespace['x'] += 1 text, expected = example.parsed actual = text.count(letter) if actual != expected: message = '{} count was {} instead of {}'.format( letter, actual, expected ) if letter=='X': raise ValueError(message) return message def parse_for(letter, document): for m in re.finditer(r'(%s+) (\d+) check' % letter, document.text): yield Region(m.start(), m.end(), (m.group(1), int(m.group(2))), partial(check, letter)) def sybil_setup(namespace): print('sybil setup') namespace['x'] = 0 def sybil_teardown(namespace): print('sybil teardown', namespace['x']) load_tests = Sybil( [partial(parse_for, 'X'), partial(parse_for, 'Y')], path='../pytest', pattern='*.rst', setup=sybil_setup, teardown=sybil_teardown ).nose() sybil-1.2.0/tests/functional/nose/test_other.py0000644000000000000000000000003013461247305021572 0ustar rootroot00000000000000def test_it(): pass sybil-1.2.0/tests/functional/pytest/0000755000000000000000000000000013461247332017433 5ustar rootroot00000000000000sybil-1.2.0/tests/functional/pytest/conftest.py0000644000000000000000000000400613461247305021632 0ustar rootroot00000000000000from __future__ import print_function from functools import partial import re import pytest from sybil import Region, Sybil from sybil.parsers.codeblock import CodeBlockParser @pytest.fixture(scope="function") def function_fixture(): print('function_fixture setup') yield 'f' print(' function_fixture teardown') @pytest.fixture(scope="class") def class_fixture(): print('class_fixture setup') yield 'c' print('class_fixture teardown') @pytest.fixture(scope="module") def module_fixture(): print('module_fixture setup') yield 'm' print('module_fixture teardown') @pytest.fixture(scope="session") def session_fixture(): print('session_fixture setup') yield 's' print('session_fixture teardown') def check(letter, example): namespace = example.namespace for name in ( 'x', 'session_fixture', 'module_fixture', 'class_fixture', 'function_fixture' ): print(namespace[name], end='') print(end=' ') namespace['x'] += 1 text, expected = example.parsed actual = text.count(letter) if actual != expected: message = '{} count was {} instead of {}'.format( letter, actual, expected ) if letter=='X': raise ValueError(message) return message def parse_for(letter, document): for m in re.finditer(r'(%s+) (\d+) check' % letter, document.text): yield Region(m.start(), m.end(), (m.group(1), int(m.group(2))), partial(check, letter)) def sybil_setup(namespace): print('sybil setup', end=' ') namespace['x'] = 0 def sybil_teardown(namespace): print('sybil teardown', namespace['x']) pytest_collect_file = Sybil( parsers=[ partial(parse_for, 'X'), partial(parse_for, 'Y'), CodeBlockParser(['print_function']) ], pattern='*.rst', setup=sybil_setup, teardown=sybil_teardown, fixtures=['function_fixture', 'class_fixture', 'module_fixture', 'session_fixture'] ).pytest() sybil-1.2.0/tests/functional/pytest/fail.rst0000644000000000000000000000033113461247305021075 0ustar rootroot00000000000000.. code-block:: python print('x is currently:', x) raise Exception('the start!') XXXX 4 check YYY 2 check XXX 4 check YYY 3 check .. code-block:: python x += 1 if x > 0: raise Exception('boom!') sybil-1.2.0/tests/functional/pytest/pass.rst0000644000000000000000000000006413461247305021133 0ustar rootroot00000000000000XXXX 4 check YYY 3 check XXX 3 check YYY 3 check sybil-1.2.0/tests/functional/pytest/pytest.ini0000644000000000000000000000005013461247305021457 0ustar rootroot00000000000000[pytest] console_output_style = classic sybil-1.2.0/tests/helpers.py0000644000000000000000000000103013461247305015747 0ustar rootroot00000000000000from os.path import dirname, join from sybil.document import Document from sybil.example import Example def sample_path(name): return join(dirname(__file__), 'samples', name) def document_from_sample(name): path = sample_path(name) with open(path) as source: return Document(source.read(), path) def evaluate_region(region, namespace): return region.evaluator(Example( document=Document('', '/the/path'), line=0, column=0, region=region, namespace=namespace )) sybil-1.2.0/tests/samples/0000755000000000000000000000000013461247332015405 5ustar rootroot00000000000000sybil-1.2.0/tests/samples/capture.txt0000644000000000000000000000126613461247305017616 0ustar rootroot00000000000000A simple example:: root.txt subdir/ subdir/file.txt subdir/logs/ .. -> expected_listing 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 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 That holds true even if more dashes are included:: example .. --> another sybil-1.2.0/tests/samples/capture_bad_indent1.txt0000644000000000000000000000013313461247305022036 0ustar rootroot00000000000000The directive here is beyond the indentation of the block:: Block .. -> foo sybil-1.2.0/tests/samples/capture_bad_indent2.txt0000644000000000000000000000013013461247305022034 0ustar rootroot00000000000000The directive here is at the same indentation as the block:: Block .. -> foo sybil-1.2.0/tests/samples/codeblock.txt0000644000000000000000000000154013461247305020073 0ustar rootroot00000000000000This is a code block: .. code-block:: python y += 1 After this text is a code block that goes boom: .. code-block:: python raise Exception('boom!') Now we have an invisible code block, great for setting things up or checking stuff within a doc: .. invisible-code-block: python z += 1 This paranoidly checks that we can use binary and unicode literals: .. code-block:: python bin = b'x' uni = u'x' - Here's a code block that should still be found!: .. code-block:: python class NoVars(object): __slots__ = ['x'] This one has some text after it that also forms part of the bullet. - Another bullet: .. code-block:: python define_this = 1 - A following bullet straight away! - Here's another code block that should still be found!: .. code-block:: python class YesVars(object): __slots__ = ['x'] sybil-1.2.0/tests/samples/codeblock_future_imports.txt0000644000000000000000000000033713461247305023245 0ustar rootroot00000000000000.. invisible-code-block: python print('pathalogical worst case for line numbers', file=buffer) More likely is one down here: .. code-block:: python print('still should work and have good line numbers', file=buffer) sybil-1.2.0/tests/samples/doctest.txt0000644000000000000000000000044413461247305017615 0ustar rootroot00000000000000This is some documentation. >>> y = 1 >>> print('here is an example') here is an example This is some more documentation. >>> x = [ ... 1, 2, 3 ... ] Here's an exception with a traceback: >>> y = 2 >>> raise Exception('uh oh') Traceback (most recent call last): ... Exception: uh oh sybil-1.2.0/tests/samples/doctest_fail.txt0000644000000000000000000000015413461247305020606 0ustar rootroot00000000000000>>> print("where's my output?") Not my output Here's an exception happening: >>> raise Exception('boom!') sybil-1.2.0/tests/samples/doctest_literals.txt0000644000000000000000000000030513461247305021510 0ustar rootroot00000000000000>>> repr(b'foo') "b'foo'" >>> repr(u'foo') "u'foo'" >>> repr(b"'") 'b"\'"' >>> repr(u"'") 'u"\'"' >>> raise Exception(repr(u'uh oh')) Traceback (most recent call last): ... Exception: u'uh oh' sybil-1.2.0/tests/samples/doctest_min_indent.txt0000644000000000000000000000005413461247305022016 0ustar rootroot00000000000000 Just for the coverage: >>> True True sybil-1.2.0/tests/samples/doctest_tabs.txt0000644000000000000000000000017513461247305020627 0ustar rootroot00000000000000>>> handler = SummarisingLogger('from@example.com',('to@example.com',), ... username='auser',password='theirpassword') sybil-1.2.0/tests/samples/sample1.txt0000644000000000000000000000003213461247305017503 0ustar rootroot00000000000000XXXX 4 check YYY 3 check sybil-1.2.0/tests/samples/sample2.txt0000644000000000000000000000003113461247305017503 0ustar rootroot00000000000000XXX 4 check YYY 3 check sybil-1.2.0/tests/samples/skip-conditional-bad.txt0000644000000000000000000000024313461247305022140 0ustar rootroot00000000000000Bad action: .. skip: lolwut Condition on end: .. skip: end if(1 > 2) Malformed if: .. skip: next if(sys.version_info < (3, 0), reason="only true on python 3" sybil-1.2.0/tests/samples/skip-conditional-edges.txt0000644000000000000000000000100213461247305022473 0ustar rootroot00000000000000This will only work on Python 2: .. skip: next if(sys.version_info >= (3, 0), reason="only true on python 2") >>> repr(u'foo') "u'foo'" This should run: >>> run.append(1) This will only parse on Python 3: .. skip: start if(sys.version_info < (3, 0), reason="only true on python 3") .. code-block:: python def myfunc(*, keyword_only): return keyword_only >>> myfunc(keyword_only='hello') 'hello' >>> def myfunc2(*, keyword_only): pass .. skip: end This should also run: >>> run.append(2) sybil-1.2.0/tests/samples/skip-conditional.txt0000644000000000000000000000043013461247305021412 0ustar rootroot00000000000000>>> result.append('start') Default reason: .. skip: next if(2 > 1) Should not run: >>> result.append('bad 1') >>> result.append('good 1') .. skip: start if(2 > 1, reason='foo') >>> result.append('bad 2') >>> result.append('bad 3') .. skip: end >>> result.append('good 2') sybil-1.2.0/tests/samples/skip.txt0000644000000000000000000000073513461247305017121 0ustar rootroot00000000000000.. invisible-code-block: python run = [] Let's skips some stuff: .. skip: next After this text is a code block that goes boom, it should be skipped: .. code-block:: python run.append(1) This one should run: .. invisible-code-block: python run.append(2) .. skip: start These should not: .. code-block:: python run.append(3) Nor this one: .. code-block:: python run.append(4) .. skip: end But this one should: .. code-block:: python run.append(5) sybil-1.2.0/tests/test_capture.py0000644000000000000000000000330713461247305017020 0ustar rootroot00000000000000import pytest from sybil.parsers.capture import parse_captures from tests.helpers import document_from_sample, sample_path, evaluate_region def test_basic(): document = document_from_sample('capture.txt') regions = list(parse_captures(document)) namespace = document.namespace assert evaluate_region(regions[-1], namespace) is None assert namespace['expected_listing'] == ( 'root.txt\n' 'subdir/\n' 'subdir/file.txt\n' 'subdir/logs/\n' ) assert evaluate_region(regions[-2], namespace) is None assert namespace['foo'] == 'Third level of indentation.\n' assert evaluate_region(regions[-3], namespace) is None assert namespace['bar'] == ( 'Second level of indentation.\n\n' ' Third level of indentation.\n\n.. -> foo\n' ) assert evaluate_region(regions[-4], namespace) is None assert namespace['another'] == ( 'example\n' ) assert len(regions) == 4 def test_directive_indent_beyond_block(): document = document_from_sample('capture_bad_indent1.txt') with pytest.raises(ValueError) as excinfo: list(parse_captures(document)) assert str(excinfo.value) == ( "couldn't find the start of the block to match' .. -> foo' " "on line 5 of "+sample_path('capture_bad_indent1.txt') ) def test_directive_indent_equal_to_block(): document = document_from_sample('capture_bad_indent2.txt') with pytest.raises(ValueError) as excinfo: list(parse_captures(document)) assert str(excinfo.value) == ( "couldn't find the start of the block to match' .. -> foo' " "on line 5 of "+sample_path('capture_bad_indent2.txt') ) sybil-1.2.0/tests/test_codeblock.py0000644000000000000000000000415513461247305017304 0ustar rootroot00000000000000import pytest from sybil.compat import StringIO from sybil.parsers.codeblock import CodeBlockParser, compile_codeblock from tests.helpers import document_from_sample, evaluate_region def test_basic(): document = document_from_sample('codeblock.txt') regions = list(CodeBlockParser()(document)) assert len(regions) == 7 namespace = document.namespace namespace['y'] = namespace['z'] = 0 assert evaluate_region(regions[0], namespace) is None assert namespace['y'] == 1 assert namespace['z'] == 0 with pytest.raises(Exception) as excinfo: evaluate_region(regions[1], namespace) assert str(excinfo.value) == 'boom!' assert evaluate_region(regions[2], namespace) is None assert namespace['y'] == 1 assert namespace['z'] == 1 assert evaluate_region(regions[3], namespace) is None assert namespace['bin'] == b'x' assert namespace['uni'] == u'x' assert evaluate_region(regions[4], namespace) is None assert 'NoVars' in namespace assert evaluate_region(regions[5], namespace) is None assert namespace['define_this'] == 1 assert evaluate_region(regions[6], namespace) is None assert 'YesVars' in namespace assert '__builtins__' not in namespace def test_future_imports(): document = document_from_sample('codeblock_future_imports.txt') regions = list(CodeBlockParser(['print_function'])(document)) assert len(regions) == 2 buffer = StringIO() namespace = {'buffer': buffer} assert evaluate_region(regions[0], namespace) is None assert buffer.getvalue() == ( 'pathalogical worst case for line numbers\n' ) # the future import line drops the firstlineno by 1 code = compile_codeblock(regions[0].parsed, document.path) assert code.co_firstlineno == 2 assert evaluate_region(regions[1], namespace) is None assert buffer.getvalue() == ( 'pathalogical worst case for line numbers\n' 'still should work and have good line numbers\n' ) # the future import line drops the firstlineno by 1 code = compile_codeblock(regions[1].parsed, document.path) assert code.co_firstlineno == 8 sybil-1.2.0/tests/test_doc_example.py0000644000000000000000000000325013461247305017632 0ustar rootroot00000000000000import sys from os import pardir from os.path import dirname, join from unittest.main import main as unittest_main from unittest.runner import TextTestRunner from nose.core import run_exit as NoseMain, TextTestRunner as NoseRunner from pytest import main as pytest_main example_dir = join(dirname(__file__), pardir, 'docs', 'example') def test_pytest(capsys): class CollectResults: def pytest_sessionfinish(self, session): self.session = session results = CollectResults() return_code = pytest_main([join(example_dir, 'docs')], plugins=[results]) assert return_code == 0 assert results.session.testsfailed == 0 assert results.session.testscollected == 3 def test_unittest(capsys): runner = TextTestRunner(verbosity=2, stream=sys.stdout) path = join(example_dir, 'example_unittest') main = unittest_main( exit=False, module=None, testRunner=runner, argv=['x', 'discover', '-s', path, '-t', path] ) assert main.result.testsRun == 3 assert len(main.result.failures) == 0 assert len(main.result.errors) == 0 def test_nose(capsys): class ResultStoringMain(NoseMain): def runTests(self): self.testRunner = NoseRunner(stream=sys.stdout, verbosity=self.config.verbosity, config=self.config) self.result = self.testRunner.run(self.test) main = ResultStoringMain( module=None, argv=['x', join(example_dir, 'example_nose')] ) assert main.result.testsRun == 3 assert len(main.result.failures) == 0 assert len(main.result.errors) == 0 sybil-1.2.0/tests/test_doctest.py0000644000000000000000000000473013461247305017023 0ustar rootroot00000000000000from doctest import REPORT_NDIFF, ELLIPSIS import pytest from sybil.document import Document from sybil.parsers.doctest import DocTestParser, FIX_BYTE_UNICODE_REPR from tests.helpers import document_from_sample, evaluate_region, sample_path def test_pass(): document = document_from_sample('doctest.txt') regions = list(DocTestParser()(document)) assert len(regions) == 5 namespace = document.namespace assert evaluate_region(regions[0], namespace) == '' assert namespace['y'] == 1 assert evaluate_region(regions[1], namespace) == '' assert namespace['y'] == 1 assert evaluate_region(regions[2], namespace) == '' assert namespace['x'] == [1, 2, 3] assert evaluate_region(regions[3], namespace) == '' assert namespace['y'] == 2 assert evaluate_region(regions[4], namespace) == '' assert namespace['y'] == 2 def test_fail(): document = document_from_sample('doctest_fail.txt') regions = list(DocTestParser()(document)) assert len(regions) == 2 assert evaluate_region(regions[0], {}) == ( "Expected:\n" " Not my output\n" "Got:\n" " where's my output?\n" ) actual = evaluate_region(regions[1], {}) assert actual.startswith('Exception raised:') assert actual.endswith('Exception: boom!\n') def test_fail_with_options(): document = document_from_sample('doctest_fail.txt') regions = list(DocTestParser(optionflags=REPORT_NDIFF|ELLIPSIS)(document)) assert len(regions) == 2 assert evaluate_region(regions[0], {}) == ( "Differences (ndiff with -expected +actual):\n" " - Not my output\n" " + where's my output?\n" ) def test_literals(): document = document_from_sample('doctest_literals.txt') regions = list(DocTestParser(FIX_BYTE_UNICODE_REPR)(document)) assert len(regions) == 5 for region in regions: assert evaluate_region(region, {}) == '' def test_min_indent(): document = document_from_sample('doctest_min_indent.txt') regions = list(DocTestParser()(document)) assert len(regions) == 1 namespace = document.namespace assert evaluate_region(regions[0], namespace) == '' def test_tabs(): path = sample_path('doctest_tabs.txt') parser = DocTestParser(optionflags=REPORT_NDIFF|ELLIPSIS) with pytest.raises(ValueError) as excinfo: Document.parse(path, parser) assert str(excinfo.value) == ( 'tabs are not supported, first one found at line 2, column 4' ) sybil-1.2.0/tests/test_functional.py0000644000000000000000000001505313461247305017520 0ustar rootroot00000000000000import sys from os.path import dirname, join from unittest.main import main as unittest_main from unittest.runner import TextTestRunner from nose.core import run_exit as NoseMain, TextTestRunner as NoseRunner from pytest import main as pytest_main functional_test_dir = join(dirname(__file__), 'functional') class Finder(object): def __init__(self, text): self.text = text self.index = 0 def then_find(self, substring): assert substring in self.text[self.index:] self.index = self.text.index(substring, self.index) def test_pytest(capsys): class CollectResults: def pytest_sessionfinish(self, session): self.session = session results = CollectResults() return_code = pytest_main(['-vvs', join(functional_test_dir, 'pytest')], plugins=[results]) assert return_code == 1 assert results.session.testsfailed == 4 assert results.session.testscollected == 10 out, err = capsys.readouterr() # check we're trimming tracebacks: index = out.find('sybil/example.py') if index > -1: # pragma: no cover raise AssertionError('\n'+out[index-500:index+500]) out = Finder(out) out.then_find('fail.rst::line:1,column:1') out.then_find('fail.rst sybil setup session_fixture setup\n' 'module_fixture setup\n' 'class_fixture setup\n' 'function_fixture setup\n' 'x is currently: 0\n' 'FAILED function_fixture teardown\n' 'class_fixture teardown') out.then_find('fail.rst::line:6,column:1') out.then_find('fail.rst class_fixture setup\n' 'function_fixture setup\n' '0smcf PASSED function_fixture teardown\n' 'class_fixture teardown') out.then_find('fail.rst::line:8,column:1') out.then_find('fail.rst class_fixture setup\n' 'function_fixture setup\n' '1smcf FAILED function_fixture teardown\n' 'class_fixture teardown') out.then_find('fail.rst::line:10,column:1') out.then_find('fail.rst class_fixture setup\n' 'function_fixture setup\n' '2smcf FAILED function_fixture teardown\n' 'class_fixture teardown') out.then_find('fail.rst::line:12,column:1') out.then_find('fail.rst class_fixture setup\n' 'function_fixture setup\n' '3smcf PASSED function_fixture teardown\n' 'class_fixture teardown') out.then_find('fail.rst::line:14,column:1') out.then_find('fail.rst class_fixture setup\n' 'function_fixture setup\n' 'FAILED function_fixture teardown\n' 'class_fixture teardown\n' 'module_fixture teardown\n' 'sybil teardown 5') out.then_find('pass.rst::line:1,column:1') out.then_find('pass.rst sybil setup module_fixture setup\n' 'class_fixture setup\n' 'function_fixture setup\n' '0smcf PASSED function_fixture teardown\n' 'class_fixture teardown') out.then_find('pass.rst::line:3,column:1') out.then_find('pass.rst class_fixture setup\n' 'function_fixture setup\n' '1smcf PASSED function_fixture teardown\n' 'class_fixture teardown') out.then_find('pass.rst::line:5,column:1') out.then_find('pass.rst class_fixture setup\n' 'function_fixture setup\n' '2smcf PASSED function_fixture teardown\n' 'class_fixture teardown') out.then_find('pass.rst::line:7,column:1') out.then_find('pass.rst class_fixture setup\n' 'function_fixture setup\n' '3smcf PASSED function_fixture teardown\n' 'class_fixture teardown\n' 'module_fixture teardown\n' 'sybil teardown 4\n' 'session_fixture teardown') out.then_find('_ fail.rst line=1 column=1 _') out.then_find( "> raise Exception('the start!')") out.then_find('_ fail.rst line=8 column=1 _') out.then_find('Y count was 3 instead of 2') out.then_find('fail.rst:8: SybilFailure') out.then_find('_ fail.rst line=10 column=1 _') out.then_find('ValueError: X count was 3 instead of 4') out.then_find('_ fail.rst line=14 column=1 _') out.then_find("> raise Exception('boom!')") out.then_find('fail.rst:18: Exception') def common_checks(out): out.then_find('sybil setup') out.then_find('fail.rst,line:6,column:1 ... 0\nok') out.then_find('fail.rst,line:8,column:1 ... 1\nFAIL') out.then_find('fail.rst,line:10,column:1 ... 2\nERROR') out.then_find('fail.rst,line:12,column:1 ... 3\nok') out.then_find('sybil teardown 4\nsybil setup') out.then_find('pass.rst,line:1,column:1 ... 0\nok') out.then_find('pass.rst,line:3,column:1 ... 1\nok') out.then_find('pass.rst,line:5,column:1 ... 2\nok') out.then_find('pass.rst,line:7,column:1 ... 3\nok') out.then_find('sybil teardown 4') out.then_find('ERROR: ') out.then_find('fail.rst,line:10,column:1') out.then_find('ValueError: X count was 3 instead of 4') out.then_find('FAIL:') out.then_find('fail.rst,line:8,column:1') out.then_find('Y count was 3 instead of 2') def test_unittest(capsys): runner = TextTestRunner(verbosity=2, stream=sys.stdout) path = join(functional_test_dir, 'functional_unittest') main = unittest_main( exit=False, module=None, testRunner=runner, argv=['x', 'discover', '-v', '-t', path, '-s', path] ) out, err = capsys.readouterr() assert err == '' out = Finder(out) common_checks(out) out.then_find('Ran 8 tests') assert main.result.testsRun == 8 assert len(main.result.failures) == 1 assert len(main.result.errors) == 1 def test_nose(capsys): class ResultStoringMain(NoseMain): def runTests(self): self.testRunner = NoseRunner(stream=sys.stdout, verbosity=self.config.verbosity, config=self.config) self.result = self.testRunner.run(self.test) main = ResultStoringMain( module=None, argv=['x', '-vs', join(functional_test_dir, 'nose')] ) assert main.result.testsRun == 9 assert len(main.result.failures) == 1 assert len(main.result.errors) == 1 out, err = capsys.readouterr() assert err == '' out = Finder(out) common_checks(out) sybil-1.2.0/tests/test_skip.py0000644000000000000000000000423413461247305016323 0ustar rootroot00000000000000import sys from unittest import SkipTest import pytest from sybil.compat import PY3 from sybil.document import Document from sybil.parsers.codeblock import CodeBlockParser from sybil.parsers.doctest import DocTestParser from sybil.parsers.skip import skip from .helpers import sample_path, document_from_sample, evaluate_region def test_basic(): document = Document.parse(sample_path('skip.txt'), CodeBlockParser(), skip) for example in document: example.evaluate() assert document.namespace['run'] == [2, 5] def test_conditional_edge_cases(): document = Document.parse( sample_path('skip-conditional-edges.txt'), CodeBlockParser(), DocTestParser(), skip ) document.namespace['sys'] = sys document.namespace['run'] = [] skipped = [] for example in document: try: example.evaluate() except SkipTest as e: skipped.append(str(e)) assert document.namespace['run'] == [1, 2] # we should always have one and only one skip from this document. if PY3: assert skipped == ['only true on python 2'] else: assert skipped == ['only true on python 3'] * 3 def test_conditional_full(): document = Document.parse( sample_path('skip-conditional.txt'), DocTestParser(), skip ) document.namespace['result'] = result = [] for example in document: try: example.evaluate() except SkipTest as e: result.append('skip:'+str(e)) assert result == [ 'start', 'skip:(2 > 1)', 'good 1', 'skip:foo', 'skip:foo', 'good 2', ] def test_bad(): document = document_from_sample('skip-conditional-bad.txt') regions = list(skip(document)) namespace = document.namespace with pytest.raises(ValueError) as excinfo: evaluate_region(regions[0], namespace) assert str(excinfo.value) == 'Bad skip action: lolwut' with pytest.raises(ValueError) as excinfo: evaluate_region(regions[1], namespace) assert str(excinfo.value) == 'Cannot have condition on end' with pytest.raises(SyntaxError): evaluate_region(regions[2], namespace) sybil-1.2.0/tests/test_sybil.py0000644000000000000000000002236013461247305016477 0ustar rootroot00000000000000from __future__ import print_function import re from functools import partial from os.path import split import pytest from sybil import Sybil, Region from sybil.document import Document from sybil.example import Example, SybilFailure from .helpers import sample_path @pytest.fixture() def document(): return Document('ABCDEFGH', '/the/path') class TestRegion(object): def test_repr(self): region = Region(0, 1, 'parsed', 'evaluator') assert repr(region) == "" class TestExample(object): def test_repr(self, document): region = Region(0, 1, 'parsed', 'evaluator') example = Example(document, 1, 2, region, {}) assert (repr(example) == "") def test_evaluate_okay(self, document): def evaluator(example): example.namespace['parsed'] = example.parsed region = Region(0, 1, 'the data', evaluator) namespace = {} example = Example(document, 1, 2, region, namespace) result = example.evaluate() assert result is None assert namespace == {'parsed': 'the data'} def test_evaluate_not_okay(self, document): def evaluator(example): return 'foo!' region = Region(0, 1, 'the data', evaluator) example = Example(document, 1, 2, region, {}) with pytest.raises(SybilFailure) as excinfo: example.evaluate() assert str(excinfo.value) == ( 'Example at /the/path, line 1, column 2 did not evaluate as ' 'expected:\nfoo!' ) assert excinfo.value.example is example assert excinfo.value.result == 'foo!' def test_evaluate_raises_exception(self, document): def evaluator(example): raise ValueError('foo!') region = Region(0, 1, 'the data', evaluator) example = Example(document, 1, 2, region, {}) with pytest.raises(ValueError) as excinfo: example.evaluate() assert str(excinfo.value) == 'foo!' class TestDocument(object): def test_add(self, document): region = Region(0, 1, None, None) document.add(region) assert [e.region for e in document] == [region] def test_add_no_overlap(self, document): region1 = Region(0, 1, None, None) region2 = Region(6, 8, None, None) document.add(region1) document.add(region2) assert [e.region for e in document] == [region1, region2] def test_add_out_of_order(self, document): region1 = Region(0, 1, None, None) region2 = Region(6, 8, None, None) document.add(region2) document.add(region1) assert [e.region for e in document] == [region1, region2] def test_add_adjacent(self, document): region1 = Region(0, 1, None, None) region2 = Region(1, 2, None, None) region3 = Region(2, 3, None, None) document.add(region1) document.add(region3) document.add(region2) assert [e.region for e in document] == [region1, region2, region3] def test_add_before_start(self, document): region = Region(-1, 0, None, None) with pytest.raises(ValueError) as excinfo: document.add(region) assert str(excinfo.value) == ( ' ' 'from line 1, column 0 to line 1, column 1 ' 'is before start of document' ) def test_add_after_end(self, document): region = Region(len(document.text), len(document.text)+1, None, None) with pytest.raises(ValueError) as excinfo: document.add(region) assert str(excinfo.value) == ( ' ' 'from line 1, column 9 to line 1, column 10 ' 'goes beyond end of document' ) def test_add_overlaps_with_previous(self, document): region1 = Region(0, 2, None, None) region2 = Region(1, 3, None, None) document.add(region1) with pytest.raises(ValueError) as excinfo: document.add(region2) assert str(excinfo.value) == ( '' ' from line 1, column 1 to line 1, column 3 overlaps ' '' ' from line 1, column 2 to line 1, column 4' ) def test_add_overlaps_with_next(self, document): region1 = Region(0, 1, None, None) region2 = Region(1, 3, None, None) region3 = Region(2, 4, None, None) document.add(region1) document.add(region3) with pytest.raises(ValueError) as excinfo: document.add(region2) assert str(excinfo.value) == ( ' ' 'from line 1, column 2 to line 1, column 4 overlaps ' ' ' 'from line 1, column 3 to line 1, column 5' ) def test_example_path(self, document): document.add(Region(0, 1, None, None)) assert [e.document for e in document] == [document] def test_example_line_and_column(self): text = 'R1XYZ\nR2XYZ\nR3XYZ\nR4XYZ\nR4XYZ\n' i = text.index document = Document(text, '') document.add(Region(0, i('R2')+2, None, None)) document.add(Region(i('R3')-1, i('R3')+2, None, None)) document.add(Region(i('R4')+3, len(text), None, None)) assert ([(e.line, e.column) for e in document] == [(1, 1), (2, 6), (4, 4)]) def check(letter, parsed, namespace): assert namespace == 42 text, expected = parsed assert set(text) == {letter} actual = text.count(letter) if actual != expected: return '{} count was {} instead of {}'.format( letter, actual, expected ) # This would normally be wrong, but handy for testing here: return '{} count was {}, as expected'.format(letter, actual) def parse_for_x(document): for m in re.finditer(r'(X+) (\d+) check', document.text): yield Region(m.start(), m.end(), (m.group(1), int(m.group(2))), partial(check, 'X')) def parse_for_y(document): for m in re.finditer(r'(Y+) (\d+) check', document.text): yield Region(m.start(), m.end(), (m.group(1), int(m.group(2))), partial(check, 'Y')) def parse_first_line(document): line = document.text.split('\n', 1)[0] yield Region(0, len(line), line, None) class TestSybil(object): def _evaluate_examples(self, examples, namespace): return [e.region.evaluator(e.region.parsed, namespace) for e in examples] def _all_examples(self, sybil): for document in sybil.all_documents(): for example in document: yield example def test_parse(self): sybil = Sybil([parse_for_x, parse_for_y], '*') document = sybil.parse(sample_path('sample1.txt')) assert (self._evaluate_examples(document, 42) == ['X count was 4, as expected', 'Y count was 3, as expected']) def test_all_paths(self): sybil = Sybil([parse_first_line], '__init__.py') assert ([e.region.parsed for e in self._all_examples(sybil)] == ['# believe it or not,']) def test_all_paths_with_base_directory(self): sybil = Sybil([parse_for_x, parse_for_y], path='./samples', pattern='*.txt') assert (self._evaluate_examples(self._all_examples(sybil), 42) == ['X count was 4, as expected', 'Y count was 3, as expected', 'X count was 3 instead of 4', 'Y count was 3, as expected']) class TestFiltering(object): def check(self, sybil, expected): assert expected == [split(d.path)[-1] for d in sybil.all_documents()] def test_excludes(self, tmp_path): (tmp_path / 'foo.txt').write_text(u'') (tmp_path / 'bar.txt').write_text(u'') sybil = Sybil([], path=str(tmp_path), pattern='*.txt', excludes=['bar.txt']) self.check(sybil, expected=['foo.txt']) def test_filenames(self, tmp_path): (tmp_path / 'foo.txt').write_text(u'') (tmp_path / 'bar.txt').write_text(u'') sybil = Sybil([], path=str(tmp_path), filenames=['bar.txt']) self.check(sybil, expected=['bar.txt']) def check_into_namespace(example): parsed, namespace = example.region.parsed, example.namespace if 'parsed' not in namespace: namespace['parsed'] = [] namespace['parsed'].append(parsed) print(namespace['parsed']) def parse(document): for m in re.finditer(r'([XY]+) (\d+) check', document.text): yield Region(m.start(), m.end(), m.start(), check_into_namespace) def test_namespace(capsys): sybil = Sybil([parse], path='./samples', pattern='*.txt') for document in sybil.all_documents(): for example in document: print(split(example.document.path)[-1], example.line) example.evaluate() out, _ = capsys.readouterr() assert out.split('\n') == [ 'sample1.txt 1', '[0]', 'sample1.txt 3', '[0, 14]', 'sample2.txt 1', '[0]', 'sample2.txt 3', '[0, 13]', '' ]