From d66d93ab0b03e7bdd00ff195cd6f72325460f0cd Mon Sep 17 00:00:00 2001 From: Tim Hatch Date: Wed, 29 Apr 2026 14:30:42 +0000 Subject: [PATCH 1/6] Fix empty-string key crash in jsonpath_to_pointer The or-chain used truthiness to pick the matched group, so an empty string key ("") would fall through all groups and leave name=None, causing _escape to raise AttributeError. Use is-not-None checks instead so empty string keys are handled correctly. --- parse_errors/_jsonpath.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/parse_errors/_jsonpath.py b/parse_errors/_jsonpath.py index 88175ce..2fb061f 100644 --- a/parse_errors/_jsonpath.py +++ b/parse_errors/_jsonpath.py @@ -44,7 +44,9 @@ def jsonpath_to_pointer(jsonpath: str) -> str: raise ValueError( f"Cannot parse JSONPath step at position {pos}: {tail[pos:]!r}" ) - name = m.group("name") or m.group("sq") or m.group("dq") or m.group("idx") + for group in ("name", "sq", "dq", "idx"): + if (name := m.group(group)) is not None: + break parts.append(_escape(name)) pos = m.end() From 00cc40d0b5f6b98208104bc09d6384d93b8aef9c Mon Sep 17 00:00:00 2001 From: Tim Hatch Date: Wed, 29 Apr 2026 14:30:58 +0000 Subject: [PATCH 2/6] Fix unknown file extension swallowing original exception When format= is omitted and the file extension is unrecognised, detect_format returns None. The bare assert raised AssertionError inside the except block, replacing the original exception with an opaque crash. Fall back to the same filename-only ParseError that other unlocatable exceptions get instead. --- parse_errors/context.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/parse_errors/context.py b/parse_errors/context.py index 404f8cc..b06481b 100644 --- a/parse_errors/context.py +++ b/parse_errors/context.py @@ -88,7 +88,10 @@ def ParseContext( raise exc fmt = format or detect_format(path) - assert fmt is not None + if fmt is None: + raise ParseError( + f"{filename}: {exc!r}", filename=filename, line=1 + ) from exc source = data if data is not None else path.read_bytes() source_map = build_source_map(source, fmt) From 9154cee9bd7ef1aa5022184f084ce33f29e064a8 Mon Sep 17 00:00:00 2001 From: Tim Hatch Date: Wed, 29 Apr 2026 14:47:32 +0000 Subject: [PATCH 3/6] Fix unparseable JSONPath leaking bare ValidationError without filename jsonpath_to_pointer raises ValueError for paths it cannot parse, such as those containing wildcard segments like $[...] that msgspec emits when the top-level type is a dict. The except ValueError branch was marked pragma: no cover because it was thought unreachable, but it is reachable and caused the original exception to re-raise without any filename information. Apply the same filename-only ParseError fallback used by other unlocatable errors instead. --- parse_errors/context.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/parse_errors/context.py b/parse_errors/context.py index b06481b..fc16fa0 100644 --- a/parse_errors/context.py +++ b/parse_errors/context.py @@ -84,8 +84,10 @@ def ParseContext( try: pointer = jsonpath_to_pointer(jsonpath) - except ValueError: # pragma: no cover - raise exc + except ValueError: + raise ParseError( + f"{filename}: {exc!r}", filename=filename, line=1 + ) from exc fmt = format or detect_format(path) if fmt is None: From 0b12bc5c4dd171d9e5d032d86848893132a1db45 Mon Sep 17 00:00:00 2001 From: Tim Hatch Date: Wed, 29 Apr 2026 19:14:53 +0000 Subject: [PATCH 4/6] Fix uppercase format= argument causing ValueError to escape context manager When the caller passed format='YAML' or another uppercase variant, build_source_map raised ValueError: Unknown format, which escaped the except handler and buried the original validation exception as __context__. Normalise format to lowercase so 'YAML', 'JSON', 'TOML' all work, and validate the normalised value against known formats so truly unknown values still get the filename-only ParseError fallback instead of leaking a bare ValueError. --- parse_errors/context.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/parse_errors/context.py b/parse_errors/context.py index fc16fa0..e951eb6 100644 --- a/parse_errors/context.py +++ b/parse_errors/context.py @@ -89,8 +89,8 @@ def ParseContext( f"{filename}: {exc!r}", filename=filename, line=1 ) from exc - fmt = format or detect_format(path) - if fmt is None: + fmt = format.lower() if format else detect_format(path) + if fmt not in ("json", "toml", "yaml"): raise ParseError( f"{filename}: {exc!r}", filename=filename, line=1 ) from exc From 95dd38dc3c049b6ef8743d2c48cad57b37f75aa8 Mon Sep 17 00:00:00 2001 From: Tim Hatch Date: Wed, 29 Apr 2026 19:15:27 +0000 Subject: [PATCH 5/6] Fix source map miss leaking bare exception without filename When closest_entry returns None (e.g. empty YAML document with a JSONPath validation error), the code re-raised the original exception via bare 'raise exc', losing the filename. Apply the same filename-only ParseError fallback used by other unlocatable paths. --- parse_errors/context.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/parse_errors/context.py b/parse_errors/context.py index e951eb6..c0be5f3 100644 --- a/parse_errors/context.py +++ b/parse_errors/context.py @@ -99,8 +99,10 @@ def ParseContext( source_map = build_source_map(source, fmt) entry = closest_entry(source_map, pointer) - if entry is None: # pragma: no cover - raise exc + if entry is None: + raise ParseError( + f"{filename}: {exc!r}", filename=filename, line=1 + ) from exc loc = entry.value_start # Lines are 0-based in source maps; convert to 1-based for humans. From a8473c6742defd4daaddbfef8b3e09a64c0f592c Mon Sep 17 00:00:00 2001 From: Tim Hatch Date: Thu, 30 Apr 2026 22:14:18 +0000 Subject: [PATCH 6/6] Fix format='yml' being rejected by format validity check build_source_map accepts 'yml' as a synonym for 'yaml', but the validation added to catch unknown formats did not include 'yml', so passing format='yml' silently fell back to filename-only errors with no line numbers. Normalise 'yml' to 'yaml' before the check. --- parse_errors/context.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/parse_errors/context.py b/parse_errors/context.py index c0be5f3..79ef6e4 100644 --- a/parse_errors/context.py +++ b/parse_errors/context.py @@ -90,6 +90,8 @@ def ParseContext( ) from exc fmt = format.lower() if format else detect_format(path) + if fmt == "yml": + fmt = "yaml" if fmt not in ("json", "toml", "yaml"): raise ParseError( f"{filename}: {exc!r}", filename=filename, line=1