Skip to content

corsair

Corsair is a control and status register (CSR) map generator for HDL projects.

It generates HDL code, documentation and other artifacts from CSR map description file.

IdentifierStr = Annotated[str, StringConstraints(to_lower=True, strip_whitespace=True, min_length=1, pattern='^[A-Za-z_][A-Za-z0-9_]*$')] module-attribute

A lowercase string that represents a valid identifier/name.

Pow2Int = Annotated[int, AfterValidator(_is_power_of_two)] module-attribute

An integer that is a power of two.

PyAttrPathStr = Annotated[str, StringConstraints(strip_whitespace=True, pattern='^.+\\.py::[A-Za-z_][A-Za-z0-9_]*$')] module-attribute

A string that represents a path to an attribute (class, function, object) within some python module.

SingleLineStr = Annotated[str, StringConstraints(strip_whitespace=True, pattern='^[^\\n\\r]*$')] module-attribute

A string that represents a single line text.

TextStr = Annotated[str, StringConstraints(strip_whitespace=True)] module-attribute

A string that represent a generic text (can be multiline).

AccessCategory

Bases: str, Enum

Common access categories every Access mode belongs to.

Source code in src/corsair/_model.py
class AccessCategory(str, enum.Enum):
    """Common access categories every `Access` mode belongs to."""

    RW = AccessMode.RW
    """Read and Write. The field can be read or written."""

    RO = AccessMode.RO
    """Read Only. Write has no effect."""

    WO = AccessMode.WO
    """Write Only. Read returns zeros."""

    def __str__(self) -> str:
        """Convert enumeration member into string."""
        return self.value

RO = AccessMode.RO class-attribute instance-attribute

Read Only. Write has no effect.

RW = AccessMode.RW class-attribute instance-attribute

Read and Write. The field can be read or written.

WO = AccessMode.WO class-attribute instance-attribute

Write Only. Read returns zeros.

__str__()

Convert enumeration member into string.

Source code in src/corsair/_model.py
def __str__(self) -> str:
    """Convert enumeration member into string."""
    return self.value

AccessMode

Bases: str, Enum

Access mode for a field.

It describes how the field can be accessed by bus/software and possible side-effects.

Source code in src/corsair/_model.py
class AccessMode(str, enum.Enum):
    """Access mode for a field.

    It describes how the field can be accessed by bus/software and possible side-effects.
    """

    RW = "rw"
    """Read and Write. The field can be read or written."""

    RW1C = "rw1c"
    """Read and Write 1 to Clear. The field can be read, and when 1 is written field is cleared."""

    RW1S = "rw1s"
    """	Read and Write 1 to Set. The field can be read, and when 1 is written field is set."""

    RO = "ro"
    """Read Only. Write has no effect."""

    ROC = "roc"
    """Read Only to Clear. The field is cleared after every read."""

    ROLL = "roll"
    """Read Only + Latch Low. The field capture hardware active low pulse signal and stuck in 0.
    The field is set after every read."""

    ROLH = "rolh"
    """Read Only + Latch High. The field capture hardware active high pulse signal and stuck in 1.
    Read the field to clear it."""

    WO = "wo"
    """Write Only. Zeros are always read."""

    WOSC = "wosc"
    """Write Only + Self Clear. The field is cleared on the next clock tick after write."""

    def __str__(self) -> str:
        """Convert enumeration member into string."""
        return self.value

    @property
    def category(self) -> AccessCategory:
        """Access category."""
        if self in (AccessMode.RO, AccessMode.ROC, AccessMode.ROLH, AccessMode.ROLL):
            return AccessCategory.RO
        if self in (AccessMode.WO, AccessMode.WOSC):
            return AccessCategory.WO
        if self in (AccessMode.RW, AccessMode.RW1C, AccessMode.RW1S):
            return AccessCategory.RW
        raise ValueError(f"Cannot map access mode {self} to any category")

    @property
    def is_ro(self) -> bool:
        """Check that this access mode belongs to RO category."""
        return self.category == AccessCategory.RO

    @property
    def is_wo(self) -> bool:
        """Check that this access mode belongs to WO category."""
        return self.category == AccessCategory.WO

    @property
    def is_rw(self) -> bool:
        """Check that this access mode belongs to RW category."""
        return self.category == AccessCategory.RW

RO = 'ro' class-attribute instance-attribute

Read Only. Write has no effect.

ROC = 'roc' class-attribute instance-attribute

Read Only to Clear. The field is cleared after every read.

ROLH = 'rolh' class-attribute instance-attribute

Read Only + Latch High. The field capture hardware active high pulse signal and stuck in 1. Read the field to clear it.

ROLL = 'roll' class-attribute instance-attribute

Read Only + Latch Low. The field capture hardware active low pulse signal and stuck in 0. The field is set after every read.

RW = 'rw' class-attribute instance-attribute

Read and Write. The field can be read or written.

RW1C = 'rw1c' class-attribute instance-attribute

Read and Write 1 to Clear. The field can be read, and when 1 is written field is cleared.

RW1S = 'rw1s' class-attribute instance-attribute

Read and Write 1 to Set. The field can be read, and when 1 is written field is set.

WO = 'wo' class-attribute instance-attribute

Write Only. Zeros are always read.

WOSC = 'wosc' class-attribute instance-attribute

Write Only + Self Clear. The field is cleared on the next clock tick after write.

category property

Access category.

is_ro property

Check that this access mode belongs to RO category.

is_rw property

Check that this access mode belongs to RW category.

is_wo property

Check that this access mode belongs to WO category.

__str__()

Convert enumeration member into string.

Source code in src/corsair/_model.py
def __str__(self) -> str:
    """Convert enumeration member into string."""
    return self.value

ArrayItem

Bases: NamedItem

NamedItem that can describe array of items with common properties following some repeatable pattern.

Source code in src/corsair/_model.py
class ArrayItem(NamedItem):
    """`NamedItem` that can describe array of items with common properties following some repeatable pattern."""

    num: PositiveInt
    """Number of elements within the array."""

    increment: PositiveInt
    """Offset increment for each array element."""

    indices: tuple[str, ...] = ()
    """Unique index (label) for each array element.

    Index can be numeric (0, 1, 2, ...), alphabetic (a, b, c, ...), or any string.
    If indices are not provided (empty tuple), they will be generated as numeric (0, 1, 2, ...) based on `num`.

    Number of indices should be greater or equal to `num`.
    """

    naming: str = "{name}{index}"
    """Pattern for element name.

    The pattern can use `{name}` and `{index}` placeholders to insert `name` and concrete index into the name.
    `{name}` can be omitted, but `{index}` is required.

    Examples:
    - `{name}{index}` -> `gpioa`, `gpiob`, `gpioc`, ...
    - `{name}_{index}` -> `irq0`, `irq1`, `irq2`, ...
    """

    @FrozenProperty
    def generated_items(self) -> tuple[NamedItem, ...]:
        """Concrete items in array created by following the pattern."""
        return self._generate_items()

    @abstractmethod
    def _generate_items(self) -> tuple[NamedItem, ...]:
        """Generate concrete items in the array."""

    @field_validator("indices", mode="after")
    @classmethod
    def _fill_indices(cls, values: tuple[str, ...], info: ValidationInfo) -> tuple[str, ...]:
        """Fill indices with numeric values if they are not provided."""
        if not values:
            return tuple(str(i) for i in range(info.data["num"]))
        return values

    @model_validator(mode="after")
    def _validate_minimum_num(self) -> Self:
        """Validate that array has at least two elements."""
        min_num = 2
        if self.num < min_num:
            raise ValueError(
                f"at least {min_num} elements are required for the array, but current size is {self.num}. "
                "Consider using non-array kind instead."
            )
        return self

    @model_validator(mode="after")
    def _validate_indices_unique(self) -> Self:
        """Validate that all indices are unique."""
        if len(set(self.indices)) != len(self.indices):
            raise ValueError(f"indices are not unique: {self.indices}")
        return self

    @model_validator(mode="after")
    def _validate_indices_length(self) -> Self:
        """Validate that number of indices is greater or equal to number of elements."""
        if len(self.indices) < self.num:
            raise ValueError(
                f"number of indices {len(self.indices)} is less than number of elements {self.num}. "
                "Consider using non-array kind instead."
            )
        return self

    @model_validator(mode="after")
    def _validate_naming(self) -> Self:
        """Validate that naming pattern is valid."""
        try:
            self.naming.format_map(defaultdict(str, index="0"))
        except KeyError as e:
            if str(e) == "'index'":
                raise ValueError(f"naming pattern {self.naming} is invalid: missing 'index'") from e
            raise ValueError(f"naming pattern {self.naming} is invalid") from e
        return self

increment instance-attribute

Offset increment for each array element.

indices = () class-attribute instance-attribute

Unique index (label) for each array element.

Index can be numeric (0, 1, 2, ...), alphabetic (a, b, c, ...), or any string. If indices are not provided (empty tuple), they will be generated as numeric (0, 1, 2, ...) based on num.

Number of indices should be greater or equal to num.

naming = '{name}{index}' class-attribute instance-attribute

Pattern for element name.

The pattern can use {name} and {index} placeholders to insert name and concrete index into the name. {name} can be omitted, but {index} is required.

Examples: - {name}{index} -> gpioa, gpiob, gpioc, ... - {name}_{index} -> irq0, irq1, irq2, ...

num instance-attribute

Number of elements within the array.

generated_items()

Concrete items in array created by following the pattern.

Source code in src/corsair/_model.py
@FrozenProperty
def generated_items(self) -> tuple[NamedItem, ...]:
    """Concrete items in array created by following the pattern."""
    return self._generate_items()

BuildSpecification

Bases: BaseModel

Specification that describes how to build everything.

Source code in src/corsair/_build.py
class BuildSpecification(BaseModel):
    """Specification that describes how to build everything."""

    loader: AnyLoaderConfig = SerializedLoader.Config(kind="yaml")
    """Configuration for the loader."""

    generators: dict[str, AnyGeneratorConfig] = Field(..., min_length=1)
    """Configuration for the generators to build all required files."""

    @classmethod
    def from_file(cls, path: Path) -> BuildSpecification:
        """Load specification from YAML file."""
        with path.open("r", encoding="utf-8") as f:
            return BuildSpecification(**yaml.safe_load(f))

    model_config = ConfigDict(
        extra="forbid",
        use_attribute_docstrings=True,
    )

generators = Field(..., min_length=1) class-attribute instance-attribute

Configuration for the generators to build all required files.

loader = SerializedLoader.Config(kind='yaml') class-attribute instance-attribute

Configuration for the loader.

from_file(path) classmethod

Load specification from YAML file.

Source code in src/corsair/_build.py
@classmethod
def from_file(cls, path: Path) -> BuildSpecification:
    """Load specification from YAML file."""
    with path.open("r", encoding="utf-8") as f:
        return BuildSpecification(**yaml.safe_load(f))

Enum

Bases: NamedItem

Enumeration of a bit field.

Source code in src/corsair/_model.py
class Enum(NamedItem):
    """Enumeration of a bit field."""

    members: tuple[EnumMember, ...]
    """Enumeration members.

    There should be at least one member.
    Members are sorted by value in ascending order.
    """

    @FrozenProperty
    def width(self) -> NonNegativeInt:
        """Minimum number of bits required to represent the largest member of enumeration."""
        return max(m.width for m in self.members)

    @FrozenProperty
    def names(self) -> tuple[IdentifierStr, ...]:
        """Names of members."""
        return tuple(m.name for m in self.members)

    @FrozenProperty
    def values(self) -> tuple[NonNegativeInt, ...]:
        """Values of members."""
        return tuple(m.value for m in self.members)

    @property
    def parent_field(self) -> Field:
        """Parent field.

        Requires parent to be set.
        """
        if self.parent is None:
            raise ValueError("Parent has to be set before accessing parent field")
        if not isinstance(self.parent, Field):
            raise TypeError("Parent is not an instance of `Field`")
        return self.parent

    @property
    def _children(self) -> tuple[NamedItem, ...]:
        """All subitems within item."""
        return self.members

    @field_validator("members", mode="after")
    @classmethod
    def _sort_members(cls, values: tuple[EnumMember, ...]) -> tuple[EnumMember, ...]:
        """Sort members by value."""
        return tuple(sorted(values, key=lambda v: v.value))

    @field_validator("members", mode="after")
    @classmethod
    def _validate_members_provided(cls, values: tuple[EnumMember, ...]) -> tuple[EnumMember, ...]:
        """Validate that at least one member is provided."""
        if len(values) == 0:
            raise ValueError("empty enumeration is not allowed, at least one member has to be provided")
        return values

    @field_validator("members", mode="after")
    @classmethod
    def _validate_members_unique_values(cls, values: tuple[EnumMember, ...]) -> tuple[EnumMember, ...]:
        """Validate that all values inside enumeration are unique."""
        if len({member.value for member in values}) != len(values):
            raise ValueError("some enumeration member values are not unique")
        return values

    @field_validator("members", mode="after")
    @classmethod
    def _validate_members_unique_names(cls, values: tuple[EnumMember, ...]) -> tuple[EnumMember, ...]:
        """Validate that all names inside enumeration are unique."""
        if len({member.name for member in values}) != len(values):
            raise ValueError("some enumeration member names are not unique")
        return values

members instance-attribute

Enumeration members.

There should be at least one member. Members are sorted by value in ascending order.

parent_field property

Parent field.

Requires parent to be set.

names()

Names of members.

Source code in src/corsair/_model.py
@FrozenProperty
def names(self) -> tuple[IdentifierStr, ...]:
    """Names of members."""
    return tuple(m.name for m in self.members)

values()

Values of members.

Source code in src/corsair/_model.py
@FrozenProperty
def values(self) -> tuple[NonNegativeInt, ...]:
    """Values of members."""
    return tuple(m.value for m in self.members)

width()

Minimum number of bits required to represent the largest member of enumeration.

Source code in src/corsair/_model.py
@FrozenProperty
def width(self) -> NonNegativeInt:
    """Minimum number of bits required to represent the largest member of enumeration."""
    return max(m.width for m in self.members)

EnumMember

Bases: NamedItem

Member of a bit field enumeration.

