From 827719fdd0fdc4c1c9130c78b8f17d7d233b53db Mon Sep 17 00:00:00 2001 From: Umut CAN Date: Wed, 16 Sep 2026 21:33:05 +0300 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd --- engineering/standards/__init__.py | 6 ++ engineering/standards/dimstyles.py | 99 +++++++++++++++----- engineering/standards/mleaderstyles.py | 13 +-- engineering/standards/names.py | 38 ++++++++ engineering/standards/textstyles.py | 54 ++++++----- tests/test_standards_presets.py | 124 +++++++++++++++++++++++++ 6 files changed, 282 insertions(+), 52 deletions(-) create mode 100644 engineering/standards/names.py diff --git a/engineering/standards/__init__.py b/engineering/standards/__init__.py index cac391e..90b4f1a 100644 --- a/engineering/standards/__init__.py +++ b/engineering/standards/__init__.py @@ -10,12 +10,15 @@ the tools and the provenance tests read one copy. from __future__ import annotations from engineering.standards.dimstyles import ( + ARROWHEAD_BLOCKS, DIM_VARIABLE_RANGES, DIM_VARIABLE_WHITELIST, PRESET_VARIABLES, PRESETS, + canonical_arrowhead, check_dim_value, describe_preset, + ezdxf_arrowhead, resolve_dimstyle, 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 __all__ = [ + "ARROWHEAD_BLOCKS", "DIM_VARIABLE_RANGES", "DIM_VARIABLE_WHITELIST", "MLEADER_PRESETS", "PRESETS", "PRESET_VARIABLES", "TEXT_PRESETS", + "canonical_arrowhead", "check_dim_value", "describe_preset", + "ezdxf_arrowhead", "resolve_dimstyle", "resolve_font", "resolve_mleaderstyle", diff --git a/engineering/standards/dimstyles.py b/engineering/standards/dimstyles.py index 8e81e42..e6b5f1a 100644 --- a/engineering/standards/dimstyles.py +++ b/engineering/standards/dimstyles.py @@ -25,8 +25,11 @@ from __future__ import annotations import math from typing import Any +from engineering.standards.names import check_name + __all__ = [ "ANSI", + "ARROWHEAD_BLOCKS", "DIM_VARIABLE_RANGES", "DIM_VARIABLE_WHITELIST", "ISO_25", @@ -34,8 +37,10 @@ __all__ = [ "PRESETS", "PRESET_SOURCES", "PRESET_VARIABLES", + "canonical_arrowhead", "check_dim_value", "describe_preset", + "ezdxf_arrowhead", "resolve_dimstyle", "validate_overrides", ] @@ -122,6 +127,23 @@ _LINEWEIGHT_TEXT = ( ) 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 #: 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). #: * ``("nonzero_float", low, high)`` — as ``float`` but ``0`` is refused. #: * ``("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. #: * a ``set`` — one of its members. DIM_VARIABLE_RANGES: dict[str, tuple | set] = { @@ -148,7 +172,7 @@ DIM_VARIABLE_RANGES: dict[str, tuple | set] = { "DIMDSEP": ("char",), "DIMLUNIT": ("int", 1, 6), "DIMZIN": ("int", 0, 15), # a bitmask - "DIMBLK": ("str",), + "DIMBLK": ("arrow",), "DIMTXSTY": ("name",), "DIMLWD": LINEWEIGHT_CODES, "DIMLWE": LINEWEIGHT_CODES, @@ -167,17 +191,56 @@ DIM_VARIABLE_RANGES: dict[str, tuple | set] = { "DIMCLRE": ("int", 0, 256), "DIMCLRT": ("int", 0, 256), "DIMSAH": {0, 1}, - "DIMBLK1": ("str",), - "DIMBLK2": ("str",), + "DIMBLK1": ("arrow",), + "DIMBLK2": ("arrow",), "DIMCEN": ("float", -1e6, 1e6), # negative draws centre lines, 0 none } 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 -#: of fastmcp. -_ILLEGAL_NAME_CHARS = frozenset('<>/\\":;?*|,=`') + +def canonical_arrowhead(key: str, value: Any) -> str: + """``value`` as the arrowhead name DIMBLK stores, or ``TypeError`` / + ``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: @@ -226,21 +289,15 @@ def check_dim_value(name: str, value: Any) -> Any: if not low <= number <= high: raise ValueError(f"{key}: {int(number)} is out of range {low}..{high}") 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): raise TypeError(f"{key}: expected a string, got {type(value).__name__}") - if kind == "char": - if len(value) != 1: - raise ValueError(f"{key}: {value!r} must be exactly one character") - 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" + if len(value) != 1: # "char" + raise ValueError(f"{key}: {value!r} must be exactly one character") + return value def validate_overrides(overrides: dict | None) -> dict[str, Any]: diff --git a/engineering/standards/mleaderstyles.py b/engineering/standards/mleaderstyles.py index 53ac0d1..48a386d 100644 --- a/engineering/standards/mleaderstyles.py +++ b/engineering/standards/mleaderstyles.py @@ -11,6 +11,8 @@ from __future__ import annotations import math from typing import Any +from engineering.standards.names import check_name + __all__ = ["MLEADER_KEYS", "MLEADER_PRESETS", "resolve_mleaderstyle"] 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") -_ILLEGAL_NAME_CHARS = frozenset('<>/\\":;?*|,=`') - def _positive(key: str, value: Any, *, allow_zero: bool = False) -> 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)}" ) if name == "text_style": - if not isinstance(value, str) or not value.strip(): - 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() + values[name] = check_name(name, value, what="text style name") elif name == "landing_gap": values[name] = _positive(name, value, allow_zero=True) else: diff --git a/engineering/standards/names.py b/engineering/standards/names.py new file mode 100644 index 0000000..a1c610f --- /dev/null +++ b/engineering/standards/names.py @@ -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 diff --git a/engineering/standards/textstyles.py b/engineering/standards/textstyles.py index daf6e53..8b2ea94 100644 --- a/engineering/standards/textstyles.py +++ b/engineering/standards/textstyles.py @@ -12,7 +12,11 @@ it, and the caller sees ``font_resolved``. from __future__ import annotations +import math from pathlib import PurePath +from typing import Any + +from engineering.standards.names import check_name __all__ = [ "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. 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]: @@ -67,26 +84,21 @@ def validate_textstyle( ) -> dict: """The typed request both engines write, or a ``ValueError``/``TypeError`` 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°.""" - if not isinstance(name, str) or not name.strip(): - raise ValueError("name: a text style name cannot be empty") - clean = name.strip() - bad = sorted(set(clean) & _ILLEGAL_NAME_CHARS) - if bad: - raise ValueError(f"name: {clean!r} contains characters DXF forbids in a name: {bad}") + the width factor must be positive; the oblique angle is AutoCAD's ±85°; + every number must be finite. The name obeys the DXF symbol-name rule + (``engineering.standards.names``) — this is the only gate on the + ``textstyle_create`` path, and a name that passes must be one the drawing + can still be saved with.""" + clean = check_name("name", name, what="text style name") font_file, known = resolve_font(font) - for key, value in ( - ("height", height), - ("width_factor", width_factor), - ("oblique_deg", oblique_deg), - ): - 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: + height_value = _number("height", height) + width_value = _number("width_factor", width_factor) + oblique_value = _number("oblique_deg", oblique_deg) + if height_value < 0.0: 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") - if abs(float(oblique_deg)) > MAX_OBLIQUE_DEG: + if abs(oblique_value) > MAX_OBLIQUE_DEG: raise ValueError( f"oblique_deg: {oblique_deg!r} is outside AutoCAD's +/-{MAX_OBLIQUE_DEG:g} degrees" ) @@ -94,7 +106,7 @@ def validate_textstyle( "name": clean, "font_file": font_file, "known": known, - "height": float(height), - "width_factor": float(width_factor), - "oblique_deg": float(oblique_deg), + "height": height_value, + "width_factor": width_value, + "oblique_deg": oblique_value, } diff --git a/tests/test_standards_presets.py b/tests/test_standards_presets.py index 3eb0321..cacd97f 100644 --- a/tests/test_standards_presets.py +++ b/tests/test_standards_presets.py @@ -13,6 +13,7 @@ from __future__ import annotations import pytest from engineering.standards.dimstyles import ( + ARROWHEAD_BLOCKS, DIM_VARIABLE_RANGES, DIM_VARIABLE_WHITELIST, LINEWEIGHT_CODES, @@ -20,6 +21,7 @@ from engineering.standards.dimstyles import ( PRESETS, check_dim_value, describe_preset, + ezdxf_arrowhead, resolve_dimstyle, validate_overrides, ) @@ -158,6 +160,12 @@ def test_resolve_leaves_the_preset_table_untouched(): ({"DIMZIN": 16}, "DIMZIN"), ({"DIMTXSTY": ""}, "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"), ({"DIMTFAC": 0}, "DIMTFAC"), ({"DIMCLRT": 257}, "DIMCLRT"), @@ -198,6 +206,110 @@ def test_validate_overrides_alone_is_what_modify_uses(): 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", "ac", "ok_name-1", "ISO-25"): + assert illegal_name_chars(sample) == security.illegal_symbol_name_chars(sample), sample + + # ── text styles ───────────────────────────────────────────────────────────── @@ -258,10 +370,19 @@ def test_validate_textstyle_types_the_request(): [ ({"name": ""}, "name"), ({"name": "A:B"}, "name"), + ({"name": "a\nb"}, "name"), + ({"name": "a\tb"}, "name"), + ({"name": "a\rb"}, "name"), ({"height": -1}, "height"), + ({"height": float("nan")}, "height"), + ({"height": float("inf")}, "height"), ({"width_factor": 0}, "width_factor"), + ({"width_factor": float("nan")}, "width_factor"), + ({"width_factor": float("inf")}, "width_factor"), ({"oblique_deg": 86}, "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): @@ -317,6 +438,9 @@ def test_resolve_mleaderstyle_applies_overrides(): ("iso", {"text_height": -2}, ValueError, "text_height"), ("iso", {"text_style": ""}, 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"], TypeError, "overrides"), ],