You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
caches.py is the largest module in the codebase (wc -l: 954 vs. next-largest remote.py at 930, config.py at 770). It also has the highest inbound-import surface: wrapper.py, config.py, query.py, state.py, remote.py, executor.py, and every test file that touches the cache layer all import from it.
Concrete symptoms of the "too many concerns in one file" pressure:
Import cycle worked around at method level.BaseCache.from_config (caches.py:34-49) does a lazy from . import config as _config because config.py imports from caches.py. The docstring calls this out explicitly. That cycle exists structurally because caches.py conflates the abstract interface (BaseCache) with concrete constructors that need config; splitting them into two files (base.py + simple.py inside a caches/ package) breaks the cycle at the module boundary, not with a per-call lazy import.
_combine_shrink (base layer) lives next to Cache (concrete) even though only _MultiCache uses it. Line 275-286 is a free helper about combining shrink results across sibling caches; it's read by _MultiCache._shrink 300 lines away.
Reader navigation. Finding "what does CacheStack do when a load misses?" requires paging through Cache, CacheWrapper, _MultiCache._first_hit, then CacheStack.load. Concern-separated files map to a mental model that already exists in the code.
Test file naming already assumes the split.tests/unit/caches/test_cache.py, test_cache_stack.py, test_size_limited.py, test_operation_context_cache.py etc. treat these as distinct units — the tests are organised by concern; the source is not.
Suggested refactor
Turn caches.py into a package. Keep the public re-exports 100% intact so no importer needs to change:
src/fleche/caches/
__init__.py # public re-exports; keeps every current import path working
base.py # BaseCache, Rejected, _combine_shrink
simple.py # Cache (the concrete single-storage cache)
wrappers.py # CacheWrapper, ReadOnlyMixin, ReadOnlyCache,
# FilteringMixin, FilteredCache, RefreshingCache
multi.py # _MultiCache, CacheStack, CachePool
size.py # SizeLimitedMixin, SizeLimitedCache
Because every current import path (from fleche.caches import Cache, from fleche.caches import BaseCache, …) still resolves via __init__.py, no other module in the repo needs to change — the whole diff is git mv caches.py caches/__init__.py + shifting classes into siblings.
Pros
Aligns files with mental model.base / simple / wrappers / multi / size are how the module's classes are actually grouped in review comments, tests, and docs.
Kills the caches ↔ config import cycle at the module boundary.BaseCache.from_config can move to caches/__init__.py (import-safe now that config doesn't need to import all of caches) and drop the in-function lazy import.
Removes reader-scroll cost. Contributors landing a fix to SizeLimitedMixin don't page past 850 lines of unrelated code.
Enables per-file circular-import discipline. Once split, importers can pin exactly what they need (e.g. state.py only actually needs BaseCache — importing the package top-level pulls all subclasses transitively).
No behaviour change. Pure move. Full test suite is the safety net.
Cons / risks
Big diff, small semantic change.git blame on each moved class points to the split commit; if you rely on git blame for context, use --follow. Mitigated by keeping the split as one commit and by conventional-commit refactor: split caches.py into subpackage messaging (per agents/DEVELOPING.md).
__init__.py becomes the public-surface source of truth. If a downstream user imports a private name (e.g. _combine_shrink, _MultiCache), the split will break them. Currently _MultiCache is at least referenced by remote.py; verify with grep -R '_MultiCache\|_combine_shrink' src/ tests/ before landing and re-export what's still used internally. Nothing named _* is documented as public API.
Sphinx docs may need updated cross-references. Any :class: refs like :class:fleche.caches.Cache keep working (Python re-exports); `:mod:` refs like `:mod:`fleche.caches still resolve to the package. Fully-qualified caches.py-in-source references (if any) would need re-pointing — a grep will show them.
__init__.py gets ~30 lines of re-exports. Trivial but has to stay in sync when a new class is added.
Doesn't itself reduce code size. LOC total is the same after the split. The win is in navigation and coupling clarity, not lines removed.
No new external dependencies. Pure stdlib package layout.
Complexity / maintainability
Medium in touched-line-count (git mv + minor import fix-ups); low in semantic risk (behaviour-preserving). Best done as a single atomic commit with all classes moved and the __init__.py populated in one go — a partial move leaves the codebase in a weird state where both the module and the package would try to claim fleche.caches.
Worth doing before the wave of open cache-layer refactors (#761 fan-out unification, #710SizeLimitedMixin reshape, #708 cache config registry) so those PRs land in the layered layout instead of extending the flat file further.
Location
src/fleche/caches.py— 954 lines, five loosely coupled concerns in one file.Rough per-concern boundaries as they stand today:
Rejected,BaseCacheabstract interface,_combine_shrinkhelperCache(plus its GC, redigest, and query decoding)CacheWrapper,ReadOnlyMixin,ReadOnlyCache,FilteringMixin,FilteredCache,RefreshingCache_MultiCache(with_first_hit/_collect/_foreach),CacheStack,CachePoolSizeLimitedMixin,SizeLimitedCacheProblem
caches.pyis the largest module in the codebase (wc -l: 954 vs. next-largestremote.pyat 930,config.pyat 770). It also has the highest inbound-import surface:wrapper.py,config.py,query.py,state.py,remote.py,executor.py, and every test file that touches the cache layer all import from it.Concrete symptoms of the "too many concerns in one file" pressure:
BaseCache.from_config(caches.py:34-49) does a lazyfrom . import config as _configbecauseconfig.pyimports fromcaches.py. The docstring calls this out explicitly. That cycle exists structurally becausecaches.pyconflates the abstract interface (BaseCache) with concrete constructors that need config; splitting them into two files (base.py+simple.pyinside acaches/package) breaks the cycle at the module boundary, not with a per-call lazy import._combine_shrink(base layer) lives next toCache(concrete) even though only_MultiCacheuses it. Line 275-286 is a free helper about combining shrink results across sibling caches; it's read by_MultiCache._shrink300 lines away.CacheStackdo when a load misses?" requires paging throughCache,CacheWrapper,_MultiCache._first_hit, thenCacheStack.load. Concern-separated files map to a mental model that already exists in the code.tests/unit/caches/test_cache.py,test_cache_stack.py,test_size_limited.py,test_operation_context_cache.pyetc. treat these as distinct units — the tests are organised by concern; the source is not.Suggested refactor
Turn
caches.pyinto a package. Keep the public re-exports 100% intact so no importer needs to change:caches/__init__.py:Because every current import path (
from fleche.caches import Cache,from fleche.caches import BaseCache, …) still resolves via__init__.py, no other module in the repo needs to change — the whole diff isgit mv caches.py caches/__init__.py+ shifting classes into siblings.Pros
base/simple/wrappers/multi/sizeare how the module's classes are actually grouped in review comments, tests, and docs.caches ↔ configimport cycle at the module boundary.BaseCache.from_configcan move tocaches/__init__.py(import-safe now thatconfigdoesn't need to import all ofcaches) and drop the in-function lazy import.SizeLimitedMixindon't page past 850 lines of unrelated code.SizeLimitedMixinas aCacheWrapperso it composes withCacheStack/ReadOnlyCache#710 (makeSizeLimitedMixincomposable as aCacheWrapper) becomes a pure inside-size.py-and-wrappers.pychange. Refactor: unifyCacheand_MultiCachefan-out via a shared_sources()hook #761 (unifyCacheand_MultiCachefan-out) lives naturally inbase.py(the shared hook) + a slim override insimple.pyandmulti.py.state.pyonly actually needsBaseCache— importing the package top-level pulls all subclasses transitively).Cons / risks
git blameon each moved class points to the split commit; if you rely ongit blamefor context, use--follow. Mitigated by keeping the split as one commit and by conventional-commitrefactor: split caches.py into subpackagemessaging (peragents/DEVELOPING.md).__init__.pybecomes the public-surface source of truth. If a downstream user imports a private name (e.g._combine_shrink,_MultiCache), the split will break them. Currently_MultiCacheis at least referenced byremote.py; verify withgrep -R '_MultiCache\|_combine_shrink' src/ tests/before landing and re-export what's still used internally. Nothing named_*is documented as public API.:class:refs like:class:fleche.caches.Cachekeep working (Python re-exports); `:mod:` refs like `:mod:`fleche.cachesstill resolve to the package. Fully-qualifiedcaches.py-in-source references (if any) would need re-pointing — a grep will show them.__init__.pygets ~30 lines of re-exports. Trivial but has to stay in sync when a new class is added.Complexity / maintainability
Medium in touched-line-count (
git mv+ minor import fix-ups); low in semantic risk (behaviour-preserving). Best done as a single atomic commit with all classes moved and the__init__.pypopulated in one go — a partial move leaves the codebase in a weird state where both the module and the package would try to claimfleche.caches.Worth doing before the wave of open cache-layer refactors (#761 fan-out unification, #710
SizeLimitedMixinreshape, #708 cache config registry) so those PRs land in the layered layout instead of extending the flat file further.Out of scope
config.pyvia a@register_cachedecorator (cache-side mirror of #660) #708) — it benefits from this split (each cache type'sfrom_config/to_configlands next to the class) but does not require it.remote.py(also ~930 lines) — same shape, distinct concern; would be a sibling issue._MultiCache/Cache._shrinkfan-out unification (Refactor: unifyCacheand_MultiCachefan-out via a shared_sources()hook #761) — becomes a natural follow-up after the split.Filed from a codebase-wide refactoring scan; not yet implemented.