|  | 
| 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