Import layering¶
How the modules under src/comfy_env/ depend on each other, the one-way rule
that keeps the graph acyclic, and how CI enforces it. For what each file
actually does, see the module inventory; for how big each
subsystem is, the code breakdown.
The import graph is layered and acyclic at the module level, enforced in
CI by import-linter (lint-imports,
contracts in .importlinter). The core invariant: nothing under
isolation/ imports the top orchestrator wrap.py except the public
facade, and the transport leaf _ipc_shared.py imports nothing from
comfy_env at all. Arrows read "depends on" and point one way.
History (fixed in 0.4.20)
Earlier versions had three module-level cycles broken by lazy imports
(an import moved into a function body so the module loader never trips
on it). A four-reviewer layering review found they were not deliberate
-- they were three misplaced definitions, each an upward lazy import
hiding a layering violation. All three were fixed by moving code down
the graph so every arrow points one way, and every former function-body
cross-module import became an ordinary top-level import:
build_isolation_env(a stdlib-only leaf) moved out ofwrap.pyintoisolation/subenv.py;subprocess.py/metadata.pynow import it downward.- the CUDA-IPC forwarding cache moved into
_ipc_shared.py(the onecomfy_env-import-free leaf, next to the eviction policy that bounds it);tensor_utils.pyimports it downward instead of reaching up into the worker driver. - the worker pool moved out of
wrap.pyintoisolation/pool.py;metadata.pyimports it downward, closing thewrap↔metadatacycle.
The lazy imports that remain are legitimate: deferring optional or
heavy dependencies (torch, comfy.*, aiohttp) so the CPU metadata
scan runs on machines without them, and a handful of intra-package
function-body imports (workspace.py reaching into environment.cache,
metadata.py probing detection.backend, pool.py pulling
contract) -- all of which point down, never up.
flowchart TD
cli["cli.py<br/>comfy-env CLI + debug TUI"]
facade["__init__.py<br/>public facade: install / setup_env / register_nodes"]
install["install/<br/>build-time orchestration<br/>(plugin.py, workspace.py, helpers.py, progress.py)"]
isolation["isolation/<br/>runtime: wrap.py (register_nodes), metadata.py,<br/>pool.py, subenv.py, model_patcher.py, procgroup.py, workers/"]
floor["memory-floor modules (top-level leaves)<br/>memory_manager.py, state_sync.py, reserve.py,<br/>contract.py, mirrored_args.py"]
environment["environment/<br/>workspace layout (cache.py),<br/>prestartup (setup.py), libomp.py, runtime.py"]
packages["packages/<br/>cuda_wheels.py,<br/>toml_generator.py, node_packs.py"]
detection["detection/<br/>backend.py, cuda.py, gpu.py, arch.py"]
pixi["pixi.py<br/>pinned pixi-binary provisioning (leaf)"]
config["config/<br/>comfy-env.toml parsing"]
settings["settings.py<br/>removed-variable tombstones"]
debug["debug.py<br/>debug categories"]
cli --> facade
cli --> install
cli --> environment
cli --> config
cli --> debug
facade --> install
facade --> isolation
facade --> environment
facade --> settings
install --> packages
install --> environment
install --> detection
install --> config
install --> pixi
isolation --> environment
isolation --> detection
isolation --> config
isolation --> debug
isolation --> pixi
isolation --> floor
environment --> detection
packages --> detection
packages --> config
packages --> pixi
detection --> pixi
The graph above is derived from two sources: the contracts in .importlinter
(which name the layers and the forbidden edges) and a sweep of every
from .., from comfy_env and import comfy_env line under
src/comfy_env/, function-body imports included. Edges that exist only as a
function-body import (install --> packages, install --> detection,
isolation --> detection, and the contract / memory_manager /
mirrored_args edges into the memory-floor group) are drawn the same as
top-level ones; every one of them points down. The five memory-floor modules,
like config/, debug.py, settings.py and pixi.py, import nothing from
comfy_env. settings.py is imported by the facade alone; nothing under
isolation/ imports packages/ or install/.
The graph is fully acyclic at the module level -- no cycles, no
exemptions for cycles. Every edge points one way, and the only
import-linter exemptions are legitimate public-facade re-exports (the
root comfy_env/__init__ and isolation/__init__ re-export their
subpackages' API), never a cycle.
pixi.py is a leaf (pinned pixi-binary provisioning): both detection
(which runs pixi info to probe CUDA) and packages import it downward
-- it was moved out of packages/ in 0.4.21 precisely to break the old
detection ↔ packages cycle where detection reached up for the PIXI
path. (0.4.22 removed the last cycle too: the parent-side shareable-pool
hook, which made environment reach up into isolation, was deleted --
it was an experimental, default-off, unsound optimization slated for
removal, so the honest fix was to delete it, not exempt it.)
Inside isolation/ the order runs bottom → top: _ipc_shared / subenv /
tensor_utils (leaves) → _ipc_parent → workers/subprocess → pool →
metadata → wrap (register_nodes).
The whole graph is checked in CI by lint-imports
(import-linter contracts in
.importlinter); the build fails if any edge points the wrong way,
including a cycle re-hidden in a function body.
See the module inventory and code breakdown for every file's responsibility.