Repository: PyCQA/pyflakes

Issue:
Error AttributeError: 'ClassScope' object has no attribute 'indirect_assignments' encountered on `3.3.1`
Hello,
When running flake8 `7.2.0` with pyflake `3.3.1`, we encountered the following errors
```
flake8 --version
7.2.0 (mccabe: 0.7.0, pycodestyle: 2.13.0, pyflakes: 3.3.1) CPython 3.11.9 on
Linux
+ flake8 -v DEPRECATED tests
flake8.checker            MainProcess    121 INFO     Making checkers
flake8.checker            ForkPoolWorker-1   1373 CRITICAL Plugin pyflakes[F] raised an unexpected exception
Traceback (most recent call last):
  File "/code/venv/lib/python3.11/site-packages/flake8/checker.py", line 342, in run_check
    return plugin.obj(**arguments, **params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/code/venv/lib/python3.11/site-packages/flake8/plugins/pyflakes.py", line 73, in __init__
    super().__init__(
  File "/code/venv/lib/python3.11/site-packages/pyflakes/checker.py", line 757, in __init__
    self.handleChildren(tree)
  File "/code/venv/lib/python3.11/site-packages/pyflakes/checker.py", line 1222, in handleChildren
    self.handleNode(node, tree)
  File "/code/venv/lib/python3.11/site-packages/pyflakes/checker.py", line 1270, in handleNode
    handler(node)
  File "/code/venv/lib/python3.11/site-packages/pyflakes/checker.py", line 2010, in CLASSDEF
    self.handleNode(stmt, node)
  File "/code/venv/lib/python3.11/site-packages/pyflakes/checker.py", line 1270, in handleNode
    handler(node)
  File "/code/venv/lib/python3.11/site-packages/pyflakes/checker.py", line 1855, in GLOBAL
    self.scope.indirect_assignments[node_name] = node
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'ClassScope' object has no attribute 'indirect_assignments'
flake8.main.application   MainProcess   1385 INFO     Finished running
flake8.main.application   MainProcess   1385 INFO     Reporting errors
flake8.main.application   MainProcess   1385 INFO     Found a total of 0 violations and reported 0
```
It seems caused by the change in this commit https://github.com/PyCQA/pyflakes/commit/cadcd60a70c118c1d8b6dc8c09e53dc8f32b1666
As only `FunctionScope` has the attr `indirect_assignments`.

Discussion:
@Huevos: Same problem here:

```
flake8.checker            MainProcess    193 INFO     Making checkers
flake8.checker            ForkPoolWorker-3   4357 CRITICAL Plugin pyflakes[F] raised an unexpected exception
Traceback (most recent call last):
  File "/opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/flake8/checker.py", line 342, in run_check
    return plugin.obj(**arguments, **params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/flake8/plugins/pyflakes.py", line 73, in __init__
    super().__init__(
  File "/opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/pyflakes/checker.py", line 757, in __init__
    self.handleChildren(tree)
  File "/opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/pyflakes/checker.py", line 1222, in handleChildren
    self.handleNode(node, tree)
  File "/opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/pyflakes/checker.py", line 1270, in handleNode
    handler(node)
  File "/opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/pyflakes/checker.py", line 2010, in CLASSDEF
    self.handleNode(stmt, node)
  File "/opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/pyflakes/checker.py", line 1270, in handleNode
    handler(node)
  File "/opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/pyflakes/checker.py", line 1855, in GLOBAL
    self.scope.indirect_assignments[node_name] = node
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'ClassScope' object has no attribute 'indirect_assignments'
flake8.main.application   MainProcess   4385 INFO     Finished running
flake8.main.application   MainProcess   4385 INFO     Reporting errors
flake8.main.application   MainProcess   4385 INFO     Found a total of 0 violations and reported 0
./lib/python/Screens/OScamInfo.py: "pyflakes[F]" failed during execution due to AttributeError("'ClassScope' object has no attribute 'indirect_assignments'")
```

https://github.com/OpenViX/enigma2/actions/runs/14169818833/job/39690925504@sigmavirus24: Can either of you provide a minimal snippet that triggers this behavior? @Huevos: This is the job: https://github.com/OpenViX/enigma2/blob/Developer/.github/workflows/enigma2.yml#L53-L65


