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: unsupported-membership-test #3045

Open larsch opened 5 years ago

larsch commented 5 years ago

Steps to reproduce

# pylint: disable=missing-docstring,too-few-public-methods

class MyClass:
    _all = None

    @classmethod
    def all(cls):
        if not cls._all:
            cls._all = find_all()
        return cls._all

    @classmethod
    def exist(cls, number):
        return number in cls.all()

def find_all():
    return [1, 2, 3]

if __name__ == '__main__':
    assert MyClass.exist(2)

Current behavior

************* Module clsmemb
clsmemb.py:14:25: E1135: Value 'cls.all()' doesn't support membership test (unsupported-membership-test)

Expected behavior

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

pylint --version output

pylint 2.3.1
astroid 2.2.5
Python 3.7.3 (default, Apr 24 2019, 15:29:51) [MSC v.1915 64 bit (AMD64)]
PCManticore commented 5 years ago

This looks like an issue that could be solved with better control flow inference, so that pylint could know that all could be either None or some other iterable value.

ncoghlan commented 5 years ago

I'm not sure if it's the same issue or a separate one, but I'm also getting this false positive with subprocess.check_output on pylint 2.3.1 and astroid 2.2.5 (old Python version, though - 3.5.3 on Debian 9):

import subprocess

bytes_data = subprocess.check_output(['echo', 'hello']);
text_data = subprocess.check_output(['echo', 'hello'], universal_newlines=True);

print(b'bytes' in bytes_data) # Prints False
print('text' in text_data)    # Prints False
'text' in bytes_data   # Runtime TypeError
b'bytes' in text_data  # Runtime TypeError
$ pylint -E check_pylint_e1135.py
************* Module check_pylint_e1135
check_pylint_e1135.py:6:18: E1135: Value 'bytes_data' doesn't support membership test (unsupported-membership-test)
check_pylint_e1135.py:7:16: E1135: Value 'text_data' doesn't support membership test (unsupported-membership-test)
check_pylint_e1135.py:8:10: E1135: Value 'bytes_data' doesn't support membership test (unsupported-membership-test)
check_pylint_e1135.py:9:12: E1135: Value 'text_data' doesn't support membership test (unsupported-membership-test)
PCManticore commented 5 years ago

Thanks for that report @ncoghlan It's probably a separate issue which we can resolve easier with a brain transform https://github.com/PyCQA/astroid/tree/master/astroid/brain so it's probably better to move the report to a separate issue.

tuukkamustonen commented 5 years ago

Also bumped into this. Even simpler example to reproduce:

from typing import Optional, Tuple

prop = None  # type: Optional[Tuple]

exists = prop and 'a' in prop

Out:

violation.py:5:27: E1135: Value 'prop' doesn't support membership test (unsupported-membership-test)
Jamie- commented 4 years ago

Equally adapting the code from the example above also generates a false positive for unsubscriptable-object in both Python 2, and Python 3 on latest version.

Python 2 & Pylint 1.6.5:

from typing import Optional

prop = None  # type: Optional[dict]

if prop and 'a' in prop:
    thing = prop['a']

Python 3 & Pylint 2.5.3:

from typing import Optional

prop: Optional[dict] = None

if prop and 'a' in prop:
    thing = prop['a']
wamserma commented 3 years ago

It might be worth to add that the pylint-issues reported from these minimal examples might be caused by the fixed assignment of None, in which case the type of prop can be inferred as NoneType and hence the warnings are correct.

from secrets import choice
from typing import Optional, Tuple

prop = choice([None, (None, )])  # type: Optional[Tuple]

exists = prop and 'a' in prop

does not produce a pylint issue.

Edit: For those landing here from a search engine: The issue from the initial example can be avoided by initializing _all = [] and avoiding None if there is no need to distinguish between the two values.

tibbe commented 1 year ago

Another example, this time with a loop and TypedDict:

from typing import Optional, TypedDict

class ADict(TypedDict):
    a: int

x: Optional[ADict] = None
while x is None or 'a' in x:
    print('test')
    # `x` gets assigned in the loop.
akusei commented 1 year ago

Also bumped into this. Even simpler example to reproduce:

from typing import Optional, Tuple

prop = None  # type: Optional[Tuple]

exists = prop and 'a' in prop

Out:

violation.py:5:27: E1135: Value 'prop' doesn't support membership test (unsupported-membership-test)

I'm having this exact same problem for both unsupported-assignment-operation and unsupported-membership-test. Has there been any movement on getting this fixed? Looks to be a pretty old bug/FP

hepcat72 commented 1 year ago

I believe my issue is the same. I'm running into it here:

I get the error from line 7 in this abbreviated code...

class MaintainedModel(Model):
    class_label_filters: Optional[List[str]] = None
    def __init__(self, *args, **kwargs):
        self.label_filters = self.class_label_filters

    def update_decorated_fields(self):
        if self.label_filters is not None and "name" in self.label_filters:
            ...

Is there some way I should change my code to prevent the error? Funny thing is, I wasn't getting the error until I made an innocuous refactor of the code (to change global_label_filters to a class attribute (class_label_filters) with a type hint. There are other such variables without the error and the code works as intended...

sidmitra commented 3 months ago

Another recent example

from pydantic.v1.dataclasses import BaseModel, Field

class Foo(BaseModel):
    my_list: list[str] = Field(default_factory=list)

The above started failing after a un-related refactor on pydantic v2 from my_list: list[str] = [] to use default_factory instead(to avoid a mutable default list)

'my_list' doesn't support membership test (unsupported-membership-test)

The type hints still specify it as a list.

davidgnx commented 2 months ago

Hi All! I ran into this (or something very similar) yesterday, when I tried to perform lazy initialization on some dicts. Here is a simplified example, I guess it is the same issue.

Example code:

# pylint: disable=C0114, C0115, R0903

####
# Case 1 (no class involved, works).
da: dict[str, str] | None = None # None at the beginning

da = {'a': 'Hello'} # Lazy initialization on demand

if 'a' in da: # OK
    print(da['a']) # OK
da['a'] = "Hi" # OK

####
# Case 2 (class involved, error).
class MyClass:
    db: dict[str, str] | None = None # None at the beginning

MyClass.db = {'b': 'World'} # Lazy initialization on demand

if 'b' in MyClass.db: # E1135
    print(MyClass.db['b']) # E1136
MyClass.db['b'] = 'America' # E1137
####

pylint result:

example.py:20:10: E1135: Value 'MyClass.db' doesn't support membership test (unsupported-membership-test)
example.py:21:10: E1136: Value 'MyClass.db' is unsubscriptable (unsubscriptable-object)
example.py:22:0: E1137: 'MyClass.db' does not support item assignment (unsupported-assignment-operation)

pylint version (running on Ubuntu 22.04 LTS 64-bit):

pylint 3.2.7
astroid 3.2.4
Python 3.10.12 (main, Jul 29 2024, 16:56:48) [GCC 11.4.0]

The two main points of this example are probably:

  1. As @hepcat72 noted, class vs. no class does seem to make a difference in some (or all?) cases.
  2. The issue is not just about E1135, other error codes are also involved.

To answer your question @hepcat72 (I know it's fairly outdated, but may still be relevant to you or to others): The way I see it now, it is a pylint issue, I'd leave the code as is and wait for the pylint fix to arrive.

If this is the same issue (is it?), should we update the title? This is not just about unsupported membership test (E1135) then...