|
| 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. |
0 commit comments