From 59450e8a0c43def8bc619b23a1855c799382049e Mon Sep 17 00:00:00 2001 From: lsrafael13 Date: Fri, 30 May 2025 20:12:48 -0300 Subject: [PATCH 1/7] Prohibit assignment to __class__ inside instance methods Implements a check to disallow assignments to self.__class__, as these can cause unsafe runtime behavior. Includes tests for __class__ and related dunder attributes. Fixes #7724 --- mypy/checker.py | 3 +++ test-data/unit/check-assign-to-class.test | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 test-data/unit/check-assign-to-class.test diff --git a/mypy/checker.py b/mypy/checker.py index 9c389cccd95f..4ac64d625e4e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8880,6 +8880,9 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: self.lvalue = True for lv in s.lvalues: lv.accept(self) + if isinstance(lv, MemberExpr): + if lv.name == '__class__': + self.fail("Assignment to '__class__' is unsafe and not allowed", lv) self.lvalue = False def visit_name_expr(self, e: NameExpr) -> None: diff --git a/test-data/unit/check-assign-to-class.test b/test-data/unit/check-assign-to-class.test new file mode 100644 index 000000000000..53043c657e12 --- /dev/null +++ b/test-data/unit/check-assign-to-class.test @@ -0,0 +1,22 @@ +[case test_assign_to_dunder_class] +class A: + def foo(self): + self.__class__ = B # E: Assignment to '__class__' is unsafe and not allowed + +class B: + pass + +[case test_assign_to_other_dunder_attributes] +class C: + def bar(self): + self.__name__ = "NewName" # OK + self.__doc__ = "Test doc" # OK + +class D: + pass + +[case test_assign_to_regular_attribute] +class E: + x = 1 + def baz(self): + self.x = 2 # OK From 523a16849b61c967b17e3a7d49a82c2bc6d07d7a Mon Sep 17 00:00:00 2001 From: lsrafael13 Date: Fri, 30 May 2025 20:25:38 -0300 Subject: [PATCH 2/7] Trigger GitHub Actions for testing Adds a dummy comment to force GitHub Actions to run CI on the updated branch. --- test-data/unit/check-assign-to-class.test | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test-data/unit/check-assign-to-class.test b/test-data/unit/check-assign-to-class.test index 53043c657e12..c45e21ef4b9c 100644 --- a/test-data/unit/check-assign-to-class.test +++ b/test-data/unit/check-assign-to-class.test @@ -20,3 +20,5 @@ class E: x = 1 def baz(self): self.x = 2 # OK + +# Trigger GitHub Actions for testing \ No newline at end of file From fe381b2779bee981f2d895ca57547d3c6c238b3e Mon Sep 17 00:00:00 2001 From: lsrafael13 Date: Sun, 1 Jun 2025 15:52:25 -0300 Subject: [PATCH 3/7] Fix AttributeError in VarAssignVisitor and cleanup test file Move __class__ assignment check to use self.chk.fail() in VarAssignVisitor, fixing AttributeError. Also remove temporary test comment used for triggering GitHub Actions. --- mypy/checker.py | 2 +- test-data/unit/check-assign-to-class.test | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 4ac64d625e4e..cf0c929f2cac 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8882,7 +8882,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: lv.accept(self) if isinstance(lv, MemberExpr): if lv.name == '__class__': - self.fail("Assignment to '__class__' is unsafe and not allowed", lv) + self.chk.fail("Assignment to '__class__' is unsafe and not allowed", lv) self.lvalue = False def visit_name_expr(self, e: NameExpr) -> None: diff --git a/test-data/unit/check-assign-to-class.test b/test-data/unit/check-assign-to-class.test index c45e21ef4b9c..10191489b4ad 100644 --- a/test-data/unit/check-assign-to-class.test +++ b/test-data/unit/check-assign-to-class.test @@ -21,4 +21,3 @@ class E: def baz(self): self.x = 2 # OK -# Trigger GitHub Actions for testing \ No newline at end of file From 111a3c4ec60f6487e4247f61e8597d6befd528aa Mon Sep 17 00:00:00 2001 From: lsrafael13 Date: Sun, 1 Jun 2025 17:48:24 -0300 Subject: [PATCH 4/7] Fix assignment to __class__ check to use errors.report() in VarAssignVisitor (Fixes #7724) Replaces previous incorrect call to self.chk.fail() with self.errors.report(), resolving AttributeError. Ensures assignment to __class__ is properly reported as an error during instance attribute assignments. --- mypy/checker.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index cf0c929f2cac..da6f1d69ba88 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8882,7 +8882,11 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: lv.accept(self) if isinstance(lv, MemberExpr): if lv.name == '__class__': - self.chk.fail("Assignment to '__class__' is unsafe and not allowed", lv) + self.errors.report( + lv.line, + lv.column, + "Assignment to '__class__' is unsafe and not allowed" + ) self.lvalue = False def visit_name_expr(self, e: NameExpr) -> None: From cd6d90b5526522bec63fbb64a45734d36f7baa1c Mon Sep 17 00:00:00 2001 From: lsrafael13 Date: Mon, 2 Jun 2025 16:55:53 -0300 Subject: [PATCH 5/7] Disallow assignment to '__class__' in TypeChecker.visit_assignment_stmt Added a check inside TypeChecker.visit_assignment_stmt to disallow assignments to '__class__', as such assignments are unsafe and may lead to unpredictable behavior. The new check raises an error using self.fail when '__class__' is assigned, matching the style used in other TypeChecker checks. This change addresses issue #7724. --- mypy/checker.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index da6f1d69ba88..4ac64d625e4e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8882,11 +8882,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: lv.accept(self) if isinstance(lv, MemberExpr): if lv.name == '__class__': - self.errors.report( - lv.line, - lv.column, - "Assignment to '__class__' is unsafe and not allowed" - ) + self.fail("Assignment to '__class__' is unsafe and not allowed", lv) self.lvalue = False def visit_name_expr(self, e: NameExpr) -> None: From 78a6c991ab4b628718bc1097daf5a113b87bad48 Mon Sep 17 00:00:00 2001 From: lsrafael13 Date: Mon, 2 Jun 2025 20:14:22 -0300 Subject: [PATCH 6/7] Add __class__ assignment check to TypeChecker.visit_assignment_stmt Implemented the check inside TypeChecker.visit_assignment_stmt to disallow assignments to '__class__', as such assignments are unsafe. The check correctly uses self.fail() following mypy's error reporting conventions, and is located after initial assignment processing to align with the design of TypeChecker. Previous redundant logic inside VarAssignVisitor.visit_assignment_stmt has been removed. This change addresses issue #7724. --- mypy/checker.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 4ac64d625e4e..f28fdd17e00d 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3077,7 +3077,12 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: if not (s.is_alias_def and self.is_stub): with self.enter_final_context(s.is_final_def): self.check_assignment(s.lvalues[-1], s.rvalue, s.type is None, s.new_syntax) - + + for lv in s.lvalues: + if isinstance(lv, MemberExpr): + if lv.name == '__class__': + self.fail("Assignment to '__class__' is unsafe and not allowed", lv) + if s.is_alias_def: self.check_type_alias_rvalue(s) @@ -8880,9 +8885,6 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: self.lvalue = True for lv in s.lvalues: lv.accept(self) - if isinstance(lv, MemberExpr): - if lv.name == '__class__': - self.fail("Assignment to '__class__' is unsafe and not allowed", lv) self.lvalue = False def visit_name_expr(self, e: NameExpr) -> None: From 3a6b580ef14963c3f5f644aa83047e51091fdaf7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 23:18:01 +0000 Subject: [PATCH 7/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 6 +++--- test-data/unit/check-assign-to-class.test | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index f28fdd17e00d..3c132731d809 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3077,12 +3077,12 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: if not (s.is_alias_def and self.is_stub): with self.enter_final_context(s.is_final_def): self.check_assignment(s.lvalues[-1], s.rvalue, s.type is None, s.new_syntax) - + for lv in s.lvalues: if isinstance(lv, MemberExpr): - if lv.name == '__class__': + if lv.name == "__class__": self.fail("Assignment to '__class__' is unsafe and not allowed", lv) - + if s.is_alias_def: self.check_type_alias_rvalue(s) diff --git a/test-data/unit/check-assign-to-class.test b/test-data/unit/check-assign-to-class.test index 10191489b4ad..53043c657e12 100644 --- a/test-data/unit/check-assign-to-class.test +++ b/test-data/unit/check-assign-to-class.test @@ -20,4 +20,3 @@ class E: x = 1 def baz(self): self.x = 2 # OK -