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