Skip to content

Commit

Permalink
Adding :windows-skip: and :windows-only: options to doctest.
Browse files Browse the repository at this point in the history
Will likely add `:linux-only:` and `:os-x-only:` options too
so that the lines in the "Extra Dependencies" sections can be
tested in a non-brittle fashion.
  • Loading branch information
dhermes committed Oct 22, 2017
1 parent 02d9e11 commit 699e39b
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 27 deletions.
1 change: 1 addition & 0 deletions docs/conf.py
Expand Up @@ -58,6 +58,7 @@
'sphinx.ext.napoleon',
'sphinx_docstring_typing',
'custom_html_writer',
'doctest_monkeypatch',
]

# Add any paths that contain templates here, relative to this directory.
Expand Down
105 changes: 105 additions & 0 deletions docs/doctest_monkeypatch.py
@@ -0,0 +1,105 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Add some features to ``sphinx.ext.doctest``.
Does so by monkey-patching the ``option_spec`` in
* ``DoctestDirective``
* ``TestcodeDirective``
* ``TestoutputDirective``
with some extra options ``:windows-skip:`` and ``:windows-only:``.
Also monkey-patches ``TestDirective.run`` to honor these directives
and skip a test based on the options.
.. note::
This works with version(s) 1.6.4 of Sphinx, but may not work with
other versions (i.e. the monkey-patch relies on some knowledge of
the implementation).
"""

import doctest
import os

import docutils.parsers.rst
import sphinx.ext.doctest


IS_WINDOWS = os.name == 'nt'
WINDOWS_ONLY = 'windows-only'
WINDOWS_SKIP = 'windows-skip'
OLD_RUN = sphinx.ext.doctest.TestDirective.run


def custom_run(directive):
"""Custom over-ride for :meth:`.TestDirective.run`.
``directive`` acts like ``self`` when this function is bound to a class.
Helps to skip tests based on the directive options:
* ``windows-only``
* ``windows-skip``
Args:
directive (sphinx.ext.doctest.TestDirective): The currently active
Sphinx directive.
Returns:
docutils.nodes.Element: The element to be added.
"""
node, = OLD_RUN(directive)

if WINDOWS_ONLY in directive.options and WINDOWS_SKIP in directive.options:
raise RuntimeError(
'At most one option can be used among', WINDOWS_ONLY, WINDOWS_SKIP)

if WINDOWS_ONLY in directive.options:
if not IS_WINDOWS:
flag = doctest.OPTIONFLAGS_BY_NAME['SKIP']
node['options'][flag] = True # Skip the test

if WINDOWS_SKIP in directive.options:
if IS_WINDOWS:
flag = doctest.OPTIONFLAGS_BY_NAME['SKIP']
node['options'][flag] = True # Skip the test

return [node]


def setup(app):
"""Set-up this extension.
Args:
app (sphinx.application.Sphinx): A running Sphinx app.
"""
sphinx.ext.doctest.TestDirective.run = custom_run

options = (
WINDOWS_ONLY,
WINDOWS_SKIP,
)
directive_types = (
sphinx.ext.doctest.DoctestDirective,
sphinx.ext.doctest.TestcodeDirective,
sphinx.ext.doctest.TestoutputDirective,
)
for directive in directive_types:
option_spec = directive.option_spec
for option in options:
if option in option_spec:
raise RuntimeError(
'Unexpected option in option spec', option)
option_spec[option] = docutils.parsers.rst.directives.flag
44 changes: 21 additions & 23 deletions docs/native-libraries.rst
Expand Up @@ -39,14 +39,11 @@ The C headers for ``libbezier`` will be included in the installed package
.. testsetup:: show-headers, show-lib, show-pxd

import os
import platform
import textwrap

import bezier


PLATFORM_SYSTEM = platform.system().lower()

class Path(object):
"""This class is a hack for Windows.

Expand Down Expand Up @@ -94,17 +91,7 @@ The C headers for ``libbezier`` will be included in the installed package
directory = directory.path
# NOTE: We **always** use posix separator.
print(os.path.basename(directory) + '/')

if directory.endswith('lib') and PLATFORM_SYSTEM == 'windows':
# NOTE: This is a hack so that doctests can pass on Windows
# even though the directory contents differ. This hack
# assumes that the speedups have been installed.
assert os.path.isdir(directory)
assert os.listdir(directory) == ['bezier.lib']
full_tree = 'libbezier.a'
else:
full_tree = tree(directory, suffix=suffix)

full_tree = tree(directory, suffix=suffix)
print(textwrap.indent(full_tree, ' '))


Expand All @@ -115,16 +102,11 @@ The C headers for ``libbezier`` will be included in the installed package

# Monkey-patch functions to return a ``Path``.
original_get_include = bezier.get_include
original_get_lib = bezier.get_lib

