Skip to content

Commit 092e935

Browse files
committed
Support variable-length and empty tuples.
Issue: #140
1 parent 36d1642 commit 092e935

File tree

4 files changed

+45
-27
lines changed

4 files changed

+45
-27
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66
## Unreleased
77
### Changed
88
* Nested literal types supported
9+
* Variable-length and empty tuples supported
910

1011
## [2.0.2](https://pypi.org/project/multimethod/2.0.2/) - 2025-11-17
1112
### Changed

multimethod/__init__.py

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,22 @@ class subtype(abc.ABCMeta):
4646
__args__: tuple
4747

4848
def __new__(cls, tp, *args):
49-
if tp is Any:
50-
return object
51-
if isinstance(tp, cls): # If already a subtype, return it directly
52-
return tp
53-
if isinstance(tp, typing.NewType):
54-
return cls(tp.__supertype__, *args)
49+
match tp:
50+
case typing.Any:
51+
return object
52+
case subtype(): # If already a subtype, return it directly
53+
return tp
54+
case typing.NewType():
55+
return cls(tp.__supertype__, *args)
56+
case TypeVar():
57+
return cls(Union[tp.__constraints__], *args) if tp.__constraints__ else object
58+
case typing._AnnotatedAlias():
59+
return cls(tp.__origin__, *args)
5560
if hasattr(typing, 'TypeAliasType') and isinstance(tp, typing.TypeAliasType):
5661
return cls(tp.__value__, *args)
57-
if isinstance(tp, TypeVar):
58-
return cls(Union[tp.__constraints__], *args) if tp.__constraints__ else object
59-
if isinstance(tp, typing._AnnotatedAlias):
60-
return cls(tp.__origin__, *args)
6162
origin = get_origin(tp) or tp
6263
args = tuple(map(cls, get_args(tp) or args))
63-
if set(args) <= {object} and not (origin is tuple and args):
64+
if set(args) <= {object} and (origin is not tuple or tp is tuple):
6465
return origin
6566
bases = (origin,) if type(origin) in (type, abc.ABCMeta) else ()
6667
if origin is Literal:
@@ -87,22 +88,27 @@ def __hash__(self) -> int:
8788
return hash(self.key())
8889

8990
def __subclasscheck__(self, subclass):
90-
origin = get_origin(subclass) or subclass
9191
args = get_args(subclass)
92-
if origin is Literal:
93-
return all(isinstance(arg, self) for arg in args)
94-
if origin in (Union, types.UnionType):
95-
return all(issubclass(cls, self) for cls in args)
96-
if self.__origin__ is Literal:
97-
return False
98-
if self.__origin__ is types.UnionType:
99-
return issubclass(subclass, self.__args__)
100-
if self.__origin__ is Callable:
101-
return (
102-
origin is Callable
103-
and signature(self.__args__[-1:]) <= signature(args[-1:]) # covariant return
104-
and signature(args[:-1]) <= signature(self.__args__[:-1]) # contravariant args
105-
)
92+
match origin := get_origin(subclass) or subclass:
93+
case typing.Literal:
94+
return all(isinstance(arg, self) for arg in args)
95+
case typing.Union | types.UnionType:
96+
return all(issubclass(cls, self) for cls in args)
97+
match self.__origin__:
98+
case typing.Literal:
99+
return False
100+
case types.UnionType:
101+
return issubclass(subclass, self.__args__)
102+
case builtins.tuple:
103+
if issubclass(origin, tuple) and ... in self.__args__:
104+
param = self.__args__[0]
105+
return all(arg is ... or issubclass(arg, param) for arg in args)
106+
case collections.abc.Callable:
107+
return (
108+
origin is Callable
109+
and signature(self.__args__[-1:]) <= signature(args[-1:]) # covariant return
110+
and signature(args[:-1]) <= signature(self.__args__[:-1]) # contravariant args
111+
)
106112
return ( # check args first to avoid recursion error: python/cpython#73407
107113
len(args) == len(self.__args__)
108114
and issubclass(origin, self.__origin__)

tests/test_methods.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ def test_subtype():
6565
assert isinstance((0, 0.0), subtype(tuple[int, float]))
6666
assert not isinstance((0,), subtype(tuple[int, float]))
6767
assert isinstance((0,), subtype(tuple[int, ...]))
68-
assert not issubclass(tuple[int], subtype(tuple[int, ...]))
6968
assert not isinstance(iter('-'), subtype(Iterable[str]))
7069
assert not issubclass(tuple[int], subtype(tuple[int, float]))
7170
assert issubclass(Iterable[bool], subtype(Iterable[int]))

tests/test_subscripts.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,18 @@ def func(x: cls[int]):
8282
assert func(obj) is None
8383

8484

85+
def test_tuple():
86+
assert subtype(tuple) is tuple
87+
assert not issubclass(tuple[int], subtype(tuple[()]))
88+
assert not isinstance(tuple[int], subtype(type[tuple[()]]))
89+
assert isinstance((), subtype(tuple[()]))
90+
assert not isinstance((0,), subtype(tuple[()]))
91+
assert issubclass(tuple[int], subtype(tuple[int, ...]))
92+
assert issubclass(tuple[bool, ...], subtype(tuple[int, ...]))
93+
assert isinstance(tuple[int], subtype(type[tuple[int, ...]]))
94+
assert not issubclass(tuple[int, float], subtype(tuple[int, ...]))
95+
96+
8597
def test_empty():
8698
@multimethod
8799
def func(arg: list[int]):

0 commit comments

Comments
 (0)