Skip to content

Commit

Permalink
Use dynamic builtins list based on Python version
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Aug 30, 2024
1 parent 34b4732 commit 8db8fb2
Show file tree
Hide file tree
Showing 17 changed files with 565 additions and 334 deletions.
69 changes: 69 additions & 0 deletions builtins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import subprocess
import json

def enumerate_builtins_by_version(python_version):
cmd = [
"uv", "run", "--python", f"{python_version}", "--no-project", "python",
"-c",
"""
import sys
import builtins
import json
python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
builtin_list = sorted(dir(builtins))
print(json.dumps(builtin_list))
"""
]
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
return json.loads(result.stdout)

if __name__ == "__main__":
python_versions = ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
all_results = {}
for version in python_versions:
print(f"\nRunning for Python {version}")
result = enumerate_builtins_by_version(version)
all_results[version] = result

print("\nRust code for listing builtins:")
print("match (major, minor) {")
for version, builtins in all_results.items():
major, minor = version.split('.')
print(f" ({major}, {minor}) => &[")
for builtin in builtins:
print(f' "{builtin}",')
print(" ],")
print(" _ => &[],")
print("}")

print("\nRust code for checking if a string is a builtin:")
print("matches!(minor, {")

# Collect all unique builtins across versions
all_builtins = set()
for builtins in all_results.values():
all_builtins.update(builtins)

print("pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool {")
print(" matches!(")
print(" (minor_version, module),")
print(" (")

# Print the builtins that are common to all versions
common_builtins = set.intersection(*map(set, all_results.values()))
for builtin in sorted(common_builtins):
print(f' (_, "{builtin}"),')

# Print version-specific builtins
for version, builtins in all_results.items():
_, minor = version.split('.')
version_specific = set(builtins) - common_builtins
if version_specific:
for builtin in sorted(version_specific):
print(f' ({minor}, "{builtin}"),')

print(" )")
print(" )")
print("}")
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@
from some import other as int
from some import input, exec
from directory import new as dir

# See: https://github.com/astral-sh/ruff/issues/13037
import sys

if sys.version_info < (3, 11):
from exceptiongroup import BaseExceptionGroup, ExceptionGroup
20 changes: 20 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pylint/no_self_use.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,23 @@ def bar(self):
def baz(self):
if super().foo():
...


# See: https://github.com/astral-sh/ruff/issues/12568
from attrs import define, field


@define
class Foo:
x: int = field()
y: int

@x.validator
def validate_x(self, attribute, value):
if value <= 0:
raise ValueError("x must be a positive integer")

@y.validator
def validate_y(self, attribute, value):
if value <= 0:
raise ValueError("y must be a positive integer")
4 changes: 2 additions & 2 deletions crates/ruff_linter/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ use ruff_python_semantic::{
ModuleKind, ModuleSource, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags,
StarImport, SubmoduleImport,
};
use ruff_python_stdlib::builtins::{IPYTHON_BUILTINS, MAGIC_GLOBALS, PYTHON_BUILTINS};
use ruff_python_stdlib::builtins::{python_builtins, IPYTHON_BUILTINS, MAGIC_GLOBALS};
use ruff_python_trivia::CommentRanges;
use ruff_source_file::{Locator, OneIndexed, SourceRow};
use ruff_text_size::{Ranged, TextRange, TextSize};
Expand Down Expand Up @@ -1912,7 +1912,7 @@ impl<'a> Checker<'a> {
}

