fix(blocks): block_define refuses a non-finite base point and a missing layer unless create_layers

base_x/base_y were only float()-cast, so a NaN base point was written into
the BLOCK record; a primitive on a layer the drawing lacks was written as a
dangling reference doc.audit() never flags (and on COM would fail mid-loop
after Blocks.Add). validate_base_point names the key; the layer table is
probed before the first write and the missing names are refused, or created
and reported as layers_created when create_layers=true (new optional kwarg,
contract + both engines + tool).

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-18 00:11:38 +03:00
co-authored by Claude Opus 5
parent 7f79497928
commit 375c9f4c70
7 changed files with 165 additions and 17 deletions
+16 -1
View File
@@ -33,7 +33,10 @@ _POSITIVE = {"r", "height"}
def _scalar(value, where: str, *, positive: bool = False) -> float:
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise TypeError(f"{where} must be a number, got {value!r}")
number = float(value)
try:
number = float(value)
except OverflowError as exc: # a 400-digit JSON integer is an int, not a float
raise TypeError(f"{where} must be finite") from exc
if not math.isfinite(number):
raise TypeError(f"{where} must be finite")
if positive and number <= 0:
@@ -204,6 +207,18 @@ def validate_attdef_specs(attdefs) -> list[dict]:
return out
def validate_base_point(base_x, base_y) -> tuple[float, float]:
"""``block_define``'s base point as finite floats; ``TypeError`` names the key.
Only ``float()`` was applied before, so ``float("nan")`` was written into
the BLOCK record headlessly and would reach AutoCAD as a NaN VARIANT.
"""
return (
_scalar(base_x, "block_define: 'base_x'"),
_scalar(base_y, "block_define: 'base_y'"),
)
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.
+27 -3
View File
@@ -3880,18 +3880,24 @@ class ComBackend(AutoCADBackend):
base_x=0.0,
base_y=0.0,
overwrite=False,
create_layers=False,
) -> dict:
"""A block definition with ATTDEFs from typed specs, through ActiveX.
``Blocks.Add`` creates the definition; each primitive is one ``Add*``
call on the Block object, each ATTDEF one ``AddAttribute``. Overwrite
deletes the existing definition's members and re-adds — the name and
its INSERTs survive. Unit-tested against a fake ActiveX surface;
executed live by ``scripts/smoke_pid_com.py``.
its INSERTs survive. The base point must be finite (a NaN would reach
AutoCAD as a VARIANT) and every primitive layer must exist
(``Layers.Item`` probe before ``Blocks.Add`` — ``obj.Layer = name`` for
an unknown layer would otherwise fail mid-loop with the definition half
written) unless ``create_layers`` adds them first. Unit-tested against
a fake ActiveX surface; executed live by ``scripts/smoke_pid_com.py``.
"""
from backends.block_specs import (
solid_vertices,
validate_attdef_specs,
validate_base_point,
validate_entity_specs,
)
@@ -3903,6 +3909,8 @@ class ComBackend(AutoCADBackend):
clean_name = sanitize_symbol_name(name, kind="block")
ents = validate_entity_specs(entities)
atts = validate_attdef_specs(attdefs or [])
base_xy = validate_base_point(base_x, base_y)
wanted_layers = sorted({spec["layer"] for spec in ents if spec["layer"] != "0"})
# acAlignmentLeft / Center / Right / MiddleCenter
align_map = {"left": 0, "center": 1, "right": 2, "middle_center": 10}
@@ -3923,7 +3931,22 @@ class ComBackend(AutoCADBackend):
"pass overwrite=true to replace its contents"
)
replaced = existing is not None
base = _apoint(float(base_x), float(base_y), 0.0)
missing_layers = []
for layer in wanted_layers:
try:
doc.Layers.Item(layer)
except Exception:
missing_layers.append(layer)
if missing_layers and not create_layers:
raise ValueError(
"block_define: layer(s) "
+ ", ".join(repr(layer) for layer in missing_layers)
+ " do not exist in the drawing; create them with layer_create "
"or pass create_layers=true"
)
for layer in missing_layers:
doc.Layers.Add(layer)
base = _apoint(base_xy[0], base_xy[1], 0.0)
if existing is None:
block = doc.Blocks.Add(base, clean_name)
else:
@@ -3989,6 +4012,7 @@ class ComBackend(AutoCADBackend):
"entity_count": len(ents),
"attdef_count": len(atts),
"replaced": replaced,
"layers_created": missing_layers,
"backend": "com",
}
+12 -2
View File
@@ -58,9 +58,19 @@ class BlockContract(ABC):
base_x: float = 0.0,
base_y: float = 0.0,
overwrite: bool = False,
create_layers: bool = False,
) -> dict:
"""Create (or with ``overwrite`` replace the contents of) a block
definition from typed primitive specs and ATTDEF specs — see
``backends/block_specs.py`` for the spec vocabulary. Returns
``{ok, name, entity_count, attdef_count, replaced, backend}``."""
``backends/block_specs.py`` for the spec vocabulary.
Refusals, all before any write: a malformed spec (``TypeError`` naming
``entities[i]``/``attdefs[i]`` and the key), a non-finite or non-numeric
``base_x``/``base_y`` (``TypeError`` naming it), a name clash without
``overwrite`` (``ValueError``), and a primitive ``layer`` that does not
exist in the drawing (``ValueError`` listing the missing names) unless
``create_layers`` is true, in which case the missing layers are created
with the engine's defaults and listed in ``layers_created``. Returns
``{ok, name, entity_count, attdef_count, replaced, layers_created,
backend}``."""
...
+23 -5
View File
@@ -4976,20 +4976,26 @@ class EzdxfBackend(AutoCADBackend):
base_x=0.0,
base_y=0.0,
overwrite=False,
create_layers=False,
) -> dict:
"""A block definition with ATTDEFs from typed specs.
Validation runs before any write, so a malformed entry leaves no
definition behind. ``overwrite`` replaces the *contents* of an existing
definition rather than deleting it: existing INSERTs keep pointing at
the name and show the new geometry (deleting a referenced block is
refused by both engines; replacing contents works on both).
definition behind: specs, the base point (``base_x``/``base_y`` must be
finite numbers — ``float("nan")`` used to be written into the BLOCK
record) and the layer table (a primitive's ``layer`` must exist, or
``create_layers`` must be true — a dangling layer reference passes
``doc.audit()`` silently). ``overwrite`` replaces the *contents* of an
existing definition rather than deleting it: existing INSERTs keep
pointing at the name and show the new geometry (deleting a referenced
block is refused by both engines; replacing contents works on both).
"""
from ezdxf.enums import TextEntityAlignment
from backends.block_specs import (
solid_vertices,
validate_attdef_specs,
validate_base_point,
validate_entity_specs,
)
from security import sanitize_symbol_name
@@ -5002,6 +5008,8 @@ class EzdxfBackend(AutoCADBackend):
clean_name = sanitize_symbol_name(name, kind="block")
ents = validate_entity_specs(entities)
atts = validate_attdef_specs(attdefs or [])
base = (*validate_base_point(base_x, base_y), 0.0)
wanted_layers = sorted({spec["layer"] for spec in ents if spec["layer"] != "0"})
align_map = {
"left": TextEntityAlignment.LEFT,
"center": TextEntityAlignment.CENTER,
@@ -5018,7 +5026,16 @@ class EzdxfBackend(AutoCADBackend):
"pass overwrite=true to replace its contents"
)
replaced = existing is not None
base = (float(base_x), float(base_y), 0.0)
missing_layers = [layer for layer in wanted_layers if layer not in doc.layers]
if missing_layers and not create_layers:
raise ValueError(
"block_define: layer(s) "
+ ", ".join(repr(layer) for layer in missing_layers)
+ " do not exist in the drawing; create them with layer_create "
"or pass create_layers=true"
)
for layer in missing_layers:
doc.layers.add(layer)
if existing is None:
blk = doc.blocks.new(name=clean_name, base_point=base)
else:
@@ -5085,6 +5102,7 @@ class EzdxfBackend(AutoCADBackend):
"entity_count": len(ents),
"attdef_count": len(atts),
"replaced": replaced,
"layers_created": missing_layers,
"backend": "ezdxf",
}