Skip to content

Commit e5abfa6

Browse files
committed
more concise readme
1 parent f36ebcd commit e5abfa6

File tree

1 file changed

+42
-144
lines changed

1 file changed

+42
-144
lines changed

tidy3d/config/README.md

Lines changed: 42 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,20 @@
11
# `tidy3d.config` Architecture
22

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.
84

95
## Big Picture
106

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.
3818

3919
## Component Map
4020

@@ -64,120 +44,38 @@ flowchart LR
6444
env_vars["Environment variables"] --> loader_py
6545
builtin_profiles["profiles.py<br/>BUILTIN_PROFILES"] --> manager_py
6646
runtime_overrides["Runtime overrides"] --> manager_py
67-
plugins["register_plugin(... )<br/>(plugin imports)"] --> registry_py
47+
plugins["register_plugin(...)<br/>(plugin imports)"] --> registry_py
6848
registry_py --> manager_py
6949
```
7050

7151
## Module Reference
7252

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

Comments
 (0)