fix(task-14): detach an xref out of every space, and stop the live engine inventing facts
Three blocking findings on the refs group, all of them the same shape: a number or a refusal the caller was entitled to read as measured. 1. ezdxf xref_manage: the victim scan and the `inserts` count both walked `doc.modelspace()` only, then dropped the definition with `delete_block(safe=False)` -- the flag that suppresses the in-use check. An xref shown on a paper-space sheet was reported as 1 insert where the drawing held 2, and detaching it left that sheet INSERT with no BLOCK behind it: `ezdxf.recover` reports `FIX 103 UNDEFINED_BLOCK: Deleted entity INSERT(#34) without required BLOCK definition` and the repo's own drawing_audit silently deletes the sheet geometry. The scan now walks `doc.blocks`, which is every entity space (model space, each paper-space tab, every block definition) and is exactly what ezdxf's own safe check searches with `doc.query('INSERT[name==...]i')` -- hence the case-insensitive name match. `delete_block` is now `safe=True`, as the plan's Interfaces block pins it, so a future miss raises instead of writing a corrupt file. 2. COM xref_manage(action='list') reported `kind: "attach"` for every row, a literal constant. Re-measured on this seat's registered type library (C:\Program Files\Common Files\Autodesk Shared\acax25*.tlb, "AutoCAD 2025 Type Library", installed with AutoCAD 2026): IAcadBlock's xref members are exactly IsXRef, Path, Name, Reload, Unload, Bind, Detach, XRefDatabase -- no overlay indicator exists on the interface, so an overlay was reported to the client as an attachment. The row now says `kind: None`, the same honesty the adjacent `inserts` field already had, and the contract plus the tool docstring say so. 3. COM xref_attach had no duplicate-block-name guard, although contracts/refs.py and the tool docstring both promise the refusal and ezdxf implements it. Two different part.dwg files, or a re-attach after a repath, silently re-pointed or half-wrote the definition while the payload claimed the new path. Added `_block_names(doc)` (Blocks.Count / Blocks.Item(index) / block.Name -- the members xref_manage already walks) and the refusal fires before AttachExternalReference is dispatched, with the same message the headless engine gives. Lower-cased, because AutoCAD symbol-table names are case-insensitive. Tests: five new ones, each failing against the unfixed backends -- an xref shown on Layout1 is counted and detached with the drawing, the detached file audits clean through ezdxf.recover, delete_block's in-use check is pinned, the live listing reports kind None (against a fake that models only the measured members, so it cannot enshrine an invented one), and the live attach refuses a held name (xref and plain block, either case) without dispatching. 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
3ef316d520
commit
301610620c
+39
-1
@@ -394,6 +394,24 @@ def _require_block_defined(doc, name):
|
||||
return block
|
||||
|
||||
|
||||
def _block_names(doc) -> set[str]:
|
||||
"""Every block name the drawing holds, lower-cased.
|
||||
|
||||
``doc.Blocks.Count`` / ``doc.Blocks.Item(index)`` / ``block.Name`` are the
|
||||
measured members ``xref_manage`` already walks. The set is lower-cased
|
||||
because AutoCAD symbol-table names are case-insensitive, so ``BASE`` and
|
||||
``Base`` are one block and re-attaching under either name hits the same
|
||||
definition.
|
||||
"""
|
||||
names: set[str] = set()
|
||||
for index in range(doc.Blocks.Count):
|
||||
try:
|
||||
names.add(str(doc.Blocks.Item(index).Name).lower())
|
||||
except Exception: # noqa: BLE001 - a block that cannot name itself cannot collide
|
||||
continue
|
||||
return names
|
||||
|
||||
|
||||
_BUILTIN_LINETYPES = {"continuous", "bylayer", "byblock"}
|
||||
|
||||
#: `drawing_properties_*` field → `IAcadSummaryInfo` property.
|
||||
@@ -1882,6 +1900,17 @@ class ComBackend(AutoCADBackend):
|
||||
|
||||
def _sync():
|
||||
doc = _acad_doc()
|
||||
# The contract and the tool docstring both promise this refusal,
|
||||
# and the ezdxf engine has always had it (`if name in doc.blocks`).
|
||||
# Without it a second attach under a name the drawing already holds
|
||||
# either re-points the existing definition or fails inside AutoCAD,
|
||||
# while the payload still claims the new `path` was attached.
|
||||
# Block names are case-insensitive in AutoCAD, so the comparison is.
|
||||
if name.lower() in _block_names(doc):
|
||||
raise ValueError(
|
||||
f"xref_attach: the drawing already holds a block named {name!r}; "
|
||||
"detach it first with xref_manage(action='detach')"
|
||||
)
|
||||
reference = doc.ModelSpace.AttachExternalReference(
|
||||
str(source),
|
||||
name,
|
||||
@@ -1929,8 +1958,17 @@ class ComBackend(AutoCADBackend):
|
||||
return found
|
||||
|
||||
if action == "list":
|
||||
# `kind` is null, not "attach". Measured from the seat's own
|
||||
# registered type library (acax25*.tlb, "AutoCAD 2025 Type
|
||||
# Library", shipped with AutoCAD 2026): IAcadBlock's xref
|
||||
# members are exactly IsXRef, Path, Name, Reload, Unload,
|
||||
# Bind, Detach, XRefDatabase -- there is no overlay indicator
|
||||
# anywhere on the interface. An overlay (which xref_attach
|
||||
# itself creates, kind='overlay' -> bOverlay=True) is
|
||||
# therefore not knowable here, and a constant "attach" would
|
||||
# read to the caller as measured. Same honesty as `inserts`.
|
||||
rows = [
|
||||
{"name": b.Name, "path": str(b.Path or ""), "kind": "attach", "inserts": None}
|
||||
{"name": b.Name, "path": str(b.Path or ""), "kind": None, "inserts": None}
|
||||
for b in _xref_blocks()
|
||||
]
|
||||
rows.sort(key=lambda row: row["name"])
|
||||
|
||||
Reference in New Issue
Block a user