Skip to content

Commit e13d7d3

Browse files
committed
explicit field persistence
1 parent 7e39e4a commit e13d7d3

File tree

13 files changed

+227
-91
lines changed

13 files changed

+227
-91
lines changed

docs/configuration/index.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ Command Line Helpers
155155
--------------------
156156

157157
Use ``tidy3d configure`` to store your API key in ``config.toml``. The command
158-
creates the directory if it is missing and updates only the ``auth`` section.
158+
creates the directory if it is missing and updates only the ``web`` section.
159159

160160
If you have older files in ``~/.tidy3d``, run ``tidy3d config migrate`` to move
161161
them into the new location described above.
@@ -173,4 +173,3 @@ Next Steps
173173

174174
- :doc:`migration`
175175
- :doc:`../api/configuration`
176-

tests/config/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"TIDY3D_ENV",
1515
"SIMCLOUD_APIKEY",
1616
"TIDY3D_AUTH__APIKEY",
17+
"TIDY3D_WEB__APIKEY",
1718
"TIDY3D_BASE_DIR",
1819
}
1920

tests/config/test_loader.py

Lines changed: 68 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@ def test_save_includes_descriptions(config_manager, mock_config_dir):
2020
manager.save(include_defaults=True)
2121

2222
content = _config_path(mock_config_dir).read_text(encoding="utf-8")
23-
assert "# Logging configuration." in content
2423
assert "# Web/HTTP configuration." in content
25-
assert "# Tidy3D API base URL." in content
2624

2725

2826
def test_preserves_user_comments(config_manager, mock_config_dir):
@@ -32,7 +30,7 @@ def test_preserves_user_comments(config_manager, mock_config_dir):
3230
config_path = _config_path(mock_config_dir)
3331
text = config_path.read_text(encoding="utf-8")
3432
text = text.replace(
35-
"Tidy3D API base URL.",
33+
"Web/HTTP configuration.",
3634
"user-modified comment",
3735
)
3836
config_path.write_text(text, encoding="utf-8")
@@ -43,57 +41,90 @@ def test_preserves_user_comments(config_manager, mock_config_dir):
4341

4442
updated = config_path.read_text(encoding="utf-8")
4543
assert "user-modified comment" in updated
46-
assert "Tidy3D API base URL." not in updated
44+
assert "Web/HTTP configuration." not in updated
4745

4846

4947
def test_profile_preserves_comments(config_manager, mock_config_dir):
50-
manager = config_manager
51-
manager.switch_profile("custom")
52-
manager.update_section("web", timeout=55)
53-
manager.save()
54-
55-
profile_path = mock_config_dir / "profiles" / "custom.toml"
56-
text = profile_path.read_text(encoding="utf-8")
57-
assert "HTTP request timeout in seconds." in text
58-
text = text.replace("HTTP request timeout in seconds.", "user comment")
59-
profile_path.write_text(text, encoding="utf-8")
48+
@config_registry.register_plugin("profile_comment")
49+
class ProfileComment(ConfigSection):
50+
"""Profile comment plugin."""
6051

61-
manager.update_section("web", timeout=56)
62-
manager.save()
52+
knob: int = pd.Field(
53+
1,
54+
description="Profile knob description.",
55+
json_schema_extra={"persist": True},
56+
)
6357

64-
updated = profile_path.read_text(encoding="utf-8")
65-
assert "user comment" in updated
66-
assert "HTTP request timeout in seconds." not in updated
58+
try:
59+
manager = config_manager
60+
manager.switch_profile("custom")
61+
manager.update_section("plugins.profile_comment", knob=5)
62+
manager.save()
63+
64+
profile_path = mock_config_dir / "profiles" / "custom.toml"
65+
text = profile_path.read_text(encoding="utf-8")
66+
assert "Profile knob description." in text
67+
text = text.replace("Profile knob description.", "user comment")
68+
profile_path.write_text(text, encoding="utf-8")
69+
70+
manager.update_section("plugins.profile_comment", knob=7)
71+
manager.save()
72+
73+
updated = profile_path.read_text(encoding="utf-8")
74+
assert "user comment" in updated
75+
assert "Profile knob description." not in updated
76+
finally:
77+
config_registry._SECTIONS.pop("plugins.profile_comment", None)
78+
reload_config(profile="default")
6779

6880

