fix(blocks): attribute values are validated before any write; null/bool/objects refused by tag
block_insert and block_set_attributes applied str() to whatever arrived, so a JSON null became the text None and a non-mapping failed after the INSERT was written. validate_attribute_values (backends/block_specs.py) runs first on both engines: strings pass the one-line rule, ints and finite floats are written as plain digits, everything else is a TypeError naming the tag. 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
b753a12bb6
commit
7f79497928
@@ -204,6 +204,38 @@ def validate_attdef_specs(attdefs) -> list[dict]:
|
||||
return out
|
||||
|
||||
|
||||
def validate_attribute_values(attributes, where: str = "attributes") -> dict[str, str]:
|
||||
"""``{TAG: value}`` for ``block_insert`` / ``block_set_attributes`` as the
|
||||
strings that will be written, refused before any write.
|
||||
|
||||
``str(value)`` used to be applied to whatever arrived, so a JSON ``null``
|
||||
became the text ``None``, ``true`` became ``True`` and an object became its
|
||||
Python repr — silently, on both engines. A string is taken as is (one line,
|
||||
the DXF rule ``_one_line`` enforces); an int or a finite float is written
|
||||
with ``str`` (``101`` → ``"101"``, ``2.5`` → ``"2.5"``); ``bool``, ``None``,
|
||||
lists and objects are refused by tag. ``None`` for the whole mapping means
|
||||
"no attributes".
|
||||
"""
|
||||
if attributes is None:
|
||||
return {}
|
||||
if not isinstance(attributes, dict):
|
||||
raise TypeError(f"{where} must be an object of {{TAG: value}} pairs")
|
||||
out: dict[str, str] = {}
|
||||
for tag, value in attributes.items():
|
||||
if not isinstance(tag, str) or not tag.strip():
|
||||
raise TypeError(f"{where}: every tag must be a non-empty string")
|
||||
label = f"{where}[{tag!r}]"
|
||||
if isinstance(value, str):
|
||||
out[tag] = _one_line(value, label)
|
||||
elif isinstance(value, bool) or value is None or not isinstance(value, (int, float)):
|
||||
raise TypeError(f"{label} must be a string or a number, got {type(value).__name__}")
|
||||
else:
|
||||
if isinstance(value, float) and not math.isfinite(value):
|
||||
raise TypeError(f"{label} must be finite")
|
||||
out[tag] = str(value)
|
||||
return out
|
||||
|
||||
|
||||
def solid_vertices(points: list[tuple[float, float]]) -> list[tuple[float, float]]:
|
||||
"""DXF SOLID draws vtx0→vtx1→vtx3→vtx2, so a quad given in polygon order
|
||||
(a, b, c, d) is stored as (a, b, d, c). Triangles are stored as given."""
|
||||
|
||||
+13
-5
@@ -3315,6 +3315,10 @@ class ComBackend(AutoCADBackend):
|
||||
attributes=None,
|
||||
layer=None,
|
||||
) -> EntityInfo:
|
||||
from backends.block_specs import validate_attribute_values
|
||||
|
||||
values = validate_attribute_values(attributes)
|
||||
|
||||
def _sync():
|
||||
_require_block_defined(_acad_doc(), name)
|
||||
mspace = _msp()
|
||||
@@ -3328,13 +3332,13 @@ class ComBackend(AutoCADBackend):
|
||||
)
|
||||
if layer:
|
||||
ref.Layer = layer
|
||||
if attributes:
|
||||
if values:
|
||||
try:
|
||||
attrs = ref.GetAttributes()
|
||||
for attr in attrs:
|
||||
tag = attr.TagString
|
||||
if tag in attributes:
|
||||
attr.TextString = str(attributes[tag])
|
||||
if tag in values:
|
||||
attr.TextString = values[tag]
|
||||
except Exception as exc:
|
||||
log.debug("Block insert: GetAttributes or attribute setting failed: %s", exc)
|
||||
return _entity_info(ref)
|
||||
@@ -3730,6 +3734,10 @@ class ComBackend(AutoCADBackend):
|
||||
return await self._run(_sync)
|
||||
|
||||
async def block_set_attributes(self, handle, attributes) -> dict:
|
||||
from backends.block_specs import validate_attribute_values
|
||||
|
||||
values = validate_attribute_values(attributes)
|
||||
|
||||
def _sync():
|
||||
doc = _acad_doc()
|
||||
ref = doc.HandleToObject(handle)
|
||||
@@ -3737,8 +3745,8 @@ class ComBackend(AutoCADBackend):
|
||||
updated = []
|
||||
for attr in attrs:
|
||||
tag = attr.TagString
|
||||
if tag in attributes:
|
||||
attr.TextString = str(attributes[tag])
|
||||
if tag in values:
|
||||
attr.TextString = values[tag]
|
||||
updated.append(tag)
|
||||
return {"ok": True, "updated_tags": updated}
|
||||
|
||||
|
||||
@@ -4520,6 +4520,10 @@ class EzdxfBackend(AutoCADBackend):
|
||||
attributes=None,
|
||||
layer=None,
|
||||
) -> EntityInfo:
|
||||
from backends.block_specs import validate_attribute_values
|
||||
|
||||
values = validate_attribute_values(attributes)
|
||||
|
||||
def _sync():
|
||||
msp = self._msp()
|
||||
attribs: dict = {
|
||||
@@ -4531,12 +4535,12 @@ class EzdxfBackend(AutoCADBackend):
|
||||
attribs["layer"] = layer
|
||||
self._require_block(name)
|
||||
ref = msp.add_blockref(name, (float(x), float(y)), dxfattribs=attribs)
|
||||
if attributes:
|
||||
if values:
|
||||
# `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()})
|
||||
ref.add_auto_attribs(values)
|
||||
self._mark_dirty()
|
||||
return _entity_info_dxf(ref)
|
||||
|
||||
@@ -4828,6 +4832,10 @@ class EzdxfBackend(AutoCADBackend):
|
||||
return await self._async(_sync)
|
||||
|
||||
async def block_set_attributes(self, handle, attributes) -> dict:
|
||||
from backends.block_specs import validate_attribute_values
|
||||
|
||||
values = validate_attribute_values(attributes)
|
||||
|
||||
def _sync():
|
||||
ent = self._get_entity(handle)
|
||||
if ent.dxftype() != "INSERT":
|
||||
@@ -4835,8 +4843,8 @@ class EzdxfBackend(AutoCADBackend):
|
||||
updated = []
|
||||
for attrib in ent.attribs:
|
||||
tag = attrib.dxf.tag
|
||||
if tag in attributes:
|
||||
self._write_attrib_value(attrib, str(attributes[tag]))
|
||||
if tag in values:
|
||||
self._write_attrib_value(attrib, values[tag])
|
||||
updated.append(tag)
|
||||
self._mark_dirty()
|
||||
return {"ok": True, "updated_tags": updated}
|
||||
|
||||
Reference in New Issue
Block a user