Add PEP 723 inline script metadata support#452
Conversation
|
Did run tests here because we only run them when target is master https://github.com/PyAr/fades/actions/runs/27750679594 should be executed when we change the target branch |
Closes #423. fades now understands the PEP 723 `# /// script` metadata block, so it can run scripts written for other runners (pipx, pip-run, uv) and vice versa. - parse the block's `dependencies` and merge them like any other source - honor `requires-python`: keep the selected interpreter if it satisfies the specifier, otherwise auto-discover a suitable one on PATH (failing cleanly if none is available); an explicit --python that conflicts is reported instead of being silently overridden - malformed metadata (bad TOML, bad requirement, bad/non-string requires-python, non-list dependencies, multiple script blocks) raises a clean FadesError with an explanatory log line instead of dumping a traceback - use stdlib tomllib on 3.11+, falling back to tomli on older Pythons Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
b2b8b22 to
55c4e93
Compare
The PyPI availability pre-check built a version-specific URL whenever a dependency had any specifier, using the first specifier's version. For range specifiers like 'requests<3' this queried a non-existent version (requests/3) and wrongly reported the package as missing. PEP 723 scripts commonly use such range specifiers, so the bug surfaced there. Only use the version-specific URL for exact pins (== / ===); check by package name otherwise. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cover previously-untested cases: - requires-python bounded ranges (>=3.10,<3.12) in parsing and in interpreter selection/discovery (pick highest within the range) - exact micro-version pins (==3.11.4) satisfying or not - merging PEP 723 deps with comment-style and other dependency sources - end-to-end wiring of requires-python into interpreter resolution (the parse_pep723 -> get_interpreter_for_requirement flow from main.go) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
facundobatista
left a comment
There was a problem hiding this comment.
Great work, thanks for this! I annotated some details and comments for stuff we need to think about...
| candidate_names += ["python3", "python"] | ||
|
|
||
| found = {} # path -> Version, to avoid probing the same interpreter twice | ||
| for name in candidate_names: |
There was a problem hiding this comment.
It feels a little clumsy to run an external subprocess 25 times just in case to find something.
What about exploring the PATH to see what's out there? See this code, for example, in my machine it produces:
('/usr/bin/python3.12', '3.12.3')
('/usr/bin/python3.14', '3.14.6')
('/usr/bin/python3.13', '3.13.14')
('/usr/bin/python3.14t', '3.14.6')
('/usr/bin/python3.11', '3.11.15')
We can work from there...
| logger.error(msg) | ||
| raise FadesError(msg) | ||
|
|
||
| # nothing was explicitly requested and fades' own python doesn't satisfy the spec: |
There was a problem hiding this comment.
this is a little confusing... if nothing was explicitly requested, what is "the spec"?
| # non-existent "version" and wrongly conclude the package is missing. | ||
| exact_specs = [spec for spec in dependency.specifier if spec.operator in ("==", "===")] | ||
| if exact_specs: | ||
| version = exact_specs[0].version |
There was a problem hiding this comment.
This may be a bug we have... but this code is not the solution at all.
The bug I think we have: "if you declare a version X not using ==, this will check for X anyway"
The bug this is introducing: "if you declare a version X not using ==, this will just check if the package exists (not caring the version)".
You should open a GH issue (if not there already) and let's handle separately,
|
|
||
| # honor a PEP 723 'requires-python' (this may pick a different interpreter than the one | ||
| # explicitly requested with --python or fades' own) | ||
| _, requires_python = parsing.parse_pep723(analyzable_child_program) |
There was a problem hiding this comment.
Can we avoid the duplicate parsing?
| @@ -1,4 +1,4 @@ | |||
| # Copyright 2015-2026 Facundo Batista, Nicolás Demarchi | |||
| # Copyright 2014-2026 Facundo Batista, Nicolás Demarchi | |||
There was a problem hiding this comment.
Why are you changing the "from" in this copyright?
| @@ -0,0 +1,13 @@ | |||
| # Copyright 2014-2026 Facundo Batista, Nicolás Demarchi | |||
There was a problem hiding this comment.
New files (here and all below) should start with copyright in current year. And they're missing the rest of the header.
| # PEP 723 dependencies are standard PEP 508 strings (including 'name @ url' direct | ||
| # references that pip understands), so they all go to the PyPI repo. | ||
| try: | ||
| deps[REPO_PYPI] = [Requirement(dep) for dep in dependencies] |
There was a problem hiding this comment.
Dependencies from other backends should not go in REPO_PYPI, and should not be wrapped in a Requirement (because there are glitches in the comparison later, that's why we use VCSDependency and such) ... take a look at parse_fade_requirement
| return candidates[-1][1] | ||
|
|
||
|
|
||
| def get_interpreter_for_requirement(requires_python, requested_python): |
There was a problem hiding this comment.
These names are very confusing. I know requires_python is PEP723 naming, but both names almost mean the same.
What about being explicit and call them python_toml and python_args or similar?
Closes #423
PEP 723 defines a standard
# /// scriptcomment block (TOML) for declaring a single-file script'sdependenciesandrequires-python. It's implemented by pipx, pip-run and uv. This adds support to fades so it can run scripts written for other runners — and so scripts written for fades can run on them.What it does
dependencies: parsed from the block and merged like every other dependency source (inline# fadesmarks, docstrings,-rfiles,-dflags).requires-python: honored by selecting a suitable interpreter — the currently selected one (--pythonor fades' own) if it satisfies the specifier, otherwise an auto-discoveredpythonX.YonPATH, failing cleanly if none is available. An explicit--pythonthat conflicts is reported rather than silently overridden, so the user stays in control.tomllibon Python 3.11+, falling back totomlion older versions (conditional dependency insetup.py).requires-python, non-listdependencies, multiplescriptblocks) raises a cleanFadesErrorwith an explanatory log line instead of dumping a traceback.Example:
requires-pythonbehaviorThis goes beyond pipx and pip-run, which both ignore
requires-python(only uv honors it, via its own interpreter management). Since fades has no interpreter management, the chosen behavior is:--python→ discover a matchingpythonX.YonPATH, else fail;--pythonwas given → fail (don't override the user's explicit choice).Discovery is best-effort over
PATH(python3.6–python3.29,python3,python). Happy to dial this back to validate-only if preferred.Tests
New
tests/test_parsing/test_pep723.pyplus fixtures, interpreter-selection/version-probe tests intest_helpers.py, and aconsolidate_dependenciestest intest_main.py. Full suite green (./test), flake8 clean. Docs updated inREADME.rstandman/fades.1.🤖 Generated with Claude Code