From b0164a65543bbdfa4014992b18920c3b9fe4538f Mon Sep 17 00:00:00 2001
From: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Date: Sun, 1 Aug 2021 17:33:04 +0200
Subject: [PATCH] PR 4783 - nested attributes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Squashed commit of the following:
commit 49c4bbaa82189c4bc90245063db22c4c635a0973
Author: Yu Shao, Pang
Date: Sun Aug 1 19:56:51 2021 +0800
Fix crash for `unused-private-member` when there are nested attributes
commit 2ad824799c112336afa7da17217d563402b418b7
Merge: 8ceb26dc 1d09bc89
Author: yushao2 <36848472+yushao2@users.noreply.github.com>
Date: Sun Aug 1 20:13:05 2021 +0800
Merge pull request #4709 from yushao2/fix-unused-private-member-4673
[unused-private-member] add logic for checking nested functions
commit 1d09bc89fd5bac7e230170a63e35d0d4e08675e6
Author: Yu Shao, Pang
Date: Sun Aug 1 20:03:42 2021 +0800
update pr based on review
commit a4198cd4429bab1e9eed3a62146f155e4acdf1bc
Author: Yu Shao, Pang
Date: Sun Aug 1 19:21:36 2021 +0800
Update pr based on review
commit c8b2cbbe379eb1337f059c27e48349e645f88399
Author: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Sun Jul 25 05:20:42 2021 +0000
[pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
commit 4ffea0b03f6d9e9feb25585c84b3ba227e53cb05
Author: Yu Shao, Pang
Date: Sun Jul 25 13:12:29 2021 +0800
Update pr based on review
commit e4d624346b1be8ae0600a226d282c806cc130b1d
Author: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Date: Sun Jul 18 17:23:31 2021 +0200
Remove empty line
commit cce5833f31d5d0412bce26010835e863a10820c5
Author: Yu Shao, Pang
Date: Sun Jul 18 22:40:54 2021 +0800
update PR based on comments
commit 712dc2b0b8128f3a56ecef8347cf23bdd538069a
Author: Yu Shao, Pang
Date: Wed Jul 14 16:42:25 2021 +0800
[unused-private-member] add logic for checking nested functions
also, improve error message for nested functions
commit 8ceb26dc66912e8f65adce0bc6ba22e87ceed3bb
Author: Michal Vasilek
Date: Sun Aug 1 08:14:58 2021 +0200
Fix IsADirectoryError in tests/lint/unittest_lint (#4781)
pylintd is a directory, so os.remove throws IsADirectoryError
commit a31e6bce1cc97611c4f58407bc613db42eb174c7
Author: Pierre Sassoulas
Date: Sat Jul 31 11:21:46 2021 +0200
Add documentation for useless-suppression
Closes #4757
commit b71be8a07ecc7d8aafbc07733e458b338eef6155
Author: Mark Byrne <31762852+mbyrnepr2@users.noreply.github.com>
Date: Fri Jul 30 20:21:02 2021 +0200
Handle has-a relationships for type-hinted arguments in class diagrams (#4745)
* Pyreverse - Show class has-a relationships inferred from type-hints
Closes #4744
Co-authored-by: Pierre Sassoulas
commit 5e5f48d05fb6f1322a7420d6ea7586966b08f80a
Author: Daniƫl van Noord <13665637+DanielNoord@users.noreply.github.com>
Date: Thu Jul 29 23:44:30 2021 +0200
Add ``forgotten-debug-statement`` checker (#4771)
* Add ``no-breakpoint`` checker this adds a checker for calls to ``breakpoint()``, ``pdb.set_trace()``, or ``sys.breakpointhook()``.
Closes #3692
Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Co-authored-by: Pierre Sassoulas
---
CONTRIBUTORS.txt | 2 +
ChangeLog | 16 ++++++
doc/user_guide/message-control.rst | 8 +++
doc/whatsnew/2.10.rst | 6 ++
pylint/checkers/classes.py | 24 ++++++--
pylint/checkers/stdlib.py | 10 ++++
pylint/pyreverse/utils.py | 6 +-
tests/data/classes_No_Name.dot | 12 ++--
tests/data/clientmodule_test.py | 5 +-
tests/data/suppliermodule_test.py | 2 +
.../f/forgotten_debug_statement_py37.py | 10 ++++
.../f/forgotten_debug_statement_py37.rc | 2 +
.../f/forgotten_debug_statement_py37.txt | 4 ++
.../u/unused/unused_private_member.py | 57 +++++++++++++++++++
.../u/unused/unused_private_member.txt | 3 +
tests/lint/unittest_lint.py | 2 +-
tests/unittest_pyreverse_diadefs.py | 4 ++
tests/unittest_pyreverse_inspector.py | 4 +-
tests/unittest_pyreverse_writer.py | 18 ++++++
19 files changed, 178 insertions(+), 17 deletions(-)
create mode 100644 tests/functional/f/forgotten_debug_statement_py37.py
create mode 100644 tests/functional/f/forgotten_debug_statement_py37.rc
create mode 100644 tests/functional/f/forgotten_debug_statement_py37.txt
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index d19b12c4727..65cba0af15e 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -527,3 +527,5 @@ contributors:
* Eisuke Kawashima (e-kwsm): contributor
* Daniel van Noord (DanielNoord): contributor
+
+* Michal Vasilek: contributor
diff --git a/ChangeLog b/ChangeLog
index b8dce9a625b..8cf5d2a8c86 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -12,6 +12,10 @@ Release date: TBA
..
Put bug fixes that should not wait for a new minor version here
+* pyreverse: Show class has-a relationships inferred from the type-hint
+
+ Closes #4744
+
* Added ``ignored-parents`` option to the design checker to ignore specific
classes from the ``too-many-ancestors`` check (R0901).
@@ -48,6 +52,18 @@ Release date: TBA
* Extended ``consider-using-tuple`` check to cover ``in`` comparisons.
+* Added ``forgotten-debug-statement``: Emitted when ``breakpoint``, ``pdb.set_trace`` or ``sys.breakpointhook`` calls are found
+
+ Closes #3692
+
+* Fix false-positive of ``unused-private-member`` when using nested functions in a class
+
+ Closes #4673
+
+* Fix crash for ``unused-private-member`` that occurs when there is a presence of nested attributes
+
+ Closes #4755
+
What's New in Pylint 2.9.6?
===========================
diff --git a/doc/user_guide/message-control.rst b/doc/user_guide/message-control.rst
index 18c9b18a8dd..217b1b5560a 100644
--- a/doc/user_guide/message-control.rst
+++ b/doc/user_guide/message-control.rst
@@ -178,3 +178,11 @@ Here's an example with all these rules in a single place:
# no error
print(self.bla)
print(self.blop)
+
+
+Detecting useless disables
+--------------------------
+
+When pylint get better and false positives are removed,
+disable that became useless can accumulate and clutter the code.
+In order to clean them you can enable the ``useless-suppression`` warning.
diff --git a/doc/whatsnew/2.10.rst b/doc/whatsnew/2.10.rst
index 6bfea5b5f65..26b792a6ab9 100644
--- a/doc/whatsnew/2.10.rst
+++ b/doc/whatsnew/2.10.rst
@@ -24,6 +24,10 @@ New checkers
Closes #4365
+* Added ``forgotten-debug-statement``: Emitted when ``breakpoint``, ``pdb.set_trace`` or ``sys.breakpointhook`` calls are found
+
+ Closes #3692
+
Extensions
==========
@@ -36,6 +40,8 @@ Extensions
Other Changes
=============
+* Pyreverse - Show class has-a relationships inferred from type-hints
+
* Performance of the Similarity checker has been improved.
* Added ``time.clock`` to deprecated functions/methods for python 3.3
diff --git a/pylint/checkers/classes.py b/pylint/checkers/classes.py
index 65ebe3423c5..ddf5710ec09 100644
--- a/pylint/checkers/classes.py
+++ b/pylint/checkers/classes.py
@@ -909,6 +909,13 @@ def _check_unused_private_functions(self, node: astroid.ClassDef) -> None:
function_def = cast(astroid.FunctionDef, function_def)
if not is_attr_private(function_def.name):
continue
+ parent_scope = function_def.parent.scope()
+ if isinstance(parent_scope, astroid.FunctionDef):
+ # Handle nested functions
+ if function_def.name in (
+ n.name for n in parent_scope.nodes_of_class(astroid.Name)
+ ):
+ continue
for attribute in node.nodes_of_class(astroid.Attribute):
attribute = cast(astroid.Attribute, attribute)
if (
@@ -931,11 +938,19 @@ def _check_unused_private_functions(self, node: astroid.ClassDef) -> None:
):
break
else:
- function_repr = f"{function_def.name}({function_def.args.as_string()})"
+ name_stack = []
+ curr = parent_scope
+ # Generate proper names for nested functions
+ while curr != node:
+ name_stack.append(curr.name)
+ curr = curr.parent.scope()
+
+ outer_level_names = f"{'.'.join(reversed(name_stack))}"
+ function_repr = f"{outer_level_names}.{function_def.name}({function_def.args.as_string()})"
self.add_message(
"unused-private-member",
node=function_def,
- args=(node.name, function_repr),
+ args=(node.name, function_repr.lstrip(".")),
)
def _check_unused_private_variables(self, node: astroid.ClassDef) -> None:
@@ -959,9 +974,10 @@ def _check_unused_private_variables(self, node: astroid.ClassDef) -> None:
def _check_unused_private_attributes(self, node: astroid.ClassDef) -> None:
for assign_attr in node.nodes_of_class(astroid.AssignAttr):
- if not is_attr_private(assign_attr.attrname):
+ if not is_attr_private(assign_attr.attrname) or not isinstance(
+ assign_attr.expr, astroid.Name
+ ):
continue
-
# Logic for checking false positive when using __new__,
# Get the returned object names of the __new__ magic function
# Then check if the attribute was consumed in other instance methods
diff --git a/pylint/checkers/stdlib.py b/pylint/checkers/stdlib.py
index afc5faa80fb..7eabe9bb406 100644
--- a/pylint/checkers/stdlib.py
+++ b/pylint/checkers/stdlib.py
@@ -55,6 +55,7 @@
SUBPROCESS_POPEN = "subprocess.Popen"
SUBPROCESS_RUN = "subprocess.run"
OPEN_MODULE = "_io"
+DEBUG_BREAKPOINTS = ("builtins.breakpoint", "sys.breakpointhook", "pdb.set_trace")
DEPRECATED_MODULES = {
@@ -434,6 +435,12 @@ class StdlibChecker(DeprecatedMixin, BaseChecker):
"Using the system default implicitly can create problems on other operating systems. "
"See https://www.python.org/dev/peps/pep-0597/",
),
+ "W1515": (
+ "Leaving functions creating breakpoints in production code is not recommended",
+ "forgotten-debug-statement",
+ "Calls to breakpoint(), sys.breakpointhook() and pdb.set_trace() should be removed "
+ "from code that is not actively being debugged.",
+ ),
}
def __init__(self, linter=None):
@@ -495,6 +502,7 @@ def _check_shallow_copy_environ(self, node):
"subprocess-run-check",
"deprecated-class",
"unspecified-encoding",
+ "forgotten-debug-statement",
)
def visit_call(self, node):
"""Visit a Call node."""
@@ -531,6 +539,8 @@ def visit_call(self, node):
self._check_env_function(node, inferred)
elif name == SUBPROCESS_RUN:
self._check_for_check_kw_in_run(node)
+ elif name in DEBUG_BREAKPOINTS:
+ self.add_message("forgotten-debug-statement", node=node)
self.check_deprecated_method(node, inferred)
except astroid.InferenceError:
return
diff --git a/pylint/pyreverse/utils.py b/pylint/pyreverse/utils.py
index 06f8d3b7e2a..6eb9d43ccfb 100644
--- a/pylint/pyreverse/utils.py
+++ b/pylint/pyreverse/utils.py
@@ -269,9 +269,9 @@ def infer_node(node: Union[astroid.AssignAttr, astroid.AssignName]) -> set:
otherwise return a set of the inferred types using the NodeNG.infer method"""
ann = get_annotation(node)
- if ann:
- return {ann}
try:
+ if ann:
+ return set(ann.infer())
return set(node.infer())
except astroid.InferenceError:
- return set()
+ return {ann} if ann else set()
diff --git a/tests/data/classes_No_Name.dot b/tests/data/classes_No_Name.dot
index 8867b4e416b..c5eb70f0c45 100644
--- a/tests/data/classes_No_Name.dot
+++ b/tests/data/classes_No_Name.dot
@@ -3,10 +3,12 @@ charset="utf-8"
rankdir=BT
"0" [label="{Ancestor|attr : str\lcls_member\l|get_value()\lset_value(value)\l}", shape="record"];
"1" [label="{DoNothing|\l|}", shape="record"];
-"2" [label="{Interface|\l|get_value()\lset_value(value)\l}", shape="record"];
-"3" [label="{Specialization|TYPE : str\lrelation\ltop : str\l|}", shape="record"];
-"3" -> "0" [arrowhead="empty", arrowtail="none"];
-"0" -> "2" [arrowhead="empty", arrowtail="node", style="dashed"];
+"2" [label="{DoNothing2|\l|}", shape="record"];
+"3" [label="{Interface|\l|get_value()\lset_value(value)\l}", shape="record"];
+"4" [label="{Specialization|TYPE : str\lrelation\lrelation2\ltop : str\l|}", shape="record"];
+"4" -> "0" [arrowhead="empty", arrowtail="none"];
+"0" -> "3" [arrowhead="empty", arrowtail="node", style="dashed"];
"1" -> "0" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="cls_member", style="solid"];
-"1" -> "3" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="relation", style="solid"];
+"1" -> "4" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="relation", style="solid"];
+"2" -> "4" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="relation2", style="solid"];
}
diff --git a/tests/data/clientmodule_test.py b/tests/data/clientmodule_test.py
index 40db2e77efe..82deaaf6f86 100644
--- a/tests/data/clientmodule_test.py
+++ b/tests/data/clientmodule_test.py
@@ -1,5 +1,5 @@
""" docstring for file clientmodule.py """
-from data.suppliermodule_test import Interface, DoNothing
+from data.suppliermodule_test import Interface, DoNothing, DoNothing2
class Ancestor:
""" Ancestor method """
@@ -23,7 +23,8 @@ class Specialization(Ancestor):
TYPE = 'final class'
top = 'class'
- def __init__(self, value, _id):
+ def __init__(self, value, _id, relation2: DoNothing2):
Ancestor.__init__(self, value)
self._id = _id
self.relation = DoNothing()
+ self.relation2 = relation2
diff --git a/tests/data/suppliermodule_test.py b/tests/data/suppliermodule_test.py
index 24dc9a02fef..6af30fa08a7 100644
--- a/tests/data/suppliermodule_test.py
+++ b/tests/data/suppliermodule_test.py
@@ -8,3 +8,5 @@ def set_value(self, value):
raise NotImplementedError
class DoNothing: pass
+
+class DoNothing2: pass
diff --git a/tests/functional/f/forgotten_debug_statement_py37.py b/tests/functional/f/forgotten_debug_statement_py37.py
new file mode 100644
index 00000000000..ebe0a7a44d3
--- /dev/null
+++ b/tests/functional/f/forgotten_debug_statement_py37.py
@@ -0,0 +1,10 @@
+# pylint: disable=missing-docstring
+
+import pdb
+import sys
+
+breakpoint() # [forgotten-debug-statement]
+sys.breakpointhook() # [forgotten-debug-statement]
+pdb.set_trace() # [forgotten-debug-statement]
+b = breakpoint
+b() # [forgotten-debug-statement]
diff --git a/tests/functional/f/forgotten_debug_statement_py37.rc b/tests/functional/f/forgotten_debug_statement_py37.rc
new file mode 100644
index 00000000000..a17bb22dafb
--- /dev/null
+++ b/tests/functional/f/forgotten_debug_statement_py37.rc
@@ -0,0 +1,2 @@
+[testoptions]
+min_pyver=3.7
diff --git a/tests/functional/f/forgotten_debug_statement_py37.txt b/tests/functional/f/forgotten_debug_statement_py37.txt
new file mode 100644
index 00000000000..40e8914a678
--- /dev/null
+++ b/tests/functional/f/forgotten_debug_statement_py37.txt
@@ -0,0 +1,4 @@
+forgotten-debug-statement:6:0::"Leaving functions creating breakpoints in production code is not recommended"
+forgotten-debug-statement:7:0::"Leaving functions creating breakpoints in production code is not recommended"
+forgotten-debug-statement:8:0::"Leaving functions creating breakpoints in production code is not recommended"
+forgotten-debug-statement:10:0::"Leaving functions creating breakpoints in production code is not recommended"
diff --git a/tests/functional/u/unused/unused_private_member.py b/tests/functional/u/unused/unused_private_member.py
index 74dad6fe446..5aba1a9f7cc 100644
--- a/tests/functional/u/unused/unused_private_member.py
+++ b/tests/functional/u/unused/unused_private_member.py
@@ -160,3 +160,60 @@ def __new__(cls, func, *args):
def exec(self):
print(self.__secret_bool)
return self.func(*self.__args)
+
+
+# Test cases for false-positive reported in #4673
+# https://github.com/PyCQA/pylint/issues/4673
+class FalsePositive4673:
+ """ The testing class """
+
+ def __init__(self, in_thing):
+ self.thing = False
+ self.do_thing(in_thing)
+
+ def do_thing(self, in_thing):
+ """ Checks the false-positive condition, sets a property. """
+ def __false_positive(in_thing):
+ print(in_thing)
+
+ def __true_positive(in_thing): # [unused-private-member]
+ print(in_thing)
+
+ __false_positive(in_thing)
+ self.thing = True
+
+ def undo_thing(self):
+ """ Unsets a property. """
+ self.thing = False
+
+ def complicated_example(self, flag):
+ def __inner_1():
+ pass
+
+ def __inner_2():
+ pass
+
+ def __inner_3(fn):
+ return fn
+
+ def __inner_4(): # [unused-private-member]
+ pass
+
+ fn_to_return = __inner_1 if flag else __inner_3(__inner_2)
+ return fn_to_return
+
+
+# Test cases for crash reported in #4755
+# https://github.com/PyCQA/pylint/issues/4755
+class Crash4755Context:
+ def __init__(self):
+ self.__messages = None # [unused-private-member]
+
+class Crash4755Command:
+ def __init__(self):
+ self.context = Crash4755Context()
+
+ def method(self):
+ self.context.__messages = []
+ for message in self.context.__messages:
+ print(message)
diff --git a/tests/functional/u/unused/unused_private_member.txt b/tests/functional/u/unused/unused_private_member.txt
index b21806c2c9c..73ee18050d5 100644
--- a/tests/functional/u/unused/unused_private_member.txt
+++ b/tests/functional/u/unused/unused_private_member.txt
@@ -7,3 +7,6 @@ unused-private-member:34:4:HasUnusedInClass.__test:Unused private member `HasUnu
unused-private-member:55:4:HasUnusedInClass.__test_recursive:Unused private member `HasUnusedInClass.__test_recursive(self)`:HIGH
unused-private-member:132:8:FalsePositive4657.__init__:Unused private member `FalsePositive4657.__attr_c`:HIGH
undefined-variable:137:15:FalsePositive4657.attr_c:Undefined variable 'cls':HIGH
+unused-private-member:179:8:FalsePositive4673.do_thing.__true_positive:Unused private member `FalsePositive4673.do_thing.__true_positive(in_thing)`:HIGH
+unused-private-member:199:8:FalsePositive4673.complicated_example.__inner_4:Unused private member `FalsePositive4673.complicated_example.__inner_4()`:HIGH
+unused-private-member:210:8:Crash4755Context.__init__:Unused private member `Crash4755Context.__messages`:HIGH
diff --git a/tests/lint/unittest_lint.py b/tests/lint/unittest_lint.py
index cd4f5667bb7..20c8a750040 100644
--- a/tests/lint/unittest_lint.py
+++ b/tests/lint/unittest_lint.py
@@ -643,7 +643,7 @@ def test_pylint_home():
assert config.PYLINT_HOME == pylintd
finally:
try:
- os.remove(pylintd)
+ rmtree(pylintd)
except FileNotFoundError:
pass
finally:
diff --git a/tests/unittest_pyreverse_diadefs.py b/tests/unittest_pyreverse_diadefs.py
index fbcf7fae320..4355b703acd 100644
--- a/tests/unittest_pyreverse_diadefs.py
+++ b/tests/unittest_pyreverse_diadefs.py
@@ -99,6 +99,7 @@ class TestDefaultDiadefGenerator:
_should_rels = [
("association", "DoNothing", "Ancestor"),
("association", "DoNothing", "Specialization"),
+ ("association", "DoNothing2", "Specialization"),
("implements", "Ancestor", "Interface"),
("specialization", "Specialization", "Ancestor"),
]
@@ -142,6 +143,7 @@ def test_known_values1(HANDLER, PROJECT):
assert classes == [
(True, "Ancestor"),
(True, "DoNothing"),
+ (True, "DoNothing2"),
(True, "Interface"),
(True, "Specialization"),
]
@@ -170,6 +172,7 @@ def test_known_values3(HANDLER, PROJECT):
(True, "data.clientmodule_test.Ancestor"),
(True, special),
(True, "data.suppliermodule_test.DoNothing"),
+ (True, "data.suppliermodule_test.DoNothing2"),
]
@@ -184,6 +187,7 @@ def test_known_values4(HANDLER, PROJECT):
assert classes == [
(True, "Ancestor"),
(True, "DoNothing"),
+ (True, "DoNothing2"),
(True, "Specialization"),
]
diff --git a/tests/unittest_pyreverse_inspector.py b/tests/unittest_pyreverse_inspector.py
index bc37bea7139..a927e18ad96 100644
--- a/tests/unittest_pyreverse_inspector.py
+++ b/tests/unittest_pyreverse_inspector.py
@@ -62,9 +62,9 @@ def test_instance_attrs_resolution(project):
klass = project.get_module("data.clientmodule_test")["Specialization"]
assert hasattr(klass, "instance_attrs_type")
type_dict = klass.instance_attrs_type
- assert len(type_dict) == 2
+ assert len(type_dict) == 3
keys = sorted(type_dict.keys())
- assert keys == ["_id", "relation"]
+ assert keys == ["_id", "relation", "relation2"]
assert isinstance(type_dict["relation"][0], astroid.bases.Instance), type_dict[
"relation"
]
diff --git a/tests/unittest_pyreverse_writer.py b/tests/unittest_pyreverse_writer.py
index 9e6cb755bd1..8a33eb7a35f 100644
--- a/tests/unittest_pyreverse_writer.py
+++ b/tests/unittest_pyreverse_writer.py
@@ -204,3 +204,21 @@ def test_infer_node_2(mock_infer, mock_get_annotation):
mock_infer.return_value = "x"
assert infer_node(node) == set("x")
assert mock_infer.called
+
+
+def test_infer_node_3():
+ """Return a set containing an astroid.ClassDef object when the attribute
+ has a type annotation"""
+ node = astroid.extract_node(
+ """
+ class Component:
+ pass
+
+ class Composite:
+ def __init__(self, component: Component):
+ self.component = component
+ """
+ )
+ instance_attr = node.instance_attrs.get("component")[0]
+ assert isinstance(infer_node(instance_attr), set)
+ assert isinstance(infer_node(instance_attr).pop(), astroid.ClassDef)