feat(api): support `.[extras]` requirements (#1016)
* fix(api): default kind="virtualenv" for python_entry_point apps The entry-point loader bypasses ``Host.parse_options`` (which is where ``Options.environment["kind"]`` would normally be defaulted for ``@fal.function``/``wrap_app``), so dispatching such an app failed at ``define_environment`` with a missing-arg error. Default the kind in ``_load_from_python_entry_point`` to match the decorator path. * feat(api): materialize local-path requirements into uploaded sdists When a ``[tool.fal.apps.*]`` block lists ``.`` or ``.[extras]`` in its ``requirements`` (the natural way to say "install this project itself on the worker"), build an sdist of the project locally, upload it via ``fal.toolkit.File.from_path``, and rewrite the requirement to ``<package>[extras] @ <url>`` so pip can install it remotely. Implementation: * ``fal.api._sdist`` is a pure helper that detects local-path entries, shells out to ``python -m build --sdist``, uploads the artefact, and rewrites the requirements list (preserving flat vs layered shape). An in-process cache keyed by resolved project root short-circuits repeated dispatches in the same invocation (e.g. ``fetch_metadata`` followed by ``_run``) so we don't rebuild and re-upload every time. * ``FalServerlessHost`` gains a ``local_project_root`` field and a ``_materialize_local_requirements`` hook called from both ``register`` (deploy) and ``_run`` right after copying ``options.environment``. ``LocalHost`` is unaffected since it rejects entrypoint mode anyway. * The CLI threads the project root from ``find_pyproject_toml`` into ``AppData`` and on through ``_create_host``. UX matches the existing ``Building environment...`` phase: a bold ``Packaging local project <name>...`` header, a dim rule, the live ``python -m build`` output, the upload progress line, a closing rule, and a green ``✓ Project packaged`` footer. The cached path is silent to avoid noise on every dispatch. Tested live against the ``python_entry_point/simple`` example app — build/upload/rewrite/install round-trips end-to-end and the function runs on the worker. 30 unit tests cover the rewrite logic, regex shape, layered-list preservation, project-root caching, and progress event contract. * chore(api): harden _sdist temp dir cleanup and cache concurrency Three small follow-ups from the review on #1016: * ``_build_sdist`` wraps its body in a single ``try/except BaseException`` that deletes the ``mkdtemp`` outdir on any failure before re-raising, so ``KeyboardInterrupt`` and other unexpected exceptions between ``mkdtemp`` and the per-branch cleanup can no longer leak the dir. * ``_SDIST_URL_CACHE`` is guarded by a module-level ``threading.Lock``. Two concurrent dispatches with the same project root used to both miss, both build, and both upload; the second now waits for the first to populate the cache and re-uses it. * ``_SDIST_URL_CACHE`` type annotation tightened from bare ``dict`` to ``dict[str, tuple[str, str]]`` to document the cached shape. * chore(api): tidy _sdist docstring and cached upload_finished payload Two follow-ups from the latest review on #1016: * ``materialize_local_paths`` docstring used to claim "once per content hash" — a leftover from an earlier draft. The cache is keyed on resolved project root path; correct the wording. * The cached path emitted ``sdist_size=0`` on ``upload_finished``, which is meaningless for a cache hit and a footgun for callers forwarding the value to analytics. Emit ``sdist_size=None`` and document the convention in the event-contract comment. * chore(fal): declare `build` as a runtime dep ``fal.api._sdist`` shells out to ``python -m build`` whenever a ``[tool.fal.apps.*]`` requirement uses ``.``/``.[extras]``. Until now we relied on ``build`` happening to be present in the user's env; make the dependency explicit so the feature works on a fresh install. Also drop the "If ``python -m build`` is not installed, run ``pip install build``" hint from the build-failure error — it's no longer reachable with the package as a hard dep. * chore(api): clarify _sdist lock scope and build_finished no-op Two comment-only nits from the latest review on #1016: * ``_SDIST_URL_CACHE_LOCK`` is a single global lock — the prior comment claimed it was scoped "to the same project root", which was misleading. Rewrite the comment to be honest about the trade-off (a finer per-root lock would let independent projects build in parallel, but the realistic shape today is at most one project per ``fal run``). * ``_on_progress`` in ``_materialize_local_requirements`` doesn't render ``build_finished``. Note in-line that this is intentional — the live ``python -m build`` output between the rules already tells the user the build is done — so the omission doesn't read as an oversight. * chore(api): require exactly one sdist artefact and cover failure paths Follow-ups from the latest review on #1016: * ``_build_sdist`` used to pick ``sorted(outdir.glob(...))[-1]`` as a defensive fallback when the outdir somehow held more than one tarball. Since the outdir is a fresh ``mkdtemp`` and ``python -m build --sdist`` emits exactly one artefact, silently picking the alphabetic last is the wrong behaviour — anything other than one file means something has gone sideways. Require exactly one and raise a descriptive error otherwise. * Add two ``_build_sdist`` failure tests that mock ``subprocess.run``: one for a non-zero exit and one for a zero exit that left no artefact. Both assert the ``RuntimeError`` shape and that the temp outdir gets cleaned up. Closes a coverage gap — previously every test mocked ``_build_sdist`` whole. * Merge an accidental split f-string in ``_materialize_local_requirements`` (artefact of an earlier ruff-format pass).
R
Ruslan Kuprieiev committed
a76250be5255e896e2ece34802489b474692d489
Parent: 8137b12
Committed by GitHub <noreply@github.com>
on 5/11/2026, 1:24:35 PM