Fix the macOS crash at import: defer config.py annotations
Build PCM package / build (push) Successful in 7s

First real run on a Mac died before the dialog could open:

    config.py line 15: CELL_UM_OVERRIDE: float | None = None
    TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'

KiCad macOS bundles Python 3.9, where PEP 604 unions in annotations
are evaluated at import time unless deferred - config.py was the one
annotated module without the __future__ import (errors.py has no
annotations, __init__.py is empty).

A new tripwire test walks every shipped module with ast and fails if
a file uses annotations without deferring them, so the next module
added (progress.py was born only last week) cannot regress this
silently while the Windows suite stays green.

Verified for real this time, not audited: the full suite passes under
CPython 3.9.25 with the exact stack a Mac venv resolves (numpy 2.0.2,
scipy 1.13.1, matplotlib 3.9.4, PySide6 6.10.0, pyamg 5.2.1) - 137
passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
janik
2026-07-23 13:19:21 +07:00
parent f9abc06082
commit 346016ba8f
2 changed files with 54 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
"""Python 3.9 compatibility tripwire.
KiCad's macOS builds bundle Python 3.9 and build the plugin venv with
it (README: Platform notes), while the dev environment runs a current
Python - so nothing else in the suite notices a construct that only
breaks on 3.9. The first real Mac run died at import: a module-level
`float | None` annotation in config.py, evaluated at runtime because
the file lacked the future import (PEP 604 unions need Python 3.10
unless annotations are deferred).
"""
import ast
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SHIPPED = sorted((ROOT / "fill_resistance").glob("*.py"))
SHIPPED.append(ROOT / "fill_res_action.py")
def _has_future_annotations(tree: ast.Module) -> bool:
return any(isinstance(node, ast.ImportFrom)
and node.module == "__future__"
and any(alias.name == "annotations" for alias in node.names)
for node in tree.body)
def _uses_annotations(tree: ast.Module) -> bool:
for node in ast.walk(tree):
if isinstance(node, ast.AnnAssign):
return True
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
if node.returns is not None:
return True
a = node.args
args = (a.posonlyargs + a.args + a.kwonlyargs
+ ([a.vararg] if a.vararg else [])
+ ([a.kwarg] if a.kwarg else []))
if any(arg.annotation is not None for arg in args):
return True
return False
def test_annotated_modules_defer_annotations():
offenders = []
for path in SHIPPED:
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
if _uses_annotations(tree) and not _has_future_annotations(tree):
offenders.append(path.name)
assert not offenders, (
f"{offenders} use annotations without 'from __future__ import "
f"annotations': they are evaluated at import time and PEP 604 "
f"unions crash on KiCad's macOS Python 3.9.")