sybil-2.0.1/0000755000000000000000000000000013760752632012605 5ustar rootroot00000000000000sybil-2.0.1/.carthorse.yml0000644000000000000000000000042213760752613015375 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-2.0.1/.circleci/0000755000000000000000000000000013760752632014440 5ustar rootroot00000000000000sybil-2.0.1/.circleci/config.yml0000644000000000000000000000132513760752613016430 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-2.0.1/.gitignore0000644000000000000000000000011313760752613014567 0ustar rootroot00000000000000/bin /*.egg-info /include /lib .coverage* _build/ .*cache/ pytestdebug.log sybil-2.0.1/.readthedocs.yml0000644000000000000000000000017013760752613015670 0ustar rootroot00000000000000version: 2 python: version: 3.7 install: - method: pip path: . extra_requirements: - build sybil-2.0.1/PKG-INFO0000644000000000000000000000245613760752632013711 0ustar rootroot00000000000000Metadata-Version: 2.1 Name: sybil Version: 2.0.1 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 ===== |CircleCI|_ |Docs|_ .. |CircleCI| image:: https://circleci.com/gh/cjw296/sybil/tree/master.svg?style=shield .. _CircleCI: https://circleci.com/gh/cjw296/sybil/tree/master .. |Docs| image:: https://readthedocs.org/projects/sybil/badge/?version=latest .. _Docs: http://sybil.readthedocs.org/en/latest/ 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 main Python test runners. 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: test Provides-Extra: build sybil-2.0.1/README.rst0000644000000000000000000000105413760752613014273 0ustar rootroot00000000000000Sybil ===== |CircleCI|_ |Docs|_ .. |CircleCI| image:: https://circleci.com/gh/cjw296/sybil/tree/master.svg?style=shield .. _CircleCI: https://circleci.com/gh/cjw296/sybil/tree/master .. |Docs| image:: https://readthedocs.org/projects/sybil/badge/?version=latest .. _Docs: http://sybil.readthedocs.org/en/latest/ 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 main Python test runners. sybil-2.0.1/docs/0000755000000000000000000000000013760752632013535 5ustar rootroot00000000000000sybil-2.0.1/docs/Makefile0000644000000000000000000000501713760752613015177 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-2.0.1/docs/api.rst0000644000000000000000000000055713760752613015046 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-2.0.1/docs/changes.rst0000644000000000000000000000633413760752613015704 0ustar rootroot00000000000000Changes ======= 2.0.1 (29 Nov 2020) ------------------- - Make :class:`~sybil.parsers.doctest.DocTestParser` more permissive with respect to tabs in documents. Tabs that aren't in the doctest block not longer cause parsing of the document to fail. 2.0.0 (17 Nov 2020) ------------------- - Drop support for nose. - Handle encoded data returned by doctest execution on Python 2. 1.4.0 (5 Aug 2020) ------------------ - Support nested directories of source files rather than just one directory. - Support multiple patterns of files to include. 1.3.1 (29 Jul 2020) ------------------- - Support pytest 6. 1.3.0 (28 Mar 2020) ------------------- - Treat all documentation source files as being ``utf-8`` encoded. This can be overridden by passing an encoding when instantiating a :class:`~sybil.Sybil`. 1.2.2 (20 Feb 2020) ------------------- - Improvements to :attr:`~sybil.parsers.doctest.FIX_BYTE_UNICODE_REPR` for multiple strings on a single line. - Better handling of files with Windows line endings on Linux under Python 2. 1.2.1 (21 Jan 2020) ------------------- - Fixes for pytest 3.1.0. 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-2.0.1/docs/conf.py0000644000000000000000000000174413760752613015041 0ustar rootroot00000000000000# -*- coding: utf-8 -*- import os, pkg_resources, datetime, time 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' build_date = datetime.datetime.utcfromtimestamp(int(os.environ.get('SOURCE_DATE_EPOCH', time.time()))) copyright = '2017 - %s Chris Withers' % build_date.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-2.0.1/docs/conftest.py0000644000000000000000000000077513760752613015744 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-2.0.1/docs/development.rst0000644000000000000000000000277613760752613016624 0ustar rootroot00000000000000Development =========== .. highlight:: bash 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:: $ source bin/activate $ 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``, and push to https://github.com/cjw296/sybil and Carthorse should take care of the rest. sybil-2.0.1/docs/example/0000755000000000000000000000000013760752632015170 5ustar rootroot00000000000000sybil-2.0.1/docs/example/docs/0000755000000000000000000000000013760752632016120 5ustar rootroot00000000000000sybil-2.0.1/docs/example/docs/conftest.py0000644000000000000000000000125313760752613020317 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-2.0.1/docs/example/docs/example.rst0000644000000000000000000000057113760752613020307 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-2.0.1/docs/example/example_unittest/0000755000000000000000000000000013760752632020562 5ustar rootroot00000000000000sybil-2.0.1/docs/example/example_unittest/__init__.py0000644000000000000000000000000013760752613022660 0ustar rootroot00000000000000sybil-2.0.1/docs/example/example_unittest/test_example_docs.py0000644000000000000000000000131713760752613024637 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-2.0.1/docs/example/pytest.ini0000644000000000000000000000000013760752613017206 0ustar rootroot00000000000000sybil-2.0.1/docs/example-skip.rst0000644000000000000000000000046513760752613016672 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-2.0.1/docs/example.rst0000644000000000000000000000115413760752613015722 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-2.0.1/docs/index.rst0000644000000000000000000000132613760752613015377 0ustar rootroot00000000000000.. include:: ../README.rst 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 Fawlty 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-2.0.1/docs/license.rst0000644000000000000000000000211513760752613015707 0ustar rootroot00000000000000======= License ======= Copyright (c) 2017-2020 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-2.0.1/docs/parsers.rst0000644000000000000000000002325113760752613015750 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() [u'root.txt', u'subdir/', u'subdir/file.txt', u'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-2.0.1/docs/use.rst0000644000000000000000000001025413760752613015064 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-11, 13- __ 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. sybil-2.0.1/setup.cfg0000644000000000000000000000035713760752632014433 0ustar rootroot00000000000000[wheel] universal = 1 [tool:pytest] addopts = --verbose --strict norecursedirs = functional .git docs/example filterwarnings = ignore::DeprecationWarning [coverage:run] omit = /the/path /tmp/* [egg_info] tag_build = tag_date = 0 sybil-2.0.1/setup.py0000644000000000000000000000225013760752613014315 0ustar rootroot00000000000000# See docs/license.rst for license details. # Copyright (c) 2017-2020 Chris Withers import os from setuptools import setup, find_packages base_dir = os.path.dirname(__file__) setup( name='sybil', version='2.0.1', author='Chris Withers', author_email='chris@withers.org', license='MIT', description="Automated testing for the examples in your documentation.", long_description=open('README.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=[ 'pytest>=3.5.0', 'pytest-cov', ], build=['sphinx', 'setuptools-git', 'twine', 'wheel'] ), ) sybil-2.0.1/sybil/0000755000000000000000000000000013760752632013727 5ustar rootroot00000000000000sybil-2.0.1/sybil/__init__.py0000644000000000000000000000006413760752613016037 0ustar rootroot00000000000000from .sybil import Sybil from .region import Region sybil-2.0.1/sybil/compat.py0000644000000000000000000000017013760752613015561 0ustar rootroot00000000000000import sys PY3 = sys.version_info[0] == 3 if PY3: from io import StringIO else: from StringIO import StringIO sybil-2.0.1/sybil/document.py0000644000000000000000000001102313760752613016113 0ustar rootroot00000000000000import re from bisect import bisect from io import open 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, **kw): """ Read the text from the supplied path and parse it into a document using the supplied parsers. """ # python 2 compat: encoding = kw.pop('encoding', 'utf-8') with open(path, encoding=encoding) 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-2.0.1/sybil/example.py0000644000000000000000000000434713760752613015743 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-2.0.1/sybil/integration/0000755000000000000000000000000013760752632016252 5ustar rootroot00000000000000sybil-2.0.1/sybil/integration/__init__.py0000644000000000000000000000000013760752613020350 0ustar rootroot00000000000000sybil-2.0.1/sybil/integration/pytest.py0000644000000000000000000001014513760752613020154 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 py.path 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 py.path.local(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): self._request._fillfixtures() 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, fspath, parent, sybil): super(SybilFile, self).__init__(fspath, parent) self.sybil = sybil def collect(self): self.document = self.sybil.parse(self.fspath.strpath) for example in self.document: try: from_parent = SybilItem.from_parent except AttributeError: yield SybilItem(self, self.sybil, example) else: yield from_parent(self, sybil=self.sybil, example=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, class_=SybilFile): def pytest_collect_file(parent, path): if sybil.should_test_path(path): try: from_parent = class_.from_parent except AttributeError: return class_(path, parent, sybil) else: return from_parent(parent, fspath=path, sybil=sybil) return pytest_collect_file sybil-2.0.1/sybil/integration/unittest.py0000644000000000000000000000225013760752613020501 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-2.0.1/sybil/parsers/0000755000000000000000000000000013760752632015406 5ustar rootroot00000000000000sybil-2.0.1/sybil/parsers/__init__.py0000644000000000000000000000000013760752613017504 0ustar rootroot00000000000000sybil-2.0.1/sybil/parsers/capture.py0000644000000000000000000000544513760752613017432 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-2.0.1/sybil/parsers/codeblock.py0000644000000000000000000000412713760752613017710 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-2.0.1/sybil/parsers/doctest.py0000644000000000000000000001143313760752613017426 0ustar rootroot00000000000000from __future__ import absolute_import import re from doctest import ( DocTest as BaseDocTest, DocTestParser as BaseDocTestParser, DocTestRunner as BaseDocTestRunner, Example as DocTestExample, OutputChecker as BaseOutputChecker, _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 OutputChecker(BaseOutputChecker): def __init__(self, encoding): self.encoding = encoding def _decode(self, got): decode = getattr(got, 'decode', None) if decode is None: return got return decode(self.encoding) def check_output(self, want, got, optionflags): return BaseOutputChecker.check_output( self, want, self._decode(got), optionflags ) def output_difference(self, example, got, optionflags): return BaseOutputChecker.output_difference( self, example, self._decode(got), optionflags ) class DocTestRunner(BaseDocTestRunner): def __init__(self, optionflags, encoding): optionflags |= _unittest_reportflags BaseDocTestRunner.__init__( self, checker=OutputChecker(encoding), 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. :param encoding: If on Python 2, this encoding will be used to decode the string resulting from execution of the examples. """ def __init__(self, optionflags=0, encoding='utf-8'): self.runner = DocTestRunner(optionflags, encoding) def __call__(self, document): # a cut down version of doctest.DocTestParser.parse: text = document.text # 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-2.0.1/sybil/parsers/skip.py0000644000000000000000000000415413760752613016731 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-2.0.1/sybil/region.py0000644000000000000000000000162713760752613015571 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-2.0.1/sybil/sybil.py0000644000000000000000000001070613760752613015426 0ustar rootroot00000000000000import os import sys from fnmatch import fnmatch from os.path import join, dirname, abspath, split from .document import Document class PathFilter(object): def __init__(self, patterns, filenames, excludes): self.patterns = patterns self.filenames = filenames self.excludes = excludes def __call__(self, path): path = str(path) return ( (any(fnmatch(path, e) for e in self.patterns) or (split(path)[-1] in self.filenames)) and not any(fnmatch(path, e) for e in self.excludes) ) def listdir(root): root_to_ignore = len(root) + 1 for directory, _, filenames in os.walk(root): for filename in filenames: yield os.path.join(directory, filename)[root_to_ignore:] 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 patterns: An optional sequence of :func:`patterns ` used to match documentation source files that will be parsed for examples. :param filenames: An optional :class:`set` of source file names that, if found anywhere within the root ``path`` or its sub-directories, 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. :param encoding: An optional string specifying the encoding to be used when decoding documentation source files. """ def __init__(self, parsers, pattern='', path='.', setup=None, teardown=None, fixtures=(), filenames=(), excludes=(), encoding='utf-8', patterns=()): 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) patterns = list(patterns) if pattern: patterns.append(pattern) self.should_test_path = PathFilter(patterns, filenames, excludes) self.setup = setup self.teardown = teardown self.fixtures = fixtures self.encoding = encoding def parse(self, path): return Document.parse(path, *self.parsers, encoding=self.encoding) def all_documents(self): for path in sorted(listdir(self.path)): if self.should_test_path(path): yield self.parse(join(self.path, path)) def pytest(self, class_=None): """ The helper method for when you use :ref:`pytest_integration`. """ from .integration.pytest import pytest_integration, SybilFile if class_ is None: class_ = SybilFile return pytest_integration(self, class_) def unittest(self): """ The helper method for when you use :ref:`unitttest_integration`. """ from .integration.unittest import unittest_integration return unittest_integration(self) sybil-2.0.1/sybil.egg-info/0000755000000000000000000000000013760752632015421 5ustar rootroot00000000000000sybil-2.0.1/sybil.egg-info/PKG-INFO0000644000000000000000000000245613760752632016525 0ustar rootroot00000000000000Metadata-Version: 2.1 Name: sybil Version: 2.0.1 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 ===== |CircleCI|_ |Docs|_ .. |CircleCI| image:: https://circleci.com/gh/cjw296/sybil/tree/master.svg?style=shield .. _CircleCI: https://circleci.com/gh/cjw296/sybil/tree/master .. |Docs| image:: https://readthedocs.org/projects/sybil/badge/?version=latest .. _Docs: http://sybil.readthedocs.org/en/latest/ 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 main Python test runners. 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: test Provides-Extra: build sybil-2.0.1/sybil.egg-info/SOURCES.txt0000644000000000000000000000366213760752632017314 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/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_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/not-zip-safe sybil.egg-info/requires.txt sybil.egg-info/top_level.txt sybil/integration/__init__.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_pytest.py tests/test_skip.py tests/test_sybil.py tests/functional/functional_unittest/__init__.py tests/functional/functional_unittest/test_unittest.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_irrelevant_tabs.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-2.0.1/sybil.egg-info/dependency_links.txt0000644000000000000000000000000113760752632021467 0ustar rootroot00000000000000 sybil-2.0.1/sybil.egg-info/not-zip-safe0000644000000000000000000000000113760752632017647 0ustar rootroot00000000000000 sybil-2.0.1/sybil.egg-info/requires.txt0000644000000000000000000000011413760752632020015 0ustar rootroot00000000000000 [build] sphinx setuptools-git twine wheel [test] pytest>=3.5.0 pytest-cov sybil-2.0.1/sybil.egg-info/top_level.txt0000644000000000000000000000000613760752632020147 0ustar rootroot00000000000000sybil sybil-2.0.1/tests/0000755000000000000000000000000013760752632013747 5ustar rootroot00000000000000sybil-2.0.1/tests/__init__.py0000644000000000000000000000005413760752613016056 0ustar rootroot00000000000000# believe it or not, # this line is a test! sybil-2.0.1/tests/functional/0000755000000000000000000000000013760752632016111 5ustar rootroot00000000000000sybil-2.0.1/tests/functional/functional_unittest/0000755000000000000000000000000013760752632022212 5ustar rootroot00000000000000sybil-2.0.1/tests/functional/functional_unittest/__init__.py0000644000000000000000000000000013760752613024310 0ustar rootroot00000000000000sybil-2.0.1/tests/functional/functional_unittest/test_unittest.py0000644000000000000000000000205613760752613025504 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-2.0.1/tests/functional/pytest/0000755000000000000000000000000013760752632017441 5ustar rootroot00000000000000sybil-2.0.1/tests/functional/pytest/conftest.py0000644000000000000000000000400613760752613021637 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-2.0.1/tests/functional/pytest/fail.rst0000644000000000000000000000033113760752613021102 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-2.0.1/tests/functional/pytest/pass.rst0000644000000000000000000000006413760752613021140 0ustar rootroot00000000000000XXXX 4 check YYY 3 check XXX 3 check YYY 3 check sybil-2.0.1/tests/functional/pytest/pytest.ini0000644000000000000000000000005013760752613021464 0ustar rootroot00000000000000[pytest] console_output_style = classic sybil-2.0.1/tests/helpers.py0000644000000000000000000000107613760752613015766 0ustar rootroot00000000000000from io import open from 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, encoding='ascii') 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-2.0.1/tests/samples/0000755000000000000000000000000013760752632015413 5ustar rootroot00000000000000sybil-2.0.1/tests/samples/capture.txt0000644000000000000000000000126613760752613017623 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-2.0.1/tests/samples/capture_bad_indent1.txt0000644000000000000000000000013313760752613022043 0ustar rootroot00000000000000The directive here is beyond the indentation of the block:: Block .. -> foo sybil-2.0.1/tests/samples/capture_bad_indent2.txt0000644000000000000000000000013013760752613022041 0ustar rootroot00000000000000The directive here is at the same indentation as the block:: Block .. -> foo sybil-2.0.1/tests/samples/codeblock.txt0000644000000000000000000000154013760752613020100 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-2.0.1/tests/samples/codeblock_future_imports.txt0000644000000000000000000000033713760752613023252 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-2.0.1/tests/samples/doctest.txt0000644000000000000000000000044413760752613017622 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-2.0.1/tests/samples/doctest_fail.txt0000644000000000000000000000015413760752613020613 0ustar rootroot00000000000000>>> print("where's my output?") Not my output Here's an exception happening: >>> raise Exception('boom!') sybil-2.0.1/tests/samples/doctest_irrelevant_tabs.txt0000644000000000000000000000004613760752613023064 0ustar rootroot00000000000000These tabs don't matter. >>> 1 + 1 2 sybil-2.0.1/tests/samples/doctest_literals.txt0000644000000000000000000000030513760752613021515 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-2.0.1/tests/samples/doctest_min_indent.txt0000644000000000000000000000005413760752613022023 0ustar rootroot00000000000000 Just for the coverage: >>> True True sybil-2.0.1/tests/samples/doctest_tabs.txt0000644000000000000000000000017513760752613020634 0ustar rootroot00000000000000>>> handler = SummarisingLogger('from@example.com',('to@example.com',), ... username='auser',password='theirpassword') sybil-2.0.1/tests/samples/sample1.txt0000644000000000000000000000003213760752613017510 0ustar rootroot00000000000000XXXX 4 check YYY 3 check sybil-2.0.1/tests/samples/sample2.txt0000644000000000000000000000003113760752613017510 0ustar rootroot00000000000000XXX 4 check YYY 3 check sybil-2.0.1/tests/samples/skip-conditional-bad.txt0000644000000000000000000000024313760752613022145 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-2.0.1/tests/samples/skip-conditional-edges.txt0000644000000000000000000000100213760752613022500 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-2.0.1/tests/samples/skip-conditional.txt0000644000000000000000000000043013760752613021417 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-2.0.1/tests/samples/skip.txt0000644000000000000000000000073513760752613017126 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-2.0.1/tests/test_capture.py0000644000000000000000000000362413760752613017027 0ustar rootroot00000000000000import pytest from sybil.parsers.capture import parse_captures from sybil.compat import PY3 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)) if PY3: block = "' .. -> foo'" else: block = "u' .. -> foo'" assert str(excinfo.value) == ( "couldn't find the start of the block to match "+block+" " "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)) if PY3: block = "' .. -> foo'" else: block = "u' .. -> foo'" assert str(excinfo.value) == ( "couldn't find the start of the block to match "+block+" " "on line 5 of "+sample_path('capture_bad_indent2.txt') ) sybil-2.0.1/tests/test_codeblock.py0000644000000000000000000000510413760752613017304 0ustar rootroot00000000000000import pytest from sybil.compat import StringIO from sybil.document import Document 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 def test_windows_line_endings(tmp_path): p = tmp_path / "example.txt" p.write_bytes( b'This is my example:\r\n\r\n' b'.. code-block:: python\r\n\r\n' b' from math import cos\r\n' b' x = 123\r\n\r\n' b'That was my example.\r\n' ) document = Document.parse(str(p), CodeBlockParser()) example, = document example.evaluate() assert document.namespace['x'] == 123 sybil-2.0.1/tests/test_doc_example.py0000644000000000000000000000204013760752613017633 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 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 sybil-2.0.1/tests/test_doctest.py0000644000000000000000000000546413760752613017035 0ustar rootroot00000000000000# coding=utf-8 from 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() with pytest.raises(ValueError): Document.parse(path, parser) def test_irrelevant_tabs(): document = document_from_sample('doctest_irrelevant_tabs.txt') regions = list(DocTestParser()(document)) assert len(regions) == 1 namespace = document.namespace assert evaluate_region(regions[0], namespace) == '' def test_unicode(): document = Document(u'>>> print("ā”œā”€")\nā”œā”€', path='dummy.rst') example, = DocTestParser()(document) namespace = document.namespace assert evaluate_region(example, namespace) == '' sybil-2.0.1/tests/test_functional.py0000644000000000000000000001374313760752613017531 0ustar rootroot00000000000000import sys from os.path import dirname, join from unittest.main import main as unittest_main from unittest.runner import TextTestRunner 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::line:1,column:1 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::line:6,column:1 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::line:8,column:1 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::line:10,column:1 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::line:12,column:1 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::line:14,column:1 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::line:1,column:1 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::line:3,column:1 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::line:5,column:1 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::line:7,column:1 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 sybil-2.0.1/tests/test_pytest.py0000644000000000000000000000242113760752613016706 0ustar rootroot00000000000000from py.path import local from sybil import Sybil class MockFile(object): def __init__(self, path, parent, sybil): self.path = path class TestCollectFile(object): def test_filenames(self, tmp_path): pytest_collect_file = Sybil(parsers=[], filenames=['test.rst']).pytest(MockFile) path = (tmp_path / 'test.rst') path.write_text(u'') local_path = local(path) assert pytest_collect_file(None, local_path).path == local_path def test_fnmatch_pattern(self, tmp_path): pytest_collect_file = Sybil(parsers=[], pattern='**/*.rst').pytest(MockFile) path = (tmp_path / 'test.rst') path.write_text(u'') local_path = local(path) assert pytest_collect_file(None, local_path).path == local_path def test_fnmatch_patterns(self, tmp_path): pytest_collect_file = Sybil(parsers=[], patterns=['*.rst', '*.py']).pytest(MockFile) rst_path = (tmp_path / 'test.rst') rst_path.write_text(u'') py_path = (tmp_path / 'test.py') py_path.write_text(u'') local_path = local(rst_path) assert pytest_collect_file(None, local_path).path == local_path local_path = local(py_path) assert pytest_collect_file(None, local_path).path == local_path sybil-2.0.1/tests/test_skip.py0000644000000000000000000000423413760752613016330 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-2.0.1/tests/test_sybil.py0000644000000000000000000002473013760752613016507 0ustar rootroot00000000000000from __future__ import print_function import os 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']) def test_explicit_encoding(self, tmp_path): (tmp_path / 'encoded.txt').write_text( u'X 1 check\n\xa3', encoding='charmap' ) sybil = Sybil([parse_for_x], path=str(tmp_path), pattern='*.txt', encoding='charmap') assert (self._evaluate_examples(self._all_examples(sybil), 42) == ['X count was 1, as expected']) class TestFiltering(object): def check(self, tmp_path, sybil, expected): assert expected == [d.path[len(str(tmp_path))+1:].split(os.sep) 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(tmp_path, sybil, expected=[['foo.txt']]) def test_filenames(self, tmp_path): (tmp_path / 'foo.txt').write_text(u'') (tmp_path / 'bar.txt').write_text(u'') (tmp_path / 'baz').mkdir() (tmp_path / 'baz' / 'bar.txt').write_text(u'') sybil = Sybil([], path=str(tmp_path), filenames=['bar.txt']) self.check(tmp_path, sybil, expected=[['bar.txt'], ['baz', 'bar.txt']]) def test_glob_patterns(self, tmp_path): (tmp_path / 'middle').mkdir() interesting = (tmp_path / 'middle' / 'interesting') interesting.mkdir() boring = (tmp_path / 'middle' / 'boring') boring.mkdir() (interesting / 'foo.txt').write_text(u'') (boring / 'bad1.txt').write_text(u'') (tmp_path / 'bad2.txt').write_text(u'') sybil = Sybil([], path=str(tmp_path), pattern='**middle/*.txt', excludes=['**/boring/*.txt']) self.check(tmp_path, sybil, expected=[['middle', 'interesting', 'foo.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]', '' ]