Skip to content

Remove pydantic: port mcuboot.py to stdlib dataclasses #133

Description

@JPHutchins

Warning

LLM Disclosure

This issue was filed by claude-opus-5[1m] on behalf of @JPHutchins, who asked that the findings from the smpclient screaming-goblin kickoff session be recorded as issues for durable context. @JPHutchins corrected an earlier plan of mine to merely declare pydantic during the smp port — the intent is to drop it entirely.

smp's screaming-goblin moves off pydantic to msgspec, so pip install smp no longer pulls pydantic. smpclient never declared pydantic itself — it has been relying on it arriving transitively — and after the port the only thing still importing it is mcuboot.py.

Rather than declare a dependency we want gone, remove the need for it. This is non-breaking and independent of smp, so it can land on main before the port — then the port never has to mention pydantic at all.

pydantic is load-bearing for exactly one thing

from pydantic import Field, GetCoreSchemaHandler
from pydantic.dataclasses import dataclass
from pydantic_core import CoreSchema, core_schema

buys three things, and only the second is real:

  1. @dataclass(frozen=True) × 6 — runtime validation of values that struct.Struct.unpack() has already produced as ints. ImageVersion.loads is the pattern: ImageVersion(*IMAGE_VERSION_STRUCT.unpack(data)).
  2. The left-to-right union coercion — the one genuinely pydantic-specific behaviour:
    ImageTLVType = Annotated[Union[IMAGE_TLV, VendorTLV, int], Field(union_mode="left_to_right")]
    Used at precisely one field (ImageTLVHeader.type) and one parameter (ImageInfo.get_tlv).
  3. VendorTLV.__get_pydantic_core_schema__ — exists only to expose (2) to pydantic. The actual range check is already plain Python in VendorTLV.__new__.

Recommendation: stdlib dataclasses, not msgspec

msgspec would be the wrong tool here. mcuboot.py does no CBOR/JSON de/serialisation — struct.Struct does the binary parsing. Pulling in a serialisation library to do validation it doesn't need trades one dependency for another. Worse, IMAGE_TLV | VendorTLV | int is a union of three int-like types, which is exactly what msgspec cannot decode directly — smp hit this and had to decode as int and resolve afterwards.

So:

  • 6 × pydantic.dataclasses.dataclassdataclasses.dataclass (stdlib).
  • Delete __get_pydantic_core_schema__.
  • ImageTLVType becomes a plain IMAGE_TLV | VendorTLV | int alias, and the coercion becomes an explicit, testable function applied at the parse boundary:
    def _tlv_type(value: int) -> ImageTLVType:
        """Resolve a raw TLV type the way pydantic's left-to-right union did."""
        try:
            return IMAGE_TLV(value)
        except ValueError:
            pass
        try:
            return VendorTLV(value)
        except ValueError:
            return value

That is strictly more explicit than a union-mode annotation, provable at the call site, and directly unit-testable with positive and negative cases — replacing implicit library behaviour with source that says what it does.

tests/test_mcuboot_tools.py also uses pydantic.TypeAdapter and will need the same treatment.

Done when

grep -r pydantic src tests is empty and pydantic appears nowhere in pyproject.toml.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions