fix(com): every system variable is read and written on the ActiveX document, not the Application
AcadApplication has no GetVariable/SetVariable members; they live on
AcadDocument. Measured on the running AutoCAD 2026 (25.1s, 2026-09-22,
scratch Documents.Add(), Close(False)):
hasattr(app, 'GetVariable') -> False
hasattr(app, 'SetVariable') -> False
app.GetVariable('INSUNITS') -> AttributeError: ... IAcadApplication
instance ... has no attribute 'GetVariable'
doc.GetVariable('INSUNITS') -> 4
doc.GetVariable('MEASUREMENT') -> 1
doc.GetVariable('FILEDIA') -> 1; doc.SetVariable('FILEDIA', 0) -> None,
reads back 0, restored to 1
doc.GetVariable('AUDITCTL') -> 0, 'CMDACTIVE' -> 0, 'USERS1' -> ''
Fifteen call sites in backends/com_backend.py still went through the
Application inside `try/except ... log.debug`, so on a live seat they
silently degraded: MEASUREMENT defaulted every seat to metric,
drawing_info reported units from a failed INSUNITS read, drawing_audit
never set AUDITCTL, the CMDACTIVE guards of system_run_command /
system_run_lisp never fired and run_lisp never read USERS1 back. All are
rerouted through `_acad_doc()`; the try/except stays only where an
optional read is defaulted (INSUNITS -> "Unknown", AUDITCTL absent on a
host that lacks it, USERS1 after the expression ran) and the log line
says so.
Linetype loading, measured the same way: `SendCommand("_-LINETYPE _LOAD
CENTER acadiso.lin\n\n")` loads nothing with FILEDIA=0 and leaves the
command at `Enter an option [?/Create/Load/Set]:` (LASTPROMPT), which
makes the whole seat answer RPC_E_CALL_REJECTED to every client until
ESC is pressed; with FILEDIA=1 it opens the modal "Select Linetype File"
dialog. `doc.Linetypes.Load("CENTER", "acadiso.lin")` returns None and
the table grows 3 -> 4; a repeat raises 'Duplicate record name', an
unknown name 'Undefined linetype', a missing file 'File system error'.
`_ensure_linetype_loaded` and `linetype_load` now call `Linetypes.Load`
(MEASUREMENT still read on the document to pick the .lin), touch no
FILEDIA, and report a failure with the English excepinfo description.
`apply_standard` reads every variable it will write before the first
style is created, so a read the engine refuses is a refusal with nothing
written (it used to fail after ISO-25 / ISOCP were created and current).
Fakes: every application fake refuses GetVariable/SetVariable
(tests/test_styles.py's COM fixture carried them on the Application,
enshrining the wrong shape); the members exist on document fakes only.
Regression tests per rerouted site: linetype load via Linetypes.Load with
MEASUREMENT on the document, drawing_info units from the document,
AUDITCTL set/restored on the document, CMDACTIVE/USERS1 on the document,
apply_standard refusing before any write.
Live end-to-end with the real ComBackend on a scratch document:
apply_standard('iso') -> INSUNITS 4, ISO-25 (already in acadiso.dwt,
created: false) and ISOCP (created: true) current, second call created:
false / changed: {}; linetype_load('CENTER') -> Linetypes 3 -> 4, FILEDIA
stays 1, repeat already_loaded, 'NOSUCHLT' refused by name;
_ensure_linetype_loaded('HIDDEN') -> 3 -> 4; drawing_info units 'mm';
drawing_audit AUDITCTL 0 -> 0; system_run_command ok;
system_run_lisp('(+ 40 2)') -> '42'.
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
70afd57831
commit
f7528b2fa5
+65
-6
@@ -982,6 +982,18 @@ class _FakeDocument:
|
||||
self._active_text = style
|
||||
|
||||
|
||||
class _FakeApplication:
|
||||
def __init__(self, support):
|
||||
self.Preferences = types.SimpleNamespace(
|
||||
Files=types.SimpleNamespace(SupportPath=str(support))
|
||||
)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in ("GetVariable", "SetVariable"):
|
||||
raise AttributeError(f"AutoCAD.Application.{name}")
|
||||
raise AttributeError(name)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def com_backend(monkeypatch, tmp_path):
|
||||
pytest.importorskip("win32com.client", reason="pywin32 not installed")
|
||||
@@ -998,11 +1010,12 @@ def com_backend(monkeypatch, tmp_path):
|
||||
(support / font).write_bytes(minimal_sfnt(family, subfamily))
|
||||
monkeypatch.setattr(_FakeStyle, "support_folder", support)
|
||||
document = _FakeDocument()
|
||||
app = types.SimpleNamespace(
|
||||
Preferences=types.SimpleNamespace(Files=types.SimpleNamespace(SupportPath=str(support))),
|
||||
GetVariable=document.GetVariable,
|
||||
SetVariable=document.SetVariable,
|
||||
)
|
||||
# The Application carries Preferences and nothing that reads or writes a
|
||||
# system variable: ``hasattr(app, "GetVariable") is False`` on AutoCAD
|
||||
# 2026 (measured 2026-09-22). GetVariable / SetVariable are members of
|
||||
# ``_FakeDocument`` only, so a backend path that still goes through the
|
||||
# Application fails here the way it fails live.
|
||||
app = _FakeApplication(support)
|
||||
monkeypatch.setattr(module, "_acad_doc", lambda: document)
|
||||
monkeypatch.setattr(module, "_acad_app", lambda: app)
|
||||
monkeypatch.setattr(module, "_regen", lambda: None)
|
||||
@@ -1708,16 +1721,62 @@ async def test_com_apply_standard_creates_both_styles_and_sets_units(com_backend
|
||||
document.variables.update(
|
||||
{"INSUNITS": 1, "LUNITS": 4, "AUNITS": 0, "LTSCALE": 1.0, "DIMSCALE": 1.0}
|
||||
)
|
||||
_record_reads(document)
|
||||
result = await apply_standard(backend, "iso", layers=False)
|
||||
assert result["dimstyle"]["created"] is True and result["textstyle"]["created"] is True
|
||||
assert result["settings"]["changed"] == {"INSUNITS": [1, 4], "LUNITS": [4, 2]}
|
||||
names = [call[0] for call in document.calls]
|
||||
assert names[0] == "TextStyles.Add" and "DimStyles.Add" in names
|
||||
assert names[:5] == ["GetVariable"] * 5, "every unit variable is read on the document first"
|
||||
assert [c[1] for c in document.calls[:5]] == [
|
||||
"INSUNITS",
|
||||
"LUNITS",
|
||||
"AUNITS",
|
||||
"LTSCALE",
|
||||
"DIMSCALE",
|
||||
]
|
||||
assert names[5] == "TextStyles.Add" and "DimStyles.Add" in names
|
||||
assert ("ActiveTextStyle", "ISOCP") in document.calls
|
||||
assert document.ActiveDimStyle.Name == "ISO-25"
|
||||
assert document.variables["DIMDSEP"] == "," and document.variables["INSUNITS"] == 4
|
||||
|
||||
|
||||
def _record_reads(document):
|
||||
"""Log ``GetVariable`` into ``document.calls`` too, so a test can see the
|
||||
reads' place among the writes (the fake logs writes only)."""
|
||||
real_get = document.GetVariable
|
||||
|
||||
def _get(name):
|
||||
document.calls.append(("GetVariable", name))
|
||||
return real_get(name)
|
||||
|
||||
document.GetVariable = _get
|
||||
|
||||
|
||||
async def test_com_apply_standard_refuses_before_writing_when_a_sysvar_read_fails(com_backend):
|
||||
"""Regression: the units loop ran *after* both styles were created, and its
|
||||
INSUNITS read went through ``AcadApplication.GetVariable`` -- a member the
|
||||
Application does not have -- so on a live seat the call failed with ISO-25
|
||||
and ISOCP already written and current. Every variable the call will write
|
||||
is read first; a read the document refuses is a refusal before any write."""
|
||||
from engineering.standards.apply import apply_standard
|
||||
|
||||
backend, document = com_backend
|
||||
document.variables.update({"INSUNITS": 4, "LUNITS": 2, "AUNITS": 0}) # no LTSCALE
|
||||
_record_reads(document)
|
||||
with pytest.raises(RuntimeError, match="LTSCALE"):
|
||||
await apply_standard(backend, "iso", layers=False)
|
||||
writes = [c for c in document.calls if c[0] != "GetVariable"]
|
||||
assert writes == [], "nothing was created, made current or set"
|
||||
assert [e.Name for e in document.TextStyles.entries] == ["Standard"]
|
||||
assert [e.Name for e in document.DimStyles.entries] == ["Standard"]
|
||||
# ``units=False`` writes no variable, so it reads none and needs none.
|
||||
result = await apply_standard(backend, "iso", layers=False, units=False)
|
||||
assert result["dimstyle"]["created"] is True and result["settings"] == {
|
||||
"changed": {},
|
||||
"applied": False,
|
||||
}
|
||||
|
||||
|
||||
# ── profile decision ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user