fix(task-9): finite text-style numbers, one symbol-name rule, canonical DIMBLK arrowheads

Three review findings on the standards data layer, each a value the layer
let through that a backend would then write as a silent wrong number or a
drawing that can no longer be saved:

* validate_textstyle never checked math.isfinite, unlike its siblings; nan
  and inf passed every comparison and reached the STYLE record verbatim.
  It now has the same _number helper dimstyles and mleaderstyles use.
* The three private _ILLEGAL_NAME_CHARS copies omitted \n \r \t that
  security._FORBIDDEN_SYMBOL_CHARS holds, so 'a\nb' was accepted as a text
  style, DIMTXSTY or leader text style and ezdxf refused the save. The rule
  now lives once in engineering/standards/names.py (fastmcp-free), the
  three modules import it, and a test pins its character set equal to
  security's and its output equal to illegal_symbol_name_chars.
* DIMBLK/DIMBLK1/DIMBLK2 were ("str",): any string went through, and the
  test pinned '_OBLIQUE', which ezdxf's writer refuses at doc.write while
  the COM engine wants exactly that spelling. They are now ("arrow",):
  canonical_arrowhead accepts a built-in with or without its underscore in
  any case (and '', '.', CLOSEDFILLED for the closed filled default), emits
  AutoCAD's documented block-name spelling (also what ezdxf reports after a
  load), and holds user block names to the symbol-name rule; ARROWHEAD_BLOCKS
  is pinned equal to ezdxf's ARROWS vocabulary, and ezdxf_arrowhead is the
  one translation the headless writer needs, proved by exporting all twenty
  headlessly and reading each back as its canonical name.

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-16 21:33:05 +03:00
co-authored by Claude Opus 5
parent 54fac471a9
commit 827719fdd0
6 changed files with 282 additions and 52 deletions
+6
View File
@@ -10,12 +10,15 @@ the tools and the provenance tests read one copy.
from __future__ import annotations from __future__ import annotations
from engineering.standards.dimstyles import ( from engineering.standards.dimstyles import (
ARROWHEAD_BLOCKS,
DIM_VARIABLE_RANGES, DIM_VARIABLE_RANGES,
DIM_VARIABLE_WHITELIST, DIM_VARIABLE_WHITELIST,
PRESET_VARIABLES, PRESET_VARIABLES,
PRESETS, PRESETS,
canonical_arrowhead,
check_dim_value, check_dim_value,
describe_preset, describe_preset,
ezdxf_arrowhead,
resolve_dimstyle, resolve_dimstyle,
validate_overrides, validate_overrides,
) )
@@ -23,14 +26,17 @@ from engineering.standards.mleaderstyles import MLEADER_PRESETS, resolve_mleader
from engineering.standards.textstyles import TEXT_PRESETS, resolve_font, validate_textstyle from engineering.standards.textstyles import TEXT_PRESETS, resolve_font, validate_textstyle
__all__ = [ __all__ = [
"ARROWHEAD_BLOCKS",
"DIM_VARIABLE_RANGES", "DIM_VARIABLE_RANGES",
"DIM_VARIABLE_WHITELIST", "DIM_VARIABLE_WHITELIST",
"MLEADER_PRESETS", "MLEADER_PRESETS",
"PRESETS", "PRESETS",
"PRESET_VARIABLES", "PRESET_VARIABLES",
"TEXT_PRESETS", "TEXT_PRESETS",
"canonical_arrowhead",
"check_dim_value", "check_dim_value",
"describe_preset", "describe_preset",
"ezdxf_arrowhead",
"resolve_dimstyle", "resolve_dimstyle",
"resolve_font", "resolve_font",
"resolve_mleaderstyle", "resolve_mleaderstyle",
+76 -19
View File
@@ -25,8 +25,11 @@ from __future__ import annotations
import math import math
from typing import Any from typing import Any
from engineering.standards.names import check_name
__all__ = [ __all__ = [
"ANSI", "ANSI",
"ARROWHEAD_BLOCKS",
"DIM_VARIABLE_RANGES", "DIM_VARIABLE_RANGES",
"DIM_VARIABLE_WHITELIST", "DIM_VARIABLE_WHITELIST",
"ISO_25", "ISO_25",
@@ -34,8 +37,10 @@ __all__ = [
"PRESETS", "PRESETS",
"PRESET_SOURCES", "PRESET_SOURCES",
"PRESET_VARIABLES", "PRESET_VARIABLES",
"canonical_arrowhead",
"check_dim_value", "check_dim_value",
"describe_preset", "describe_preset",
"ezdxf_arrowhead",
"resolve_dimstyle", "resolve_dimstyle",
"validate_overrides", "validate_overrides",
] ]
@@ -122,6 +127,23 @@ _LINEWEIGHT_TEXT = (
) )
LINEWEIGHT_CODES: frozenset[int] = frozenset(int(code) for code in _LINEWEIGHT_TEXT.split()) LINEWEIGHT_CODES: frozenset[int] = frozenset(int(code) for code in _LINEWEIGHT_TEXT.split())
#: AutoCAD's nineteen named built-in arrowheads, spelled the way the DIMBLK
#: documentation, the ``$DIMBLK`` header, ActiveX and a loaded ezdxf DIMSTYLE
#: all spell them: the block name, underscore first. The twentieth, closed
#: filled, has no block name and is ``""`` (the spec §4.1 default).
#: ``tests/test_standards_presets.py`` pins this set equal to ezdxf's
#: ``ARROWS.__acad__`` so the two vocabularies cannot drift apart unseen.
ARROWHEAD_BLOCKS: frozenset[str] = frozenset(
"_ARCHTICK _BOXBLANK _BOXFILLED _CLOSED _CLOSEDBLANK _DATUMBLANK _DATUMFILLED "
"_DOT _DOTBLANK _DOTSMALL _INTEGRAL _NONE _OBLIQUE _OPEN _OPEN30 _OPEN90 "
"_ORIGIN _ORIGIN2 _SMALL".split()
)
#: The spellings of the closed filled default a caller may send: AutoCAD's own
#: "enter a single period to return to closed filled", and the block name
#: ezdxf gives the arrow it draws for it.
_CLOSED_FILLED_ALIASES = frozenset({".", "CLOSEDFILLED", "_CLOSEDFILLED"})
#: What ``dimstyle_create`` / ``dimstyle_modify`` will write, and the range each #: What ``dimstyle_create`` / ``dimstyle_modify`` will write, and the range each
#: value must sit in. The shape of each entry: #: value must sit in. The shape of each entry:
#: #:
@@ -131,7 +153,9 @@ LINEWEIGHT_CODES: frozenset[int] = frozenset(int(code) for code in _LINEWEIGHT_T
#: * ``("int", low, high)`` — an integer (a float with no fraction is accepted). #: * ``("int", low, high)`` — an integer (a float with no fraction is accepted).
#: * ``("nonzero_float", low, high)`` — as ``float`` but ``0`` is refused. #: * ``("nonzero_float", low, high)`` — as ``float`` but ``0`` is refused.
#: * ``("char",)`` — exactly one character. #: * ``("char",)`` — exactly one character.
#: * ``("str",)`` — any string (arrowhead block names; ``""`` is the closed filled arrow). #: * ``("arrow",)`` — an arrowhead: ``""`` (closed filled), one of
#: :data:`ARROWHEAD_BLOCKS` given with or without its underscore in any case,
#: or a user block name under the symbol-name rule; see :func:`canonical_arrowhead`.
#: * ``("name",)`` — a non-empty DXF symbol-table name. #: * ``("name",)`` — a non-empty DXF symbol-table name.
#: * a ``set`` — one of its members. #: * a ``set`` — one of its members.
DIM_VARIABLE_RANGES: dict[str, tuple | set] = { DIM_VARIABLE_RANGES: dict[str, tuple | set] = {
@@ -148,7 +172,7 @@ DIM_VARIABLE_RANGES: dict[str, tuple | set] = {
"DIMDSEP": ("char",), "DIMDSEP": ("char",),
"DIMLUNIT": ("int", 1, 6), "DIMLUNIT": ("int", 1, 6),
"DIMZIN": ("int", 0, 15), # a bitmask "DIMZIN": ("int", 0, 15), # a bitmask
"DIMBLK": ("str",), "DIMBLK": ("arrow",),
"DIMTXSTY": ("name",), "DIMTXSTY": ("name",),
"DIMLWD": LINEWEIGHT_CODES, "DIMLWD": LINEWEIGHT_CODES,
"DIMLWE": LINEWEIGHT_CODES, "DIMLWE": LINEWEIGHT_CODES,
@@ -167,17 +191,56 @@ DIM_VARIABLE_RANGES: dict[str, tuple | set] = {
"DIMCLRE": ("int", 0, 256), "DIMCLRE": ("int", 0, 256),
"DIMCLRT": ("int", 0, 256), "DIMCLRT": ("int", 0, 256),
"DIMSAH": {0, 1}, "DIMSAH": {0, 1},
"DIMBLK1": ("str",), "DIMBLK1": ("arrow",),
"DIMBLK2": ("str",), "DIMBLK2": ("arrow",),
"DIMCEN": ("float", -1e6, 1e6), # negative draws centre lines, 0 none "DIMCEN": ("float", -1e6, 1e6), # negative draws centre lines, 0 none
} }
DIM_VARIABLE_WHITELIST: frozenset[str] = frozenset(DIM_VARIABLE_RANGES) DIM_VARIABLE_WHITELIST: frozenset[str] = frozenset(DIM_VARIABLE_RANGES)
#: DXF forbids these in a symbol-table name; the same rule ``security.py``
#: applies to block and layer names, restated here so this module stays free def canonical_arrowhead(key: str, value: Any) -> str:
#: of fastmcp. """``value`` as the arrowhead name DIMBLK stores, or ``TypeError`` /
_ILLEGAL_NAME_CHARS = frozenset('<>/\\":;?*|,=`') ``ValueError`` naming ``key``.
``""``, ``"."``, ``CLOSEDFILLED`` and ``_CLOSEDFILLED`` are the closed
filled default and come back as ``""``. A built-in is accepted with or
without its underscore in any case (``oblique``, ``_Dot``) and comes back
as its block name (``_OBLIQUE``, ``_DOT``) — AutoCAD's documented spelling,
what ActiveX takes verbatim and what ezdxf reports after a load. Anything
else is a user block name and must obey the symbol-name rule; whether the
block exists is the backend's question, asked against the open drawing.
"""
if not isinstance(value, str):
raise TypeError(f"{key}: expected a string, got {type(value).__name__}")
if value == "":
return ""
text = value.strip()
if text.upper() in _CLOSED_FILLED_ALIASES:
return ""
if not text:
raise ValueError(
f'{key}: {value!r} is blank; use "" for the closed filled arrowhead or a block name'
)
upper = text.upper()
candidate = upper if upper.startswith("_") else "_" + upper
if candidate in ARROWHEAD_BLOCKS:
return candidate
return check_name(key, text, what="block name")
def ezdxf_arrowhead(canonical: str) -> str:
"""The name ezdxf's DIMSTYLE takes for a :func:`canonical_arrowhead` value.
ezdxf's ``ARROWS`` vocabulary drops the underscore (``_DOT`` → ``DOT``,
``""`` stays ``""``) and its export raises ``DXFValueError`` on ``_DOT``
because no block of that name exists until it creates one from ``DOT``. A
user block is passed through unchanged. The COM engine needs no mapping:
AutoCAD takes the canonical spelling.
"""
if canonical in ARROWHEAD_BLOCKS:
return canonical[1:]
return canonical
def _number(key: str, value: Any) -> float: def _number(key: str, value: Any) -> float:
@@ -226,21 +289,15 @@ def check_dim_value(name: str, value: Any) -> Any:
if not low <= number <= high: if not low <= number <= high:
raise ValueError(f"{key}: {int(number)} is out of range {low}..{high}") raise ValueError(f"{key}: {int(number)} is out of range {low}..{high}")
return int(number) return int(number)
if kind == "arrow":
return canonical_arrowhead(key, value)
if kind == "name":
return check_name(key, value, what="style name")
if not isinstance(value, str): if not isinstance(value, str):
raise TypeError(f"{key}: expected a string, got {type(value).__name__}") raise TypeError(f"{key}: expected a string, got {type(value).__name__}")
if kind == "char": if len(value) != 1: # "char"
if len(value) != 1:
raise ValueError(f"{key}: {value!r} must be exactly one character") raise ValueError(f"{key}: {value!r} must be exactly one character")
return value return value
if kind == "name":
text = value.strip()
if not text:
raise ValueError(f"{key}: a style name cannot be empty")
bad = sorted(set(text) & _ILLEGAL_NAME_CHARS)
if bad:
raise ValueError(f"{key}: {text!r} contains characters DXF forbids in a name: {bad}")
return text
return value # "str"
def validate_overrides(overrides: dict | None) -> dict[str, Any]: def validate_overrides(overrides: dict | None) -> dict[str, Any]:
+3 -10
View File
@@ -11,6 +11,8 @@ from __future__ import annotations
import math import math
from typing import Any from typing import Any
from engineering.standards.names import check_name
__all__ = ["MLEADER_KEYS", "MLEADER_PRESETS", "resolve_mleaderstyle"] __all__ = ["MLEADER_KEYS", "MLEADER_PRESETS", "resolve_mleaderstyle"]
MLEADER_PRESETS: dict[str, dict[str, Any]] = { MLEADER_PRESETS: dict[str, dict[str, Any]] = {
@@ -20,8 +22,6 @@ MLEADER_PRESETS: dict[str, dict[str, Any]] = {
MLEADER_KEYS: tuple[str, ...] = ("arrow_size", "landing_gap", "text_style", "text_height") MLEADER_KEYS: tuple[str, ...] = ("arrow_size", "landing_gap", "text_style", "text_height")
_ILLEGAL_NAME_CHARS = frozenset('<>/\\":;?*|,=`')
def _positive(key: str, value: Any, *, allow_zero: bool = False) -> float: def _positive(key: str, value: Any, *, allow_zero: bool = False) -> float:
if isinstance(value, bool) or not isinstance(value, (int, float)): if isinstance(value, bool) or not isinstance(value, (int, float)):
@@ -51,14 +51,7 @@ def resolve_mleaderstyle(preset: str, overrides: dict | None = None) -> dict[str
f"overrides: {raw_key!r} is not a leader style key; allowed: {list(MLEADER_KEYS)}" f"overrides: {raw_key!r} is not a leader style key; allowed: {list(MLEADER_KEYS)}"
) )
if name == "text_style": if name == "text_style":
if not isinstance(value, str) or not value.strip(): values[name] = check_name(name, value, what="text style name")
raise ValueError("text_style: a text style name cannot be empty")
bad = sorted(set(value.strip()) & _ILLEGAL_NAME_CHARS)
if bad:
raise ValueError(
f"text_style: {value!r} contains characters DXF forbids in a name: {bad}"
)
values[name] = value.strip()
elif name == "landing_gap": elif name == "landing_gap":
values[name] = _positive(name, value, allow_zero=True) values[name] = _positive(name, value, allow_zero=True)
else: else:
+38
View File
@@ -0,0 +1,38 @@
"""The DXF symbol-table name rule, in one place, for the standards data.
``security.illegal_symbol_name_chars`` is the same rule at the tool layer; it
sits behind a fastmcp import and this package stays free of fastmcp, so the
rule is restated here and ``tests/test_standards_presets.py`` pins the two
character sets equal. A style, block or font name that passes here is one
ezdxf will save and AutoCAD will load; ezdxf's own check fires at
``doc.write``, long after a create tool has already said ``ok``.
"""
from __future__ import annotations
from typing import Any
__all__ = ["ILLEGAL_NAME_CHARS", "check_name", "illegal_name_chars"]
#: Characters DXF forbids in a symbol-table name, plus the whitespace that
#: ends a SendCommand segment — byte-for-byte ``security._FORBIDDEN_SYMBOL_CHARS``.
ILLEGAL_NAME_CHARS: frozenset[str] = frozenset('<>/\\":;?*|,=`') | frozenset("\n\r\t")
def illegal_name_chars(name: str) -> list[str]:
"""The characters in ``name`` a DXF symbol-table name cannot hold, sorted."""
return sorted({c for c in name if c in ILLEGAL_NAME_CHARS or ord(c) < 32})
def check_name(key: str, value: Any, *, what: str = "name") -> str:
"""``value`` as a stripped, non-empty, legal symbol-table name, or a
``TypeError`` / ``ValueError`` naming ``key`` (``what`` words the refusal)."""
if not isinstance(value, str):
raise TypeError(f"{key}: expected a string, got {type(value).__name__}")
text = value.strip()
if not text:
raise ValueError(f"{key}: a {what} cannot be empty")
bad = illegal_name_chars(text)
if bad:
raise ValueError(f"{key}: {text!r} contains characters DXF forbids in a name: {bad}")
return text
+33 -21
View File
@@ -12,7 +12,11 @@ it, and the caller sees ``font_resolved``.
from __future__ import annotations from __future__ import annotations
import math
from pathlib import PurePath from pathlib import PurePath
from typing import Any
from engineering.standards.names import check_name
__all__ = [ __all__ = [
"MAX_OBLIQUE_DEG", "MAX_OBLIQUE_DEG",
@@ -31,7 +35,20 @@ TEXT_PRESETS: dict[str, tuple[str, float, float]] = {
#: AutoCAD's own limit for the STYLE command's obliquing angle. #: AutoCAD's own limit for the STYLE command's obliquing angle.
MAX_OBLIQUE_DEG = 85.0 MAX_OBLIQUE_DEG = 85.0
_ILLEGAL_NAME_CHARS = frozenset('<>/\\":;?*|,=`')
def _number(key: str, value: Any) -> float:
"""A finite number, or ``TypeError`` / ``ValueError`` naming ``key``.
``nan`` and ``inf`` pass every ``<`` / ``>`` check, and a backend writing
them verbatim leaves ``50\\nnan`` in the STYLE record — the silent wrong
number the contract forbids.
"""
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise TypeError(f"{key}: expected a number, got {type(value).__name__}")
number = float(value)
if not math.isfinite(number):
raise ValueError(f"{key}: {value!r} is not a finite number")
return number
def resolve_font(font: str) -> tuple[str, bool]: def resolve_font(font: str) -> tuple[str, bool]:
@@ -67,26 +84,21 @@ def validate_textstyle(
) -> dict: ) -> dict:
"""The typed request both engines write, or a ``ValueError``/``TypeError`` """The typed request both engines write, or a ``ValueError``/``TypeError``
naming the field. ``height`` 0 is AutoCAD's "prompt per text" and is legal; naming the field. ``height`` 0 is AutoCAD's "prompt per text" and is legal;
the width factor must be positive; the oblique angle is AutoCAD's ±85°.""" the width factor must be positive; the oblique angle is AutoCAD's ±85°;
if not isinstance(name, str) or not name.strip(): every number must be finite. The name obeys the DXF symbol-name rule
raise ValueError("name: a text style name cannot be empty") (``engineering.standards.names``) — this is the only gate on the
clean = name.strip() ``textstyle_create`` path, and a name that passes must be one the drawing
bad = sorted(set(clean) & _ILLEGAL_NAME_CHARS) can still be saved with."""
if bad: clean = check_name("name", name, what="text style name")
raise ValueError(f"name: {clean!r} contains characters DXF forbids in a name: {bad}")
font_file, known = resolve_font(font) font_file, known = resolve_font(font)
for key, value in ( height_value = _number("height", height)
("height", height), width_value = _number("width_factor", width_factor)
("width_factor", width_factor), oblique_value = _number("oblique_deg", oblique_deg)
("oblique_deg", oblique_deg), if height_value < 0.0:
):
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise TypeError(f"{key}: expected a number, got {type(value).__name__}")
if float(height) < 0.0:
raise ValueError(f"height: {height!r} is negative; 0 means 'ask per text'") raise ValueError(f"height: {height!r} is negative; 0 means 'ask per text'")
if float(width_factor) <= 0.0: if width_value <= 0.0:
raise ValueError(f"width_factor: {width_factor!r} must be greater than 0") raise ValueError(f"width_factor: {width_factor!r} must be greater than 0")
if abs(float(oblique_deg)) > MAX_OBLIQUE_DEG: if abs(oblique_value) > MAX_OBLIQUE_DEG:
raise ValueError( raise ValueError(
f"oblique_deg: {oblique_deg!r} is outside AutoCAD's +/-{MAX_OBLIQUE_DEG:g} degrees" f"oblique_deg: {oblique_deg!r} is outside AutoCAD's +/-{MAX_OBLIQUE_DEG:g} degrees"
) )
@@ -94,7 +106,7 @@ def validate_textstyle(
"name": clean, "name": clean,
"font_file": font_file, "font_file": font_file,
"known": known, "known": known,
"height": float(height), "height": height_value,
"width_factor": float(width_factor), "width_factor": width_value,
"oblique_deg": float(oblique_deg), "oblique_deg": oblique_value,
} }
+124
View File
@@ -13,6 +13,7 @@ from __future__ import annotations
import pytest import pytest
from engineering.standards.dimstyles import ( from engineering.standards.dimstyles import (
ARROWHEAD_BLOCKS,
DIM_VARIABLE_RANGES, DIM_VARIABLE_RANGES,
DIM_VARIABLE_WHITELIST, DIM_VARIABLE_WHITELIST,
LINEWEIGHT_CODES, LINEWEIGHT_CODES,
@@ -20,6 +21,7 @@ from engineering.standards.dimstyles import (
PRESETS, PRESETS,
check_dim_value, check_dim_value,
describe_preset, describe_preset,
ezdxf_arrowhead,
resolve_dimstyle, resolve_dimstyle,
validate_overrides, validate_overrides,
) )
@@ -158,6 +160,12 @@ def test_resolve_leaves_the_preset_table_untouched():
({"DIMZIN": 16}, "DIMZIN"), ({"DIMZIN": 16}, "DIMZIN"),
({"DIMTXSTY": ""}, "DIMTXSTY"), ({"DIMTXSTY": ""}, "DIMTXSTY"),
({"DIMTXSTY": "a/b"}, "DIMTXSTY"), ({"DIMTXSTY": "a/b"}, "DIMTXSTY"),
({"DIMTXSTY": "a\nb"}, "DIMTXSTY"),
({"DIMTXSTY": "a\tb"}, "DIMTXSTY"),
({"DIMBLK": "a/b"}, "DIMBLK"),
({"DIMBLK": "a,b"}, "DIMBLK"),
({"DIMBLK1": "x\ny"}, "DIMBLK1"),
({"DIMBLK2": " "}, "DIMBLK2"),
({"DIMLFAC": 0}, "DIMLFAC"), ({"DIMLFAC": 0}, "DIMLFAC"),
({"DIMTFAC": 0}, "DIMTFAC"), ({"DIMTFAC": 0}, "DIMTFAC"),
({"DIMCLRT": 257}, "DIMCLRT"), ({"DIMCLRT": 257}, "DIMCLRT"),
@@ -198,6 +206,110 @@ def test_validate_overrides_alone_is_what_modify_uses():
assert check_dim_value("DIMBLK", "_OBLIQUE") == "_OBLIQUE" assert check_dim_value("DIMBLK", "_OBLIQUE") == "_OBLIQUE"
# ── arrowheads: DIMBLK / DIMBLK1 / DIMBLK2 ──────────────────────────────────
def test_the_builtin_arrowheads_are_autocads_nineteen_named_blocks():
assert ARROWHEAD_BLOCKS == frozenset(
"_ARCHTICK _BOXBLANK _BOXFILLED _CLOSED _CLOSEDBLANK _DATUMBLANK _DATUMFILLED "
"_DOT _DOTBLANK _DOTSMALL _INTEGRAL _NONE _OBLIQUE _OPEN _OPEN30 _OPEN90 "
"_ORIGIN _ORIGIN2 _SMALL".split()
)
def test_the_builtin_arrowheads_are_exactly_ezdxfs_vocabulary():
from ezdxf.render.arrows import ARROWS
assert ARROWHEAD_BLOCKS == {ARROWS.block_name(name) for name in ARROWS.__acad__ if name}
assert ARROWS.block_name("") == "_CLOSEDFILLED", "closed filled is the nameless default"
@pytest.mark.parametrize(
("given", "canonical"),
[
("", ""),
(".", ""),
("CLOSEDFILLED", ""),
("_ClosedFilled", ""),
("_OBLIQUE", "_OBLIQUE"),
("OBLIQUE", "_OBLIQUE"),
("oblique", "_OBLIQUE"),
(" _dot ", "_DOT"),
("Open30", "_OPEN30"),
("none", "_NONE"),
("MYARROW", "MYARROW"),
(" my_arrow-1 ", "my_arrow-1"),
],
)
def test_arrowhead_names_canonicalise_to_autocads_documented_spelling(given, canonical):
for var in ("DIMBLK", "DIMBLK1", "DIMBLK2"):
assert check_dim_value(var, given) == canonical, var
@pytest.mark.parametrize("bad", ["a/b", "a,b", "x\ny", "x\ty", "x\rx", "a\x01b", " ", "\t"])
def test_arrowhead_user_blocks_obey_the_symbol_name_rule(bad):
for var in ("DIMBLK", "DIMBLK1", "DIMBLK2"):
with pytest.raises(ValueError, match=var):
check_dim_value(var, bad)
def test_ezdxf_arrowhead_strips_the_underscore_only_for_built_ins():
assert ezdxf_arrowhead("") == ""
assert ezdxf_arrowhead("_OBLIQUE") == "OBLIQUE"
assert ezdxf_arrowhead("_DOT") == "DOT"
assert ezdxf_arrowhead("MYARROW") == "MYARROW"
assert ezdxf_arrowhead("_MYBLOCK") == "_MYBLOCK", (
"a user block with an underscore is not built in"
)
@pytest.mark.parametrize("canonical", [*sorted(ARROWHEAD_BLOCKS), ""])
def test_every_canonical_arrowhead_exports_headlessly_and_reads_back_as_itself(canonical):
"""The canonical spelling is what ezdxf itself reports after a load; only
its *writer* wants the underscore-less name, and ``ezdxf_arrowhead`` is that
one translation."""
import io
import ezdxf
doc = ezdxf.new("R2018")
style = doc.dimstyles.new("X")
style.dxf.dimblk = ezdxf_arrowhead(canonical)
style.dxf.dimblk1 = ezdxf_arrowhead(canonical)
buffer = io.StringIO()
doc.write(buffer)
buffer.seek(0)
reloaded = ezdxf.read(buffer).dimstyles.get("X")
assert reloaded.dxf.dimblk == canonical
assert reloaded.dxf.dimblk1 == canonical
def test_the_canonical_spelling_is_not_what_the_headless_writer_takes():
"""Why ``ezdxf_arrowhead`` exists: ``_OBLIQUE`` stored verbatim raises at
``doc.write``, after ``dimstyle_create`` would already have said ``ok``."""
import io
import ezdxf
from ezdxf.lldxf.const import DXFValueError
doc = ezdxf.new("R2018")
doc.dimstyles.new("X").dxf.dimblk = "_OBLIQUE"
with pytest.raises(DXFValueError, match="_OBLIQUE"):
doc.write(io.StringIO())
# ── the symbol-name rule ────────────────────────────────────────────────────
def test_the_name_rule_is_the_one_security_applies():
import security
from engineering.standards.names import ILLEGAL_NAME_CHARS, illegal_name_chars
assert ILLEGAL_NAME_CHARS == frozenset(security._FORBIDDEN_SYMBOL_CHARS)
for sample in ("a\nb", "a\tb", "a\rb", "a\x01b", "a/b", "a<b>c", "ok_name-1", "ISO-25"):
assert illegal_name_chars(sample) == security.illegal_symbol_name_chars(sample), sample
# ── text styles ───────────────────────────────────────────────────────────── # ── text styles ─────────────────────────────────────────────────────────────
@@ -258,10 +370,19 @@ def test_validate_textstyle_types_the_request():
[ [
({"name": ""}, "name"), ({"name": ""}, "name"),
({"name": "A:B"}, "name"), ({"name": "A:B"}, "name"),
({"name": "a\nb"}, "name"),
({"name": "a\tb"}, "name"),
({"name": "a\rb"}, "name"),
({"height": -1}, "height"), ({"height": -1}, "height"),
({"height": float("nan")}, "height"),
({"height": float("inf")}, "height"),
({"width_factor": 0}, "width_factor"), ({"width_factor": 0}, "width_factor"),
({"width_factor": float("nan")}, "width_factor"),
({"width_factor": float("inf")}, "width_factor"),
({"oblique_deg": 86}, "oblique_deg"), ({"oblique_deg": 86}, "oblique_deg"),
({"oblique_deg": -90}, "oblique_deg"), ({"oblique_deg": -90}, "oblique_deg"),
({"oblique_deg": float("nan")}, "oblique_deg"),
({"oblique_deg": float("-inf")}, "oblique_deg"),
], ],
) )
def test_validate_textstyle_refuses_by_field(kwargs, fragment): def test_validate_textstyle_refuses_by_field(kwargs, fragment):
@@ -317,6 +438,9 @@ def test_resolve_mleaderstyle_applies_overrides():
("iso", {"text_height": -2}, ValueError, "text_height"), ("iso", {"text_height": -2}, ValueError, "text_height"),
("iso", {"text_style": ""}, ValueError, "text_style"), ("iso", {"text_style": ""}, ValueError, "text_style"),
("iso", {"text_style": "a|b"}, ValueError, "text_style"), ("iso", {"text_style": "a|b"}, ValueError, "text_style"),
("iso", {"text_style": "a\nb"}, ValueError, "text_style"),
("iso", {"text_style": "a\tb"}, ValueError, "text_style"),
("iso", {"arrow_size": float("nan")}, ValueError, "arrow_size"),
("iso", {"arrow_size": "2.5"}, TypeError, "arrow_size"), ("iso", {"arrow_size": "2.5"}, TypeError, "arrow_size"),
("iso", ["arrow_size"], TypeError, "overrides"), ("iso", ["arrow_size"], TypeError, "overrides"),
], ],