6981
def test_cli_reset_config(mock_config_dir):
70-
reload_config(profile="default")
71-
manager = get_manager()
72-
manager.update_section("web", api_endpoint="https://example.com")
73-
manager.save(include_defaults=True)
74-
manager.switch_profile("custom")
75-
manager.update_section("web", timeout=70)
76-
manager.save()
82+
@config_registry.register_plugin("cli_comment")
83+
class CLIPlugin(ConfigSection):
84+
"""CLI plugin configuration."""
7785

78-
profiles_dir = mock_config_dir / "profiles"
79-
assert profiles_dir.exists()
86+
knob: int = pd.Field(
87+
3,
88+
description="CLI knob description.",
89+
json_schema_extra={"persist": True},
90+
)
8091

81-
runner = CliRunner()
82-
result = runner.invoke(tidy3d_cli, ["config-reset", "--yes"])
83-
assert result.exit_code == 0, result.output
84-
85-
config_text = _config_path(mock_config_dir).read_text(encoding="utf-8")
86-
assert "Web/HTTP configuration." in config_text
87-
assert "Tidy3D API base URL" in config_text
88-
assert not profiles_dir.exists()
92+
try:
93+
reload_config(profile="default")
94+
manager = get_manager()
95+
manager.update_section("web", apikey="secret")
96+
manager.save(include_defaults=True)
97+
manager.switch_profile("custom")
98+
manager.update_section("plugins.cli_comment", knob=42)
99+
manager.save()
100+
101+
profiles_dir = mock_config_dir / "profiles"
102+
assert profiles_dir.exists()
103+
104+
runner = CliRunner()
105+
result = runner.invoke(tidy3d_cli, ["config-reset", "--yes"])
106+
assert result.exit_code == 0, result.output
107+
108+
config_text = _config_path(mock_config_dir).read_text(encoding="utf-8")
109+
assert "Web/HTTP configuration." in config_text
110+
assert "[web]" in config_text
111+
assert "secret" not in config_text
112+
assert not profiles_dir.exists()
113+
finally:
114+
config_registry._SECTIONS.pop("plugins.cli_comment", None)
115+
reload_config(profile="default")
89116

90117

91118
def test_plugin_descriptions(mock_config_dir):
92119
@config_registry.register_plugin("comment_test")
93120
class CommentPlugin(ConfigSection):
94121
"""Comment plugin configuration."""
95122

96-
knob: int = pd.Field(3, description="Plugin knob description.")
123+
knob: int = pd.Field(
124+
3,
125+
description="Plugin knob description.",
126+
json_schema_extra={"persist": True},
127+
)
97128

98129
try:
99130
reload_config(profile="default")

tests/config/test_manager.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import numpy as np
44
import pytest
55

6+
from tidy3d.config import Env, get_manager, reload_config
7+
68

79
def test_default_web_settings(config_manager):
810
web = config_manager.get_section("web")
@@ -42,6 +44,19 @@ def test_builtin_profiles(profile, config_manager):
4244
assert web.s3_region is not None
4345

4446

47+
def test_uppercase_profile_normalization(monkeypatch):
48+
monkeypatch.setenv("TIDY3D_ENV", "DEV")
49+
try:
50+
reload_config()
51+
manager = get_manager()
52+
assert manager.profile == "dev"
53+
web = manager.get_section("web")
54+
assert str(web.api_endpoint) == "https://tidy3d-api.dev-simulation.cloud"
55+
assert Env.current.name == "dev"
56+
finally:
57+
reload_config(profile="default")
58+
59+
4560
def test_autograd_defaults(config_manager):
4661
autograd = config_manager.get_section("autograd")
4762
assert autograd.min_wvl_fraction == pytest.approx(5e-2)

tests/config/test_plugins.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import pydantic.v1 as pd
34
import toml
45

56
from tidy3d.config.__init__ import get_manager, reload_config
@@ -13,8 +14,8 @@ def ensure_dummy_plugin():
1314

1415
@register_plugin("dummy")
1516
class DummyPlugin(ConfigSection):
16-
enabled: bool = False
17-
precision: int = 1
17+
enabled: bool = pd.Field(False, json_schema_extra={"persist": True})
18+
precision: int = pd.Field(1, json_schema_extra={"persist": True})
1819

1920

2021
def test_plugin_defaults_available(mock_config_dir):

tests/config/test_profiles.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44

55

66
def test_save_default_profile(config_manager):
7+
config_manager.update_section("web", apikey="token")
78
config_manager.update_section("web", timeout=30)
89
config_manager.save()
910

1011
config_path = config_manager.config_dir / "config.toml"
1112
assert config_path.exists()
1213
data = toml.load(config_path)
13-
assert data["web"]["timeout"] == 30
14+
assert data["web"]["apikey"] == "token"
1415

