Skip to content

Add PEP 723 inline script metadata support#452

Open
gilgamezh wants to merge 3 commits into
masterfrom
pep723-support
Open

Add PEP 723 inline script metadata support#452
gilgamezh wants to merge 3 commits into
masterfrom
pep723-support

Conversation

@gilgamezh

@gilgamezh gilgamezh commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Closes #423

PEP 723 defines a standard # /// script comment block (TOML) for declaring a single-file script's dependencies and requires-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 # fades marks, docstrings, -r files, -d flags).
  • requires-python: honored by selecting a suitable interpreter — the currently selected one (--python or fades' own) if it satisfies the specifier, otherwise an auto-discovered pythonX.Y on PATH, failing cleanly if none is available. An explicit --python that conflicts is reported rather than silently overridden, so the user stays in control.
  • TOML parsing: stdlib tomllib on Python 3.11+, falling back to tomli on older versions (conditional dependency in setup.py).
  • Robust errors: malformed metadata (bad TOML, invalid requirement string, 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.

Example:

# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "requests<3",
#   "rich",
# ]
# ///

import requests
from rich.pretty import pprint

⚠️ Point for review: requires-python behavior

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

  • selected interpreter satisfies the specifier → use it;
  • it doesn't, and no explicit --python → discover a matching pythonX.Y on PATH, else fail;
  • it doesn't, and --python was given → fail (don't override the user's explicit choice).

Discovery is best-effort over PATH (python3.6python3.29, python3, python). Happy to dial this back to validate-only if preferred.

Tests

New tests/test_parsing/test_pep723.py plus fixtures, interpreter-selection/version-probe tests in test_helpers.py, and a consolidate_dependencies test in test_main.py. Full suite green (./test), flake8 clean. Docs updated in README.rst and man/fades.1.

🤖 Generated with Claude Code

@gilgamezh gilgamezh requested a review from facundobatista June 18, 2026 09:36
@gilgamezh gilgamezh changed the base branch from master to change_run_old June 18, 2026 09:37
@gilgamezh

Copy link
Copy Markdown
Contributor Author

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

@gilgamezh gilgamezh changed the base branch from change_run_old to master June 20, 2026 09:06
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>
gilgamezh and others added 2 commits June 20, 2026 11:17
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 facundobatista left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work, thanks for this! I annotated some details and comments for stuff we need to think about...

Comment thread fades/helpers.py
candidate_names += ["python3", "python"]

found = {} # path -> Version, to avoid probing the same interpreter twice
for name in candidate_names:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread fades/helpers.py
logger.error(msg)
raise FadesError(msg)

# nothing was explicitly requested and fades' own python doesn't satisfy the spec:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a little confusing... if nothing was explicitly requested, what is "the spec"?

Comment thread fades/helpers.py
# 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Comment thread fades/main.py

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid the duplicate parsing?

Comment thread tests/test_helpers.py
@@ -1,4 +1,4 @@
# Copyright 2015-2026 Facundo Batista, Nicolás Demarchi
# Copyright 2014-2026 Facundo Batista, Nicolás Demarchi

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you changing the "from" in this copyright?

@@ -0,0 +1,13 @@
# Copyright 2014-2026 Facundo Batista, Nicolás Demarchi

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New files (here and all below) should start with copyright in current year. And they're missing the rest of the header.

Comment thread fades/parsing.py
# 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]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread fades/helpers.py
return candidates[-1][1]


def get_interpreter_for_requirement(requires_python, requested_python):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PEP 723 support

2 participants