`./lib/python/Screens/OScamInfo.py: "pyflakes[F]" failed during execution due to AttributeError("'ClassScope' object has no attribute 'indirect_assignments'")`

That file is this: https://github.com/OpenViX/enigma2/blob/Developer/lib/python/Screens/OScamInfo.py@asottile: ah I thought I checked that `global ` / `nonlocal` was illegal in a class body -- but I must have checked module scope instead.  should be an easy fix...

Solution patch:
diff --git a/pyflakes/checker.py b/pyflakes/checker.py
index 9888d7ac..0c96af3b 100644
--- a/pyflakes/checker.py
+++ b/pyflakes/checker.py
@@ -526,7 +526,10 @@ def __repr__(self):
 
 
 class ClassScope(Scope):
-    pass
+    def __init__(self):
+        super().__init__()
+        # {name: node}
+        self.indirect_assignments = {}
 
 
 class FunctionScope(Scope):
@@ -566,9 +569,6 @@ def unused_annotations(self):
             if not binding.used and isinstance(binding, Annotation):
                 yield name, binding
 
-    def unused_indirect_assignments(self):
-        return self.indirect_assignments.items()
-
 
 class TypeScope(Scope):
     pass
@@ -835,6 +835,10 @@ def checkDeadScopes(self):
         which were imported but unused.
         """
         for scope in self.deadScopes:
+            if isinstance(scope, (ClassScope, FunctionScope)):
+                for name, node in scope.indirect_assignments.items():
+                    self.report(messages.UnusedIndirectAssignment, node, name)
+
             # imports in classes are public members
             if isinstance(scope, ClassScope):
                 continue
@@ -844,8 +848,6 @@ def checkDeadScopes(self):
                     self.report(messages.UnusedVariable, binding.source, name)
                 for name, binding in scope.unused_annotations():
                     self.report(messages.UnusedAnnotation, binding.source, name)
-                for name, node in scope.unused_indirect_assignments():
-                    self.report(messages.UnusedIndirectAssignment, node, name)
 
             all_binding = scope.get('__all__')
             if all_binding and not isinstance(all_binding, ExportBinding):
@@ -988,7 +990,7 @@ def addBinding(self, node, value):
                     self.report(messages.RedefinedWhileUnused,
                                 node, value.name, existing.source)
 
-                if isinstance(scope, FunctionScope):
+                if isinstance(scope, (ClassScope, FunctionScope)):
                     scope.indirect_assignments.pop(value.name, None)
 
             elif isinstance(existing, Importation) and value.redefines(existing):
@@ -1191,7 +1193,7 @@ def on_conditional_branch():
             # be executed.
             return
 
-        if isinstance(self.scope, FunctionScope):
+        if isinstance(self.scope, (ClassScope, FunctionScope)):
             self.scope.indirect_assignments.pop(name, None)
 
         if isinstance(self.scope, FunctionScope) and name in self.scope.globals:


Test patch:
diff --git a/pyflakes/test/test_other.py b/pyflakes/test/test_other.py
index 23990820..fcf95572 100644
--- a/pyflakes/test/test_other.py
+++ b/pyflakes/test/test_other.py
@@ -1121,6 +1121,36 @@ def f2():
                 g = 2
         ''', m.UnusedIndirectAssignment, m.UnusedVariable)
 
+    def test_global_nonlocal_in_class_bodies(self):
+        self.flakes('''
+        g = 0
+        class C:
+            global g
+            g = 1
+        def f():
+            nl = 0
+            class C:
+                nonlocal nl
+                nl = 1
+        ''')
+
+    def test_unused_global_in_class(self):
+        self.flakes('''
+        g = 0
+        class C:
+            global g
+            u = g
+        ''', m.UnusedIndirectAssignment)
+
+    def test_unused_nonlocal_in_clas(self):
+        self.flakes('''
+        def f():
+            nl = 1
+            class C:
+                nonlocal nl
+                u = nl
+        ''', m.UnusedIndirectAssignment)
+
     def test_function_arguments(self):
         """
         Test to traverse ARG and ARGUMENT handler