Skip to content

Commit 87aca99

Browse files
Merge branch 'develop' into pre/2.10
2 parents 6373ea8 + 7c41951 commit 87aca99

File tree

3 files changed

+304
-6
lines changed

3 files changed

+304
-6
lines changed

.github/workflows/lint-notebooks.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches: [ develop ]
66
pull_request:
77
branches: [ develop ]
8+
workflow_dispatch:
89

910
permissions:
1011
contents: read
@@ -29,6 +30,9 @@ jobs:
2930
- name: Run ruff lint check
3031
run: uvx ruff check .
3132

33+
- name: Check misc file references
34+
run: python3 misc/check_misc_references.py
35+
3236
- name: Get changed notebook files
3337
id: changed_notebooks
3438
if: github.event_name == 'pull_request'

misc/check_misc_references.py

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Scan all ipynb files in the root directory and check if any file references
4+
misc/ directory files that either don't exist or are not listed in
5+
misc/import_file_mapping.json.
6+
7+
Usage:
8+
python check_misc_references.py # Check only
9+
python check_misc_references.py --fix # Check and auto-fix missing mappings
10+
"""
11+
12+
import argparse
13+
import json
14+
import re
15+
import sys
16+
from pathlib import Path
17+
18+
# Methods that write files to misc directory (these references should be ignored)
19+
WRITE_METHODS = [
20+
"write_gds",
21+
"to_gds_file",
22+
"to_file",
23+
]
24+
25+
# Variable name patterns that indicate output/write file paths
26+
# These are checked as substrings in variable names before "="
27+
WRITE_VAR_PATTERNS = [
28+
"history_fname",
29+
"history_file_path",
30+
]
31+
32+
# File name whitelist - these are output files that should be ignored
33+
WRITE_FILE_WHITELIST = [
34+
"my_medium.json",
35+
"inv_des_diamond_light_extractor.gds",
36+
]
37+
38+
39+
def get_project_root() -> Path:
40+
"""Get the project root directory (parent of misc)."""
41+
return Path(__file__).parent.parent
42+
43+
44+
def load_import_file_mapping(misc_dir: Path) -> dict[str, list[str]]:
45+
"""Load import_file_mapping.json."""
46+
mapping_file = misc_dir / "import_file_mapping.json"
47+
if not mapping_file.exists():
48+
print(f"Error: Cannot find {mapping_file}")
49+
sys.exit(1)
50+
with open(mapping_file, encoding="utf-8") as f:
51+
return json.load(f)
52+
53+
54+
def get_misc_files(misc_dir: Path) -> set[str]:
55+
"""Get all file names in the misc directory."""
56+
return {f.name for f in misc_dir.iterdir() if f.is_file()}
57+
58+
59+
def is_write_context(content: str, match_start: int) -> bool:
60+
"""
61+
Check if the match position is in a write method or write variable context.
62+
Look backwards from the match position to see if it's preceded by a write method
63+
or a variable assignment with a write-related variable name.
64+
"""
65+
# Look at the 200 characters before the match
66+
lookback = content[max(0, match_start - 200) : match_start]
67+
68+
# Check if any write method appears in the lookback context
69+
for method in WRITE_METHODS:
70+
# Match patterns like ".write_gds(" or "write_gds("
71+
if re.search(rf"\.?{re.escape(method)}\s*\([^)]*$", lookback):
72+
return True
73+
74+
# Check if this is a variable assignment with a write-related variable name
75+
# Match patterns like "history_fname = " or "history_fname="
76+
for var_pattern in WRITE_VAR_PATTERNS:
77+
if re.search(rf"{re.escape(var_pattern)}\s*=\s*$", lookback):
78+
return True
79+
80+
return False
81+
82+
83+
def find_misc_references(notebook_path: Path) -> set[str]:
84+
"""
85+
Find all references to misc/ directory in an ipynb file.
86+
Returns a set of referenced file names (without misc/ prefix).
87+
Excludes references that appear in write method contexts.
88+
"""
89+
with open(notebook_path, encoding="utf-8") as f:
90+
content = f.read()
91+
92+
# ipynb files are JSON format, double quotes in strings are escaped as \"
93+
# Match patterns like "misc/xxx", 'misc/xxx', \"misc/xxx\", "./misc/xxx", etc.
94+
# File names contain only valid characters: letters, numbers, underscores, hyphens, dots
95+
patterns = [
96+
# Match escaped quotes \"./misc/xxx\" or \"misc/xxx\"
97+
r"\\\"(?:\./)?misc/([a-zA-Z0-9_\-\.]+)\\\"",
98+
# Match regular quotes "./misc/xxx" or "misc/xxx" or './misc/xxx'
99+
r'["\'](?:\./)?misc/([a-zA-Z0-9_\-\.]+)["\']',
100+
]
101+
102+
references = set()
103+
for pattern in patterns:
104+
for match in re.finditer(pattern, content):
105+
filename = match.group(1).strip()
106+
107+
# Filter out invalid file names
108+
if not filename:
109+
continue
110+
# Skip files starting with . (hidden files)
111+
if filename.startswith("."):
112+
continue
113+
# Skip files ending with _ (likely part of string concatenation)
114+
if filename.endswith("_"):
115+
continue
116+
# Skip files in the whitelist (known output files)
117+
if filename in WRITE_FILE_WHITELIST:
118+
continue
119+
# Skip if this is in a write method context (output file, not input)
120+
if is_write_context(content, match.start()):
121+
continue
122+
123+
references.add(filename)
124+
125+
return references
126+
127+
128+
def check_notebook(
129+
notebook_path: Path,
130+
misc_files: set[str],
131+
import_mapping: dict[str, list[str]],
132+
errors: list[str],
133+
missing_mappings: dict[str, list[str]],
134+
) -> None:
135+
"""Check a single notebook file."""
136+
notebook_name = notebook_path.name
137+
references = find_misc_references(notebook_path)
138+
139+
if not references:
140+
return
141+
142+
# Get the files declared for this notebook in the mapping
143+
declared_files = set(import_mapping.get(notebook_name, []))
144+
145+
for ref in references:
146+
# Check if the file exists in the misc directory
147+
if ref not in misc_files:
148+
errors.append(
149+
f"[FILE NOT FOUND] {notebook_name}: references 'misc/{ref}', "
150+
f"but the file does not exist in misc directory"
151+
)
152+
continue
153+
154+
# Check if the file is declared in import_file_mapping.json
155+
if ref not in declared_files:
156+
errors.append(
157+
f"[NOT IN MAPPING] {notebook_name}: references 'misc/{ref}', "
158+
f"but it is not declared in import_file_mapping.json"
159+
)
160+
# Collect missing mappings for potential fix
161+
if notebook_name not in missing_mappings:
162+
missing_mappings[notebook_name] = []
163+
if ref not in missing_mappings[notebook_name]:
164+
missing_mappings[notebook_name].append(ref)
165+
166+
167+
def check_mapping_keys(
168+
import_mapping: dict[str, list[str]],
169+
notebooks: set[str],
170+
errors: list[str],
171+
) -> None:
172+
"""Check if all keys in import_file_mapping.json are existing notebook files."""
173+
for notebook_name in import_mapping:
174+
if notebook_name not in notebooks:
175+
errors.append(
176+
f"[INVALID MAPPING KEY] '{notebook_name}' in import_file_mapping.json "
177+
f"does not exist as a notebook file"
178+
)
179+
180+
181+
def update_import_file_mapping(
182+
misc_dir: Path,
183+
import_mapping: dict[str, list[str]],
184+
missing_mappings: dict[str, list[str]],
185+
) -> None:
186+
"""Update import_file_mapping.json with missing mappings."""
187+
# Merge missing mappings into import_mapping
188+
for notebook_name, files in missing_mappings.items():
189+
if notebook_name in import_mapping:
190+
# Add new files to existing entry
191+
existing = set(import_mapping[notebook_name])
192+
for f in files:
193+
if f not in existing:
194+
import_mapping[notebook_name].append(f)
195+
else:
196+
# Create new entry
197+
import_mapping[notebook_name] = files
198+
199+
# Write back to file with standard JSON formatting
200+
mapping_file = misc_dir / "import_file_mapping.json"
201+
with open(mapping_file, "w", encoding="utf-8") as f:
202+
json.dump(import_mapping, f, indent=4)
203+
f.write("\n")
204+
205+
print(f"\nUpdated {mapping_file}")
206+
print(f"Added mappings for {len(missing_mappings)} notebook(s)")
207+
208+
209+
def parse_args():
210+
"""Parse command line arguments."""
211+
parser = argparse.ArgumentParser(description="Check misc directory references in notebooks.")
212+
parser.add_argument(
213+
"--fix",
214+
action="store_true",
215+
help="Auto-fix missing mappings by adding them to import_file_mapping.json",
216+
)
217+
return parser.parse_args()
218+
219+
220+
def main():
221+
args = parse_args()
222+
223+
root_dir = get_project_root()
224+
misc_dir = root_dir / "misc"
225+
226+
print(f"Project root: {root_dir}")
227+
print(f"Misc directory: {misc_dir}")
228+
print("-" * 60)
229+
230+
# Load data
231+
import_mapping = load_import_file_mapping(misc_dir)
232+
misc_files = get_misc_files(misc_dir)
233+
234+
print(f"Found {len(misc_files)} files in misc directory")
235+
print(f"Found {len(import_mapping)} notebook mappings in import_file_mapping.json")
236+
print("-" * 60)
237+
238+
# Scan all ipynb files
239+
notebooks = list(root_dir.glob("*.ipynb"))
240+
notebook_names = {nb.name for nb in notebooks}
241+
print(f"Found {len(notebooks)} notebook files")
242+
print("-" * 60)
243+
244+
errors = []
245+
missing_mappings: dict[str, list[str]] = {}
246+
247+
# Check if all mapping keys are valid notebook files
248+
check_mapping_keys(import_mapping, notebook_names, errors)
249+
250+
# Check each notebook for misc references
251+
for notebook_path in sorted(notebooks):
252+
check_notebook(notebook_path, misc_files, import_mapping, errors, missing_mappings)
253+
254+
# Output results
255+
if errors:
256+
print(f"\nFound {len(errors)} issues:\n")
257+
for error in errors:
258+
print(f" X {error}")
259+
print()
260+
261+
# Auto-fix if requested
262+
if args.fix and missing_mappings:
263+
update_import_file_mapping(misc_dir, import_mapping, missing_mappings)
264+
265+
# Only return error if there are FILE NOT FOUND errors
266+
# (missing mappings are fixed if --fix is used)
267+
if args.fix:
268+
file_not_found_errors = [e for e in errors if "[FILE NOT FOUND]" in e]
269+
if file_not_found_errors:
270+
sys.exit(1)
271+
else:
272+
print("\n[OK] Missing mappings have been fixed!\n")
273+
sys.exit(0)
274+
else:
275+
sys.exit(1)
276+
else:
277+
print("\n[OK] All checks passed, no issues found!\n")
278+
sys.exit(0)
279+
280+
281+
if __name__ == "__main__":
282+
main()

misc/import_file_mapping.json

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@
99
"Autograd6GratingCoupler.ipynb": [
1010
"grating_coupler_history_autograd.pkl"
1111
],
12-
"CMOSRGBSensor": [
13-
"red_eps.csv",
14-
"green_eps.csv",
15-
"blue_eps.csv"
12+
"Autograd6GratingCoupler.ipynb": [
13+
"grating_coupler_history_autograd.pkl"
1614
],
1715
"DifferentialStripline.ipynb": [
1816
"stripline_fem_mode.csv",
@@ -28,7 +26,7 @@
2826
"cpw_fem_mode.csv",
2927
"cpw_fit_mode.csv",
3028
"cpw_fem_sparam.csv",
31-
"cpw_fit_sparam"
29+
"cpw_fit_sparam.csv"
3230
],
3331
"CPWRFPhotonics2.ipynb": [
3432
"seg_fem_sparam.csv"
@@ -130,5 +128,19 @@
130128
"mie_magnetic_dipole",
131129
"mie_electric_quadrupole",
132130
"mie_magnetic_quadrupole"
131+
],
132+
"CMOSRGBSensor.ipynb": [
133+
"green_eps.csv",
134+
"blue_eps.csv",
135+
"red_eps.csv"
136+
],
137+
"HeatDissipationSOI.ipynb": [
138+
"heat_dissipation_SOI.xlsx"
139+
],
140+
"KLayoutPlugin_DRCQuickstart.ipynb": [
141+
"drc_runset.drc"
142+
],
143+
"MetasurfaceBIC.ipynb": [
144+
"amorphous_silicon_from_paper.txt"
133145
]
134-
}
146+
}

0 commit comments

Comments
 (0)