fix(task-2): headless block_set_attributes rewrites the embedded MTEXT; block_explode refuses members ezdxf would silently drop

Two review findings on the headless engine, both content lost with ok: True.

Multi-line attribute writer: `block_set_attributes` set only `attrib.dxf.text`,
so after an edit `block_get_attributes` reported the new value while
`block_explode` (`_burst_attrib`) burst the embedded MTEXT at its pre-edit
content -- and on the default R2010 document, where ezdxf exports no embedded
MTEXT, a save/reload carried the new value the exploded drawing never showed.
`_write_attrib_value` now rebuilds the embedded MTEXT from its own virtual
entity with the new content (`virtual_mtext_entity()` + `set_mtext(...,
graphic_properties=False)`, the round trip ezdxf's `BaseAttrib.transform`
uses), keeping placement, style and height; `_attrib_value` reports the
embedded MTEXT's full `Line1\PLine2` content for a multi-line attribute, as
AutoCAD's `TextString` does, so get / set / explode / COM agree.

Opaque members: `block_explode` iterated `virtual_entities()` without a
`skipped_entity_callback`, so ezdxf dropped every member it cannot copy
(OLE2FRAME) or transform (VIEWPORT) with a debug log, an ACAD_PROXY_ENTITY
without proxy graphics contributed nothing, and the reference was then deleted.
`_explodable_members` materializes the list before any write and refuses by
member with the typed capability `explode_opaque_members` (declared on both
engines: headless False with the reason, COM native), naming the block and
the live way out. The block_explode docstrings (backend and tool) now state
this refusal instead of claiming the xref/MINSERT gate covered everything.

Tests: eight new headless cases in tests/test_track_a_hardening.py -- the
multi-line reader, the rewrite (value on every surface, placement unchanged,
clean audit), a `\P` value and the empty value, an R2010 save/reload, the
single-line path unchanged, the opaque-member refusal (nothing written,
reference alive, members named), an ordinary block still exploding under a
uniform scale, and the capability declared on both engines.

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 15:02:44 +03:00
co-authored by Claude Opus 5
parent 6cb7a13c66
commit e7dafb85ab
4 changed files with 263 additions and 8 deletions
+17 -4
View File
@@ -3040,8 +3040,12 @@ async def block_explode(
(one TEXT or MTEXT handle per attribute), `exploded_handle` and
`backend`. Refused, with nothing written: a handle that is not a block
reference, a reference nested inside a block definition (explode the
outer reference instead), an external reference (bind it first), and a
MINSERT grid. Not undoable except through `drawing_undo` / a transaction.
outer reference instead), an external reference (bind it first), a
MINSERT grid, and -- headless only, capability `explode_opaque_members`
-- a block holding a member ezdxf cannot transform (an OLE2FRAME logo, a
VIEWPORT, a proxy entity without proxy graphics), named by member; the
live engine hands those to AutoCAD. Not undoable except through
`drawing_undo` / a transaction.
"""
await ctx.warning(f"Exploding block reference {handle}")
return await _backend(ctx).block_explode(handle)
@@ -3056,7 +3060,11 @@ async def block_get_attributes(
handle: Annotated[str, "Block reference (INSERT) entity handle"],
ctx: Context = None,
) -> dict:
"""Get all attribute values from a block reference as {TAG: value} dict."""
"""Get all attribute values from a block reference as {TAG: value} dict.
A multi-line attribute reports its whole content with `\\P` between the
lines (AutoCAD's `TextString`), on both engines.
"""
return await _backend(ctx).block_get_attributes(handle)
@@ -3073,7 +3081,12 @@ async def block_set_attributes(
attributes: Annotated[dict, "Attribute values to update: {TAG: new_value}"],
ctx: Context = None,
) -> dict:
"""Update attribute values in a block reference."""
"""Update attribute values in a block reference.
A multi-line attribute takes `Line1\\PLine2` and keeps every line; the
value written is the one `block_get_attributes` reads back and
`block_explode` bursts, on both engines.
"""
return await _backend(ctx).block_set_attributes(handle, attributes)