Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyflakes/F841_0.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,39 @@ def f():
pass
except Exception as _:
pass


# OK, `__class__` in this case is not the special `__class__` cell, so we don't
# emit a diagnostic. (It has its own special semantics -- see
# https://github.com/astral-sh/ruff/pull/20048#discussion_r2298338048 -- but
# those aren't relevant here.)
class A:
__class__ = 1


# The following three cases are flagged because they declare local `__class__`
# variables that don't refer to the special `__class__` cell.
class A:
def set_class(self, cls):
__class__ = cls # F841


class A:
class B:
def set_class(self, cls):
__class__ = cls # F841


class A:
def foo():
class B:
print(__class__)
def set_class(self, cls):
__class__ = cls # F841


# OK, the `__class__` cell is nonlocal and declared as such.
class NonlocalDunderClass:
def foo():
nonlocal __class__
__class__ = 1
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,8 @@ def f():
def g():
nonlocal x
x = 2

# OK
class A:
def method(self):
nonlocal __class__
33 changes: 30 additions & 3 deletions crates/ruff_linter/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -703,7 +703,10 @@ impl SemanticSyntaxContext for Checker<'_> {
match scope.kind {
ScopeKind::Class(_) | ScopeKind::Lambda(_) => return false,
ScopeKind::Function(ast::StmtFunctionDef { is_async, .. }) => return *is_async,
ScopeKind::Generator { .. } | ScopeKind::Module | ScopeKind::Type => {}
ScopeKind::Generator { .. }
| ScopeKind::Module
| ScopeKind::Type
| ScopeKind::DunderClassCell => {}
}
}
false
Expand All @@ -714,7 +717,10 @@ impl SemanticSyntaxContext for Checker<'_> {
match scope.kind {
ScopeKind::Class(_) => return false,
ScopeKind::Function(_) | ScopeKind::Lambda(_) => return true,
ScopeKind::Generator { .. } | ScopeKind::Module | ScopeKind::Type => {}
ScopeKind::Generator { .. }
| ScopeKind::Module
| ScopeKind::Type
| ScopeKind::DunderClassCell => {}
}
}
false
Expand All @@ -725,7 +731,7 @@ impl SemanticSyntaxContext for Checker<'_> {
match scope.kind {
ScopeKind::Class(_) | ScopeKind::Generator { .. } => return false,
ScopeKind::Function(_) | ScopeKind::Lambda(_) => return true,
ScopeKind::Module | ScopeKind::Type => {}
ScopeKind::Module | ScopeKind::Type | ScopeKind::DunderClassCell => {}
}
}
false
Expand Down Expand Up @@ -1092,6 +1098,24 @@ impl<'a> Visitor<'a> for Checker<'a> {
}
}

// Here we add the implicit scope surrounding a method which allows code in the
// method to access `__class__` at runtime. See the `ScopeKind::DunderClassCell`
// docs for more information.
let added_dunder_class_scope = if self.semantic.current_scope().kind.is_class() {
self.semantic.push_scope(ScopeKind::DunderClassCell);
let binding_id = self.semantic.push_binding(
TextRange::default(),
BindingKind::DunderClassCell,
BindingFlags::empty(),
);
self.semantic
.current_scope_mut()
.add("__class__", binding_id);
true
} else {
false
};

self.semantic.push_scope(ScopeKind::Type);

