python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.5k stars 2.83k forks source link

pyqtProperty setter always raises an error #9911

Open BoboTiG opened 3 years ago

BoboTiG commented 3 years ago

Bug Report

It seems that Mypy is not enjoying pyqtProperty setters. Someone already asked on the Qt mailing-list.

To Reproduce

from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal

class FeatureModel(QObject):

    stateChanged = pyqtSignal()

    def __init__(self, enabled: bool, /) -> None:
        super().__init__()
        self._enabled = enabled

    @pyqtProperty(bool, notify=stateChanged)
    def enabled(self):
        return self._enabled

    @enabled.setter
    def enabled(self, enabled):
        self._enabled = enabled
        self.stateChanged.emit()

Expected Behavior

Mypy should be happy.

Actual Behavior

$ python -m mypy file.py
file.py:16: error: Name 'enabled' already defined on line 12
Found 1 error in 1 file (checked 1 source file)

Your Environment

freundTech commented 3 years ago

On the Qt mailing-list someone already posted these two links to the mypy source-code: https://github.com/python/mypy/blob/v0.782/mypy/semanal.py#L964-L973 https://github.com/python/mypy/blob/v0.782/mypy/semanal.py#L842-L869

mypy has some special casing to make pythons @property (and a few other decorators) work. The easy solution would probably be to add a special case for @pyqtProperty. Cleaner solutions would be to either somehow make it possible to annotate that a decorator creates a property (This could be problematic, as I don't know if existing type annotations can handle this) or write a mypy plugin. I only took a quick look at the plugin interface and think it should be possible to do this. The problem here would be that the plugin API is unstable (See #6617).

EDIT: Looks like writing a plugin for this isn't that easy after all. There is a "get_class_decorator_hook", but no corresponding hook for methods and functions. If such hooks would be added writing a plugin would be possible.

gvanrossum commented 3 years ago

The way to do that without baking knowledge of qt into mypy is to write a mypy extension.

freundTech commented 3 years ago

Should the plugin hook I suggested in #9915 get added to mypy you could solve this using the following mypy plugin (function and class names subject to change):

from typing import Callable, Optional
from typing import Type

from mypy.plugin import (
    Plugin, DecoratorContext,
)

def plugin(version: str) -> Type[Plugin]:
    return PyQt5Plugin

class PyQt5Plugin(Plugin):
    """PyQt5 Plugin"""

    def get_decorator_hook(self, fullname: str
                           ) -> Optional[Callable[[DecoratorContext], None]]:
        if fullname == 'PyQt5.QtCore.pyqtProperty':
            return pyqt_property_callback
        return None

def pyqt_property_callback(ctx: DecoratorContext):
    ctx.decorator.func.is_property = True