pytest-doctestplus-0.5.0/0000755000077000000240000000000013563614055015341 5ustar tomstaff00000000000000pytest-doctestplus-0.5.0/.gitignore0000644000077000000240000000112513563613534017331 0ustar tomstaff00000000000000# Compiled files *.py[cod] *.a *.o *.so *.pyd __pycache__ # Ignore .c files by default to avoid including generated code. If you want to # add a non-generated .c extension, use `git add -f filename.c`. *.c # Other generated files MANIFEST # Sphinx _build _generated docs/api docs/generated # Packages/installer info *.egg *.egg-info dist build eggs .eggs parts bin var sdist develop-eggs .installed.cfg distribute-*.tar.gz # Other .cache .tox .*.swp .*.swo *~ .project .pydevproject .settings .coverage cover htmlcov .pytest_cache # Env .venv venv .env # Mac OSX .DS_Store # PyCharm .idea pytest-doctestplus-0.5.0/.travis.yml0000644000077000000240000000327313563613534017460 0ustar tomstaff00000000000000# We set the language to c because python isn't supported on the MacOS X nodes # on Travis. However, the language ends up being irrelevant anyway, since we # install Python ourselves using conda. language: c os: - linux - windows stage: All tests # Use Travis' container-based architecture sudo: false env: global: # The following versions are the 'default' for tests, unless # overidden underneath. They are defined here in order to save having # to repeat them for all configurations. - PYTHON_VERSION=3.6 - PYTEST_VERSION=4 - PYTEST_COMMAND='pytest' - CONDA_DEPENDENCIES='six' matrix: - PYTHON_VERSION=2.7 PYTEST_COMMAND='py.test' - PYTHON_VERSION=3.5 NUMPY_VERSION=1.15 - PYTHON_VERSION=3.6 - PYTHON_VERSION=3.7 PYTEST_VERSION=3.8 - PYTHON_VERSION=3.7 PYTEST_VERSION=3.9 stages: # only run 2 jobs initially - name: Initial tests - name: All tests matrix: include: - os: linux env: PYTHON_VERSION=3.7 NUMPY_VERSION=stable stage: Initial tests - os: linux env: PYTHON_VERSION=2.7 stage: Initial tests # Try a run on OSX with latest versions of python and pytest - os: osx env: PYTHON_VERSION=3.7 # Try a run against latest pytest - env: PYTHON_VERSION=3.7 PYTEST_VERSION=5 install: - git clone git://github.com/astropy/ci-helpers.git - source ci-helpers/travis/setup_conda.sh - python ./setup.py install script: - $PYTEST_COMMAND --doctest-plus - $PYTEST_COMMAND --doctest-plus --doctest-rst - $PYTEST_COMMAND --doctest-plus --doctest-rst --text-file-format=tex pytest-doctestplus-0.5.0/CHANGES.rst0000644000077000000240000000416413563613553017152 0ustar tomstaff000000000000000.5.0 (2019-11-15) ================== - No longer require Numpy. [#69] - Fixed a bug that caused ``__doctest_requires__`` to not work correctly with submodules. [#73] - Fixed a limitation that meant that ``ELLIPSIS`` and ``FLOAT_CMP`` could not be used at the same time. [#75] - Fixed a bug that caused ``.. doctest-requires::`` to not work correctly. [#78] - Fixed a FutureWarning related to split() with regular expressions. [#78] - Make it possible to specify versions in ``.. doctest-requires::``. [#78] - Allow to use doctest-glob option instead of doctest-rst and text-file-format [#80] - Make comment character configurable via ini variable text_file_comment_chars [#80] - Respect ``ignore`` and ``ignore-glob`` options from pytest. [#82] - Add ``--doctest-only`` option. [#83] - Added an ``IGNORE_WARNINGS`` option for ``# doctest:`` [#84] 0.4.0 (2019-09-17) ================== - Avoid ``SyntaxWarning`` regarding invalid escape sequence in Python 3.9. [#62] - Compatibility with ``pytest`` 5.1 to avoid ``AttributeError`` caused by ``FixtureRequest``. [#63] 0.3.0 (2019-03-06) ================== - Honor the ``collect_ignore`` option used in ``conftest.py``. [#36] - Make use of ``doctest_optionflags`` settings. [#39] - Make it possible to set ``FLOAT_CMP`` globally in ``setup.cfg``. [#40] - Drop support for ``pytest`` versions earlier than 3.0. [#46] - Extend ``doctest-skip``, ``doctest-skip-all``, and ``doctest-requires`` directives to work in TeX files. [#43, #47] 0.2.0 (2018-11-14) ================== - Add ``doctest-plus-atol`` and ``doctest-plus-rtol`` options for setting the numerical tolerance. [#21] - Update behavior of ``--doctest-modules`` option when plugin is installed. [#26] 0.1.3 (2018-04-20) ================== - Fix packaging error: do not include tests as part of package distribution. [#19] 0.1.2 (2017-12-07) ================== - Update README. Use README for long description on PyPi. [#12] 0.1.1 (2017-10-18) ================== - Port fix from astropy core that addresses changes to numpy formatting of float scalars. [#8] 0.1 (2017-10-10) ================ - Alpha release. pytest-doctestplus-0.5.0/LICENSE.rst0000644000077000000240000000273013202042231017134 0ustar tomstaff00000000000000Copyright (c) 2011-2017, Astropy Developers All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the Astropy Team nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. pytest-doctestplus-0.5.0/MANIFEST.in0000644000077000000240000000020313563613534017073 0ustar tomstaff00000000000000include LICENSE.rst include README.rst include CHANGES.rst include setup.cfg recursive-include tests * global-exclude *.pyc *.o pytest-doctestplus-0.5.0/PKG-INFO0000644000077000000240000003141213563614055016437 0ustar tomstaff00000000000000Metadata-Version: 1.2 Name: pytest-doctestplus Version: 0.5.0 Summary: Pytest plugin with advanced doctest features. Home-page: https://astropy.org Author: The Astropy Developers Author-email: astropy.team@gmail.com License: BSD Description: ================== pytest-doctestplus ================== This package contains a plugin for the `pytest`_ framework that provides advanced doctest support and enables the testing of `reStructuredText`_ (".rst") files. It was originally part of the `astropy`_ core package, but has been moved to a separate package in order to be of more general use. .. _pytest: https://pytest.org/en/latest/ .. _astropy: https://astropy.org/en/latest/ .. _reStructuredText: https://en.wikipedia.org/wiki/ReStructuredText Motivation ---------- This plugin provides advanced features for testing example Python code that is included in Python docstrings and in standalone documentation files. Good documentation for developers contains example code. This is true of both standalone documentation and of documentation that is integrated with the code itself. Python provides a mechanism for testing code snippets that are provided in Python docstrings. The unit test framework pytest provides a mechanism for running doctests against both docstrings in source code and in standalone documentation files. This plugin augments the functionality provided by Python and pytest by providing the following features: * approximate floating point comparison for doctests that produce floating point results (see `Floating Point Comparison`_) * skipping particular classes, methods, and functions when running doctests (see `Skipping Tests`_) * handling doctests that use remote data in conjunction with the `pytest-remotedata`_ plugin (see `Remote Data`_) * optional inclusion of ``*.rst`` files for doctests (see `Setup and Configuration`_) .. _pytest-remotedata: https://github.com/astropy/pytest-remotedata Installation ------------ The ``pytest-doctestplus`` plugin can be installed using ``pip``:: $ pip install pytest-doctestplus It is also possible to install the latest development version from the source repository:: $ git clone https://github.com/astropy/pytest-doctestplus $ cd pytest-doctestplus $ python ./setup.py install In either case, the plugin will automatically be registered for use with ``pytest``. Usage ----- .. _setup: Setup and Configuration ~~~~~~~~~~~~~~~~~~~~~~~ This plugin provides two command line options: ``--doctest-plus`` for enabling the advanced features mentioned above, and ``--doctest-rst`` for including ``*.rst`` files in doctest collection. This plugin can also be enabled by default by adding ``doctest_plus = enabled`` to the ``[tool:pytest]`` section of the package's ``setup.cfg`` file. The plugin is applied to all directories and files that ``pytest`` collects. This means that configuring ``testpaths`` and ``norecursedirs`` in ``setup.cfg`` also affects the files that will be discovered by ``pytest-doctestplus``. In addition, this plugin provides a ``doctest_norecursedirs`` configuration variable that indicates directories that should be ignored by ``pytest-doctestplus`` but do not need to be ignored by other ``pytest`` features. Using ``pytest``'s built-in ``--doctest-modules`` option will override the behavior of this plugin, even if ``doctest_plus = enabled`` in ``setup.cfg``, and will cause the default doctest plugin to be used. However, if for some reason both ``--doctest-modules`` and ``--doctest-plus`` are given, the ``pytest-doctestplus`` plugin will be used, regardless of the contents of ``setup.cfg``. This plugin respects the doctest options that are used by the built-in doctest plugin and are set in ``doctest_optionflags`` in ``setup.cfg``. By default, ``ELLIPSIS`` and ``NORMALIZE_WHITESPACE`` are used. For a description of all doctest settings, see the `doctest documentation `_. Doctest Directives ~~~~~~~~~~~~~~~~~~ The ``pytest-doctestplus`` plugin defines `doctest directives`_ that are used to control the behavior of particular features. For general information on directives and how they are used, consult the `documentation`_. The specifics of the directives that this plugin defines are described in the sections below. .. _doctest directives: https://docs.python.org/3/library/doctest.html#directives .. _documentation: https://docs.python.org/3/library/doctest.html#directives Floating Point Comparison ~~~~~~~~~~~~~~~~~~~~~~~~~ Some doctests may produce output that contains string representations of floating point values. Floating point representations are often not exact and contain roundoffs in their least significant digits. Depending on the platform the tests are being run on (different Python versions, different OS, etc.) the exact number of digits shown can differ. Because doctests work by comparing strings this can cause such tests to fail. To address this issue, the ``pytest-doctestplus`` plugin provides support for a ``FLOAT_CMP`` flag that can be used with doctests. For example: .. code-block:: python >>> 1.0 / 3.0 # doctest: +FLOAT_CMP 0.333333333333333311 When this flag is used, the expected and actual outputs are both parsed to find any floating point values in the strings. Those are then converted to actual Python `float` objects and compared numerically. This means that small differences in representation of roundoff digits will be ignored by the doctest. The values are otherwise compared exactly, so more significant (albeit possibly small) differences will still be caught by these tests. This flag can be enabled globally by adding it to ``setup.cfg`` as in .. code-block:: ini doctest_optionflags = NORMALIZE_WHITESPACE ELLIPSIS FLOAT_CMP Ignoring warnings ~~~~~~~~~~~~~~~~~ If code in a doctest emits a warning and you want to make sure that warning is silenced, you can make use of the ``IGNORE_WARNINGS`` flag. For example: .. code-block:: python >>> import numpy as np >>> np.mean([]) # doctest: +IGNORE_WARNINGS np.nan Skipping Tests ~~~~~~~~~~~~~~ Doctest provides the ``+SKIP`` directive for skipping statements that should not be executed when testing documentation. .. code-block:: python >>> open('file.txt') # doctest: +SKIP In Sphinx ``.rst`` documentation, whole code example blocks can be skipped with the directive .. code-block:: rst .. doctest-skip:: >>> import asdf >>> asdf.open('file.asdf') However, it is often useful to be able to skip docstrings associated with particular functions, methods, classes, or even entire files. Skip Unconditionally ^^^^^^^^^^^^^^^^^^^^ The ``pytest-doctestplus`` plugin provides a way to indicate that certain docstrings should be skipped altogether. This is configured by defining the variable ``__doctest_skip__`` in each module where tests should be skipped. The value of ``__doctest_skip__`` should be a list of wildcard patterns for all functions/classes whose doctests should be skipped. For example:: __doctest_skip__ = ['myfunction', 'MyClass', 'MyClass.*'] skips the doctests in a function called ``myfunction``, the doctest for a class called ``MyClass``, and all *methods* of ``MyClass``. Module docstrings may contain doctests as well. To skip the module-level doctests:: __doctest_skip__ = ['.', 'myfunction', 'MyClass'] To skip all doctests in a module:: __doctest_skip__ = ['*'] Doctest Dependencies ^^^^^^^^^^^^^^^^^^^^ It is also possible to skip certain doctests depending on whether particular dependencies are available. This is configured by defining the variable ``__doctest_requires__`` at the module level. The value of this variable is a dictionary that indicates the modules that are required to run the doctests associated with particular functions, classes, and methods. The keys in the dictionary are wildcard patterns like those described above, or tuples of wildcard patterns, indicating which docstrings should be skipped. The values in the dictionary are lists of module names that are required in order for the given doctests to be executed. Consider the following example:: __doctest_requires__ = {('func1', 'func2'): ['scipy']} Having this module-level variable will require ``scipy`` to be importable in order to run the doctests for functions ``func1`` and ``func2`` in that module. Similarly, in Sphinx ``.rst`` documentation, whole code example blocks can be conditionally skipped if a dependency is not available. .. code-block:: rst .. doctest-requires:: asdf >>> import asdf >>> asdf.open('file.asdf') Remote Data ~~~~~~~~~~~ The ``pytest-doctestplus`` plugin can be used in conjunction with the `pytest-remotedata`_ plugin in order to control doctest code that requires access to data from the internet. In order to make use of these features, the ``pytest-remotedata`` plugin must be installed, and remote data access must be enabled using the ``--remote-data`` command line option to ``pytest``. See the `pytest-remotedata plugin documentation`__ for more details. The following example illustrates how a doctest that uses remote data should be marked: .. code-block:: python >>> from urlib.request import urlopen >>> url = urlopen('http://astropy.org') # doctest: +REMOTE_DATA The ``+REMOTE_DATA`` directive indicates that the marked statement should only be executed if the ``--remote-data`` option is given. By default, all statements marked with ``--remote-data`` will be skipped. .. _pytest-remotedata: https://github.com/astropy/pytest-remotedata __ pytest-remotedata_ Development Status ------------------ .. image:: https://travis-ci.org/astropy/pytest-doctestplus.svg :target: https://travis-ci.org/astropy/pytest-doctestplus :alt: Travis CI Status Questions, bug reports, and feature requests can be submitted on `github`_. .. _github: https://github.com/astropy/pytest-doctestplus License ------- This plugin is licensed under a 3-clause BSD style license - see the ``LICENSE.rst`` file. Keywords: doctest,rst,pytest,py.test Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Framework :: Pytest Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Utilities Requires-Python: >=2.7 pytest-doctestplus-0.5.0/README.rst0000644000077000000240000002324613563613534017040 0ustar tomstaff00000000000000================== pytest-doctestplus ================== This package contains a plugin for the `pytest`_ framework that provides advanced doctest support and enables the testing of `reStructuredText`_ (".rst") files. It was originally part of the `astropy`_ core package, but has been moved to a separate package in order to be of more general use. .. _pytest: https://pytest.org/en/latest/ .. _astropy: https://astropy.org/en/latest/ .. _reStructuredText: https://en.wikipedia.org/wiki/ReStructuredText Motivation ---------- This plugin provides advanced features for testing example Python code that is included in Python docstrings and in standalone documentation files. Good documentation for developers contains example code. This is true of both standalone documentation and of documentation that is integrated with the code itself. Python provides a mechanism for testing code snippets that are provided in Python docstrings. The unit test framework pytest provides a mechanism for running doctests against both docstrings in source code and in standalone documentation files. This plugin augments the functionality provided by Python and pytest by providing the following features: * approximate floating point comparison for doctests that produce floating point results (see `Floating Point Comparison`_) * skipping particular classes, methods, and functions when running doctests (see `Skipping Tests`_) * handling doctests that use remote data in conjunction with the `pytest-remotedata`_ plugin (see `Remote Data`_) * optional inclusion of ``*.rst`` files for doctests (see `Setup and Configuration`_) .. _pytest-remotedata: https://github.com/astropy/pytest-remotedata Installation ------------ The ``pytest-doctestplus`` plugin can be installed using ``pip``:: $ pip install pytest-doctestplus It is also possible to install the latest development version from the source repository:: $ git clone https://github.com/astropy/pytest-doctestplus $ cd pytest-doctestplus $ python ./setup.py install In either case, the plugin will automatically be registered for use with ``pytest``. Usage ----- .. _setup: Setup and Configuration ~~~~~~~~~~~~~~~~~~~~~~~ This plugin provides two command line options: ``--doctest-plus`` for enabling the advanced features mentioned above, and ``--doctest-rst`` for including ``*.rst`` files in doctest collection. This plugin can also be enabled by default by adding ``doctest_plus = enabled`` to the ``[tool:pytest]`` section of the package's ``setup.cfg`` file. The plugin is applied to all directories and files that ``pytest`` collects. This means that configuring ``testpaths`` and ``norecursedirs`` in ``setup.cfg`` also affects the files that will be discovered by ``pytest-doctestplus``. In addition, this plugin provides a ``doctest_norecursedirs`` configuration variable that indicates directories that should be ignored by ``pytest-doctestplus`` but do not need to be ignored by other ``pytest`` features. Using ``pytest``'s built-in ``--doctest-modules`` option will override the behavior of this plugin, even if ``doctest_plus = enabled`` in ``setup.cfg``, and will cause the default doctest plugin to be used. However, if for some reason both ``--doctest-modules`` and ``--doctest-plus`` are given, the ``pytest-doctestplus`` plugin will be used, regardless of the contents of ``setup.cfg``. This plugin respects the doctest options that are used by the built-in doctest plugin and are set in ``doctest_optionflags`` in ``setup.cfg``. By default, ``ELLIPSIS`` and ``NORMALIZE_WHITESPACE`` are used. For a description of all doctest settings, see the `doctest documentation `_. Doctest Directives ~~~~~~~~~~~~~~~~~~ The ``pytest-doctestplus`` plugin defines `doctest directives`_ that are used to control the behavior of particular features. For general information on directives and how they are used, consult the `documentation`_. The specifics of the directives that this plugin defines are described in the sections below. .. _doctest directives: https://docs.python.org/3/library/doctest.html#directives .. _documentation: https://docs.python.org/3/library/doctest.html#directives Floating Point Comparison ~~~~~~~~~~~~~~~~~~~~~~~~~ Some doctests may produce output that contains string representations of floating point values. Floating point representations are often not exact and contain roundoffs in their least significant digits. Depending on the platform the tests are being run on (different Python versions, different OS, etc.) the exact number of digits shown can differ. Because doctests work by comparing strings this can cause such tests to fail. To address this issue, the ``pytest-doctestplus`` plugin provides support for a ``FLOAT_CMP`` flag that can be used with doctests. For example: .. code-block:: python >>> 1.0 / 3.0 # doctest: +FLOAT_CMP 0.333333333333333311 When this flag is used, the expected and actual outputs are both parsed to find any floating point values in the strings. Those are then converted to actual Python `float` objects and compared numerically. This means that small differences in representation of roundoff digits will be ignored by the doctest. The values are otherwise compared exactly, so more significant (albeit possibly small) differences will still be caught by these tests. This flag can be enabled globally by adding it to ``setup.cfg`` as in .. code-block:: ini doctest_optionflags = NORMALIZE_WHITESPACE ELLIPSIS FLOAT_CMP Ignoring warnings ~~~~~~~~~~~~~~~~~ If code in a doctest emits a warning and you want to make sure that warning is silenced, you can make use of the ``IGNORE_WARNINGS`` flag. For example: .. code-block:: python >>> import numpy as np >>> np.mean([]) # doctest: +IGNORE_WARNINGS np.nan Skipping Tests ~~~~~~~~~~~~~~ Doctest provides the ``+SKIP`` directive for skipping statements that should not be executed when testing documentation. .. code-block:: python >>> open('file.txt') # doctest: +SKIP In Sphinx ``.rst`` documentation, whole code example blocks can be skipped with the directive .. code-block:: rst .. doctest-skip:: >>> import asdf >>> asdf.open('file.asdf') However, it is often useful to be able to skip docstrings associated with particular functions, methods, classes, or even entire files. Skip Unconditionally ^^^^^^^^^^^^^^^^^^^^ The ``pytest-doctestplus`` plugin provides a way to indicate that certain docstrings should be skipped altogether. This is configured by defining the variable ``__doctest_skip__`` in each module where tests should be skipped. The value of ``__doctest_skip__`` should be a list of wildcard patterns for all functions/classes whose doctests should be skipped. For example:: __doctest_skip__ = ['myfunction', 'MyClass', 'MyClass.*'] skips the doctests in a function called ``myfunction``, the doctest for a class called ``MyClass``, and all *methods* of ``MyClass``. Module docstrings may contain doctests as well. To skip the module-level doctests:: __doctest_skip__ = ['.', 'myfunction', 'MyClass'] To skip all doctests in a module:: __doctest_skip__ = ['*'] Doctest Dependencies ^^^^^^^^^^^^^^^^^^^^ It is also possible to skip certain doctests depending on whether particular dependencies are available. This is configured by defining the variable ``__doctest_requires__`` at the module level. The value of this variable is a dictionary that indicates the modules that are required to run the doctests associated with particular functions, classes, and methods. The keys in the dictionary are wildcard patterns like those described above, or tuples of wildcard patterns, indicating which docstrings should be skipped. The values in the dictionary are lists of module names that are required in order for the given doctests to be executed. Consider the following example:: __doctest_requires__ = {('func1', 'func2'): ['scipy']} Having this module-level variable will require ``scipy`` to be importable in order to run the doctests for functions ``func1`` and ``func2`` in that module. Similarly, in Sphinx ``.rst`` documentation, whole code example blocks can be conditionally skipped if a dependency is not available. .. code-block:: rst .. doctest-requires:: asdf >>> import asdf >>> asdf.open('file.asdf') Remote Data ~~~~~~~~~~~ The ``pytest-doctestplus`` plugin can be used in conjunction with the `pytest-remotedata`_ plugin in order to control doctest code that requires access to data from the internet. In order to make use of these features, the ``pytest-remotedata`` plugin must be installed, and remote data access must be enabled using the ``--remote-data`` command line option to ``pytest``. See the `pytest-remotedata plugin documentation`__ for more details. The following example illustrates how a doctest that uses remote data should be marked: .. code-block:: python >>> from urlib.request import urlopen >>> url = urlopen('http://astropy.org') # doctest: +REMOTE_DATA The ``+REMOTE_DATA`` directive indicates that the marked statement should only be executed if the ``--remote-data`` option is given. By default, all statements marked with ``--remote-data`` will be skipped. .. _pytest-remotedata: https://github.com/astropy/pytest-remotedata __ pytest-remotedata_ Development Status ------------------ .. image:: https://travis-ci.org/astropy/pytest-doctestplus.svg :target: https://travis-ci.org/astropy/pytest-doctestplus :alt: Travis CI Status Questions, bug reports, and feature requests can be submitted on `github`_. .. _github: https://github.com/astropy/pytest-doctestplus License ------- This plugin is licensed under a 3-clause BSD style license - see the ``LICENSE.rst`` file. pytest-doctestplus-0.5.0/licenses/0000755000077000000240000000000013563614055017146 5ustar tomstaff00000000000000pytest-doctestplus-0.5.0/licenses/README.rst0000644000077000000240000000035413563613534020640 0ustar tomstaff00000000000000Licenses ======== This directory holds license and credit information for works this plugin is derived from or distributes, and/or datasets. The license file for this package itself is placed in the root directory of this repository. pytest-doctestplus-0.5.0/licenses/SYMPY_LICENSE.rst0000644000077000000240000000274013563613534021767 0ustar tomstaff00000000000000Copyright (c) 2006-2014 SymPy Development Team All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: a. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. b. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. c. Neither the name of SymPy nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. pytest-doctestplus-0.5.0/pytest_doctestplus/0000755000077000000240000000000013563614055021322 5ustar tomstaff00000000000000pytest-doctestplus-0.5.0/pytest_doctestplus/__init__.py0000644000077000000240000000025513563613632023435 0ustar tomstaff00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This package contains pytest plugins that are used by the astropy test suite. """ __version__ = '0.5.0' pytest-doctestplus-0.5.0/pytest_doctestplus/output_checker.py0000644000077000000240000002572713563613534024736 0ustar tomstaff00000000000000""" Implements a replacement for `doctest.OutputChecker` that handles certain normalizations of Python expression output. See the docstring on `OutputChecker` for more details. """ import doctest import re import math import six from six.moves import zip # Much of this code, particularly the parts of floating point handling, is # borrowed from the SymPy project with permission. See # licenses/SYMPY_LICENSE.rst for the full SymPy license. FIX = doctest.register_optionflag('FIX') FLOAT_CMP = doctest.register_optionflag('FLOAT_CMP') REMOTE_DATA = doctest.register_optionflag('REMOTE_DATA') IGNORE_OUTPUT = doctest.register_optionflag('IGNORE_OUTPUT') IGNORE_OUTPUT_2 = doctest.register_optionflag('IGNORE_OUTPUT_2') IGNORE_OUTPUT_3 = doctest.register_optionflag('IGNORE_OUTPUT_3') IGNORE_WARNINGS = doctest.register_optionflag('IGNORE_WARNINGS') # These might appear in some doctests and are used in the default pytest # doctest plugin. This plugin doesn't actually implement these flags but this # allows them to appear in docstrings. ALLOW_BYTES = doctest.register_optionflag('ALLOW_BYTES') ALLOW_UNICODE = doctest.register_optionflag('ALLOW_UNICODE') class OutputChecker(doctest.OutputChecker): """ - Removes u'' prefixes on string literals - Ignores the 'L' suffix on long integers - In Numpy dtype strings, removes the leading pipe, i.e. '|S9' -> 'S9'. Numpy 1.7 no longer includes it in display. - Supports the FLOAT_CMP flag, which parses floating point values out of the output and compares their numerical values rather than their string representation. This naturally supports complex numbers as well (simply by comparing their real and imaginary parts separately). """ rtol = 1e-05 atol = 1e-08 _original_output_checker = doctest.OutputChecker _str_literal_re = re.compile( r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE) _byteorder_re = re.compile( r"([\'\"])[|<>]([biufcSaUV][0-9]+)([\'\"])", re.UNICODE) _fix_32bit_re = re.compile( r"([\'\"])([iu])[48]([\'\"])", re.UNICODE) _long_int_re = re.compile( r"([0-9]+)L", re.UNICODE) def __init__(self): # NOTE OutputChecker is an old-style class with no __init__ method, # so we can't call the base class version of __init__ here exp = r'(?:e[+-]?\d+)' got_floats = (r'\s*([+-]?\d+\.\d*{0}?|' r'[+-]?\.\d+{0}?|' r'[+-]?\d+{0}|' r'nan|' r'[+-]?inf)').format(exp) # floats in the 'want' string may contain ellipses want_floats = got_floats + r'(\.{3})?' front_sep = r'\s|[*+-,<=(\[]' back_sep = front_sep + r'|[>j)\]]' fbeg = r'^{}(?={}|$)'.format(got_floats, back_sep) fmidend = r'(?<={}){}(?={}|$)'.format(front_sep, got_floats, back_sep) self.num_got_rgx = re.compile(r'({}|{})'.format(fbeg, fmidend)) fbeg = r'^{}(?={}|$)'.format(want_floats, back_sep) fmidend = r'(?<={}){}(?={}|$)'.format(front_sep, want_floats, back_sep) self.num_want_rgx = re.compile(r'({}|{})'.format(fbeg, fmidend)) def do_fixes(self, want, got): want = re.sub(self._str_literal_re, r'\1\2', want) want = re.sub(self._byteorder_re, r'\1\2\3', want) want = re.sub(self._fix_32bit_re, r'\1\2\3', want) want = re.sub(self._long_int_re, r'\1', want) got = re.sub(self._str_literal_re, r'\1\2', got) got = re.sub(self._byteorder_re, r'\1\2\3', got) got = re.sub(self._fix_32bit_re, r'\1\2\3', got) got = re.sub(self._long_int_re, r'\1', got) return want, got def find_numbers(self, text): """ Find float strings in text. >>> OutputChecker().find_numbers("1.1 foo abr 2.22") ['1.1', '2.22'] """ matches = self.num_want_rgx.finditer(text) return [match.group(1) for match in matches] def equal_floats(self, a, b): """ Compare float strings. >>> OutputChecker().equal_floats('1.1', '1.10000000001') True >>> OutputChecker().equal_floats('1.1', '1.11') False """ a, b = float(a), float(b) return isclose(a, b, rtol=self.rtol, atol=self.atol) def startswith(self, arr, prefix): """ Check if array of str/floats starts with floats in prefix. >>> OutputChecker().startswith(['1', '2', '3'], ['1', '2.00000000001']) True >>> OutputChecker().startswith(['1', '2', '3'], ['1', '2.1']) False """ if len(prefix) == 0: return True if len(arr) < len(prefix): return False for a, b in zip(arr, prefix): if not self.equal_floats(a, b): return False return True def endswith(self, arr, postfix): """ Check if array of str/floats ends with floats in postfix. >>> OutputChecker().endswith(['1', '2', '3'], ['2', '3.00000000001']) True >>> OutputChecker().endswith(['1', '2', '3'], ['2', '3.1']) False """ return self.startswith(arr[::-1], postfix[::-1]) def find(self, arr, suffix, start, end): """ Search for floats from suffix in arr. >>> OutputChecker().find(['1', '2', '3', '4'], ['2', '3.00000000001'], 0, 4) 1 >>> OutputChecker().find(['1', '2', '3', '4'], ['2', '3.1'], 0, 4) -1 """ if len(suffix) == 0: return start arr = arr[start:end] for i, a in enumerate(arr): # if current floats match... if self.equal_floats(a, suffix[0]): # ... then compare the rest of numbers from suffix if self.startswith(arr[i:], suffix): return start + i return -1 def partial_match(self, arr, chunks): """ Check that each chunk in chunks is inside provided array of strings/floats. This is essentially list-with-floats equivalent of ellipsis matching. >>> OutputChecker().partial_match( ... ['1', '2', '3', '4'], ... [['1', '2'], ['4']], ... ) True >>> OutputChecker().partial_match( ... ['1', '2', '3', '4'], ... [['1', '2'], []], ... ) True >>> OutputChecker().partial_match( ... ['1', '2', '3', '4'], ... [['1', '2'], ['5']], ... ) False """ assert len(chunks) >= 2 startpos, endpos = 0, len(arr) chunk = chunks[0] if chunk: # starts with exact match if self.startswith(arr, chunk): startpos = len(chunk) del chunks[0] else: return False chunk = chunks[-1] if chunk: # ends with exact match if self.endswith(arr, chunk): endpos -= len(chunk) del chunks[-1] else: return False if startpos > endpos: return False for chunk in chunks: startpos = self.find(arr, chunk, startpos, endpos) if startpos < 0: return False startpos += len(chunk) return True def normalize_floats(self, want, got, flags): """ Alternative to the built-in check_output that also handles parsing float values and comparing their numeric values rather than their string representations. This requires rewriting enough of the basic check_output that, when FLOAT_CMP is enabled, it totally takes over for check_output. """ # can be used as a special sequence to signify a # blank line, unless the DONT_ACCEPT_BLANKLINE flag is used. if not (flags & doctest.DONT_ACCEPT_BLANKLINE): # Replace in want with a blank line. want = re.sub(r'(?m)^{}\s*?$'.format(re.escape(doctest.BLANKLINE_MARKER)), '', want) # If a line in got contains only spaces, then remove the # spaces. got = re.sub(r'(?m)^\s*?$', '', got) # This flag causes doctest to ignore any differences in the # contents of whitespace strings. Note that this can be used # in conjunction with the ELLIPSIS flag. if flags & doctest.NORMALIZE_WHITESPACE: got = ' '.join(got.split()) want = ' '.join(want.split()) # Handle the common case first, for efficiency: # if they're string-identical, always return true. if got == want: return True got_ = self.num_got_rgx.sub('0.0', got) want_ = self.num_got_rgx.sub('0.0', want) # fail if strings with ellipsis and normalize floats are not equal if flags & doctest.ELLIPSIS: if not doctest._ellipsis_match(want_, got_): return False else: if not got_ == want_: return False # at this point we made sure that non-float parts of strings are equivalent # so now we need to compare each number numbers_got = self.find_numbers(got) numbers_want_chunks = [ self.find_numbers(chunk) for chunk in want.split(doctest.ELLIPSIS_MARKER) ] if flags & doctest.ELLIPSIS and len(numbers_want_chunks) >= 2: return self.partial_match(numbers_got, numbers_want_chunks) # TODO parse integers as well ? # Parse floats and compare them. numbers_want = [f for chunk in numbers_want_chunks for f in chunk] # flatten array if len(numbers_got) != len(numbers_want): return False for ng, nw in zip(numbers_got, numbers_want): if not self.equal_floats(ng, nw): return False return True def check_output(self, want, got, flags): if (flags & IGNORE_OUTPUT or (six.PY2 and flags & IGNORE_OUTPUT_2) or (not six.PY2 and flags & IGNORE_OUTPUT_3)): return True if flags & FIX: want, got = self.do_fixes(want, got) if flags & FLOAT_CMP: return self.normalize_floats(want, got, flags) # Can't use super here because doctest.OutputChecker is not a # new-style class. return self._original_output_checker.check_output( self, want, got, flags) def output_difference(self, want, got, flags): if flags & FIX: want, got = self.do_fixes(want, got) # Can't use super here because doctest.OutputChecker is not a # new-style class. return self._original_output_checker.output_difference( self, want, got, flags) try: import numpy def isclose(a, b, rtol=1e-05, atol=1e-08, equal_nan=True): return numpy.isclose(a, b, rtol=rtol, atol=atol, equal_nan=equal_nan) except ImportError: def isclose(a, b, rtol=1e-05, atol=1e-08, equal_nan=True): return abs(a - b) <= atol + rtol * abs(b) or (equal_nan and math.isnan(a) and math.isnan(b)) pytest-doctestplus-0.5.0/pytest_doctestplus/plugin.py0000644000077000000240000005472613563613534023211 0ustar tomstaff00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This plugin provides advanced doctest support and enables the testing of .rst files. """ import doctest import fnmatch import os import re import sys import warnings import pytest import six from pytest_doctestplus.utils import ModuleChecker from .output_checker import FIX, IGNORE_WARNINGS, OutputChecker, REMOTE_DATA try: from textwrap import indent except ImportError: # PY2 def indent(text, prefix): return '\n'.join([prefix + line for line in text.splitlines()]) comment_characters = { '.txt': '#', '.tex': '%', '.rst': r'\.\.' } # For the IGNORE_WARNINGS option, we create a context manager that doesn't # require us to add any imports to the example list and contains everything # that is needed to silence warnings. IGNORE_WARNINGS_CONTEXT = """ class _doctestplus_ignore_all_warnings(object): def __init__(self): import warnings self._cw = warnings.catch_warnings() def __enter__(self, *args, **kwargs): result = self._cw.__enter__(*args, **kwargs) import warnings warnings.simplefilter('ignore') return result def __exit__(self, *args, **kwargs): return self._cw.__exit__(*args, **kwargs) """.lstrip() # these pytest hooks allow us to mark tests and run the marked tests with # specific command line options. def pytest_addoption(parser): parser.addoption("--doctest-plus", action="store_true", help="enable running doctests with additional " "features not found in the normal doctest " "plugin") parser.addoption("--doctest-rst", action="store_true", help=( "Enable running doctests in .rst documentation. " "This is no longer recommended, use --doctest-glob instead." )) parser.addoption("--text-file-format", action="store", help=( "Text file format for narrative documentation. " "Options accepted are 'txt', 'tex', and 'rst'. " "This is no longer recommended, use --doctest-glob instead." )) # Defaults to `atol` parameter from `numpy.allclose`. parser.addoption("--doctest-plus-atol", action="store", help="set the absolute tolerance for float comparison", default=1e-08) # Defaults to `rtol` parameter from `numpy.allclose`. parser.addoption("--doctest-plus-rtol", action="store", help="set the relative tolerance for float comparison", default=1e-05) parser.addoption("--doctest-only", action="store_true", help="Test only doctests. Implies usage of doctest-plus.") parser.addini("text_file_format", "Default format for docs. " "This is no longer recommended, use --doctest-glob instead.") parser.addini("doctest_optionflags", "option flags for doctests", type="args", default=["ELLIPSIS", "NORMALIZE_WHITESPACE"],) parser.addini("doctest_plus", "enable running doctests with additional " "features not found in the normal doctest plugin") parser.addini("doctest_norecursedirs", "like the norecursedirs option but applies only to doctest " "collection", type="args", default=()) parser.addini("doctest_rst", "Run the doctests in the rst documentation", default=False) parser.addini("doctest_plus_atol", "set the absolute tolerance for float comparison", default=1e-08) parser.addini("doctest_plus_rtol", "set the relative tolerance for float comparison", default=1e-05) parser.addini('text_file_comment_chars', help='list of pairs in format file_extension=comment_chars, eg: .rst=..', type='linelist', default=[]) def get_optionflags(parent): optionflags_str = parent.config.getini('doctest_optionflags') flag_int = 0 for flag_str in optionflags_str: flag_int |= doctest.OPTIONFLAGS_BY_NAME[flag_str] return flag_int def pytest_configure(config): doctest_plugin = config.pluginmanager.getplugin('doctest') run_regular_doctest = config.option.doctestmodules and not config.option.doctest_plus use_doctest_plus = config.getini('doctest_plus') or config.option.doctest_plus or config.option.doctest_only if doctest_plugin is None or run_regular_doctest or not use_doctest_plus: return # We monkey-patch in our replacement doctest OutputChecker. Not # great, but there isn't really an API to replace the checker when # using doctest.testfile, unfortunately. doctest.OutputChecker = OutputChecker OutputChecker.rtol = max(float(config.getini("doctest_plus_rtol")), float(config.getoption("doctest_plus_rtol"))) OutputChecker.atol = max(float(config.getini("doctest_plus_atol")), float(config.getoption("doctest_plus_atol"))) use_rst = config.getini('doctest_rst') or config.option.doctest_rst file_ext = config.option.text_file_format or config.getini('text_file_format') or 'rst' if use_rst: config.option.doctestglob.append('*.{}'.format(file_ext)) # override default comment characters ext_comment_pairs = [pair.split('=') for pair in config.getini('text_file_comment_chars')] for ext, chars in ext_comment_pairs: comment_characters[ext] = chars class DocTestModulePlus(doctest_plugin.DoctestModule): # pytest 2.4.0 defines "collect". Prior to that, it defined # "runtest". The "collect" approach is better, because we can # skip modules altogether that have no doctests. However, we # need to continue to override "runtest" so that the built-in # behavior (which doesn't do whitespace normalization or # handling __doctest_skip__) doesn't happen. def collect(self): # When running directly from pytest we need to make sure that we # don't accidentally import setup.py! if self.fspath.basename == "setup.py": return elif self.fspath.basename == "conftest.py": try: module = self.config._conftest.importconftest(self.fspath) except AttributeError: # pytest >= 2.8.0 module = self.config.pluginmanager._importconftest(self.fspath) else: try: module = self.fspath.pyimport() # Just ignore searching modules that can't be imported when # collecting doctests except ImportError: return options = get_optionflags(self) | FIX # uses internal doctest module parsing mechanism finder = DocTestFinderPlus() runner = doctest.DebugRunner( verbose=False, optionflags=options, checker=OutputChecker()) for test in finder.find(module): if test.examples: # skip empty doctests if config.getoption('remote_data', 'none') != 'any': ignore_warnings_context_needed = False for example in test.examples: # If warnings are to be ignored we need to catch them by # wrapping the source in a context manager. if example.options.get(IGNORE_WARNINGS, False): example.source = ("with _doctestplus_ignore_all_warnings():\n" + indent(example.source, ' ')) ignore_warnings_context_needed = True if example.options.get(REMOTE_DATA): example.options[doctest.SKIP] = True # We insert the definition of the context manager to ignore # warnings at the start of the file if needed. if ignore_warnings_context_needed: test.examples.insert(0, doctest.Example(source=IGNORE_WARNINGS_CONTEXT, want='')) yield doctest_plugin.DoctestItem( test.name, self, runner, test) class DocTestTextfilePlus(doctest_plugin.DoctestItem, pytest.Module): # Some pytest plugins such as hypothesis try and access the 'obj' # attribute, and by default this returns an error for this class # so we override it here to avoid any issues. def obj(self): pass def runtest(self): # satisfy `FixtureRequest` constructor... self.funcargs = {} fixture_request = doctest_plugin._setup_fixtures(self) options = get_optionflags(self) | FIX doctest.testfile( str(self.fspath), module_relative=False, optionflags=options, parser=DocTestParserPlus(), extraglobs=dict(getfixture=fixture_request.getfixturevalue), raise_on_error=True, verbose=False, encoding='utf-8') def reportinfo(self): """ Overwrite pytest's ``DoctestItem`` because ``DocTestTextfilePlus`` does not have a ``dtest`` attribute which is used by pytest>=3.2.0 to return the location of the tests. For details see `pytest-dev/pytest#2651 `_. """ return self.fspath, None, "[doctest] %s" % self.name class DocTestParserPlus(doctest.DocTestParser): """ An extension to the builtin DocTestParser that handles the special directives for skipping tests. The directives are: - ``.. doctest-skip::``: Skip the next doctest chunk. - ``.. doctest-requires:: module1, module2``: Skip the next doctest chunk if the given modules/packages are not installed. - ``.. doctest-skip-all``: Skip all subsequent doctests. """ def parse(self, s, name=None): result = doctest.DocTestParser.parse(self, s, name=name) # result is a sequence of alternating text chunks and # doctest.Example objects. We need to look in the text # chunks for the special directives that help us determine # whether the following examples should be skipped. required = [] skip_next = False skip_all = False ext = os.path.splitext(name)[1] if name else '.rst' if ext not in comment_characters: warnings.warn("file format '{}' is not recognized, assuming " "'{}' as the comment character." .format(ext, comment_characters['rst'])) ext = '.rst' comment_char = comment_characters[ext] ignore_warnings_context_needed = False for entry in result: if isinstance(entry, six.string_types) and entry: required = [] skip_next = False lines = entry.strip().splitlines() if any([re.match('{} doctest-skip-all'.format(comment_char), x.strip()) for x in lines]): skip_all = True continue if not len(lines): continue # We allow last and second to last lines to match to allow # special environment to be in between, e.g. \begin{python} last_lines = lines[-2:] matches = [re.match( r'{}\s+doctest-skip\s*::(\s+.*)?'.format(comment_char), last_line) for last_line in last_lines] if len(matches) > 1: match = matches[0] or matches[1] else: match = matches[0] if match: marker = match.group(1) if (marker is None or (marker.strip() == 'win32' and sys.platform == 'win32')): skip_next = True continue matches = [re.match( r'{}\s+doctest-requires\s*::\s+(.*)'.format(comment_char), last_line) for last_line in last_lines] if len(matches) > 1: match = matches[0] or matches[1] else: match = matches[0] if match: # 'a a' or 'a,a' or 'a, a'-> [a, a] required = re.split(r'\s*[,\s]\s*', match.group(1)) elif isinstance(entry, doctest.Example): # If warnings are to be ignored we need to catch them by # wrapping the source in a context manager. if entry.options.get(IGNORE_WARNINGS, False): entry.source = ("with _doctestplus_ignore_all_warnings():\n" + indent(entry.source, ' ')) ignore_warnings_context_needed = True has_required_modules = DocTestFinderPlus.check_required_modules(required) if skip_all or skip_next or not has_required_modules: entry.options[doctest.SKIP] = True if config.getoption('remote_data', 'none') != 'any' and entry.options.get(REMOTE_DATA): entry.options[doctest.SKIP] = True # We insert the definition of the context manager to ignore # warnings at the start of the file if needed. if ignore_warnings_context_needed: result.insert(0, doctest.Example(source=IGNORE_WARNINGS_CONTEXT, want='')) return result config.pluginmanager.register( DoctestPlus( DocTestModulePlus, DocTestTextfilePlus, config.option.doctestglob, ), 'doctestplus', ) # Remove the doctest_plugin, or we'll end up testing the .rst files twice. config.pluginmanager.unregister(doctest_plugin) class DoctestPlus(object): def __init__(self, doctest_module_item_cls, doctest_textfile_item_cls, file_globs): """ doctest_module_item_cls should be a class inheriting `pytest.doctest.DoctestItem` and `pytest.File`. This class handles running of a single doctest found in a Python module. This is passed in as an argument because the actual class to be used may not be available at import time, depending on whether or not the doctest plugin for py.test is available. """ self._doctest_module_item_cls = doctest_module_item_cls self._doctest_textfile_item_cls = doctest_textfile_item_cls self._file_globs = file_globs # Directories to ignore when adding doctests self._ignore_paths = [] def pytest_ignore_collect(self, path, config): """ Skip paths that match any of the doctest_norecursedirs patterns or if doctest_only is True then skip all regular test files (eg test_*.py). """ collect_ignore = config._getconftest_pathlist("collect_ignore", path=path.dirpath()) # The collect_ignore conftest.py variable should cause all test # runners to ignore this file and all subfiles and subdirectories if collect_ignore is not None and path in collect_ignore: return True if config.option.doctest_only: for pattern in config.getini('python_files'): if path.check(fnmatch=pattern): return True def get_list_opt(name): return getattr(config.option, name, None) or [] for ignore_path in get_list_opt('ignore'): ignore_path = os.path.abspath(ignore_path) if str(path).startswith(ignore_path): return True for pattern in get_list_opt('ignore_glob'): if path.check(fnmatch=pattern): return True for pattern in config.getini("doctest_norecursedirs"): if path.check(fnmatch=pattern): # Apparently pytest_ignore_collect causes files not to be # collected by any test runner; for DoctestPlus we only want to # avoid creating doctest nodes for them self._ignore_paths.append(path) break return False def pytest_collect_file(self, path, parent): """Implements an enhanced version of the doctest module from py.test (specifically, as enabled by the --doctest-modules option) which supports skipping all doctests in a specific docstring by way of a special ``__doctest_skip__`` module-level variable. It can also skip tests that have special requirements by way of ``__doctest_requires__``. ``__doctest_skip__`` should be a list of functions, classes, or class methods whose docstrings should be ignored when collecting doctests. This also supports wildcard patterns. For example, to run doctests in a class's docstring, but skip all doctests in its modules use, at the module level:: __doctest_skip__ = ['ClassName.*'] You may also use the string ``'.'`` in ``__doctest_skip__`` to refer to the module itself, in case its module-level docstring contains doctests. ``__doctest_requires__`` should be a dictionary mapping wildcard patterns (in the same format as ``__doctest_skip__``) to a list of one or more modules that should be *importable* in order for the tests to run. For example, if some tests require the scipy module to work they will be skipped unless ``import scipy`` is possible. It is also possible to use a tuple of wildcard patterns as a key in this dict:: __doctest_requires__ = {('func1', 'func2'): ['scipy']} """ for ignore_path in self._ignore_paths: if ignore_path.common(path) == ignore_path: return None if path.ext == '.py': if path.basename == 'conf.py': return None # Don't override the built-in doctest plugin return self._doctest_module_item_cls(path, parent) elif any([path.check(fnmatch=pat) for pat in self._file_globs]): # Ignore generated .rst files parts = str(path).split(os.path.sep) # Don't test files that start with a _ if path.basename.startswith('_'): return None # Don't test files in directories that start with a '_' if those # directories are inside docs. Note that we *should* allow for # example /tmp/_q/docs/file.rst but not /tmp/docs/_build/file.rst # If we don't find 'docs' in the path, we should just skip this # check to be safe. We also want to skip any api sub-directory # of docs. if 'docs' in parts: # We index from the end on the off chance that the temporary # directory includes 'docs' in the path, e.g. # /tmp/docs/371j/docs/index.rst You laugh, but who knows! :) # Also, it turns out lists don't have an rindex method. Huh??!! docs_index = len(parts) - 1 - parts[::-1].index('docs') if any(x.startswith('_') or x == 'api' for x in parts[docs_index:]): return None # TODO: Get better names on these items when they are # displayed in py.test output return self._doctest_textfile_item_cls(path, parent) class DocTestFinderPlus(doctest.DocTestFinder): """Extension to the default `doctest.DoctestFinder` that supports ``__doctest_skip__`` magic. See `pytest_collect_file` for more details. """ # Caches the results of import attempts _import_cache = {} _module_checker = ModuleChecker() @classmethod def check_required_modules(cls, mods): """Check that modules in `mods` list are available. Parameters ---------- mods : list of str List of modules. Modules can have specified versions (eg 'numpy>=1.15') Returns ------- bool True if all modules in list are available. """ for mod in mods: if mod in cls._import_cache: if not cls._import_cache[mod]: return False if cls._module_checker.check(mod): cls._import_cache[mod] = True else: cls._import_cache[mod] = False return False return True def find(self, obj, name=None, module=None, globs=None, extraglobs=None): tests = doctest.DocTestFinder.find(self, obj, name, module, globs, extraglobs) if hasattr(obj, '__doctest_skip__') or hasattr(obj, '__doctest_requires__'): if name is None and hasattr(obj, '__name__'): name = obj.__name__ else: raise ValueError("DocTestFinder.find: name must be given " "when obj.__name__ doesn't exist: {!r}" .format((type(obj),))) def test_filter(test): for pat in getattr(obj, '__doctest_skip__', []): if pat == '*': return False elif pat == '.' and test.name == name: return False elif fnmatch.fnmatch(test.name, '.'.join((name, pat))): return False reqs = getattr(obj, '__doctest_requires__', {}) for pats, mods in six.iteritems(reqs): if not isinstance(pats, tuple): pats = (pats,) for pat in pats: if not fnmatch.fnmatch(test.name, '.'.join((name, pat))): continue if not self.check_required_modules(mods): return False return True tests = list(filter(test_filter, tests)) return tests pytest-doctestplus-0.5.0/pytest_doctestplus/utils.py0000644000077000000240000000613713563613534023044 0ustar tomstaff00000000000000import logging import operator import re import subprocess import sys from distutils.version import LooseVersion logger = logging.getLogger(__name__) class ModuleChecker: def __init__(self): if LooseVersion(sys.version) < LooseVersion('3.4'): import imp self._find_module = imp.find_module self._find_distribution = self._check_distribution self.packages = self.get_packages() else: import importlib.util import pkg_resources self._find_module = importlib.util.find_spec self._find_distribution = pkg_resources.require self.packages = {} def get_packages(self): packages = subprocess.check_output([sys.executable, '-m', 'pip', 'freeze']).decode().splitlines() packages = [package.split('==') for package in packages if '==' in package] return {name.lower(): version for name, version in packages} def compare_versions(self, v1, v2, op): op_map = { '<': operator.lt, '<=': operator.le, '>': operator.gt, '>=': operator.ge, '==': operator.eq, } if op not in op_map: return False op = op_map[op] return op(LooseVersion(v1), LooseVersion(v2)) def _check_distribution(self, module): """ Python 2 (and <3.4) compatible version of pkg_resources.require. But unlike pkg_resources.require it just checks whether package is installed and has required version. """ match = re.match(r'([A-Za-z0-9-_]+)([^A-Za-z0-9-_]+)([\d.]+$)', module) if not match: return False package, cmp, version = match.groups() package = package.lower() if package in self.packages: installed_version = self.packages[package] if self.compare_versions(installed_version, version, cmp): return True else: logger.warning( "{} {} is installed. Version {}{} is required".format( package, installed_version, cmp, version ) ) return False logger.warning("The '{}' distribution was not found and is required by the application".format(package)) return False def find_module(self, module): """Search for modules specification.""" try: return self._find_module(module) except ImportError: return None def find_distribution(self, dist): """Search for distribution with specified version (eg 'numpy>=1.15').""" try: return self._find_distribution(dist) except Exception as e: logger.warning(e) return None def check(self, module): """ Return True if module with specified version exists. >>> ModuleChecker().check('foo>=1.0') False >>> ModuleChecker().check('pytest>1.0') True """ mods = self.find_module(module) or self.find_distribution(module) return bool(mods) pytest-doctestplus-0.5.0/pytest_doctestplus.egg-info/0000755000077000000240000000000013563614055023014 5ustar tomstaff00000000000000pytest-doctestplus-0.5.0/pytest_doctestplus.egg-info/PKG-INFO0000644000077000000240000003141213563614054024111 0ustar tomstaff00000000000000Metadata-Version: 1.2 Name: pytest-doctestplus Version: 0.5.0 Summary: Pytest plugin with advanced doctest features. Home-page: https://astropy.org Author: The Astropy Developers Author-email: astropy.team@gmail.com License: BSD Description: ================== pytest-doctestplus ================== This package contains a plugin for the `pytest`_ framework that provides advanced doctest support and enables the testing of `reStructuredText`_ (".rst") files. It was originally part of the `astropy`_ core package, but has been moved to a separate package in order to be of more general use. .. _pytest: https://pytest.org/en/latest/ .. _astropy: https://astropy.org/en/latest/ .. _reStructuredText: https://en.wikipedia.org/wiki/ReStructuredText Motivation ---------- This plugin provides advanced features for testing example Python code that is included in Python docstrings and in standalone documentation files. Good documentation for developers contains example code. This is true of both standalone documentation and of documentation that is integrated with the code itself. Python provides a mechanism for testing code snippets that are provided in Python docstrings. The unit test framework pytest provides a mechanism for running doctests against both docstrings in source code and in standalone documentation files. This plugin augments the functionality provided by Python and pytest by providing the following features: * approximate floating point comparison for doctests that produce floating point results (see `Floating Point Comparison`_) * skipping particular classes, methods, and functions when running doctests (see `Skipping Tests`_) * handling doctests that use remote data in conjunction with the `pytest-remotedata`_ plugin (see `Remote Data`_) * optional inclusion of ``*.rst`` files for doctests (see `Setup and Configuration`_) .. _pytest-remotedata: https://github.com/astropy/pytest-remotedata Installation ------------ The ``pytest-doctestplus`` plugin can be installed using ``pip``:: $ pip install pytest-doctestplus It is also possible to install the latest development version from the source repository:: $ git clone https://github.com/astropy/pytest-doctestplus $ cd pytest-doctestplus $ python ./setup.py install In either case, the plugin will automatically be registered for use with ``pytest``. Usage ----- .. _setup: Setup and Configuration ~~~~~~~~~~~~~~~~~~~~~~~ This plugin provides two command line options: ``--doctest-plus`` for enabling the advanced features mentioned above, and ``--doctest-rst`` for including ``*.rst`` files in doctest collection. This plugin can also be enabled by default by adding ``doctest_plus = enabled`` to the ``[tool:pytest]`` section of the package's ``setup.cfg`` file. The plugin is applied to all directories and files that ``pytest`` collects. This means that configuring ``testpaths`` and ``norecursedirs`` in ``setup.cfg`` also affects the files that will be discovered by ``pytest-doctestplus``. In addition, this plugin provides a ``doctest_norecursedirs`` configuration variable that indicates directories that should be ignored by ``pytest-doctestplus`` but do not need to be ignored by other ``pytest`` features. Using ``pytest``'s built-in ``--doctest-modules`` option will override the behavior of this plugin, even if ``doctest_plus = enabled`` in ``setup.cfg``, and will cause the default doctest plugin to be used. However, if for some reason both ``--doctest-modules`` and ``--doctest-plus`` are given, the ``pytest-doctestplus`` plugin will be used, regardless of the contents of ``setup.cfg``. This plugin respects the doctest options that are used by the built-in doctest plugin and are set in ``doctest_optionflags`` in ``setup.cfg``. By default, ``ELLIPSIS`` and ``NORMALIZE_WHITESPACE`` are used. For a description of all doctest settings, see the `doctest documentation `_. Doctest Directives ~~~~~~~~~~~~~~~~~~ The ``pytest-doctestplus`` plugin defines `doctest directives`_ that are used to control the behavior of particular features. For general information on directives and how they are used, consult the `documentation`_. The specifics of the directives that this plugin defines are described in the sections below. .. _doctest directives: https://docs.python.org/3/library/doctest.html#directives .. _documentation: https://docs.python.org/3/library/doctest.html#directives Floating Point Comparison ~~~~~~~~~~~~~~~~~~~~~~~~~ Some doctests may produce output that contains string representations of floating point values. Floating point representations are often not exact and contain roundoffs in their least significant digits. Depending on the platform the tests are being run on (different Python versions, different OS, etc.) the exact number of digits shown can differ. Because doctests work by comparing strings this can cause such tests to fail. To address this issue, the ``pytest-doctestplus`` plugin provides support for a ``FLOAT_CMP`` flag that can be used with doctests. For example: .. code-block:: python >>> 1.0 / 3.0 # doctest: +FLOAT_CMP 0.333333333333333311 When this flag is used, the expected and actual outputs are both parsed to find any floating point values in the strings. Those are then converted to actual Python `float` objects and compared numerically. This means that small differences in representation of roundoff digits will be ignored by the doctest. The values are otherwise compared exactly, so more significant (albeit possibly small) differences will still be caught by these tests. This flag can be enabled globally by adding it to ``setup.cfg`` as in .. code-block:: ini doctest_optionflags = NORMALIZE_WHITESPACE ELLIPSIS FLOAT_CMP Ignoring warnings ~~~~~~~~~~~~~~~~~ If code in a doctest emits a warning and you want to make sure that warning is silenced, you can make use of the ``IGNORE_WARNINGS`` flag. For example: .. code-block:: python >>> import numpy as np >>> np.mean([]) # doctest: +IGNORE_WARNINGS np.nan Skipping Tests ~~~~~~~~~~~~~~ Doctest provides the ``+SKIP`` directive for skipping statements that should not be executed when testing documentation. .. code-block:: python >>> open('file.txt') # doctest: +SKIP In Sphinx ``.rst`` documentation, whole code example blocks can be skipped with the directive .. code-block:: rst .. doctest-skip:: >>> import asdf >>> asdf.open('file.asdf') However, it is often useful to be able to skip docstrings associated with particular functions, methods, classes, or even entire files. Skip Unconditionally ^^^^^^^^^^^^^^^^^^^^ The ``pytest-doctestplus`` plugin provides a way to indicate that certain docstrings should be skipped altogether. This is configured by defining the variable ``__doctest_skip__`` in each module where tests should be skipped. The value of ``__doctest_skip__`` should be a list of wildcard patterns for all functions/classes whose doctests should be skipped. For example:: __doctest_skip__ = ['myfunction', 'MyClass', 'MyClass.*'] skips the doctests in a function called ``myfunction``, the doctest for a class called ``MyClass``, and all *methods* of ``MyClass``. Module docstrings may contain doctests as well. To skip the module-level doctests:: __doctest_skip__ = ['.', 'myfunction', 'MyClass'] To skip all doctests in a module:: __doctest_skip__ = ['*'] Doctest Dependencies ^^^^^^^^^^^^^^^^^^^^ It is also possible to skip certain doctests depending on whether particular dependencies are available. This is configured by defining the variable ``__doctest_requires__`` at the module level. The value of this variable is a dictionary that indicates the modules that are required to run the doctests associated with particular functions, classes, and methods. The keys in the dictionary are wildcard patterns like those described above, or tuples of wildcard patterns, indicating which docstrings should be skipped. The values in the dictionary are lists of module names that are required in order for the given doctests to be executed. Consider the following example:: __doctest_requires__ = {('func1', 'func2'): ['scipy']} Having this module-level variable will require ``scipy`` to be importable in order to run the doctests for functions ``func1`` and ``func2`` in that module. Similarly, in Sphinx ``.rst`` documentation, whole code example blocks can be conditionally skipped if a dependency is not available. .. code-block:: rst .. doctest-requires:: asdf >>> import asdf >>> asdf.open('file.asdf') Remote Data ~~~~~~~~~~~ The ``pytest-doctestplus`` plugin can be used in conjunction with the `pytest-remotedata`_ plugin in order to control doctest code that requires access to data from the internet. In order to make use of these features, the ``pytest-remotedata`` plugin must be installed, and remote data access must be enabled using the ``--remote-data`` command line option to ``pytest``. See the `pytest-remotedata plugin documentation`__ for more details. The following example illustrates how a doctest that uses remote data should be marked: .. code-block:: python >>> from urlib.request import urlopen >>> url = urlopen('http://astropy.org') # doctest: +REMOTE_DATA The ``+REMOTE_DATA`` directive indicates that the marked statement should only be executed if the ``--remote-data`` option is given. By default, all statements marked with ``--remote-data`` will be skipped. .. _pytest-remotedata: https://github.com/astropy/pytest-remotedata __ pytest-remotedata_ Development Status ------------------ .. image:: https://travis-ci.org/astropy/pytest-doctestplus.svg :target: https://travis-ci.org/astropy/pytest-doctestplus :alt: Travis CI Status Questions, bug reports, and feature requests can be submitted on `github`_. .. _github: https://github.com/astropy/pytest-doctestplus License ------- This plugin is licensed under a 3-clause BSD style license - see the ``LICENSE.rst`` file. Keywords: doctest,rst,pytest,py.test Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Framework :: Pytest Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Utilities Requires-Python: >=2.7 pytest-doctestplus-0.5.0/pytest_doctestplus.egg-info/SOURCES.txt0000644000077000000240000000140413563614055024677 0ustar tomstaff00000000000000.gitignore .travis.yml CHANGES.rst LICENSE.rst MANIFEST.in README.rst setup.cfg setup.py licenses/README.rst licenses/SYMPY_LICENSE.rst pytest_doctestplus/__init__.py pytest_doctestplus/output_checker.py pytest_doctestplus/plugin.py pytest_doctestplus/utils.py pytest_doctestplus.egg-info/PKG-INFO pytest_doctestplus.egg-info/SOURCES.txt pytest_doctestplus.egg-info/dependency_links.txt pytest_doctestplus.egg-info/entry_points.txt pytest_doctestplus.egg-info/not-zip-safe pytest_doctestplus.egg-info/requires.txt pytest_doctestplus.egg-info/top_level.txt tests/conftest.py tests/test_doctestplus.py tests/test_utils.py tests/docs/skip_all.rst tests/docs/skip_all.tex tests/docs/skip_some.rst tests/docs/skip_some.tex tests/python/doctests.py tests/python/skip_doctests.pypytest-doctestplus-0.5.0/pytest_doctestplus.egg-info/dependency_links.txt0000644000077000000240000000000113563614054027061 0ustar tomstaff00000000000000 pytest-doctestplus-0.5.0/pytest_doctestplus.egg-info/entry_points.txt0000644000077000000240000000007313563614054026311 0ustar tomstaff00000000000000[pytest11] pytest_doctestplus = pytest_doctestplus.plugin pytest-doctestplus-0.5.0/pytest_doctestplus.egg-info/not-zip-safe0000644000077000000240000000000113563614052025237 0ustar tomstaff00000000000000 pytest-doctestplus-0.5.0/pytest_doctestplus.egg-info/requires.txt0000644000077000000240000000002013563614054025403 0ustar tomstaff00000000000000six pytest>=3.0 pytest-doctestplus-0.5.0/pytest_doctestplus.egg-info/top_level.txt0000644000077000000240000000002313563614054025540 0ustar tomstaff00000000000000pytest_doctestplus pytest-doctestplus-0.5.0/setup.cfg0000644000077000000240000000035413563614055017164 0ustar tomstaff00000000000000[tool:pytest] minversion = 3.0 testpaths = tests pytest_doctestplus xfail_strict = true filterwarnings = error ignore:file format.*:UserWarning ignore:.*non-empty pattern match.*:FutureWarning [egg_info] tag_build = tag_date = 0 pytest-doctestplus-0.5.0/setup.py0000755000077000000240000000307213563613733017062 0ustar tomstaff00000000000000#!/usr/bin/env python # Licensed under a 3-clause BSD style license - see LICENSE.rst # -*- encoding: utf-8 -*- from setuptools import setup, find_packages def readme(): with open('README.rst') as ff: return ff.read() setup( name='pytest-doctestplus', version='0.5.0', license='BSD', description='Pytest plugin with advanced doctest features.', long_description=readme(), author='The Astropy Developers', author_email='astropy.team@gmail.com', url='https://astropy.org', packages=find_packages(exclude=['tests']), include_package_data=True, zip_safe=False, classifiers=[ 'Development Status :: 3 - Alpha', 'Framework :: Pytest', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: Implementation :: CPython', 'Topic :: Software Development :: Testing', 'Topic :: Utilities', ], keywords=['doctest', 'rst', 'pytest', 'py.test'], install_requires=['six', 'pytest>=3.0'], python_requires='>=2.7', entry_points={ 'pytest11': [ 'pytest_doctestplus = pytest_doctestplus.plugin', ], }, ) pytest-doctestplus-0.5.0/tests/0000755000077000000240000000000013563614055016503 5ustar tomstaff00000000000000pytest-doctestplus-0.5.0/tests/conftest.py0000644000077000000240000000236713563613534020713 0ustar tomstaff00000000000000from functools import partial import textwrap import pytest def _wrap_docstring_in_func(func_name, docstring): template = textwrap.dedent(r""" def {}(): r''' {} ''' """) return template.format(func_name, docstring) @pytest.fixture def makepyfile(testdir): """Fixture for making python files with single function and docstring.""" def make(*args, **kwargs): func_name = kwargs.pop('func_name', 'f') # content in args and kwargs is treated as docstring wrap = partial(_wrap_docstring_in_func, func_name) args = map(wrap, args) kwargs = dict(zip(kwargs.keys(), map(wrap, kwargs.values()))) return testdir.makepyfile(*args, **kwargs) return make @pytest.fixture def maketestfile(makepyfile): """Fixture for making python test files with single function and docstring.""" def make(*args, **kwargs): func_name = kwargs.pop('func_name', 'test_foo') return makepyfile(*args, func_name=func_name, **kwargs) return make @pytest.fixture def makerstfile(testdir): """Fixture for making rst files with specified content.""" def make(*args, **kwargs): return testdir.makefile('.rst', *args, **kwargs) return make pytest-doctestplus-0.5.0/tests/docs/0000755000077000000240000000000013563614055017433 5ustar tomstaff00000000000000pytest-doctestplus-0.5.0/tests/docs/skip_all.rst0000644000077000000240000000120313202042231021734 0ustar tomstaff00000000000000.. doctest-skip-all Some Bad Test Cases ******************* All of the example code blocks in this file are going to fail if they run. So if the directive used at the top of this file does not work, then there are going to be test failures. Undefined Variables =================== This one will fail because the variables haven't been defined:: >>> x + y 5 No Such Module ============== This one will fail because there's (probably) no such module:: >>> import foobar >>> foobar.baz(42) 0 What??? ======= This one will fail because it's just not valid python:: >>> NOT VALID PYTHON, OKAY? >>> + 5 10 pytest-doctestplus-0.5.0/tests/docs/skip_all.tex0000644000077000000240000000153013563613534021753 0ustar tomstaff00000000000000% doctest-skip-all \documentclass{article} \setlength{\parindent}{0cm} \usepackage{listings} \lstnewenvironment{python}[1][]{ \lstset{ language=python, }}{} \begin{document} \section{Some Bad Test Cases} All of the example code blocks in this file are going to fail if they run. So if the directive used at the top of this file does not work, then there are going to be test failures. \subsection{Undefined Variables} This one will fail because the variables haven't been defined:: \begin{python} >>> x + y 5 \end{python} \subsection{No Such Module} This one will fail because there's (probably) no such module:: \begin{python} >>> import foobar >>> foobar.baz(42) 0 \end{python} \section{What???} This one will fail because it's just not valid python:: \begin{python} >>> NOT VALID PYTHON, OKAY? >>> + 5 10 \end{python} \end{document} pytest-doctestplus-0.5.0/tests/docs/skip_some.rst0000644000077000000240000000261513563613534022163 0ustar tomstaff00000000000000Some Good Test Cases ******************** Some of the example code blocks in this file are perfectly valid and will pass if they run. Some are not. The intent of this file is to test the directives that are provided by the `--doctest-rst` option to make sure that the bad ones get skipped. Here's One That Works ===================== This code block should work just fine:: >>> 1 + 1 2 This one should work just fine as well:: >>> x = 5 >>> x 5 Here's One That Doesn't ======================= This code won't run. So let's make sure that it gets skipped: .. doctest-skip:: >>> y + z 42 This one doesn't work either: .. doctest-skip:: >>> print(blue) 'blue' Good Imports ============ There should be nothing wrong with this code, so we'll let it run:: >>> import os >>> os.path.curdir '.' Make sure the `doctest-requires` directive works for modules that are available: .. doctest-requires:: sys >>> import sys Bad Imports =========== I don't think this module exists, so we should make sure that this code doesn't run: .. doctest-requires:: foobar >>> import foobar >>> foobar.baz(42) 1 Package version =============== Code in doctest should run only if version condition is satisfied: .. doctest-requires:: numpy<=0.1 >>> import numpy >>> assert 0 .. doctest-requires:: pytest>=1.0 pytest>=2.0 >>> import pytest pytest-doctestplus-0.5.0/tests/docs/skip_some.tex0000644000077000000240000000260113563613534022146 0ustar tomstaff00000000000000\documentclass{article} \setlength{\parindent}{0cm} \usepackage{listings} \lstnewenvironment{python}[1][]{ \lstset{ language=python, }}{} \begin{document} Some of the example code blocks in this file are perfectly valid and will pass if they run. Some are not. The intent of this file is to test the directives that are provided by the ``--doctest-text'' option to make sure that the bad ones get skipped. \section{Here's One That Works} This code block should work just fine:: \begin{python} >>> 1 + 1 2 \end{python} This one should work just fine as well:: \begin{python} >>> x = 5 >>> x 5 \end{python} \section{Here's One That Doesn't} This code won't run. So let's make sure that it gets skipped: % doctest-skip:: \begin{python} >>> y + z 42 \end{python} This one doesn't work either: % doctest-skip:: \begin{python} >>> print(blue) 'blue' \end{python} \section{Good Imports} There should be nothing wrong with this code, so we'll let it run: \begin{python} >>> import os >>> os.path.curdir '.' \end{python} Make sure the `doctest-requires` directive works for modules that are available: % doctest-requires:: sys \begin{python} >>> import sys \end{python} \section{Bad Imports} I don't think this module exists, so we should make sure that this code doesn't run: % doctest-requires:: foobar \begin{python} >>> import foobar >>> foobar.baz(42) 1 \end{python} \end{document} pytest-doctestplus-0.5.0/tests/python/0000755000077000000240000000000013563614055020024 5ustar tomstaff00000000000000pytest-doctestplus-0.5.0/tests/python/doctests.py0000644000077000000240000000410513563613534022227 0ustar tomstaff00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst __doctest_skip__ = [ 'skip_this_test', 'ClassWithSomeBadDocTests.this_test_fails', 'ClassWithAllBadDocTests.*', ] __doctest_requires__ = { 'depends_on_foobar': ['foobar'], 'depends_on_foobar_submodule': ['foobar.baz'], 'depends_on_two_modules': ['os', 'foobar'], } def this_test_works(): """ This test should be executed by --doctest-plus and should pass. >>> 1 + 1 2 """ def skip_this_test(): """ This test will cause a failure if __doctest_skip__ is not working properly. >>> x + y 2 """ def depends_on_real_module(): """ This test should be executed by --doctest-plus and should pass. >>> import os >>> os.path.curdir '.' """ def depends_on_foobar(): """ This test will cause a failure if __doctest_requires__ is not working. >>> import foobar >>> foobar.foo.bar('baz') 42 """ def depends_on_foobar_submodule(): """ This test will cause a failure if __doctest_requires__ is not working. >>> import foobar.baz >>> foobar.baz.bar('baz') 42 """ def depends_on_two_modules(): """ This test will cause a failure if __doctest_requires__ is not working. >>> import os >>> import foobar >>> foobar.foo.bar(os.path.curdir) 'The meaning of life' """ class ClassWithSomeBadDocTests(object): def this_test_works(): """ This test should be executed by --doctest-plus and should pass. >>> 1 + 1 2 """ def this_test_fails(): """ This test will cause a failure if __doctest_skip__ is not working. >>> x + y 5 """ class ClassWithAllBadDocTests(object): def this_test_fails(self): """ This test will cause a failure if __doctest_skip__ is not working. >>> x + y 5 """ def this_test_also_fails(self): """ This test will cause a failure if __doctest_skip__ is not working. >>> print(blue) 'blue' """ pytest-doctestplus-0.5.0/tests/python/skip_doctests.py0000644000077000000240000000113413563613534023254 0ustar tomstaff00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst # Skip all tests in this file since they'll all fail __doctest_skip__ = ['*'] def bad_doctest(): """ This test will fail if __doctest_skip__ is not working properly. >>> x + y 5 """ def another_bad_doctest(): """ This test will fail if __doctest_skip__ is not working properly. >>> import foobar >>> foobar.baz() 5 """ def yet_another_bad_doctest(): """ This test will fail if __doctest_skip__ is not working properly. >>> NOT VALID PYTHON, RIGHT >>> + 7 42 """ pytest-doctestplus-0.5.0/tests/test_doctestplus.py0000644000077000000240000004021113563613534022464 0ustar tomstaff00000000000000from distutils.version import LooseVersion import pytest import doctest from pytest_doctestplus.output_checker import OutputChecker, FLOAT_CMP pytest_plugins = ['pytester'] def test_ignored_whitespace(testdir): testdir.makeini( """ [pytest] doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE doctestplus = enabled """ ) p = testdir.makepyfile( """ class MyClass(object): ''' >>> a = "foo " >>> print(a) foo ''' pass """ ) reprec = testdir.inline_run(p, "--doctest-plus") reprec.assertoutcome(passed=1) def test_non_ignored_whitespace(testdir): testdir.makeini( """ [pytest] doctest_optionflags = ELLIPSIS doctestplus = enabled """ ) p = testdir.makepyfile( """ class MyClass(object): ''' >>> a = "foo " >>> print(a) foo ''' pass """ ) reprec = testdir.inline_run(p, "--doctest-plus") reprec.assertoutcome(failed=1, passed=0) def test_float_cmp(testdir): testdir.makeini( """ [pytest] doctest_optionflags = ELLIPSIS doctestplus = enabled """ ) p = testdir.makepyfile( """ def f(): ''' >>> x = 1/3. >>> x 0.333333 ''' fail def g(): ''' >>> x = 1/3. >>> x # doctest: +FLOAT_CMP 0.333333 ''' pass """ ) reprec = testdir.inline_run(p, "--doctest-plus") reprec.assertoutcome(failed=1, passed=1) def test_float_cmp_list(testdir): testdir.makeini( """ [pytest] doctest_optionflags = ELLIPSIS doctestplus = enabled """ ) p = testdir.makepyfile( """ def g(): ''' >>> x = [1/3., 2/3.] >>> x # doctest: +FLOAT_CMP [0.333333, 0.666666] ''' pass """ ) reprec = testdir.inline_run(p, "--doctest-plus") reprec.assertoutcome(failed=0, passed=1) def test_float_cmp_global(testdir): testdir.makeini(""" [pytest] doctest_optionflags = FLOAT_CMP doctestplus = enabled """) p = testdir.makepyfile(""" def f(): ''' >>> x = 1/3. >>> x 0.333333 ''' pass """) testdir.inline_run(p, "--doctest-plus").assertoutcome(passed=1) p = testdir.makepyfile(""" def f(): ''' >>> x = 2/7. >>> x 0.285714 ''' pass """) testdir.inline_run(p, "--doctest-plus").assertoutcome(passed=1) p = testdir.makepyfile(""" def f(): ''' >>> x = 1/13. >>> x 0.076923 ''' pass """) testdir.inline_run(p, "--doctest-plus").assertoutcome(passed=1) p = testdir.makepyfile(""" def f(): ''' >>> x = 1/13. >>> x 0.07692 ''' pass """) testdir.inline_run(p, "--doctest-plus").assertoutcome(failed=1) # not close enough def test_float_cmp_and_ellipsis(testdir): testdir.makeini( """ [pytest] doctest_optionflags = FLOAT_CMP ELLIPSIS doctestplus = enabled """) # whitespace is normalized by default p = testdir.makepyfile( """ from __future__ import print_function def f(): ''' >>> for char in ['A', 'B', 'C', 'D', 'E']: ... print(char, float(ord(char))) A 65.0 B 66.0 ... ''' pass """) testdir.inline_run(p, "--doctest-plus").assertoutcome(passed=1) p = testdir.makepyfile( """ from __future__ import print_function def f(): ''' >>> for char in ['A', 'B', 'C', 'D', 'E']: ... print(char, float(ord(char))) A 65.0 B 66.0 ... E 69.0 ''' pass """) testdir.inline_run(p, "--doctest-plus").assertoutcome(passed=1) p = testdir.makepyfile( """ from __future__ import print_function def f(): ''' >>> for char in ['A', 'B', 'C', 'D', 'E']: ... print(char, float(ord(char))) A 65.0 ... C 67.0 ... E 69.0 ''' pass """) testdir.inline_run(p, "--doctest-plus").assertoutcome(passed=1) p = testdir.makepyfile( """ from __future__ import print_function def f(): ''' >>> for char in ['A', 'B', 'C', 'D', 'E']: ... print(char, float(ord(char))) A 65.0 ... E 70.0 ''' pass """) testdir.inline_run(p, "--doctest-plus").assertoutcome(failed=1) def test_allow_bytes_unicode(testdir): testdir.makeini( """ [pytest] doctestplus = enabled """ ) # These are dummy tests just to check tht doctest-plus can parse the # ALLOW_BYTES and ALLOW_UNICODE options. It doesn't actually implement # these options. p = testdir.makepyfile( """ def f(): ''' >>> 1 # doctest: +ALLOW_BYTES 1 >>> 1 # doctest: +ALLOW_UNICODE 1 ''' pass """ ) reprec = testdir.inline_run(p, "--doctest-plus") reprec.assertoutcome(passed=1) class TestFloats: def test_normalize_floats(self): c = OutputChecker() got = "A 65.0\nB 66.0" want = "A 65.0\nB 66.0" assert c.normalize_floats(want, got, flags=FLOAT_CMP) want = "A 65.0\nB 66.0 " assert c.normalize_floats(want, got, flags=FLOAT_CMP | doctest.NORMALIZE_WHITESPACE) want = "A 65.0\nB 66.01" assert not c.normalize_floats(want, got, flags=FLOAT_CMP) def test_normalize_with_blank_line(self): c = OutputChecker() got = "\nA 65.0\nB 66.0" want = "\nA 65.0\nB 66.0" assert c.normalize_floats(want, got, flags=FLOAT_CMP) assert not c.normalize_floats(want, got, flags=FLOAT_CMP | doctest.DONT_ACCEPT_BLANKLINE) def test_normalize_with_ellipsis(self): c = OutputChecker() got = [] for char in ['A', 'B', 'C', 'D', 'E']: got.append('%s %s' % (char, float(ord(char)))) got = '\n'.join(got) want = "A 65.0\nB 66.0\n...G 70.0" assert not c.normalize_floats(want, got, flags=doctest.ELLIPSIS | FLOAT_CMP) want = "A 65.0\nB 66.0\n..." assert c.normalize_floats(want, got, flags=doctest.ELLIPSIS | FLOAT_CMP) want = "A 65.0\nB 66.0\n...\nE 69.0" assert c.normalize_floats(want, got, flags=doctest.ELLIPSIS | FLOAT_CMP) got = "\n" + got want = "\nA 65.0\nB 66.0\n..." assert c.normalize_floats(want, got, flags=doctest.ELLIPSIS | FLOAT_CMP) def test_partial_match(self): c = OutputChecker() assert not c.partial_match( ['1', '2', '3', '4'], [['2'], []], ) assert c.partial_match( ['1', '2', '3', '4'], [[], ['2'], []], ) assert c.partial_match( ['1', '2', '3', '4'], [['1', '2'], []], ) assert c.partial_match( ['1', '2', '3', '4'], [['1', '2'], ['4']], ) assert c.partial_match( ['1', '2', '3', '4', '5'], [['1', '2'], ['4', '5']], ) assert c.partial_match( ['1', '2', '3', '4', '5', '6'], [['1', '2'], ['4'], ['6']], ) assert c.partial_match( [str(i) for i in range(20)], [[], ['1', '2'], ['4'], ['6'], []], ) assert not c.partial_match( [str(i) for i in range(20)], [[], ['1', '2'], ['7'], ['6'], []], ) def test_requires(testdir): testdir.makeini( """ [pytest] doctestplus = enabled """) # should be ignored p = testdir.makefile( '.rst', """ .. doctest-requires:: foobar >>> import foobar """ ) testdir.inline_run(p, '--doctest-plus', '--doctest-rst').assertoutcome(passed=1) # should run as expected p = testdir.makefile( '.rst', """ .. doctest-requires:: sys >>> import sys """ ) testdir.inline_run(p, '--doctest-plus', '--doctest-rst').assertoutcome(passed=1) # testing this in case if doctest-requires just ignores everything and pass unconditionally p = testdir.makefile( '.rst', """ .. doctest-requires:: sys glob, re,math >>> import sys >>> assert 0 """ ) testdir.inline_run(p, '--doctest-plus', '--doctest-rst').assertoutcome(failed=1) # package with version is available p = testdir.makefile( '.rst', """ .. doctest-requires:: sys pytest>=1.0 >>> import sys, pytest """ ) testdir.inline_run(p, '--doctest-plus', '--doctest-rst').assertoutcome(passed=1) # package with version is not available p = testdir.makefile( '.rst', """ .. doctest-requires:: sys pytest<1.0 glob >>> import sys, pytest, glob >>> assert 0 """ ) # passed because 'pytest<1.0' was not satisfied and 'assert 0' was not evaluated testdir.inline_run(p, '--doctest-plus', '--doctest-rst').assertoutcome(passed=1) def test_ignore_warnings_module(testdir): # First check that we get a warning if we don't add the IGNORE_WARNINGS # directive p = testdir.makepyfile( """ def myfunc(): ''' >>> import warnings >>> warnings.warn('A warning occurred', UserWarning) ''' pass """) reprec = testdir.inline_run(p, "--doctest-plus", "-W error") reprec.assertoutcome(failed=1, passed=0) # Now try with the IGNORE_WARNINGS directive p = testdir.makepyfile( """ def myfunc(): ''' >>> import warnings >>> warnings.warn('A warning occurred', UserWarning) # doctest: +IGNORE_WARNINGS ''' pass """) reprec = testdir.inline_run(p, "--doctest-plus", "-W error") reprec.assertoutcome(failed=0, passed=1) def test_ignore_warnings_rst(testdir): # First check that we get a warning if we don't add the IGNORE_WARNINGS # directive p = testdir.makefile(".rst", """ :: >>> import warnings >>> warnings.warn('A warning occurred', UserWarning) """) reprec = testdir.inline_run(p, "--doctest-plus", "--doctest-rst", "--text-file-format=rst", "-W error") reprec.assertoutcome(failed=1, passed=0) # Now try with the IGNORE_WARNINGS directive p = testdir.makefile(".rst", """ :: >>> import warnings >>> warnings.warn('A warning occurred', UserWarning) # doctest: +IGNORE_WARNINGS """) reprec = testdir.inline_run(p, "--doctest-plus", "--doctest-rst", "--text-file-format=rst", "-W error") reprec.assertoutcome(failed=0, passed=1) def test_doctest_glob(testdir): testdir.makefile( '.rst', foo_1=">>> 1 + 1\n2", ) testdir.makefile( '.rst', foo_2=">>> 1 + 1\n2", ) testdir.makefile( '.txt', foo_3=">>> 1 + 1\n2", ) testdir.makefile( '.rst', bar_2=">>> 1 + 1\n2", ) testdir.inline_run().assertoutcome(passed=0) testdir.inline_run('--doctest-plus').assertoutcome(passed=0) testdir.inline_run('--doctest-plus', '--doctest-rst').assertoutcome(passed=3) testdir.inline_run( '--doctest-plus', '--doctest-rst', '--text-file-format', 'txt' ).assertoutcome(passed=1) testdir.inline_run( '--doctest-plus', '--doctest-glob', '*.rst' ).assertoutcome(passed=3) testdir.inline_run( '--doctest-plus', '--doctest-glob', '*.rst', '--doctest-glob', '*.txt' ).assertoutcome(passed=4) testdir.inline_run( '--doctest-plus', '--doctest-glob', 'foo_*.rst' ).assertoutcome(passed=2) testdir.inline_run( '--doctest-plus', '--doctest-glob', 'foo_*.txt' ).assertoutcome(passed=1) def test_text_file_comments(testdir): testdir.makefile( '.rst', foo_1=".. >>> 1 + 1\n3", ) testdir.makefile( '.tex', foo_2="% >>> 1 + 1\n3", ) testdir.makefile( '.txt', foo_3="# >>> 1 + 1\n3", ) testdir.inline_run( '--doctest-plus', '--doctest-glob', '*.rst', '--doctest-glob', '*.tex', '--doctest-glob', '*.txt' ).assertoutcome(passed=3) def test_text_file_comment_chars(testdir): # override default comment chars testdir.makeini( """ [pytest] text_file_extensions = .rst=# .tex=# """ ) testdir.makefile( '.rst', foo_1="# >>> 1 + 1\n3", ) testdir.makefile( '.tex', foo_2="# >>> 1 + 1\n3", ) testdir.inline_run( '--doctest-plus', '--doctest-glob', '*.rst', '--doctest-glob', '*.tex', '--doctest-glob', '*.txt' ).assertoutcome(passed=2) def test_ignore_option(testdir): testdir.makepyfile(foo=""" def f(): ''' >>> 1+1 2 ''' pass """) testdir.makepyfile(bar=""" def f(): ''' >>> 1+1 2 ''' pass """) testdir.makefile('.rst', foo='>>> 1+1\n2') testdir.inline_run('--doctest-plus').assertoutcome(passed=2) testdir.inline_run('--doctest-plus', '--doctest-rst').assertoutcome(passed=3) testdir.inline_run( '--doctest-plus', '--doctest-rst', '--ignore', '.' ).assertoutcome(passed=0) testdir.inline_run( '--doctest-plus', '--doctest-rst', '--ignore', 'bar.py' ).assertoutcome(passed=2) if LooseVersion('4.3.0') <= LooseVersion(pytest.__version__): def test_ignore_glob_option(testdir): testdir.makepyfile(foo=""" def f(): ''' >>> 1+1 2 ''' pass """) testdir.makepyfile(bar=""" def f(): ''' >>> 1+1 2 ''' pass """) testdir.makefile('.rst', foo='>>> 1+1\n2') testdir.inline_run( '--doctest-plus', '--doctest-rst', '--ignore-glob', 'foo*' ).assertoutcome(passed=1) testdir.inline_run( '--doctest-plus', '--doctest-rst', '--ignore-glob', 'bar*' ).assertoutcome(passed=2) testdir.inline_run( '--doctest-plus', '--doctest-rst', '--ignore-glob', '*.rst' ).assertoutcome(passed=2) def test_doctest_only(testdir, makepyfile, maketestfile, makerstfile): # regular python files with doctests makepyfile(p1='>>> 1 + 1\n2') makepyfile(p2='>>> 1 + 1\n3') # regular test files maketestfile(test_1='foo') maketestfile(test_2='bar') # rst files makerstfile(r1='>>> 1 + 1\n2') makerstfile(r3='>>> 1 + 1\n3') makerstfile(r2='>>> 1 + 2\n3') # regular tests testdir.inline_run().assertoutcome(passed=2) # regular + doctests testdir.inline_run("--doctest-plus").assertoutcome(passed=3, failed=1) # regular + doctests + doctest in rst files testdir.inline_run("--doctest-plus", "--doctest-rst").assertoutcome(passed=5, failed=2) # only doctests in python files, implicit usage of doctest-plus testdir.inline_run("--doctest-only").assertoutcome(passed=1, failed=1) # only doctests in python files testdir.inline_run("--doctest-only", "--doctest-rst").assertoutcome(passed=3, failed=2) pytest-doctestplus-0.5.0/tests/test_utils.py0000644000077000000240000000154713563613534021264 0ustar tomstaff00000000000000from pytest_doctestplus.utils import ModuleChecker class TestModuleChecker: def test_simple(self): c = ModuleChecker() assert c.check('sys') assert not c.check('foobar') def test_with_version(self): c = ModuleChecker() assert c.check('pytest>1.0') assert not c.check('foobar>1.0') def test_check_distribution(self): c = ModuleChecker() # in python3.4+ packages attribute will not be populated # because it calls 'pip freeze' which is slow if not c.packages: c.packages = c.get_packages() # after this we will be able to test _check_distribution even in # python3.4+ environment assert c._check_distribution('pytest>1.0') assert not c._check_distribution('pytest<1.0') assert not c._check_distribution('foobar>1.0')