Description
I'm trying to use mypy with Urwid, and ran into something I can't quite get to work. I'm not sure if this is a bug, a missing feature, or me expecting too much magic.
Urwid provides a hierarchy of Widget classes. One of them is WidgetWrap: it wraps a widget, exposes the wrapped widget as self._w
, and forwards the methods of the Widget base class to the wrapped widget. It is intended to be subclassed, with the subclass methods accessing the wrapped widget but keeping it hidden from other code.
I'm writing an application that uses Urwid, so I can't change Urwid itself.
With an urwid.pyi
of:
from typing import TypeVar, Generic, Any
class Widget: ...
class Text(Widget):
def __init__(self, markup: Any) -> None: ...
def set_text(self, text: str) -> None: ...
W = TypeVar('W', bound=Widget)
class WidgetWrap(Widget, Generic[W]):
def __init__(self, w: W) -> None: ...
_w: W
and some example code:
import typing
import urwid
class WrappedText(urwid.WidgetWrap):
def __init__(self):
# Using super() behaves the same way.
urwid.WidgetWrap.__init__(self, urwid.Text(''))
def update(self):
self._w.set_text('Ni!')
self._w.no_such_method()
raw_wrap = urwid.WidgetWrap(urwid.Text(''))
if typing.TYPE_CHECKING:
reveal_type(raw_wrap)
reveal_type(raw_wrap._w)
raw_wrap._w.set_text('This is ok.')
try:
raw_wrap._w.no_such_method()
except AttributeError:
pass
class_wrap = WrappedText()
if typing.TYPE_CHECKING:
reveal_type(class_wrap)
reveal_type(class_wrap._w)
class_wrap.update()
I want mypy to catch the two calls to no_such_method
.
Mypy 0.540 catches the first one, but not the second one:
wid.py:19: error: Revealed type is 'urwid.WidgetWrap[urwid.Text*]'
wid.py:20: error: Revealed type is 'urwid.Text*'
wid.py:24: error: "Text" has no attribute "no_such_method"
wid.py:31: error: Revealed type is 'wid.WrappedText'
wid.py:32: error: Revealed type is 'Any'
So when instantiating WidgetWrap directly, mypy correctly infers the type of _w
, and catches the call to a nonexistent method. But when subclassing WidgetWrap, the type for _w
is Any (this surprises me: I was at least expecting the bound of Widget to apply), so it cannot detect the subclass doing something wrong.
I tried to work around this by explicit use of Generic in my code, not just in the .pyi stub:
class WidgetWrap(urwid.WidgetWrap, Generic[W]):
def __init__(self, widget: W) -> None:
super().__init__(widget)
_w: W
class WrappedText(WidgetWrap[urwid.Text]):
# as before
This works great with mypy, but runs afoul of python/typing#449 (Urwid uses metaclasses internally, so mixing in Generic triggers a metaclass conflict).
I found a workaround for that, but it's a bit ugly:
if TYPE_CHECKING:
class WidgetWrap(urwid.WidgetWrap, Generic[W]):
# as in previous code snippet
else:
WidgetWrap = collections.defaultdict(lambda: urwid.WidgetWrap)
class WrappedText(WidgetWrap[urwid.Text]):
This works at runtime and seems to work with mypy.
Is there a neat way of dealing with this class I'm overlooking? And is the different behaviour from mypy when a subclass passes fixed arguments to the superclass __init__
versus directly instantiating the class expected?