From 85250fdfded3ea94456e7eea327c296862c529f5 Mon Sep 17 00:00:00 2001 From: Danny Hermes Date: Mon, 23 Oct 2017 11:04:06 -0700 Subject: [PATCH] Updating `native-libraries` to use doctest for OS X "extra dependencies". This is part of an effort to keep the doctests from getting stale. In the process: - Added a special `scripts/osx/nox-install-for-doctest.sh` which will create a "delocated" wheel and install it when running `nox -s doctest` - Added `:linux-only:` and `:mac-os-x-only:` options for the `doctest` directive (in `doctest_monkeypatch.py`) Filed: https://github.com/sphinx-doc/sphinx/issues/4183 --- docs/doctest_monkeypatch.py | 44 +++++++++++++++------ docs/native-libraries.rst | 55 +++++++++++++++++++++----- nox.py | 11 +++++- scripts/osx/nox-install-for-doctest.sh | 37 +++++++++++++++++ 4 files changed, 123 insertions(+), 24 deletions(-) create mode 100755 scripts/osx/nox-install-for-doctest.sh diff --git a/docs/doctest_monkeypatch.py b/docs/doctest_monkeypatch.py index ff23e097..bad1d39c 100644 --- a/docs/doctest_monkeypatch.py +++ b/docs/doctest_monkeypatch.py @@ -18,7 +18,12 @@ * ``TestcodeDirective`` * ``TestoutputDirective`` -with some extra options ``:windows-skip:`` and ``:windows-only:``. +with some extra options: + +* ``:linux-only:`` +* ``:mac-os-x-only:`` +* ``:windows-skip:`` +* ``:windows-only:`` Also monkey-patches ``TestDirective.run`` to honor these directives and skip a test based on the options. @@ -32,15 +37,27 @@ import doctest import os +import sys import docutils.parsers.rst import sphinx.ext.doctest IS_WINDOWS = os.name == 'nt' +IS_LINUX = sys.platform in ('linux', 'linux2') +IS_MAC_OS_X = sys.platform == 'darwin' +LINUX_ONLY = 'linux-only' +MAC_OS_X_ONLY = 'mac-os-x-only' WINDOWS_ONLY = 'windows-only' WINDOWS_SKIP = 'windows-skip' +OPTION_NAMES = ( + LINUX_ONLY, + MAC_OS_X_ONLY, + WINDOWS_ONLY, + WINDOWS_SKIP, +) OLD_RUN = sphinx.ext.doctest.TestDirective.run +SKIP_FLAG = doctest.OPTIONFLAGS_BY_NAME['SKIP'] def custom_run(directive): @@ -62,19 +79,26 @@ def custom_run(directive): """ node, = OLD_RUN(directive) - if WINDOWS_ONLY in directive.options and WINDOWS_SKIP in directive.options: + num_options = sum(1 for name in OPTION_NAMES if name in directive.options) + if num_options > 1: raise RuntimeError( - 'At most one option can be used among', WINDOWS_ONLY, WINDOWS_SKIP) + 'At most one option can be used among', *OPTION_NAMES) + + if LINUX_ONLY in directive.options: + if not IS_LINUX: + node['options'][SKIP_FLAG] = True + + if MAC_OS_X_ONLY in directive.options: + if not IS_MAC_OS_X: + node['options'][SKIP_FLAG] = True if WINDOWS_ONLY in directive.options: if not IS_WINDOWS: - flag = doctest.OPTIONFLAGS_BY_NAME['SKIP'] - node['options'][flag] = True # Skip the test + node['options'][SKIP_FLAG] = True if WINDOWS_SKIP in directive.options: if IS_WINDOWS: - flag = doctest.OPTIONFLAGS_BY_NAME['SKIP'] - node['options'][flag] = True # Skip the test + node['options'][SKIP_FLAG] = True return [node] @@ -87,10 +111,6 @@ def setup(app): """ sphinx.ext.doctest.TestDirective.run = custom_run - options = ( - WINDOWS_ONLY, - WINDOWS_SKIP, - ) directive_types = ( sphinx.ext.doctest.DoctestDirective, sphinx.ext.doctest.TestcodeDirective, @@ -98,7 +118,7 @@ def setup(app): ) for directive in directive_types: option_spec = directive.option_spec - for option in options: + for option in OPTION_NAMES: if option in option_spec: raise RuntimeError( 'Unexpected option in option spec', option) diff --git a/docs/native-libraries.rst b/docs/native-libraries.rst index 886c0748..98b19a7b 100644 --- a/docs/native-libraries.rst +++ b/docs/native-libraries.rst @@ -36,7 +36,7 @@ C Headers The C headers for ``libbezier`` will be included in the installed package -.. testsetup:: show-headers, show-lib, show-dll, show-pxd +.. testsetup:: show-headers, show-lib, show-dll, show-pxd, os-x-dylibs import os import textwrap @@ -117,6 +117,10 @@ The C headers for ``libbezier`` will be included in the installed package # Allow this value to be re-used. include_directory = get_include() + # OS X specific. + base_dir = os.path.dirname(include_directory.path) + dylibs_directory = os.path.join(base_dir, '.dylibs') + .. doctest:: show-headers >>> include_directory = bezier.get_include() @@ -132,7 +136,7 @@ The C headers for ``libbezier`` will be included in the installed package surface.h bezier.h -.. testcleanup:: show-headers, show-lib, show-dll, show-pxd +.. testcleanup:: show-headers, show-lib, show-dll, show-pxd, os-x-dylibs # Restore the monkey-patched functions. bezier.get_include = original_get_include @@ -275,19 +279,47 @@ Mac OS X The command line tool `delocate`_ adds a ``bezier/.dylibs`` directory with copies of ``libgfortran``, ``libquadmath`` and ``libgcc_s``: -.. code-block:: console +.. doctest:: os-x-dylibs + :mac-os-x-only: - $ cd .../site-packages/bezier/.dylibs - $ ls -1 - libgcc_s.1.dylib - libgfortran.4.dylib - libquadmath.0.dylib + >>> dylibs_directory + '.../site-packages/bezier/.dylibs' + >>> print_tree(dylibs_directory) + .dylibs/ + libgcc_s.1.dylib + libgfortran.4.dylib + libquadmath.0.dylib For example, the ``_curve_speedup`` module depends on the local copy of ``libgfortran``: -.. code-block:: console +.. testsetup:: os-x-extension, os-x-delocated-libgfortran + + import os + import subprocess + + import bezier + + + def invoke_shell(*args): + print('$ ' + ' '.join(args)) + prev_cwd = os.getcwd() + os.chdir(bezier_directory) + # NOTE: We print to the stdout of the doctest, rather than using + # `subprocess.call()` directly. + output_bytes = subprocess.check_output(args).rstrip() + print(output_bytes.decode('utf-8')) + os.chdir(prev_cwd) + + bezier_directory = os.path.dirname(bezier.__file__) + +.. doctest:: os-x-extension + :options: +NORMALIZE_WHITESPACE + :mac-os-x-only: + :pyversion: >= 3.6 + + >>> invoke_shell('otool', '-L', '_curve_speedup.cpython-36m-darwin.so') $ otool -L _curve_speedup.cpython-36m-darwin.so _curve_speedup.cpython-36m-darwin.so: @loader_path/.dylibs/libgfortran.4.dylib (...) @@ -297,8 +329,11 @@ Though the Python extension modules (``.so`` files) only depend on ``libgfortran``, they indirectly depend on ``libquadmath`` and ``libgcc_s``: -.. code-block:: console +.. doctest:: os-x-delocated-libgfortran + :options: +NORMALIZE_WHITESPACE + :mac-os-x-only: + >>> invoke_shell('otool', '-L', '.dylibs/libgfortran.4.dylib') $ otool -L .dylibs/libgfortran.4.dylib .dylibs/libgfortran.4.dylib: /DLC/bezier/libgfortran.4.dylib (...) diff --git a/nox.py b/nox.py index 80a1e6ce..b74bfe66 100644 --- a/nox.py +++ b/nox.py @@ -15,6 +15,7 @@ import glob import os import shutil +import sys import tempfile import nox @@ -22,7 +23,9 @@ NUMPY = 'numpy >= 1.13.3' -if os.name == 'nt': +IS_MAC_OS_X = sys.platform == 'darwin' +IS_WINDOWS = os.name == 'nt' +if IS_WINDOWS: # Windows wheels don't exist until 1.0.0. SCIPY = 'scipy >= 1.0.0rc1' else: @@ -198,7 +201,11 @@ def doctest(session): local_deps = DOCS_DEPS + (MOCK_DEP,) session.install(*local_deps) # Install this package. - session.install('.') + if IS_MAC_OS_X: + command = get_path('scripts', 'osx', 'nox-install-for-doctest.sh') + session.run(command) + else: + session.install('.') # Run the script for building docs and running doctests. run_args = get_doctest_args(session) diff --git a/scripts/osx/nox-install-for-doctest.sh b/scripts/osx/nox-install-for-doctest.sh new file mode 100755 index 00000000..a87a2241 --- /dev/null +++ b/scripts/osx/nox-install-for-doctest.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# +# 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. + +set -e -x + +# 0. Install the ``delocate`` tool. +pip install --upgrade delocate + +# 1. Build the wheel from source. +BASIC_DIR=$(mktemp -d) +pip wheel . --wheel-dir ${BASIC_DIR} + +# 2. "delocate" the built wheel. +DELOCATED_DIR=$(mktemp -d) +# NOTE: This intentionally does not use ``--check-archs``. +delocate-wheel \ + --wheel-dir ${DELOCATED_DIR} \ + --verbose \ + ${BASIC_DIR}/bezier*.whl + +# 3. Install from the "delocated" wheel. +pip install ${DELOCATED_DIR}/bezier*.whl + +# Clean up temporary directories. +rm -fr ${BASIC_DIR} +rm -fr ${DELOCATED_DIR}