Problem
Codec.decode documents that it raises CodecError "on any structural mismatch or conversion failure," but the type-hint resolution step only wraps NameError. get_type_hints evaluates every string annotation, so a dataclass field with a stale qualified reference (e.g. "os.ThisDoesNotExist") or a malformed annotation expression raises AttributeError, TypeError, or SyntaxError from inside resolution. Those escape _resolve_info unwrapped and propagate straight out of decode, so a caller that only catches CodecError (or its base DeserializationError) will miss them.
Where
packages/dexpace-sdk-core/src/dexpace/sdk/core/serde/codec.py:909-919:
try:
hints = get_type_hints(target, include_extras=True, localns=localns)
except NameError 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.
raise CodecError(
f"cannot resolve a type hint on {target.__name__}: {err}",
target_name=target.__name__,
error=err,
) from err
_resolve_info is called from _decode_dataclass (codec.py:404) outside any try/except — the only except in that function (codec.py:408-411) wraps the target(**kwargs) construction call, not hint resolution. The public decode (codec.py:251-268) declares Raises: CodecError, and CodecError subclasses DeserializationError (codec.py:48).
Impact
Resolution failures other than NameError reach decode callers as the raw builtin exception, bypassing the CodecError contract. Reproduced on the current tree (Python 3.13):
import os
from dataclasses import dataclass
from dexpace.sdk.core.serde.codec import Codec
@dataclass(frozen=True, slots=True)
class BadAttr:
a: "os.ThisDoesNotExist" # leaks AttributeError
@dataclass(frozen=True, slots=True)
class BadExpr:
a: "1 + 'x'" # leaks TypeError
@dataclass(frozen=True, slots=True)
class BadSyntax:
a: "def f(" # leaks SyntaxError
Codec().decode({"a": 1}, BadAttr) # AttributeError: module 'os' has no attribute 'ThisDoesNotExist'
Codec().decode({"a": 1}, BadExpr) # TypeError: unsupported operand type(s) for +: 'int' and 'str'
Codec().decode({"a": 1}, BadSyntax) # SyntaxError: Forward reference must be an expression -- got 'def f('
A consumer with a typo'd or stale annotation in one of its own dataclasses gets an exception type that its except CodecError/except DeserializationError block does not catch, so the failure surfaces as an unexpected uncaught error instead of the documented decode error.
Suggested fix
Broaden the except to cover the other exception types get_type_hints can raise while evaluating annotations, and generalise the wrapped message so it no longer claims the cause is specifically a forward reference:
except (NameError, AttributeError, TypeError, SyntaxError) as err:
raise CodecError(
f"cannot resolve a type hint on {target.__name__}: {err}",
target_name=target.__name__,
error=err,
) from err
Add tests alongside the existing NameError case (tests/serde/test_codec.py:777) for an AttributeError-raising qualified reference and a malformed-expression annotation, asserting both raise CodecError.
Problem
Codec.decodedocuments that it raisesCodecError"on any structural mismatch or conversion failure," but the type-hint resolution step only wrapsNameError.get_type_hintsevaluates every string annotation, so a dataclass field with a stale qualified reference (e.g."os.ThisDoesNotExist") or a malformed annotation expression raisesAttributeError,TypeError, orSyntaxErrorfrom inside resolution. Those escape_resolve_infounwrapped and propagate straight out ofdecode, so a caller that only catchesCodecError(or its baseDeserializationError) will miss them.Where
packages/dexpace-sdk-core/src/dexpace/sdk/core/serde/codec.py:909-919:_resolve_infois called from_decode_dataclass(codec.py:404) outside anytry/except— the onlyexceptin that function (codec.py:408-411) wraps thetarget(**kwargs)construction call, not hint resolution. The publicdecode(codec.py:251-268) declaresRaises: CodecError, andCodecErrorsubclassesDeserializationError(codec.py:48).Impact
Resolution failures other than
NameErrorreachdecodecallers as the raw builtin exception, bypassing theCodecErrorcontract. Reproduced on the current tree (Python 3.13):A consumer with a typo'd or stale annotation in one of its own dataclasses gets an exception type that its
except CodecError/except DeserializationErrorblock does not catch, so the failure surfaces as an unexpected uncaught error instead of the documented decode error.Suggested fix
Broaden the
exceptto cover the other exception typesget_type_hintscan raise while evaluating annotations, and generalise the wrapped message so it no longer claims the cause is specifically a forward reference:Add tests alongside the existing
NameErrorcase (tests/serde/test_codec.py:777) for anAttributeError-raising qualified reference and a malformed-expression annotation, asserting both raiseCodecError.