Source code for sphinx_pyproject

#!/usr/bin/env python3
#
#  __init__.py
"""
Move some of your Sphinx configuration into ``pyproject.toml``.
"""
#
#  Copyright © 2021 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in all
#  copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
#  OR OTHER DEALINGS IN THE SOFTWARE.
#

# stdlib
import re
from typing import Any, Dict, Iterator, List, Mapping, MutableMapping, Optional

# 3rd party
import dom_toml
from dom_toml.decoder import TomlPureDecoder
from dom_toml.parser import TOML_TYPES, AbstractConfigParser, BadConfigError
from domdf_python_tools.paths import PathPlus
from domdf_python_tools.typing import PathLike
from domdf_python_tools.words import word_join

__author__: str = "Dominic Davis-Foster"
__copyright__: str = "2021 Dominic Davis-Foster"
__license__: str = "MIT License"
__version__: str = "0.3.0"
__email__: str = "dominic@davis-foster.co.uk"

__all__ = ["SphinxConfig", "ProjectParser", "PoetryProjectParser"]


[docs]class SphinxConfig(Mapping[str, Any]): """ Read the Sphinx configuration from ``pyproject.toml``. :param pyproject_file: The path to the ``pyproject.toml`` file. :param globalns: The global namespace of the ``conf.py`` file. The variables parsed from the ``[tool.sphinx-pyproject]`` table will be added to this namespace. By default, or if explicitly :py:obj:`None`, this does not happen. :no-default globalns: :param style: Either ``pep621`` (default), or ``poetry`` to read configuration from the ``[tool.poetry]`` table. :no-default style: :param config_overrides: Custom configuration overrides. This parameter can be used to dynamically update values from ``pyproject.toml``. This can be used to patch dynamic values like ``version``. By default, or if explicitly :py:obj:`None`, no config updates are performed. :no-default config_overrides: .. versionchanged:: 0.2.0 Added the ``style`` keyword argument. .. versionchanged:: 0.3.0 Added the ``config_overrides`` keyword argument. .. autosummary-widths:: 1/4 """ name: str """ The value of the :pep621:`project.name <name>` key in the :pep:`621` metadata. Underscores are replaced by dashes but :pep:`508` normalization is *not* applied. The recommendation is to assign this to the `project <https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-project>`_ variable in ``conf.py``: .. code-block:: python from sphinx_pyproject import SphinxConfig config = SphinxConfig() project = config.name """ version: str """ The value of the :pep621:`project.version <version>` key in the :pep:`621` metadata. Converted to a string if the value was a number in the ``pyproject.toml`` file. """ description: str """ The value of the :pep621:`project.description <description>` key in the :pep:`621` metadata. """ author: str """ A string giving the names of the authors. This is parsed from the :pep621:`project.authors <authors>` key in the :pep:`621` metadata, or the :pep621:`project.maintainers <maintainers>` key as a fallback. The names are joined together, e.g.: .. code-block:: TOML # pyproject.toml [[project.authors]] name = "Dominic Davis-Foster" [[project.authors]] name = "Joe Bloggs" [[project.authors]] name = "Jane Doe" .. code-block:: python >>> SphinxConfig("pyproject.toml").author 'Dominic Davis-Foster, Joe Bloggs and Jane Doe' """ def __init__( self, pyproject_file: PathLike = "../pyproject.toml", *, globalns: Optional[MutableMapping] = None, style: str = "pep621", config_overrides: Optional[MutableMapping] = None, ): pyproject_file = PathPlus(pyproject_file).abspath() config = dom_toml.load(pyproject_file, decoder=TomlPureDecoder) parser_cls = project_parser_styles.get(style) if parser_cls is None: styles = ", ".join(project_parser_styles) raise ValueError(f"'style' argument must be one of: {styles}") namespace = parser_cls.get_namespace(pyproject_file, config) pep621_config = parser_cls().parse(namespace) for key, value in (config_overrides or {}).items(): pep621_config[key] = value for key in ("name", "version", "description"): if key not in pep621_config: raise BadConfigError( f"Either {key!r} was not declared in the 'project' table " f"or it was marked as 'dynamic', which is unsupported by 'sphinx-pyproject'." ) if "author" not in pep621_config: raise BadConfigError( f"Either 'authors/maintainers' was not declared in the 'project' table " f"or it was marked as 'dynamic', which is unsupported by 'sphinx-pyproject'." ) self.name = pep621_config["name"] self.version = pep621_config["version"] self.description = pep621_config["description"] self.author = pep621_config["author"] self._freeform = config.get("tool", {}).get("sphinx-pyproject", {}) if globalns is not None: globalns.update(pep621_config) globalns.update(self._freeform)
[docs] def __getitem__(self, item: str) -> Any: """ Returns the value of the given key in the ``tool.sphinx-pyproject`` table. :param item: """ return self._freeform[item]
[docs] def __len__(self) -> int: """ Returns the number of keys in the ``tool.sphinx-pyproject`` table. """ return len(self._freeform)
[docs] def __iter__(self) -> Iterator[str]: """ Returns an iterator over the keys in the ``tool.sphinx-pyproject`` table. """ yield from self._freeform
[docs]class ProjectParser(AbstractConfigParser): """ Parser for :pep:`621` metadata from ``pyproject.toml``. .. autosummary-widths:: 7/16 """
[docs] @staticmethod def get_namespace(filename: PathPlus, config: Dict[str, TOML_TYPES]) -> Dict[str, TOML_TYPES]: """ Returns the ``[project]`` table in a ``project.toml`` file. :param filename: The filename the TOML data was read from. Used in error messages. :param config: The data from the TOML file. .. versionadded:: 0.2.0 """ if "project" not in config: raise BadConfigError(f"No 'project' table found in {filename.as_posix()}") return config["project"]
[docs] def parse_name(self, config: Dict[str, TOML_TYPES]) -> str: """ Parse the :pep621:`name` key. :param config: The unparsed TOML config for the ``[project]`` table. """ name = config["name"] self.assert_type(name, str, ["project", "name"]) return str(name).replace('_', '-')
[docs] def parse_version(self, config: Dict[str, TOML_TYPES]) -> str: """ Parse the :pep621:`version` key. :param config: The unparsed TOML config for the ``[project]`` table. """ version = config["version"] self.assert_type(version, (str, int), ["project", "version"]) return str(version)
[docs] def parse_description(self, config: Dict[str, TOML_TYPES]) -> str: """ Parse the :pep621:`description` key. :param config: The unparsed TOML config for the ``[project]`` table. """ description = config["description"] self.assert_type(description, str, ["project", "description"]) return description
[docs] @staticmethod def parse_author(config: Dict[str, TOML_TYPES]) -> str: """ Parse the :pep621:`authors/maintainers` key. :param config: The unparsed TOML config for the ``[project]`` table. """ all_authors: List[Optional[str]] = [] for idx, author in enumerate(config["author"]): name = author.get("name", None) if name is not None and ',' in name: raise BadConfigError(f"The 'project.authors[{idx}].name' key cannot contain commas.") all_authors.append(name) all_authors = list(filter(bool, all_authors)) if not all_authors: raise BadConfigError(f"The 'project.authors' key cannot be empty.") return word_join(all_authors) # type: ignore
@property def keys(self) -> List[str]: """ The keys to parse from the TOML file. """ return [ "name", "version", "description", "author", ]
[docs] def parse( self, config: Dict[str, TOML_TYPES], set_defaults: bool = False, ) -> Dict[str, TOML_TYPES]: """ Parse the TOML configuration. :param config: :param set_defaults: Has no effect in this class. """ if "authors" in config: config["author"] = config.pop("authors") elif "maintainers" in config: config["author"] = config.pop("maintainers") return super().parse(config)
[docs]class PoetryProjectParser(ProjectParser): """ Parser for poetry metadata from ``pyproject.toml``. .. versionadded:: 0.2.0 """
[docs] @staticmethod def get_namespace(filename: PathPlus, config: Dict[str, TOML_TYPES]) -> Dict[str, TOML_TYPES]: """ Returns the ``[tool.poetry]`` table in a ``project.toml`` file. :param filename: The filename the TOML data was read from. Used in error messages. :param config: The data from the TOML file. """ result = config.get("tool", {}).get("poetry") if result is None: raise BadConfigError(f"No 'tool.poetry' table found in {filename.as_posix()}") return result
[docs] @staticmethod def parse_author(config: Dict[str, TOML_TYPES]) -> str: """ Parse poetry's authors key. :param config: The unparsed TOML config for the ``[tool.poetry]`` table. """ pep621_style_authors: List[Dict[str, str]] = [] for author in config["author"]: match = re.match(r"(?P<name>.*)<(?P<email>.*)>", author) if match: name = match.group("name").strip() email = match.group("email").strip() pep621_style_authors.append({"name": name, "email": email}) return ProjectParser.parse_author({"author": pep621_style_authors})
project_parser_styles = { "pep621": ProjectParser, "poetry": PoetryProjectParser, }