diff --git a/sample_code/ep574/README.md b/sample_code/ep574/README.md new file mode 100755 index 0000000..c3d15e7 --- /dev/null +++ b/sample_code/ep574/README.md @@ -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 +``` diff --git a/sample_code/ep574/rev01/t.py b/sample_code/ep574/rev01/t.py new file mode 100755 index 0000000..611f461 --- /dev/null +++ b/sample_code/ep574/rev01/t.py @@ -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() diff --git a/sample_code/ep574/rev02/t.py b/sample_code/ep574/rev02/t.py new file mode 100755 index 0000000..781e783 --- /dev/null +++ b/sample_code/ep574/rev02/t.py @@ -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()) diff --git a/sample_code/ep574/rev03/t.py b/sample_code/ep574/rev03/t.py new file mode 100755 index 0000000..c207966 --- /dev/null +++ b/sample_code/ep574/rev03/t.py @@ -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)) diff --git a/sample_code/ep574/rev04/_lazy_plugin.py b/sample_code/ep574/rev04/_lazy_plugin.py new file mode 100644 index 0000000..9200402 --- /dev/null +++ b/sample_code/ep574/rev04/_lazy_plugin.py @@ -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 diff --git a/sample_code/ep574/rev04/lazy.py b/sample_code/ep574/rev04/lazy.py new file mode 100755 index 0000000..01d21ec --- /dev/null +++ b/sample_code/ep574/rev04/lazy.py @@ -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)) diff --git a/sample_code/ep574/rev04/mypy.ini b/sample_code/ep574/rev04/mypy.ini new file mode 100644 index 0000000..b594696 --- /dev/null +++ b/sample_code/ep574/rev04/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +plugins = _lazy_plugin diff --git a/sample_code/ep574/rev05/_lazy_plugin.py b/sample_code/ep574/rev05/_lazy_plugin.py new file mode 100755 index 0000000..f97373b --- /dev/null +++ b/sample_code/ep574/rev05/_lazy_plugin.py @@ -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 diff --git a/sample_code/ep574/rev05/lazy.py b/sample_code/ep574/rev05/lazy.py new file mode 100755 index 0000000..01d21ec --- /dev/null +++ b/sample_code/ep574/rev05/lazy.py @@ -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)) diff --git a/sample_code/ep574/rev05/mypy.ini b/sample_code/ep574/rev05/mypy.ini new file mode 100644 index 0000000..b594696 --- /dev/null +++ b/sample_code/ep574/rev05/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +plugins = _lazy_plugin diff --git a/sample_code/ep574/rev06/_lazy_plugin.py b/sample_code/ep574/rev06/_lazy_plugin.py new file mode 100755 index 0000000..c790933 --- /dev/null +++ b/sample_code/ep574/rev06/_lazy_plugin.py @@ -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 diff --git a/sample_code/ep574/rev06/lazy.py b/sample_code/ep574/rev06/lazy.py new file mode 100755 index 0000000..c421c3b --- /dev/null +++ b/sample_code/ep574/rev06/lazy.py @@ -0,0 +1,49 @@ +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)) +reveal_type(lazy.some_unknown()) diff --git a/sample_code/ep574/rev06/mypy.ini b/sample_code/ep574/rev06/mypy.ini new file mode 100644 index 0000000..b594696 --- /dev/null +++ b/sample_code/ep574/rev06/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +plugins = _lazy_plugin diff --git a/sample_code/ep574/rev07/_lazy_plugin.py b/sample_code/ep574/rev07/_lazy_plugin.py new file mode 100755 index 0000000..1174f07 --- /dev/null +++ b/sample_code/ep574/rev07/_lazy_plugin.py @@ -0,0 +1,39 @@ +import functools +from collections.abc import Callable + +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: + 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 diff --git a/sample_code/ep574/rev07/lazy.py b/sample_code/ep574/rev07/lazy.py new file mode 100755 index 0000000..c421c3b --- /dev/null +++ b/sample_code/ep574/rev07/lazy.py @@ -0,0 +1,49 @@ +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)) +reveal_type(lazy.some_unknown()) diff --git a/sample_code/ep574/rev07/mypy.ini b/sample_code/ep574/rev07/mypy.ini new file mode 100755 index 0000000..b594696 --- /dev/null +++ b/sample_code/ep574/rev07/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +plugins = _lazy_plugin diff --git a/setup.cfg b/setup.cfg index 799cf2a..d47503e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,3 +56,5 @@ per-file-ignores = */ep559/rev04/t.py:E999 */ep561/rev*/t*.py:E701,F401,F811 */ep573/rev0*/t.py:F821 + */ep574/rev0*/t.py:F821 + */ep574/rev0*/lazy.py:F821