|
1 | 1 | # `tidy3d.config` Architecture |
2 | 2 |
|
3 | | -The configuration subsystem wires together runtime defaults, environment |
4 | | -overrides, profile files, and optional plugin hooks so that `from tidy3d.config |
5 | | -import config` always exposes an up-to-date view of the active settings. This |
6 | | -document focuses on the developer-facing design so you can confidently extend |
7 | | -or debug the module. |
| 3 | +`tidy3d.config` combines defaults, environment overrides, profile files, and plugin sections so `config` always reflects the active settings. This note is aimed at contributors who need to extend or debug the module. |
8 | 4 |
|
9 | 5 | ## Big Picture |
10 | 6 |
|
11 | | -- **Pydantic schemas define sections.** Every built-in section lives in |
12 | | - `sections.py` and is registered via the `@register_section` decorator. |
13 | | -- **A central manager composes state.** `ConfigManager` merges defaults, |
14 | | - environment variables, builtin/user profiles, and runtime overrides. It also |
15 | | - applies side-effect handlers whenever values change. |
16 | | -- **Persistence is layered.** `ConfigLoader` handles filesystem reads/writes; |
17 | | - `serializer.py` keeps TOML files annotated with descriptions and stable key |
18 | | - ordering. |
19 | | -- **Registries are dynamic.** `registry.py` tracks section schemas and handler |
20 | | - callables and notifies the manager whenever new items arrive (including from |
21 | | - plugins). |
22 | | -- **Legacy wrappers exist for compatibility.** `legacy.py` exposes the previous |
23 | | - API surface while delegating to the modern manager. |
24 | | - |
25 | | -## Lifecycle Overview |
26 | | - |
27 | | -1. Importing `tidy3d.config` loads `sections.py`, which registers every built-in |
28 | | - section and handler. |
29 | | -2. `ConfigManager` instantiates, attaches itself to the registry, and loads the |
30 | | - effective tree by merging builtin profiles, `config.toml`, profile overrides, |
31 | | - environment variables, and any runtime overrides. |
32 | | -3. Handlers declared with `@register_handler` run to push settings into global |
33 | | - side effects (log level, environment variables, cache directories, etc.). |
34 | | -4. The public `config` object is a `LegacyConfigWrapper` that proxies to the |
35 | | - active manager but still honours legacy attributes. |
36 | | -5. Runtime mutations go through `ConfigManager.update_section`, which triggers a |
37 | | - reload+reapply cycle so memoized models always represent the latest state. |
| 7 | +- Section schemas live in `sections.py` and register via `register_section`. |
| 8 | +- `ConfigManager` merges builtin defaults, saved files, environment overrides, and runtime edits, then runs section handlers. |
| 9 | +- `ConfigLoader` handles disk IO while `serializer.py` preserves comments and key order inside TOML files. |
| 10 | +- `registry.py` tracks sections and handlers so late imports (plugins, tests) attach automatically. |
| 11 | +- `legacy.py` keeps the historical API working by delegating to the manager. |
| 12 | + |
| 13 | +## Runtime Flow |
| 14 | + |
| 15 | +1. Importing `tidy3d.config` registers built-in sections and handlers. |
| 16 | +2. `ConfigManager` attaches to the registry, loads builtin and user profiles, applies environment overrides, and composes the effective tree. |
| 17 | +3. Handlers push side effects (logging level, env vars, cache dirs). Calls to `update_section` reload the tree and re-run the relevant handlers. |
38 | 18 |
|
39 | 19 | ## Component Map |
40 | 20 |
|
@@ -64,120 +44,38 @@ flowchart LR |
64 | 44 | env_vars["Environment variables"] --> loader_py |
65 | 45 | builtin_profiles["profiles.py<br/>BUILTIN_PROFILES"] --> manager_py |
66 | 46 | runtime_overrides["Runtime overrides"] --> manager_py |
67 | | - plugins["register_plugin(... )<br/>(plugin imports)"] --> registry_py |
| 47 | + plugins["register_plugin(...)<br/>(plugin imports)"] --> registry_py |
68 | 48 | registry_py --> manager_py |
69 | 49 | ``` |
70 | 50 |
|
71 | 51 | ## Module Reference |
72 | 52 |
|
73 | | -### `sections.py` |
74 | | - |
75 | | -- Defines concrete `ConfigSection` subclasses (Pydantic models) for built-in |
76 | | - sections (`logging`, `simulation`, `microwave`, `adjoint`, `web`, |
77 | | - `local_cache`, `plugins` container). |
78 | | -- Decorated with `@register_section("name")` so the schema is discoverable at |
79 | | - import time. For plugins, use `@register_plugin("plugin_name")`. |
80 | | -- Optional `@register_handler("name")` functions apply side effects after the |
81 | | - manager reloads (e.g., update logger globals, export environment variables). |
82 | | -- Fields can include `json_schema_extra={"persist": True}` to mark values that |
83 | | - should be written to disk by default. |
84 | | - |
85 | | -### `registry.py` |
86 | | - |
87 | | -- Holds the global section and handler dictionaries. |
88 | | -- Exposes decorators (`register_section`, `register_plugin`, `register_handler`) |
89 | | - and accessors (`get_sections`, `get_handlers`). |
90 | | -- Provides `attach_manager()` so the active `ConfigManager` is notified whenever |
91 | | - new sections or handlers register. This allows late imports (plugins, tests) |
92 | | - to automatically appear without manual refresh. |
93 | | - |
94 | | -### `manager.py` |
95 | | - |
96 | | -- `ConfigManager` orchestrates the entire system: |
97 | | - - Attaches to the registry and caches per-section Pydantic model instances. |
98 | | - - Loads data through `ConfigLoader` and merges layers using `deep_merge`. |
99 | | - - Tracks runtime overrides in memory per profile. |
100 | | - - Filters persisted values (`_filter_persisted`) so `config.save()` writes only |
101 | | - annotated fields unless `include_defaults=True`. |
102 | | - - Invokes handlers after every reload or targeted update. |
103 | | - - Exposes helper accessors (`plugins`, `profiles`, `get_section`, `format`, |
104 | | - etc.). |
105 | | -- `SectionAccessor` proxies dot-attribute access back into `update_section`. |
106 | | -- Normalizes profile names so builtin aliases like `"prod"` resolve |
107 | | - consistently. |
108 | | - |
109 | | -### `loader.py` |
110 | | - |
111 | | -- Resolves the configuration directory (`resolve_config_directory`) based on |
112 | | - platform, `$TIDY3D_BASE_DIR`, and legacy fallbacks. |
113 | | -- Reads/writes `config.toml` and profile overrides atomically, including |
114 | | - temporary file + backup behaviour. |
115 | | -- Parses environment variables into nested dictionaries (`load_environment_overrides`). |
116 | | -- Supplies dictionary utilities (`deep_merge`, `deep_diff`) used throughout |
117 | | - the manager. |
118 | | -- Coordinates with `serializer.build_document` to preserve inline comments and |
119 | | - descriptions when writing TOML. |
120 | | - |
121 | | -### `serializer.py` |
122 | | - |
123 | | -- Looks up field descriptions from registered Pydantic models so TOML files |
124 | | - stay annotated. |
125 | | -- Converts nested dictionaries into `tomlkit` documents with stable formatting |
126 | | - and comment preservation—critical for user-friendly diffs. |
127 | | - |
128 | | -### `profiles.py` |
129 | | - |
130 | | -- Maintains the shipped profile presets (`default`, `prod`, `dev`, `uat`, |
131 | | - `pre`, `nexus`). The manager merges these into the baseline before applying |
132 | | - user overrides. |
133 | | - |
134 | | -### `legacy.py` |
135 | | - |
136 | | -- Provides backwards-compatible attribute access (`LegacyConfigWrapper`, |
137 | | - `LegacyEnvironment`, etc.) that forward to the modern manager. |
138 | | -- Emits `DeprecationWarning`s to encourage migrations while keeping older code |
139 | | - functional. |
140 | | - |
141 | | -## Adding a New Section |
142 | | - |
143 | | -1. Define a Pydantic model in `sections.py` (or your plugin) that inherits from |
144 | | - `ConfigSection` and decorate it with `@register_section("name")`. |
145 | | -2. Optionally decorate a function with `@register_handler("name")` to apply |
146 | | - side effects whenever the section changes. |
147 | | -3. Add field descriptions and `json_schema_extra={"persist": True}` where |
148 | | - appropriate so they show up in the generated docs and persisted config. |
149 | | -4. Import the module somewhere reachable so registration happens as part of |
150 | | - normal startup (built-ins live in `sections.py`; plugins should register at |
151 | | - import time). |
152 | | - |
153 | | -`ConfigManager` will automatically detect the new section, expose it under |
154 | | -`config.<name>`, and include it in persistence and documentation without extra |
155 | | -wiring. |
156 | | - |
157 | | -## Handler Side Effects |
158 | | - |
159 | | -- Handlers receive the fully validated Pydantic model for the section. |
160 | | -- Only the sections explicitly updated trigger their handler; call |
161 | | - `config.reload_config()` or `ConfigManager._apply_handlers()` manually if you |
162 | | - need to reapply everything (usually done during initialization). |
163 | | -- Handlers must be robust to repeated calls because they run on every reload. |
164 | | - |
165 | | -## Persistence and Serialization Notes |
166 | | - |
167 | | -- Only fields marked with `json_schema_extra={"persist": True}` are written by |
168 | | - default. Call `config.save(include_defaults=True)` to flush the entire model |
169 | | - tree to disk (useful for debugging). |
170 | | -- TOML output preserves descriptions derived from field docstrings, so keep the |
171 | | - docstrings concise and informative. |
172 | | -- The loader writes files atomically and stores a `.bak` during the swap to |
173 | | - prevent data loss. Beware of direct edits to `_loader._docs`; tests can use |
174 | | - `ConfigLoader` to manipulate the cached documents safely. |
175 | | - |
176 | | -## Debugging Tips |
177 | | - |
178 | | -- `config.format()` (or `print(config)`) renders a Rich tree of the current |
179 | | - effective configuration—useful when verifying merges. |
180 | | -- To inspect persisted values without env overrides, call |
181 | | - `ConfigManager._compose_without_env()` in a debugger. |
182 | | -- The registry exposes `get_sections()` / `get_handlers()` for quick sanity |
183 | | - checks that your plugin registered correctly. |
| 53 | +- `sections.py` - Pydantic models for built-in sections (logging, simulation, microwave, adjoint, web, local cache, plugin container) registered via `register_section`. The bundled models inherit from the internal `ConfigSection` helper, but external code can use plain `BaseModel` subclasses. Optional handlers perform side effects. Fields mark persistence with `json_schema_extra={"persist": True}`. |
| 54 | +- `registry.py` - Stores section and handler registries and notifies the attached manager so new entries appear immediately. |
| 55 | +- `manager.py` - `ConfigManager` caches validated models, tracks runtime overrides per profile, filters persisted fields, exposes helpers such as `plugins`, `profiles`, and `format`. `SectionAccessor` routes attribute access to `update_section`. |
| 56 | +- `loader.py` - Resolves the config directory, loads `config.toml` and `profiles/<name>.toml`, parses environment overrides, and writes atomically through `serializer.build_document`. |
| 57 | +- `serializer.py` - Builds stable TOML documents with descriptive comments derived from section docstrings. |
| 58 | +- `profiles.py` - Supplies builtin profiles merged ahead of user overrides. |
| 59 | +- `legacy.py` - Implements backward-compatible wrappers and deprecation warnings around the manager. |
| 60 | + |
| 61 | +## Extending the System |
| 62 | + |
| 63 | +1. Define a Pydantic model and decorate it with `register_section`. Built-in sections use `ConfigSection`, but the decorator accepts any `BaseModel`. |
| 64 | +2. Optionally define a handler with `register_handler` for side effects that must track the section. |
| 65 | +3. Ensure the module imports during startup so registration happens automatically. |
| 66 | + |
| 67 | +## Handler Rules |
| 68 | + |
| 69 | +- Handlers receive the validated section model. |
| 70 | +- They must tolerate repeated calls and only run for sections that changed unless you trigger `config.reload_config()` for a full pass. |
| 71 | + |
| 72 | +## Persistence Notes |
| 73 | + |
| 74 | +- Only fields tagged with `persist` write by default. Call `config.save(include_defaults=True)` to emit the full tree. |
| 75 | +- `ConfigLoader` writes files atomically and leaves a `.bak` backup while swapping. |
| 76 | + |
| 77 | +## Debugging |
| 78 | + |
| 79 | +- `config.format()` prints the composed tree - handy for verifying merges. |
| 80 | +- Inspect `_compose_without_env()` in a debugger to view the persisted state only. |
| 81 | +- `get_sections()` and `get_handlers()` confirm that new registrations landed. |
0 commit comments