fix(task-14): seed AutoCAD's default scale list before adding an annotation scale headlessly
An ezdxf-authored drawing has no ACAD_SCALELIST entries, and
`_ensure_scale_entry` appended only the requested scale. AutoCAD repopulates
its default list only when the dictionary is *empty*, so the saved file
opened with a one-entry scale list and `SetVariable("CANNOSCALE", "1:1")`
failed with `Error setting system variable` -- the annotation scale could
no longer be changed in AutoCAD, while the tool reported `ok: True` and the
test pinned the degraded state (`len(keys) == 1`).
An empty list is now seeded with AutoCAD's own default metric list before
the requested scale is considered: the 17 entries a fresh AutoCAD 2026
drawing carries (`Documents.Add()` saved as DXF, MEASUREMENT 1), in that
order and under AutoCAD's own keys A0 ... A9, B0 ... B6. New entries take
the next key in that sequence (`_scale_list_key`) instead of being keyed by
their name; presence is still decided by group 300, so a non-empty
AutoCAD-authored list is never re-seeded or duplicated. Verified live: the
headless file now opens in AutoCAD 2026 with the 17 keys, CANNOSCALE 1:50
current, and switching to 1:1 and 1:100 works.
Tests: the fresh-drawing test asserts the seeded names and keys, that
setting 1:50 appends nothing (it is a default), that 1:25 lands under B7
once, and that the list survives save + reopen with 1:1 present; a new test
pins that *reading* the annotation scale creates nothing (the read side
stays pure, AutoCAD fills an empty list itself); the AutoCAD-keyed list test
now expects the A3 key. The `drawing_settings` docstring says what each
engine does and that AutoCAD refuses the live write on a paper-space layout
with no active viewport (measured).
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
33604c4408
commit
909bc47e5e
+68
-24
@@ -5954,39 +5954,52 @@ class EzdxfBackend(AutoCADBackend):
|
|||||||
return str(tag.value)
|
return str(tag.value)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
# AutoCAD's default metric ACAD_SCALELIST, in its own order (measured:
|
||||||
def _ensure_scale_entry(cls, doc, name: str, paper: float, drawing: float) -> bool:
|
# AutoCAD 2026, MEASUREMENT 1, `Documents.Add()` saved as DXF — 17 SCALE
|
||||||
"""Add `name` to ACAD_SCALELIST unless present; True when added.
|
# objects keyed A0 … A9, B0 … B6). An imperial seat (MEASUREMENT 0) adds the
|
||||||
|
# architectural fractions on top; those are not seeded — every ratio here
|
||||||
|
# is valid in either unit system, and AutoCAD's SCALELISTEDIT can add more.
|
||||||
|
_DEFAULT_SCALE_LIST: tuple[str, ...] = (
|
||||||
|
"1:1",
|
||||||
|
"1:2",
|
||||||
|
"1:4",
|
||||||
|
"1:5",
|
||||||
|
"1:8",
|
||||||
|
"1:10",
|
||||||
|
"1:16",
|
||||||
|
"1:20",
|
||||||
|
"1:30",
|
||||||
|
"1:40",
|
||||||
|
"1:50",
|
||||||
|
"1:100",
|
||||||
|
"2:1",
|
||||||
|
"4:1",
|
||||||
|
"8:1",
|
||||||
|
"10:1",
|
||||||
|
"100:1",
|
||||||
|
)
|
||||||
|
|
||||||
Presence is decided by the SCALE objects' group-300 names, never by
|
@staticmethod
|
||||||
the dictionary key — an AutoCAD-authored drawing keys the list `A0`,
|
def _scale_list_key(index: int) -> str:
|
||||||
`A1`, … (its default metric list already carries 1:1 … 1:100, 2:1),
|
"""The dictionary key AutoCAD gives the ``index``-th SCALE: A0 … A9, B0 …"""
|
||||||
and matching on the key appended a duplicate `1:50` to every one of
|
return f"{chr(ord('A') + index // 10)}{index % 10}"
|
||||||
them. A new entry is keyed by its name unless that key is taken, in
|
|
||||||
which case it takes the next free `A<n>` slot like AutoCAD does.
|
@classmethod
|
||||||
|
def _add_scale_object(cls, doc, scales, name: str, paper: float, drawing: float) -> None:
|
||||||
|
"""Author one SCALE object and file it under the next free AutoCAD-style key.
|
||||||
|
|
||||||
ezdxf has no SCALE entity class, so the object is authored as raw tags
|
ezdxf has no SCALE entity class, so the object is authored as raw tags
|
||||||
and loaded through the factory (a `DXFTagStorage` that exports
|
and loaded through the factory (a `DXFTagStorage` that exports
|
||||||
verbatim). Measured: it round-trips, ezdxf registers the CLASS, and
|
verbatim). Measured: it round-trips, ezdxf registers the CLASS, and
|
||||||
`doc.audit()` is clean after reopen. AutoCAD only honours a CANNOSCALE
|
`doc.audit()` is clean after reopen.
|
||||||
that names an entry of this list.
|
|
||||||
"""
|
"""
|
||||||
from ezdxf.entities import factory
|
from ezdxf.entities import factory
|
||||||
from ezdxf.lldxf.extendedtags import ExtendedTags
|
from ezdxf.lldxf.extendedtags import ExtendedTags
|
||||||
|
|
||||||
scales = doc.rootdict.get("ACAD_SCALELIST")
|
|
||||||
if scales is None:
|
|
||||||
scales = doc.rootdict.add_new_dict("ACAD_SCALELIST")
|
|
||||||
existing_keys = set(scales.keys())
|
existing_keys = set(scales.keys())
|
||||||
for _key, entry in scales.items():
|
index = 0
|
||||||
if cls._scale_entry_name(entry) == name:
|
while cls._scale_list_key(index) in existing_keys:
|
||||||
return False
|
index += 1
|
||||||
key = name
|
|
||||||
if key in existing_keys:
|
|
||||||
index = 0
|
|
||||||
while f"A{index}" in existing_keys:
|
|
||||||
index += 1
|
|
||||||
key = f"A{index}"
|
|
||||||
handle = doc.entitydb.next_handle()
|
handle = doc.entitydb.next_handle()
|
||||||
unit = 1 if paper == drawing else 0
|
unit = 1 if paper == drawing else 0
|
||||||
text = (
|
text = (
|
||||||
@@ -5996,7 +6009,38 @@ class EzdxfBackend(AutoCADBackend):
|
|||||||
entry = factory.load(ExtendedTags.from_text(text), doc)
|
entry = factory.load(ExtendedTags.from_text(text), doc)
|
||||||
doc.entitydb.add(entry)
|
doc.entitydb.add(entry)
|
||||||
doc.objects.add_object(entry)
|
doc.objects.add_object(entry)
|
||||||
scales.add(key, entry)
|
scales.add(cls._scale_list_key(index), entry)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _ensure_scale_entry(cls, doc, name: str, paper: float, drawing: float) -> bool:
|
||||||
|
"""Add `name` to ACAD_SCALELIST unless present; True when added.
|
||||||
|
|
||||||
|
Presence is decided by the SCALE objects' group-300 names, never by
|
||||||
|
the dictionary key — an AutoCAD-authored drawing keys the list `A0`,
|
||||||
|
`A1`, … and only group 300 says `1:50`; matching on the key appended a
|
||||||
|
duplicate `1:50` to every one of them.
|
||||||
|
|
||||||
|
An ezdxf-authored drawing has **no** ACAD_SCALELIST entries at all.
|
||||||
|
AutoCAD repopulates its default list only when the dictionary is
|
||||||
|
empty, so appending the one requested scale to an empty list shipped a
|
||||||
|
file whose scale list *was* that one entry — measured (AutoCAD 2026):
|
||||||
|
`SetVariable("CANNOSCALE", "1:1")` then fails with `Error setting
|
||||||
|
system variable` and the annotation scale cannot be changed in the
|
||||||
|
UI. An empty list is therefore seeded with AutoCAD's own default list
|
||||||
|
first, so the saved file offers what a fresh AutoCAD drawing offers.
|
||||||
|
AutoCAD only honours a CANNOSCALE that names an entry of this list.
|
||||||
|
"""
|
||||||
|
scales = doc.rootdict.get("ACAD_SCALELIST")
|
||||||
|
if scales is None:
|
||||||
|
scales = doc.rootdict.add_new_dict("ACAD_SCALELIST")
|
||||||
|
if len(scales) == 0:
|
||||||
|
for default in cls._DEFAULT_SCALE_LIST:
|
||||||
|
_, default_paper, default_drawing = parse_scale(default)
|
||||||
|
cls._add_scale_object(doc, scales, default, default_paper, default_drawing)
|
||||||
|
for _key, entry in scales.items():
|
||||||
|
if cls._scale_entry_name(entry) == name:
|
||||||
|
return False
|
||||||
|
cls._add_scale_object(doc, scales, name, paper, drawing)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _set_annotation_scale(self, doc, value: Any) -> str:
|
def _set_annotation_scale(self, doc, value: Any) -> str:
|
||||||
|
|||||||
@@ -5082,7 +5082,9 @@ async def drawing_settings(
|
|||||||
headless engine (`capability: registry_sysvar` — AutoCAD keeps them in the
|
headless engine (`capability: registry_sysvar` — AutoCAD keeps them in the
|
||||||
registry, a file cannot); `dimstyle` / `textstyle` when the backend has no
|
registry, a file cannot); `dimstyle` / `textstyle` when the backend has no
|
||||||
styles contract. On the live engine `annotation_scale` must name a scale in
|
styles contract. On the live engine `annotation_scale` must name a scale in
|
||||||
the drawing's scale list (SCALELISTEDIT); the headless engine adds it.
|
the drawing's scale list (SCALELISTEDIT) and AutoCAD refuses the write on a
|
||||||
|
paper-space layout with no active viewport; the headless engine adds the
|
||||||
|
scale, seeding AutoCAD's default list first when the drawing has none.
|
||||||
Headlessly, grid/snap are stored on the active VPORT and the annotation
|
Headlessly, grid/snap are stored on the active VPORT and the annotation
|
||||||
scale in the variable dictionary — the places AutoCAD reads them from.
|
scale in the variable dictionary — the places AutoCAD reads them from.
|
||||||
"""
|
"""
|
||||||
|
|||||||
+115
-14
@@ -17,6 +17,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -156,13 +157,30 @@ async def test_annotation_scale_writes_the_variable_dictionary_and_scale_list(ba
|
|||||||
vardict = backend._doc.rootdict.get("AcDbVariableDictionary")
|
vardict = backend._doc.rootdict.get("AcDbVariableDictionary")
|
||||||
assert vardict["CANNOSCALE"].dxf.value == "1:50"
|
assert vardict["CANNOSCALE"].dxf.value == "1:50"
|
||||||
scales = backend._doc.rootdict.get("ACAD_SCALELIST")
|
scales = backend._doc.rootdict.get("ACAD_SCALELIST")
|
||||||
assert scales.get("1:50") is not None, "AutoCAD only honours a scale that is in the list"
|
assert "1:50" in _scale_names(scales), "AutoCAD only honours a scale that is in the list"
|
||||||
assert await backend.system_get_variable("CANNOSCALEVALUE") == pytest.approx(0.02)
|
assert await backend.system_get_variable("CANNOSCALEVALUE") == pytest.approx(0.02)
|
||||||
|
|
||||||
|
# Regression: the list used to be *only* the requested scale. AutoCAD
|
||||||
|
# repopulates its default list only when the dictionary is empty, so the
|
||||||
|
# saved file opened with a one-entry scale list and `CANNOSCALE` could no
|
||||||
|
# longer be changed there (measured, AutoCAD 2026). An empty list is seeded
|
||||||
|
# with AutoCAD's default metric list, keyed and ordered the way AutoCAD
|
||||||
|
# writes it, so 1:50 was already there and nothing was appended.
|
||||||
|
assert _scale_names(scales) == list(_AUTOCAD_DEFAULT_SCALES)
|
||||||
|
assert list(scales.keys()) == _AUTOCAD_DEFAULT_KEYS
|
||||||
|
|
||||||
# re-setting the same scale is not a change and adds no second list entry
|
# re-setting the same scale is not a change and adds no second list entry
|
||||||
again = await backend.drawing_settings({"annotation_scale": "1:50"})
|
again = await backend.drawing_settings({"annotation_scale": "1:50"})
|
||||||
assert again["changed"] == {}
|
assert again["changed"] == {}
|
||||||
assert len(list(scales.keys())) == 1
|
assert len(list(scales.keys())) == 17
|
||||||
|
|
||||||
|
# a scale outside the default list is appended once, under the next key
|
||||||
|
# AutoCAD itself would use, and the defaults are not seeded twice
|
||||||
|
res = await backend.drawing_settings({"annotation_scale": "1:25"})
|
||||||
|
assert res["ok"] is True, res.get("errors")
|
||||||
|
assert _scale_names(scales) == [*_AUTOCAD_DEFAULT_SCALES, "1:25"]
|
||||||
|
assert list(scales.keys()) == [*_AUTOCAD_DEFAULT_KEYS, "B7"]
|
||||||
|
await backend.drawing_settings({"annotation_scale": "1:50"})
|
||||||
|
|
||||||
path = str(tmp_path / "scale.dxf")
|
path = str(tmp_path / "scale.dxf")
|
||||||
await backend.drawing_save_as(path)
|
await backend.drawing_save_as(path)
|
||||||
@@ -171,9 +189,32 @@ async def test_annotation_scale_writes_the_variable_dictionary_and_scale_list(ba
|
|||||||
"name": "1:50",
|
"name": "1:50",
|
||||||
"value": 0.02,
|
"value": 0.02,
|
||||||
}
|
}
|
||||||
|
reopened = backend._doc.rootdict.get("ACAD_SCALELIST")
|
||||||
|
assert _scale_names(reopened) == [*_AUTOCAD_DEFAULT_SCALES, "1:25"]
|
||||||
|
assert "1:1" in _scale_names(reopened), "AutoCAD refuses a list without its unit scale"
|
||||||
assert backend._doc.audit().has_errors is False
|
assert backend._doc.audit().has_errors is False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reading_the_annotation_scale_never_creates_the_scale_list(backend):
|
||||||
|
"""The read side stays pure: a fresh headless drawing keeps its empty
|
||||||
|
ACAD_SCALELIST (AutoCAD fills it on open) until a scale is written."""
|
||||||
|
assert (await backend.drawing_settings())["settings"]["annotation_scale"] == {
|
||||||
|
"name": "1:1",
|
||||||
|
"value": 1.0,
|
||||||
|
}
|
||||||
|
scales = backend._doc.rootdict.get("ACAD_SCALELIST")
|
||||||
|
assert scales is None or len(list(scales.keys())) == 0
|
||||||
|
|
||||||
|
|
||||||
|
# AutoCAD 2026's default metric list, in its own order and under its own keys
|
||||||
|
# (`Documents.Add()` saved as DXF, MEASUREMENT 1): 17 SCALE objects, A0 … B6.
|
||||||
|
_AUTOCAD_DEFAULT_SCALES = (
|
||||||
|
"1:1", "1:2", "1:4", "1:5", "1:8", "1:10", "1:16", "1:20", "1:30", "1:40",
|
||||||
|
"1:50", "1:100", "2:1", "4:1", "8:1", "10:1", "100:1",
|
||||||
|
) # fmt: skip
|
||||||
|
_AUTOCAD_DEFAULT_KEYS = [f"A{i}" for i in range(10)] + [f"B{i}" for i in range(7)]
|
||||||
|
|
||||||
|
|
||||||
def _seed_autocad_style_scale_list(doc, names):
|
def _seed_autocad_style_scale_list(doc, names):
|
||||||
"""ACAD_SCALELIST the way AutoCAD saves it: keyed A0, A1, ... with the scale
|
"""ACAD_SCALELIST the way AutoCAD saves it: keyed A0, A1, ... with the scale
|
||||||
name only in the SCALE object's group 300 (the default metric list already
|
name only in the SCALE object's group 300 (the default metric list already
|
||||||
@@ -222,11 +263,13 @@ async def test_annotation_scale_matches_an_autocad_keyed_scale_list_by_name(back
|
|||||||
assert _scale_names(scales).count("1:50") == 1
|
assert _scale_names(scales).count("1:50") == 1
|
||||||
assert backend._doc.rootdict.get("AcDbVariableDictionary")["CANNOSCALE"].dxf.value == "1:50"
|
assert backend._doc.rootdict.get("AcDbVariableDictionary")["CANNOSCALE"].dxf.value == "1:50"
|
||||||
|
|
||||||
# a scale the list does not carry is still appended, exactly once, under a
|
# a scale the list does not carry is still appended, exactly once, under
|
||||||
# key that does not collide with AutoCAD's A<n> keys
|
# the next key in AutoCAD's own A0 … A9, B0 … sequence; a non-empty list
|
||||||
|
# is never re-seeded with the defaults
|
||||||
res = await backend.drawing_settings({"annotation_scale": "1:25"})
|
res = await backend.drawing_settings({"annotation_scale": "1:25"})
|
||||||
assert res["ok"] is True, res.get("errors")
|
assert res["ok"] is True, res.get("errors")
|
||||||
assert _scale_names(scales).count("1:25") == 1
|
assert _scale_names(scales).count("1:25") == 1
|
||||||
|
assert list(scales.keys()) == ["A0", "A1", "A2", "A3"]
|
||||||
assert len(list(scales.keys())) == 4
|
assert len(list(scales.keys())) == 4
|
||||||
await backend.drawing_settings({"annotation_scale": "1:25"})
|
await backend.drawing_settings({"annotation_scale": "1:25"})
|
||||||
assert len(list(scales.keys())) == 4
|
assert len(list(scales.keys())) == 4
|
||||||
@@ -383,6 +426,24 @@ async def test_server_tool_carries_the_new_keys(backend):
|
|||||||
|
|
||||||
|
|
||||||
class _FakeApp:
|
class _FakeApp:
|
||||||
|
"""The ActiveX *Application*: it has an ActiveDocument and no sysvar
|
||||||
|
members at all. `GetVariable` / `SetVariable` live on `AcadDocument`;
|
||||||
|
a backend that calls them on the Application raises `AttributeError:
|
||||||
|
AutoCAD.Application.GetVariable` on a live seat before anything reaches
|
||||||
|
AutoCAD (measured, AutoCAD 2026) -- so this fake refuses them the same way
|
||||||
|
instead of quietly answering."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.ActiveDocument = _FakeDoc()
|
||||||
|
self.Documents = SimpleNamespace(Count=1)
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
if name in ("GetVariable", "SetVariable"):
|
||||||
|
raise AttributeError(f"AutoCAD.Application.{name}")
|
||||||
|
raise AttributeError(name)
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeDoc:
|
||||||
"""Records GetVariable / SetVariable; stores what AutoCAD would store."""
|
"""Records GetVariable / SetVariable; stores what AutoCAD would store."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -425,6 +486,8 @@ def com_backend(monkeypatch):
|
|||||||
from backends import com_backend as module
|
from backends import com_backend as module
|
||||||
|
|
||||||
app = _FakeApp()
|
app = _FakeApp()
|
||||||
|
# Only the Application is faked: the real `_acad_doc()` runs and resolves
|
||||||
|
# `ActiveDocument`, so the test exercises the true call path.
|
||||||
monkeypatch.setattr(module, "_acad_app", lambda: app)
|
monkeypatch.setattr(module, "_acad_app", lambda: app)
|
||||||
backend = module.ComBackend()
|
backend = module.ComBackend()
|
||||||
|
|
||||||
@@ -432,11 +495,11 @@ def com_backend(monkeypatch):
|
|||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
monkeypatch.setattr(backend, "_run", _run_inline)
|
monkeypatch.setattr(backend, "_run", _run_inline)
|
||||||
return backend, app
|
return backend, app.ActiveDocument
|
||||||
|
|
||||||
|
|
||||||
async def test_com_facade_sends_autocad_shaped_values(com_backend):
|
async def test_com_facade_sends_autocad_shaped_values(com_backend):
|
||||||
backend, app = com_backend
|
backend, doc = com_backend
|
||||||
res = await backend.drawing_settings(
|
res = await backend.drawing_settings(
|
||||||
{
|
{
|
||||||
"limits": [[0, 0], [500, 350]],
|
"limits": [[0, 0], [500, 350]],
|
||||||
@@ -450,7 +513,7 @@ async def test_com_facade_sends_autocad_shaped_values(com_backend):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert res["ok"] is True, res.get("errors")
|
assert res["ok"] is True, res.get("errors")
|
||||||
sets = [call[1:] for call in app.calls if call[0] == "SetVariable"]
|
sets = [call[1:] for call in doc.calls if call[0] == "SetVariable"]
|
||||||
assert sets == [
|
assert sets == [
|
||||||
("LIMMIN", (0.0, 0.0)),
|
("LIMMIN", (0.0, 0.0)),
|
||||||
("LIMMAX", (500.0, 350.0)),
|
("LIMMAX", (500.0, 350.0)),
|
||||||
@@ -469,15 +532,15 @@ async def test_com_facade_sends_autocad_shaped_values(com_backend):
|
|||||||
async def test_com_point_variables_travel_as_variant_double_arrays(com_backend):
|
async def test_com_point_variables_travel_as_variant_double_arrays(com_backend):
|
||||||
import pythoncom
|
import pythoncom
|
||||||
|
|
||||||
backend, app = com_backend
|
backend, doc = com_backend
|
||||||
seen: dict = {}
|
seen: dict = {}
|
||||||
real_set = app.SetVariable
|
real_set = doc.SetVariable
|
||||||
|
|
||||||
def _capture(name, value):
|
def _capture(name, value):
|
||||||
seen[name] = value
|
seen[name] = value
|
||||||
real_set(name, value)
|
real_set(name, value)
|
||||||
|
|
||||||
app.SetVariable = _capture
|
doc.SetVariable = _capture
|
||||||
res = await backend.system_set_variable("LIMMAX", (300, 200))
|
res = await backend.system_set_variable("LIMMAX", (300, 200))
|
||||||
variant = seen["LIMMAX"]
|
variant = seen["LIMMAX"]
|
||||||
assert variant.varianttype == pythoncom.VT_ARRAY | pythoncom.VT_R8
|
assert variant.varianttype == pythoncom.VT_ARRAY | pythoncom.VT_R8
|
||||||
@@ -498,7 +561,7 @@ async def test_com_point_variable_reply_survives_the_mcp_output_schema(com_backe
|
|||||||
|
|
||||||
import server
|
import server
|
||||||
|
|
||||||
backend, app = com_backend
|
backend, doc = com_backend
|
||||||
|
|
||||||
async def _fake_make_backend():
|
async def _fake_make_backend():
|
||||||
return backend
|
return backend
|
||||||
@@ -507,11 +570,49 @@ async def test_com_point_variable_reply_survives_the_mcp_output_schema(com_backe
|
|||||||
async with Client(server.mcp) as client:
|
async with Client(server.mcp) as client:
|
||||||
res = await client.call_tool("system_set_variable", {"name": "LIMMAX", "value": [300, 200]})
|
res = await client.call_tool("system_set_variable", {"name": "LIMMAX", "value": [300, 200]})
|
||||||
assert res.data == {"ok": True, "variable": "LIMMAX", "value": [300.0, 200.0]}
|
assert res.data == {"ok": True, "variable": "LIMMAX", "value": [300.0, 200.0]}
|
||||||
assert app.store["LIMMAX"] == (300.0, 200.0)
|
assert doc.store["LIMMAX"] == (300.0, 200.0)
|
||||||
|
|
||||||
|
|
||||||
async def test_com_refuses_before_touching_activex(com_backend):
|
async def test_com_refuses_before_touching_activex(com_backend):
|
||||||
backend, app = com_backend
|
backend, doc = com_backend
|
||||||
res = await backend.drawing_settings({"limits": [[0, 0], [0, 0]], "annotation_scale": "x"})
|
res = await backend.drawing_settings({"limits": [[0, 0], [0, 0]], "annotation_scale": "x"})
|
||||||
assert res["ok"] is False
|
assert res["ok"] is False
|
||||||
assert not [call for call in app.calls if call[0] == "SetVariable"]
|
assert not [call for call in doc.calls if call[0] == "SetVariable"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_com_sysvars_are_read_and_written_on_the_document(com_backend):
|
||||||
|
"""Regression: both methods called `GetVariable` / `SetVariable` on the
|
||||||
|
object `_acad_app()` returns, which has neither, so every
|
||||||
|
`drawing_settings` read and write on the live engine died with
|
||||||
|
`AttributeError: AutoCAD.Application.GetVariable`."""
|
||||||
|
backend, doc = com_backend
|
||||||
|
assert await backend.system_get_variable("LIMMAX") == (420.0, 297.0)
|
||||||
|
res = await backend.system_set_variable("GRIDMODE", "1")
|
||||||
|
assert res == {"ok": True, "variable": "GRIDMODE", "value": 1}
|
||||||
|
assert doc.store["GRIDMODE"] == 1
|
||||||
|
assert doc.calls == [
|
||||||
|
("GetVariable", "LIMMAX"),
|
||||||
|
("GetVariable", "GRIDMODE"), # the type probe
|
||||||
|
("SetVariable", "GRIDMODE", 1),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_com_sysvars_need_an_open_drawing(monkeypatch):
|
||||||
|
"""With no document open there is nothing to read the variable from:
|
||||||
|
`_acad_doc()`'s clear refusal is what surfaces, not an ActiveX error."""
|
||||||
|
pytest.importorskip("win32com.client", reason="pywin32 not installed")
|
||||||
|
from backends import com_backend as module
|
||||||
|
|
||||||
|
app = _FakeApp()
|
||||||
|
app.Documents = SimpleNamespace(Count=0)
|
||||||
|
monkeypatch.setattr(module, "_acad_app", lambda: app)
|
||||||
|
backend = module.ComBackend()
|
||||||
|
|
||||||
|
async def _run_inline(func, *args, **kwargs):
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr(backend, "_run", _run_inline)
|
||||||
|
with pytest.raises(RuntimeError, match="No drawing is open"):
|
||||||
|
await backend.system_get_variable("LIMMAX")
|
||||||
|
with pytest.raises(RuntimeError, match="No drawing is open"):
|
||||||
|
await backend.system_set_variable("GRIDMODE", 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user