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.32k stars 1.14k forks source link

False positive E1126 (invalid-sequence-index) on generic type alias with forward ref #9908

Open dairiki opened 2 months ago

dairiki commented 2 months ago

Bug description

# pylint: disable=missing-docstring
from typing import TypeAlias, TypeVar

T = TypeVar("T")

Alias1: TypeAlias = list[T]
Alias2: TypeAlias = "list[T]"

x1: Alias1[int]  # ok
x2: Alias2[int]  # <= triggers E1126 (invalid-sequence-index)

Configuration

No response

Command used

pylint --rcfile /dev/null test.py

Pylint output

************* Module test
test.py:10:4: E1126: Sequence index is not an int, slice, or instance with __index__ (invalid-sequence-index)

------------------------------------------------------------------
Your code has been rated at 1.67/10 (previous run: 1.67/10, +0.00)

Expected behavior

Should not emit warning.

I've also tested with pylint from the head of the main branch (eb33f8a6c0d).

Pylint version

pylint 3.2.7
astroid 3.2.4
Python 3.12.5 (main, Aug  7 2024, 10:47:46) [GCC 11.4.0]

OS / Environment

Ubuntu 22.04

Additional dependencies

No response

nickdrozd commented 2 months ago

Running this code raises a type error:

python3 asdf.py

Traceback (most recent call last):
  File "/pylint/asdf.py", line 9, in <module>
    x2: Alias2[int]  # <= triggers E1126 (invalid-sequence-index)
        ~~~~~~^^^^^
TypeError: string indices must be integers, not 'type'

So this looks like a true positive, i.e. Pylint is behaving correctly by warning about an actual error.

dairiki commented 2 months ago

Running this code raises a type error: So this looks like a true positive, i.e. Pylint is behaving correctly by warning about an actual error.

My apologies, you're right. I oversimplified my failure test case. It fails without PEP563 deferred evaluation of annotations. (I.e. it needs a from __future__ import annotations.)

Try this one:

# pylint: disable=missing-docstring
from __future__ import annotations

from typing import TypeAlias, TypeVar

T = TypeVar("T")

Alias1: TypeAlias = list[T]
Alias2: TypeAlias = "list[T]"

x1: Alias1[int]  # ok
x2: Alias2[int]  # <= triggers E1126 (invalid-sequence-index)

To add motivation as to why one might want to do this, here's a slightly less contrived example that fails pylint validation the same way:

# pylint: disable=missing-docstring,too-few-public-methods
from __future__ import annotations

from typing import Generic, TypeAlias, TypeVar

T = TypeVar("T")

MaybeWrapped: TypeAlias = "Wrapped[T] | T"

class Wrapped(Generic[T]):
    value: T

    def __init__(self, v: MaybeWrapped[T], /) -> None:  # <= triggers E1126
        self.value = v.value if isinstance(v, Wrapped) else v
nickdrozd commented 2 months ago

Thanks, I see the problem now. Looks like Pylint thinks the TypeAlias is just a string? TBH I don't understand how the string-as-annotation system works. Mypy accepts it fine.

As a workaround, you could work around the bug by moving MaybeWrapped after Wrapped and not using string-as-annotation:

from __future__ import annotations

from typing import Generic, TypeAlias, TypeVar

T = TypeVar("T")

class Wrapped(Generic[T]):
    value: T

    def __init__(self, v: MaybeWrapped[T], /) -> None:  # <= triggers E1126
        self.value = v.value if isinstance(v, Wrapped) else v

MaybeWrapped: TypeAlias = Wrapped[T] | T

Or in 3.12, you can avoid all that mess by using the type keyword:

from __future__ import annotations

type MaybeWrapped[T] = Wrapped[T] | T

class Wrapped[T]:
    value: T

    def __init__(self, v: MaybeWrapped[T], /) -> None:
        self.value = v.value if isinstance(v, Wrapped) else v