fn bind_builtins(&mut self) {
for builtin in PYTHON_BUILTINS
for builtin in python_builtins(self.settings.target_version.minor())
.iter()
.chain(MAGIC_GLOBALS.iter())
.chain(
Expand Down
8 changes: 6 additions & 2 deletions crates/ruff_linter/src/rules/flake8_builtins/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
use crate::settings::types::PythonVersion;
use ruff_python_ast::PySourceType;
use ruff_python_stdlib::builtins::{is_ipython_builtin, is_python_builtin};

pub(super) fn shadows_builtin(
name: &str,
ignorelist: &[String],
source_type: PySourceType,
ignorelist: &[String],
python_version: PythonVersion,
) -> bool {
if is_python_builtin(name) || source_type.is_ipynb() && is_ipython_builtin(name) {
if is_python_builtin(name, python_version.minor())
|| source_type.is_ipynb() && is_ipython_builtin(name)
{
ignorelist.iter().all(|ignore| ignore != name)
} else {
false
Expand Down
15 changes: 15 additions & 0 deletions crates/ruff_linter/src/rules/flake8_builtins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod tests {

use crate::assert_messages;
use crate::registry::Rule;
use crate::settings::types::PythonVersion;
use crate::settings::LinterSettings;
use crate::test::test_path;

Expand Down Expand Up @@ -112,4 +113,18 @@ mod tests {
assert_messages!(snapshot, diagnostics);
Ok(())
}

#[test_case(Rule::BuiltinImportShadowing, Path::new("A004.py"))]
fn rules_py312(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}_py312", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_builtins").join(path).as_path(),
&LinterSettings {
target_version: PythonVersion::Py312,
..LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,9 @@ impl Violation for BuiltinArgumentShadowing {
pub(crate) fn builtin_argument_shadowing(checker: &mut Checker, parameter: &Parameter) {
if shadows_builtin(
parameter.name.as_str(),
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.source_type,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.settings.target_version,
) {
// Ignore `@override` and `@overload` decorated functions.
if checker
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,9 @@ pub(crate) fn builtin_attribute_shadowing(

if shadows_builtin(
name,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.source_type,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.settings.target_version,
) {
// Ignore explicit overrides.
if class_def.decorator_list.iter().any(|decorator| {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ pub(crate) fn builtin_import_shadowing(checker: &mut Checker, alias: &Alias) {
let name = alias.asname.as_ref().unwrap_or(&alias.name);
if shadows_builtin(
name.as_str(),
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.source_type,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.settings.target_version,
) {
checker.diagnostics.push(Diagnostic::new(
BuiltinImportShadowing {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ pub(crate) fn builtin_lambda_argument_shadowing(checker: &mut Checker, lambda: &
let name = &param.parameter.name;
if shadows_builtin(
name.as_ref(),
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.source_type,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.settings.target_version,
) {
checker.diagnostics.push(Diagnostic::new(
BuiltinLambdaArgumentShadowing {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ impl Violation for BuiltinVariableShadowing {
pub(crate) fn builtin_variable_shadowing(checker: &mut Checker, name: &str, range: TextRange) {
if shadows_builtin(
name,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.source_type,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.settings.target_version,
) {
checker.diagnostics.push(Diagnostic::new(
BuiltinVariableShadowing {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,6 @@ A004.py:5:30: A004 Import `dir` is shadowing a Python builtin
4 | from some import input, exec
5 | from directory import new as dir
| ^^^ A004
6 |
7 | # See: https://github.com/astral-sh/ruff/issues/13037
|
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
source: crates/ruff_linter/src/rules/flake8_builtins/mod.rs
---
A004.py:1:16: A004 Import `sum` is shadowing a Python builtin
|
1 | import some as sum
| ^^^ A004
2 | import float
3 | from some import other as int
|

A004.py:2:8: A004 Import `float` is shadowing a Python builtin
|
1 | import some as sum
2 | import float
| ^^^^^ A004
3 | from some import other as int
4 | from some import input, exec
|

A004.py:3:27: A004 Import `int` is shadowing a Python builtin
|
1 | import some as sum
2 | import float
3 | from some import other as int
| ^^^ A004
4 | from some import input, exec
5 | from directory import new as dir
|

A004.py:4:18: A004 Import `input` is shadowing a Python builtin
|
2 | import float
3 | from some import other as int
4 | from some import input, exec
| ^^^^^ A004
5 | from directory import new as dir
|

A004.py:4:25: A004 Import `exec` is shadowing a Python builtin
|
2 | import float
3 | from some import other as int
4 | from some import input, exec
| ^^^^ A004
5 | from directory import new as dir
|

A004.py:5:30: A004 Import `dir` is shadowing a Python builtin
|
3 | from some import other as int
4 | from some import input, exec
5 | from directory import new as dir
| ^^^ A004
6 |
7 | # See: https://github.com/astral-sh/ruff/issues/13037
|

A004.py:11:32: A004 Import `BaseExceptionGroup` is shadowing a Python builtin
|
10 | if sys.version_info < (3, 11):
11 | from exceptiongroup import BaseExceptionGroup, ExceptionGroup
| ^^^^^^^^^^^^^^^^^^ A004
|

A004.py:11:52: A004 Import `ExceptionGroup` is shadowing a Python builtin
|
10 | if sys.version_info < (3, 11):
11 | from exceptiongroup import BaseExceptionGroup, ExceptionGroup
| ^^^^^^^^^^^^^^ A004
|
1 change: 1 addition & 0 deletions crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ pub(crate) fn no_self_use(
|| visibility::is_override(decorator_list, semantic)
|| visibility::is_overload(decorator_list, semantic)
|| visibility::is_property(decorator_list, extra_property_decorators, semantic)
|| visibility::is_validator(decorator_list, semantic)
{
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,11 @@ no_self_use.py:13:9: PLR6301 Method `greeting_2` could be a function, class meth
14 | print("Hi!")
|


no_self_use.py:103:9: PLR6301 Method `validate_y` could be a function, class method, or static method
|
102 | @y.validator
103 | def validate_y(self, attribute, value):
| ^^^^^^^^^^ PLR6301
104 | if value <= 0:
105 | raise ValueError("y must be a positive integer")
|
23 changes: 22 additions & 1 deletion crates/ruff_python_semantic/src/analyze/visibility.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Decorator};
use ruff_python_ast::{self as ast, Decorator, Expr};

use ruff_python_ast::helpers::map_callable;
use ruff_python_ast::name::{QualifiedName, UnqualifiedName};
Expand Down Expand Up @@ -90,6 +90,27 @@ where
})
}

/// Returns `true` if a function definition is an `attrs`-like validator based on its decorators.
pub fn is_validator(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool {
decorator_list.iter().any(|decorator| {
let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = &decorator.expression else {
return false;
};

if attr.as_str() != "validator" {
return false;
}

let Expr::Name(value) = value.as_ref() else {
return false;
};

semantic
.resolve_name(value)
.is_some_and(|id| semantic.binding(id).kind.is_assignment())
})
}

/// Returns `true` if a class is an `final`.
pub fn is_final(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool {
decorator_list
Expand Down
Loading

0 comments on commit 8db8fb2

Please sign in to comment.