Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions sample_code/ep574/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# [typing the untype-able with mypy plugins (advanced)](https://youtu.be/tH3Nul6jDQM)

today I show an approach to make mypy understand a very dynamic pattern with a plugin!

## Interactive examples

### Python debugger (pdb)

```
ctx
attr

ctx.type
attr

find_member
help(find_member)

import inspect
inspect.getfullargspec(find_member)

find_member.__doc__
n
find_member(attr, generic_type, generic_type)

ret = find_member(attr, generic_type, generic_type)
dir(ret)
```

### Bash

```bash
python3 t.py

virtualenv venv
. venv/bin/activate
pip install mypy

mypy t.py

python -m mypy lazy.py
python -m mypy lazy.py --show-traceback
```
30 changes: 30 additions & 0 deletions sample_code/ep574/rev01/t.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
empty = object()


class Lazy:
def __init__(self, f):
self._f = f
self._inst = empty

def __getattr__(self, attr):
if self._inst is empty:
self._inst = self._f()

return getattr(self._inst, attr)


class C:
def __init__(self):
print('expensive!')

def foo(self):
print('foo() was called')


def make_c():
return C()


lazy = Lazy(make_c)
lazy.foo()
lazy.foo()
43 changes: 43 additions & 0 deletions sample_code/ep574/rev02/t.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from collections.abc import Callable
from typing import Any, Generic, TypeVar

empty = object()
R = TypeVar('R')


class Lazy(Generic[R]):
def __init__(self, f: Callable[[], R]) -> None:
self._f = f
self._inst = empty

def lazy_func(self) -> str:
return 'hello hello'

def __getattr__(self, attr: str) -> Any:
if self._inst is empty:
self._inst = self._f()

return getattr(self._inst, attr)


class C:
def __init__(self) -> None:
print('expensive!')

def foo(self) -> None:
print('foo() was called')

def f(self) -> int:
return 4


def make_c() -> C:
return C()


lazy = Lazy(make_c)
lazy.foo()
lazy.foo()

reveal_type(lazy.lazy_func())
reveal_type(lazy.f())
45 changes: 45 additions & 0 deletions sample_code/ep574/rev03/t.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from collections.abc import Callable
from typing import Any, Generic, TypeVar

empty = object()
R = TypeVar('R')


class Lazy(Generic[R]):
def __init__(self, f: Callable[[], R]) -> None:
self._f = f
self._inst = empty

def lazy_func(self) -> str:
return 'hello hello'

def __getattr__(self, attr: str) -> Any:
if self._inst is empty:
self._inst = self._f()

return getattr(self._inst, attr)


class C:
def __init__(self) -> None:
print('expensive!')

def foo(self) -> None:
print('foo() was called')

def f(self, x: int) -> int:
return 4


def make_c() -> C:
return C()


lazy = Lazy(make_c)
reveal_type(lazy)

lazy.foo()
lazy.foo()

reveal_type(lazy.lazy_func())
reveal_type(lazy.f(2))
27 changes: 27 additions & 0 deletions sample_code/ep574/rev04/_lazy_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import functools
from collections.abc import Callable

from mypy.plugin import AttributeContext, Plugin
from mypy.types import Type


def _lazy_attribute(ctx: AttributeContext, *, attr: str) -> Type:
breakpoint()
...


class MyPlugin(Plugin):
def get_attribute_hook(
self,
fullname: str,
) -> Callable[[AttributeContext], Type] | None:
# fullname: lazy.Lazy.foo
if fullname.startswith('lazy.Lazy.'):
_, _, attr = fullname.rpartition('.')
return functools.partial(_lazy_attribute, attr=attr)
else:
return None


def plugin(version: str) -> type[MyPlugin]:
return MyPlugin
48 changes: 48 additions & 0 deletions sample_code/ep574/rev04/lazy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import enum

from collections.abc import Callable
from typing import Any, Generic, TypeVar

_EmptyType = enum.Enum('_EmptyType', 'EMPTY')
empty = _EmptyType.EMPTY
R = TypeVar('R')


class Lazy(Generic[R]):
def __init__(self, f: Callable[[], R]) -> None:
self._f = f
self._inst: _EmptyType | R = empty

def lazy_func(self) -> str:
return 'hello hello'

def __getattr__(self, attr: str) -> Any:
if self._inst is empty:
self._inst = self._f()

