Open clint-lawrence opened 5 months ago
@daniel-montanari?
S = TypeVar("S")
class VirtualMux(Generic[S]):
def __init__(self):
self._signal_map: dict[Signal, set[Pin]] = {}
def __call__(self, signal: S, trigger_update: bool=False) -> None:
self.multiplex(signal, trigger_update)
def multiplex(self, signal: S, trigger_update: bool=False) -> None:
print(self._signal_map[signal])
MuxOneSigDef = Union[
Annotated[Literal["sig1"], ("x0",)],
Annotated[Literal["sig2"], ("x1",)],
Annotated[Literal["sig3"], ("x0", "x1")],
]
class MuxOne(VirtualMux[MuxOneSigDef]):
pass
Possible way to extend this idea to "Tree" mux definitions.
Unravelling this in code to build the signal map would be a bit of a mess, but I think it captures all the required info.
There is one weird thing. I'm stealing the ""
literal as a place holder for "check the metadata". It is always a valid signal, but defined to mean "all pins off". So we can use it here as a place holder for either a mux input that isn't used, or as a mux input that "connects" to another mux. Distinguishing between those two is a matter of context and the second metadata value (i.e. the tuple of (MuxB, MuxC)
in the definition of MuxA
below).
For each instance of Annotated
there are two or three values:
MuxC = Annotated[Literal["c0", "c1", "c2"], ("x4", "x5")]
MuxB = Annotated[Literal["b0", "b1", "", "b3"], ("x2", "x3"), (None,)] # None corresponds to an unused signal, marked by ""
MuxA = Annotated[Literal["a0", "", "a2", ""], ("x0", "x1"), (MuxB, MuxC)] # MuxB & MuxC connect to the entries marked by ""
MuxTreeDef = Union[MuxA, MuxB, MuxC]
class TreeDefinedMux[MuxTreeDef]:
pass
Need to check if this has an impact on that tree definition idea, because it is relying on ordering https://docs.python.org/3/library/typing.html#typing.get_args
If X is a union or Literal contained in another generic type, the order of (Y, Z, ...) may be different from the order of the original arguments [Y, Z, ...] due to type caching. Return () for unsupported objects.
def _map_signals(self) -> SignalMap:
"""
Default implementation of the signal mapping
We need to construct a dictionary mapping signals to a set of pins.
In the case that self.map_list is set, the is pretty trival.
If the mux is defined with self.map_tree we have more work to
do, which is recursively delegated to _map_tree
Avoid subclassing. Consider creating helper functions to build
map_tree or map_list.
"""
if hasattr(self, "map_tree"):
return self._map_tree(self.map_tree, self.pin_list, fixed_pins=frozenset())
elif hasattr(self, "map_list"):
return {sig: frozenset(pins) for sig, *pins in self.map_list}
elif (self.__orig_bases__ != VirtualMux.__orig_bases__):
# the user has provided map_list using annotations
# if the type annotations have not been supplied, then self.__orig_bases__ == VirtualMux.__orig_bases__
# if they have been overridden, then self.__orig_bases__ != VirtualMux.__orig_bases__
# works in 3.8 and 3.12
# this will only work if VirtualMux was subclassed
# if creating an instance using VirtualMux[type]() then __orig_bases__ does not exist
# there seems to be some
# we are expecting exactly 1 value, unpack it
cls, = self.__orig_bases__
assert get_origin(cls) == VirtualMux
lst, = get_args(cls)
assert get_origin(lst) == Union
signals = get_args(lst)
sigmap = {}
for s in signals:
if not hasattr(s, "__metadata__"):
raise ValueError("VirtualMux Subclass must define the pins for each signal")
# s.__metadata__ is our pin list
pins = s.__metadata__
# get_args gives Literal
literal, = get_args(s)
# get_args gives members of Literal
signame, = get_args(literal)
sigmap[signame] = frozenset(pins)
return sigmap
else:
raise ValueError(
"VirtualMux subclass must define either map_tree or map_list or provide a type to VirtualMux"
)
This nearly gets there. It'd be nice if we could do something like
MuxOne = VirtualMux[MuxOneSigDef]
@dataclass
class JigMuxGroup(MuxGroup):
mux_one: MuxOne = field(default_factory=MuxOne)
I think that would get rid of my weird feelings about the way we subclass a thing to do basically nothing. But I get that this whole idea is bastardising the crap out of this system anyway.
The __orig_bases__
trick only works due to subclassing. You'd need another way to bind the argument of __class_getitem__
to the associated __init__
def __class_getitem__(cls, *args, **kwargs):
# without calling getitem the class doesn't work as a generic
getitm = super().__class_getitem__(*args, **kwargs) # normally returns a generic
# create a proxy to force the __init_subclass__ hook
class Hack(getitm):
...
return Hack # now the actual class can be initialised
Forgive me for I have sinned.
Type checkers ignore this completely and just use the inferred behaviour due to subclassing Generic
. We could use TYPE_CHECKING
if we want to lie to the type checker as well.
Looks like they changed how Annotated behaves with get_args
between 3.8 and 3.10.
In 3.8 __metadata__
is ignored by get_args
. In 3.10 it is used. I guess this makes sense if get_args
is meant to return whatever is inside the brackets of a generic.
Changing the unpack operator to allow variable length is enough to get this working on all versions.
Made a PR for people to look at and play around with. Not expecting it to be merged in any time soon.
This came about while thinking of other ways to create a VirtualMux that will be type hint and IDE friendly. Originally a comment on #184 but I think it is better to have an issue separate from that so it doesn't get lost.
Each signal would be a method on the resulting VirtualMux subclass. This would be a big change for existing scripts. But once the jig refactor PR lands, we could introduce something like this and scripts could choose which style to use. For new scripts, the IDE autocomplete benefits of this approach would be nice to have. (There might be a way to type-hint the current VirtualMux, but I haven't figured it out)
Implementation - Maybe? very not tested... Most likely bugs, but I think the concept is O.K. I think we would end up with signal returning a class instead of a function, but this maps out the general idea.