|
2 | 2 |
|
3 | 3 | import itertools
|
4 | 4 | import os
|
| 5 | +import re |
5 | 6 | import shlex
|
6 | 7 | import sys
|
7 | 8 | import tempfile
|
|
33 | 34 | from . import options
|
34 | 35 | from .options import BuildTargetT
|
35 | 36 |
|
| 37 | +if sys.version_info >= (3, 11): |
| 38 | + import tomllib |
| 39 | +else: |
| 40 | + import tomli as tomllib |
| 41 | + |
36 | 42 | DEFAULT_REQUIREMENTS_FILES = (
|
37 | 43 | "requirements.in",
|
38 | 44 | "setup.py",
|
|
43 | 49 | DEFAULT_REQUIREMENTS_OUTPUT_FILE = "requirements.txt"
|
44 | 50 | METADATA_FILENAMES = frozenset({"setup.py", "setup.cfg", "pyproject.toml"})
|
45 | 51 |
|
| 52 | +INLINE_SCRIPT_METADATA_REGEX = ( |
| 53 | + r"(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$" |
| 54 | +) |
| 55 | + |
46 | 56 |
|
47 | 57 | def _determine_linesep(
|
48 | 58 | strategy: str = "preserve", filenames: tuple[str, ...] = ()
|
@@ -170,7 +180,8 @@ def cli(
|
170 | 180 | ) -> None:
|
171 | 181 | """
|
172 | 182 | Compiles requirements.txt from requirements.in, pyproject.toml, setup.cfg,
|
173 |
| - or setup.py specs. |
| 183 | + or setup.py specs, as well as Python scripts containing inline script |
| 184 | + metadata. |
174 | 185 | """
|
175 | 186 | if color is not None:
|
176 | 187 | ctx.color = color
|
@@ -344,14 +355,50 @@ def cli(
|
344 | 355 | )
|
345 | 356 | raise click.BadParameter(msg)
|
346 | 357 |
|
347 |
| - if src_file == "-": |
348 |
| - # pip requires filenames and not files. Since we want to support |
349 |
| - # piping from stdin, we need to briefly save the input from stdin |
350 |
| - # to a temporary file and have pip read that. also used for |
| 358 | + if src_file == "-" or ( |
| 359 | + os.path.basename(src_file).endswith(".py") and not is_setup_file |
| 360 | + ): |
| 361 | + # pip requires filenames and not files. Since we want to support |
| 362 | + # piping from stdin, and inline script metadadata within Python |
| 363 | + # scripts, we need to briefly save the input or extracted script |
| 364 | + # dependencies to a temporary file and have pip read that. Also used for |
351 | 365 | # reading requirements from install_requires in setup.py.
|
| 366 | + if os.path.basename(src_file).endswith(".py"): |
| 367 | + # Probably contains inline script metadata |
| 368 | + with open(src_file, encoding="utf-8") as f: |
| 369 | + script = f.read() |
| 370 | + name = "script" |
| 371 | + matches = list( |
| 372 | + filter( |
| 373 | + lambda m: m.group("type") == name, |
| 374 | + re.finditer(INLINE_SCRIPT_METADATA_REGEX, script), |
| 375 | + ) |
| 376 | + ) |
| 377 | + if len(matches) > 1: |
| 378 | + raise ValueError(f"Multiple {name} blocks found") |
| 379 | + elif len(matches) == 1: |
| 380 | + content = "".join( |
| 381 | + line[2:] if line.startswith("# ") else line[1:] |
| 382 | + for line in matches[0] |
| 383 | + .group("content") |
| 384 | + .splitlines(keepends=True) |
| 385 | + ) |
| 386 | + metadata = tomllib.loads(content) |
| 387 | + reqs_str = metadata.get("dependencies", []) |
| 388 | + tmpfile = tempfile.NamedTemporaryFile(mode="wt", delete=False) |
| 389 | + input_reqs = "\n".join(reqs_str) |
| 390 | + comes_from = ( |
| 391 | + f"{os.path.basename(src_file)} (inline script metadata)" |
| 392 | + ) |
| 393 | + else: |
| 394 | + raise PipToolsError( |
| 395 | + "Input script does not contain valid inline script metadata!" |
| 396 | + ) |
| 397 | + else: |
| 398 | + input_reqs = sys.stdin.read() |
| 399 | + comes_from = "-r -" |
352 | 400 | tmpfile = tempfile.NamedTemporaryFile(mode="wt", delete=False)
|
353 |
| - tmpfile.write(sys.stdin.read()) |
354 |
| - comes_from = "-r -" |
| 401 | + tmpfile.write(input_reqs) |
355 | 402 | tmpfile.flush()
|
356 | 403 | reqs = list(
|
357 | 404 | parse_requirements(
|
|
0 commit comments