From 5daf59397f9c4b4569fa18b4fc891cd22f61ab00 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 28 Jul 2021 14:26:34 +0200 Subject: [PATCH 1/4] Omit no-member error if guarded behind if stmt --- ChangeLog | 6 ++++++ pylint/checkers/typecheck.py | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/ChangeLog b/ChangeLog index 65b03de5f6..1020826fe3 100644 --- a/ChangeLog +++ b/ChangeLog @@ -26,6 +26,12 @@ Release date: TBA Close #4120 +* Don't emit ``no-member`` error if guarded behind if statement. + + Ref #1162 + Closes #1990 + Closes #4168 + What's New in Pylint 2.9.6? =========================== diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index eab1a74276..360676b6e5 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -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): @@ -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 From a87200d6e2be881ff0629fa9a7e31c660ec028d6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 28 Jul 2021 17:40:37 +0200 Subject: [PATCH 2/4] Add tests --- .../n/no/no_member_if_statements.py | 78 +++++++++++++++++++ .../n/no/no_member_if_statements.txt | 3 + 2 files changed, 81 insertions(+) create mode 100644 tests/functional/n/no/no_member_if_statements.py create mode 100644 tests/functional/n/no/no_member_if_statements.txt diff --git a/tests/functional/n/no/no_member_if_statements.py b/tests/functional/n/no/no_member_if_statements.py new file mode 100644 index 0000000000..bdacc09081 --- /dev/null +++ b/tests/functional/n/no/no_member_if_statements.py @@ -0,0 +1,78 @@ +# pylint: disable=invalid-name,missing-docstring,pointless-statement +from datetime import datetime + +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: str | float | datetime = "Unknown" + + @property + def state(self) -> str | float | 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 diff --git a/tests/functional/n/no/no_member_if_statements.txt b/tests/functional/n/no/no_member_if_statements.txt new file mode 100644 index 0000000000..1c1d75d329 --- /dev/null +++ b/tests/functional/n/no/no_member_if_statements.txt @@ -0,0 +1,3 @@ +no-member:5:0::Instance of 'str' has no 'isoformat' member:INFERENCE +no-member:11:4::Instance of 'str' has no 'isoformat' member:INFERENCE +no-member:22:3::Instance of 'str' has no 'none_existent' member:INFERENCE From 79f6b7e74070080c1b845c972b3d7f5eecbd2a8d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 28 Jul 2021 18:17:22 +0200 Subject: [PATCH 3/4] Add entry to What's New --- doc/whatsnew/2.10.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/whatsnew/2.10.rst b/doc/whatsnew/2.10.rst index 8980b0f91e..7d7312153f 100644 --- a/doc/whatsnew/2.10.rst +++ b/doc/whatsnew/2.10.rst @@ -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 From b4cd3fd9939abb01482b75c3baf16860739527ea Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 28 Jul 2021 18:25:42 +0200 Subject: [PATCH 4/4] Fix tests --- tests/functional/n/no/no_member_if_statements.py | 5 +++-- tests/functional/n/no/no_member_if_statements.txt | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/functional/n/no/no_member_if_statements.py b/tests/functional/n/no/no_member_if_statements.py index bdacc09081..ad87e9950b 100644 --- a/tests/functional/n/no/no_member_if_statements.py +++ b/tests/functional/n/no/no_member_if_statements.py @@ -1,5 +1,6 @@ # pylint: disable=invalid-name,missing-docstring,pointless-statement from datetime import datetime +from typing import Union value = "Hello World" value.isoformat() # [no-member] @@ -26,10 +27,10 @@ def func(): class Base: - _attr_state: str | float | datetime = "Unknown" + _attr_state: Union[str, datetime] = "Unknown" @property - def state(self) -> str | float | datetime: + def state(self) -> Union[str, datetime]: return self._attr_state def some_function(self) -> str: diff --git a/tests/functional/n/no/no_member_if_statements.txt b/tests/functional/n/no/no_member_if_statements.txt index 1c1d75d329..2bc673b491 100644 --- a/tests/functional/n/no/no_member_if_statements.txt +++ b/tests/functional/n/no/no_member_if_statements.txt @@ -1,3 +1,3 @@ -no-member:5:0::Instance of 'str' has no 'isoformat' member:INFERENCE -no-member:11:4::Instance of 'str' has no 'isoformat' member:INFERENCE -no-member:22:3::Instance of 'str' has no 'none_existent' member:INFERENCE +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