Toilal
> **Refined** โ verified against source at `cf8eed1`. Severity: ๐ก Minor. Related: #1, #2, #3. Roundup of small, independently-fixable findings. Each nit below was confirmed against the source; pick them up individually or in one cleanup pass. ## Verified nits - [ ] **(a) `deps_cache` silently ignored when already patched.** `patch.py:232-233` returns `False` before `_active_cache` is assigned at `patch.py:235`. Calling `patch_fastapi_deps_cache(deps_cache=my_cache)` while already patched discards `my_cache` with no signal. *Fix:* log a warning (or raise) when a `deps_cache` is passed but a patch is already active, so the caller knows their cache was not adopted. (See also #2/#3 for the context-manager side.) - [ ] **(b) Patching `openapi.utils.get_flat_dependant` is dead code.** Both OpenAPI call sites โ `fastapi/openapi/utils.py:117` and `:286` โ call `get_flat_dependant(..., skip_repeats=True)`, and the wrapper short-circuits `skip_repeats=True` straight to the original (`patch.py:186-189`), never caching. So the install at `patch.py:254` (and its unpatch at `patch.py:300`) buy nothing. *Fix:* either drop the OpenAPI patch (and update `test_all_modules_patched`, `tests/test_patch.py:294-303`, plus `test_unpatch_restores_originals`), or keep it and add a comment that it exists only for symmetry. Harmless today โ just not load-bearing. - [ ] **(c) `dependant.path` is stale for routes sharing a cache entry.** The `get_dependant` key (`patch.py:116-122`) folds in only `get_path_param_names(path)`, not the path string. `Dependant.path` is set to the full path in FastAPI (`get_dependant(..., path=path)`), and is read for error/endpoint context at `fastapi/routing.py:412` and `:415`. Two routes with the same param names but different literal paths (e.g. `/a/{id}/x` and `/b/{id}/x`) share one cached `Dependant`, so the second carries the first route's `.path`. This is asserted (as intended sharing) by `test_different_paths_same_param_names_share_cache` (`tests/test_patch.py:172-184`). *Fix:* include the literal `path` string in the cache key, or accept it and document that `.path` is only used for diagnostics. Note this only surfaces in error messages/logs, not routing behavior. - [ ] **(d) `_hashable` only coerces `list`.** `patch.py:91-95` turns `list` into `tuple` but passes everything else through. Today the extra `get_dependant` kwargs are scope-related (`scope: str`, `own_oauth_scopes` / `parent_oauth_scopes`: `list`), all covered, so no crash currently. But any future unhashable kwarg value (`set`, `dict`) would raise when the key tuple/`frozenset` is built. *Fix:* make `_hashable` handle the general unhashable case (recurse into `set`/`dict`, or fall back to a stable representation). - [ ] **(e) Module-level `pytest.skip` placed at the bottom of `test_readme.py`.** `tests/test_readme.py:68-69` guards ruff/mypy availability with `pytest.skip(allow_module_level=True)` after all test defs. It works (module-level code runs at collection) but is unconventional and easy to miss. *Fix:* move the guard to the top of the module, above the tests it protects. ## Suggestions - [ ] **Document thread-safety / process-global state.** All patch state lives in module globals (`patch.py:46-57`): originals, `_active_cache`, and hit/miss counters. Patching mutates FastAPI module attributes process-wide and is not re-entrant or thread-safe. Document that `patch`/`unpatch` and the context manager must be used from a single thread during startup only. (Re-entrancy of the context manager is tracked in #2.) - [ ] **Expose hit/miss stats on `DepsCache`.** Counters are module globals (`_sig_hits` โฆ `_flat_misses`) reset on unpatch and only surfaced via the log line at `patch.py:279-293`. Moving them onto `DepsCache` would let callers inspect effectiveness after loading (ties into #3's "cache survives" contract). - [ ] **Document that `_copy_flat_dependant` shares list *elements*.** `patch.py:154-166` copies the six list attributes but the elements (`ModelField` / sub-`Dependant`) remain shared references. Appending to a returned list is safe; mutating an element is not. Worth a one-line note in the function/README. - [ ] **Add missing test coverage.** Shared endpoint across multiple routes with differing `dependencies=` (#1), nested contexts and manual-patch + context (#2), user cache state after context exit (#3), `use_cache=False`, and `Security`/OAuth-scope dependencies. None are currently exercised. - [ ] **The 3ร benchmark gate is timing-dependent.** `_REQUIRED_SPEEDUP = 3.0` in `tests/test_performance.py:35`, asserted in `test_startup_speedup_meets_threshold` (`test_performance.py:233-241`) via real uvicorn startup timing. It is designed to be runner-independent (ratio of cached vs uncached on the same machine, slower runners only raise the ratio), but remains inherently flaky under noisy/loaded CI. Consider documenting the rationale inline and/or marking it so it can be quarantined if it flakes. ## Related - #1 โ shared mutable `Dependant` cross-route corruption (the (c) sharing behavior is the benign cousin of #1's mutable-object bug). - #2 โ context manager unpatches unconditionally (related to nit (a) and the thread-safety note). - #3 โ unpatch clears a user-provided cache (related to the stats-on-`DepsCache` suggestion). **File(s):** `src/fastapi_cache_di/patch.py`, `src/fastapi_cache_di/cache.py`, `tests/test_readme.py`, `tests/test_performance.py`, `README.md`.