fix(task-16): custom keys follow AutoCAD's simple case compare, and on COM AutoCAD routes the write
4fe2d96matched custom keys with str.casefold(), but AutoCAD's key compare is a simple one-to-one case compare, not Unicode full case folding. Measured on AutoCAD 2026 (AddCustomInfo of the second spelling over the first on a scratch document, Close(False)): Project/PROJECT, Grün/GRÜN, é/É, ÿ/Ÿ, ł/Ł, σ/Σ, я/Я and dž/DŽ are one key ('Duplicate key'), while Straße/STRASSE, ẞ/ß, kelvin/Kelvin-sign, µ/Μ, σς/ΣΣ and Dž/DŽ are two keys (both stored) -- every one of those six is casefold()-equal. So with 'Straße' present, `custom={'BEFORE': 'b', 'STRASSE': 's2', 'AFTER': ...}` was routed to SetCustomByKey, AutoCAD answered 'Key not found', and the request raised with subject and BEFORE applied and AFTER never written -- the half-write class the fix claimed to close; a delete of 'STRASSE' over 'Straße' raised the same way; headlessly 'STRASSE' overwrote the 'Straße' value instead of becoming a second tag; and the validator refused {'Straße', 'STRASSE'} with a reason AutoCAD contradicts. COM no longer routes from a Python approximation. An exact spelling goes to SetCustomByKey; anything else is AddCustomInfo, and its 'Duplicate key' (excepinfo scode -2145386475, measured) falls back to SetCustomByKey. A delete is RemoveCustomByKey, and its 'Key not found' (scode -2145386476) is reported as not deleted. Any other com_error keeps propagating. The scode is matched before the English description because the HRESULT text is localised. The headless engine and the validator match on `custom_key_fold`: a per-character fold to lowercase only when lower()/upper() are single characters that round-trip and the character is one of the two; it never changes string length and reproduces every measured pair (ß, ς, µ, the Kelvin sign, Dž, ı and İ stay as they are). Docstrings in settings.py and server.py now state that rule instead of 'case-insensitive'. Tests: parametrized fold over the measured SAME / DISTINCT table (plus a pin that casefold() merges the six DISTINCT pairs, which is why4fe2d96regressed), validator acceptance of each DISTINCT pair, headless side-by-side storage and deletion of each pair, the fake SummaryInfo now matches by the same fold and raises pywintypes.com_error with the measured excepinfo tuples, the COM half-write scenario for each pair (no SetCustomByKey guess, 'Key not found' delete tolerated, rest of the request lands), the 'Duplicate key' fallback, and an unrelated SummaryInfo failure still raising. The scodes were measured live before the change; AutoCAD stopped answering COM (blocked in another agent's command or a modal dialog) before the post-change re-run, so that run is recorded as not done. 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
4fe2d963c0
commit
dda11755cf
@@ -41,6 +41,7 @@ from .base import (
|
||||
)
|
||||
from .contracts.settings import (
|
||||
SUMMARY_FIELDS,
|
||||
custom_key_fold,
|
||||
parse_scale,
|
||||
scale_value,
|
||||
validate_drawing_properties,
|
||||
@@ -6142,24 +6143,26 @@ class EzdxfBackend(AutoCADBackend):
|
||||
"Save as R2004 or newer first."
|
||||
)
|
||||
custom_vars = doc.header.custom_vars
|
||||
# AutoCAD's custom keys are case-insensitive (measured on 2026:
|
||||
# SetCustomByKey("PROJECT") updates an existing "Project" and keeps
|
||||
# that spelling; a second AddCustomInfo is 'Duplicate key'), and
|
||||
# ezdxf's CustomVars are plain case-sensitive tuples, so the match
|
||||
# is done here: an existing tag is updated in place under its
|
||||
# AutoCAD compares custom keys with a simple per-character case
|
||||
# compare (measured on 2026: SetCustomByKey("PROJECT") updates an
|
||||
# existing "Project" and keeps that spelling, a second AddCustomInfo
|
||||
# is 'Duplicate key' -- but 'Straße' and 'STRASSE' are two keys,
|
||||
# which str.casefold() would merge), and ezdxf's CustomVars are
|
||||
# plain case-sensitive tuples, so the match is done here with
|
||||
# custom_key_fold: an existing tag is updated in place under its
|
||||
# stored spelling, never duplicated under a second one.
|
||||
props = custom_vars.properties
|
||||
for key, value in to_write.items():
|
||||
folded = key.casefold()
|
||||
hits = [i for i, (tag, _v) in enumerate(props) if tag.casefold() == folded]
|
||||
folded = custom_key_fold(key)
|
||||
hits = [i for i, (tag, _v) in enumerate(props) if custom_key_fold(tag) == folded]
|
||||
if hits:
|
||||
props[hits[0]] = (props[hits[0]][0], value)
|
||||
else:
|
||||
custom_vars.append(key, value)
|
||||
deleted = []
|
||||
for key in to_delete:
|
||||
folded = key.casefold()
|
||||
kept = [(tag, v) for tag, v in props if tag.casefold() != folded]
|
||||
folded = custom_key_fold(key)
|
||||
kept = [(tag, v) for tag, v in props if custom_key_fold(tag) != folded]
|
||||
if len(kept) != len(props):
|
||||
props[:] = kept
|
||||
deleted.append(key)
|
||||
|
||||
Reference in New Issue
Block a user