Source code for sphinxcontrib.extras_require.sources

#!/usr/bin/env python3
#
#  sources.py
"""
Supported sources for the requirements are implemented here.

.. TODO:: From .dist-info

**Data:**

.. autosummary::

	~sphinxcontrib.extras_require.sources.sources

"""
#
#  Copyright © 2020-2021 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
#  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.
#
#  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.
#

# stdlib
import importlib.util
import inspect
import mimetypes
import pathlib
from typing import Callable, Dict, List, Tuple

# 3rd party
import sphinx.environment
from docutils.parsers.rst import directives
from domdf_python_tools.paths import PathPlus
from setuptools.config import read_configuration  # type: ignore[import]
from shippinglabel import normalize_keep_dot
from shippinglabel.requirements import combine_requirements, parse_pyproject_extras, read_requirements
from sphinx_toolbox.utils import flag

__all__ = [
		"requirements_from_file",
		"requirements_from_pkginfo",
		"requirements_from_setup_cfg",
		"requirements_from_flit",
		"requirements_from_pyproject",
		"sources",
		"Sources",
		]


[docs]class Sources(List[Tuple[str, Callable, Callable]]): """ Class to store functions that provide requirements sources. The syntax of each entry is:: (option_name, getter_function, validator_function) * a string to use in the directive to specify the source to use, * the function that returns the list of additional requirements, * a function to validate the option value provided by the user. .. latex:clearpage:: """ _args = ["package_root", "options", "env", "extra"] _directive_name = "extras_require"
[docs] def register( self, option_name: str, validator: Callable = directives.unchanged, ) -> Callable: """ Decorator to register a function. The function must have the following signature: .. code-block:: python def function( package_root: pathlib.Path, options: Dict, env: sphinx.environment.BuildEnvironment, extra: str, ) -> List[str]: ... :param option_name: A string to use in the directive to specify the source to use. :param validator: A function to validate the option value provided by the user. :return: The registered function. :raises: :exc:`SyntaxError` if the decorated function does not take the correct arguments. """ def _decorator(function: Callable) -> Callable: signature = inspect.signature(function) if list(signature.parameters.keys()) != self._args: raise SyntaxError( "The decorated function must take only the following arguments: " "'package_root', 'options', 'env', and 'extra'" ) self.append((option_name, function, validator)) setattr(function, f"_{self._directive_name}_registered", True) return function return _decorator
#: Instance of :class:`~.Sources`. sources = Sources()
[docs]@sources.register("file", directives.unchanged) def requirements_from_file( package_root: pathlib.Path, options: Dict, env: sphinx.environment.BuildEnvironment, extra: str, ) -> List[str]: """ Load requirements from the specified file. :param package_root: The path to the package root :param options: :param env: :param extra: The name of the "extra" that the requirements are for :return: List of requirements """ requirements_file = package_root / options["file"] if not requirements_file.is_file(): raise FileNotFoundError(f"Cannot find requirements file '{requirements_file}'") mime_type = mimetypes.guess_type(str(requirements_file))[0] if not mime_type or not mime_type.startswith("text/"): raise ValueError(f"'{requirements_file}' is not a text file.") requirements, comments = read_requirements( requirements_file, normalize_func=normalize_keep_dot, ) return list(map(str, sorted(combine_requirements(requirements))))
[docs]@sources.register("__pkginfo__", flag) def requirements_from_pkginfo( package_root: pathlib.Path, options: Dict, env: sphinx.environment.BuildEnvironment, extra: str, ) -> List[str]: """ Load requirements from a ``__pkginfo__.py`` file in the root of the repository. :param package_root: The path to the package root :param options: :param env: :param extra: The name of the "extra" that the requirements are for :return: List of requirements """ __pkginfo___file = PathPlus(env.srcdir).parent / "__pkginfo__.py" if not __pkginfo___file.is_file(): raise FileNotFoundError(f"Cannot find __pkginfo__.py in '{__pkginfo___file.parent}'") try: spec = importlib.util.spec_from_file_location("__pkginfo__", str(__pkginfo___file)) if spec is not None: __pkginfo__ = importlib.util.module_from_spec(spec) if spec.loader: spec.loader.exec_module(__pkginfo__) requirements = __pkginfo__.extras_require[extra] return requirements # TODO: handle extra not found except ValueError: pass except SyntaxError as e: if e.msg != "source code string cannot contain null bytes": raise e raise ImportError("Could not import __pkginfo__.py")
requirements_from___pkginfo__ = requirements_from_pkginfo
[docs]@sources.register("setup.cfg", flag) def requirements_from_setup_cfg( package_root: pathlib.Path, options: Dict, env: sphinx.environment.BuildEnvironment, extra: str, ) -> List[str]: """ Load requirements from a ``setup.cfg`` file in the root of the repository. :param package_root: The path to the package root. :param options: :param env: :param extra: The name of the "extra" that the requirements are for. :return: List of requirements. """ setup_cfg_file = PathPlus(env.srcdir).parent / "setup.cfg" assert setup_cfg_file.is_file() setup_cfg = read_configuration(setup_cfg_file) if "options" in setup_cfg and "extras_require" in setup_cfg["options"]: if extra in setup_cfg["options"]["extras_require"]: return setup_cfg["options"]["extras_require"][extra] else: raise ValueError(f"'{extra}' not found in '[options.extras_require]'") else: raise ValueError("'options.extras_require' section not found in 'setup.cfg")
[docs]@sources.register("flit", flag) def requirements_from_flit( package_root: pathlib.Path, options: Dict, env: sphinx.environment.BuildEnvironment, extra: str, ) -> List[str]: """ Load requirements from the ``[tool.flit.metadata.requires-extra]`` section of a ``pyproject.toml`` file in the root of the repository. :param package_root: The path to the package root. :param options: :param env: :param extra: The name of the "extra" that the requirements are for. :return: List of requirements. """ # noqa D400 pyproject_file = PathPlus(env.srcdir).parent / "pyproject.toml" if not pyproject_file.is_file(): raise FileNotFoundError(f"Cannot find pyproject.toml in '{pyproject_file.parent}'") flit_extras = parse_pyproject_extras(pyproject_file, flavour="flit", normalize_func=normalize_keep_dot) if extra not in flit_extras: raise ValueError(f"'{extra}' not found in '[tool.flit.metadata.requires-extra]'") requirements = flit_extras[extra] return list(map(str, sorted(combine_requirements(requirements))))
[docs]@sources.register("pyproject", flag) def requirements_from_pyproject( package_root: pathlib.Path, options: Dict, env: sphinx.environment.BuildEnvironment, extra: str, ) -> List[str]: """ Load requirements from the ``[project.optional-dependencies]`` section of a ``pyproject.toml`` file in the root of the repository. .. seealso:: :pep:`621` -- Storing project metadata in pyproject.toml .. versionadded:: 0.3.0 :param package_root: The path to the package root. :param options: :param env: :param extra: The name of the "extra" that the requirements are for. :return: List of requirements. """ # noqa D400 pyproject_file = PathPlus(env.srcdir).parent / "pyproject.toml" if not pyproject_file.is_file(): raise FileNotFoundError(f"Cannot find pyproject.toml in '{pyproject_file.parent}'") flit_extras = parse_pyproject_extras(pyproject_file, flavour="pep621", normalize_func=normalize_keep_dot) if extra not in flit_extras: raise ValueError(f"'{extra}' not found in '[project.optional-dependencies]'") requirements = flit_extras[extra] return list(map(str, sorted(combine_requirements(requirements))))