Skip to content

Commit 7d7beca

Browse files
authored
Merge pull request #620 from jorenham/infer
`optype infer` 🧙
2 parents db974ec + d1f3274 commit 7d7beca

10 files changed

Lines changed: 1388 additions & 1 deletion

File tree

docs/index.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,15 @@ def twice[R](x: RMul2[R]) -> R:
6565
return 2 * x
6666
```
6767

68+
You don't have to work this out by hand. The experimental
69+
[`optype infer`](reference/experimental/infer.md) command derives the same signature
70+
straight from the implementation:
71+
72+
```console
73+
$ optype infer "lambda x: 2 * x"
74+
[R](x: CanRMul[Literal[2], R]) -> R
75+
```
76+
6877
See the [Getting Started](getting-started.md) guide for more detailed examples.
6978

7079
## Next Steps
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
---
2+
status: new
3+
tags:
4+
- experimental
5+
- v0.18+
6+
---
7+
8+
# optype.infer
9+
10+
!!! warning "Experimental"
11+
12+
The `optype.infer` module is experimental and its API may change without notice.
13+
14+
`optype.infer` works out which `optype` protocols a function requires of its parameters.
15+
It runs the function against recording proxies that trace every operation, then renders
16+
the result as a [PEP 695](https://peps.python.org/pep-0695/) signature.
17+
18+
## Usage
19+
20+
`infer(func, *params)` returns the inferred signature as a string:
21+
22+
```pycon
23+
>>> from optype.infer import infer
24+
>>> infer(lambda x: x + 1)
25+
'[R](x: CanAdd[Literal[1], R]) -> R'
26+
>>> infer(list)
27+
'[R](iterable: CanIter[CanNext[R]] & CanLen) -> list[R]'
28+
```
29+
30+
Pass parameter names or positions to report only those parameters:
31+
32+
```pycon
33+
>>> infer(lambda x, y: x[y], "x")
34+
'[T, R](x: CanGetitem[T, R]) -> R'
35+
```
36+
37+
The `optype infer` command takes a Python expression; leading statements are allowed, as
38+
long as the last line is an expression:
39+
40+
```console
41+
$ optype infer "lambda x: x * 2"
42+
[R](x: CanMul[Literal[2], R]) -> R
43+
44+
$ optype infer "import math; math.sqrt"
45+
(x: CanFloat | CanIndex) -> float
46+
```
47+
48+
## Overloads
49+
50+
A binary operator can dispatch to either operand, so it is reported as one overload per
51+
line:
52+
53+
```console
54+
$ optype infer "lambda x, y: x * y"
55+
[T, R](x: CanMul[T, R], y: T) -> R
56+
[T, R](x: T, y: CanRMul[T, R]) -> R
57+
```
58+
59+
An operator applied to its own result yields a recursive bound, where the bound of `T`
60+
refers back to `T`:
61+
62+
```console
63+
$ optype infer "lambda x: -x + x"
64+
[T: CanNeg[CanAdd[T, R]], R](x: T) -> R
65+
[T, R](x: CanNeg[T] & CanRAdd[T, R]) -> R
66+
```
67+
68+
## Branches
69+
70+
Both sides of a conditional are explored, so the parameter has to satisfy every branch
71+
(an intersection) and the return is the union of the branch results:
72+
73+
```console
74+
$ optype infer "lambda x: x if x > 0 else -x"
75+
[T: CanGt[Literal[0], CanBool] & CanNeg[R], R](x: T) -> T | R
76+
```
77+
78+
Branching and overloads combine:
79+
80+
```console
81+
$ optype infer "lambda x, y: (x + y) if x else y"
82+
[T, R](x: CanBool & CanAdd[T, R], y: T) -> R | T
83+
[T: CanBool, U: CanRAdd[T, R], R](x: T, y: U) -> R | U
84+
```
85+
86+
## Limitations
87+
88+
`infer` calls the function, so it only works on functions that are safe to run with
89+
placeholder arguments (no real side effects, no reliance on concrete values).
90+
91+
It can only observe operations that go through a dunder method. Anything that inspects a
92+
parameter at the C level is invisible, so a parameter passed to `type()`, `id()`,
93+
`isinstance()`, or an identity check (`is`) is reported as `object` rather than its real
94+
requirement.
95+
96+
!!! warning "Generic bounds"
97+
98+
An inferred typevar bound can itself be generic, such as `[T: CanAdd[T, R], R]` where
99+
`T`'s bound references `T` and `R`. Python's type system does not currently support
100+
generic typevar bounds, so these signatures are not always valid Python.

optype/__main__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""Command-line entry point: ``optype infer EXPR [PARAM ...]``."""
2+
3+
import sys
4+
5+
from optype.infer._cli import run
6+
7+
8+
def main() -> None:
9+
match sys.argv[1:]:
10+
case ["infer", *args]:
11+
run(args)
12+
case _:
13+
sys.exit("usage: optype infer EXPR [PARAM ...]")
14+
15+
16+
if __name__ == "__main__":
17+
main()

0 commit comments

Comments
 (0)