pyfaro dev¶
pyfaro dev runs the project's development process, usually a web server,
inside the project environment and supervises it: it streams the output
unchanged, tells you what it started, and on Ctrl+C stops the whole process
tree without leaving anything behind. What to run is configured in
[tool.pyfaro.dev].
Quick example¶
$ pyfaro dev
pyfaro dev: starting demo.main:app with uvicorn (reload on) at http://127.0.0.1:8000 - press Ctrl-C to stop
INFO: Will watch for changes in these directories: ['/work/demo/src']
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [4121] using WatchFiles
INFO: Application startup complete.
^C
INFO: Shutting down
INFO: Stopping reloader process [4121]
pyfaro dev: stopped
$ echo $?
130
The lines starting with pyfaro dev: are Pyfaro's, written to standard
error. Everything else is Uvicorn's own output, passed through untouched.
Scenarios¶
1. Configure the entry point¶
Add a [tool.pyfaro.dev] table to pyproject.toml. Without it, pyfaro dev
exits 1 and shows a minimal example:
$ pyfaro dev
pyfaro dev: no [tool.pyfaro.dev] table in /work/demo/pyproject.toml
pyfaro dev: add one to run the project's development process, for example:
[tool.pyfaro.dev]
preset = "fastapi"
entry-point = "my_package.main:app"
or, to run a module with python -m:
[tool.pyfaro.dev]
entry-point = "my_package.worker"
pyfaro dev: see https://pyfaro.dev/commands/dev/
| Key | Meaning |
|---|---|
preset |
Optional. "fastapi" runs the entry point with Uvicorn. |
entry-point |
Required. package.module:attribute with the preset; package.module (run as python -m) without. |
args |
Optional array of strings appended after the entry point. |
reload |
Optional boolean, only with a preset. The fastapi preset reloads by default; reload = false turns it off. |
2. The FastAPI preset¶
With preset = "fastapi", Pyfaro runs Uvicorn through uv run:
$ pyfaro dev --verbose
pyfaro dev: running /usr/bin/uv --version
pyfaro dev: using uv 0.9.26 at /usr/bin/uv
pyfaro dev: starting demo.main:app with uvicorn (reload on) at http://127.0.0.1:8000 - press Ctrl-C to stop
pyfaro dev: running /usr/bin/uv --no-python-downloads --no-progress run --project /work/demo -- uvicorn demo.main:app --reload --reload-dir /work/demo/src
Reloading is Uvicorn's own: Pyfaro adds no file watcher. It watches
<project>/src when that directory exists, otherwise the project root. The
project must declare uvicorn; otherwise pyfaro dev stops before launching:
$ pyfaro dev
pyfaro dev: uvicorn is not declared in the project dependencies
pyfaro dev: add it with: pyfaro add "uvicorn[standard]"
The FastAPI template writes this table for you. After pyfaro create fastapi
acme-api and pyfaro sync, pyfaro dev serves it, and pyfaro dev -- --port
8001 picks another port. Editing src/acme_api/main.py reloads the server; a
syntax error shows its SyntaxError traceback in the output until you fix it.
3. Run a module¶
Without a preset the entry point is a module, run as python -m in the
project root:
reload is rejected here, because Pyfaro does not watch files itself.
4. Extra arguments and precedence¶
Arguments after -- are appended after the configured args, so they win:
Preset defaults come first, then [tool.pyfaro.dev], then the command line.
5. A port that is already in use¶
With the fastapi preset Pyfaro checks the effective host and port (the last
--host and --port, defaulting to 127.0.0.1 and 8000) before it
launches anything:
$ pyfaro dev
pyfaro dev: port 8000 on 127.0.0.1 is already in use
pyfaro dev: stop the process using it, run pyfaro dev -- --port 8001, or set args = ["--port", "8001"] in [tool.pyfaro.dev]
$ echo $?
1
A --port that is not a number from 0 to 65535 is also an error before
launch. The check is skipped with --uds or --fd.
6. When the process stops¶
| Situation | Exit code |
|---|---|
| The process exits 0 on its own | 0, silently |
It exits with status N |
N, after pyfaro dev: uvicorn exited with status N (python without the preset) |
| You press Ctrl+C | 130, after pyfaro dev: stopped |
| Invalid configuration, missing Uvicorn, busy port | 1 |
On Unix, Ctrl+C sends SIGINT to the whole tree (Uvicorn's reloader and its worker included) and waits about five seconds for a clean shutdown before killing what is left. Either way no process survives, and the port is free again.
Ctrl+C on Windows
A Windows console delivers Ctrl+C to every process attached to it, and
Uvicorn's reloader uses that event to restart its worker after each file
change. pyfaro dev therefore waits about five seconds after Ctrl+C for
the tree to end by itself: if it does, you get pyfaro dev: stopped and
exit 130; if it keeps running, or a new process starts (a restarted
worker), the event was not a stop request and pyfaro dev keeps
supervising. If only part of the tree ended and nothing replaced it, the
rest is terminated. Press Ctrl+C a second time to stop the tree, for
example when the process ignores the first: it stops at once, except on a
busy machine where a reload may have gone unnoticed. The Ctrl+C of the next
reload could then be mistaken for a second one, so pyfaro dev first
gives the command half a second (a second at most) to show a restart, and
stops the tree if none comes. A third Ctrl+C never waits.
While it supervises, pyfaro dev also touches the console every 20 ms,
from a thread of its own so that a slow console never delays the
supervision. A console delivers a Ctrl+C that one of its processes
generated (Uvicorn's reload restart) only when something next uses it; a
terminal window always does, but a console reached only through pipes (an
IDE, a service, a test harness) can hold the event back, and the worker
would never restart.
Options¶
| Option | Meaning |
|---|---|
--project <PATH> |
Use that exact directory instead of discovering the nearest project. |
--locked, --frozen |
Passed to uv run; mutually exclusive. |
--uv <EXE> |
Choose the uv executable. |
--offline |
Disable network access in uv. |
-v, --verbose |
Print the uv version and path and every command line, including the full uv run line, before launching. This is Pyfaro's global flag, so pyfaro -v dev works too. See Verbose output. |
-- <extra>... |
Arguments appended to the configured args. |
Validation¶
The table is validated before anything starts, and the error names the key:
$ pyfaro dev
error: invalid /work/demo/pyproject.toml: tool.pyfaro.dev.reload requires preset = "fastapi"; Pyfaro does not watch files itself
See Configuration for the full list. The repository guide is docs/dev.md.