Coverage for pycommons / dev / building / make_dist.py: 98%
96 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 03:04 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 03:04 +0000
1"""Make the distribution."""
3from argparse import ArgumentParser
4from configparser import ConfigParser
5from itertools import chain
6from typing import Final
8from pycommons.dev.building.build_info import (
9 BuildInfo,
10 parse_project_arguments,
11)
12from pycommons.dev.doc.doc_info import DocInfo, load_doc_info_from_setup_cfg
13from pycommons.io.arguments import pycommons_argparser
14from pycommons.io.console import logger
15from pycommons.io.path import UTF8, Path, delete_path, write_lines
16from pycommons.io.temp import temp_dir, temp_file
17from pycommons.processes.python import PYTHON_INTERPRETER
18from pycommons.processes.shell import STREAM_FORWARD, Command
19from pycommons.types import type_error
21#: the prefix commands
22__PRE_PREFIX: Final[tuple[str, ...]] = (
23 "#!/bin/bash", "set -o pipefail", "set -o errtrace", "set -o nounset",
24 "set -o errexit")
26#: the prefix commands
27__PREFIX: Final[tuple[str, ...]] = (
28 'echo "Creating virtual environment."',
29 'python3 -m venv --copies "{VENV}"',
30 'echo "Activating virtual environment."',
31 'source "{VENV}/bin/activate"',
32)
34#: the suffix commands
35__SUFFIX: Final[tuple[str, ...]] = (
36 'echo "Deactivating virtual environment."',
37 "deactivate",
38 'echo "Done."',
39)
42#: the virtual environment commands
43__VENV_CMD: Final[tuple[tuple[str, tuple[str, ...]], ...]] = (
44 ("gz distribution with extras", (
45 'echo "Installing {GZ_DIST}{EXTRAS} and capturing {REQUIREMENTS}."',
46 'python3 -m pip --no-input --timeout {TIMEOUT} --retries 100 '
47 '--require-virtualenv install "{GZ_DIST}{EXTRAS}"',
48 'echo "Freezing requirements to {REQUIREMENTS}."',
49 'pip freeze --all --require-virtualenv '
50 '--no-input > "{REQUIREMENTS}"')),
51 ("wheel distribution with extras", (
52 'echo "Installing {WHEEL_DIST}{EXTRAS}."',
53 'python3 -m pip --no-input --timeout {TIMEOUT} --retries 100 '
54 '--require-virtualenv install "{WHEEL_DIST}{EXTRAS}"')),
55 ("gz distribution without extras", (
56 'echo "Installing {GZ_DIST} without extras."',
57 'python3 -m pip --no-input --timeout {TIMEOUT} --retries 100 '
58 '--require-virtualenv install "{GZ_DIST}"')),
59 ("wheel distribution without extras", (
60 'echo "Installing {WHEEL_DIST} without extras."',
61 'python3 -m pip --no-input --timeout {TIMEOUT} --retries 100 '
62 '--require-virtualenv install "{WHEEL_DIST}{EXTRAS}"')))
64#: the prefix commands
65__XZ: Final[tuple[str, ...]] = (
66 'tar --dereference --exclude=".nojekyll" -c --transform '
67 '"s,^,{BASE}/," * | xz -v -9e -c > "{DEST}"',
68)
71def __get_extras(setup_cfg: Path) -> list[str]:
72 """
73 Get all package extras.
75 :param setup_cfg: the `setup.cfg` file
76 :returns: the set of extras
78 >>> root = Path(__file__).up(4)
79 >>> from contextlib import redirect_stdout
80 >>> with redirect_stdout(None):
81 ... ex = __get_extras(root.resolve_inside("setup.cfg"))
82 >>> print(ex)
83 ['dev']
84 """
85 logger(f"Loading extras from {setup_cfg!r}.")
86 cfg: Final[ConfigParser] = ConfigParser()
87 cfg.read(setup_cfg, UTF8)
88 if not cfg.has_section("options.extras_require"):
89 logger(f"No extras from {setup_cfg!r}.")
90 return []
91 res = sorted(set(map(str.strip, cfg.options("options.extras_require"))))
92 logger(f"Found extras {res} from {setup_cfg!r}.")
93 return res
96def make_dist(info: BuildInfo) -> None:
97 """
98 Create the distribution files.
100 This code cannot really be unit tested, as it would run the itself
101 recursively.
103 :param info: the build information
105 >>> root = Path(__file__).up(4)
106 >>> bf = BuildInfo(root, "pycommons",
107 ... examples_dir=root.resolve_inside("examples"),
108 ... tests_dir=root.resolve_inside("tests"),
109 ... dist_dir=root.resolve_inside("dist"),
110 ... doc_source_dir=root.resolve_inside("docs/source"),
111 ... doc_dest_dir=root.resolve_inside("docs/build"))
112 >>> from contextlib import redirect_stdout
113 >>> with redirect_stdout(None):
114 ... make_dist(bf)
116 >>> try:
117 ... make_dist(None)
118 ... except TypeError as te:
119 ... print(str(te)[:50])
120 info should be an instance of pycommons.dev.buildi
122 >>> try:
123 ... make_dist(1)
124 ... except TypeError as te:
125 ... print(str(te)[:50])
126 info should be an instance of pycommons.dev.buildi
127 """
128 if not isinstance(info, BuildInfo):
129 raise type_error(info, "info", BuildInfo)
131 dest: Final[Path] = info.dist_dir
132 if dest is None:
133 raise ValueError(f"Require distribution directory to build {info}.")
134 logger(f"Now building distribution for {info} to {dest!r}.")
135 if dest.is_dir():
136 delete_path(dest)
137 dest.ensure_dir_exists()
138 dest.enforce_dir()
140 setup_py: Final[Path] = info.base_dir.resolve_inside("setup.py")
141 if setup_py.is_file():
142 logger(f"Checking setup.py file {setup_py!r}.")
143 info.command((PYTHON_INTERPRETER, setup_py, "check")).execute()
144 else:
145 logger("No setup.py file found.")
147 logger("Building distribution.")
148 info.command((PYTHON_INTERPRETER, "-m", "build", "-o", dest)).execute()
150 logger("Now checking distribution.")
151 info.command((PYTHON_INTERPRETER, "-m", "twine", "check",
152 f"{dest}/*")).execute()
154 logger("Loading version information from setup.cfg.")
155 setup_cfg: Final[Path] = info.base_dir.resolve_inside("setup.cfg")
156 setup_cfg.enforce_file()
157 doc_info: Final[DocInfo] = load_doc_info_from_setup_cfg(setup_cfg)
158 dist_base: Final[str] = f"{info.package_name}-{doc_info.version}"
159 logger(f"Base file name is {dist_base!r}.")
160 gz_dist: Final[Path] = dest.resolve_inside(f"{dist_base}.tar.gz")
161 gz_dist.enforce_file()
162 logger(f"gz distribution is {gz_dist!r}.")
163 wheel_dist: Final[Path] = dest.resolve_inside(
164 f"{dist_base}-py3-none-any.whl")
165 wheel_dist.enforce_file()
166 logger(f"wheel distribution is {wheel_dist!r}.")
167 requirements: Final[Path] = dest.resolve_inside(
168 f"{dist_base}-requirements_frozen.txt")
169 logger(f"Will store requirements in {requirements!r}.")
170 to: Final[str] = str(info.timeout)
172 extras: Final[list[str]] = __get_extras(setup_cfg)
173 extras_str: Final[str] = "" if list.__len__(extras) <= 0 \
174 else f"[{','.join(extras)}]"
176 count: int = 0
177 for what, steps in __VENV_CMD:
178 count += 1 # noqa: SIM113
179 if (count > 2) and (str.__len__(extras_str) <= 0):
180 break
181 logger(f"Now testing {what}.")
182 with temp_dir() as venv:
183 venv_build: Path = temp_file(directory=venv, suffix=".sh")
184 with venv_build.open_for_write() as wd:
185 write_lines((s.replace(
186 "{VENV}", venv).replace("{GZ_DIST}", gz_dist).replace(
187 "{WHEEL_DIST}", wheel_dist).replace(
188 "{REQUIREMENTS}", requirements).replace(
189 "{TIMEOUT}", to).replace("{EXTRAS}", extras_str)
190 for s in chain(__PRE_PREFIX, __PREFIX,
191 steps, __SUFFIX)), wd)
192 Command(("bash", "--noprofile", "-e", "-E",
193 venv_build), working_dir=venv,
194 timeout=info.timeout, stdout=STREAM_FORWARD,
195 stderr=STREAM_FORWARD).execute()
197 logger("Fixing exact package requirements.")
198 pack: Final[str] = info.package_name
199 pack_replace: Final[str] = f"{pack}{extras_str} @"
200 requirements_txt: Final[str] = requirements.read_all_str()
201 with requirements.open_for_write() as wd:
202 write_lines((
203 f"{pack}{extras_str}=={doc_info.version}" if s.startswith(
204 pack_replace) else s for s in str.splitlines(
205 requirements_txt)), wd)
207 docs: Final[Path | None] = info.doc_dest_dir
208 if (docs is not None) and docs.is_dir():
209 logger(f"Now compressing documentation {docs!r}.")
210 doc_name: Final[str] = f"{dist_base}-documentation"
211 doc_dest: Final[Path] = dest.resolve_inside(f"{doc_name}.tar.xz")
212 with temp_file(suffix=".sh") as tf:
213 with tf.open_for_write() as wd:
214 write_lines((s.replace("{DEST}", doc_dest).replace(
215 "{BASE}", doc_name) for s in chain(
216 __PRE_PREFIX, __XZ)), wd)
217 Command(("bash", "--noprofile", "-e", "-E", tf),
218 working_dir=docs, timeout=info.timeout,
219 stdout=STREAM_FORWARD, stderr=STREAM_FORWARD).execute()
220 doc_dest.enforce_file()
222 logger("Finished building distribution.")
225# Run conversion if executed as script
226if __name__ == "__main__":
227 parser: Final[ArgumentParser] = pycommons_argparser(
228 __file__,
229 "Build the Distribution Files.",
230 "This utility builds a distribution for python projects "
231 "in a unified way.")
232 make_dist(parse_project_arguments(parser))