summaryrefslogtreecommitdiff
path: root/packaging/pep517_backend/_backend.py
blob: d4d10f2364c4627f26aa294b8e8c4e9120e8f975 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
"""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`
        'straight.plugin',  # used in `hacking/build-ansible.py` for subcommand
        '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