if let Some(type_params) = type_params {
Expand Down Expand Up @@ -1155,6 +1179,9 @@ impl<'a> Visitor<'a> for Checker<'a> {
self.semantic.pop_scope(); // Function scope
self.semantic.pop_definition();
self.semantic.pop_scope(); // Type parameter scope
if added_dunder_class_scope {
self.semantic.pop_scope(); // `__class__` cell closure scope
}
self.add_binding(
name,
stmt.identifier(),
Expand Down
5 changes: 4 additions & 1 deletion crates/ruff_linter/src/renamer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,10 @@ impl Renamer {
))
}
// Avoid renaming builtins and other "special" bindings.
BindingKind::FutureImport | BindingKind::Builtin | BindingKind::Export(_) => None,
BindingKind::FutureImport
| BindingKind::Builtin
| BindingKind::Export(_)
| BindingKind::DunderClassCell => None,
// By default, replace the binding's name with the target name.
BindingKind::Annotation
| BindingKind::Argument
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/rules/pyflakes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,7 @@ mod tests {

/// A re-implementation of the Pyflakes test runner.
/// Note that all tests marked with `#[ignore]` should be considered TODOs.
#[track_caller]
fn flakes(contents: &str, expected: &[Rule]) {
let contents = dedent(contents);
let source_type = PySourceType::default();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,63 @@ F841 Local variable `value` is assigned to but never used
128 | print(key)
|
help: Remove assignment to unused variable `value`

F841 [*] Local variable `__class__` is assigned to but never used
--> F841_0.py:168:9
|
166 | class A:
167 | def set_class(self, cls):
168 | __class__ = cls # F841
| ^^^^^^^^^
|
help: Remove assignment to unused variable `__class__`

ℹ Unsafe fix
165 165 | # variables that don't refer to the special `__class__` cell.
166 166 | class A:
167 167 | def set_class(self, cls):
168 |- __class__ = cls # F841
168 |+ pass # F841
169 169 |
170 170 |
171 171 | class A:

F841 [*] Local variable `__class__` is assigned to but never used
--> F841_0.py:174:13
|
172 | class B:
173 | def set_class(self, cls):
174 | __class__ = cls # F841
| ^^^^^^^^^
|
help: Remove assignment to unused variable `__class__`

ℹ Unsafe fix
171 171 | class A:
172 172 | class B:
173 173 | def set_class(self, cls):
174 |- __class__ = cls # F841
174 |+ pass # F841
175 175 |
176 176 |
177 177 | class A:

F841 [*] Local variable `__class__` is assigned to but never used
--> F841_0.py:182:17
|
180 | print(__class__)
181 | def set_class(self, cls):
182 | __class__ = cls # F841
| ^^^^^^^^^
|
help: Remove assignment to unused variable `__class__`

ℹ Unsafe fix
179 179 | class B:
180 180 | print(__class__)
181 181 | def set_class(self, cls):
182 |- __class__ = cls # F841
182 |+ pass # F841
183 183 |
184 184 |
185 185 | # OK, the `__class__` cell is nonlocal and declared as such.
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,65 @@ help: Remove assignment to unused variable `_`
152 |- except Exception as _:
152 |+ except Exception:
153 153 | pass
154 154 |
155 155 |

F841 [*] Local variable `__class__` is assigned to but never used
--> F841_0.py:168:9
|
166 | class A:
167 | def set_class(self, cls):
168 | __class__ = cls # F841
| ^^^^^^^^^
|
help: Remove assignment to unused variable `__class__`

ℹ Unsafe fix
165 165 | # variables that don't refer to the special `__class__` cell.
166 166 | class A:
167 167 | def set_class(self, cls):
168 |- __class__ = cls # F841
168 |+ pass # F841
169 169 |
170 170 |
171 171 | class A:

F841 [*] Local variable `__class__` is assigned to but never used
--> F841_0.py:174:13
|
172 | class B:
173 | def set_class(self, cls):
174 | __class__ = cls # F841
| ^^^^^^^^^
|
help: Remove assignment to unused variable `__class__`

ℹ Unsafe fix
171 171 | class A:
172 172 | class B:
173 173 | def set_class(self, cls):
174 |- __class__ = cls # F841
174 |+ pass # F841
175 175 |
176 176 |
177 177 | class A:

F841 [*] Local variable `__class__` is assigned to but never used
--> F841_0.py:182:17
|
180 | print(__class__)
181 | def set_class(self, cls):
182 | __class__ = cls # F841
| ^^^^^^^^^
|
help: Remove assignment to unused variable `__class__`

ℹ Unsafe fix
179 179 | class B:
180 180 | print(__class__)
181 181 | def set_class(self, cls):
182 |- __class__ = cls # F841
182 |+ pass # F841
183 183 |
184 184 |
185 185 | # OK, the `__class__` cell is nonlocal and declared as such.
3 changes: 2 additions & 1 deletion crates/ruff_linter/src/rules/pylint/rules/non_ascii_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ pub(crate) fn non_ascii_name(checker: &Checker, binding: &Binding) {
| BindingKind::SubmoduleImport(_)
| BindingKind::Deletion
| BindingKind::ConditionalDeletion(_)
| BindingKind::UnboundException(_) => {
| BindingKind::UnboundException(_)
| BindingKind::DunderClassCell => {
return;
}
};
Expand Down
20 changes: 19 additions & 1 deletion crates/ruff_python_semantic/src/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ impl Ranged for Binding<'_> {
/// ID uniquely identifying a [Binding] in a program.
///
/// Using a `u32` to identify [Binding]s should be sufficient because Ruff only supports documents with a
/// size smaller than or equal to `u32::max`. A document with the size of `u32::max` must have fewer than `u32::max`
/// size smaller than or equal to `u32::MAX`. A document with the size of `u32::MAX` must have fewer than `u32::MAX`
/// bindings because bindings must be separated by whitespace (and have an assignment).
#[newtype_index]
pub struct BindingId;
Expand Down Expand Up @@ -672,6 +672,24 @@ pub enum BindingKind<'a> {
/// Stores the ID of the binding that was shadowed in the enclosing
/// scope, if any.
UnboundException(Option<BindingId>),

/// A binding to `__class__` in the implicit closure created around every method in a class
/// body, if any method refers to either `__class__` or `super`.
///
/// ```python
/// class C:
/// __class__ # NameError: name '__class__' is not defined
///
/// def f():
/// print(__class__) # allowed
///
/// def g():
/// nonlocal __class__ # also allowed because the scope is *not* the function scope
/// ```
///
/// See <https://docs.python.org/3/reference/datamodel.html#creating-the-class-object> for more
/// details.
DunderClassCell,
}

bitflags! {
Expand Down
Loading
Loading