feat(environment): system_launch, preference whitelist and operator prompts on COM; declared refusals headlessly
user_pick_point / user_select answer {cancelled: true} on ESC (DISP_E_EXCEPTION)
and {timed_out: true} past COM_CALL_TIMEOUT; preferences_set refuses read-only,
unknown and out-of-range keys before any ActiveX call.
The read-only key is Files.PrinterStyleSheetPath, not the spec's
PrintStyleSheetPath: IAcadPreferencesFiles has no member of that name
(measured live on AutoCAD 2026 / R25.1), and a misspelt key would make
preferences_get() raise on every live call.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
co-authored by
Claude Opus 5
parent
c7dd72410a
commit
b83a74e969
@@ -1,5 +1,6 @@
|
||||
"""Environment helpers (v1.6 track E): portable layer states, view and UCS
|
||||
argument resolution. Pure data and validation — no ezdxf, no COM."""
|
||||
argument resolution, the preference whitelist. Pure data and validation — no
|
||||
ezdxf, no COM."""
|
||||
|
||||
from .layer_states import ( # noqa: F401
|
||||
DICT_NAME,
|
||||
@@ -12,5 +13,11 @@ from .layer_states import ( # noqa: F401
|
||||
validate_state_name,
|
||||
)
|
||||
from .names import validate_name # noqa: F401
|
||||
from .preferences import ( # noqa: F401
|
||||
PREFERENCE_KEYS,
|
||||
READ_ONLY_KEYS,
|
||||
describe_preferences,
|
||||
validate_preference,
|
||||
)
|
||||
from .ucs import WORLD, resolve_ucs_axes # noqa: F401
|
||||
from .views import resolve_view_args # noqa: F401
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
"""The AutoCAD preferences a model may read and write, as data.
|
||||
|
||||
``Preferences.*`` lives in the running application (registry-backed), not in
|
||||
a drawing, so the headless engine refuses with ``capability: "preferences"``.
|
||||
The whitelist is deliberately short: every key has a type and a range so a
|
||||
write is refused *by name* before any ActiveX call, and the four ``Files.*``
|
||||
paths are read-only because rewriting a support path is how an installation
|
||||
gets broken. ``OpenSave.SaveAsType`` speaks AutoCAD's ``AcSaveAsType`` enum
|
||||
by name in both directions.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
#: AcSaveAsType: each release adds 12; dwg, dxf (+1), template (+2).
|
||||
SAVE_AS_TYPES: dict[str, int] = {
|
||||
"ac2000_dwg": 12,
|
||||
"ac2000_dxf": 13,
|
||||
"ac2000_template": 14,
|
||||
"ac2004_dwg": 24,
|
||||
"ac2004_dxf": 25,
|
||||
"ac2004_template": 26,
|
||||
"ac2007_dwg": 36,
|
||||
"ac2007_dxf": 37,
|
||||
"ac2007_template": 38,
|
||||
"ac2010_dwg": 48,
|
||||
"ac2010_dxf": 49,
|
||||
"ac2010_template": 50,
|
||||
"ac2013_dwg": 60,
|
||||
"ac2013_dxf": 61,
|
||||
"ac2013_template": 62,
|
||||
"ac2018_dwg": 64,
|
||||
"ac2018_dxf": 65,
|
||||
"ac2018_template": 66,
|
||||
}
|
||||
|
||||
#: key -> (kind, spec). kind: "bool" | "int" (spec = (lo, hi)) | "str" | "enum" (spec = name -> value)
|
||||
PREFERENCE_KEYS: dict[str, tuple[str, object]] = {
|
||||
"OpenSave.SaveAsType": ("enum", SAVE_AS_TYPES),
|
||||
"OpenSave.AutoSaveInterval": ("int", (0, 600)),
|
||||
"OpenSave.CreateBackup": ("bool", None),
|
||||
"OpenSave.IncrementalSavePercent": ("int", (0, 100)),
|
||||
"Display.CursorSize": ("int", (1, 100)),
|
||||
"Drafting.AutoSnapMarkerSize": ("int", (1, 20)),
|
||||
"Drafting.AutoSnapTooltip": ("bool", None),
|
||||
"Selection.PickBoxSize": ("int", (0, 50)),
|
||||
"Output.DefaultPlotStyleTable": ("str", None),
|
||||
"Output.DefaultOutputDevice": ("str", None),
|
||||
}
|
||||
|
||||
#: Every key is ``<Preferences member>.<its member>`` verbatim, so ``split_key``
|
||||
#: reaches the ActiveX property without a translation table. The spec spelled
|
||||
#: the third one ``PrintStyleSheetPath``; ``IAcadPreferencesFiles`` has no such
|
||||
#: member (measured on AutoCAD 2026 / R25.1: ``PrinterStyleSheetPath``), and a
|
||||
#: misspelling here would make ``preferences_get()`` raise on every live call.
|
||||
READ_ONLY_KEYS: tuple[str, ...] = (
|
||||
"Files.SupportPath",
|
||||
"Files.TemplateDwgPath",
|
||||
"Files.PrinterStyleSheetPath",
|
||||
"Files.PrinterConfigPath",
|
||||
)
|
||||
|
||||
|
||||
def split_key(key: str) -> tuple[str, str]:
|
||||
"""``"OpenSave.AutoSaveInterval"`` → ``("OpenSave", "AutoSaveInterval")``."""
|
||||
section, _, member = key.partition(".")
|
||||
return section, member
|
||||
|
||||
|
||||
def known_keys() -> list[str]:
|
||||
return list(PREFERENCE_KEYS) + list(READ_ONLY_KEYS)
|
||||
|
||||
|
||||
def validate_preference(key: str, value):
|
||||
"""The value to write, coerced to the key's type — or a refusal naming the key.
|
||||
|
||||
Writes nothing; both engines call it before touching anything.
|
||||
"""
|
||||
if key in READ_ONLY_KEYS:
|
||||
raise ValueError(f"preference {key!r} is read-only")
|
||||
if key not in PREFERENCE_KEYS:
|
||||
raise ValueError(
|
||||
f"unknown preference {key!r}; writable keys: {list(PREFERENCE_KEYS)}; "
|
||||
f"read-only keys: {list(READ_ONLY_KEYS)}"
|
||||
)
|
||||
kind, spec = PREFERENCE_KEYS[key]
|
||||
if kind == "bool":
|
||||
if not isinstance(value, bool):
|
||||
raise TypeError(f"preference {key!r} takes a bool, got {value!r}")
|
||||
return value
|
||||
if kind == "int":
|
||||
whole = isinstance(value, int) and not isinstance(value, bool)
|
||||
whole = whole or (isinstance(value, float) and value.is_integer())
|
||||
if not whole:
|
||||
raise TypeError(f"preference {key!r} takes an int, got {value!r}")
|
||||
number = int(value)
|
||||
lo, hi = spec
|
||||
if not lo <= number <= hi:
|
||||
raise ValueError(f"preference {key!r} must be between {lo} and {hi}, got {number}")
|
||||
return number
|
||||
if kind == "str":
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
raise TypeError(f"preference {key!r} takes a non-empty string, got {value!r}")
|
||||
return value.strip()
|
||||
# enum
|
||||
if isinstance(value, str):
|
||||
name = value.strip().lower()
|
||||
if name in spec:
|
||||
return spec[name]
|
||||
raise ValueError(f"preference {key!r} must be one of {sorted(spec)}, got {value!r}")
|
||||
if isinstance(value, bool) or not isinstance(value, int) or value not in spec.values():
|
||||
raise ValueError(
|
||||
f"preference {key!r} must be one of {sorted(spec)} (or their values), got {value!r}"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def decode_value(key: str, raw):
|
||||
"""What the application returned, in the whitelist's vocabulary (enum names, real bools)."""
|
||||
kind, spec = PREFERENCE_KEYS.get(key, (None, None))
|
||||
if kind == "enum":
|
||||
for name, code in spec.items():
|
||||
if code == raw:
|
||||
return name
|
||||
return raw
|
||||
if kind == "bool":
|
||||
return bool(raw)
|
||||
if kind == "int":
|
||||
return int(raw)
|
||||
return raw
|
||||
|
||||
|
||||
def describe_preferences() -> list[dict]:
|
||||
rows = []
|
||||
for key, (kind, spec) in PREFERENCE_KEYS.items():
|
||||
rows.append(
|
||||
{
|
||||
"key": key,
|
||||
"kind": kind,
|
||||
"range": list(spec) if kind == "int" else None,
|
||||
"enum": sorted(spec) if kind == "enum" else None,
|
||||
"writable": True,
|
||||
}
|
||||
)
|
||||
for key in READ_ONLY_KEYS:
|
||||
rows.append({"key": key, "kind": "str", "range": None, "enum": None, "writable": False})
|
||||
return rows
|
||||
Reference in New Issue
Block a user