def get_include():
return Path(original_get_include())

def get_lib():
return Path(original_get_lib())

bezier.get_include = get_include
bezier.get_lib = get_lib

# Allow this value to be re-used.
include_directory = get_include()
Expand All @@ -146,9 +128,8 @@ The C headers for ``libbezier`` will be included in the installed package

.. testcleanup:: show-headers, show-lib, show-pxd

# Restore the monkey-patched functions.
# Restore the monkey-patched function.
bezier.get_include = original_get_include
bezier.get_lib = original_get_lib

Note that this includes a catch-all ``bezier.h`` that just includes all of
the headers.
Expand Down Expand Up @@ -185,6 +166,7 @@ On Linux and Mac OS X, ``libbezier`` is included as a single static
library (i.e. a ``.a`` file):

.. doctest:: show-lib
:windows-skip:

>>> lib_directory = bezier.get_lib()
>>> lib_directory
Expand All @@ -201,8 +183,24 @@ library (i.e. a ``.a`` file):
``bezier`` can be installed in virtual environments, in different
Python versions, as an egg or wheel, and so on.

On Windows, an `import library`_ --- ``lib/bezier.lib`` --- is included
to specify the symbols in the **shared** library ``extra-dll/libbezier.dll``.
On Windows, an `import library`_ (i.e. a ``.lib`` file) is included to
specify the symbols in the Windows **shared** library (DLL):

.. doctest:: show-lib
:windows-only:

>>> lib_directory = bezier.get_lib()
>>> lib_directory
'.../site-packages/bezier/lib'
>>> print_tree(lib_directory)
lib/
bezier.lib
>>> dll_directory = bezier.get_dll()
>>> dll_directory
'.../site-packages/bezier/extra-dll'
>>> print_tree(dll_directory)
extra-dll/
libbezier.dll

.. _import library: https://docs.python.org/3/extending/windows.html#differences-between-unix-and-windows

Expand Down
32 changes: 28 additions & 4 deletions src/bezier/__init__.py
Expand Up @@ -22,6 +22,8 @@
:trim:
"""

import os

import pkg_resources

# NOTE: ``__config__`` **must** be the first import because it (may)
Expand Down Expand Up @@ -68,8 +70,8 @@ def get_include():
"""Get the directory with ``.h`` header files.
Extension modules (and Cython modules) that need to compile against
``bezier`` should use this function to locate the appropriate include
directory.
``libbezier`` should use this function to locate the appropriate
include directory.
For more information, see :doc:`../native-libraries`.
Expand All @@ -84,8 +86,8 @@ def get_lib():
"""Get the directory with ``.a`` / ``.lib`` static libraries.
Extension modules (and Cython modules) that need to compile against
the ``libbezier`` static library should use this function to locate the
appropriate lib directory.
``libbezier`` should use this function to locate the appropriate lib
directory.
For more information, see :doc:`../native-libraries`.
Expand All @@ -94,3 +96,25 @@ def get_lib():
``libbezier`` Fortran library.
"""
return pkg_resources.resource_filename('bezier', 'lib')


def get_dll():
"""Get the directory with the Windows shared library.
Extension modules (and Cython modules) that need to compile against
``libbezier`` should use this function to locate the appropriate
Windows shared library or libraries (DLLs).
For more information, see :doc:`../native-libraries`.
Returns:
str: ``extra-dll`` directory that contains the Windows shared library
for the ``libbezier`` Fortran library.
Raises:
OSError: If this function is used anywhere other than Windows.
"""
if os.name == 'nt':
return pkg_resources.resource_filename('bezier', 'extra-dll')
else:
raise OSError('This function should only be used on Windows.')
21 changes: 21 additions & 0 deletions tests/unit/test___init__.py
Expand Up @@ -13,6 +13,8 @@
import os
import unittest

import mock


CHECK_PKG_MSG = """\
path = {!r}
Expand Down Expand Up @@ -47,6 +49,25 @@ def test_it(self):
_check_pkg_filename(self, lib_directory, 'lib')


class Test_get_dll(unittest.TestCase):

@staticmethod
def _call_function_under_test():
import bezier

return bezier.get_dll()

@mock.patch('os.name', new='nt')
def test_windows(self):
dll_directory = self._call_function_under_test()
_check_pkg_filename(self, dll_directory, 'extra-dll')

@mock.patch('os.name', new='posix')
def test_non_windows(self):
with self.assertRaises(OSError):
self._call_function_under_test()


def _check_pkg_filename(test_case, path, last_segment):
short = os.path.join('bezier', last_segment)
from_egg = path.endswith(short) and '.egg' in path
Expand Down

0 comments on commit 699e39b

Please sign in to comment.