From d16e70f963700927f25d89d719e3226a9aa2f2ba Mon Sep 17 00:00:00 2001 From: esynr3z Date: Thu, 31 Oct 2024 22:24:31 +0300 Subject: [PATCH] feat: implement HwMode class to represent bitfield hardware modes --- corsair/__init__.py | 3 + corsair/core/__init__.py | 7 ++ corsair/core/hwmode.py | 209 ++++++++++++++++++++++++++++++++++++++ poetry.lock | 12 +-- pyproject.toml | 3 + tests/core/__init__.py | 1 + tests/core/test_hwmode.py | 171 +++++++++++++++++++++++++++++++ 7 files changed, 400 insertions(+), 6 deletions(-) create mode 100644 corsair/core/__init__.py create mode 100644 corsair/core/hwmode.py create mode 100644 tests/core/__init__.py create mode 100644 tests/core/test_hwmode.py diff --git a/corsair/__init__.py b/corsair/__init__.py index 121b003..0dcc1d9 100755 --- a/corsair/__init__.py +++ b/corsair/__init__.py @@ -11,6 +11,7 @@ __description__ = "Control and status register (CSR) map generator for HDL projects." +from .core import HwMode from .input import ( AnyTarget, BaseTarget, @@ -46,4 +47,6 @@ "ForceNameCase", "RegisterReset", "GlobalConfig", + # core + "HwMode", ) diff --git a/corsair/core/__init__.py b/corsair/core/__init__.py new file mode 100644 index 0000000..563094a --- /dev/null +++ b/corsair/core/__init__.py @@ -0,0 +1,7 @@ +"""Internal representation of a register map.""" + +from __future__ import annotations + +from .hwmode import HwMode + +__all__ = ("HwMode",) diff --git a/corsair/core/hwmode.py b/corsair/core/hwmode.py new file mode 100644 index 0000000..7cca63d --- /dev/null +++ b/corsair/core/hwmode.py @@ -0,0 +1,209 @@ +"""Enumeration for hardare modes of a bitfield.""" + +from __future__ import annotations + +from enum import Flag +from typing import TYPE_CHECKING, Iterable, Iterator + +if TYPE_CHECKING: + from enum import Enum + + from typing_extensions import Self + + +class HwMode(str, Flag): + """Hardware mode for a bitfield. + + 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: + """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 delaration 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 __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) diff --git a/poetry.lock b/poetry.lock index e2b0578..54397a4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -549,13 +549,13 @@ wcwidth = "*" [[package]] name = "pyright" -version = "1.1.386" +version = "1.1.387" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.386-py3-none-any.whl", hash = "sha256:7071ac495593b2258ccdbbf495f1a5c0e5f27951f6b429bed4e8b296eb5cd21d"}, - {file = "pyright-1.1.386.tar.gz", hash = "sha256:8e9975e34948ba5f8e07792a9c9d2bdceb2c6c0b61742b068d2229ca2bc4a9d9"}, + {file = "pyright-1.1.387-py3-none-any.whl", hash = "sha256:6a1f495a261a72e12ad17e20d1ae3df4511223c773b19407cfa006229b1b08a5"}, + {file = "pyright-1.1.387.tar.gz", hash = "sha256:577de60224f7fe36505d5b181231e3a395d427b7873be0bbcaa962a29ea93a60"}, ] [package.dependencies] @@ -858,13 +858,13 @@ files = [ [[package]] name = "virtualenv" -version = "20.27.0" +version = "20.27.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" files = [ - {file = "virtualenv-20.27.0-py3-none-any.whl", hash = "sha256:44a72c29cceb0ee08f300b314848c86e57bf8d1f13107a5e671fb9274138d655"}, - {file = "virtualenv-20.27.0.tar.gz", hash = "sha256:2ca56a68ed615b8fe4326d11a0dca5dfbe8fd68510fb6c6349163bed3c15f2b2"}, + {file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"}, + {file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"}, ] [package.dependencies] diff --git a/pyproject.toml b/pyproject.toml index c3f2141..fc46d86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,7 +103,10 @@ ignore = [ "TD003", # missing-todo-link "FIX", # flake8-fixme "EM101", # raw-string-in-exception + "EM102", # f-string-in-exception "FBT", # flake8-boolean-trap + "TRY003", # raise-vanilla-args + "E741", # ambiguous-variable-name ] [tool.ruff.lint.per-file-ignores] diff --git a/tests/core/__init__.py b/tests/core/__init__.py new file mode 100644 index 0000000..2f71c40 --- /dev/null +++ b/tests/core/__init__.py @@ -0,0 +1 @@ +"""Tests for register map internal representation.""" diff --git a/tests/core/test_hwmode.py b/tests/core/test_hwmode.py new file mode 100644 index 0000000..5e09932 --- /dev/null +++ b/tests/core/test_hwmode.py @@ -0,0 +1,171 @@ +"""Tests hardware mode of a bitfield.""" + +from __future__ import annotations + +import pytest +from pydantic import BaseModel + +from corsair import HwMode + + +def test_aliases() -> None: + """Test for aliases.""" + assert HwMode.INPUT == HwMode.I == HwMode("i") == HwMode("I") + assert HwMode.OUTPUT == HwMode.O == HwMode("o") == HwMode("O") + assert HwMode.CLEAR == HwMode.C == HwMode("c") == HwMode("C") + assert HwMode.SET == HwMode.S == HwMode("s") == HwMode("S") + assert HwMode.ENABLE == HwMode.E == HwMode("e") == HwMode("E") + assert HwMode.LOCK == HwMode.L == HwMode("l") == HwMode("L") + assert HwMode.ACCESS == HwMode.A == HwMode("a") == HwMode("A") + assert HwMode.QUEUE == HwMode.Q == HwMode("q") == HwMode("Q") + assert HwMode.FIXED == HwMode.F == HwMode("f") == HwMode("F") + assert HwMode.NA == HwMode.N == HwMode("n") == HwMode("N") + + +def test_single_item_operations() -> None: + """Test operations over single item.""" + mode = HwMode.I + assert repr(mode) == "" + assert str(mode) == "i" + assert set(mode) == set(HwMode.I) + assert mode == HwMode.I + assert mode != HwMode.O + assert HwMode.I in mode + assert mode <= HwMode.I + assert mode >= HwMode.I + assert not mode < HwMode.I + assert not mode > HwMode.I + assert not mode <= HwMode.O + assert not mode >= HwMode.O + assert not mode < HwMode.O + assert not mode > HwMode.O + + +def test_comb_item_operations() -> None: + """Test operations over item as a combination of flags.""" + mode = HwMode.I | HwMode.O | HwMode.E + assert repr(mode) == "" + assert str(mode) == "i|o|e" + assert set(mode) == {HwMode.I, HwMode.O, HwMode.E} + assert HwMode.I in mode + assert HwMode.O in mode + assert HwMode.L not in mode + assert mode != HwMode.I + assert mode != HwMode.O + assert mode != HwMode.E + + subset_mode = HwMode.I | HwMode.O + assert not subset_mode > mode + assert not subset_mode >= mode + assert subset_mode < mode + assert subset_mode <= mode + + superset_mode = HwMode.I | HwMode.O | HwMode.E | HwMode.L + assert superset_mode > mode + assert superset_mode >= mode + assert not superset_mode < mode + assert not superset_mode <= mode + + +def test_creation_from_string() -> None: + """Test creation from literals.""" + mode_io = HwMode("io") + assert mode_io == (HwMode.I | HwMode.O) + mode_io = HwMode("ioioio") + assert mode_io == (HwMode.I | HwMode.O) + mode_io = HwMode("i|o|e") + assert mode_io == (HwMode.I | HwMode.O | HwMode.E) + mode_cs = HwMode("cS") + assert mode_cs == (HwMode.C | HwMode.S) + mode_ei = HwMode("EI") + assert mode_ei == (HwMode.E | HwMode.I) + mode_n = HwMode("") + assert mode_n == HwMode.N + mode_f = HwMode("f") + assert mode_f == HwMode.F + mode_q = HwMode("q") + assert mode_q == HwMode.Q + + +def test_invalid_string_creation() -> None: + """Test for unknown flags.""" + with pytest.raises(ValueError, match="Unknown hardware mode"): + HwMode("x") + with pytest.raises(ValueError, match="Unknown hardware mode"): + HwMode("xyz") + with pytest.raises(ValueError, match="Unknown hardware mode"): + HwMode("z|i") + + +def test_single_str_item_operations() -> None: + """Test operations over a single item represented as a string.""" + mode = HwMode.I + assert "i" in mode + assert mode <= "i" + assert mode >= "i" + assert not mode < "i" + assert not mode > "i" + assert not mode <= "o" + assert not mode >= "o" + assert not mode < "o" + assert not mode > "o" + + +def test_comb_str_item_operations() -> None: + """Test operations over a string item as a combination of flags.""" + mode = HwMode.I | HwMode.O | HwMode.E + assert "i" in mode + assert "o" in mode + assert "io" in mode + assert "ioe" in mode + assert "l" not in mode + assert "ioel" not in mode + + subset = "io" + assert not subset > mode + assert not subset >= mode + assert subset < mode + assert subset <= mode + + superset = "ioel" + assert superset > mode + assert superset >= mode + assert not superset < mode + assert not superset <= mode + + +def test_na_flag_from_str() -> None: + """Test for NA flag.""" + mode = HwMode("") + assert str(mode) == "n" + assert mode == HwMode.N + + +def test_str_conversion() -> None: + """Test flags to string conversion.""" + mode = HwMode("iocl") + assert str(mode) == "i|o|c|l" == mode.value + mode = HwMode("oicl") + assert str(mode) == "i|o|c|l" + mode = HwMode("o|i|l|c") + assert str(mode) == "i|o|c|l" + + +def test_pydantic_validate() -> None: + """Test of validation with pydantic model.""" + + class Wrapper(BaseModel): + mode: HwMode + + model = Wrapper(mode=HwMode("i|o")) + assert model.mode == HwMode("io") == (HwMode.I | HwMode.O) + + +def test_pydantic_dump_json() -> None: + """Test of dump from pydantic model.""" + + class Wrapper(BaseModel): + mode: HwMode + + dump = Wrapper(mode=HwMode("io")).model_dump_json() + assert dump == '{"mode":"i|o"}'