Skip to content

Commit ec50bca

Browse files
refactor(tidy3d): FXC-4683-move-config-to-new-common-submodule
1 parent 61fe5ad commit ec50bca

File tree

24 files changed

+2782
-2443
lines changed

24 files changed

+2782
-2443
lines changed

.github/workflows/tidy3d-python-client-tests.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,27 @@ jobs:
270270
run: |
271271
mypy --config-file=pyproject.toml
272272
273+
ensure-common-imports:
274+
name: ensure-common-imports
275+
needs: determine-test-scope
276+
if: needs.determine-test-scope.outputs.code_quality_tests == 'true'
277+
runs-on: ubuntu-latest
278+
steps:
279+
- uses: actions/checkout@v4
280+
with:
281+
fetch-depth: 1
282+
submodules: false
283+
persist-credentials: false
284+
285+
- name: set-python-3.10
286+
uses: actions/setup-python@v4
287+
with:
288+
python-version: '3.10'
289+
290+
- name: Run tidy3d._common import check
291+
run: |
292+
python scripts/ensure_imports_from_common.py
293+
273294
zizmor:
274295
name: Run zizmor 🌈
275296
runs-on: ubuntu-latest
@@ -983,6 +1004,7 @@ jobs:
9831004
- remote-tests
9841005
- lint
9851006
- mypy
1007+
- ensure-common-imports
9861008
- verify-schema-change
9871009
- lint-commit-messages
9881010
- lint-branch-name
@@ -1004,6 +1026,12 @@ jobs:
10041026
run: |
10051027
echo "❌ Mypy type checking failed."
10061028
exit 1
1029+
1030+
- name: check-common-imports-result
1031+
if: ${{ needs.determine-test-scope.outputs.code_quality_tests == 'true' && needs.ensure-common-imports.result != 'success' && needs.ensure-common-imports.result != 'skipped' }}
1032+
run: |
1033+
echo "❌ tidy3d._common import check failed."
1034+
exit 1
10071035
10081036
- name: check-schema-change-verification
10091037
if: ${{ needs.determine-test-scope.outputs.code_quality_tests == 'true' && needs.verify-schema-change.result != 'success' && needs.verify-schema-change.result != 'skipped' }}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""Ensure tidy3d._common modules avoid importing from tidy3d outside tidy3d._common."""
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import ast
7+
import sys
8+
from collections.abc import Iterable
9+
from dataclasses import dataclass
10+
from pathlib import Path
11+
12+
13+
@dataclass(frozen=True)
14+
class ImportViolation:
15+
file: str
16+
line: int
17+
statement: str
18+
19+
20+
def parse_args(argv: Iterable[str]) -> argparse.Namespace:
21+
parser = argparse.ArgumentParser(
22+
description=(
23+
"Ensure tidy3d._common does not import from tidy3d modules outside tidy3d._common."
24+
)
25+
)
26+
parser.add_argument(
27+
"--root",
28+
default="tidy3d/_common",
29+
help="Root directory to scan (relative to repo root).",
30+
)
31+
return parser.parse_args(argv)
32+
33+
34+
def main(argv: Iterable[str]) -> None:
35+
args = parse_args(argv)
36+
repo_root = Path.cwd().resolve()
37+
root = (repo_root / args.root).resolve()
38+
if not root.exists():
39+
print(f"No directory found at {root}. Skipping check.")
40+
return
41+
42+
violations: list[ImportViolation] = []
43+
for path in sorted(root.rglob("*.py")):
44+
violations.extend(_violations_in_file(path, repo_root))
45+
46+
if violations:
47+
print("Invalid tidy3d imports found in tidy3d._common:")
48+
for violation in violations:
49+
print(f"{violation.file}:{violation.line}: {violation.statement}")
50+
raise SystemExit(1)
51+
52+
print("No invalid tidy3d imports found in tidy3d._common.")
53+
54+
55+
def _violations_in_file(path: Path, repo_root: Path) -> list[ImportViolation]:
56+
source = path.read_text(encoding="utf-8")
57+
try:
58+
tree = ast.parse(source)
59+
except SyntaxError as exc:
60+
raise SystemExit(f"Syntax error parsing {path}: {exc}") from exc
61+
62+
rel_path = str(path.relative_to(repo_root))
63+
violations: list[ImportViolation] = []
64+
for node in ast.walk(tree):
65+
if isinstance(node, ast.Import):
66+
for alias in node.names:
67+
name = alias.name
68+
if name == "tidy3d" or (
69+
name.startswith("tidy3d.") and not name.startswith("tidy3d._common")
70+
):
71+
violations.append(
72+
ImportViolation(
73+
file=rel_path,
74+
line=node.lineno,
75+
statement=_statement(source, node),
76+
)
77+
)
78+
elif isinstance(node, ast.ImportFrom):
79+
if node.level:
80+
continue
81+
module = node.module
82+
if not module:
83+
continue
84+
if module == "tidy3d":
85+
for alias in node.names:
86+
if alias.name != "_common":
87+
violations.append(
88+
ImportViolation(
89+
file=rel_path,
90+
line=node.lineno,
91+
statement=_statement(source, node),
92+
)
93+
)
94+
continue
95+
if module.startswith("tidy3d.") and not module.startswith("tidy3d._common"):
96+
violations.append(
97+
ImportViolation(
98+
file=rel_path,
99+
line=node.lineno,
100+
statement=_statement(source, node),
101+
)
102+
)
103+
return violations
104+
105+
106+
def _statement(source: str, node: ast.AST) -> str:
107+
segment = ast.get_source_segment(source, node)
108+
if segment:
109+
return " ".join(segment.strip().splitlines())
110+
return node.__class__.__name__
111+
112+
113+
if __name__ == "__main__":
114+
main(sys.argv[1:])

tests/config/test_loader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
from click.testing import CliRunner
66
from pydantic import Field
77

8+
from tidy3d._common.config import loader as config_loader # import from common as it is patched
89
from tidy3d.config import get_manager, reload_config
9-
from tidy3d.config import loader as config_loader
1010
from tidy3d.config import registry as config_registry
1111
from tidy3d.config.legacy import finalize_legacy_migration
1212
from tidy3d.config.loader import migrate_legacy_config

tidy3d/_common/__init__.py

Whitespace-only changes.

tidy3d/_common/_runtime.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""Runtime environment detection for tidy3d.
2+
3+
This module must have ZERO dependencies on other tidy3d modules to avoid
4+
circular imports. It is imported very early in the initialization chain.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import sys
10+
11+
# Detect WASM/Pyodide environment where web and filesystem features are unavailable
12+
WASM_BUILD = "pyodide" in sys.modules or sys.platform == "emscripten"
File renamed without changes.

tidy3d/_common/config/__init__.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Tidy3D configuration system public API."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any
6+
7+
from .legacy import (
8+
LegacyConfigWrapper,
9+
LegacyEnvironment,
10+
LegacyEnvironmentConfig,
11+
)
12+
from .manager import ConfigManager
13+
from .registry import (
14+
get_handlers,
15+
get_sections,
16+
register_handler,
17+
register_plugin,
18+
register_section,
19+
)
20+
21+
__all__ = [
22+
"ConfigManager",
23+
"Env",
24+
"Environment",
25+
"EnvironmentConfig",
26+
"config",
27+
"get_handlers",
28+
"get_sections",
29+
"register_handler",
30+
"register_plugin",
31+
"register_section",
32+
]
33+
34+
35+
def _create_manager() -> ConfigManager:
36+
return ConfigManager()
37+
38+
39+
_base_manager = _create_manager()
40+
# TODO(FXC-3827): Drop LegacyConfigWrapper once legacy accessors are removed in Tidy3D 2.12.
41+
_config_wrapper = LegacyConfigWrapper(_base_manager)
42+
config = _config_wrapper
43+
44+
# TODO(FXC-3827): Remove legacy Env exports after deprecation window (planned 2.12).
45+
Environment = LegacyEnvironment
46+
EnvironmentConfig = LegacyEnvironmentConfig
47+
Env: LegacyEnvironment | None = None
48+
49+
50+
def initialize_env() -> None:
51+
"""Initialize legacy Env after sections register."""
52+
53+
global Env
54+
if Env is None:
55+
Env = LegacyEnvironment(_base_manager)
56+
57+
58+
def reload_config(*, profile: str | None = None) -> LegacyConfigWrapper:
59+
"""Recreate the global configuration manager (primarily for tests)."""
60+
61+
global _base_manager, Env
62+
if _base_manager is not None:
63+
try:
64+
_base_manager.apply_web_env({})
65+
except AttributeError:
66+
pass
67+
_base_manager = ConfigManager(profile=profile)
68+
_config_wrapper.reset_manager(_base_manager)
69+
if Env is None:
70+
initialize_env()
71+
Env.reset_manager(_base_manager)
72+
return _config_wrapper
73+
74+
75+
def get_manager() -> ConfigManager:
76+
"""Return the underlying configuration manager instance."""
77+
78+
return _base_manager
79+
80+
81+
def __getattr__(name: str) -> Any:
82+
if name == "Env":
83+
initialize_env()
84+
return Env
85+
return getattr(config, name)

0 commit comments

Comments
 (0)