pylint-dev / pylint

It's not just a linter that annoys you!
https://pylint.readthedocs.io/en/latest/
GNU General Public License v2.0
5.2k stars 1.1k forks source link

`not-callable` false positive for class #8138

Open emontnemery opened 1 year ago

emontnemery commented 1 year ago

Bug description

Pylint complains about func.myfunc in the snippet below not being callable.

# pylint: disable=invalid-name, missing-class-docstring, missing-function-docstring, missing-module-docstring, too-few-public-methods

from typing import TYPE_CHECKING, Any, Type, TypeVar

_T = TypeVar("_T", bound=Any)

class myfunc:
    def __init__(self, *args: Any) -> None:
        pass

class _FunctionGenerator:
    if TYPE_CHECKING:
        @property
        def myfunc(self) -> Type[myfunc]:
            ...

func = _FunctionGenerator()

func.myfunc(1, 2, 3)

A concrete use case is in https://github.com/sqlalchemy/sqlalchemy/issues/9189

Configuration

No response

Command used

pylint test4b.py

Pylint output

************* Module test4b
test4b.py:19:0: E1102: func.myfunc is not callable (not-callable)

Expected behavior

Pylint should not complain about func.myfunc not being callable

Pylint version

pylint 2.16.0b1
astroid 2.13.3
Python 3.10.4 (main, Dec  5 2022, 21:19:56) [GCC 9.4.0]

OS / Environment

Ubuntu

Additional dependencies

No response

mbyrnepr2 commented 1 year ago

Thanks @emontnemery for the report. I can reproduce this using the example over in the sqlalchemy issue (copy/paste below). The example in the description doesn't run successfully as a Python script however.

from sqlalchemy import func, select, Integer
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.sql.selectable import Select

class Base(DeclarativeBase):
    pass

class MyTable(Base):
    __tablename__ = "host_entry"

    id = mapped_column(Integer, primary_key=True)

def state_attrs_exist(attr: int | None) -> Select:
    """Check if a state attributes id exists in the states table."""
    return select(func.min(MyTable.id)).where(MyTable.id == attr)
emontnemery commented 1 year ago

The example in the description doesn't run successfully as a Python script however.

Sorry, I did not know that was a requirement. Should I edit the example?

mbyrnepr2 commented 1 year ago

The example in the description doesn't run successfully as a Python script however.

Sorry, I did not know that was a requirement. Should I edit the example?

No worries! Its handy for us if the example is working code so that we can focus on only the reported false positive. If you have time to edit it then please feel free but otherwise the other example should be sufficient I think. I mentioned it originally just as a heads-up and to clarify to any future readers.

cdcadman commented 1 year ago

Here is a working script. The last line produces an error message in pylint, but only if the sqlalchemy version is at least 2.0. C:\temp\sa_func_count.py:19:31: E1102: sa.func.count is not callable (not-callable)

import sqlalchemy as sa

metadata = sa.MetaData()

user = sa.Table(
    "user",
    metadata,
    sa.Column("user_id", sa.Integer, primary_key=True),
    sa.Column("user_name", sa.String(16), nullable=False),
    sa.Column("email_address", sa.String(60)),
    sa.Column("nickname", sa.String(50), nullable=False),
)

db_uri = "sqlite:///:memory:"
engine = sa.create_engine(db_uri)
with engine.connect() as conn:
    user.create(bind=conn)
    assert (
        conn.execute(sa.select(sa.func.count()).select_from(user)).fetchall()[0][0] == 0
    )
andreehultgren commented 4 months ago

+1

luciidlou commented 3 weeks ago

bump

cdcadman commented 3 weeks ago

In the if TYPE_CHECKING block, the myfunc property is set to None, which is not callable. So the underlying issue is that pylint is using the if TYPE_CHECKING block to infer the type, but it is not using the type hint provided there (see discussion of type hint vs. inference here: https://github.com/pylint-dev/pylint/issues/4813). Type inference comes from the astroid package, and I don't see a way to make it ignore type checking blocks.

Here is a modification of the original script which runs in python, but outputs the not-callable false positive:

# pylint: disable=invalid-name, missing-class-docstring, missing-function-docstring, missing-module-docstring, too-few-public-methods
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Type

class myfunc:
    def __init__(self, *args: Any) -> None:
        pass

class _FunctionGenerator:
    def __getattr__(self, name: str) -> _FunctionGenerator:
        return _FunctionGenerator()

    def __call__(self, *args: Any) -> None:
        pass

    if TYPE_CHECKING:

        @property
        def myfunc(self) -> Type[myfunc]: ...

func = _FunctionGenerator()

func.myfunc(1, 2, 3)

If you modify the property definition to return something that is callable, then pylint does not output not-callable:

# pylint: disable=invalid-name, missing-class-docstring, missing-function-docstring, missing-module-docstring, too-few-public-methods
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Type

class myfunc:
    def __init__(self, *args: Any) -> None:
        pass

class _FunctionGenerator:
    def __getattr__(self, name: str) -> _FunctionGenerator:
        return _FunctionGenerator()

    def __call__(self, *args: Any) -> None:
        pass

    if TYPE_CHECKING:

        @property
        def myfunc(self) -> Type[myfunc]:
            return myfunc

func = _FunctionGenerator()

func.myfunc(1, 2, 3)

Removing the if TYPE_CHECKING block also makes the not-callable output go away.