From be7dfa86d313668fc8ad508e5c3009854231abf8 Mon Sep 17 00:00:00 2001 From: Umut CAN Date: Tue, 15 Sep 2026 13:10:41 +0300 Subject: [PATCH 1/2] fix(ezdxf): block_insert with attributes referenced an anonymous *U block and lost its ATTRIBs Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd --- backends/ezdxf_backend.py | 11 +++---- tests/test_block_insert_attributes.py | 43 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 tests/test_block_insert_attributes.py diff --git a/backends/ezdxf_backend.py b/backends/ezdxf_backend.py index c36ad56..73f9acd 100644 --- a/backends/ezdxf_backend.py +++ b/backends/ezdxf_backend.py @@ -4453,12 +4453,13 @@ class EzdxfBackend(AutoCADBackend): } if layer: attribs["layer"] = layer + ref = msp.add_blockref(name, (float(x), float(y)), dxfattribs=attribs) if attributes: - ref = msp.add_auto_blockref( - name, (float(x), float(y)), attributes, dxfattribs=attribs - ) - else: - ref = msp.add_blockref(name, (float(x), float(y)), dxfattribs=attribs) + # `add_auto_blockref` wraps the INSERT in an anonymous *U block, + # so the returned handle names `*U1` and carries no ATTRIBs. + # `add_auto_attribs` attaches ATTRIBs to this INSERT for every + # ATTDEF the definition has; values for tags it lacks are ignored. + ref.add_auto_attribs({str(k): str(v) for k, v in attributes.items()}) self._mark_dirty() return _entity_info_dxf(ref) diff --git a/tests/test_block_insert_attributes.py b/tests/test_block_insert_attributes.py new file mode 100644 index 0000000..b334f8f --- /dev/null +++ b/tests/test_block_insert_attributes.py @@ -0,0 +1,43 @@ +"""block_insert(attributes=...) must reference the named block, not an anonymous wrapper. + +ezdxf's `add_auto_blockref` wraps the INSERT in an anonymous `*U` block. The +handle it returns then reports block_name `*U1`, and `block_get_attributes` +finds no ATTRIBs on it — an insert with attributes silently lost them. +""" + +from __future__ import annotations + +import pytest + +pytestmark = pytest.mark.asyncio + + +def _define_tagged_block(backend, name: str = "TB") -> None: + blk = backend._doc.blocks.new(name=name) + blk.add_line((0, 0), (10, 0)) + blk.add_attdef("TAG", insert=(0, 3), text="", dxfattribs={"height": 2.5}) + + +async def test_insert_with_attributes_references_the_named_block(backend): + _define_tagged_block(backend) + ref = await backend.block_insert("TB", 10, 10, attributes={"TAG": "P-101"}) + + info = await backend.entity_get(ref.handle) + assert info.properties["block_name"] == "TB" + assert await backend.block_get_attributes(ref.handle) == {"TAG": "P-101"} + anonymous = [b.name for b in backend._doc.blocks if b.name.startswith("*U")] + assert anonymous == [], "no anonymous wrapper block may be created" + + +async def test_insert_ignores_values_for_tags_the_block_does_not_define(backend): + _define_tagged_block(backend) + ref = await backend.block_insert("TB", 0, 0, attributes={"TAG": "V-1", "NOPE": "x"}) + assert await backend.block_get_attributes(ref.handle) == {"TAG": "V-1"} + + +async def test_insert_without_attributes_is_unchanged(backend): + _define_tagged_block(backend) + ref = await backend.block_insert("TB", 0, 0) + info = await backend.entity_get(ref.handle) + assert info.properties["block_name"] == "TB" + assert await backend.block_get_attributes(ref.handle) == {} From 6f4bda50287a0198c7059b6a91b88e65bacf5fd5 Mon Sep 17 00:00:00 2001 From: Umut CAN Date: Tue, 15 Sep 2026 13:21:16 +0300 Subject: [PATCH 2/2] fix(task-1): refuse block_insert of an undefined block before writing `add_blockref` never checks the name and `add_auto_attribs` skips autofill when `Insert.block()` resolves to None, so a typo'd block name returned a handle whose attribute values were silently dropped and whose dangling INSERT `doc.audit()` later deletes. Check `name in self._doc.blocks` and raise `ValueError` on both the attribute and no-attribute paths, matching the COM engine's `InsertBlock` refusal for an unknown name. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd --- backends/ezdxf_backend.py | 7 +++++++ tests/test_block_insert_attributes.py | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/backends/ezdxf_backend.py b/backends/ezdxf_backend.py index 73f9acd..2e49151 100644 --- a/backends/ezdxf_backend.py +++ b/backends/ezdxf_backend.py @@ -4453,6 +4453,13 @@ class EzdxfBackend(AutoCADBackend): } if layer: attribs["layer"] = layer + # `add_blockref` never checks the name, and `add_auto_attribs` + # silently skips autofill when `Insert.block()` resolves to None, + # so a typo'd name used to yield a dangling INSERT that dropped its + # attribute values and that `doc.audit()` later deletes. Refuse + # before writing, as COM's `InsertBlock` does for an unknown name. + if name not in self._doc.blocks: + raise ValueError(f"block {name!r} is not defined") ref = msp.add_blockref(name, (float(x), float(y)), dxfattribs=attribs) if attributes: # `add_auto_blockref` wraps the INSERT in an anonymous *U block, diff --git a/tests/test_block_insert_attributes.py b/tests/test_block_insert_attributes.py index b334f8f..59a69f7 100644 --- a/tests/test_block_insert_attributes.py +++ b/tests/test_block_insert_attributes.py @@ -41,3 +41,15 @@ async def test_insert_without_attributes_is_unchanged(backend): info = await backend.entity_get(ref.handle) assert info.properties["block_name"] == "TB" assert await backend.block_get_attributes(ref.handle) == {} + + +@pytest.mark.parametrize("attributes", [{"TAG": "P-101"}, None], ids=["with-attrs", "no-attrs"]) +async def test_insert_of_undefined_block_is_refused_before_writing(backend, attributes): + # `add_blockref` never checks the name and `add_auto_attribs` skips autofill + # when the definition is missing, so a typo'd name used to return a handle + # whose attribute values were dropped and which `doc.audit()` later deletes. + before = len(list(backend._msp())) + with pytest.raises(ValueError, match="'NO_SUCH_BLOCK' is not defined"): + await backend.block_insert("NO_SUCH_BLOCK", 5, 5, attributes=attributes) + assert len(list(backend._msp())) == before, "a refused insert must not write" + assert [b.name for b in backend._doc.blocks if b.name == "NO_SUCH_BLOCK"] == []