Repository navigation
docs: Explain FastAPI double initialization with Uvicorn Closes #106 (documentation) - #130
Conversation
|
ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
json_logging.init_fastapi() can run only once per process. The reported
RuntimeError ("Can not call init more than once") occurs when a module is
started as a script and uvicorn also imports it through an import string,
so the module body runs twice.
Adds example/fastapi_startup, a small app with a README that documents the
two working launch patterns (python -m uvicorn app.main:app, and passing the
app object to uvicorn.run) and the pattern that fails, plus a FAQ entry.
tests/test_fastapi_startup.py checks that both working patterns initialise
once, and that the reported pattern and a second init in one process still
raise the same error. No library code changes; the protection against double
initialisation is kept as it is.
Fixes #106
The second-initialisation test only asserted that the subprocess printed a marker, so a failure reported only a missing marker. Add an explicit else branch that raises AssertionError with the expected outcome. Verified both ways: with the guard intact the four startup tests pass; with the guard removed in a copy, the test fails with the new message.
…he app The previous test imported app.main and never exercised the documented uvicorn.run(app) launcher. Add example/fastapi_startup/run.py, the launcher the README already described, and run it as __main__ with uvicorn.run() replaced by a recorder. The test checks that uvicorn.run() gets the app object from app.main, not an import string, and that init_fastapi() runs once. Negative controls confirm the check fails for a launcher that reloads the app module or passes an import string.
…ding - The example README says to run commands from example/fastapi_startup but gave a repository-root test path, which finds no tests there. Use ../../tests/test_fastapi_startup.py, and mention the root-relative form. - Document reload and multiple workers: uvicorn requires the import-string pattern for both (an app object exits with an error), the app-object pattern suits a single process, and init_fastapi() running once per worker process is expected. - Replace the import_from_string() check with the real uvicorn.Config(...).load() path that 'python -m uvicorn app.main:app' uses. The test checks the loaded app is app.main.app, that init_fastapi() ran once, and that no socket is bound.
b03cbe8 to
fd1f35f
Compare
Bump the version to 1.6.0 and collect the changelog into a 1.6.0 section covering every change merged since 1.5.1: - Added: json_logging.EXCLUDED_FIELDS (#129, fixes #98) - Fixed: FastAPI request-context propagation (#123, fixes #124); outside-request correlation IDs in Flask, Quart and Connexion (#127, fixes #125); non-web get_correlation_id() (#128, fixes #126); Sanic 24+ logging config (#133) - Documentation: FastAPI/Uvicorn startup (#130, fixes #106); Gunicorn and Flask example (#131, fixes #25) - Compatibility: default output unchanged unless EXCLUDED_FIELDS is set; the framework behaviour changes; Python 3.9+ FastAPI context path; CI coverage The previous 'other integrations unchanged' and 'no change to the JSON schema' statements are replaced, since they do not hold for the whole release. The date is added when the release is cut. No library code change.
* chore(release): prepare json-logging 1.6.0 Bump the version to 1.6.0 and collect the changelog into a 1.6.0 section covering every change merged since 1.5.1: - Added: json_logging.EXCLUDED_FIELDS (#129, fixes #98) - Fixed: FastAPI request-context propagation (#123, fixes #124); outside-request correlation IDs in Flask, Quart and Connexion (#127, fixes #125); non-web get_correlation_id() (#128, fixes #126); Sanic 24+ logging config (#133) - Documentation: FastAPI/Uvicorn startup (#130, fixes #106); Gunicorn and Flask example (#131, fixes #25) - Compatibility: default output unchanged unless EXCLUDED_FIELDS is set; the framework behaviour changes; Python 3.9+ FastAPI context path; CI coverage The previous 'other integrations unchanged' and 'no change to the JSON schema' statements are replaced, since they do not hold for the whole release. The date is added when the release is cut. No library code change. * chore(release): credit Bob T. and Udit Agrawal in 1.6.0 metadata
Final status (head
fd1f35f, rebased on master0cf565b)Root cause of #106.
init_fastapi()may run only once per process. That guard is intentional and unchanged.app/main.pyinitialises json-logging and then, underif __name__ == "__main__":, callsuvicorn.run("app.main:app").__main__, then again asapp.mainwhen uvicorn imports the string. The secondinit_fastapi()raisesRuntimeError: Can not call init more than once.Two safe startup patterns (
example/fastapi_startup/):python -m uvicorn app.main:app. uvicorn importsapp.mainonce. Required for--reloadand--workers.run.py):uvicorn.run(app, ...). Suits a simple single-process start.This round:
README verify command fixed. The example README says to run commands from
example/fastapi_startup, but gavetests/test_fastapi_startup.py, which finds no tests there. It now gives../../tests/test_fastapi_startup.py(6 passed when run from that directory) and mentions the root-relative form.Real uvicorn config path. The import-string test now runs
uvicorn.Config("app.main:app").load(), which is what the CLI does, instead ofimport_from_string(). It runs in a fresh subprocess withsocket.bindmade to fail, and checks that:app.main.app;init_fastapi()ran once;A mutation where
app/main.pycallsinit_fastapi()twice and suppresses the error fails it with "init_fastapi() ran 2 times". Checked with uvicorn 0.39 (Python 3.9) and 0.54 (Python 3.12/3.13).Reload and multiple workers documented.
reloadorworkers > 1; confirmed in the uvicorn 0.39 and 0.54 source.Kept unchanged:
run.pylauncher test (uvicorn.runrecorded; app object, not a string; one init) and its two negative controls;__main__, then imported asapp.main, raisesRuntimeError);With the guard disabled as a mutation, both negative tests fail.
Manual check, not a committed test: a real
python -m uvicorn app.main:appserver answered with the suppliedX-Correlation-IDafter one initialisation. With--workers 2it initialised once per worker, 2 in total, with no error.Rebase: onto master
0cf565b, which includes fix: prevent fabricated correlation IDs outside active requests #127, fix: safely handle correlation ID lookup in non-web mode #128, feat: support configurable exclusion of JSON log fields #129 and ci: add Python 3.14 unit and framework smoke-test coverage #133. OnlyCHANGELOG.mdconflicted; all master entries are kept and the Initialisation error: "Can not call init more than once" [FastAPI] #106 entry is added. The diff against master is docs, example, tests and changelog only. Nojson_logging/ortests/test_fastapi.pychanges.Results:
tests/test_fastapi_startup.py; 94/94 full suite on Python 3.9–3.13, in forward and reversed order; each startup test passes alone and after the other FastAPI and field-exclusion suites. Blocking flake8 0.fd1f35f: 15/15 (unit tests on Python 3.9–3.14; Flask/Quart/FastAPI/Sanic smoke tests on 3.9 and 3.14; Flake8). Run: https://github.com/bobbui/json-logging-python/actions/runs/39519610612Not covered by committed tests:
--reload(the file-watcher process) and real port binding. These are covered only by the manual check and the uvicorn source.PR 4 —
docs: clarify FastAPI initialisation and Uvicorn startup(Fixes #106)Branch
docs/106-fastapi-init, commit178a295.Problem. The reported
RuntimeError: Can not call init more than oncehappens when a module started as a script alsopasses an import string to uvicorn, so the module body runs twice.
Reproduced on master by running the reporter's pattern: the error is raised at the second
init_fastapi().Change (docs and example only).
example/fastapi_startup/withapp/main.pyand a README listing the two workingpatterns (
python -m uvicorn app.main:app, and passing the app object touvicorn.run) and the failing pattern. A FAQentry points to the example. No library code changes: the protection against double initialisation is kept.
Tests.
tests/test_fastapi_startup.py(4 tests): both working patterns import once; the reported pattern stillfails with the same error; a second init in one process still raises. All 4 pass on master, as they are guards.
Local results. Full unit suite on Python 3.9 and 3.13: 39 passed each (35 + 4). Blocking flake8: 0. Not run on
3.10–3.12 in this session.
Not verified. A real uvicorn server was not bound, because the sandbox blocks binding. The working patterns were
checked through
uvicorn.importer.import_from_stringand by importing the app object.Review update (round 2)
44a0201: the duplicate-initialisation test now raises an explicitAssertionErrorif the secondinitialisation succeeds, instead of failing only on a missing marker. Verified: with the guard intact, 4/4 pass; with
the guard removed in a copy, the test fails with the explicit message.
core initialisation guard is not modified.
Review update (round 3, head
b03cbe8)The app-object pattern was not actually exercised. Confirmed and fixed.
test_app_object_pattern_initialises_once_without_servingonly importedapp.main. It never ran the documenteduvicorn.run(app)launcher, so it could not show that this pattern initialises once.example/fastapi_startup/run.py, the launcher the README already showed, and pointed the README at it.test_app_object_launcher_passes_the_app_to_uvicorn_and_initialises_once, in a subprocess:json_logging.init_fastapiwith a call counter;uvicorn.runwith a recorder, so no port is bound and no server starts;run.pywithrunpy.run_path(..., run_name="__main__").uvicorn.run()is called once with the app object fromsys.modules["app.main"], not a string, and thatinit_fastapi()ran exactly once.test_launcher_check_detects_a_launcher_that_reimports_the_app): the same check fails for a launcher that reloadsapp.main(it raises the double-initRuntimeError) and for one that passes"app.main:app"(it trips the assertion).Results: 41/41 on Python 3.9–3.13; blocking flake8 0; GitHub Actions 10/10 on
b03cbe8.Not verified: a real uvicorn server binding a port; the sandbox here does not allow it.
Summary by CodeRabbit
Documentation
New Examples
Tests