corel_mmio 8.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. corel_mmio-8.0.0/LICENSE +21 -0
  2. corel_mmio-8.0.0/PKG-INFO +81 -0
  3. corel_mmio-8.0.0/README.md +60 -0
  4. corel_mmio-8.0.0/pyproject.toml +111 -0
  5. corel_mmio-8.0.0/setup.cfg +4 -0
  6. corel_mmio-8.0.0/src/corel_mmio/__init__.py +5 -0
  7. corel_mmio-8.0.0/src/corel_mmio/bytes.py +108 -0
  8. corel_mmio-8.0.0/src/corel_mmio/cache.py +54 -0
  9. corel_mmio-8.0.0/src/corel_mmio/device.py +102 -0
  10. corel_mmio-8.0.0/src/corel_mmio/doc.py +83 -0
  11. corel_mmio-8.0.0/src/corel_mmio/eval.py +51 -0
  12. corel_mmio-8.0.0/src/corel_mmio/interface/__init__.py +1 -0
  13. corel_mmio-8.0.0/src/corel_mmio/interface/message.py +125 -0
  14. corel_mmio-8.0.0/src/corel_mmio/interface/mock.py +287 -0
  15. corel_mmio-8.0.0/src/corel_mmio/interface/protocol.py +316 -0
  16. corel_mmio-8.0.0/src/corel_mmio/mapping.py +148 -0
  17. corel_mmio-8.0.0/src/corel_mmio/memmap/__init__.py +3 -0
  18. corel_mmio-8.0.0/src/corel_mmio/memmap/array.py +265 -0
  19. corel_mmio-8.0.0/src/corel_mmio/memmap/block.py +274 -0
  20. corel_mmio-8.0.0/src/corel_mmio/memmap/field.py +430 -0
  21. corel_mmio-8.0.0/src/corel_mmio/memmap/file.py +264 -0
  22. corel_mmio-8.0.0/src/corel_mmio/memmap/memmap.py +677 -0
  23. corel_mmio-8.0.0/src/corel_mmio/memmap/mode.py +50 -0
  24. corel_mmio-8.0.0/src/corel_mmio/memmap/register.py +524 -0
  25. corel_mmio-8.0.0/src/corel_mmio/memmap/type.py +544 -0
  26. corel_mmio-8.0.0/src/corel_mmio/memmap/type_serialization.py +52 -0
  27. corel_mmio-8.0.0/src/corel_mmio/memmap/value.py +377 -0
  28. corel_mmio-8.0.0/src/corel_mmio/memory.py +43 -0
  29. corel_mmio-8.0.0/src/corel_mmio/py.typed +0 -0
  30. corel_mmio-8.0.0/src/corel_mmio/signal.py +68 -0
  31. corel_mmio-8.0.0/src/corel_mmio.egg-info/PKG-INFO +81 -0
  32. corel_mmio-8.0.0/src/corel_mmio.egg-info/SOURCES.txt +40 -0
  33. corel_mmio-8.0.0/src/corel_mmio.egg-info/dependency_links.txt +1 -0
  34. corel_mmio-8.0.0/src/corel_mmio.egg-info/requires.txt +10 -0
  35. corel_mmio-8.0.0/src/corel_mmio.egg-info/top_level.txt +1 -0
  36. corel_mmio-8.0.0/tests/test_bytes.py +62 -0
  37. corel_mmio-8.0.0/tests/test_device.py +106 -0
  38. corel_mmio-8.0.0/tests/test_eval.py +63 -0
  39. corel_mmio-8.0.0/tests/test_interface.py +143 -0
  40. corel_mmio-8.0.0/tests/test_mapping.py +129 -0
  41. corel_mmio-8.0.0/tests/test_memory.py +97 -0
  42. corel_mmio-8.0.0/tests/test_signal.py +65 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Deutsches Elektronen-Synchrotron DESY
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: corel_mmio
3
+ Version: 8.0.0
4
+ Summary: Common Memory-Mapped IO operations and in-memory representations.
5
+ Author-email: Jason Watson <jason.watson@desy.de>
6
+ License-Expression: MIT
7
+ Project-URL: repository, https://gitlab.desy.de/corel/corel-mmio
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: typing-extensions~=4.12
12
+ Provides-Extra: dev
13
+ Requires-Dist: ipython; extra == "dev"
14
+ Requires-Dist: pre-commit~=4.1; extra == "dev"
15
+ Requires-Dist: mypy~=1.15; extra == "dev"
16
+ Requires-Dist: pytest~=8.3; extra == "dev"
17
+ Requires-Dist: pytest-asyncio~=0.23; extra == "dev"
18
+ Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
19
+ Requires-Dist: coverage>=7.4.0; extra == "dev"
20
+ Dynamic: license-file
21
+
22
+ # <img align="right" src="misc/logo.png" height="200"></a>
23
+
24
+ # corel-mmio
25
+
26
+ [![pipeline status](https://gitlab.desy.de/corel/corel-mmio/badges/main/pipeline.svg)](https://gitlab.desy.de/corel/corel-mmio) [![coverage report](https://gitlab.desy.de/corel/corel-mmio/badges/main/coverage.svg)](https://gitlab.desy.de/corel/corel-mmio)
27
+
28
+ Common Memory-Mapped Input/Output (MMIO) operations and in-memory
29
+ representations, to aid in the development of packages which communicate with
30
+ hardware containing a memory-mapped address space.
31
+
32
+ This package is part of the [corel](https://gitlab.desy.de/corel) project ecosystem,
33
+ which is a collection of Python packages for the cooperative development of
34
+ software and firmware for memory mapped FPGAs, microcontrollers and other hardware.
35
+
36
+ * Supported Python versions: 3.10+
37
+ * Supported platforms: Linux, OSX
38
+ * Source: <https://gitlab.desy.de/corel/corel-mmio>
39
+ * Pypi: <https://pypi.org/project/corel-mmio>
40
+ * Documentation: TODO
41
+ * License: [MIT](LICENSE)
42
+
43
+
44
+ ## Features
45
+
46
+ * Unification of the interfaces involved in the interaction between software
47
+ and Memory-Mapped Input/Output (MMIO) hardware.
48
+ * Object-oriented description of a memory map address space, providing
49
+ register address access and validation and supporting multiple layers of
50
+ submaps.
51
+ * Convenient asynchronous read/write methods attached to the memory-mapped
52
+ registers via an abstract "interface" class defined through the structural
53
+ subtyping of Python `Protocol`.
54
+ * Supports the development of hardware drivers which can be reused in different
55
+ bench setups, where the underlying protocol (message format or communication
56
+ protocol) changes, but the memory map of the device (and therefore the
57
+ device operation) is common.
58
+
59
+
60
+ ## Acknowledgements
61
+
62
+ Concepts in corel-mmio have been inspired by the
63
+ [hdl_registers](https://hdl-registers.com/) package. However, corel-mmio
64
+ deliberately avoids a dependency on hdl_registers, so that it may be used in
65
+ software architectures which strive to minimise dependencies.
66
+
67
+
68
+ ## Installation
69
+
70
+ This package is installable via pip from PyPI:
71
+ ```
72
+ pip install corel-mmio
73
+ ```
74
+
75
+ This package is installable via pip directly from this repository
76
+ (via https or ssh), e.g.:
77
+ ```
78
+ pip install 'git+https://gitlab.desy.de/corel/corel-mmio.git'
79
+ ```
80
+ For developers, follow the usual `git clone` procedure and see the
81
+ [Makefile](Makefile) for development commands.
@@ -0,0 +1,60 @@
1
+ # <img align="right" src="misc/logo.png" height="200"></a>
2
+
3
+ # corel-mmio
4
+
5
+ [![pipeline status](https://gitlab.desy.de/corel/corel-mmio/badges/main/pipeline.svg)](https://gitlab.desy.de/corel/corel-mmio) [![coverage report](https://gitlab.desy.de/corel/corel-mmio/badges/main/coverage.svg)](https://gitlab.desy.de/corel/corel-mmio)
6
+
7
+ Common Memory-Mapped Input/Output (MMIO) operations and in-memory
8
+ representations, to aid in the development of packages which communicate with
9
+ hardware containing a memory-mapped address space.
10
+
11
+ This package is part of the [corel](https://gitlab.desy.de/corel) project ecosystem,
12
+ which is a collection of Python packages for the cooperative development of
13
+ software and firmware for memory mapped FPGAs, microcontrollers and other hardware.
14
+
15
+ * Supported Python versions: 3.10+
16
+ * Supported platforms: Linux, OSX
17
+ * Source: <https://gitlab.desy.de/corel/corel-mmio>
18
+ * Pypi: <https://pypi.org/project/corel-mmio>
19
+ * Documentation: TODO
20
+ * License: [MIT](LICENSE)
21
+
22
+
23
+ ## Features
24
+
25
+ * Unification of the interfaces involved in the interaction between software
26
+ and Memory-Mapped Input/Output (MMIO) hardware.
27
+ * Object-oriented description of a memory map address space, providing
28
+ register address access and validation and supporting multiple layers of
29
+ submaps.
30
+ * Convenient asynchronous read/write methods attached to the memory-mapped
31
+ registers via an abstract "interface" class defined through the structural
32
+ subtyping of Python `Protocol`.
33
+ * Supports the development of hardware drivers which can be reused in different
34
+ bench setups, where the underlying protocol (message format or communication
35
+ protocol) changes, but the memory map of the device (and therefore the
36
+ device operation) is common.
37
+
38
+
39
+ ## Acknowledgements
40
+
41
+ Concepts in corel-mmio have been inspired by the
42
+ [hdl_registers](https://hdl-registers.com/) package. However, corel-mmio
43
+ deliberately avoids a dependency on hdl_registers, so that it may be used in
44
+ software architectures which strive to minimise dependencies.
45
+
46
+
47
+ ## Installation
48
+
49
+ This package is installable via pip from PyPI:
50
+ ```
51
+ pip install corel-mmio
52
+ ```
53
+
54
+ This package is installable via pip directly from this repository
55
+ (via https or ssh), e.g.:
56
+ ```
57
+ pip install 'git+https://gitlab.desy.de/corel/corel-mmio.git'
58
+ ```
59
+ For developers, follow the usual `git clone` procedure and see the
60
+ [Makefile](Makefile) for development commands.
@@ -0,0 +1,111 @@
1
+ [build-system]
2
+ requires = [
3
+ "setuptools>=77",
4
+ "setuptools_scm[toml]>=8",
5
+ ]
6
+ build-backend = "setuptools.build_meta"
7
+
8
+ [project]
9
+ name = "corel_mmio"
10
+ version = "8.0.0"
11
+ description = "Common Memory-Mapped IO operations and in-memory representations."
12
+ readme = "README.md"
13
+ requires-python = ">=3.10"
14
+ license = "MIT"
15
+ license-files = ["LICENSE"]
16
+ authors = [
17
+ {name = "Jason Watson", email = "jason.watson@desy.de"},
18
+ ]
19
+ dependencies = [
20
+ "typing-extensions~=4.12", # TODO: replace for Python 3.12
21
+ ]
22
+
23
+ [project.optional-dependencies]
24
+ dev = [
25
+ "ipython",
26
+ "pre-commit~=4.1",
27
+ "mypy~=1.15",
28
+ "pytest~=8.3",
29
+ "pytest-asyncio~=0.23",
30
+ "pytest-cov>=4.1.0",
31
+ "coverage>=7.4.0",
32
+ ]
33
+
34
+ [project.urls]
35
+ repository = "https://gitlab.desy.de/corel/corel-mmio"
36
+
37
+ [tool.setuptools.packages.find]
38
+ where = ["src"]
39
+
40
+ [tool.uv]
41
+ required-version = ">=0.6.0"
42
+ default-groups = []
43
+
44
+ [tool.pytest.ini_options]
45
+ minversion = "8"
46
+ xfail_strict = true
47
+ addopts = ["-ra", "--strict-config", "--strict-markers"]
48
+ testpaths = ["tests"]
49
+ asyncio_mode = "strict"
50
+ asyncio_default_fixture_loop_scope = "function"
51
+ log_cli_level = "DEBUG"
52
+
53
+ [tool.ruff]
54
+ line-length = 90
55
+ target-version = "py310"
56
+
57
+ [tool.ruff.lint]
58
+ extend-select = [
59
+ "I", # isort
60
+ "N", # pep8 naming
61
+ "D", # pydocstyle
62
+ "NPY", # numpy
63
+ "PT", # pytest
64
+ "UP", # pyupgrade
65
+ "COM", # flake8-commas
66
+ "ISC", # implicit string concat rules
67
+ "ICN", # import name conventions
68
+ "G", # logging
69
+ ]
70
+ ignore = [
71
+ "COM812", # incompatible with ruff format
72
+ "ISC001", # incompatible with ruff format
73
+ "G004", # premature optimization, f-strings are more convenient
74
+ "D401", # don't force imperative mood
75
+ "D105", # don't force docstring for magic methods
76
+ "PT013", # useful for pytest.approx
77
+ ]
78
+
79
+ [tool.ruff.lint.pydocstyle]
80
+ convention = "numpy"
81
+ ignore-decorators = [ # Functions with these decorators don't have to have docstrings.
82
+ "typing.overload", # Default configuration
83
+ # The next four are all variations on override, so child classes don't have to repeat parent classes' docstrings.
84
+ "overrides.override",
85
+ "overrides.overrides",
86
+ "typing.override",
87
+ "typing_extensions.override",
88
+ ]
89
+
90
+ [tool.ruff.lint.per-file-ignores]
91
+ # no documentation linting for test files
92
+ "**/tests/**" = ["D"]
93
+ "**/tests_*.py" = ["D"]
94
+ "*test*.py" = ["D"]
95
+
96
+ [tool.ruff.format]
97
+ quote-style = "double"
98
+ indent-style = "space"
99
+ skip-magic-trailing-comma = false
100
+ line-ending = "auto"
101
+
102
+ [tool.isort]
103
+ profile = "black"
104
+ line_length = 90
105
+
106
+ [[tool.mypy.overrides]]
107
+ module = "tests.*"
108
+ allow_untyped_defs = true
109
+ allow_incomplete_defs = true
110
+ allow_untyped_calls = true
111
+ disable_error_code = ["var-annotated", "has-type", "abstract"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """Common Memory-Mapped IO operations and in-memory representations."""
2
+
3
+ import importlib.metadata
4
+
5
+ __version__ = importlib.metadata.version(__package__ or __name__)
@@ -0,0 +1,108 @@
1
+ """Tools related to bytes of the memory map and data."""
2
+
3
+ from collections.abc import Iterator
4
+ from typing import Literal
5
+
6
+ __all__ = [
7
+ "word_increments",
8
+ "pad_data",
9
+ "flip_byteorder",
10
+ "flip_integer_byteorder",
11
+ ]
12
+
13
+
14
+ def word_increments(
15
+ start_address: int,
16
+ byte_length: int,
17
+ bus_width: int,
18
+ auto_increment: bool,
19
+ ) -> Iterator[tuple[int, int]]:
20
+ """
21
+ Generator to increment over word addresses.
22
+
23
+ Parameters
24
+ ----------
25
+ start_address : int
26
+ Address in the memory map to start from.
27
+ byte_length : int
28
+ Total number of bytes to increment over.
29
+ bus_width : int
30
+ Byte width of the register bus of the memory map.
31
+ auto_increment : bool
32
+ Automatically increment the register address when writing/reading more
33
+ than the bus length (4 bytes).
34
+
35
+ Yields
36
+ ------
37
+ current_address: int
38
+ Address the generator is currently at.
39
+ remaining_word_bytes
40
+ Number of bytes left in the current word.
41
+ """
42
+ current_address = start_address
43
+ remaining_byte_length = byte_length
44
+ word_start_address = (start_address // bus_width) * bus_width
45
+ while remaining_byte_length > 0:
46
+ remaining_word_bytes = bus_width - (current_address % bus_width)
47
+ if remaining_word_bytes > remaining_byte_length:
48
+ remaining_word_bytes = remaining_byte_length
49
+
50
+ yield current_address, remaining_word_bytes
51
+
52
+ current_address += remaining_word_bytes
53
+ if not auto_increment:
54
+ current_address = word_start_address + (current_address % bus_width)
55
+ remaining_byte_length -= remaining_word_bytes
56
+
57
+
58
+ def pad_data(
59
+ data: bytes | bytearray,
60
+ padded_byte_length: int | None,
61
+ ) -> bytearray:
62
+ """
63
+ Pad the start of a bytearray to have the desired length.
64
+
65
+ Parameters
66
+ ----------
67
+ data : bytearray
68
+ Data to be padded.
69
+ padded_byte_length : int or None
70
+ Desired length to pad the data to.
71
+
72
+ Returns
73
+ -------
74
+ data_padded : bytearray
75
+ Padded data.
76
+ """
77
+ if padded_byte_length is None:
78
+ return bytearray(data)
79
+
80
+ byte_length = len(data)
81
+ if padded_byte_length < len(data):
82
+ raise ValueError(
83
+ f"Data (len: {byte_length}) is longer than requested "
84
+ f"byte_length: {padded_byte_length}"
85
+ )
86
+
87
+ data_padded = bytearray(padded_byte_length - len(data))
88
+ data_padded.extend(data)
89
+ return data_padded
90
+
91
+
92
+ def flip_byteorder(
93
+ data_bytes: bytes, from_: Literal["little", "big"], to_: Literal["little", "big"]
94
+ ) -> bytes:
95
+ """Reorder the bytes from one endianness into another."""
96
+ data_int = int.from_bytes(data_bytes, byteorder=from_)
97
+ return data_int.to_bytes(len(data_bytes), byteorder=to_)
98
+
99
+
100
+ def flip_integer_byteorder(
101
+ value: int,
102
+ n_bytes: int,
103
+ from_: Literal["little", "big"],
104
+ to_: Literal["little", "big"],
105
+ ) -> int:
106
+ """Flip the byteorder of the integer."""
107
+ value_bytes = value.to_bytes(n_bytes, byteorder=from_)
108
+ return int.from_bytes(value_bytes, byteorder=to_)
@@ -0,0 +1,54 @@
1
+ """Local cache of the hardware memory map register values."""
2
+
3
+ from collections import defaultdict
4
+ from typing import TYPE_CHECKING
5
+
6
+ from typing_extensions import TypeVar
7
+
8
+ from corel_mmio.mapping import SliceableMutableMapping
9
+
10
+ if TYPE_CHECKING:
11
+ from corel_mmio.memmap.memmap import MemoryMap
12
+ from corel_mmio.memmap.register import Register
13
+
14
+ TType = TypeVar("TType")
15
+
16
+
17
+ class Cache:
18
+ """
19
+ Local cache of the hardware memory map register values.
20
+
21
+ Useful for tracking the values written to write-only registers, so that
22
+ unintentional overwrites can be avoided when writing to fields which
23
+ share that same register.
24
+ """
25
+
26
+ def __init__(self, size: int):
27
+ self._size = size
28
+ self._bus = SliceableMutableMapping(size=size, mapping=defaultdict(lambda: 0))
29
+
30
+ def push_address(self, word_address: int, value: int) -> None:
31
+ """Update the cache with the value of a register at an address."""
32
+ self._bus[word_address] = value
33
+
34
+ def push_register(self, register: "Register", value: int) -> None:
35
+ """Update the cache with the value of a register."""
36
+ self.push_address(register.word_address, value)
37
+
38
+ def pull_address(self, word_address: int) -> int:
39
+ """Obtain the currently cached value of a register at an address."""
40
+ return self._bus[word_address]
41
+
42
+ def pull_register(self, register: "Register") -> int:
43
+ """Obtain the currently cached value of a register."""
44
+ return self.pull_address(register.word_address)
45
+
46
+ def pull_fields(self, register: "Register[TType]") -> dict[str, TType]:
47
+ """Obtain the currently cached values of the fields in a register."""
48
+ register_value = self.pull_register(register)
49
+ return register.extract_field_values(register_value)
50
+
51
+ def update_from_defaults(self, memmap: "MemoryMap") -> None:
52
+ """Update the cache with the default value of each register in the memory map."""
53
+ for register in memmap.registers:
54
+ self.push_register(register, register.default_value)
@@ -0,0 +1,102 @@
1
+ """Containers related to device addressing."""
2
+
3
+ from collections.abc import Iterable, Iterator
4
+ from enum import Flag
5
+ from functools import cache, reduce
6
+ from operator import or_ as _or_
7
+ from typing import TypeVar
8
+
9
+ from typing_extensions import Self # TODO: in Python 3.11 use typing.Self
10
+
11
+ __all__ = [
12
+ "DeviceFlag",
13
+ ]
14
+
15
+ T = TypeVar("T", bound="DeviceFlag")
16
+
17
+
18
+ class DeviceFlag(Flag):
19
+ """
20
+ Flag enum for indicating the sub-device involved in an operation.
21
+
22
+ Used when addressing multiple devices in a single operation, which is then
23
+ executed either sequentially or simultaneously (broadcast).
24
+
25
+ This class is intended as a baseclass to be inherited, to be defined for
26
+ the specific sub-devices of a project.
27
+ """
28
+
29
+ # noinspection PyTypeChecker
30
+ @property
31
+ def value(self) -> int:
32
+ """The value of the Enum member."""
33
+ return super().value
34
+
35
+ @property
36
+ def name(self) -> str:
37
+ """The name of the Enum member."""
38
+ class_name = self.__class__.__name__
39
+ return super().name or str(self).replace(f"{class_name}.", "")
40
+
41
+ @property
42
+ def solo(self) -> bool:
43
+ """True if the addressed device is solo."""
44
+ n = self.value
45
+ return n > 0 and (n & (n - 1)) == 0
46
+
47
+ @classmethod
48
+ @cache
49
+ def distinct(cls) -> list[Self]:
50
+ """Obtain a list of the distinct devices."""
51
+ # TODO: unnecessary in 3.11?
52
+ return [n for n in cls if n.solo]
53
+
54
+ @classmethod
55
+ @cache
56
+ def n_distinct(cls) -> int:
57
+ """Number of distinct devices."""
58
+ return len(cls.distinct())
59
+
60
+ @classmethod
61
+ def validate_bit_string(cls, string: str) -> None:
62
+ """Validate that a 32 digit bit string is valid for indicating the device."""
63
+ n = cls.n_distinct()
64
+ error_msg = f"Must be a bit string of at-most length {n}"
65
+
66
+ if len(string) > n:
67
+ raise ValueError(error_msg)
68
+
69
+ # Test that the string is a binary
70
+ try:
71
+ int(string, 2)
72
+ except ValueError:
73
+ raise ValueError(error_msg)
74
+
75
+ @classmethod
76
+ def from_bit_string(cls, string: str) -> Self:
77
+ """Create from a bit string notation."""
78
+ cls.validate_bit_string(string)
79
+ return cls(int(string, 2))
80
+
81
+ def to_bit_string(self) -> str:
82
+ """Convert to a bit string notation."""
83
+ n = self.n_distinct()
84
+ return f"{self.value:0{n}b}"
85
+
86
+ @classmethod
87
+ def join(cls, devices: Iterable[Self]) -> Self:
88
+ """Join multiple devices into a single flag."""
89
+ return cls(reduce(_or_, devices, cls(0)))
90
+
91
+ def split(self) -> Iterator[Self]:
92
+ """Split into solo devices."""
93
+ for device in self.distinct():
94
+ if device in self:
95
+ yield device
96
+
97
+ @classmethod
98
+ @cache
99
+ def permutations(cls) -> list[Self]:
100
+ """Obtain a list of all possible permutations of the devices."""
101
+ n = cls.n_distinct()
102
+ return [cls(v) for v in range(2**n)]
@@ -0,0 +1,83 @@
1
+ """Utils for documentation."""
2
+
3
+ from dataclasses import MISSING
4
+ from typing import (
5
+ TYPE_CHECKING,
6
+ Generic,
7
+ Literal,
8
+ TypeVar,
9
+ overload,
10
+ )
11
+
12
+ from typing_extensions import Self
13
+
14
+ if TYPE_CHECKING:
15
+ pass
16
+
17
+ Owner = TypeVar("Owner")
18
+ Value = TypeVar("Value")
19
+
20
+ ___all__ = [
21
+ "DocDescriptor",
22
+ ]
23
+
24
+
25
+ class DocDescriptor(Generic[Owner, Value]):
26
+ """Descriptor to add runtime-retrievable documentation to a dataclass field."""
27
+
28
+ # noinspection PyShadowingBuiltins
29
+ def __init__(
30
+ self,
31
+ *,
32
+ doc: str,
33
+ default: Value | Literal["MISSING"] = MISSING, # type: ignore[assignment]
34
+ ):
35
+ self._doc = doc
36
+ self._default = default
37
+
38
+ if self._doc:
39
+ self.__doc__ = doc
40
+
41
+ def __set_name__(self, owner: type[Owner], name: str) -> None:
42
+ self._private_name = "_" + name
43
+ self._public_name = name
44
+
45
+ @overload
46
+ def __get__(self, obj: None, obj_type: type[Owner] | None) -> Self: ...
47
+
48
+ @overload
49
+ def __get__(self, obj: Owner, obj_type: type[Owner] | None = None) -> Value: ...
50
+
51
+ def __get__(
52
+ self, obj: Owner | None, obj_type: type[Owner] | None = None
53
+ ) -> Value | Self:
54
+ # if obj is None:
55
+ # return self
56
+
57
+ if not hasattr(obj, self._private_name):
58
+ raise AttributeError(f"'{self._private_name[1:]}' has not been set")
59
+
60
+ value: Value = getattr(obj, self._private_name)
61
+ return value
62
+
63
+ def __set__(self, obj: Owner, value: Value | Self) -> None:
64
+ if value == self:
65
+ if self._default is not MISSING:
66
+ value = self._default # type: ignore[assignment]
67
+ else:
68
+ raise TypeError(
69
+ f"TypeError: {obj.__class__.__name__}.__init__() missing "
70
+ f"required positional argument: '{self._public_name}'"
71
+ )
72
+
73
+ setattr(obj, self._private_name, value)
74
+
75
+ @property
76
+ def doc(self) -> str:
77
+ """Get the documentation string for this descriptor."""
78
+ return self._doc
79
+
80
+ @property
81
+ def default(self) -> Value | Literal["MISSING"]:
82
+ """Get the default value for this descriptor."""
83
+ return self._default
@@ -0,0 +1,51 @@
1
+ """Safe evaluation of expressions."""
2
+
3
+ import ast
4
+ from typing import TypeVar
5
+
6
+ __all__ = [
7
+ "UnsupportedClassError",
8
+ "safe_eval_expr",
9
+ ]
10
+
11
+ _T = TypeVar("_T")
12
+
13
+
14
+ class UnsupportedClassError(ValueError):
15
+ """Raised when a class is not supported."""
16
+
17
+
18
+ def safe_eval_expr(expr: str, supported: dict[str, type[_T]]) -> _T:
19
+ """Safely evaluates an expression of the form ClassName(arg=value)."""
20
+ tree = ast.parse(expr, mode="eval")
21
+
22
+ # Ensure it's a function call
23
+ if not isinstance(tree.body, ast.Call):
24
+ raise ValueError("Expression must be a function call")
25
+
26
+ # Ensure it's a valid function name
27
+ if not isinstance(tree.body.func, ast.Name):
28
+ raise ValueError("Invalid function call")
29
+
30
+ class_name = tree.body.func.id
31
+
32
+ # Ensure it's a supported class
33
+ if class_name not in supported:
34
+ raise UnsupportedClassError(f"Class {class_name} is not allowed")
35
+
36
+ # Extract positional arguments
37
+ args = []
38
+ for arg in tree.body.args:
39
+ if not isinstance(arg, ast.Constant): # Only allow simple constants
40
+ raise ValueError("Only constant positional arguments are allowed")
41
+ args.append(arg.value)
42
+
43
+ # Extract keyword arguments
44
+ kwargs = {}
45
+ for kw in tree.body.keywords:
46
+ if not isinstance(kw.value, ast.Constant) or not isinstance(kw.arg, str):
47
+ raise ValueError("Only constant keyword arguments are allowed")
48
+ kwargs[kw.arg] = kw.value.value
49
+
50
+ # Instantiate the class safely
51
+ return supported[class_name](*args, **kwargs)
@@ -0,0 +1 @@
1
+ """Interface definitions between the PC software and remote memory-mapped hardware."""