return getattr(self._inst, attr)


class C:
def __init__(self) -> None:
print('expensive!')

def foo(self) -> None:
print('foo() was called')

def f(self, x: int) -> int:
return 4


def make_c() -> C:
return C()


lazy = Lazy(make_c)
reveal_type(lazy)

lazy.foo()
lazy.foo()

reveal_type(lazy.lazy_func())
reveal_type(lazy.f(2))
2 changes: 2 additions & 0 deletions sample_code/ep574/rev04/mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[mypy]
plugins = _lazy_plugin
34 changes: 34 additions & 0 deletions sample_code/ep574/rev05/_lazy_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import functools
from collections.abc import Callable

from mypy.plugin import AttributeContext, Plugin
from mypy.types import AnyType, Instance, Type


def _lazy_attribute(ctx: AttributeContext, *, attr: str) -> Type:
if not isinstance(ctx.default_attr_type, AnyType):
return ctx.default_attr_type

assert isinstance(ctx.type, Instance), ctx.type
assert len(ctx.type.args) == 1, ctx.type
assert isinstance(ctx.type.args[0], Instance), ctx.type
breakpoint()

return ctx.default_attr_type


class MyPlugin(Plugin):
def get_attribute_hook(
self,
fullname: str,
) -> Callable[[AttributeContext], Type] | None:
# fullname: lazy.Lazy.foo
if fullname.startswith('lazy.Lazy.'):
_, _, attr = fullname.rpartition('.')
return functools.partial(_lazy_attribute, attr=attr)
else:
return None


def plugin(version: str) -> type[MyPlugin]:
return MyPlugin
48 changes: 48 additions & 0 deletions sample_code/ep574/rev05/lazy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import enum

from collections.abc import Callable
from typing import Any, Generic, TypeVar

_EmptyType = enum.Enum('_EmptyType', 'EMPTY')
empty = _EmptyType.EMPTY
R = TypeVar('R')


class Lazy(Generic[R]):
def __init__(self, f: Callable[[], R]) -> None:
self._f = f
self._inst: _EmptyType | R = empty

def lazy_func(self) -> str:
return 'hello hello'

def __getattr__(self, attr: str) -> Any:
if self._inst is empty:
self._inst = self._f()

return getattr(self._inst, attr)


class C:
def __init__(self) -> None:
print('expensive!')

def foo(self) -> None:
print('foo() was called')

def f(self, x: int) -> int:
return 4


def make_c() -> C:
return C()


lazy = Lazy(make_c)
reveal_type(lazy)

lazy.foo()
lazy.foo()

reveal_type(lazy.lazy_func())
reveal_type(lazy.f(2))
2 changes: 2 additions & 0 deletions sample_code/ep574/rev05/mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[mypy]
plugins = _lazy_plugin
47 changes: 47 additions & 0 deletions sample_code/ep574/rev06/_lazy_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import functools
from collections.abc import Callable

from mypy.errorcodes import ATTR_DEFINED
from mypy.messages import format_type
from mypy.plugin import AttributeContext, Plugin
from mypy.subtypes import find_member
from mypy.types import AnyType, Instance, Type


def _lazy_attribute(ctx: AttributeContext, *, attr: str) -> Type:
if not isinstance(ctx.default_attr_type, AnyType):
return ctx.default_attr_type

assert isinstance(ctx.type, Instance), ctx.type
assert len(ctx.type.args) == 1, ctx.type
assert isinstance(ctx.type.args[0], Instance), ctx.type
generic_type = ctx.type.args[0]

member = find_member(attr, generic_type, generic_type)
if member is None:
ctx.api.fail(
f'{format_type(ctx.type, ctx.api.options)}'
f' has no attribute "{attr}"',
ctx.context,
code=ATTR_DEFINED,
)
return ctx.default_attr_type
else:
return member


class MyPlugin(Plugin):
def get_attribute_hook(
self,
fullname: str,
) -> Callable[[AttributeContext], Type] | None:
# fullname: lazy.Lazy.foo
if fullname.startswith('lazy.Lazy.'):
_, _, attr = fullname.rpartition('.')
return functools.partial(_lazy_attribute, attr=attr)
else:
return None


def plugin(version: str) -> type[MyPlugin]:
return MyPlugin
Loading