Structure & polymorphism¶
How a field becomes a file¶
Three annotations from flync.core.annotations (External, Implied, Reference)
decouple the model from the repository layout: they tell the loader where a value comes from and
the generator where it goes. The directory tree they produce is the one config authors see,
documented at Writing a FLYNC config — read the two together when you change an annotation,
because changing one changes the other.
Annotation |
Meaning |
Typical use |
|---|---|---|
|
value lives in a separate file or folder |
|
|
value is derived, not stored |
an object’s |
|
value is a key naming an object loaded elsewhere |
connections referring to a port/controller by name |
External — one field, one place¶
from typing import Annotated
from flync.core.annotations import External, NamingStrategy, OutputStrategy
ecus: Annotated[
list[ECU],
External(output_structure=OutputStrategy.FOLDER, naming_strategy=NamingStrategy.FIELD_NAME),
] = Field()
metadata: Annotated[
SystemMetadata,
External(
output_structure=OutputStrategy.SINGLE_FILE | OutputStrategy.OMMIT_ROOT,
naming_strategy=NamingStrategy.FIXED_PATH,
path="system_metadata",
),
]
Strategies combine with |: SINGLE_FILE writes one <field>.flync.yaml, FOLDER writes a
directory, OMMIT_ROOT suppresses the wrapper key, FIXED_PATH fixes the name instead of
deriving it from the field. FIXED_ROOT together with External(root=...) re-bases the
path on a directory other than the parent’s. The full behavior is in Field Annotations.
Implied — the name is the folder¶
from flync.core.annotations import Implied, ImpliedStrategy
name: Annotated[str, Implied(strategy=ImpliedStrategy.FOLDER_NAME)] = Field()
Reference — a key plus the object it resolves to¶
class ECUPortToXConnection(InternalConnection):
ecu_port_name: Annotated[str, Reference(source="_ecu_port")] = Field(alias="ecu_port")
_ecu_port: ECUPort | None = None # wired during workspace resolution
@property
def ecu_port(self) -> ECUPort:
return self._ecu_port
The public field is the string that appears in YAML (hence alias); the named private
attribute holds the resolved object. When the field is not a plain name but a composite key, add a
field_serializer so dumps write the key back, as
SOMEIPServiceDeployment does for its service id.
Discriminated unions¶
Polymorphic fields never probe dicts — each variant carries a Literal tag with a default, and
the field points the union at that tag.
1 — the tag field (mode, type or deployment_type by domain):
class BASET1(FLYNCBaseModel):
mode: Literal["base_t1"] = Field(default="base_t1")
...
class MII(FLYNCBaseModel):
type: Literal["mii"] = Field(default="mii")
...
2 — the union field declares the discriminator:
class ECUPort(FLYNCBaseModel):
mdi_config: BASET1 | BASET1S | BASET = Field(default_factory=BASET1, discriminator="mode")
mii_config: MII | RMII | SGMII | RGMII | XFI | None = Field(default=None, discriminator="type")
For a list whose items are a discriminated union, attach the Field to the element
annotation (as CANBus.frames does):
frames: list[Annotated[CANFrame | CANFDFrame, Field(discriminator="type")]] = Field(default_factory=list)
3 — list items of mixed variants get a thin RootModel wrapper that renders as the bare
mapping in YAML:
class DeploymentUnion(RootModel):
root: (
SOMEIPServiceConsumer
| SOMEIPServiceProvider
| SOMEIPSDDeployment
| PDUSender
| PDUReceiver
| PDUForwarder
| DoIPServerDeployment
| DoIPDiscoveryDeployment
) = Field(discriminator="deployment_type")
Variants that share behavior hang off an abstract base, which keeps the common fields and the tag type honest:
class SOMEIPServiceDeployment(abc.ABC, FLYNCBaseModel):
deployment_type: DeploymentTypes
service: int = Field(gt=0, lt=0xFFFF, strict=True)
...
class SOMEIPServiceConsumer(SOMEIPServiceDeployment):
deployment_type: Literal["someip_consumer"] = Field(default="someip_consumer")
When adding variants¶
The tag must be
Literal, and it must have a default. The two halves matter for different reasons, and conflating them makes a missingtype:line hard to debug:flync.sdk.utils.model_dependenciescollects a model’s discriminator fields by testingget_origin(annotation) is Literal— the annotation alone. A tag annotated as plainstris invisible to the dependency graph.flync.sdk.utils.model_dumperthen adds those names tomodel_fields_setbefore dumping, which is what stops pydantic from dropping a tag the YAML never set explicitly. Without the default there is nothing to write back.
Keep the tag spelled like the class (
"someip_consumer"forSOMEIPServiceConsumer); loaders and factory helpers resolve it by value.Add each variant to the model docstring’s type list so the YAML schema docs stay complete.