Source code in src/corsair/_model.py
class EnumMember(NamedItem):
    """Member of a bit field enumeration."""

    value: NonNegativeInt
    """Enumeration value."""

    @FrozenProperty
    def width(self) -> NonNegativeInt:
        """Minimum number of bits required to represent the value."""
        return max(self.value.bit_length(), 1)

    @property
    def parent_enum(self) -> Enum:
        """Parent enumeration.

        Requires parent to be set.
        """
        if self.parent is None:
            raise ValueError("Parent has to be set before accessing parent enum")
        if not isinstance(self.parent, Enum):
            raise TypeError("Parent is not an instance of `Enum`")
        return self.parent

    @property
    def _children(self) -> tuple[NamedItem, ...]:
        """All subitems within item."""
        return ()

parent_enum property

Parent enumeration.

Requires parent to be set.

value instance-attribute

Enumeration value.

width()

Minimum number of bits required to represent the value.

Source code in src/corsair/_model.py
@FrozenProperty
def width(self) -> NonNegativeInt:
    """Minimum number of bits required to represent the value."""
    return max(self.value.bit_length(), 1)

Field

Bases: NamedItem

Bit field inside a register.

Source code in src/corsair/_model.py
class Field(NamedItem):
    """Bit field inside a register."""

    offset: NonNegativeInt
    """Bit offset within register."""

    width: PositiveInt
    """Bit width of the item."""

    reset: NonNegativeInt | None
    """Reset value. Can be unknown."""

    access: AccessMode
    """Access mode."""

    hardware: HardwareMode
    """Hardware interaction options."""

    enum: Enum | None = None
    """Optional enumeration for the field."""

    @FrozenProperty
    def bit_indices(self) -> tuple[int, ...]:
        """Field bit positions inside register from LSB to MSB."""
        return tuple(range(self.offset, self.offset + self.width))

    @FrozenProperty
    def byte_indices(self) -> tuple[int, ...]:
        """Field byte indices inside register from lower to higher."""
        return tuple(range(self.offset // 8, (self.offset + self.width - 1) // 8 + 1))

    @FrozenProperty
    def lsb(self) -> int:
        """Position of the least significant bit (LSB) inside register."""
        return self.offset

    @FrozenProperty
    def msb(self) -> int:
        """Position of the most significant bit (MSB) inside register."""
        return self.lsb + self.width - 1

    @FrozenProperty
    def mask(self) -> int:
        """Bit mask for the bitfield inside register."""
        return (2 ** (self.width) - 1) << self.offset

    @FrozenProperty
    def is_multibit(self) -> bool:
        """Bitfield has more than one bit width."""
        return self.width > 1

    @FrozenProperty
    def parent_register(self) -> Register:
        """Parent register.

        Requires parent to be set.
        """
        if self.parent is None:
            raise ValueError("Parent has to be set before accessing parent register")
        if not isinstance(self.parent, Register):
            raise TypeError("Parent is not an instance of `Register`")
        return self.parent

    @property
    def _children(self) -> tuple[NamedItem, ...]:
        """All subitems within item."""
        return (self.enum,) if self.enum else ()

    def byte_select(self, byte_idx: int) -> tuple[int, int]:
        """Return register bit slice infomation (MSB, LSB) for a field projection into Nth byte of a register.

        This method facilitates organization of "byte select" logic within HDL templates.

        Example for `offset=3` and `width=7` bitfield:

        ```
                           6     3     0  <-- field bits
                           |     |     |
        field:             1 1 1 1 1 1 1
        reg:   0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0
               |    byte 1     |   byte 0    |
               15              7             0  <-- register bits
        ```

        For `byte_idx=0` result is `(7, 3)`, for `byte_idx=1` -- `(9, 8)`
        """
        byte_indices = tuple(self.byte_indices)
        if byte_idx not in byte_indices:
            raise ValueError(f"Provided {byte_idx=} has to be one of {byte_indices} for the field.")

        lsb = self.lsb if byte_idx == byte_indices[0] else byte_idx * 8
        msb = (byte_idx + 1) * 8 - 1 if ((byte_idx + 1) * 8 - 1 - self.msb) < 0 else self.msb

        return (msb, lsb)

    def byte_select_self(self, byte_idx: int) -> tuple[int, int]:
        """Return field bit slice infomation for field projection into Nth byte of a register.

        Refer to `byte_select` to get the idea. The only difference is
        that current method returns non-offsetted bit slice to represent positions within bitfield itself.
        """
        msb, lsb = self.byte_select(byte_idx)
        return (msb - self.offset, lsb - self.offset)

    @field_validator("hardware", mode="after")
    @classmethod
    def _validate_hardware_constraints(cls, value: HardwareMode, info: ValidationInfo) -> HardwareMode:
        """Validate that `hardware` field follows expected constraints."""
        # Check exclusive hardware flags
        for flag in (HardwareMode.NA, HardwareMode.QUEUE, HardwareMode.FIXED):
            if flag in value and len(value) > 1:
                raise ValueError(f"hardware mode '{flag}' must be exclusive, but current mode is '{value}'")

        # Hardware queue mode can be only combined with specific access values
        if HardwareMode.QUEUE in value:
            q_access_allowed = [AccessMode.RW, AccessMode.RO, AccessMode.WO]
            if info.data["access"] not in q_access_allowed:
                raise ValueError(
                    f"hardware mode 'q' is allowed to use only with '{q_access_allowed}', "
                    f"but current access mode is '{info.data['access']}'"
                )

        # Enable must be used with Input
        if HardwareMode.ENABLE in value and HardwareMode.INPUT not in value:
            raise ValueError(
                f"hardware mode 'e' is allowed to use only with 'i', " f"but current hardware mode is '{value}'"
            )
        return value

    @field_validator("reset", mode="after")
    @classmethod
    def _validate_reset_width(cls, value: NonNegativeInt | None, info: ValidationInfo) -> NonNegativeInt | None:
        """Validate that reset value width less or equal field width."""
        if value is not None:
            reset_value_width = value.bit_length()
            if reset_value_width > info.data["width"]:
                raise ValueError(
                    f"reset value 0x{value:x} requires {reset_value_width} bits to represent,"
                    f" but field is {info.data['width']} bits wide"
                )
        return value

    @field_validator("enum", mode="after")
    @classmethod
    def _validate_enum_members_width(cls, value: Enum | None, info: ValidationInfo) -> Enum | None:
        """Validate that enumeration members has values, which width fit field width."""
        if value is not None and value.width > info.data["width"]:
            raise ValueError(
                f"enumeration requires {value.width} bits to represent," f" but field is {info.data['width']} bits wide"
            )
        return value

access instance-attribute

Access mode.

enum = None class-attribute instance-attribute

Optional enumeration for the field.

hardware instance-attribute

Hardware interaction options.

offset instance-attribute

Bit offset within register.

reset instance-attribute

Reset value. Can be unknown.

width instance-attribute

Bit width of the item.

bit_indices()

Field bit positions inside register from LSB to MSB.

Source code in src/corsair/_model.py
@FrozenProperty
def bit_indices(self) -> tuple[int, ...]:
    """Field bit positions inside register from LSB to MSB."""
    return tuple(range(self.offset, self.offset + self.width))

byte_indices()

Field byte indices inside register from lower to higher.

Source code in src/corsair/_model.py
@FrozenProperty
def byte_indices(self) -> tuple[int, ...]:
    """Field byte indices inside register from lower to higher."""
    return tuple(range(self.offset // 8, (self.offset + self.width - 1) // 8 + 1))

byte_select(byte_idx)

Return register bit slice infomation (MSB, LSB) for a field projection into Nth byte of a register.

This method facilitates organization of "byte select" logic within HDL templates.

Example for offset=3 and width=7 bitfield:

                   6     3     0  <-- field bits
                   |     |     |
field:             1 1 1 1 1 1 1
reg:   0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0
       |    byte 1     |   byte 0    |
       15              7             0  <-- register bits

For byte_idx=0 result is (7, 3), for byte_idx=1 -- (9, 8)

Source code in src/corsair/_model.py
def byte_select(self, byte_idx: int) -> tuple[int, int]:
    """Return register bit slice infomation (MSB, LSB) for a field projection into Nth byte of a register.

    This method facilitates organization of "byte select" logic within HDL templates.

    Example for `offset=3` and `width=7` bitfield:

    ```
                       6     3     0  <-- field bits
                       |     |     |
    field:             1 1 1 1 1 1 1
    reg:   0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0
           |    byte 1     |   byte 0    |
           15              7             0  <-- register bits
    ```

    For `byte_idx=0` result is `(7, 3)`, for `byte_idx=1` -- `(9, 8)`
    """
    byte_indices = tuple(self.byte_indices)
    if byte_idx not in byte_indices:
        raise ValueError(f"Provided {byte_idx=} has to be one of {byte_indices} for the field.")

    lsb = self.lsb if byte_idx == byte_indices[0] else byte_idx * 8
    msb = (byte_idx + 1) * 8 - 1 if ((byte_idx + 1) * 8 - 1 - self.msb) < 0 else self.msb

    return (msb, lsb)

byte_select_self(byte_idx)

Return field bit slice infomation for field projection into Nth byte of a register.

Refer to byte_select to get the idea. The only difference is that current method returns non-offsetted bit slice to represent positions within bitfield itself.

Source code in src/corsair/_model.py
def byte_select_self(self, byte_idx: int) -> tuple[int, int]:
    """Return field bit slice infomation for field projection into Nth byte of a register.

    Refer to `byte_select` to get the idea. The only difference is
    that current method returns non-offsetted bit slice to represent positions within bitfield itself.
    """
    msb, lsb = self.byte_select(byte_idx)
    return (msb - self.offset, lsb - self.offset)

is_multibit()

Bitfield has more than one bit width.

Source code in src/corsair/_model.py
@FrozenProperty
def is_multibit(self) -> bool:
    """Bitfield has more than one bit width."""
    return self.width > 1

lsb()

Position of the least significant bit (LSB) inside register.

Source code in src/corsair/_model.py
@FrozenProperty
def lsb(self) -> int:
    """Position of the least significant bit (LSB) inside register."""
    return self.offset

mask()

Bit mask for the bitfield inside register.

Source code in src/corsair/_model.py
@FrozenProperty
def mask(self) -> int:
    """Bit mask for the bitfield inside register."""
    return (2 ** (self.width) - 1) << self.offset

msb()

Position of the most significant bit (MSB) inside register.

Source code in src/corsair/_model.py
@FrozenProperty
def msb(self) -> int:
    """Position of the most significant bit (MSB) inside register."""
    return self.lsb + self.width - 1

parent_register()

Parent register.

Requires parent to be set.

Source code in src/corsair/_model.py
@FrozenProperty
def parent_register(self) -> Register:
    """Parent register.

    Requires parent to be set.
    """
    if self.parent is None:
        raise ValueError("Parent has to be set before accessing parent register")
    if not isinstance(self.parent, Register):
        raise TypeError("Parent is not an instance of `Register`")
    return self.parent

FieldArray

Bases: Field, ArrayItem

Logical collection of similar fields with common properties.

Source code in src/corsair/_model.py
class FieldArray(Field, ArrayItem):
    """Logical collection of similar fields with common properties."""

    kind: Literal["field_array"] = "field_array"  # type: ignore reportIncompatibleVariableOverride
    """Item kind discriminator."""

    @property
    def generated_fields(self) -> tuple[Field, ...]:
        """Generated fields in the array."""
        # _generate_items creates tuple of `Field` items, so waiver is safe here
        return self.generated_items  # type: ignore reportReturnType

    def _generate_items(self) -> tuple[NamedItem, ...]:
        """Generate concrete fields in the array."""
        raise NotImplementedError

generated_fields property

Generated fields in the array.

kind = 'field_array' class-attribute instance-attribute

Item kind discriminator.

Generator

Bases: ABC

Base class for all generators.

Source code in src/corsair/_generators/base.py
class Generator(ABC):
    """Base class for all generators."""

    def __init__(
        self,
        label: str,
        register_map: Map,
        config: GeneratorConfig,
        output_dir: Path,
    ) -> None:
        """Initialize the generator."""
        self.label = label
        self.register_map = register_map
        self.config = config
        self.output_dir = output_dir.resolve()

        if not isinstance(self.config, self.get_config_cls()):
            raise TypeError(
                f"Configuration instance is not of the expected type of "
                f"{self.__class__.__name__}.{self.get_config_cls().__name__}"
            )

    def __call__(self) -> TypeGenerator[Path, None, None]:
        """Generate all the outputs."""
        self._check_register_map()

        # Generation is isolated within the output directory
        self.output_dir.mkdir(parents=True, exist_ok=True)
        with _change_workdir(self.output_dir):
            try:
                yield from (p.resolve() for p in self._generate())
            except jinja2.TemplateError as e:
                raise GeneratorTemplateError(self, e) from e
            except Exception:
                raise

    def _render_to_text(self, template_name: str, context: dict[str, Any]) -> str:
        """Render text with Jinja2."""
        env = TemplateEnvironment(searchpath=self.config.template_searchpaths)
        template = env.get_template(template_name)
        return template.render(context)

    def _render_to_file(self, template_name: str, context: dict[str, Any], file_name: str) -> Path:
        """Render text with Jinja2 and save it to the file."""
        path = Path(file_name)
        text = self._render_to_text(template_name, context)
        with path.open("w") as f:
            f.write(text)
        return path

    def _check_register_map(self) -> None:
        """Check if the register map contains unsupported features.

        Every generator should at least support flatmap with registers.
        Child generator can override this method to check for more specific features or relax the checks.

        Raises:
            GeneratorUnsupportedFeatureError: If the register map contains unsupported features.

        """
        if self.register_map.has_maps:
            raise GeneratorUnsupportedFeatureError(self, "maps inside register map")

        if self.register_map.has_map_arrays:
            raise GeneratorUnsupportedFeatureError(self, "map arrays inside register map")

        if self.register_map.has_memories:
            raise GeneratorUnsupportedFeatureError(self, "memories inside register map")

        if self.register_map.has_memory_arrays:
            raise GeneratorUnsupportedFeatureError(self, "memory arrays inside register map")

        if self.register_map.has_register_arrays:
            raise GeneratorUnsupportedFeatureError(self, "register arrays inside register map")

    @abstractmethod
    def _generate(self) -> TypeGenerator[Path, None, None]:
        """Generate all the outputs.

        Method should yield paths to the every generated file.
        Method is called within the output directory, so all the paths are relative to it.
        """

    @classmethod
    @abstractmethod
    def get_config_cls(cls) -> type[GeneratorConfig]:
        """Get the configuration class for the generator."""

__call__()

Generate all the outputs.

Source code in src/corsair/_generators/base.py
def __call__(self) -> TypeGenerator[Path, None, None]:
    """Generate all the outputs."""
    self._check_register_map()

    # Generation is isolated within the output directory
    self.output_dir.mkdir(parents=True, exist_ok=True)
    with _change_workdir(self.output_dir):
        try:
            yield from (p.resolve() for p in self._generate())
        except jinja2.TemplateError as e:
            raise GeneratorTemplateError(self, e) from e
        except Exception:
            raise

__init__(label, register_map, config, output_dir)

Initialize the generator.

Source code in src/corsair/_generators/base.py
def __init__(
    self,
    label: str,
    register_map: Map,
    config: GeneratorConfig,
    output_dir: Path,
) -> None:
    """Initialize the generator."""
    self.label = label
    self.register_map = register_map
    self.config = config
    self.output_dir = output_dir.resolve()

    if not isinstance(self.config, self.get_config_cls()):
        raise TypeError(
            f"Configuration instance is not of the expected type of "
            f"{self.__class__.__name__}.{self.get_config_cls().__name__}"
        )

get_config_cls() abstractmethod classmethod

Get the configuration class for the generator.

Source code in src/corsair/_generators/base.py
@classmethod
@abstractmethod
def get_config_cls(cls) -> type[GeneratorConfig]:
    """Get the configuration class for the generator."""

GeneratorConfig

Bases: BaseModel, ABC

Base configuration for a generator.

Source code in src/corsair/_generators/base.py
class GeneratorConfig(BaseModel, ABC):
    """Base configuration for a generator."""

    template_searchpaths: list[Path] = Field(default_factory=list)
    """Additional paths to search for templates.

    Templates will be searched for in the given paths first.
    """

    extra: dict[str, Any] = Field(default_factory=dict)
    """Extra configuration parameters for the generator."""

    model_config = ConfigDict(
        extra="forbid",
        use_attribute_docstrings=True,
    )

    @property
    @abstractmethod
    def generator_cls(self) -> type[Generator]:
        """Related generator class."""

    @abstractmethod
    def get_kind(self) -> str:
        """Get the kind of the generator."""

extra = Field(default_factory=dict) class-attribute instance-attribute

Extra configuration parameters for the generator.

generator_cls abstractmethod property

Related generator class.

template_searchpaths = Field(default_factory=list) class-attribute instance-attribute

Additional paths to search for templates.

Templates will be searched for in the given paths first.

get_kind() abstractmethod

Get the kind of the generator.

Source code in src/corsair/_generators/base.py
@abstractmethod
def get_kind(self) -> str:
    """Get the kind of the generator."""

GeneratorTemplateError

Bases: Exception

Raised when generator fails to render a template.

Source code in src/corsair/_generators/base.py
class GeneratorTemplateError(Exception):
    """Raised when generator fails to render a template."""

    def __init__(self, generator: Generator, j2_error: jinja2.TemplateError) -> None:
        """Initialize the exception."""
        self.generator = generator
        self.j2_error = j2_error
        super().__init__(f"Generator '{generator.label}' failed to render template")

    def __str__(self) -> str:
        """Represent exception as a string."""
        err = f"{self.args[0]}\n"

        if isinstance(self.j2_error, jinja2.TemplateSyntaxError):
            err += "Syntax error within template"
            if self.j2_error.name:
                err += f" '{self.j2_error.name}'"
            if self.j2_error.filename:
                err += f" {self.j2_error.filename}:{self.j2_error.lineno}"
            if self.j2_error.message:
                err += f" {self.j2_error.message}"
            if self.j2_error.source:
                err += f"\n{self.j2_error.source.splitlines()[self.j2_error.lineno - 1]}"

        elif isinstance(self.j2_error, jinja2.UndefinedError):
            err += "Undefined variable within template"
            if self.j2_error.message:
                err += f": {self.j2_error.message}"

        return err

__init__(generator, j2_error)

Initialize the exception.

Source code in src/corsair/_generators/base.py
def __init__(self, generator: Generator, j2_error: jinja2.TemplateError) -> None:
    """Initialize the exception."""
    self.generator = generator
    self.j2_error = j2_error
    super().__init__(f"Generator '{generator.label}' failed to render template")

__str__()

Represent exception as a string.

Source code in src/corsair/_generators/base.py
def __str__(self) -> str:
    """Represent exception as a string."""
    err = f"{self.args[0]}\n"

    if isinstance(self.j2_error, jinja2.TemplateSyntaxError):
        err += "Syntax error within template"
        if self.j2_error.name:
            err += f" '{self.j2_error.name}'"
        if self.j2_error.filename:
            err += f" {self.j2_error.filename}:{self.j2_error.lineno}"
        if self.j2_error.message:
            err += f" {self.j2_error.message}"
        if self.j2_error.source:
            err += f"\n{self.j2_error.source.splitlines()[self.j2_error.lineno - 1]}"

    elif isinstance(self.j2_error, jinja2.UndefinedError):
        err += "Undefined variable within template"
        if self.j2_error.message:
            err += f": {self.j2_error.message}"

    return err

GeneratorUnsupportedFeatureError

Bases: Exception

Raised when generator encounters unsupported feature in the register map.

Source code in src/corsair/_generators/base.py
class GeneratorUnsupportedFeatureError(Exception):
    """Raised when generator encounters unsupported feature in the register map."""

    def __init__(self, generator: Generator, feature: str) -> None:
        """Initialize the exception."""
        self.generator = generator
        self.feature = feature

    def __str__(self) -> str:
        """Represent exception as a string."""
        err = f"Generator '{self.generator.label}' ({self.generator.config.get_kind()}) does not support feature: "
        err += self.feature
        return err

__init__(generator, feature)

Initialize the exception.

Source code in src/corsair/_generators/base.py
def __init__(self, generator: Generator, feature: str) -> None:
    """Initialize the exception."""
    self.generator = generator
    self.feature = feature

__str__()

Represent exception as a string.

Source code in src/corsair/_generators/base.py
def __str__(self) -> str:
    """Represent exception as a string."""
    err = f"Generator '{self.generator.label}' ({self.generator.config.get_kind()}) does not support feature: "
    err += self.feature
    return err

HardwareMode

Bases: str, Flag

Hardware mode for a field.

Mode reflects hardware possibilities and interfaces to observe and modify bitfield value.

Source code in src/corsair/_model.py
class HardwareMode(str, enum.Flag):
    """Hardware mode for a field.

    Mode reflects hardware possibilities and interfaces to observe and modify bitfield value.
    """

    INPUT = "i"
    """Use input value from hardware to update the field."""

    I = INPUT
    """Shorthand for `INPUT`."""

    OUTPUT = "o"
    """Enable output value from the field to be accessed by hardware."""

    O = OUTPUT
    """Shorthand for `OUTPUT`."""

    CLEAR = "c"
    """Add signal to clear the field (fill with all zeros)."""

    C = CLEAR
    """Shorthand for `CLEAR`."""

    SET = "s"
    """Add signal to set the field (fill with all ones)."""

    S = SET
    """Shorthand for `SET`."""

    ENABLE = "e"
    """Add signal to enable the field to capture input value (must be used with `INPUT`)."""

    E = ENABLE

    LOCK = "l"
    """Add signal to lock the field (to prevent any changes)."""

    L = LOCK
    """Shorthand for `LOCK`."""

    ACCESS = "a"
    """Add signals to notify when bus access to the field is performed (at the same cycle)."""

    A = ACCESS
    """Shorthand for `ACCESS`."""

    QUEUE = "q"
    """Add simple interface to external queue (LIFO, FIFO)."""

    Q = QUEUE
    """Shorthand for `QUEUE`."""

    FIXED = "f"
    """Enable fixed mode (field is a constant)."""

    F = FIXED
    """Shorthand for `FIXED`."""

    NA = "n"
    """Not accessible by hardware."""

    N = NA
    """Shorthand for `NA`"""

    # Override original type hints to match actually used type
    value: str  # pyright: ignore [reportIncompatibleMethodOverride]
    _value_: str  # pyright: ignore [reportIncompatibleVariableOverride]

    @classmethod
    def _missing_(cls, value: object) -> enum.Enum:
        """Return member if one can be found for value, otherwise create a composite member.

        Composite member is created only iff value contains only members, else `ValueError` is raised.
        Based on `enum.Flag._create_pseudo_member_()` and `enum._decompose()`.
        """
        if not isinstance(value, str):
            raise TypeError(f"Member value has to be 'str', but {type(value)} is provided")
        if value == "":
            value = "n"  # Empty literal considered as 'n'
        value = value.lower()

        # Lookup for already created members (all members are singletons)
        member = cls._value2member_map_.get(value, None)
        if member is not None:
            return member

        # Create composite member
        flags = tuple(cls._split_flags(value))  # Can raise ValueError for unknown flags
        composite = str.__new__(cls)
        # Name style is the same as in `enum.Flag.__str__`
        composite._name_ = "|".join(
            [
                member._name_
                for member in cls  # Use iteration to follow order of declaration, rather than provided flags order
                if member._value_ in flags and member._name_
            ]
        )
        composite._value_ = cls._join_flags(flags)

        # Use setdefault in case another thread already created a composite with this value
        return cls._value2member_map_.setdefault(value, composite)

    @classmethod
    def _split_flags(cls, value: str) -> Iterator[str]:
        """Split string into flag values."""
        # For legacy reasons there could be a string without separators, where all flags are single chars.
        # Code below allows "ioe" and "i-o-e" as well.
        raw_flags = set(value.split("-") if "-" in value else value)

        # Collect all known flags in order of declaration
        self_flags = [member._value_ for member in cls]

        # Check that all raw flags are valid values
        for flag in raw_flags:
            if flag not in self_flags:
                raise ValueError(f"Unknown hardware mode {flag!r}")

        # Then generate flags in declaration order
        for member in cls:
            if member._value_ in raw_flags:
                yield member._value_

    @classmethod
    def _join_flags(cls, flags: Iterable[str]) -> str:
        """Concatenate all flag values into single string."""
        # For input strings flags without separators are allowed (refer to `_split_flags`),
        # but all other representations always use separators.
        return "-".join(flags)

    def __len__(self) -> int:
        """Return number of combined flags."""
        return sum(1 for _ in self)

    def __repr__(self) -> str:
        """Represent flags as a string in the same style as in `enum.Flag.__repr__()`."""
        return f"<{self.__class__.__name__}.{self._name_}: {self._value_!r}>"

    def __str__(self) -> str:
        """Represent flags as a compact string."""
        return self._value_

    def __or__(self, other: Self) -> Self:
        """Override `|` operator to combine flags into composite one."""
        cls = self.__class__
        if not isinstance(other, cls):
            raise TypeError(f"Can't OR {type(self)} with {type(other)}")
        return cls(cls._join_flags((self._value_, other._value_)))

    def __contains__(self, item: object) -> bool:
        """Overload `in` operator to check flag inclusions."""
        cls = self.__class__
        if isinstance(item, str):
            item = cls(item)
        if not isinstance(item, cls):
            raise TypeError(f"Can't use `in` for {type(self)} with {type(item)}")
        self_flags = tuple(cls._split_flags(self._value_))
        return all(flag in self_flags for flag in cls._split_flags(item._value_))

    def __iter__(self) -> Iterator[Self]:
        """Iterate over combination of flags."""
        cls = self.__class__
        for flag in cls._split_flags(self._value_):
            yield cls(flag)

    def __le__(self, other: object) -> bool:
        """Overload `<=` operator to check if current flags are the same or the subset of other."""
        cls = self.__class__
        if isinstance(other, str):
            other = cls(other)
        if not isinstance(other, cls):
            raise TypeError(f"Can't compare {type(self)} with {type(other)}")
        return self in other

    def __ge__(self, other: object) -> bool:
        """Overload `>=` operator to check if current flags are the same or the superset of other."""
        cls = self.__class__
        if isinstance(other, str):
            other = cls(other)
        if not isinstance(other, cls):
            raise TypeError(f"Can't compare {type(self)} with {type(other)}")
        return other in self

    def __lt__(self, other: object) -> bool:
        """Overload `<` operator to check if current flags are the subset of other."""
        cls = self.__class__
        if isinstance(other, str):
            other = cls(other)
        if not isinstance(other, cls):
            raise TypeError(f"Can't compare {type(self)} with {type(other)}")
        return self._value_ != other._value_ and self.__le__(other)

    def __gt__(self, other: object) -> bool:
        """Overload `>` operator to check if current flags are the superset of other."""
        cls = self.__class__
        if isinstance(other, str):
            other = cls(other)
        if not isinstance(other, cls):
            raise TypeError(f"Can't compare {type(self)} with {type(other)}")
        return self._value_ != other._value_ and self.__ge__(other)

A = ACCESS class-attribute instance-attribute

Shorthand for ACCESS.

ACCESS = 'a' class-attribute instance-attribute

Add signals to notify when bus access to the field is performed (at the same cycle).

C = CLEAR class-attribute instance-attribute

Shorthand for CLEAR.

CLEAR = 'c' class-attribute instance-attribute

Add signal to clear the field (fill with all zeros).

ENABLE = 'e' class-attribute instance-attribute

Add signal to enable the field to capture input value (must be used with INPUT).

F = FIXED class-attribute instance-attribute

Shorthand for FIXED.

FIXED = 'f' class-attribute instance-attribute

Enable fixed mode (field is a constant).

I = INPUT class-attribute instance-attribute

Shorthand for INPUT.

INPUT = 'i' class-attribute instance-attribute

Use input value from hardware to update the field.

L = LOCK class-attribute instance-attribute

Shorthand for LOCK.

LOCK = 'l' class-attribute instance-attribute

Add signal to lock the field (to prevent any changes).

N = NA class-attribute instance-attribute

Shorthand for NA

NA = 'n' class-attribute instance-attribute

Not accessible by hardware.

O = OUTPUT class-attribute instance-attribute

Shorthand for OUTPUT.

OUTPUT = 'o' class-attribute instance-attribute

Enable output value from the field to be accessed by hardware.

Q = QUEUE class-attribute instance-attribute

Shorthand for QUEUE.

QUEUE = 'q' class-attribute instance-attribute

Add simple interface to external queue (LIFO, FIFO).

S = SET class-attribute instance-attribute

Shorthand for SET.

SET = 's' class-attribute instance-attribute

Add signal to set the field (fill with all ones).

__contains__(item)

Overload in operator to check flag inclusions.

Source code in src/corsair/_model.py
def __contains__(self, item: object) -> bool:
    """Overload `in` operator to check flag inclusions."""
    cls = self.__class__
    if isinstance(item, str):
        item = cls(item)
    if not isinstance(item, cls):
        raise TypeError(f"Can't use `in` for {type(self)} with {type(item)}")
    self_flags = tuple(cls._split_flags(self._value_))
    return all(flag in self_flags for flag in cls._split_flags(item._value_))

__ge__(other)

Overload >= operator to check if current flags are the same or the superset of other.

Source code in src/corsair/_model.py
def __ge__(self, other: object) -> bool:
    """Overload `>=` operator to check if current flags are the same or the superset of other."""
    cls = self.__class__
    if isinstance(other, str):
        other = cls(other)
    if not isinstance(other, cls):
        raise TypeError(f"Can't compare {type(self)} with {type(other)}")
    return other in self

__gt__(other)

Overload > operator to check if current flags are the superset of other.

Source code in src/corsair/_model.py
def __gt__(self, other: object) -> bool:
    """Overload `>` operator to check if current flags are the superset of other."""
    cls = self.__class__
    if isinstance(other, str):
        other = cls(other)
    if not isinstance(other, cls):
        raise TypeError(f"Can't compare {type(self)} with {type(other)}")
    return self._value_ != other._value_ and self.__ge__(other)

__iter__()

Iterate over combination of flags.

Source code in src/corsair/_model.py
def __iter__(self) -> Iterator[Self]:
    """Iterate over combination of flags."""
    cls = self.__class__
    for flag in cls._split_flags(self._value_):
        yield cls(flag)

__le__(other)

Overload <= operator to check if current flags are the same or the subset of other.

Source code in src/corsair/_model.py
def __le__(self, other: object) -> bool:
    """Overload `<=` operator to check if current flags are the same or the subset of other."""
    cls = self.__class__
    if isinstance(other, str):
        other = cls(other)
    if not isinstance(other, cls):
        raise TypeError(f"Can't compare {type(self)} with {type(other)}")
    return self in other

__len__()

Return number of combined flags.

Source code in src/corsair/_model.py
def __len__(self) -> int:
    """Return number of combined flags."""
    return sum(1 for _ in self)

__lt__(other)

Overload < operator to check if current flags are the subset of other.

Source code in src/corsair/_model.py
def __lt__(self, other: object) -> bool:
    """Overload `<` operator to check if current flags are the subset of other."""
    cls = self.__class__
    if isinstance(other, str):
        other = cls(other)
    if not isinstance(other, cls):
        raise TypeError(f"Can't compare {type(self)} with {type(other)}")
    return self._value_ != other._value_ and self.__le__(other)

__or__(other)

Override | operator to combine flags into composite one.

Source code in src/corsair/_model.py
def __or__(self, other: Self) -> Self:
    """Override `|` operator to combine flags into composite one."""
    cls = self.__class__
    if not isinstance(other, cls):
        raise TypeError(f"Can't OR {type(self)} with {type(other)}")
    return cls(cls._join_flags((self._value_, other._value_)))

__repr__()

Represent flags as a string in the same style as in enum.Flag.__repr__().

Source code in src/corsair/_model.py
def __repr__(self) -> str:
    """Represent flags as a string in the same style as in `enum.Flag.__repr__()`."""
    return f"<{self.__class__.__name__}.{self._name_}: {self._value_!r}>"

__str__()

Represent flags as a compact string.

Source code in src/corsair/_model.py
def __str__(self) -> str:
    """Represent flags as a compact string."""
    return self._value_

ItemMetadata

Bases: BaseModel

Metadata for an item.

Source code in src/corsair/_model.py
class ItemMetadata(BaseModel):
    """Metadata for an item."""

    # All fields are created by user

    model_config = ConfigDict(
        # Model is faux-immutable
        frozen=True,
        # Extra values are allowed
        extra="allow",
    )

Loader

Bases: ABC

Base class for all register map loaders.

Source code in src/corsair/_loaders/base.py
class Loader(ABC):
    """Base class for all register map loaders."""

    def __init__(self, config: LoaderConfig) -> None:
        """Initialize the loader."""
        self.config = config
        self.raw_data: dict[str, Any] = {}

        if not isinstance(self.config, self.get_config_cls()):
            raise TypeError(
                f"Configuration instance is not of the expected type of "
                f"{self.__class__.__name__}.{self.get_config_cls().__name__}"
            )

    def __call__(self) -> Map:
        """Load the register map."""
        if not self.config.mapfile.exists():
            raise FileNotFoundError(f"CSR map file not found: {self.config.mapfile}")

        self.raw_data = self._load_raw()

        try:
            return Map.model_validate(self.raw_data)
        except ValidationError as e:
            raise LoaderValidationError(e, stringify_model_errors(e, self.raw_data)) from e

    @abstractmethod
    def _load_raw(self) -> dict[str, Any]:
        """Load the register map into a dictionary, compatible with the `Map` model."""

    @classmethod
    @abstractmethod
    def get_config_cls(cls) -> type[LoaderConfig]:
        """Get the configuration class for the loader."""

__call__()

Load the register map.

Source code in src/corsair/_loaders/base.py
def __call__(self) -> Map:
    """Load the register map."""
    if not self.config.mapfile.exists():
        raise FileNotFoundError(f"CSR map file not found: {self.config.mapfile}")

    self.raw_data = self._load_raw()

    try:
        return Map.model_validate(self.raw_data)
    except ValidationError as e:
        raise LoaderValidationError(e, stringify_model_errors(e, self.raw_data)) from e

__init__(config)

Initialize the loader.

Source code in src/corsair/_loaders/base.py
def __init__(self, config: LoaderConfig) -> None:
    """Initialize the loader."""
    self.config = config
    self.raw_data: dict[str, Any] = {}

    if not isinstance(self.config, self.get_config_cls()):
        raise TypeError(
            f"Configuration instance is not of the expected type of "
            f"{self.__class__.__name__}.{self.get_config_cls().__name__}"
        )

get_config_cls() abstractmethod classmethod

Get the configuration class for the loader.

Source code in src/corsair/_loaders/base.py
@classmethod
@abstractmethod
def get_config_cls(cls) -> type[LoaderConfig]:
    """Get the configuration class for the loader."""

LoaderConfig

Bases: BaseModel, ABC

Base configuration for a loader.

Source code in src/corsair/_loaders/base.py
class LoaderConfig(BaseModel, ABC):
    """Base configuration for a loader."""

    mapfile: Path = Path("csrmap.yaml")
    """Path to the register map file."""

    model_config = ConfigDict(
        extra="forbid",
        use_attribute_docstrings=True,
    )

    @property
    @abstractmethod
    def loader_cls(self) -> type[Loader]:
        """Related loader class."""

    @abstractmethod
    def get_kind(self) -> str:
        """Get the kind of the loader."""

loader_cls abstractmethod property

Related loader class.

mapfile = Path('csrmap.yaml') class-attribute instance-attribute

Path to the register map file.

get_kind() abstractmethod

Get the kind of the loader.

Source code in src/corsair/_loaders/base.py
@abstractmethod
def get_kind(self) -> str:
    """Get the kind of the loader."""

LoaderValidationError

Bases: Exception

Raised when loader fails during data validation.

Source code in src/corsair/_loaders/base.py
class LoaderValidationError(Exception):
    """Raised when loader fails during data validation."""

    def __init__(self, pydantic_error: ValidationError, error_messages: list[str]) -> None:
        """Initialize the exception."""
        self.pydantic_error = pydantic_error
        self.error_messages = error_messages  # Store the stringified errors
        super().__init__("Loader failed during data validation")

    def __str__(self) -> str:
        """Represent exception as a string."""
        err = "\n".join(self.error_messages)
        return f"{self.args[0]}\n{err}"

__init__(pydantic_error, error_messages)

Initialize the exception.

Source code in src/corsair/_loaders/base.py
def __init__(self, pydantic_error: ValidationError, error_messages: list[str]) -> None:
    """Initialize the exception."""
    self.pydantic_error = pydantic_error
    self.error_messages = error_messages  # Store the stringified errors
    super().__init__("Loader failed during data validation")

__str__()

Represent exception as a string.

Source code in src/corsair/_loaders/base.py
def __str__(self) -> str:
    """Represent exception as a string."""
    err = "\n".join(self.error_messages)
    return f"{self.args[0]}\n{err}"

Map

Bases: MapableItem

Collection of memory-mapped items.

Source code in src/corsair/_model.py
class Map(MapableItem):
    """Collection of memory-mapped items."""

    kind: Literal["map"] = "map"
    """Item kind discriminator."""

    address_width: PositiveInt
    """Map address bit width."""

    register_width: Pow2Int
    """Map register bit width."""

    items: tuple[AnyMapableItem, ...]
    """Items within the map. Should be not empty.

    Items are sorted by offsets.
    """

    @FrozenProperty
    def size(self) -> Pow2Int:
        """Byte size of address space that map covers."""
        return 2**self.address_width

    @FrozenProperty
    def granularity(self) -> Pow2Int:
        """Byte size of a single register within map."""
        return math.ceil(self.register_width / 8)

    @FrozenProperty
    def maps(self) -> tuple[Map, ...]:
        """All submaps within map."""
        return tuple(item for item in self.items if isinstance(item, Map))

    @FrozenProperty
    def map_arrays(self) -> tuple[MapArray, ...]:
        """All submap arrays within map."""
        return tuple(item for item in self.items if isinstance(item, MapArray))

    @FrozenProperty
    def registers(self) -> tuple[Register, ...]:
        """All registers within map."""
        return tuple(item for item in self.items if isinstance(item, Register))

    @FrozenProperty
    def register_arrays(self) -> tuple[RegisterArray, ...]:
        """All register arrays within map."""
        return tuple(item for item in self.items if isinstance(item, RegisterArray))

    @FrozenProperty
    def memories(self) -> tuple[Memory, ...]:
        """All memories within map."""
        return tuple(item for item in self.items if isinstance(item, Memory))

    @FrozenProperty
    def memory_arrays(self) -> tuple[MemoryArray, ...]:
        """All memory arrays within map."""
        return tuple(item for item in self.items if isinstance(item, MemoryArray))

    @FrozenProperty
    def has_maps(self) -> bool:
        """Check if current map contains submaps."""
        return any(self.maps)

    @FrozenProperty
    def has_map_arrays(self) -> bool:
        """Check if current map contains submap arrays."""
        return any(self.map_arrays)

    @FrozenProperty
    def has_registers(self) -> bool:
        """Check if current map contains registers."""
        return any(self.registers)

    @FrozenProperty
    def has_register_arrays(self) -> bool:
        """Check if current map contains register arrays."""
        return any(self.register_arrays)

    @FrozenProperty
    def has_memories(self) -> bool:
        """Check if current map contains memory blocks."""
        return any(self.memories)

    @FrozenProperty
    def has_memory_arrays(self) -> bool:
        """Check if current map contains memory arrays."""
        return any(self.memory_arrays)

    @FrozenProperty
    def address(self) -> NonNegativeInt:
        """Global byte address of the item."""
        # root map has no parent, so its address is its offset
        return self.offset if self.is_root else super().address

    @property
    def is_root(self) -> bool:
        """Check if current map is a root map."""
        return self.parent is None

    @property
    def parent_map(self) -> Map:
        """Parent map.

        Requires parent to be set.
        """
        if self.parent is None:
            raise ValueError("Parent has to be set before accessing parent map")
        if not isinstance(self.parent, Map):
            raise TypeError("Parent is not an instance of `Map`")
        return self.parent

    @property
    def _children(self) -> tuple[NamedItem, ...]:
        """All subitems within item."""
        return self.items

    @field_validator("items", mode="after")
    @classmethod
    def _sort_items(cls, values: tuple[MapableItem, ...]) -> tuple[MapableItem, ...]:
        """Sort items by offset."""
        return tuple(sorted(values, key=lambda v: v.offset))

    @model_validator(mode="after")
    def _validate_items_provided(self) -> Self:
        """Validate that at least one item is provided."""
        if len(self.items) == 0:
            raise ValueError("empty map is not allowed, at least one item has to be provided")
        return self

    @model_validator(mode="after")
    def _validate_not_implemented_features(self) -> Self:
        """Validate that not implemented features are not used."""
        if self.has_maps:
            raise ValueError("submaps are not implemented yet")
        if self.has_memories:
            raise ValueError("memories are not implemented yet")
        if self.has_map_arrays:
            raise ValueError("submap arrays are not implemented yet")
        if self.has_register_arrays:
            raise ValueError("register arrays are not implemented yet")
        if self.has_memory_arrays:
            raise ValueError("memory arrays are not implemented yet")
        return self

    @model_validator(mode="after")
    def _validate_min_register_width(self) -> Self:
        """Validate that register width is at least single byte."""
        min_width = 8
        if self.register_width < min_width:
            raise ValueError(
                f"minimal allowed 'register_width' for a map is {min_width}, but {self.register_width} provided"
            )
        return self

    @model_validator(mode="after")
    def _validate_items_unique_names(self) -> Self:
        """Validate that all item names inside map are unique."""
        names = [item.name for item in self.items]
        duplicates = {name for name in names if names.count(name) > 1}
        if duplicates:
            raise ValueError(f"some item names are not unique and used more than once: {duplicates}")
        return self

    @model_validator(mode="after")
    def _validate_items_unique_offsets(self) -> Self:
        """Validate that all item offsets inside map are unique."""
        offsets = [item.offset for item in self.items]
        duplicates = {offset for offset in offsets if offsets.count(offset) > 1}
        if duplicates:
            raise ValueError(f"some item offsets are not unique and used more than once: {duplicates}")
        return self

    @model_validator(mode="after")
    def _validate_items_offset_alignment(self) -> Self:
        """Validate that all item offsets are aligned to the map granularity."""
        for item in self.items:
            if item.offset % self.granularity != 0:
                raise ValueError(
                    f"item {item.name} offset 0x{item.offset:x} is not aligned to map granularity {self.granularity}"
                )
        return self

    @model_validator(mode="after")
    def _validate_items_address_width(self) -> Self:
        """Validate that all child maps and memories have address width less or equal to current map address width."""
        for item in itertools.chain(self.maps, self.memories):
            if item.address_width > self.address_width:
                raise ValueError(
                    f"item {item.name} address width {item.address_width} is "
                    f"greater than map address width {self.address_width}"
                )
        return self

    @model_validator(mode="after")
    def _validate_items_address_collisions(self) -> Self:
        """Validate that there is no address collisions between items."""
        item_address_ranges: dict[AnyMapableItem, tuple[NonNegativeInt, NonNegativeInt]] = {}
        for item in self.items:
            if isinstance(item, Map | Memory):
                item_address_ranges[item] = (item.offset, item.offset + item.size - 1)
            elif isinstance(item, Register):
                item_address_ranges[item] = (
                    item.offset,
                    item.offset + self.granularity - 1,
                )

        # Check for collisions
        for item, (start, end) in item_address_ranges.items():
            for other_item, (other_start, other_end) in item_address_ranges.items():
                if item is other_item:
                    continue
                if start <= other_end and end >= other_start:
                    raise ValueError(f"address collision between {item.name} and {other_item.name}")

        # Check that no item is falling out of the root map address space
        for item, (start, end) in item_address_ranges.items():
            if end >= self.size:
                raise ValueError(
                    f"item {item.name} address range [0x{start:x};0x{end+1:x}) is "
                    f"falling out of the root map address space [0x0;0x{self.size:x})"
                )
        return self

    @model_validator(mode="after")
    def _validate_register_fields_width(self) -> Self:
        """Validate that all fields fit register width."""
        for reg in self.registers:
            last_field = reg.fields[-1]
            if last_field.msb >= self.register_width:
                raise ValueError(
                    f"field {last_field.name} (lsb={last_field.lsb} msb={last_field.msb}) "
                    f"exceeds size {self.register_width} of the register within map"
                )
        return self

    @model_validator(mode="after")
    def _validate_array_items_increment(self) -> Self:
        """Validate that all array items within map has correct `increment`."""
        # TODO: implement
        return self

address_width instance-attribute

Map address bit width.

is_root property

Check if current map is a root map.

items instance-attribute

Items within the map. Should be not empty.

Items are sorted by offsets.

kind = 'map' class-attribute instance-attribute

Item kind discriminator.

parent_map property

Parent map.

Requires parent to be set.

register_width instance-attribute

Map register bit width.

address()

Global byte address of the item.

Source code in src/corsair/_model.py
@FrozenProperty
def address(self) -> NonNegativeInt:
    """Global byte address of the item."""
    # root map has no parent, so its address is its offset
    return self.offset if self.is_root else super().address

granularity()

Byte size of a single register within map.

Source code in src/corsair/_model.py
@FrozenProperty
def granularity(self) -> Pow2Int:
    """Byte size of a single register within map."""
    return math.ceil(self.register_width / 8)

has_map_arrays()

Check if current map contains submap arrays.

Source code in src/corsair/_model.py
@FrozenProperty
def has_map_arrays(self) -> bool:
    """Check if current map contains submap arrays."""
    return any(self.map_arrays)

has_maps()

Check if current map contains submaps.

Source code in src/corsair/_model.py
@FrozenProperty
def has_maps(self) -> bool:
    """Check if current map contains submaps."""
    return any(self.maps)

has_memories()

Check if current map contains memory blocks.

Source code in src/corsair/_model.py
@FrozenProperty
def has_memories(self) -> bool:
    """Check if current map contains memory blocks."""
    return any(self.memories)

has_memory_arrays()

Check if current map contains memory arrays.

Source code in src/corsair/_model.py
@FrozenProperty
def has_memory_arrays(self) -> bool:
    """Check if current map contains memory arrays."""
    return any(self.memory_arrays)

has_register_arrays()

Check if current map contains register arrays.

Source code in src/corsair/_model.py
@FrozenProperty
def has_register_arrays(self) -> bool:
    """Check if current map contains register arrays."""
    return any(self.register_arrays)

has_registers()

Check if current map contains registers.

Source code in src/corsair/_model.py
@FrozenProperty
def has_registers(self) -> bool:
    """Check if current map contains registers."""
    return any(self.registers)

map_arrays()

All submap arrays within map.

Source code in src/corsair/_model.py
@FrozenProperty
def map_arrays(self) -> tuple[MapArray, ...]:
    """All submap arrays within map."""
    return tuple(item for item in self.items if isinstance(item, MapArray))

maps()

All submaps within map.

Source code in src/corsair/_model.py
@FrozenProperty
def maps(self) -> tuple[Map, ...]:
    """All submaps within map."""
    return tuple(item for item in self.items if isinstance(item, Map))

memories()

All memories within map.

Source code in src/corsair/_model.py
@FrozenProperty
def memories(self) -> tuple[Memory, ...]:
    """All memories within map."""
    return tuple(item for item in self.items if isinstance(item, Memory))

memory_arrays()

All memory arrays within map.

Source code in src/corsair/_model.py
@FrozenProperty
def memory_arrays(self) -> tuple[MemoryArray, ...]:
    """All memory arrays within map."""
    return tuple(item for item in self.items if isinstance(item, MemoryArray))

register_arrays()

All register arrays within map.

Source code in src/corsair/_model.py
@FrozenProperty
def register_arrays(self) -> tuple[RegisterArray, ...]:
    """All register arrays within map."""
    return tuple(item for item in self.items if isinstance(item, RegisterArray))

registers()

All registers within map.

Source code in src/corsair/_model.py
@FrozenProperty
def registers(self) -> tuple[Register, ...]:
    """All registers within map."""
    return tuple(item for item in self.items if isinstance(item, Register))

size()

Byte size of address space that map covers.

Source code in src/corsair/_model.py
@FrozenProperty
def size(self) -> Pow2Int:
    """Byte size of address space that map covers."""
    return 2**self.address_width

MapArray

Bases: Map, ArrayItem

Logical collection of similar maps with common properties.

Source code in src/corsair/_model.py
class MapArray(Map, ArrayItem):
    """Logical collection of similar maps with common properties."""

    kind: Literal["map_array"] = "map_array"  # type: ignore reportIncompatibleVariableOverride
    """Item kind discriminator."""

    @property
    def generated_maps(self) -> tuple[Map, ...]:
        """Generated maps in the array."""
        # _generate_items creates tuple of `Map` items, so waiver is safe here
        return self.generated_items  # type: ignore reportReturnType

    def _generate_items(self) -> tuple[NamedItem, ...]:
        """Generate concrete items in the array."""
        raise NotImplementedError

generated_maps property

Generated maps in the array.

kind = 'map_array' class-attribute instance-attribute

Item kind discriminator.

MapableItem

Bases: NamedItem

NamedItem that can be mappend into global address space.

This class add properties to work with address space and get address of the item.

Source code in src/corsair/_model.py
class MapableItem(NamedItem):
    """NamedItem that can be mappend into global address space.

    This class add properties to work with address space and get address of the item.
    """

    offset: NonNegativeInt
    """Byte offset from the parent addressable item."""

    @FrozenProperty
    def base_address(self) -> NonNegativeInt:
        """Address, which this item is offsetted from."""
        if self.parent is None:
            return 0
        if not isinstance(self.parent, MapableItem):
            raise TypeError("Parent has to be a `MapableItem`")
        return self.parent.address

    @FrozenProperty
    def address(self) -> NonNegativeInt:
        """Global address of the item."""
        return self.base_address + self.offset

offset instance-attribute

Byte offset from the parent addressable item.

address()

Global address of the item.

Source code in src/corsair/_model.py
@FrozenProperty
def address(self) -> NonNegativeInt:
    """Global address of the item."""
    return self.base_address + self.offset

base_address()

Address, which this item is offsetted from.

Source code in src/corsair/_model.py
@FrozenProperty
def base_address(self) -> NonNegativeInt:
    """Address, which this item is offsetted from."""
    if self.parent is None:
        return 0
    if not isinstance(self.parent, MapableItem):
        raise TypeError("Parent has to be a `MapableItem`")
    return self.parent.address

MarkdownGenerator

Bases: Generator

Markdown file generator for a register map.

Source code in src/corsair/_generators/markdown.py
class MarkdownGenerator(Generator):
    """Markdown file generator for a register map."""

    class Config(GeneratorConfig):
        """Configuration for the Markdown generator."""

        kind: Literal["markdown"] = "markdown"
        """Generator kind discriminator."""

        file_name: str = "regmap.md"
        """Name of the output file."""

        title: str = "Register Map"
        """Document title."""

        template_name: str = "regmap.md.j2"
        """Name of the Jinja2 template to use."""

        show_images: bool = False
        """Enable generating images for bit fields of a register."""

        image_dir: Path = Path("img")
        """Directory for storing images."""

        show_conventions: bool = True
        """Enable generating table with register access modes explained."""

        show_disclaimer: bool = True
        """Enable generating disclaimer with version at the beginning of the file."""

        show_hardware_mode: bool = False
        """Enable showing hardware mode for each field."""

        use_table_for_fields: bool = False
        """Use tables to describe fields instead of lists."""

        wavedrom: WaveDromGenerator.Config = WaveDromGenerator.Config()
        """Configuration for the WaveDrom generator."""

        @property
        def generator_cls(self) -> type[Generator]:
            """Related generator class."""
            return MarkdownGenerator

        def get_kind(self) -> str:
            """Get the kind of the generator."""
            return self.kind

    @classmethod
    def get_config_cls(cls) -> type[GeneratorConfig]:
        """Get the configuration class for the generator."""
        return cls.Config

    def _generate(self) -> TypeGenerator[Path, None, None]:
        """Generate all the outputs."""
        assert isinstance(self.config, self.Config)  # noqa: S101, to help type checker

        context = {
            "cfg": self.config,
            "regmap": self.register_map,
        }

        yield self._render_to_file(
            template_name=self.config.template_name,
            context=context,
            file_name=self.config.file_name,
        )

        if self.config.show_images:
            wd_gen = WaveDromGenerator(
                label=f"{self.label}.wavedrom",
                register_map=self.register_map,
                config=self.config.wavedrom,
                output_dir=self.config.image_dir,
            )
            yield from wd_gen()

Config

Bases: GeneratorConfig

Configuration for the Markdown generator.

Source code in src/corsair/_generators/markdown.py
class Config(GeneratorConfig):
    """Configuration for the Markdown generator."""

    kind: Literal["markdown"] = "markdown"
    """Generator kind discriminator."""

    file_name: str = "regmap.md"
    """Name of the output file."""

    title: str = "Register Map"
    """Document title."""

    template_name: str = "regmap.md.j2"
    """Name of the Jinja2 template to use."""

    show_images: bool = False
    """Enable generating images for bit fields of a register."""

    image_dir: Path = Path("img")
    """Directory for storing images."""

    show_conventions: bool = True
    """Enable generating table with register access modes explained."""

    show_disclaimer: bool = True
    """Enable generating disclaimer with version at the beginning of the file."""

    show_hardware_mode: bool = False
    """Enable showing hardware mode for each field."""

    use_table_for_fields: bool = False
    """Use tables to describe fields instead of lists."""

    wavedrom: WaveDromGenerator.Config = WaveDromGenerator.Config()
    """Configuration for the WaveDrom generator."""

    @property
    def generator_cls(self) -> type[Generator]:
        """Related generator class."""
        return MarkdownGenerator

    def get_kind(self) -> str:
        """Get the kind of the generator."""
        return self.kind

file_name = 'regmap.md' class-attribute instance-attribute

Name of the output file.

generator_cls property

Related generator class.

image_dir = Path('img') class-attribute instance-attribute

Directory for storing images.

kind = 'markdown' class-attribute instance-attribute

Generator kind discriminator.

show_conventions = True class-attribute instance-attribute

Enable generating table with register access modes explained.

show_disclaimer = True class-attribute instance-attribute

Enable generating disclaimer with version at the beginning of the file.

show_hardware_mode = False class-attribute instance-attribute

Enable showing hardware mode for each field.

show_images = False class-attribute instance-attribute

Enable generating images for bit fields of a register.

template_name = 'regmap.md.j2' class-attribute instance-attribute

Name of the Jinja2 template to use.

title = 'Register Map' class-attribute instance-attribute

Document title.

use_table_for_fields = False class-attribute instance-attribute

Use tables to describe fields instead of lists.

wavedrom = WaveDromGenerator.Config() class-attribute instance-attribute

Configuration for the WaveDrom generator.

get_kind()

Get the kind of the generator.

Source code in src/corsair/_generators/markdown.py
def get_kind(self) -> str:
    """Get the kind of the generator."""
    return self.kind

get_config_cls() classmethod

Get the configuration class for the generator.

Source code in src/corsair/_generators/markdown.py
@classmethod
def get_config_cls(cls) -> type[GeneratorConfig]:
    """Get the configuration class for the generator."""
    return cls.Config

Memory

Bases: MapableItem

Memory block.

Source code in src/corsair/_model.py
class Memory(MapableItem):
    """Memory block."""

    kind: Literal["memory"] = "memory"
    """Item kind discriminator."""

    address_width: PositiveInt
    """Memory address bit width."""

    data_width: PositiveInt
    """Memory data bit width."""

    style: MemoryStyle
    """Memory implementation style."""

    initial_values: tuple[tuple[NonNegativeInt, NonNegativeInt], ...]
    """Initial values for selected memory locations.

    Each tuple contains memory word index (address within memory) and value.
    """

    @FrozenProperty
    def capacity(self) -> Pow2Int:
        """Memory capacity in memory words."""
        return 2**self.address_width

    @FrozenProperty
    def size(self) -> Pow2Int:
        """Memory size in bytes."""
        return self.capacity * self.granularity

    @FrozenProperty
    def granularity(self) -> PositiveInt:
        """Memory granularity in bytes."""
        return math.ceil(self.data_width / 8)

    @FrozenProperty
    def access(self) -> AccessCategory:
        """Memory access rights for CSR map."""
        return self.style.access

    @property
    def parent_map(self) -> Map:
        """Parent map.

        Requires parent to be set.
        """
        if self.parent is None:
            raise ValueError("Parent has to be set before accessing parent map")
        if not isinstance(self.parent, Map):
            raise TypeError("Parent is not an instance of `Map`")
        return self.parent

    @property
    def _children(self) -> tuple[NamedItem, ...]:
        """All subitems within item."""
        return ()

    @model_validator(mode="after")
    def _validate_initial_values(self) -> Self:
        """Validate that initial values are valid."""
        for i, (addr, value) in enumerate(self.initial_values):
            if addr >= self.capacity:
                raise ValueError(f"initial value {i} address 0x{addr:x} is out of memory capacity 0x{self.capacity:x}")
            if value >= 2**self.data_width:
                raise ValueError(
                    f"initial value {i} data 0x{value:x} is out of memory data width {self.data_width} bits"
                )
        return self

address_width instance-attribute

Memory address bit width.

data_width instance-attribute

Memory data bit width.

initial_values instance-attribute

Initial values for selected memory locations.

Each tuple contains memory word index (address within memory) and value.

kind = 'memory' class-attribute instance-attribute

Item kind discriminator.

parent_map property

Parent map.

Requires parent to be set.

style instance-attribute

Memory implementation style.

access()

Memory access rights for CSR map.

Source code in src/corsair/_model.py
@FrozenProperty
def access(self) -> AccessCategory:
    """Memory access rights for CSR map."""
    return self.style.access

capacity()

Memory capacity in memory words.

Source code in src/corsair/_model.py
@FrozenProperty
def capacity(self) -> Pow2Int:
    """Memory capacity in memory words."""
    return 2**self.address_width

granularity()

Memory granularity in bytes.

Source code in src/corsair/_model.py
@FrozenProperty
def granularity(self) -> PositiveInt:
    """Memory granularity in bytes."""
    return math.ceil(self.data_width / 8)

size()

Memory size in bytes.

Source code in src/corsair/_model.py
@FrozenProperty
def size(self) -> Pow2Int:
    """Memory size in bytes."""
    return self.capacity * self.granularity

MemoryArray

Bases: Memory, ArrayItem

Logical collection of similar memory blocks with common properties.

Source code in src/corsair/_model.py
class MemoryArray(Memory, ArrayItem):
    """Logical collection of similar memory blocks with common properties."""

    kind: Literal["memory_array"] = "memory_array"  # type: ignore reportIncompatibleVariableOverride
    """Item kind discriminator."""

    @property
    def generated_memories(self) -> tuple[Memory, ...]:
        """Generated memories in the array."""
        # _generate_items creates tuple of `Memory` items, so waiver is safe here
        return self.generated_items  # type: ignore reportReturnType

    def _generate_items(self) -> tuple[NamedItem, ...]:
        """Generate concrete items in the array."""
        raise NotImplementedError

generated_memories property

Generated memories in the array.

kind = 'memory_array' class-attribute instance-attribute

Item kind discriminator.

MemoryStyle

Bases: str, Enum

Memory block implementation style for Memory.

Source code in src/corsair/_model.py
class MemoryStyle(str, enum.Enum):
    """Memory block implementation style for `Memory`."""

    INTERNAL_RO = "internal_ro"
    """Memory block is implemented inside CSR map RTL module.
    It can be read from bus and can be written to by hardware.
    """

    INTERNAL_CONST = "internal_const"
    """Memory block is implemented inside CSR map RTL module.
    It filled with constants that can be read from bus only.
    """

    INTERNAL_WO = "internal_wo"
    """Memory block is implemented inside CSR map RTL module.
    It can be written to by bus and can be read by hardware.
    """

    INTERNAL_RW = "internal_rw"
    """Memory block is implemented inside CSR map RTL module.
    It can be read from bus and can be written to by bus only.
    """

    EXTERNAL_RO = "external_ro"
    """Memory block is implemented outside CSR map RTL module.
    It can be read from bus by read port within interface.
    """

    EXTERNAL_WO = "external_wo"
    """Memory block is implemented outside CSR map RTL module.
    It can be written to by bus by write port within interface.
    """

    EXTERNAL_RW = "external_rw"
    """Memory block is implemented outside CSR map RTL module.
    It can be read and written from bus by read and write ports within interface.
    """

    def __str__(self) -> str:
        """Convert enumeration member into string."""
        return self.value

    @property
    def access(self) -> AccessCategory:
        """Access category."""
        if "ro" in self.value:
            return AccessCategory.RO
        if "wo" in self.value:
            return AccessCategory.WO
        if "rw" in self.value:
            return AccessCategory.RW
        raise ValueError(f"Cannot map memory style {self} to any access category")

    @property
    def is_ro(self) -> bool:
        """Check that this access mode belongs to RO category."""
        return self.access == AccessCategory.RO

    @property
    def is_wo(self) -> bool:
        """Check that this access mode belongs to WO category."""
        return self.access == AccessCategory.WO

    @property
    def is_rw(self) -> bool:
        """Check that this access mode belongs to RW category."""
        return self.access == AccessCategory.RW

EXTERNAL_RO = 'external_ro' class-attribute instance-attribute

Memory block is implemented outside CSR map RTL module. It can be read from bus by read port within interface.

EXTERNAL_RW = 'external_rw' class-attribute instance-attribute

Memory block is implemented outside CSR map RTL module. It can be read and written from bus by read and write ports within interface.

EXTERNAL_WO = 'external_wo' class-attribute instance-attribute

Memory block is implemented outside CSR map RTL module. It can be written to by bus by write port within interface.

INTERNAL_CONST = 'internal_const' class-attribute instance-attribute

Memory block is implemented inside CSR map RTL module. It filled with constants that can be read from bus only.

INTERNAL_RO = 'internal_ro' class-attribute instance-attribute

Memory block is implemented inside CSR map RTL module. It can be read from bus and can be written to by hardware.

INTERNAL_RW = 'internal_rw' class-attribute instance-attribute

Memory block is implemented inside CSR map RTL module. It can be read from bus and can be written to by bus only.

INTERNAL_WO = 'internal_wo' class-attribute instance-attribute

Memory block is implemented inside CSR map RTL module. It can be written to by bus and can be read by hardware.

access property

Access category.

is_ro property

Check that this access mode belongs to RO category.

is_rw property

Check that this access mode belongs to RW category.

is_wo property

Check that this access mode belongs to WO category.

__str__()

Convert enumeration member into string.

Source code in src/corsair/_model.py
def __str__(self) -> str:
    """Convert enumeration member into string."""
    return self.value

NamedItem

Bases: BaseModel, ABC

Base data structure for all CSR model internal items: map, register, field, etc.

Its immutability allows to provide solid contract for generators, when they traverse the model. Immutability also allows to cache properties to avoid recalculations and use model hashing.

This class provides basic functionality to differentiate an item from other items and understand its place in the hierarchy.

Source code in src/corsair/_model.py
class NamedItem(BaseModel, ABC):
    """Base data structure for all CSR model internal items: map, register, field, etc.

    Its immutability allows to provide solid contract for generators, when they traverse the model.
    Immutability also allows to cache properties to avoid recalculations and use model hashing.

    This class provides basic functionality to differentiate an item from other items
    and understand its place in the hierarchy.
    """

    model_config = ConfigDict(
        # Docstrings of attributesshould be used for field descriptions
        use_attribute_docstrings=True,
        # Model is faux-immutable
        frozen=True,
        # Extra values are not permitted
        extra="forbid",
        # Hide input in errors
        hide_input_in_errors=True,
        # Types below are not fields, but properties
        ignored_types=(FrozenProperty,),
    )

    name: IdentifierStr
    """Name of an item."""

    doc: TextStr
    """Docstring for an item.

    Follows Python docstring rules - first line is a brief summary,
    then optionally, empty line following detailed multiline description.
    """

    metadata: ItemMetadata = ItemMetadata()
    """Optional user metadata attached to an item."""

    # Private fields to store property values
    _parent: NamedItem | None = None

    @FrozenProperty
    def brief(self) -> SingleLineStr:
        """Brief description for an item, derived from `doc`.

        First line of `doc` is used.
        """
        return self.doc.split("\n", 1)[0].strip()

    @FrozenProperty
    def description(self) -> TextStr:
        """Detailed description for an item, derived from `doc`."""
        parts = self.doc.split("\n", 1)
        return (parts[1] if len(parts) > 1 else "").strip()

    @FrozenProperty
    def doc_as_single_line(self) -> str:
        """Docstring converted to a single line string."""
        return " ".join(self.doc.split())

    @FrozenProperty
    def path(self) -> str:
        """Full path in object hierarchy."""
        return self.name if self.parent is None else f"{self.parent.path}.{self.name}"

    @property
    def parent(self) -> NamedItem | None:
        """Parent for the current register model item."""
        return self._parent

    @property
    @abstractmethod
    def _children(self) -> tuple[NamedItem, ...]:
        """All subitems within item."""

    @model_validator(mode="after")
    def _update_backlinks(self) -> Self:
        """Update backlinks to current parent in all child items and clear relevant caches."""
        for child in self._children:
            # Set the parent first
            child._parent = self  # noqa: SLF001

            # Clear all FrozenProperty caches on the child
            # This ensures that properties recalculated after the parent is set.
            for attr_name, attr_value in type(child).__dict__.items():
                if isinstance(attr_value, FrozenProperty):
                    # The cached value is stored in the instance's dict with the property name.
                    # Use pop with a default to avoid KeyError if the cache wasn't populated yet.
                    child.__dict__.pop(attr_name, None)

        return self

doc instance-attribute

Docstring for an item.

Follows Python docstring rules - first line is a brief summary, then optionally, empty line following detailed multiline description.

metadata = ItemMetadata() class-attribute instance-attribute

Optional user metadata attached to an item.

name instance-attribute

Name of an item.

parent property

Parent for the current register model item.

brief()

Brief description for an item, derived from doc.

First line of doc is used.

Source code in src/corsair/_model.py
@FrozenProperty
def brief(self) -> SingleLineStr:
    """Brief description for an item, derived from `doc`.

    First line of `doc` is used.
    """
    return self.doc.split("\n", 1)[0].strip()

description()

Detailed description for an item, derived from doc.

Source code in src/corsair/_model.py
@FrozenProperty
def description(self) -> TextStr:
    """Detailed description for an item, derived from `doc`."""
    parts = self.doc.split("\n", 1)
    return (parts[1] if len(parts) > 1 else "").strip()

doc_as_single_line()

Docstring converted to a single line string.

Source code in src/corsair/_model.py
@FrozenProperty
def doc_as_single_line(self) -> str:
    """Docstring converted to a single line string."""
    return " ".join(self.doc.split())

path()

Full path in object hierarchy.

Source code in src/corsair/_model.py
@FrozenProperty
def path(self) -> str:
    """Full path in object hierarchy."""
    return self.name if self.parent is None else f"{self.parent.path}.{self.name}"

PyModuleLoader

Bases: Loader

Load register map from a Python module.

Source code in src/corsair/_loaders/pymodule.py
class PyModuleLoader(Loader):
    """Load register map from a Python module."""

    class Config(LoaderConfig):
        """Configuration for the Python module loader."""

        kind: Literal["py"] = "py"
        """Loader kind discriminator."""

        @property
        def loader_cls(self) -> type[Loader]:
            """Loader class to use."""
            return PyModuleLoader

        def get_kind(self) -> str:
            """Get the kind of the loader."""
            return self.kind

    @classmethod
    def get_config_cls(cls) -> type[LoaderConfig]:
        """Get the configuration class for the loader."""
        return cls.Config

    def _load_raw(self) -> dict[str, Any]:
        """Load the register map."""
        raise NotImplementedError

Config

Bases: LoaderConfig

Configuration for the Python module loader.

Source code in src/corsair/_loaders/pymodule.py
class Config(LoaderConfig):
    """Configuration for the Python module loader."""

    kind: Literal["py"] = "py"
    """Loader kind discriminator."""

    @property
    def loader_cls(self) -> type[Loader]:
        """Loader class to use."""
        return PyModuleLoader

    def get_kind(self) -> str:
        """Get the kind of the loader."""
        return self.kind

kind = 'py' class-attribute instance-attribute

Loader kind discriminator.

loader_cls property

Loader class to use.

get_kind()

Get the kind of the loader.

Source code in src/corsair/_loaders/pymodule.py
def get_kind(self) -> str:
    """Get the kind of the loader."""
    return self.kind

get_config_cls() classmethod

Get the configuration class for the loader.

Source code in src/corsair/_loaders/pymodule.py
@classmethod
def get_config_cls(cls) -> type[LoaderConfig]:
    """Get the configuration class for the loader."""
    return cls.Config

Register

Bases: MapableItem

Control and Status Register.

Source code in src/corsair/_model.py
class Register(MapableItem):
    """Control and Status Register."""

    kind: Literal["register"] = "register"
    """Item kind discriminator."""

    fields: tuple[Field, ...]
    """Bit fields inside a register.

    Fields are sorted from LSB to MSB.
    """

    @property
    def parent_map(self) -> Map:
        """Parent map.

        Requires parent to be set.
        """
        if self.parent is None:
            raise ValueError("Parent has to be set before accessing parent map")
        if not isinstance(self.parent, Map):
            raise TypeError("Parent is not an instance of `Map`")
        return self.parent

    @property
    def _children(self) -> tuple[NamedItem, ...]:
        """All subitems within item."""
        return self.fields

    @FrozenProperty
    def width(self) -> NonNegativeInt:
        """Minimum number of bits required to represent the register."""
        return max(f.msb for f in self.fields) + 1

    @FrozenProperty
    def access(self) -> AccessCategory:
        """Access rights based on access modes of the fields."""
        if all(f.access.is_ro for f in self.fields):
            return AccessCategory.RO
        if all(f.access.is_wo for f in self.fields):
            return AccessCategory.WO
        return AccessCategory.RW

    @FrozenProperty
    def reset(self) -> NonNegativeInt:
        """Reset value based on reset values of the fields.

        Unknown reset value for a field converted to zero.
        """
        return sum(f.reset << f.lsb for f in self.fields if f.reset is not None)

    @FrozenProperty
    def reset_binstr(self) -> str:
        """Reset value represented as a binary string where unknown bits masked as 'x'.

        No any prefix. Leading zeroes are added to match `width`.
        Examples: 00110, 1x10
        """
        bits = ["0"] * self.width
        for f in self.fields:
            if f.reset:
                bits[f.lsb : f.msb + 1] = list(f"{f.reset:0{f.width}b}")
            else:
                bits[f.lsb : f.msb + 1] = ["x"] * f.width
        return "".join(reversed(bits))

    @FrozenProperty
    def reset_hexstr(self) -> str:
        """Reset value represented as a hexadecimal string where unknown nibbles masked as 'x'.

        No any prefix. Leading zeroes are added to match `width`.
        Examples: 0002bca, x2ax
        """
        nibbles = ["0"] * math.ceil(self.width / 4)
        for i in range(len(nibbles)):
            bit_slice = self.reset_binstr[::-1][i * 4 : i * 4 + 4]
            if "x" in bit_slice:
                nibbles[i] = "x"
            else:
                nibbles[i] = f"{int(bit_slice, 2):x}"
        return "".join(nibbles)

    @FrozenProperty
    def fields_with_reserved(self) -> tuple[Field, ...]:
        """All fields including virtual fields for reserved bits."""

        def _create_reserved_field(lsb: int, msb: int) -> Field:
            field = Field(
                name=f"_reserved_{msb}_{lsb}",
                reset=0,
                access=AccessMode.RO,
                hardware=HardwareMode.NA,
                doc="These bits are unused and reserved for future use",
                offset=lsb,
                width=msb - lsb + 1,
            )
            field._parent = self  # noqa: SLF001
            return field

        res = []
        lsb = 0
        for f in self.fields:
            if f.lsb == lsb:
                res.append(f)
                lsb = f.msb + 1
            elif f.lsb > lsb:
                res.append(_create_reserved_field(lsb=lsb, msb=f.lsb - 1))
                res.append(f)
                lsb = f.msb + 1
        if lsb < self.parent_map.register_width - 1:
            res.append(_create_reserved_field(lsb=lsb, msb=self.parent_map.register_width - 1))
        return tuple(res)

    @field_validator("fields", mode="after")
    @classmethod
    def _sort_fields(cls, values: tuple[Field, ...]) -> tuple[Field, ...]:
        """Sort fields by LSB."""
        return tuple(sorted(values, key=lambda v: v.lsb))

    @model_validator(mode="after")
    def _validate_fields_unique_names(self) -> Self:
        """Validate that all field names inside register are unique."""
        names = [field.name for field in self.fields]
        duplicates = {name for name in names if names.count(name) > 1}
        if duplicates:
            raise ValueError(f"some field names are not unique and used more than once: {duplicates}")
        return self

    @model_validator(mode="after")
    def _validate_fields_overlapping(self) -> Self:
        """Validate that no fields overlap other ones."""
        field_bits = {field.name: set(field.bit_indices) for field in self.fields}

        for field in self.fields:
            overlaps = {
                name: bool(set(field.bit_indices).intersection(bits))
                for name, bits in field_bits.items()
                if name != field.name
            }
            if any(v for v in overlaps.values()):
                raise ValueError(f"field {field.name} overlaps with other fields: {', '.join(overlaps.keys())}")
        return self

fields instance-attribute

Bit fields inside a register.

Fields are sorted from LSB to MSB.

kind = 'register' class-attribute instance-attribute

Item kind discriminator.

parent_map property

Parent map.

Requires parent to be set.

access()

Access rights based on access modes of the fields.

Source code in src/corsair/_model.py
@FrozenProperty
def access(self) -> AccessCategory:
    """Access rights based on access modes of the fields."""
    if all(f.access.is_ro for f in self.fields):
        return AccessCategory.RO
    if all(f.access.is_wo for f in self.fields):
        return AccessCategory.WO
    return AccessCategory.RW

fields_with_reserved()

All fields including virtual fields for reserved bits.

Source code in src/corsair/_model.py
@FrozenProperty
def fields_with_reserved(self) -> tuple[Field, ...]:
    """All fields including virtual fields for reserved bits."""

    def _create_reserved_field(lsb: int, msb: int) -> Field:
        field = Field(
            name=f"_reserved_{msb}_{lsb}",
            reset=0,
            access=AccessMode.RO,
            hardware=HardwareMode.NA,
            doc="These bits are unused and reserved for future use",
            offset=lsb,
            width=msb - lsb + 1,
        )
        field._parent = self  # noqa: SLF001
        return field

    res = []
    lsb = 0
    for f in self.fields:
        if f.lsb == lsb:
            res.append(f)
            lsb = f.msb + 1
        elif f.lsb > lsb:
            res.append(_create_reserved_field(lsb=lsb, msb=f.lsb - 1))
            res.append(f)
            lsb = f.msb + 1
    if lsb < self.parent_map.register_width - 1:
        res.append(_create_reserved_field(lsb=lsb, msb=self.parent_map.register_width - 1))
    return tuple(res)

reset()

Reset value based on reset values of the fields.

Unknown reset value for a field converted to zero.

Source code in src/corsair/_model.py
@FrozenProperty
def reset(self) -> NonNegativeInt:
    """Reset value based on reset values of the fields.

    Unknown reset value for a field converted to zero.
    """
    return sum(f.reset << f.lsb for f in self.fields if f.reset is not None)

reset_binstr()

Reset value represented as a binary string where unknown bits masked as 'x'.

No any prefix. Leading zeroes are added to match width. Examples: 00110, 1x10

Source code in src/corsair/_model.py
@FrozenProperty
def reset_binstr(self) -> str:
    """Reset value represented as a binary string where unknown bits masked as 'x'.

    No any prefix. Leading zeroes are added to match `width`.
    Examples: 00110, 1x10
    """
    bits = ["0"] * self.width
    for f in self.fields:
        if f.reset:
            bits[f.lsb : f.msb + 1] = list(f"{f.reset:0{f.width}b}")
        else:
            bits[f.lsb : f.msb + 1] = ["x"] * f.width
    return "".join(reversed(bits))

reset_hexstr()

Reset value represented as a hexadecimal string where unknown nibbles masked as 'x'.

No any prefix. Leading zeroes are added to match width. Examples: 0002bca, x2ax

Source code in src/corsair/_model.py
@FrozenProperty
def reset_hexstr(self) -> str:
    """Reset value represented as a hexadecimal string where unknown nibbles masked as 'x'.

    No any prefix. Leading zeroes are added to match `width`.
    Examples: 0002bca, x2ax
    """
    nibbles = ["0"] * math.ceil(self.width / 4)
    for i in range(len(nibbles)):
        bit_slice = self.reset_binstr[::-1][i * 4 : i * 4 + 4]
        if "x" in bit_slice:
            nibbles[i] = "x"
        else:
            nibbles[i] = f"{int(bit_slice, 2):x}"
    return "".join(nibbles)

width()

Minimum number of bits required to represent the register.

Source code in src/corsair/_model.py
@FrozenProperty
def width(self) -> NonNegativeInt:
    """Minimum number of bits required to represent the register."""
    return max(f.msb for f in self.fields) + 1

RegisterArray

Bases: Register, ArrayItem

Logical collection of similar registers with common properties.

Source code in src/corsair/_model.py
class RegisterArray(Register, ArrayItem):
    """Logical collection of similar registers with common properties."""

    kind: Literal["register_array"] = "register_array"  # type: ignore reportIncompatibleVariableOverride
    """Item kind discriminator."""

    @property
    def generated_registers(self) -> tuple[Register, ...]:
        """Concrete registers in the array."""
        # _generate_items creates tuple of `Register` items, so waiver is safe here
        return self.generated_items  # type: ignore reportReturnType

    def _generate_items(self) -> tuple[NamedItem, ...]:
        """Generate concrete items in the array."""
        raise NotImplementedError

generated_registers property

Concrete registers in the array.

kind = 'register_array' class-attribute instance-attribute

Item kind discriminator.

ResetStyle

Bases: str, Enum

Flip-flop reset style.

Source code in src/corsair/_generators/base.py
class ResetStyle(str, Enum):
    """Flip-flop reset style."""

    SYNC_POS = "sync_pos"
    """Synchronous active high reset."""

    SYNC_NEG = "sync_neg"
    """Synchronous active low reset."""

    ASYNC_POS = "async_pos"
    """Asynchronous active high reset."""

    ASYNC_NEG = "async_neg"
    """Asynchronous active low reset."""

ASYNC_NEG = 'async_neg' class-attribute instance-attribute

Asynchronous active low reset.

ASYNC_POS = 'async_pos' class-attribute instance-attribute

Asynchronous active high reset.

SYNC_NEG = 'sync_neg' class-attribute instance-attribute

Synchronous active low reset.

SYNC_POS = 'sync_pos' class-attribute instance-attribute

Synchronous active high reset.

SerializedLoader

Bases: Loader

Load register map from serialized format (JSON, HJSON, YAML).

Source code in src/corsair/_loaders/serialized.py
class SerializedLoader(Loader):
    """Load register map from serialized format (JSON, HJSON, YAML)."""

    class Config(LoaderConfig):
        """Configuration for the serialized loader."""

        kind: Literal["json", "yaml", "hjson"]
        """Loader kind discriminator."""

        @property
        def loader_cls(self) -> type[Loader]:
            """Related loader class."""
            return SerializedLoader

        def get_kind(self) -> str:
            """Get the kind of the loader."""
            return self.kind

    @classmethod
    def get_config_cls(cls) -> type[LoaderConfig]:
        """Get the configuration class for the loader."""
        return cls.Config

    def _load_raw(self) -> dict[str, Any]:
        """Load the register map."""
        assert isinstance(self.config, self.Config)  # noqa: S101, to help type checker

        if self.config.kind == "json":
            regmap = self._load_json()
        elif self.config.kind == "yaml":
            regmap = self._load_yaml()
        elif self.config.kind == "hjson":
            regmap = self._load_hjson()
        else:
            raise ValueError(f"Invalid kind: {self.config.kind}")

        return regmap

    def _load_json(self) -> dict[str, Any]:
        """Load the register map from a JSON file."""
        with self.config.mapfile.open("r", encoding="utf-8") as file:
            return json.load(file)

    def _load_yaml(self) -> dict[str, Any]:
        """Load the register map from a YAML file."""
        with self.config.mapfile.open("r", encoding="utf-8") as file:
            return yaml.safe_load(file)

    def _load_hjson(self) -> dict[str, Any]:
        """Load the register map from a HJSON file."""
        try:
            import hjson  # type: ignore reportMissingImports
        except ImportError as e:
            raise ImportError(
                "hjson is not installed. "
                "Try installing it with `pip install corsair[hjson]` or `pip install corsair[full]`."
            ) from e

        with self.config.mapfile.open("r", encoding="utf-8") as file:
            return hjson.load(file)

Config

Bases: LoaderConfig

Configuration for the serialized loader.

Source code in src/corsair/_loaders/serialized.py
class Config(LoaderConfig):
    """Configuration for the serialized loader."""

    kind: Literal["json", "yaml", "hjson"]
    """Loader kind discriminator."""

    @property
    def loader_cls(self) -> type[Loader]:
        """Related loader class."""
        return SerializedLoader

    def get_kind(self) -> str:
        """Get the kind of the loader."""
        return self.kind

kind instance-attribute

Loader kind discriminator.

loader_cls property

Related loader class.

get_kind()

Get the kind of the loader.

Source code in src/corsair/_loaders/serialized.py
def get_kind(self) -> str:
    """Get the kind of the loader."""
    return self.kind

get_config_cls() classmethod

Get the configuration class for the loader.

Source code in src/corsair/_loaders/serialized.py
@classmethod
def get_config_cls(cls) -> type[LoaderConfig]:
    """Get the configuration class for the loader."""
    return cls.Config

TemplateEnvironment

Bases: Environment

Singleton environment for managing Jinja2 templates.

Source code in src/corsair/_templates/env.py
class TemplateEnvironment(Environment):
    """Singleton environment for managing Jinja2 templates."""

    _instance: Self | None = None

    def __new__(cls, *args: Any, **kwargs: Any) -> Self:  # noqa: ARG003
        """Create a new template environment."""
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, searchpath: list[Path] | None = None) -> None:
        """Initialize the template environment."""
        if not searchpath:
            searchpath = []

        # Always include the directory containing the built-in templates
        searchpath.append(Path(__file__).parent)

        super().__init__(
            loader=_EnhancedFileSystemLoader(searchpath=searchpath),
            trim_blocks=True,
            lstrip_blocks=True,
            undefined=StrictUndefined,  # to throw exception on any undefined variable within template
        )

        self.globals.update(
            version=VERSION,  # version should be available in all templates
            zip=zip,  # add zip function
        )

__init__(searchpath=None)

Initialize the template environment.

Source code in src/corsair/_templates/env.py
def __init__(self, searchpath: list[Path] | None = None) -> None:
    """Initialize the template environment."""
    if not searchpath:
        searchpath = []

    # Always include the directory containing the built-in templates
    searchpath.append(Path(__file__).parent)

    super().__init__(
        loader=_EnhancedFileSystemLoader(searchpath=searchpath),
        trim_blocks=True,
        lstrip_blocks=True,
        undefined=StrictUndefined,  # to throw exception on any undefined variable within template
    )

    self.globals.update(
        version=VERSION,  # version should be available in all templates
        zip=zip,  # add zip function
    )

__new__(*args, **kwargs)

Create a new template environment.

Source code in src/corsair/_templates/env.py
def __new__(cls, *args: Any, **kwargs: Any) -> Self:  # noqa: ARG003
    """Create a new template environment."""
    if cls._instance is None:
        cls._instance = super().__new__(cls)
    return cls._instance

VerilogGenerator

Bases: Generator

Verilog file generator for a register map.

Source code in src/corsair/_generators/verilog.py
class VerilogGenerator(Generator):
    """Verilog file generator for a register map."""

    class Config(GeneratorConfig):
        """Configuration for the Verilog generator."""

        kind: Literal["verilog"] = "verilog"
        """Generator kind discriminator."""

        reset_style: ResetStyle = ResetStyle.ASYNC_NEG
        """Flip-flop reset style."""

        @property
        def generator_cls(self) -> type[Generator]:
            """Related generator class."""
            return VerilogGenerator

        def get_kind(self) -> str:
            """Get the kind of the generator."""
            return self.kind

    @classmethod
    def get_config_cls(cls) -> type[GeneratorConfig]:
        """Get the configuration class for the generator."""
        return cls.Config

    def _generate(self) -> TypeGenerator[Path, None, None]:
        """Generate all the outputs."""
        raise NotImplementedError

Config

Bases: GeneratorConfig

Configuration for the Verilog generator.

Source code in src/corsair/_generators/verilog.py
class Config(GeneratorConfig):
    """Configuration for the Verilog generator."""

    kind: Literal["verilog"] = "verilog"
    """Generator kind discriminator."""

    reset_style: ResetStyle = ResetStyle.ASYNC_NEG
    """Flip-flop reset style."""

    @property
    def generator_cls(self) -> type[Generator]:
        """Related generator class."""
        return VerilogGenerator

    def get_kind(self) -> str:
        """Get the kind of the generator."""
        return self.kind

generator_cls property

Related generator class.

kind = 'verilog' class-attribute instance-attribute

Generator kind discriminator.

reset_style = ResetStyle.ASYNC_NEG class-attribute instance-attribute

Flip-flop reset style.

get_kind()

Get the kind of the generator.

Source code in src/corsair/_generators/verilog.py
def get_kind(self) -> str:
    """Get the kind of the generator."""
    return self.kind

get_config_cls() classmethod

Get the configuration class for the generator.

Source code in src/corsair/_generators/verilog.py
@classmethod
def get_config_cls(cls) -> type[GeneratorConfig]:
    """Get the configuration class for the generator."""
    return cls.Config

VhdlGenerator

Bases: Generator

VHDL file generator for a register map.

Source code in src/corsair/_generators/vhdl.py
class VhdlGenerator(Generator):
    """VHDL file generator for a register map."""

    class Config(GeneratorConfig):
        """Configuration for the VHDL generator."""

        kind: Literal["vhdl"] = "vhdl"
        """Generator kind discriminator."""

        reset_style: ResetStyle = ResetStyle.ASYNC_NEG
        """Flip-flop reset style."""

        @property
        def generator_cls(self) -> type[Generator]:
            """Related generator class."""
            return VhdlGenerator

        def get_kind(self) -> str:
            """Get the kind of the generator."""
            return self.kind

    @classmethod
    def get_config_cls(cls) -> type[GeneratorConfig]:
        """Get the configuration class for the generator."""
        return cls.Config

    def _generate(self) -> TypeGenerator[Path, None, None]:
        """Generate all the outputs."""
        raise NotImplementedError

Config

Bases: GeneratorConfig

Configuration for the VHDL generator.

Source code in src/corsair/_generators/vhdl.py
class Config(GeneratorConfig):
    """Configuration for the VHDL generator."""

    kind: Literal["vhdl"] = "vhdl"
    """Generator kind discriminator."""

    reset_style: ResetStyle = ResetStyle.ASYNC_NEG
    """Flip-flop reset style."""

    @property
    def generator_cls(self) -> type[Generator]:
        """Related generator class."""
        return VhdlGenerator

    def get_kind(self) -> str:
        """Get the kind of the generator."""
        return self.kind

generator_cls property

Related generator class.

kind = 'vhdl' class-attribute instance-attribute

Generator kind discriminator.

reset_style = ResetStyle.ASYNC_NEG class-attribute instance-attribute

Flip-flop reset style.

get_kind()

Get the kind of the generator.

Source code in src/corsair/_generators/vhdl.py
def get_kind(self) -> str:
    """Get the kind of the generator."""
    return self.kind

get_config_cls() classmethod

Get the configuration class for the generator.

Source code in src/corsair/_generators/vhdl.py
@classmethod
def get_config_cls(cls) -> type[GeneratorConfig]:
    """Get the configuration class for the generator."""
    return cls.Config

WaveDromGenerator

Bases: Generator

WaveDrom bit field images generator for a register map.

Source code in src/corsair/_generators/wavedrom.py
class WaveDromGenerator(Generator):
    """WaveDrom bit field images generator for a register map."""

    class Config(GeneratorConfig):
        """Configuration for the WaveDrom generator."""

        kind: Literal["wavedrom"] = "wavedrom"
        """Generator kind discriminator."""

        vspace: int = PydanticField(default=80, ge=20)
        """Vertical space between lanes."""

        hspace: int = PydanticField(default=800, ge=40)
        """Horizontal space between lanes."""

        lanes: int = PydanticField(default=1, ge=1)
        """Number of lanes."""

        bits: int = PydanticField(default=32, ge=4)
        """Overall bit width."""

        hflip: bool = False
        """Horizontal flip."""

        vflip: bool = False
        """Vertical flip."""

        fontsize: int = PydanticField(default=14, ge=6)
        """Font size."""

        fontfamily: str = "sans-serif"
        """Font family."""

        fontweight: str = "normal"
        """Font weight."""

        use_bits_from_map: bool = True
        """Use the bit width from the register map. When True, `bits` is ignored."""

        dump_json: bool = False
        """Dump the JSON wavedrom descriptions for every register."""

        render_svg: bool = True
        """Render the SVG images."""

        @property
        def generator_cls(self) -> type[Generator]:
            """Related generator class."""
            return WaveDromGenerator

        def get_kind(self) -> str:
            """Get the kind of the generator."""
            return self.kind

    @classmethod
    def get_config_cls(cls) -> type[GeneratorConfig]:
        """Get the configuration class for the generator."""
        return cls.Config

    def _generate(self) -> TypeGenerator[Path, None, None]:
        """Generate all the outputs."""
        assert isinstance(self.config, self.Config)  # noqa: S101, to help type checker

        # Prepare the config for the WaveDrom library
        config_dict = {
            "vspace": self.config.vspace,
            "hspace": self.config.hspace,
            "lanes": self.config.lanes,
            "bits": self.register_map.register_width if self.config.use_bits_from_map else self.config.bits,
            "hflip": self.config.hflip,
            "vflip": self.config.vflip,
            "fontsize": self.config.fontsize,
            "fontfamily": self.config.fontfamily,
            "fontweight": self.config.fontweight,
        }

        for reg in self.register_map.registers:
            file_name = f"{self.register_map.name}_{reg.name}"

            # Prepare the fields
            fields = []
            for field in reg.fields_with_reserved:
                if field.name.startswith("_reserved"):
                    fields.append({"bits": field.width})
                else:
                    fields.append(
                        {
                            "name": field.name.upper(),
                            "bits": field.width,
                            "attr": field.access.value.upper(),
                        }
                    )

            # Save the JSON description
            if self.config.dump_json:
                json_file = Path(f"{file_name}.json")
                with json_file.open("w") as f:
                    json.dump({"reg": fields, "config": config_dict}, f)
                yield json_file

            # Render and save the SVG image
            try:
                from wavedrom.bitfield import BitField as WaveDromField
                from wavedrom.bitfield import Options as WaveDromFieldOptions

                svg_file = Path(f"{file_name}.svg")
                WaveDromField().render(fields, WaveDromFieldOptions(**config_dict)).saveas(svg_file)
                yield svg_file
            except ImportError as e:
                raise ImportError(
                    "wavedrom is not installed. "
                    "Try installing it with `pip install corsair[wavedrom]` or `pip install corsair[full]`."
                ) from e

Config

Bases: GeneratorConfig

Configuration for the WaveDrom generator.

Source code in src/corsair/_generators/wavedrom.py
class Config(GeneratorConfig):
    """Configuration for the WaveDrom generator."""

    kind: Literal["wavedrom"] = "wavedrom"
    """Generator kind discriminator."""

    vspace: int = PydanticField(default=80, ge=20)
    """Vertical space between lanes."""

    hspace: int = PydanticField(default=800, ge=40)
    """Horizontal space between lanes."""

    lanes: int = PydanticField(default=1, ge=1)
    """Number of lanes."""

    bits: int = PydanticField(default=32, ge=4)
    """Overall bit width."""

    hflip: bool = False
    """Horizontal flip."""

    vflip: bool = False
    """Vertical flip."""

    fontsize: int = PydanticField(default=14, ge=6)
    """Font size."""

    fontfamily: str = "sans-serif"
    """Font family."""

    fontweight: str = "normal"
    """Font weight."""

    use_bits_from_map: bool = True
    """Use the bit width from the register map. When True, `bits` is ignored."""

    dump_json: bool = False
    """Dump the JSON wavedrom descriptions for every register."""

    render_svg: bool = True
    """Render the SVG images."""

    @property
    def generator_cls(self) -> type[Generator]:
        """Related generator class."""
        return WaveDromGenerator

    def get_kind(self) -> str:
        """Get the kind of the generator."""
        return self.kind

bits = PydanticField(default=32, ge=4) class-attribute instance-attribute

Overall bit width.

dump_json = False class-attribute instance-attribute

Dump the JSON wavedrom descriptions for every register.

fontfamily = 'sans-serif' class-attribute instance-attribute

Font family.

fontsize = PydanticField(default=14, ge=6) class-attribute instance-attribute

Font size.

fontweight = 'normal' class-attribute instance-attribute

Font weight.

generator_cls property

Related generator class.

hflip = False class-attribute instance-attribute

Horizontal flip.

hspace = PydanticField(default=800, ge=40) class-attribute instance-attribute

Horizontal space between lanes.

kind = 'wavedrom' class-attribute instance-attribute

Generator kind discriminator.

lanes = PydanticField(default=1, ge=1) class-attribute instance-attribute

Number of lanes.

render_svg = True class-attribute instance-attribute

Render the SVG images.

use_bits_from_map = True class-attribute instance-attribute

Use the bit width from the register map. When True, bits is ignored.

vflip = False class-attribute instance-attribute

Vertical flip.

vspace = PydanticField(default=80, ge=20) class-attribute instance-attribute

Vertical space between lanes.

get_kind()

Get the kind of the generator.

Source code in src/corsair/_generators/wavedrom.py
def get_kind(self) -> str:
    """Get the kind of the generator."""
    return self.kind

get_config_cls() classmethod

Get the configuration class for the generator.

Source code in src/corsair/_generators/wavedrom.py
@classmethod
def get_config_cls(cls) -> type[GeneratorConfig]:
    """Get the configuration class for the generator."""
    return cls.Config

convert_schema_loc_to_path_loc(loc, data)

Convert pydantic schema location to path location based on item names in data.

Source code in src/corsair/_model.py
def convert_schema_loc_to_path_loc(  # noqa: C901, PLR0912, PLR0915
    loc: tuple[str | int, ...], data: Mapping[str, Any]
) -> tuple[str, ...]:
    """Convert pydantic schema location to path location based on item names in data."""

    def get_name(obj: Any) -> str:
        """Safely get the 'name' attribute from a mapping."""
        if isinstance(obj, collections.abc.Mapping):
            name = obj.get("name")
            # Ensure name is a non-empty string
            if isinstance(name, str) and name:
                return name
        return "<unknown>"

    path_elements: list[str] = []
    current_data: Any = data
    loc_list = list(loc)
    loc_idx = 0
    known_containers = {"items", "fields", "members"}
    # Tags associated with items within containers (e.g., items.0.register)
    known_tags = {"register", "map", "memory"}

    # Start with the top-level item's name
    path_elements.append(get_name(current_data))

    while loc_idx < len(loc_list):
        segment = loc_list[loc_idx]

        try:
            if isinstance(segment, str) and segment in known_containers:
                list_key = segment
                # Expect an integer index next
                if loc_idx + 1 < len(loc_list) and isinstance(loc_list[loc_idx + 1], int):
                    item_index = loc_list[loc_idx + 1]
                    loc_idx += 2  # Consume list_key and item_index
                    assert isinstance(item_index, int)  # noqa: S101 # Help type checker

                    # --- Access Data ---
                    if not isinstance(current_data, collections.abc.Mapping):
                        raise TypeError(f"Expected a mapping to access key '{list_key}', got {type(current_data)}")
                    container_list = current_data[list_key]  # Can raise KeyError
                    # Ensure it's a sequence (list/tuple) but not a string, and index is valid
                    if not isinstance(container_list, collections.abc.Sequence) or isinstance(container_list, str):
                        raise TypeError(f"Expected a sequence for key '{list_key}', got {type(container_list)}")
                    if not 0 <= item_index < len(container_list):
                        raise IndexError(f"Index {item_index} out of bounds for key '{list_key}'")
                    current_data = container_list[item_index]  # Access the item
                    # --- End Access ---

                    path_elements.append(get_name(current_data))  # Add item's name

                    # Check for and skip an optional discriminator tag immediately following the index
                    if (
                        loc_idx < len(loc_list)
                        and isinstance(loc_list[loc_idx], str)
                        and loc_list[loc_idx] in known_tags
                    ):
                        loc_idx += 1
                else:
                    # Error in loc structure (expected index after container key)
                    raise ValueError(  # type: ignore[unreachable]
                        f"Invalid loc: Expected integer index after '{list_key}', got {loc_list[loc_idx + 1]!r}"
                    )

            elif isinstance(segment, str) and segment == "enum":
                # Handle 'enum' as a direct dictionary key access
                enum_key = segment
                loc_idx += 1

                # --- Access Data ---
                if not isinstance(current_data, collections.abc.Mapping):
                    raise TypeError(f"Expected a mapping to access key '{enum_key}', got {type(current_data)}")
                current_data = current_data[enum_key]  # Can raise KeyError
                # --- End Access ---

                path_elements.append(get_name(current_data))  # Add enum's name

            elif isinstance(segment, str) and segment in known_tags:
                # Skip discriminator tags if encountered directly (should normally follow an index)
                loc_idx += 1

            elif isinstance(segment, int):
                # Error in loc structure (unexpected index without preceding container key)
                raise ValueError(f"Invalid loc: Unexpected integer index '{segment}'")  # type: ignore[unreachable]
            else:
                # Assume it's the final attribute name or an object identifier where validation failed
                # Append this segment and all subsequent segments as strings, then finish
                path_elements.extend(map(str, loc_list[loc_idx:]))
                loc_idx = len(loc_list)  # Ensure loop terminates

        except (KeyError, IndexError, TypeError, ValueError):
            # Data structure mismatch, invalid loc, or access error encountered.
            # Append the segment that caused the error (if not already processed by extend above)
            # and all subsequent segments as strings.
            # The 'extend' in the 'else' block handles the case where the loop terminates normally
            # on an attribute. This handles unexpected termination due to errors.
            path_elements.extend(map(str, loc_list[loc_idx:]))
            break  # Stop processing

    return tuple(path_elements)

stringify_model_errors(e, data)

Stringify pydantic validation errors.

Source code in src/corsair/_model.py
def stringify_model_errors(e: ValidationError, data: dict) -> list[str]:
    """Stringify pydantic validation errors."""
    errors: list[str] = []

    for error in e.errors():
        path_loc = ".".join(convert_schema_loc_to_path_loc(error["loc"], data))
        schema_loc = ".".join(map(str, error["loc"]))

        err_str = f"{error['msg']} [type={error['type']}, path={path_loc}, schema={schema_loc}]"
        errors.append(err_str)

    return errors