Skip to content

Commit

Permalink
Omit no-member error if guarded behind if stmt (pylint-dev#4764)
Browse files Browse the repository at this point in the history
* Omit no-member error if guarded behind if stmt

Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
  • Loading branch information
cdce8p and Pierre-Sassoulas authored Jul 28, 2021
1 parent ae6cbd1 commit 107f59e
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 0 deletions.
7 changes: 7 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,18 @@ Release date: TBA

Close #4120

* Don't emit ``no-member`` error if guarded behind if statement.

Ref #1162
Closes #1990
Closes #4168

* The default for ``PYLINTHOME`` is now the standard ``XDG_CACHE_HOME``, and pylint now uses ``appdirs``.

Closes #3878



What's New in Pylint 2.9.6?
===========================
Release date: 2021-07-28
Expand Down
7 changes: 7 additions & 0 deletions doc/whatsnew/2.10.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,12 @@ Other Changes
* Performance of the Similarity checker has been improved.

* Added ``time.clock`` to deprecated functions/methods for python 3.3

* Added ``ignored-parents`` option to the design checker to ignore specific
classes from the ``too-many-ancestors`` check (R0901).

* Don't emit ``no-member`` error if guarded behind if statement.

Ref #1162
Closes #1990
Closes #4168
25 changes: 25 additions & 0 deletions pylint/checkers/typecheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ def _emit_no_member(node, owner, owner_name, ignored_mixins=True, ignored_none=T
* the owner is a class and the name can be found in its metaclass.
* The access node is protected by an except handler, which handles
AttributeError, Exception or bare except.
* The node is guarded behind and `IF` or `IFExp` node
"""
# pylint: disable=too-many-return-statements
if node_ignores_exception(node, AttributeError):
Expand Down Expand Up @@ -522,6 +523,30 @@ def _emit_no_member(node, owner, owner_name, ignored_mixins=True, ignored_none=T
# Avoid false positive on Enum.__members__.{items(), values, keys}
# See https://github.com/PyCQA/pylint/issues/4123
return False
# Don't emit no-member if guarded behind `IF` or `IFExp`
# * Walk up recursively until if statement is found.
# * Check if condition can be inferred as `Const`,
# would evaluate as `False`,
# and wheater the node is part of the `body`.
# * Continue checking until scope of node is reached.
scope: astroid.NodeNG = node.scope()
node_origin: astroid.NodeNG = node
parent: astroid.NodeNG = node.parent
while parent != scope:
if isinstance(parent, (astroid.If, astroid.IfExp)):
inferred = safe_infer(parent.test)
if ( # pylint: disable=too-many-boolean-expressions
isinstance(inferred, astroid.Const)
and inferred.bool_value() is False
and (
isinstance(parent, astroid.If)
and node_origin in parent.body
or isinstance(parent, astroid.IfExp)
and node_origin == parent.body
)
):
return False
node_origin, parent = parent, parent.parent

return True

Expand Down
79 changes: 79 additions & 0 deletions tests/functional/n/no/no_member_if_statements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# pylint: disable=invalid-name,missing-docstring,pointless-statement
from datetime import datetime
from typing import Union

value = "Hello World"
value.isoformat() # [no-member]


if isinstance(value, datetime):
value.isoformat()
else:
value.isoformat() # [no-member]


def func():
if hasattr(value, "load"):
value.load()

if getattr(value, "load", None):
value.load


if value.none_existent(): # [no-member]
pass

res = value.isoformat() if isinstance(value, datetime) else value


class Base:
_attr_state: Union[str, datetime] = "Unknown"

@property
def state(self) -> Union[str, datetime]:
return self._attr_state

def some_function(self) -> str:
state = self.state
if isinstance(state, datetime):
return state.isoformat()
return str(state)


# https://github.com/PyCQA/pylint/issues/1990
# Attribute access after 'isinstance' should not cause 'no-member' error
import subprocess # pylint: disable=wrong-import-position # noqa: E402

try:
subprocess.check_call(['ls', '-']) # Deliberately made error in this line
except Exception as err:
if isinstance(err, subprocess.CalledProcessError):
print(f'Subprocess error occured. Return code: {err.returncode}')
else:
print(f'An error occured: {str(err)}')
raise


# https://github.com/PyCQA/pylint/issues/4168
# 'encode' for 'arg' should not cause 'no-member' error
mixed_tuple = (b"a", b"b", b"c", b"d")
byte_tuple = [arg.encode('utf8') if isinstance(arg, str) else arg for arg in mixed_tuple]

for arg in mixed_tuple:
if isinstance(arg, str):
print(arg.encode('utf8'))
else:
print(arg)


# https://github.com/PyCQA/pylint/issues/1162
# Attribute access after 'isinstance' should not cause 'no-member' error
class FoobarException(Exception):
foobar = None

try: # noqa: E305
pass
except Exception as ex:
if isinstance(ex, FoobarException):
ex.foobar
raise
3 changes: 3 additions & 0 deletions tests/functional/n/no/no_member_if_statements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
no-member:6:0::Instance of 'str' has no 'isoformat' member:INFERENCE
no-member:12:4::Instance of 'str' has no 'isoformat' member:INFERENCE
no-member:23:3::Instance of 'str' has no 'none_existent' member:INFERENCE

0 comments on commit 107f59e

Please sign in to comment.