Files
autocad-mcp-pro/engineering/environment/preferences.py
T
Umut CANandClaude Opus 5 b83a74e969 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
2026-09-21 18:34:09 +03:00

147 lines
5.3 KiB
Python

"""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