1516

1617
def test_save_custom_profile(config_manager):
@@ -19,6 +20,4 @@ def test_save_custom_profile(config_manager):
1920
config_manager.save()
2021

2122
profile_path = config_manager.config_dir / "profiles" / "customer.toml"
22-
assert profile_path.exists()
23-
profile_data = toml.load(profile_path)
24-
assert profile_data == {"logging": {"level": "DEBUG"}}
23+
assert not profile_path.exists()

tidy3d/config/legacy.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import ssl
77
from typing import Any, Optional
88

9-
from .manager import ConfigManager
9+
from .manager import ConfigManager, normalize_profile_name
1010
from .profiles import BUILTIN_PROFILES
1111

1212

@@ -116,6 +116,7 @@ def __init__(
116116
) -> None:
117117
if name is None:
118118
raise ValueError("Environment name is required")
119+
name = normalize_profile_name(name)
119120
self._manager = manager
120121
self._name = name
121122
self._environment = environment
@@ -207,7 +208,7 @@ def name(self) -> str:
207208

208209
@name.setter
209210
def name(self, value: str) -> None:
210-
self._name = value
211+
self._name = normalize_profile_name(value)
211212

212213
def get_real_url(self, path: str) -> str:
213214
endpoint = self.web_api_endpoint or ""
@@ -248,14 +249,17 @@ def reset_manager(self, manager: ConfigManager) -> None:
248249
for name in BUILTIN_PROFILES:
249250
self.env_map[name] = LegacyEnvironmentConfig(manager, name, environment=self)
250251

251-
desired = os.getenv("TIDY3D_ENV")
252-
desired = desired.lower() if desired else None
253-
254-
if not desired:
252+
desired_env = os.getenv("TIDY3D_ENV")
253+
if desired_env:
254+
desired = normalize_profile_name(desired_env)
255+
else:
255256
desired = manager.profile
257+
256258
if desired == "default":
257259
desired = "prod"
258260

261+
desired = normalize_profile_name(desired)
262+
259263
self._current = self.env_map.setdefault(
260264
desired, LegacyEnvironmentConfig(manager, desired, environment=self)
261265
)
@@ -266,13 +270,15 @@ def current(self) -> LegacyEnvironmentConfig:
266270
return self._current
267271

268272
def set_current(self, env_config: LegacyEnvironmentConfig) -> None:
273+
key = normalize_profile_name(env_config.name)
269274
if env_config.manager is self._manager:
270-
if self._manager.profile != env_config.name:
271-
self._manager.switch_profile(env_config.name)
272-
stored = self.env_map.setdefault(env_config.name, env_config)
275+
if self._manager.profile != key:
276+
self._manager.switch_profile(key)
277+
stored = self.env_map.setdefault(key, env_config)
273278
else:
274279
stored = env_config
275-
self.env_map[env_config.name] = stored
280+
stored.name = key
281+
self.env_map[key] = stored
276282

277283
stored._environment = self
278284
self._current = stored
@@ -289,7 +295,8 @@ def set_ssl_version(self, ssl_version) -> None:
289295
self._current._overrides["ssl_version"] = ssl_version
290296

291297
def __getattr__(self, name: str) -> LegacyEnvironmentConfig:
292-
return self.env_map.setdefault(name, LegacyEnvironmentConfig(self._manager, name))
298+
key = normalize_profile_name(name)
299+
return self.env_map.setdefault(key, LegacyEnvironmentConfig(self._manager, key))
293300

294301
def _apply_env_vars(self, config: LegacyEnvironmentConfig) -> None:
295302
self._restore_env_vars()

tidy3d/config/loader.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def load_environment_overrides() -> dict[str, Any]:
157157
overrides: dict[str, Any] = {}
158158
for key, value in os.environ.items():
159159
if key == "SIMCLOUD_APIKEY":
160-
_assign_path(overrides, ("auth", "apikey"), value)
160+
_assign_path(overrides, ("web", "apikey"), value)
161161
continue
162162
if not key.startswith("TIDY3D_"):
163163
continue
@@ -167,6 +167,8 @@ def load_environment_overrides() -> dict[str, Any]:
167167
segments = tuple(segment.lower() for segment in rest.split("__") if segment)
168168
if not segments:
169169
continue
170+
if segments[0] == "auth":
171+
segments = ("web",) + segments[1:]
170172
_assign_path(overrides, segments, value)
171173
return overrides
172174

0 commit comments

Comments
 (0)