"""Legacy compatibility layer for tidy3d.config.
This module holds (most) of the compatibility layer to the pre-2.10 tidy3d config
and is intended to be removed in a future release.
"""
from __future__ import annotations
import os
import warnings
from pathlib import Path
from typing import Any, Optional
import toml
from tidy3d._runtime import WASM_BUILD
from tidy3d.log import LogLevel, log
# TODO(FXC-3827): Remove LegacyConfigWrapper/Environment shims and related helpers in Tidy3D 2.12.
from .manager import ConfigManager, normalize_profile_name
from .profiles import BUILTIN_PROFILES
def _warn_env_deprecated() -> None:
message = "'tidy3d.config.Env' is deprecated; use 'config.switch_profile(...)' instead."
warnings.warn(message, DeprecationWarning, stacklevel=3)
log.warning(message, log_once=True)
# TODO(FXC-3827): Delete LegacyConfigWrapper once legacy attribute access is dropped.
[docs]
class LegacyConfigWrapper:
"""Provide attribute-level compatibility with the legacy config module."""
[docs]
def __init__(self, manager: ConfigManager):
self._manager = manager
self._frozen = False # retained for backwards compatibility tests
@property
def logging_level(self) -> LogLevel:
return self._manager.get_section("logging").level
@logging_level.setter
def logging_level(self, value: LogLevel) -> None:
from warnings import warn
warn(
"'config.logging_level' is deprecated; use 'config.logging.level' instead.",
DeprecationWarning,
stacklevel=2,
)
self._manager.update_section("logging", level=value)
@property
def log_suppression(self) -> bool:
return self._manager.get_section("logging").suppression
@log_suppression.setter
def log_suppression(self, value: bool) -> None:
from warnings import warn
warn(
"'config.log_suppression' is deprecated; use 'config.logging.suppression'.",
DeprecationWarning,
stacklevel=2,
)
self._manager.update_section("logging", suppression=value)
@property
def use_local_subpixel(self) -> Optional[bool]:
return self._manager.get_section("simulation").use_local_subpixel
@use_local_subpixel.setter
def use_local_subpixel(self, value: Optional[bool]) -> None:
from warnings import warn
warn(
"'config.use_local_subpixel' is deprecated; use 'config.simulation.use_local_subpixel'.",
DeprecationWarning,
stacklevel=2,
)
self._manager.update_section("simulation", use_local_subpixel=value)
@property
def suppress_rf_license_warning(self) -> bool:
return self._manager.get_section("microwave").suppress_rf_license_warning
@suppress_rf_license_warning.setter
def suppress_rf_license_warning(self, value: bool) -> None:
from warnings import warn
warn(
"'config.suppress_rf_license_warning' is deprecated; "
"use 'config.microwave.suppress_rf_license_warning'.",
DeprecationWarning,
stacklevel=2,
)
self._manager.update_section("microwave", suppress_rf_license_warning=value)
@property
def frozen(self) -> bool:
return self._frozen
@frozen.setter
def frozen(self, value: bool) -> None:
self._frozen = bool(value)
[docs]
def save(self, include_defaults: bool = False) -> None:
self._manager.save(include_defaults=include_defaults)
[docs]
def reset_manager(self, manager: ConfigManager) -> None:
"""Swap the underlying manager instance."""
self._manager = manager
[docs]
def switch_profile(self, profile: str) -> None:
"""Switch active profile and synchronize the legacy environment proxy."""
normalized = normalize_profile_name(profile)
self._manager.switch_profile(normalized)
try:
from tidy3d.config import Env as _legacy_env
except Exception:
_legacy_env = None
if _legacy_env is not None:
_legacy_env._sync_to_manager(apply_env=True)
def __getattr__(self, name: str) -> Any:
return getattr(self._manager, name)
def __setattr__(self, name: str, value: Any) -> None:
if name.startswith("_"):
object.__setattr__(self, name, value)
elif name in {
"logging_level",
"log_suppression",
"use_local_subpixel",
"suppress_rf_license_warning",
"frozen",
}:
prop = getattr(type(self), name)
prop.fset(self, value)
else:
setattr(self._manager, name, value)
def __str__(self) -> str:
return self._manager.format()
# TODO(FXC-3827): Delete LegacyEnvironmentConfig once profile-based Env shim is removed.
class LegacyEnvironmentConfig:
"""Backward compatible environment config wrapper that proxies ConfigManager."""
def __init__(
self,
manager: Optional[ConfigManager] = None,
name: Optional[str] = None,
*,
web_api_endpoint: Optional[str] = None,
website_endpoint: Optional[str] = None,
s3_region: Optional[str] = None,
ssl_verify: Optional[bool] = None,
enable_caching: Optional[bool] = None,
ssl_version: Optional[str] = None,
env_vars: Optional[dict[str, str]] = None,
environment: Optional[LegacyEnvironment] = None,
) -> None:
if name is None:
raise ValueError("Environment name is required")
self._manager = manager
self._name = normalize_profile_name(name)
self._environment = environment
self._pending: dict[str, Any] = {}
if web_api_endpoint is not None:
self._pending["api_endpoint"] = web_api_endpoint
if website_endpoint is not None:
self._pending["website_endpoint"] = website_endpoint
if s3_region is not None:
self._pending["s3_region"] = s3_region
if ssl_verify is not None:
self._pending["ssl_verify"] = ssl_verify
if enable_caching is not None:
self._pending["enable_caching"] = enable_caching
if ssl_version is not None:
self._pending["ssl_version"] = ssl_version
if env_vars is not None:
self._pending["env_vars"] = dict(env_vars)
def reset_manager(self, manager: ConfigManager) -> None:
self._manager = manager
@property
def manager(self) -> Optional[ConfigManager]:
if self._manager is not None:
return self._manager
if self._environment is not None:
return self._environment._manager
return None
def active(self) -> None:
_warn_env_deprecated()
environment = self._environment
if environment is None:
from tidy3d.config import Env # local import to avoid circular
environment = Env
environment.set_current(self)
@property
def web_api_endpoint(self) -> Optional[str]:
value = self._value("api_endpoint")
return _maybe_str(value)
@property
def website_endpoint(self) -> Optional[str]:
value = self._value("website_endpoint")
return _maybe_str(value)
@property
def s3_region(self) -> Optional[str]:
return self._value("s3_region")
@property
def ssl_verify(self) -> bool:
value = self._value("ssl_verify")
if value is None:
return True
return bool(value)
@property
def enable_caching(self) -> bool:
value = self._value("enable_caching")
if value is None:
return True
return bool(value)
@enable_caching.setter
def enable_caching(self, value: Optional[bool]) -> None:
self._set_pending("enable_caching", value)
@property
def ssl_version(self) -> Optional[str]:
return self._value("ssl_version")
@ssl_version.setter
def ssl_version(self, value: Optional[str]) -> None:
self._set_pending("ssl_version", value)
@property
def env_vars(self) -> dict[str, str]:
value = self._value("env_vars")
if value is None:
return {}
return dict(value)
@env_vars.setter
def env_vars(self, value: dict[str, str]) -> None:
self._set_pending("env_vars", dict(value))
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str) -> None:
self._name = normalize_profile_name(value)
def copy_state_from(self, other: LegacyEnvironmentConfig) -> None:
if not isinstance(other, LegacyEnvironmentConfig):
raise TypeError("Expected LegacyEnvironmentConfig instance.")
for key, value in other._pending.items():
if key == "env_vars" and value is not None:
self._pending[key] = dict(value)
else:
self._pending[key] = value
def get_real_url(self, path: str) -> str:
manager = self.manager
if manager is not None and manager.profile == self._name:
web_section = manager.get_section("web")
if hasattr(web_section, "build_api_url"):
return web_section.build_api_url(path)
endpoint = self.web_api_endpoint or ""
if not path:
return endpoint
return "/".join([endpoint.rstrip("/"), str(path).lstrip("/")])
def apply_pending_overrides(self) -> None:
manager = self.manager
if manager is None or manager.profile != self._name:
return
if not self._pending:
return
updates = dict(self._pending)
manager.update_section("web", **updates)
self._pending.clear()
def _set_pending(self, key: str, value: Any) -> None:
if key == "env_vars" and value is not None:
self._pending[key] = dict(value)
else:
self._pending[key] = value
self.apply_pending_overrides()
def _web_section(self) -> dict[str, Any]:
manager = self.manager
if manager is None or WASM_BUILD:
return {}
profile = normalize_profile_name(self._name)
if manager.profile == profile:
section = manager.get_section("web")
return section.model_dump(mode="python", exclude_unset=False)
preview = manager.preview_profile(profile)
source = preview.get("web", {})
return dict(source) if isinstance(source, dict) else {}
def _value(self, key: str) -> Any:
if key in self._pending:
return self._pending[key]
return self._web_section().get(key)
# TODO(FXC-3827): Delete LegacyEnvironment after deprecating `tidy3d.config.Env`.
class LegacyEnvironment:
"""Legacy Env wrapper that maps to profiles."""
def __init__(self, manager: ConfigManager):
self._previous_env_vars: dict[str, Optional[str]] = {}
self.env_map: dict[str, LegacyEnvironmentConfig] = {}
self._current: Optional[LegacyEnvironmentConfig] = None
self._manager: Optional[ConfigManager] = None
self._applied_profile: Optional[str] = None
self.reset_manager(manager)
def reset_manager(self, manager: ConfigManager) -> None:
self._manager = manager
self.env_map = {}
for name in BUILTIN_PROFILES:
key = normalize_profile_name(name)
self.env_map[key] = LegacyEnvironmentConfig(manager, key, environment=self)
self._applied_profile = None
self._current = None
self._sync_to_manager(apply_env=True)
@property
def current(self) -> LegacyEnvironmentConfig:
self._sync_to_manager()
assert self._current is not None
return self._current
def set_current(self, env_config: LegacyEnvironmentConfig) -> None:
_warn_env_deprecated()
key = normalize_profile_name(env_config.name)
stored = self._get_config(key)
stored.copy_state_from(env_config)
if self._manager and self._manager.profile != key:
self._manager.switch_profile(key)
self._sync_to_manager(apply_env=True)
def enable_caching(self, enable_caching: Optional[bool] = True) -> None:
config = self.current
config.enable_caching = enable_caching
self._sync_to_manager()
def set_ssl_version(self, ssl_version: Optional[str]) -> None:
config = self.current
config.ssl_version = ssl_version
self._sync_to_manager()
def __getattr__(self, name: str) -> LegacyEnvironmentConfig:
return self._get_config(name)
def _get_config(self, name: str) -> LegacyEnvironmentConfig:
key = normalize_profile_name(name)
config = self.env_map.get(key)
if config is None:
config = LegacyEnvironmentConfig(self._manager, key, environment=self)
self.env_map[key] = config
else:
manager = self._manager
if manager is not None:
config.reset_manager(manager)
config._environment = self
return config
def _sync_to_manager(self, *, apply_env: bool = False) -> None:
if self._manager is None:
return
active = normalize_profile_name(self._manager.profile)
config = self._get_config(active)
config.apply_pending_overrides()
self._current = config
if apply_env or self._applied_profile != active:
self._apply_env_vars(config)
self._applied_profile = active
def _apply_env_vars(self, config: LegacyEnvironmentConfig) -> None:
self._restore_env_vars()
env_vars = config.env_vars or {}
self._previous_env_vars = {}
for key, value in env_vars.items():
self._previous_env_vars[key] = os.environ.get(key)
os.environ[key] = value
def _restore_env_vars(self) -> None:
for key, previous in self._previous_env_vars.items():
if previous is None:
os.environ.pop(key, None)
else:
os.environ[key] = previous
self._previous_env_vars = {}
def _maybe_str(value: Any) -> Optional[str]:
if value is None:
return None
return str(value)
def load_legacy_flat_config(config_dir: Path) -> dict[str, Any]:
"""Load legacy flat configuration file (pre-migration format).
This function now supports both the original flat config format and
Nexus custom deployment settings introduced in later versions.
Legacy key mappings:
- apikey -> web.apikey
- web_api_endpoint -> web.api_endpoint
- website_endpoint -> web.website_endpoint
- s3_region -> web.s3_region
- s3_endpoint -> web.env_vars.AWS_ENDPOINT_URL_S3
- ssl_verify -> web.ssl_verify
- enable_caching -> web.enable_caching
"""
legacy_path = config_dir / "config"
if not legacy_path.exists():
return {}
try:
text = legacy_path.read_text(encoding="utf-8")
except Exception as exc:
log.warning(f"Failed to read legacy configuration file '{legacy_path}': {exc}")
return {}
try:
parsed = toml.loads(text)
except Exception as exc:
log.warning(f"Failed to decode legacy configuration file '{legacy_path}': {exc}")
return {}
legacy_data: dict[str, Any] = {}
# Migrate API key (original functionality)
apikey = parsed.get("apikey")
if apikey is not None:
legacy_data.setdefault("web", {})["apikey"] = apikey
# Migrate Nexus API endpoint
web_api = parsed.get("web_api_endpoint")
if web_api is not None:
legacy_data.setdefault("web", {})["api_endpoint"] = web_api
# Migrate Nexus website endpoint
website = parsed.get("website_endpoint")
if website is not None:
legacy_data.setdefault("web", {})["website_endpoint"] = website
# Migrate S3 region
s3_region = parsed.get("s3_region")
if s3_region is not None:
legacy_data.setdefault("web", {})["s3_region"] = s3_region
# Migrate SSL verification setting
ssl_verify = parsed.get("ssl_verify")
if ssl_verify is not None:
legacy_data.setdefault("web", {})["ssl_verify"] = ssl_verify
# Migrate caching setting
enable_caching = parsed.get("enable_caching")
if enable_caching is not None:
legacy_data.setdefault("web", {})["enable_caching"] = enable_caching
# Migrate S3 endpoint to env_vars
s3_endpoint = parsed.get("s3_endpoint")
if s3_endpoint is not None:
env_vars = legacy_data.setdefault("web", {}).setdefault("env_vars", {})
env_vars["AWS_ENDPOINT_URL_S3"] = s3_endpoint
return legacy_data
__all__ = [
"LegacyConfigWrapper",
"LegacyEnvironment",
"LegacyEnvironmentConfig",
"finalize_legacy_migration",
"load_legacy_flat_config",
]
def finalize_legacy_migration(config_dir: Path) -> None:
"""Promote a copied legacy configuration tree into the structured format.
Parameters
----------
config_dir : Path
Destination directory (typically the canonical config location).
"""
legacy_data = load_legacy_flat_config(config_dir)
from .manager import ConfigManager # local import to avoid circular dependency
manager = ConfigManager(profile="default", config_dir=config_dir)
config_path = config_dir / "config.toml"
for section, values in legacy_data.items():
if isinstance(values, dict):
manager.update_section(section, **values)
try:
manager.save(include_defaults=True)
except Exception:
if config_path.exists():
try:
config_path.unlink()
except Exception:
pass
raise
legacy_flat_path = config_dir / "config"
if legacy_flat_path.exists():
try:
legacy_flat_path.unlink()
except Exception as exc:
log.warning(f"Failed to remove legacy configuration file '{legacy_flat_path}': {exc}")