fix(task-14): refuse CANNOSCALE on an R12 file headlessly instead of losing it at save

Measured: an R12 (AC1009) document has no OBJECTS section, so ezdxf exports
neither AcDbVariableDictionary/DICTIONARYVAR nor the seeded SCALE objects, and
drawing_save_as keeps AC1009. `_set_annotation_scale` reported `ok: True,
changed 1:1 -> 1:50` and the file reloaded as 1:1 -- the header-only write
that vanishes on reload, the class this task exists to remove, and a breach
of the backend's own R12 honesty convention (viewport_set_scale / lock).

The write is now refused with a ValueError naming R12 and "save as R2000 or
newer", after parse_scale (a malformed value is still named as such) and
before either dictionary is touched, so a refused call leaves the root
dictionary as it found it and drawing_settings still applies the sibling keys
(limits / grid / ortho survive R12). The refusal is listed with the others in
the contract and server docstrings. Pinned by two tests that save, reopen and
read the R12 file back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-17 10:14:05 +03:00
co-authored by Claude Opus 5
parent 909bc47e5e
commit f3ff703144
4 changed files with 78 additions and 2 deletions
+3 -1
View File
@@ -42,7 +42,9 @@ class SettingsContract:
Refusals (per key, in ``errors``; the other keys still apply): an Refusals (per key, in ``errors``; the other keys still apply): an
unknown key; a value outside ``_SETTING_RANGES``; a malformed unknown key; a value outside ``_SETTING_RANGES``; a malformed
``limits`` / ``annotation_scale``; ``polar`` / ``polar_angle`` on the ``limits`` / ``annotation_scale``; ``annotation_scale`` on an R12 file
headlessly (no OBJECTS section, so the value would vanish at save —
save as R2000 or newer first); ``polar`` / ``polar_angle`` on the
headless engine (registry-saved — ``capability: registry_sysvar``); headless engine (registry-saved — ``capability: registry_sysvar``);
``dimstyle`` / ``textstyle`` when the backend has no styles contract. ``dimstyle`` / ``textstyle`` when the backend has no styles contract.
+12
View File
@@ -6045,6 +6045,18 @@ class EzdxfBackend(AutoCADBackend):
def _set_annotation_scale(self, doc, value: Any) -> str: def _set_annotation_scale(self, doc, value: Any) -> str:
name, paper, drawing = parse_scale(value) # refused before any write name, paper, drawing = parse_scale(value) # refused before any write
if self._is_r12():
# Measured: an R12 (AC1009) file has no OBJECTS section, so ezdxf
# exports neither AcDbVariableDictionary/DICTIONARYVAR nor the
# SCALE objects. The in-memory write "succeeded" and reloaded as
# 1:1 -- the header-only-write-that-vanishes class again. Refused
# here, before either dictionary is created, so a refused call
# leaves the root dictionary as it found it.
raise ValueError(
"CANNOSCALE: R12 has no OBJECTS section, so the annotation scale "
"(AcDbVariableDictionary and ACAD_SCALELIST) would be lost on save. "
"Save as R2000 or newer first."
)
self._ensure_scale_entry(doc, name, paper, drawing) self._ensure_scale_entry(doc, name, paper, drawing)
vardict = self._variable_dictionary(doc, create=True) vardict = self._variable_dictionary(doc, create=True)
entry = vardict.get("CANNOSCALE") entry = vardict.get("CANNOSCALE")
+3 -1
View File
@@ -5084,7 +5084,9 @@ async def drawing_settings(
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) and AutoCAD refuses the write on a 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 paper-space layout with no active viewport; the headless engine adds the
scale, seeding AutoCAD's default list first when the drawing has none. scale, seeding AutoCAD's default list first when the drawing has none,
and refuses it on an R12 file (no OBJECTS section, so the value would
vanish at save — save as R2000 or newer first; limits/grid/ortho survive).
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.
""" """
+60
View File
@@ -387,6 +387,66 @@ async def test_cannoscalevalue_is_read_only(backend):
await backend.system_set_variable("CANNOSCALEVALUE", 0.5) await backend.system_set_variable("CANNOSCALEVALUE", 0.5)
async def _r12_backend(tmp_path):
import ezdxf
from backends.ezdxf_backend import EzdxfBackend
path = tmp_path / "r12.dxf"
ezdxf.new("R12").saveas(str(path))
backend = EzdxfBackend()
await backend.connect()
await backend.drawing_open(str(path))
return backend
async def test_annotation_scale_on_r12_is_refused_rather_than_lost(tmp_path):
"""Measured: an R12 (AC1009) file has no OBJECTS section, so ezdxf exports
neither AcDbVariableDictionary nor ACAD_SCALELIST — a CANNOSCALE written to
the in-memory document reported `changed 1:1 -> 1:50` and reloaded as 1:1.
The write is refused up front, before any dictionary is created, and the
other settings on the same call still apply (limits survive R12)."""
backend = await _r12_backend(tmp_path)
try:
# ezdxf seeds an *empty* ACAD_SCALELIST at new(); the refusal must not
# populate it (the default-list seeding) nor create the variable dict.
assert len(backend._doc.rootdict.get("ACAD_SCALELIST")) == 0
with pytest.raises(ValueError, match="R12") as excinfo:
await backend.system_set_variable("CANNOSCALE", "1:50")
assert "R2000 or newer" in str(excinfo.value)
assert backend._doc.rootdict.get("AcDbVariableDictionary") is None
assert len(backend._doc.rootdict.get("ACAD_SCALELIST")) == 0
res = await backend.drawing_settings(
{"annotation_scale": "1:50", "limits": [[0, 0], [420, 297]]}
)
assert res["ok"] is False
assert "R12" in res["errors"]["annotation_scale"]
assert "annotation_scale" not in res["applied"]
assert res["applied"]["limits"] == [[0, 0], [420, 297]]
out = tmp_path / "r12_out.dxf"
await backend.drawing_save_as(str(out))
await backend.drawing_open(str(out))
assert backend._doc.dxfversion == "AC1009"
read = (await backend.drawing_settings())["settings"]
assert read["annotation_scale"] == {"name": "1:1", "value": 1.0}
assert read["limits"] == [[0.0, 0.0], [420.0, 297.0]]
finally:
await backend.disconnect()
async def test_annotation_scale_on_r12_refuses_a_malformed_value_by_the_value(tmp_path):
"""A malformed scale is the caller's mistake whatever the file version, so
it is named as such — the R12 refusal is not allowed to mask it."""
backend = await _r12_backend(tmp_path)
try:
with pytest.raises(ValueError, match="paper:drawing"):
await backend.system_set_variable("CANNOSCALE", "fifty")
finally:
await backend.disconnect()
def test_both_capability_maps_declare_registry_sysvar(): def test_both_capability_maps_declare_registry_sysvar():
from backends.com_backend import ComBackend from backends.com_backend import ComBackend
from backends.ezdxf_backend import EzdxfBackend from backends.ezdxf_backend import EzdxfBackend