fix(task-16): custom keys follow AutoCAD's simple case compare, and on COM AutoCAD routes the write

4fe2d96 matched 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 why 4fe2d96
regressed), 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:
Umut CAN
2026-09-21 22:04:22 +03:00
co-authored by Claude Opus 5
parent 4fe2d963c0
commit dda11755cf
5 changed files with 297 additions and 55 deletions
+11 -8
View File
@@ -6969,14 +6969,17 @@ async def drawing_properties_set(
and unicode are fine — `ValueError` naming the key, on both engines; a
delete is exempt because RemoveCustomByKey never validates syntax, so a
key that reached the drawing another way stays removable), two keys in
one request that differ only by case (custom keys are case-insensitive in
AutoCAD), a line break in a custom key or value (it corrupts the DXF on
save), and headlessly a document older than R2004 (`ValueError` — ezdxf
only writes the custom-property header pairs for AC1018+, so they would
vanish at save; save as R2004 or newer first). A key is matched to the
drawing case-insensitively on both engines and keeps its stored spelling
when updated. On the live engine a custom key is added with AddCustomInfo
when new and changed with SetCustomByKey when present.
one request that AutoCAD would call the same key (its key compare is a
simple per-character case compare: `Project`/`PROJECT` and `Grün`/`GRÜN`
are one key, `Straße`/`STRASSE` are two), a line break in a custom key or
value (it corrupts the DXF on save), and headlessly a document older than
R2004 (`ValueError` — ezdxf only writes the custom-property header pairs
for AC1018+, so they would vanish at save; save as R2004 or newer first).
A key is matched to the drawing by that same rule on both engines and
keeps its stored spelling when updated. On the live engine an exact
spelling is changed with SetCustomByKey; otherwise AutoCAD decides —
AddCustomInfo, and on its 'Duplicate key' SetCustomByKey; a delete is
RemoveCustomByKey, and its 'Key not found' is reported as not deleted.
"""
summary = {
"title": title,