Final Types
Control whether a composite type is recursively expanded or treated as an atomic value. Three approaches can be combined freely.
Approach 1: Constructor Parameter
@dataclass
class Money:
amount: float
currency: str
@dataclass
class Product:
name: str
price: Money
extractor = MetadataExtractor(final_types={Money})
metadata = extractor.extract(Product)
assert "price" in metadata
assert "price.amount" not in metadata # not expanded
assert metadata["price"].composite is False
assert metadata["price"].final is True
Approach 2: @final_type Decorator
from fields_metadata import final_type
@final_type
@dataclass
class Money:
amount: float
currency: str
extractor = MetadataExtractor() # no final_types parameter needed
metadata = extractor.extract(Product)
assert "price.amount" not in metadata
Approach 3: FinalType Annotation (per-field)
from typing import Annotated
from fields_metadata import FinalType
@dataclass
class Product:
regular_price: Money # expanded
special_price: Annotated[Money, FinalType()] # not expanded
metadata = MetadataExtractor().extract(Product)
assert "regular_price.amount" in metadata
assert "special_price.amount" not in metadata
Works with containers and optional types:
@dataclass
class Document:
tags: list[Annotated[Tag, FinalType()]]
author: Annotated[Author, FinalType()] | None
Understanding final
A field is final=True when it has no non-derived subfields:
- Primitive types are always
final. - Composite types marked as final are
final. - Derived fields are always
final.
extractor = MetadataExtractor()
metadata = extractor.extract(Product)
assert metadata["name"].final is True # primitive
assert metadata["price"].final is False # has subfields
extractor2 = MetadataExtractor(final_types={Money})
metadata2 = extractor2.extract(Product)
assert metadata2["price"].final is True # now treated as atomic