Skip to content

Commit 626d16f

Browse files
authored
Add support for positional only params. (#38)
1 parent 202518e commit 626d16f

5 files changed

Lines changed: 208 additions & 28 deletions

File tree

spec-draft.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ We introduce a ``Param`` type the contains all the information about a function
2929

3030
ParamQuals = typing.Literal["*", "**", "default", "keyword"]
3131

32-
type PosParam[T] = Param[Literal[None], T]
33-
type PosDefaultParam[T] = Param[Literal[None], T, Literal["default"]]
32+
type PosParam[N: str | None, T] = Param[N, T, Literal["positional"]]
33+
type PosDefaultParam[N: str | None, T] = Param[N, T, Literal["positional", "default"]]
3434
type DefaultParam[N: str, T] = Param[N, T, Literal["default"]]
3535
type NamedParam[N: str, T] = Param[N, T, Literal["keyword"]]
3636
type NamedDefaultParam[N: str, T] = Param[N, T, Literal["keyword", "default"]]
@@ -55,7 +55,7 @@ as (we are omiting the ``Literal`` in places)::
5555

5656
Callable[
5757
[
58-
Param[None, int],
58+
Param["a", int, "positional"],
5959
Param["b", int],
6060
Param["c", int, "default"],
6161
Param[None, int, "*"],
@@ -71,7 +71,7 @@ or, using the type abbreviations we provide::
7171

7272
Callable[
7373
[
74-
PosParam[int],
74+
PosParam["a", int],
7575
Param["b", int],
7676
DefaultParam["c", int,
7777
ArgsParam[int, "*"],

tests/test_type_dir.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ def test_type_dir_1b():
218218
assert format_helper.format_class(d) == textwrap.dedent("""\
219219
class CMethod:
220220
@classmethod
221-
def cbase2(_arg0: type[tests.test_type_dir.CMethod], _arg1: int, /, a: bool | None) -> int: ...
221+
def cbase2(cls: type[tests.test_type_dir.CMethod], lol: int, /, a: bool | None) -> int: ...
222222
""")
223223

224224

tests/test_type_eval.py

Lines changed: 182 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
List,
1010
Literal,
1111
Never,
12+
Self,
1213
Tuple,
1314
TypeVar,
1415
Union,
@@ -30,6 +31,7 @@
3031
Iter,
3132
Length,
3233
Member,
34+
Members,
3335
NewProtocol,
3436
Param,
3537
SpecialFormEllipsis,
@@ -137,6 +139,38 @@ class F[bool]:
137139
type NestedTree = str | list[NestedTree] | list[IntTree]
138140

139141

142+
def test_eval_types_4():
143+
d = eval_typing(
144+
Callable[
145+
[
146+
Param[Literal["a"], int, Literal["positional"]],
147+
Param[Literal["b"], int],
148+
Param[Literal["c"], int, Literal["default"]],
149+
Param[None, int, Literal["*"]],
150+
Param[Literal["d"], int, Literal["keyword"]],
151+
Param[Literal["e"], int, Literal["default", "keyword"]],
152+
Param[None, int, Literal["**"]],
153+
],
154+
int,
155+
]
156+
)
157+
assert (
158+
d
159+
== Callable[
160+
[
161+
Param[Literal["a"], int, Literal["positional"]],
162+
Param[Literal["b"], int],
163+
Param[Literal["c"], int, Literal["default"]],
164+
Param[None, int, Literal["*"]],
165+
Param[Literal["d"], int, Literal["keyword"]],
166+
Param[Literal["e"], int, Literal["default", "keyword"]],
167+
Param[None, int, Literal["**"]],
168+
],
169+
int,
170+
]
171+
)
172+
173+
140174
class TA:
141175
x: int
142176
y: list[float]
@@ -381,7 +415,7 @@ def test_eval_getarg_callable_old():
381415
assert args == Any
382416

383417

384-
def test_eval_getarg_callable():
418+
def test_eval_getarg_callable_01():
385419
t = Callable[[int, str], str]
386420
args = eval_typing(GetArg[t, Callable, 0])
387421
assert (
@@ -413,6 +447,153 @@ def test_eval_getarg_callable():
413447
assert args == Any
414448

415449

450+
type IndirectProtocol[T] = NewProtocol[*[m for m in Iter[Members[T]]],]
451+
type GetMethodLike[T, Name] = GetArg[
452+
tuple[
453+
*[
454+
GetType[p]
455+
for p in Iter[Members[T]]
456+
if (
457+
IsSub[GetType[p], Callable]
458+
or IsSub[GetType[p], staticmethod]
459+
or IsSub[GetType[p], classmethod]
460+
)
461+
and IsSub[Name, GetName[p]]
462+
],
463+
],
464+
tuple,
465+
0,
466+
]
467+
468+
469+
def test_eval_getarg_callable_02a():
470+
class C:
471+
def f(self, x: int, /, y: int, *, z: int) -> int: ...
472+
473+
f = eval_typing(GetMethodLike[C, Literal["f"]])
474+
t = eval_typing(GetArg[f, Callable, 0])
475+
assert (
476+
t
477+
== tuple[
478+
Param[Literal["self"], C, Literal["positional"]],
479+
Param[Literal["x"], int, Literal["positional"]],
480+
Param[Literal["y"], int],
481+
Param[Literal["z"], int, Literal["keyword"]],
482+
]
483+
)
484+
t = eval_typing(GetArg[f, Callable, 1])
485+
assert t is int
486+
487+
488+
def test_eval_getarg_callable_02b():
489+
class C:
490+
def f(self, x: int, /, y: int, *, z: int) -> int: ...
491+
492+
f = eval_typing(GetMethodLike[IndirectProtocol[C], Literal["f"]])
493+
t = eval_typing(GetArg[f, Callable, 0])
494+
assert (
495+
t
496+
== tuple[
497+
Param[Literal["self"], Self, Literal["positional"]],
498+
Param[Literal["x"], int, Literal["positional"]],
499+
Param[Literal["y"], int],
500+
Param[Literal["z"], int, Literal["keyword"]],
501+
]
502+
)
503+
t = eval_typing(GetArg[f, Callable, 1])
504+
assert t is int
505+
506+
507+
def test_eval_getarg_callable_03a():
508+
class C:
509+
@classmethod
510+
def f(cls, x: int, /, y: int, *, z: int) -> int: ...
511+
512+
f = eval_typing(GetMethodLike[C, Literal["f"]])
513+
t = eval_typing(GetArg[f, classmethod, 0])
514+
assert t == C
515+
t = eval_typing(GetArg[f, classmethod, 1])
516+
assert (
517+
t
518+
== tuple[
519+
Param[Literal["x"], int, Literal["positional"]],
520+
Param[Literal["y"], int],
521+
Param[Literal["z"], int, Literal["keyword"]],
522+
]
523+
)
524+
t = eval_typing(GetArg[f, classmethod, 2])
525+
assert t is int
526+
527+
528+
def test_eval_getarg_callable_03b():
529+
class C:
530+
@classmethod
531+
def f(cls, x: int, /, y: int, *, z: int) -> int: ...
532+
533+
f = eval_typing(GetMethodLike[IndirectProtocol[C], Literal["f"]])
534+
t = eval_typing(GetArg[f, Callable, 0])
535+
assert (
536+
t
537+
== tuple[
538+
Param[Literal["cls"], type[C], Literal["positional"]],
539+
Param[Literal["x"], int, Literal["positional"]],
540+
Param[Literal["y"], int],
541+
Param[Literal["z"], int, Literal["keyword"]],
542+
]
543+
)
544+
t = eval_typing(GetArg[f, Callable, 1])
545+
assert t is int
546+
547+
548+
def test_eval_getarg_callable_04a():
549+
class C:
550+
@staticmethod
551+
def f(x: int, /, y: int, *, z: int) -> int: ...
552+
553+
f = eval_typing(GetMethodLike[C, Literal["f"]])
554+
t = eval_typing(GetArg[f, staticmethod, 0])
555+
assert (
556+
t
557+
== tuple[
558+
Param[Literal["x"], int, Literal["positional"]],
559+
Param[Literal["y"], int],
560+
Param[Literal["z"], int, Literal["keyword"]],
561+
]
562+
)
563+
t = eval_typing(GetArg[f, staticmethod, 1])
564+
assert t is int
565+
566+
567+
def test_eval_getarg_callable_04b():
568+
class C:
569+
@staticmethod
570+
def f(x: int, /, y: int, *, z: int) -> int: ...
571+
572+
f = eval_typing(GetMethodLike[IndirectProtocol[C], Literal["f"]])
573+
t = eval_typing(GetArg[f, Callable, 0])
574+
assert (
575+
t
576+
== tuple[
577+
Param[Literal["x"], int, Literal["positional"]],
578+
Param[Literal["y"], int],
579+
Param[Literal["z"], int, Literal["keyword"]],
580+
]
581+
)
582+
t = eval_typing(GetArg[f, Callable, 1])
583+
assert t is int
584+
585+
586+
def test_eval_getarg_callable_05():
587+
class C:
588+
f: Callable[[int], int]
589+
590+
f = eval_typing(GetMethodLike[IndirectProtocol[C], Literal["f"]])
591+
t = eval_typing(GetArg[f, Callable, 0])
592+
assert t == tuple[Param[Literal[None], int, Never],]
593+
t = eval_typing(GetArg[f, Callable, 1])
594+
assert t is int
595+
596+
416597
def test_eval_getarg_tuple():
417598
t = tuple[int, ...]
418599
args = eval_typing(GetArg[t, tuple, 1])

typemap/type_eval/_eval_operators.py

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ def _callable_type_to_signature(callable_type: object) -> inspect.Signature:
335335
elif "keyword" in quals:
336336
kind = inspect.Parameter.KEYWORD_ONLY
337337
saw_keyword_only = True
338-
elif name is None:
338+
elif "positional" in quals or name is None:
339339
kind = inspect.Parameter.POSITIONAL_ONLY
340340
elif saw_keyword_only:
341341
kind = inspect.Parameter.KEYWORD_ONLY
@@ -395,8 +395,10 @@ def fn(*args, **kwargs):
395395

396396
def _is_pos_only(param):
397397
name, _, quals = typing.get_args(param)
398-
name = _from_literal(name)
399-
return name is None and not (_get_quals(quals) & {"*", "**"})
398+
qual_set = _get_quals(quals)
399+
return "positional" in qual_set or (
400+
name is None and not (_get_quals(quals) & {"*", "**"})
401+
)
400402

401403

402404
def _callable_type_to_method(name, typ):
@@ -419,16 +421,9 @@ def _callable_type_to_method(name, typ):
419421
cls, params, ret = typing.get_args(typ)
420422
# We have to make class positional only if there is some other
421423
# positional only argument. Annoying!
422-
pname = (
423-
"cls"
424-
if not any(_is_pos_only(p) for p in typing.get_args(params))
425-
else None
426-
)
427-
cls_param = Param[
428-
typing.Literal[pname],
429-
type[cls],
430-
typing.Never,
431-
]
424+
has_pos_only = any(_is_pos_only(p) for p in typing.get_args(params))
425+
quals = typing.Literal["positional"] if has_pos_only else typing.Never
426+
cls_param = Param[typing.Literal["cls"], type[cls], quals]
432427
typ = typing.Callable[[cls_param] + list(typing.get_args(params)), ret]
433428
elif head is staticmethod:
434429
params, ret = typing.get_args(typ)
@@ -485,22 +480,20 @@ def _ann(x):
485480
else:
486481
specified_receiver = ann
487482

488-
has_name = p.kind in (
489-
inspect.Parameter.POSITIONAL_OR_KEYWORD,
490-
inspect.Parameter.KEYWORD_ONLY,
491-
)
492483
quals = []
493484
if p.kind == inspect.Parameter.VAR_POSITIONAL:
494485
quals.append("*")
495486
if p.kind == inspect.Parameter.VAR_KEYWORD:
496487
quals.append("**")
497488
if p.kind == inspect.Parameter.KEYWORD_ONLY:
498489
quals.append("keyword")
490+
if p.kind == inspect.Parameter.POSITIONAL_ONLY:
491+
quals.append("positional")
499492
if p.default is not empty:
500493
quals.append("default")
501494
params.append(
502495
Param[
503-
typing.Literal[p.name if has_name else None],
496+
typing.Literal[p.name],
504497
_ann(ann),
505498
typing.Literal[*quals] if quals else typing.Never,
506499
]
@@ -594,7 +587,11 @@ def _fix_callable_args(base, args):
594587
if typing.get_origin(special) is tuple:
595588
args[idx] = tuple[
596589
*[
597-
t if isinstance(t, Param) else Param[typing.Literal[None], t]
590+
(
591+
t
592+
if typing.get_origin(t) is Param
593+
else Param[typing.Literal[None], t]
594+
)
598595
for t in typing.get_args(special)
599596
]
600597
]

typemap/typing.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ class Member[
9292
definer: D
9393

9494

95-
ParamQuals = Literal["*", "**", "keyword", "default"]
95+
ParamQuals = Literal["*", "**", "keyword", "positional", "default"]
9696

9797

9898
class Param[N: str | None, T, Q: ParamQuals = typing.Never]:
@@ -101,8 +101,10 @@ class Param[N: str | None, T, Q: ParamQuals = typing.Never]:
101101
quals: Q
102102

103103

104-
type PosParam[T] = Param[Literal[None], T]
105-
type PosDefaultParam[T] = Param[Literal[None], T, Literal["default"]]
104+
type PosParam[N: str | None, T] = Param[N, T, Literal["positional"]]
105+
type PosDefaultParam[N: str | None, T] = Param[
106+
N, T, Literal["positional", "default"]
107+
]
106108
type DefaultParam[N: str, T] = Param[N, T, Literal["default"]]
107109
type NamedParam[N: str, T] = Param[N, T, Literal["keyword"]]
108110
type NamedDefaultParam[N: str, T] = Param[N, T, Literal["keyword", "default"]]

0 commit comments

Comments
 (0)