fix(task-14): read and write system variables on the ActiveX document, not the Application
`system_get_variable` / `system_set_variable` called `GetVariable` /
`SetVariable` on the object `_acad_app()` returns. Those are `AcadDocument`
members; `AcadApplication` has neither, so on a live seat every call died
with `AttributeError: AutoCAD.Application.GetVariable` before anything
reached AutoCAD -- `drawing_settings` (all keys, read and write) was dead on
the COM engine although both engines were promised. Measured on AutoCAD
2026: `hasattr(app, "GetVariable")` is False. Both methods now resolve the
document through `_acad_doc()`, whose "No drawing is open" refusal is what a
seat without a document reports. Verified live through `ComBackend`:
LIMMAX reads (277.0, 202.5); `drawing_settings({"annotation_scale": "1:50",
"polar_angle": 45, "polar": True})` applies and reads back 1:50 / 0.02 /
45.0 / True in model space.
The test fake shipped with the task had GetVariable / SetVariable on the
*Application*, so the gate asserted a call shape the real object does not
have. `_FakeApp` now carries only `ActiveDocument` + `Documents.Count` and
raises `AttributeError` for the two names, exactly like the real
Application; the sysvar store is a `_FakeDoc` that the real `_acad_doc()`
resolves. Two regression tests: the document is the target (call list), and
no open drawing raises the clear RuntimeError. The older
`tests/test_com_sprint3.py` coercion test pinned the same wrong target and
now fakes the document.
`tests/test_com_backend.py::_measure_backend` replaced the module-level
`_acad_doc` without ever restoring it, so a MagicMock document leaked into
every later test of the session -- invisible while nothing downstream
resolved the document itself, fatal once the sysvar path did. An autouse
fixture in that module restores it.
Not touched (same root cause, other owners): `app.GetVariable` /
`app.SetVariable` in `_ensure_linetype_loaded`, `drawing_info`,
`drawing_audit`, `system_run_command` and `system_run_lisp` in
backends/com_backend.py.
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
df2ab91900
commit
33604c4408
@@ -3990,15 +3990,18 @@ class ComBackend(AutoCADBackend):
|
||||
return await self._run(_sync)
|
||||
|
||||
async def system_get_variable(self, name) -> Any:
|
||||
# GetVariable / SetVariable are members of AcadDocument, not of
|
||||
# AcadApplication: `AutoCAD.Application.GetVariable` raises
|
||||
# AttributeError on a live seat before anything reaches AutoCAD.
|
||||
def _sync():
|
||||
app = _acad_app()
|
||||
return app.GetVariable(name)
|
||||
doc = _acad_doc()
|
||||
return doc.GetVariable(name)
|
||||
|
||||
return await self._run(_sync)
|
||||
|
||||
async def system_set_variable(self, name, value) -> dict:
|
||||
def _sync():
|
||||
app = _acad_app()
|
||||
doc = _acad_doc() # the sysvar host is the document (see system_get_variable)
|
||||
# Coerce to the sysvar's actual type (NEW-com-set-variable-coercion):
|
||||
# AutoCAD rejects e.g. a string "0" for the integer OSMODE. Probe the
|
||||
# current value's type and convert to match; pass through on failure.
|
||||
@@ -4010,7 +4013,7 @@ class ComBackend(AutoCADBackend):
|
||||
# AutoCAD had already applied the write.
|
||||
payload = value
|
||||
try:
|
||||
current = app.GetVariable(name)
|
||||
current = doc.GetVariable(name)
|
||||
if isinstance(current, bool):
|
||||
coerced = bool(int(value)) if isinstance(value, str) else bool(value)
|
||||
elif isinstance(current, int):
|
||||
@@ -4027,7 +4030,7 @@ class ComBackend(AutoCADBackend):
|
||||
payload = _av(coerced) if isinstance(current, (tuple, list)) else coerced
|
||||
except Exception as exc:
|
||||
log.debug("set_variable type probe for %s failed: %s", name, exc)
|
||||
app.SetVariable(name, payload)
|
||||
doc.SetVariable(name, payload)
|
||||
return {"ok": True, "variable": name, "value": coerced}
|
||||
|
||||
return await self._run(_sync)
|
||||
|
||||
@@ -317,6 +317,19 @@ async def test_com_backend_connects():
|
||||
# number it already has?
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _restore_acad_doc():
|
||||
"""`_measure_backend` swaps the module-level `_acad_doc`; put it back after
|
||||
every test here. Left in place, the MagicMock document leaked into every
|
||||
later test of the session that resolves the document itself (the settings
|
||||
facade's COM tests read sysvars through `_acad_doc()`)."""
|
||||
import backends.com_backend as cb
|
||||
|
||||
original = cb._acad_doc
|
||||
yield
|
||||
cb._acad_doc = original
|
||||
|
||||
|
||||
def _measure_backend(entity):
|
||||
"""A ComBackend whose HandleToObject returns exactly this entity."""
|
||||
import backends.com_backend as cb
|
||||
@@ -329,7 +342,7 @@ def _measure_backend(entity):
|
||||
backend._run = _run
|
||||
doc = MagicMock()
|
||||
doc.HandleToObject.return_value = entity
|
||||
cb._acad_doc = lambda: doc
|
||||
cb._acad_doc = lambda: doc # restored by `_restore_acad_doc`
|
||||
return backend
|
||||
|
||||
|
||||
|
||||
@@ -55,14 +55,16 @@ class _Doc:
|
||||
|
||||
|
||||
async def test_set_variable_coerces_string_to_sysvar_int_type(monkeypatch):
|
||||
app = MagicMock()
|
||||
app.GetVariable.return_value = 4159 # OSMODE current value is an int
|
||||
monkeypatch.setattr(cb, "_acad_app", lambda: app)
|
||||
# GetVariable / SetVariable are AcadDocument members (the Application has
|
||||
# neither), so the fake is the document that `_acad_doc()` returns.
|
||||
doc = MagicMock()
|
||||
doc.GetVariable.return_value = 4159 # OSMODE current value is an int
|
||||
monkeypatch.setattr(cb, "_acad_doc", lambda: doc)
|
||||
|
||||
b = _backend_no_executor()
|
||||
out = await b.system_set_variable("OSMODE", "0")
|
||||
|
||||
name, val = app.SetVariable.call_args[0]
|
||||
name, val = doc.SetVariable.call_args[0]
|
||||
assert name == "OSMODE"
|
||||
assert val == 0 and isinstance(val, int) # coerced from "0" to int 0
|
||||
assert out["value"] == 0
|
||||
|
||||
Reference in New Issue
Block a user