"""PEP 517 build backend wrapper for optionally pre-building docs for sdist.""" from __future__ import annotations import os import re import subprocess import sys import typing as t from configparser import ConfigParser from contextlib import contextmanager, suppress from importlib.metadata import import_module from io import StringIO from pathlib import Path from shutil import copytree from tempfile import TemporaryDirectory try: from contextlib import chdir as _chdir_cm except ImportError: @contextmanager def _chdir_cm(path: os.PathLike) -> t.Iterator[None]: original_wd = Path.cwd() os.chdir(path) try: yield finally: os.chdir(original_wd) from setuptools.build_meta import ( build_sdist as _setuptools_build_sdist, get_requires_for_build_sdist as _setuptools_get_requires_for_build_sdist, ) with suppress(ImportError): # NOTE: Only available for sdist builds that bundle manpages. Declared by # NOTE: `get_requires_for_build_sdist()` when `--build-manpages` is passed. from docutils.core import publish_file from docutils.writers import manpage __all__ = ( # noqa: WPS317, WPS410 'build_sdist', 'get_requires_for_build_sdist', ) BUILD_MANPAGES_CONFIG_SETTING = '--build-manpages' """Config setting name toggle that is used to request manpage in sdists.""" @contextmanager def _run_in_temporary_directory() -> t.Iterator[Path]: with TemporaryDirectory(prefix='.tmp-ansible-pep517-') as tmp_dir: with _chdir_cm(tmp_dir): yield Path(tmp_dir) def _make_in_tree_ansible_importable() -> None: """Add the library directory to module lookup paths.""" lib_path = str(Path.cwd() / 'lib/') sys.path.insert(0, lib_path) # NOTE: for the current runtime session def _get_package_distribution_version() -> str: """Retrieve the current version number from setuptools config.""" setup_cfg_path = Path.cwd() / 'setup.cfg' setup_cfg = ConfigParser() setup_cfg.read_string(setup_cfg_path.read_text()) cfg_version = setup_cfg.get('metadata', 'version') importable_version_str = cfg_version.removeprefix('attr: ') version_mod_str, version_var_str = importable_version_str.rsplit('.', 1) _make_in_tree_ansible_importable() return getattr(import_module(version_mod_str), version_var_str) def _generate_rst_in_templates() -> t.Iterable[Path]: """Create ``*.1.rst.in`` files out of CLI Python modules.""" generate_man_cmd = ( sys.executable, 'hacking/build-ansible.py', 'generate-man', '--template-file=docs/templates/man.j2', '--output-dir=docs/man/man1/', '--output-format=man', *Path('lib/ansible/cli/').glob('*.py'), ) subprocess.check_call(tuple(map(str, generate_man_cmd))) return Path('docs/man/man1/').glob('*.1.rst.in') def _convert_rst_in_template_to_manpage( rst_doc_template: str, destination_path: os.PathLike, version_number: str, ) -> None: """Render pre-made ``*.1.rst.in`` templates into manpages. This includes pasting the hardcoded version into the resulting files. The resulting ``in``-files are wiped in the process. """ templated_rst_doc = rst_doc_template.replace('%VERSION%', version_number) with StringIO(templated_rst_doc) as in_mem_rst_doc: publish_file( source=in_mem_rst_doc, destination_path=destination_path, writer=manpage.Writer(), ) def build_sdist( # noqa: WPS210, WPS430 sdist_directory: os.PathLike, config_settings: dict[str, str] | None = None, ) -> str: build_manpages_requested = BUILD_MANPAGES_CONFIG_SETTING in ( config_settings or {} ) original_src_dir = Path.cwd().resolve() with _run_in_temporary_directory() as tmp_dir: tmp_src_dir = Path(tmp_dir) / 'src' copytree(original_src_dir, tmp_src_dir) os.chdir(tmp_src_dir) if build_manpages_requested: Path('docs/man/man1/').mkdir(exist_ok=True, parents=True) version_number = _get_package_distribution_version() for rst_in in _generate_rst_in_templates(): _convert_rst_in_template_to_manpage( rst_doc_template=rst_in.read_text(), destination_path=rst_in.with_suffix('').with_suffix(''), version_number=version_number, ) rst_in.unlink() Path('pyproject.toml').write_text( re.sub( r"""(?x) backend-path\s=\s\[ # value is a list of double-quoted strings [^]]+ ].*\n build-backend\s=\s"[^"]+".*\n # value is double-quoted """, 'build-backend = "setuptools.build_meta"\n', Path('pyproject.toml').read_text(), ) ) built_sdist_basename = _setuptools_build_sdist( sdist_directory=sdist_directory, config_settings=config_settings, ) return built_sdist_basename def get_requires_for_build_sdist( config_settings: dict[str, str] | None = None, ) -> list[str]: build_manpages_requested = BUILD_MANPAGES_CONFIG_SETTING in ( config_settings or {} ) build_manpages_requested = True # FIXME: Once pypa/build#559 is addressed. manpage_build_deps = [ 'docutils', # provides `rst2man` 'jinja2', # used in `hacking/build-ansible.py generate-man` 'pyyaml', # needed for importing in-tree `ansible-core` from `lib/` ] if build_manpages_requested else [] return _setuptools_get_requires_for_build_sdist( config_settings=config_settings, ) + manpage_build_deps