From 702885073c1fd7de10f94ca19951e0f5e1c836c6 Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Thu, 25 Jun 2026 17:38:59 +1000 Subject: [PATCH 01/15] explicitly create types with new method --- src/fixate/_switching.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/fixate/_switching.py b/src/fixate/_switching.py index eaf671e..a9b6288 100644 --- a/src/fixate/_switching.py +++ b/src/fixate/_switching.py @@ -48,12 +48,12 @@ from functools import reduce from operator import or_ -Signal = str -Pin = str -PinList = Sequence[Pin] -PinSet = FrozenSet[Pin] -SignalMap = Dict[Signal, PinSet] -TreeDef = Sequence[Union[Signal, "TreeDef"]] +type Signal = str +type Pin = str +type PinList = Sequence[Pin] +type PinSet = FrozenSet[Pin] +type SignalMap = Dict[Signal, PinSet] +type TreeDef = Sequence[Union[Signal, "TreeDef"]] @dataclass(frozen=True) From efd15b70687443ebd88071776b61aca4414588a6 Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Thu, 25 Jun 2026 18:42:47 +1000 Subject: [PATCH 02/15] Make Vmux and relaymatrix generic --- src/fixate/_switching.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/fixate/_switching.py b/src/fixate/_switching.py index a9b6288..b446afd 100644 --- a/src/fixate/_switching.py +++ b/src/fixate/_switching.py @@ -43,12 +43,16 @@ Dict, FrozenSet, Iterable, + TypeGuard, + Any, + Literal, ) from dataclasses import dataclass from functools import reduce from operator import or_ type Signal = str +type EmptySignal = Literal[""] type Pin = str type PinList = Sequence[Pin] type PinSet = FrozenSet[Pin] @@ -56,6 +60,14 @@ type TreeDef = Sequence[Union[Signal, "TreeDef"]] +def is_Signal(obj: Any) -> TypeGuard[Signal]: + """ + like isinstance but using types + """ + # in python 3.14 this can be updated to use .evaluate_value() + return isinstance(obj, Signal.__value__) + + @dataclass(frozen=True) class PinSetState: off: PinSet = frozenset() @@ -88,7 +100,10 @@ def __or__(self, other: PinUpdate) -> PinUpdate: PinUpdateCallback = Callable[[PinUpdate, bool], None] -class VirtualMux: +class VirtualMux[S: Signal]: + # define the union of what the user supplied and the automatically created + # signal here so we don't have to keep typing this union everywhere + type MuxSignal = S | EmptySignal pin_list: PinList = () clearing_time: float = 0.0 @@ -129,7 +144,7 @@ def __init__(self, update_pins: Optional[PinUpdateCallback] = None): if hasattr(self, "default_signal"): raise ValueError("'default_signal' should not be set on a VirtualMux") - def __call__(self, signal: Signal, trigger_update: bool = True) -> None: + def __call__(self, signal: MuxSignal, trigger_update: bool = True) -> None: """ Convenience to avoid having to type jig.mux..multiplex. @@ -138,7 +153,7 @@ def __call__(self, signal: Signal, trigger_update: bool = True) -> None: """ self.multiplex(signal, trigger_update) - def multiplex(self, signal: Signal, trigger_update: bool = True) -> None: + def multiplex(self, signal: MuxSignal, trigger_update: bool = True) -> None: """ Update the multiplexer state to signal. @@ -163,7 +178,7 @@ def multiplex(self, signal: Signal, trigger_update: bool = True) -> None: self._last_update_time = time.monotonic() self._state = signal - def all_signals(self) -> tuple[Signal, ...]: + def all_signals(self) -> tuple[MuxSignal, ...]: return tuple(self._signal_map.keys()) def reset(self, trigger_update: bool = True) -> None: @@ -191,7 +206,7 @@ def pins(self) -> frozenset[Pin]: # The following methods are potential candidates to override in a subclass def _calculate_pins( - self, old_signal: Signal, new_signal: Signal + self, old_signal: MuxSignal, new_signal: MuxSignal ) -> tuple[PinSetState, PinSetState]: """ Calculate the pin sets for the two-step state change. @@ -409,7 +424,7 @@ class Mux(VirtualMux): ): if signal_or_tree is None: continue - if isinstance(signal_or_tree, Signal): + if is_Signal(signal_or_tree): signal_map[signal_or_tree] = frozenset(pins_for_signal) | fixed_pins else: signal_map.update( @@ -482,7 +497,7 @@ def __init__( super().__init__(update_pins) -class RelayMatrixMux(VirtualMux): +class RelayMatrixMux[S: Signal](VirtualMux[S]): clearing_time = 0.01 def _calculate_pins( From cc2327a3089e6ca355d47a2aaaaed7c38c1e9f61 Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Thu, 25 Jun 2026 19:00:15 +1000 Subject: [PATCH 03/15] Fix up some typing errors --- src/fixate/_switching.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/fixate/_switching.py b/src/fixate/_switching.py index b446afd..c30d45b 100644 --- a/src/fixate/_switching.py +++ b/src/fixate/_switching.py @@ -56,8 +56,8 @@ type Pin = str type PinList = Sequence[Pin] type PinSet = FrozenSet[Pin] -type SignalMap = Dict[Signal, PinSet] -type TreeDef = Sequence[Union[Signal, "TreeDef"]] +type SignalMap[S: Signal] = Dict[S, PinSet] +type TreeDef[S: Signal] = Sequence[Union[S, "TreeDef"]] def is_Signal(obj: Any) -> TypeGuard[Signal]: @@ -103,7 +103,7 @@ def __or__(self, other: PinUpdate) -> PinUpdate: class VirtualMux[S: Signal]: # define the union of what the user supplied and the automatically created # signal here so we don't have to keep typing this union everywhere - type MuxSignal = S | EmptySignal + type MuxSignal[M: Signal] = M | EmptySignal pin_list: PinList = () clearing_time: float = 0.0 @@ -125,9 +125,9 @@ def __init__(self, update_pins: Optional[PinUpdateCallback] = None): # we convert here and keep a reference to the set for future use. self._pin_set = frozenset(self.pin_list) - self._state = "" + self._state: VirtualMux.MuxSignal[S] = "" - self._signal_map: SignalMap = self._map_signals() + self._signal_map: SignalMap[VirtualMux.MuxSignal[S]] = self._map_signals() # Define the implicit signal "" which can be used to turn off all pins. # If the signal map already has this defined, raise an error. In the old @@ -144,7 +144,7 @@ def __init__(self, update_pins: Optional[PinUpdateCallback] = None): if hasattr(self, "default_signal"): raise ValueError("'default_signal' should not be set on a VirtualMux") - def __call__(self, signal: MuxSignal, trigger_update: bool = True) -> None: + def __call__(self, signal: MuxSignal[S], trigger_update: bool = True) -> None: """ Convenience to avoid having to type jig.mux..multiplex. @@ -153,7 +153,7 @@ def __call__(self, signal: MuxSignal, trigger_update: bool = True) -> None: """ self.multiplex(signal, trigger_update) - def multiplex(self, signal: MuxSignal, trigger_update: bool = True) -> None: + def multiplex(self, signal: MuxSignal[S], trigger_update: bool = True) -> None: """ Update the multiplexer state to signal. @@ -178,7 +178,7 @@ def multiplex(self, signal: MuxSignal, trigger_update: bool = True) -> None: self._last_update_time = time.monotonic() self._state = signal - def all_signals(self) -> tuple[MuxSignal, ...]: + def all_signals(self) -> tuple[MuxSignal[S], ...]: return tuple(self._signal_map.keys()) def reset(self, trigger_update: bool = True) -> None: @@ -206,7 +206,7 @@ def pins(self) -> frozenset[Pin]: # The following methods are potential candidates to override in a subclass def _calculate_pins( - self, old_signal: MuxSignal, new_signal: MuxSignal + self, old_signal: MuxSignal[S], new_signal: MuxSignal[S] ) -> tuple[PinSetState, PinSetState]: """ Calculate the pin sets for the two-step state change. @@ -233,7 +233,7 @@ def _calculate_pins( # The following methods are intended as implementation detail and # subclasses should avoid overriding. - def _map_signals(self) -> SignalMap: + def _map_signals(self) -> SignalMap[VirtualMux.MuxSignal[S]]: """ Default implementation of the signal mapping @@ -254,7 +254,9 @@ def _map_signals(self) -> SignalMap: "VirtualMux subclass must define either map_tree or map_list" ) - def _map_tree(self, tree: TreeDef, pins: PinList, fixed_pins: PinSet) -> SignalMap: + def _map_tree( + self, tree: TreeDef[S], pins: PinList, fixed_pins: PinSet + ) -> SignalMap[VirtualMux.MuxSignal[S]]: """recursively add nested signal lists to the signal map. tree: is the current sub-branch to be added. At the first call level, this would be initialised with self.map_tree. It can be @@ -501,7 +503,7 @@ class RelayMatrixMux[S: Signal](VirtualMux[S]): clearing_time = 0.01 def _calculate_pins( - self, old_signal: Signal, new_signal: Signal + self, old_signal: VirtualMux.MuxSignal[S], new_signal: VirtualMux.MuxSignal[S] ) -> tuple[PinSetState, PinSetState]: """ Override of _calculate_pins to implement break-before-make switching. From 34c9b4b51ca221c0ca405e37878cc5450048f160 Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Thu, 25 Jun 2026 19:00:45 +1000 Subject: [PATCH 04/15] Add a test --- test/test_switching.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/test/test_switching.py b/test/test_switching.py index 611d9dc..948762b 100644 --- a/test/test_switching.py +++ b/test/test_switching.py @@ -1,4 +1,4 @@ -from typing import Collection, Sequence +from typing import Collection, Sequence, Literal from fixate._switching import ( Pin, @@ -289,6 +289,13 @@ class MuxA(VirtualMux): map_list = (("sig_a1", "a0", "a1"), ("sig_a2", "a1")) +class MuxATyped(VirtualMux[Literal["sig_a1", "sig_a2"]]): + """A mux definition used by a few tests""" + + pin_list = ("a0", "a1") + map_list = (("sig_a1", "a0", "a1"), ("sig_a2", "a1")) + + def test_virtual_mux_basic(): updates = [] mux_a = MuxA(lambda x, y: updates.append((x, y))) @@ -309,6 +316,26 @@ def test_virtual_mux_basic(): ] +def test_virtual_mux_basic_typed(): + updates = [] + mux_a = MuxATyped(lambda x, y: updates.append((x, y))) + + # test both the __call__ and multiplex methods trigger + # the appropriate update callback. + mux_a("sig_a1") + mux_a.multiplex("sig_a2", trigger_update=False) + mux_a("") + + clear = PinSetState(off=frozenset({"a0", "a1"})) + a1 = PinSetState(on=frozenset({"a0", "a1"})) + a2 = PinSetState(on=frozenset({"a1"}), off=frozenset({"a0"})) + assert updates == [ + (PinUpdate(PinSetState(), a1), True), + (PinUpdate(PinSetState(), a2), False), + (PinUpdate(PinSetState(), clear), True), + ] + + def test_virtual_mux_reset(): """Check that reset sends an update that sets all pins off""" From 81cd73d5d953c6d4c4859c6ae8133cf1b2a8a84b Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Thu, 25 Jun 2026 19:15:59 +1000 Subject: [PATCH 05/15] update example --- examples/jig_driver.py | 53 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/examples/jig_driver.py b/examples/jig_driver.py index 977318a..81bb22d 100644 --- a/examples/jig_driver.py +++ b/examples/jig_driver.py @@ -11,6 +11,7 @@ MuxGroup, PinValueAddressHandler, VirtualSwitch, + RelayMatrixMux, ) @@ -59,3 +60,55 @@ class JigMuxGroup(MuxGroup): jig.mux.mux_two("sig5") jig.mux.mux_three("On") jig.mux.mux_three(False) + + +# VirtualMuxes can be made generic +from typing import Literal + +# the type keyword can be used to create reusable definitions +# otherwise Literal can be used directly +type Signals = Literal["signal_1", "signal_2"] + +# note: the type keyword can't be used inside functions! +# generally we want to use type to avoid confusion around the type system +# this makes it clear we are creating something for typehinting +# e.g type MyInt = int - won't work in functions +# variable = int - is not obvious what the intent is and can behave differently depending on its scope + + +def do_some_stuff(): + # otherwise the mux is created as normal + class MyTypedMux(VirtualMux[Signals]): + pin_list = ("x0", "x1") + map_list = ( + ("signal_1", "x0"), + ("signal_2", "x1"), + ) + + mymux = MyTypedMux() + + # signal names will appear in the autocompletion options (including the empty signal "") + mymux.multiplex("") + mymux.multiplex("signal_1") + mymux.multiplex("signal_2") + + # anything that isn't a signal will be flagged + try: + mymux.multiplex("not_a_signal") + except ValueError as e: + print(e) + + class MyTypedRelay(RelayMatrixMux[Signals]): + pin_list = ("x3", "x4") + map_list = ( + ("signal_1", "x3"), + ("signal_2", "x4"), + ) + + myrelay = MyTypedRelay() + myrelay.multiplex("") + myrelay.multiplex("signal_1") + myrelay.multiplex("signal_2") + + +do_some_stuff() From 354959db0111cbcac4ad8d82aaaa0425912ec870 Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Thu, 25 Jun 2026 19:52:55 +1000 Subject: [PATCH 06/15] move muxsignal out of virtualmux --- src/fixate/_switching.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/fixate/_switching.py b/src/fixate/_switching.py index c30d45b..4854f05 100644 --- a/src/fixate/_switching.py +++ b/src/fixate/_switching.py @@ -53,6 +53,7 @@ type Signal = str type EmptySignal = Literal[""] +type MuxSignal[M: Signal] = M | EmptySignal type Pin = str type PinList = Sequence[Pin] type PinSet = FrozenSet[Pin] @@ -103,7 +104,6 @@ def __or__(self, other: PinUpdate) -> PinUpdate: class VirtualMux[S: Signal]: # define the union of what the user supplied and the automatically created # signal here so we don't have to keep typing this union everywhere - type MuxSignal[M: Signal] = M | EmptySignal pin_list: PinList = () clearing_time: float = 0.0 @@ -125,9 +125,9 @@ def __init__(self, update_pins: Optional[PinUpdateCallback] = None): # we convert here and keep a reference to the set for future use. self._pin_set = frozenset(self.pin_list) - self._state: VirtualMux.MuxSignal[S] = "" + self._state: MuxSignal[S] = "" - self._signal_map: SignalMap[VirtualMux.MuxSignal[S]] = self._map_signals() + self._signal_map: SignalMap[MuxSignal[S]] = self._map_signals() # Define the implicit signal "" which can be used to turn off all pins. # If the signal map already has this defined, raise an error. In the old @@ -233,7 +233,7 @@ def _calculate_pins( # The following methods are intended as implementation detail and # subclasses should avoid overriding. - def _map_signals(self) -> SignalMap[VirtualMux.MuxSignal[S]]: + def _map_signals(self) -> SignalMap[MuxSignal[S]]: """ Default implementation of the signal mapping @@ -256,7 +256,7 @@ def _map_signals(self) -> SignalMap[VirtualMux.MuxSignal[S]]: def _map_tree( self, tree: TreeDef[S], pins: PinList, fixed_pins: PinSet - ) -> SignalMap[VirtualMux.MuxSignal[S]]: + ) -> SignalMap[MuxSignal[S]]: """recursively add nested signal lists to the signal map. tree: is the current sub-branch to be added. At the first call level, this would be initialised with self.map_tree. It can be @@ -503,7 +503,7 @@ class RelayMatrixMux[S: Signal](VirtualMux[S]): clearing_time = 0.01 def _calculate_pins( - self, old_signal: VirtualMux.MuxSignal[S], new_signal: VirtualMux.MuxSignal[S] + self, old_signal: MuxSignal[S], new_signal: MuxSignal[S] ) -> tuple[PinSetState, PinSetState]: """ Override of _calculate_pins to implement break-before-make switching. From 58e3bfe213fcd89bfd4106f87d50ed2546756999 Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Thu, 25 Jun 2026 19:54:11 +1000 Subject: [PATCH 07/15] move to new Union --- src/fixate/_switching.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/fixate/_switching.py b/src/fixate/_switching.py index 4854f05..475b364 100644 --- a/src/fixate/_switching.py +++ b/src/fixate/_switching.py @@ -38,7 +38,6 @@ Sequence, TypeVar, Generator, - Union, Collection, Dict, FrozenSet, @@ -58,7 +57,7 @@ type PinList = Sequence[Pin] type PinSet = FrozenSet[Pin] type SignalMap[S: Signal] = Dict[S, PinSet] -type TreeDef[S: Signal] = Sequence[Union[S, "TreeDef"]] +type TreeDef[S: Signal] = Sequence[S | "TreeDef"] def is_Signal(obj: Any) -> TypeGuard[Signal]: @@ -473,9 +472,7 @@ class VirtualSwitch(VirtualMux): pin_name: Pin = "" map_tree = ("Off", "On") - def multiplex( - self, signal: Union[Signal, bool], trigger_update: bool = True - ) -> None: + def multiplex(self, signal: Signal | bool, trigger_update: bool = True) -> None: if signal is True: converted_signal = "On" elif signal is False: @@ -484,9 +481,7 @@ def multiplex( converted_signal = signal super().multiplex(converted_signal, trigger_update=trigger_update) - def __call__( - self, signal: Union[Signal, bool], trigger_update: bool = True - ) -> None: + def __call__(self, signal: Signal | bool, trigger_update: bool = True) -> None: """Override call to set the type on signal_output correctly.""" self.multiplex(signal, trigger_update) From e968d8cfaa1a00a4869daf82a2df534695679ee6 Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Thu, 25 Jun 2026 19:55:35 +1000 Subject: [PATCH 08/15] new optional --- src/fixate/_switching.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/fixate/_switching.py b/src/fixate/_switching.py index 475b364..76f479e 100644 --- a/src/fixate/_switching.py +++ b/src/fixate/_switching.py @@ -33,7 +33,6 @@ import time from typing import ( Generic, - Optional, Callable, Sequence, TypeVar, @@ -58,6 +57,7 @@ type PinSet = FrozenSet[Pin] type SignalMap[S: Signal] = Dict[S, PinSet] type TreeDef[S: Signal] = Sequence[S | "TreeDef"] +type PinUpdateCallback = Callable[[PinUpdate, bool], None] def is_Signal(obj: Any) -> TypeGuard[Signal]: @@ -97,9 +97,6 @@ def __or__(self, other: PinUpdate) -> PinUpdate: return NotImplemented -PinUpdateCallback = Callable[[PinUpdate, bool], None] - - class VirtualMux[S: Signal]: # define the union of what the user supplied and the automatically created # signal here so we don't have to keep typing this union everywhere @@ -109,7 +106,7 @@ class VirtualMux[S: Signal]: ########################################################################### # These methods are the public API for the class - def __init__(self, update_pins: Optional[PinUpdateCallback] = None): + def __init__(self, update_pins: PinUpdateCallback | None = None): self._last_update_time = time.monotonic() self._update_pins: PinUpdateCallback @@ -487,7 +484,7 @@ def __call__(self, signal: Signal | bool, trigger_update: bool = True) -> None: def __init__( self, - update_pins: Optional[PinUpdateCallback] = None, + update_pins: PinUpdateCallback | None = None, ): if not self.pin_list: self.pin_list = [self.pin_name] From a2c21a2af48b1e983a1129376e6e596c30e1e2d5 Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Thu, 25 Jun 2026 19:57:58 +1000 Subject: [PATCH 09/15] new Generic --- src/fixate/_switching.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/fixate/_switching.py b/src/fixate/_switching.py index 76f479e..09383a3 100644 --- a/src/fixate/_switching.py +++ b/src/fixate/_switching.py @@ -32,7 +32,6 @@ import itertools import time from typing import ( - Generic, Callable, Sequence, TypeVar, @@ -693,10 +692,7 @@ def active_signals(self) -> list[str]: return [str(mux) for mux in self.get_multiplexers()] -JigSpecificMuxGroup = TypeVar("JigSpecificMuxGroup", bound=MuxGroup) - - -class JigDriver(Generic[JigSpecificMuxGroup]): +class JigDriver[M: MuxGroup](): """ Combine multiple VirtualMux's and multiple AddressHandler's. @@ -705,7 +701,7 @@ class JigDriver(Generic[JigSpecificMuxGroup]): def __init__( self, - mux_group_factory: Callable[[], JigSpecificMuxGroup], + mux_group_factory: Callable[[], M], handlers: Sequence[AddressHandler], ): # keep a reference to handlers so that we can close them if required. From 87f00a0101130e3fad86fb1728eeed5f609b9ba0 Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Thu, 25 Jun 2026 20:01:20 +1000 Subject: [PATCH 10/15] remove more old types --- src/fixate/_switching.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/fixate/_switching.py b/src/fixate/_switching.py index 09383a3..6a2d866 100644 --- a/src/fixate/_switching.py +++ b/src/fixate/_switching.py @@ -34,11 +34,8 @@ from typing import ( Callable, Sequence, - TypeVar, Generator, Collection, - Dict, - FrozenSet, Iterable, TypeGuard, Any, @@ -53,8 +50,8 @@ type MuxSignal[M: Signal] = M | EmptySignal type Pin = str type PinList = Sequence[Pin] -type PinSet = FrozenSet[Pin] -type SignalMap[S: Signal] = Dict[S, PinSet] +type PinSet = frozenset[Pin] +type SignalMap[S: Signal] = dict[S, PinSet] type TreeDef[S: Signal] = Sequence[S | "TreeDef"] type PinUpdateCallback = Callable[[PinUpdate, bool], None] @@ -779,10 +776,7 @@ def _validate(self) -> None: ) -_T = TypeVar("_T") - - -def _generate_bit_sets(bits: Sequence[_T]) -> Generator[set[_T], None, None]: +def _generate_bit_sets[_T](bits: Sequence[_T]) -> Generator[set[_T], None, None]: """ Create subsets of bits, representing bits of a list of integers From eabbc8ab3ed90eb335c671c8284d17e2ef9282dc Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Thu, 25 Jun 2026 20:03:51 +1000 Subject: [PATCH 11/15] supply default types instead of unknown --- src/fixate/_switching.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fixate/_switching.py b/src/fixate/_switching.py index 6a2d866..1dd370d 100644 --- a/src/fixate/_switching.py +++ b/src/fixate/_switching.py @@ -47,12 +47,12 @@ type Signal = str type EmptySignal = Literal[""] -type MuxSignal[M: Signal] = M | EmptySignal +type MuxSignal[M: Signal = Signal] = M | EmptySignal type Pin = str type PinList = Sequence[Pin] type PinSet = frozenset[Pin] -type SignalMap[S: Signal] = dict[S, PinSet] -type TreeDef[S: Signal] = Sequence[S | "TreeDef"] +type SignalMap[S: Signal = Signal] = dict[S, PinSet] +type TreeDef[S: Signal = Signal] = Sequence[S | "TreeDef"] type PinUpdateCallback = Callable[[PinUpdate, bool], None] From ff44a434523887b114062f81da3bf5005f5cdc79 Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Fri, 26 Jun 2026 11:54:32 +1000 Subject: [PATCH 12/15] make isSignal generic, add tests --- src/fixate/_switching.py | 19 +++++++++---------- test/test_switching.py | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/fixate/_switching.py b/src/fixate/_switching.py index 1dd370d..9f2695b 100644 --- a/src/fixate/_switching.py +++ b/src/fixate/_switching.py @@ -56,14 +56,6 @@ type PinUpdateCallback = Callable[[PinUpdate, bool], None] -def is_Signal(obj: Any) -> TypeGuard[Signal]: - """ - like isinstance but using types - """ - # in python 3.14 this can be updated to use .evaluate_value() - return isinstance(obj, Signal.__value__) - - @dataclass(frozen=True) class PinSetState: off: PinSet = frozenset() @@ -102,6 +94,13 @@ class VirtualMux[S: Signal]: ########################################################################### # These methods are the public API for the class + def isSignal(self, obj: Any) -> TypeGuard[S]: + # we can tell the typechecker about our user specified signals here + # at runtime we just check if this is a string + # in the future S can be inspected using get_origin, get_args + # resolve_bases and get_original_bases + return isinstance(obj, Signal.__value__) + def __init__(self, update_pins: PinUpdateCallback | None = None): self._last_update_time = time.monotonic() @@ -408,7 +407,7 @@ class Mux(VirtualMux): mux_b = TreeMap(("a1_b0", "a1_b1", "a1_b2", None), ("x2", "x3")) map_tree = TreeMap(("a0", mux_b, "a2", mux_c), ("x1", "x0")) """ - signal_map: SignalMap = dict() + signal_map: SignalMap[MuxSignal[S]] = dict() bits_at_this_level = (len(tree) - 1).bit_length() pins_at_this_level = pins[:bits_at_this_level] @@ -418,7 +417,7 @@ class Mux(VirtualMux): ): if signal_or_tree is None: continue - if is_Signal(signal_or_tree): + if self.isSignal(signal_or_tree): signal_map[signal_or_tree] = frozenset(pins_for_signal) | fixed_pins else: signal_map.update( diff --git a/test/test_switching.py b/test/test_switching.py index 948762b..ce4ee09 100644 --- a/test/test_switching.py +++ b/test/test_switching.py @@ -336,6 +336,26 @@ def test_virtual_mux_basic_typed(): ] +@pytest.mark.xfail(reason="Signal narrowowing not implemented") +def test_virtual_mux_typed_isSignal(): + mux_a = MuxATyped() + + assert mux_a.isSignal("sig_a1") # should pass + assert not mux_a.isSignal("") # this shouldn't be supplied by the user + assert not mux_a.isSignal(1) # wrong type + assert not mux_a.isSignal("1") # not a signal for MuxATyped - not yet implemented + + +def test_virtual_mux_isSignal(): + mux_a = MuxA() + # this mux isn't type, so anything that is a string should pass this + # to check we don't accidentally break untyped muxes in the future + assert mux_a.isSignal("sig_a1") # should pass + assert mux_a.isSignal("") # should pass + assert not mux_a.isSignal(1) # wrong type + assert mux_a.isSignal("1") # should pass + + def test_virtual_mux_reset(): """Check that reset sends an update that sets all pins off""" From 976b2ffbc9250982623262023a6a72e38c4746ed Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Fri, 26 Jun 2026 12:14:27 +1000 Subject: [PATCH 13/15] default type annotations cause syntax errors in 3.12 --- src/fixate/_switching.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fixate/_switching.py b/src/fixate/_switching.py index 9f2695b..6b62282 100644 --- a/src/fixate/_switching.py +++ b/src/fixate/_switching.py @@ -47,13 +47,13 @@ type Signal = str type EmptySignal = Literal[""] -type MuxSignal[M: Signal = Signal] = M | EmptySignal type Pin = str type PinList = Sequence[Pin] type PinSet = frozenset[Pin] -type SignalMap[S: Signal = Signal] = dict[S, PinSet] -type TreeDef[S: Signal = Signal] = Sequence[S | "TreeDef"] type PinUpdateCallback = Callable[[PinUpdate, bool], None] +type MuxSignal[M: Signal] = M | EmptySignal +type SignalMap[S: Signal] = dict[S, PinSet] +type TreeDef[S: Signal] = Sequence[S | "TreeDef"] @dataclass(frozen=True) From 0c30dbd1cba3a2ac3bf5ca539c82ce176073c4eb Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Fri, 26 Jun 2026 13:49:27 +1000 Subject: [PATCH 14/15] Update example --- examples/jig_driver.py | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/examples/jig_driver.py b/examples/jig_driver.py index 81bb22d..c891027 100644 --- a/examples/jig_driver.py +++ b/examples/jig_driver.py @@ -65,20 +65,20 @@ class JigMuxGroup(MuxGroup): # VirtualMuxes can be made generic from typing import Literal -# the type keyword can be used to create reusable definitions -# otherwise Literal can be used directly -type Signals = Literal["signal_1", "signal_2"] - # note: the type keyword can't be used inside functions! # generally we want to use type to avoid confusion around the type system # this makes it clear we are creating something for typehinting # e.g type MyInt = int - won't work in functions # variable = int - is not obvious what the intent is and can behave differently depending on its scope +# the type keyword can be used to create reusable definitions +# otherwise Literal can be used directly +type MyTypedMuxSignals = Literal["signal_1", "signal_2"] + def do_some_stuff(): # otherwise the mux is created as normal - class MyTypedMux(VirtualMux[Signals]): + class MyTypedMux(VirtualMux[MyTypedMuxSignals]): pin_list = ("x0", "x1") map_list = ( ("signal_1", "x0"), @@ -98,7 +98,31 @@ class MyTypedMux(VirtualMux[Signals]): except ValueError as e: print(e) - class MyTypedRelay(RelayMatrixMux[Signals]): + # the annotations can also be used directly with Literal + class MyDirectlyTypedMux(VirtualMux[Literal["Sig_1", "Sig_2"]]): + pin_list = ("x0", "x1") + # Note this currently doesn't point out the incorrect signal mapping below! + # it is still up to the user to set up muxes correctly + map_list = ( + ("signal_1", "x0"), + ("signal_2", "x1"), + ) + + # suggestions will work as normal + myothermux = MyDirectlyTypedMux() + + myothermux.multiplex("") + myothermux.multiplex("Sig_1") + myothermux.multiplex("Sig_2") + + try: + myothermux.multiplex("not_a_signal") + except ValueError as e: + print(e) + + # general subclasses and RelayMatrixMux also work with this + # currently VirtualSwitch doesn't, it creates its own signal names doesn't really benefit from this + class MyTypedRelay(RelayMatrixMux[MyTypedMuxSignals]): pin_list = ("x3", "x4") map_list = ( ("signal_1", "x3"), From 14953c0df1f18a87508f8f490de8a2e01ece5744 Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Fri, 26 Jun 2026 13:50:12 +1000 Subject: [PATCH 15/15] update comment --- examples/jig_driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/jig_driver.py b/examples/jig_driver.py index c891027..8ec950f 100644 --- a/examples/jig_driver.py +++ b/examples/jig_driver.py @@ -101,7 +101,7 @@ class MyTypedMux(VirtualMux[MyTypedMuxSignals]): # the annotations can also be used directly with Literal class MyDirectlyTypedMux(VirtualMux[Literal["Sig_1", "Sig_2"]]): pin_list = ("x0", "x1") - # Note this currently doesn't point out the incorrect signal mapping below! + # Note neither definition currently point out the incorrect signal mapping below! # it is still up to the user to set up muxes correctly map_list = ( ("signal_1", "x0"),