From a78bd0f68bda71a273d374601dc3c5b327237f10 Mon Sep 17 00:00:00 2001 From: Ziang Zhang Date: Wed, 17 Jun 2026 12:57:22 +0800 Subject: [PATCH] fix: wrap all type-hint resolution errors in CodecError Codec.decode documented that it raises CodecError on any structural mismatch or conversion failure, but get_type_hints can also raise AttributeError (stale qualified references), TypeError (malformed expressions), and SyntaxError (invalid annotations). These escaped the CodecError contract. Fix: broaden the except clause to catch all four exception types. Closes #38 --- .../src/dexpace/sdk/core/serde/codec.py | 6 +++-- .../tests/serde/test_codec.py | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/dexpace-sdk-core/src/dexpace/sdk/core/serde/codec.py b/packages/dexpace-sdk-core/src/dexpace/sdk/core/serde/codec.py index 80e0afb..2a364c0 100644 --- a/packages/dexpace-sdk-core/src/dexpace/sdk/core/serde/codec.py +++ b/packages/dexpace-sdk-core/src/dexpace/sdk/core/serde/codec.py @@ -908,10 +908,12 @@ def _resolve_info(target: type) -> _ModelInfo: localns = {tp.__name__: tp for tp in type_params} or None try: hints = get_type_hints(target, include_extras=True, localns=localns) - except NameError as err: + except (NameError, AttributeError, TypeError, SyntaxError) as err: # An unresolvable forward reference (a string annotation whose name is # not in scope) surfaces as a bare ``NameError`` from ``get_type_hints``; - # wrap it so the codec keeps its ``CodecError`` contract. + # other evaluation failures (stale qualified references, malformed + # expressions) raise ``AttributeError``, ``TypeError``, or ``SyntaxError``. + # Wrap them all so the codec keeps its ``CodecError`` contract. raise CodecError( f"cannot resolve a type hint on {target.__name__}: {err}", target_name=target.__name__, diff --git a/packages/dexpace-sdk-core/tests/serde/test_codec.py b/packages/dexpace-sdk-core/tests/serde/test_codec.py index cc169ab..6badb6e 100644 --- a/packages/dexpace-sdk-core/tests/serde/test_codec.py +++ b/packages/dexpace-sdk-core/tests/serde/test_codec.py @@ -786,6 +786,30 @@ class HasBadRef: assert "resolve" in str(info.value) +def test_decode_stale_qualified_ref_raises_codec_error(codec: Codec) -> None: + # ``get_type_hints`` raises ``AttributeError`` for a string annotation + # referencing a non-existent attribute on a valid module. + @dataclass(frozen=True, slots=True) + class HasStaleRef: + value: "os.ThisDoesNotExist" # type: ignore[name-defined] # noqa: F821 + + with pytest.raises(CodecError) as info: + codec.decode({"value": 1}, HasStaleRef) + assert "resolve" in str(info.value) + + +def test_decode_malformed_expression_raises_codec_error(codec: Codec) -> None: + # ``get_type_hints`` raises ``SyntaxError`` for a string annotation + # that is not a valid expression. + @dataclass(frozen=True, slots=True) + class HasBadSyntax: + value: "def f(" # type: ignore[name-defined] # noqa: F821 + + with pytest.raises(CodecError) as info: + codec.decode({"value": 1}, HasBadSyntax) + assert "resolve" in str(info.value) + + @dataclass(frozen=True, slots=True) class _Box[T]: item: T