Skip to content

Codec only wraps NameError when resolving type hints; AttributeError/TypeError/SyntaxError escape the CodecError contract #38

@OmarAlJarrah

Description

@OmarAlJarrah

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinggood first issueGood for newcomers

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions