Skip to content

Commit

Permalink
[pylint] - Implement non-ascii-module-import (C2403) (#8056)
Browse files Browse the repository at this point in the history
## Summary

Adds [`non-ascii-module-import` /
`C2403`](https://pylint.pycqa.org/en/latest/user_guide/messages/convention/non-ascii-module-import.html)

See #970

## Test Plan

`cargo test` and manually
  • Loading branch information
diceroll123 authored Oct 20, 2023
1 parent 348b649 commit 7a5f988
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from os.path import join as łos # Error
from os.path import join as los # OK

import os.path.join as łos # Error
import os.path.join as los # OK

import os.path.łos # Error (recommend an ASCII alias)
import os.path.los # OK

from os.path import łos # Error (recommend an ASCII alias)
from os.path import los # OK

from os.path.łos import foo # OK
from os.path.los import foo # OK

from os.path import łos as foo # OK
from os.path import los as foo # OK
10 changes: 9 additions & 1 deletion crates/ruff_linter/src/checkers/ast/analyze/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}

for alias in names {
if checker.enabled(Rule::NonAsciiImportName) {
pylint::rules::non_ascii_module_import(checker, alias);
}
if let Some(asname) = &alias.asname {
if checker.enabled(Rule::BuiltinVariableShadowing) {
flake8_builtins::rules::builtin_variable_shadowing(
Expand Down Expand Up @@ -698,8 +701,8 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
range: _,
},
) => {
let module = module.as_deref();
let level = *level;
let module = module.as_deref();
if checker.enabled(Rule::ModuleImportNotAtTopOfFile) {
pycodestyle::rules::module_import_not_at_top_of_file(checker, stmt);
}
Expand All @@ -712,6 +715,11 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
}
if checker.enabled(Rule::NonAsciiImportName) {
for alias in names {
pylint::rules::non_ascii_module_import(checker, alias);
}
}
if checker.enabled(Rule::UnnecessaryFutureImport) {
if checker.settings.target_version >= PythonVersion::Py37 {
if let Some("__future__") = module {
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "C0205") => (RuleGroup::Stable, rules::pylint::rules::SingleStringSlots),
(Pylint, "C0208") => (RuleGroup::Stable, rules::pylint::rules::IterationOverSet),
(Pylint, "C0414") => (RuleGroup::Stable, rules::pylint::rules::UselessImportAlias),
(Pylint, "C2403") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiImportName),
#[allow(deprecated)]
(Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString),
(Pylint, "C3002") => (RuleGroup::Stable, rules::pylint::rules::UnnecessaryDirectLambdaCall),
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/rules/pylint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ mod tests {
#[test_case(Rule::LiteralMembership, Path::new("literal_membership.py"))]
#[test_case(Rule::GlobalAtModuleLevel, Path::new("global_at_module_level.py"))]
#[test_case(Rule::UnnecessaryLambda, Path::new("unnecessary_lambda.py"))]
#[test_case(Rule::NonAsciiImportName, Path::new("non_ascii_module_import.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/rules/pylint/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub(crate) use misplaced_bare_raise::*;
pub(crate) use named_expr_without_context::*;
pub(crate) use nested_min_max::*;
pub(crate) use no_self_use::*;
pub(crate) use non_ascii_module_import::*;
pub(crate) use nonlocal_without_binding::*;
pub(crate) use property_with_parameters::*;
pub(crate) use redefined_loop_name::*;
Expand Down Expand Up @@ -100,6 +101,7 @@ mod misplaced_bare_raise;
mod named_expr_without_context;
mod nested_min_max;
mod no_self_use;
mod non_ascii_module_import;
mod nonlocal_without_binding;
mod property_with_parameters;
mod redefined_loop_name;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
use ruff_python_ast::Alias;

use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::Ranged;

use crate::checkers::ast::Checker;

/// ## What it does
/// Checks for the use of non-ASCII characters in import statements.
///
/// ## Why is this bad?
/// Pylint discourages the use of non-ASCII characters in symbol names as
/// they can cause confusion and compatibility issues.
///
/// ## Example
/// ```python
/// import bár
/// ```
///
/// Use instead:
/// ```python
/// import bar
/// ```
///
/// If the module is third-party, use an ASCII-only alias:
/// ```python
/// import bár as bar
/// ```
///
/// ## References
/// - [PEP 672](https://peps.python.org/pep-0672/)
#[violation]
pub struct NonAsciiImportName {
name: String,
kind: Kind,
}

impl Violation for NonAsciiImportName {
#[derive_message_formats]
fn message(&self) -> String {
let Self { name, kind } = self;
match kind {
Kind::Aliased => {
format!(
"Module alias `{name}` contains a non-ASCII character, use an ASCII-only alias"
)
}
Kind::Unaliased => {
format!(
"Module name `{name}` contains a non-ASCII character, use an ASCII-only alias"
)
}
}
}
}

#[derive(Debug, PartialEq, Eq)]
enum Kind {
/// The import uses a non-ASCII alias (e.g., `import foo as bár`).
Aliased,
/// The imported module is non-ASCII, and could be given an ASCII alias (e.g., `import bár`).
Unaliased,
}

/// PLC2403
pub(crate) fn non_ascii_module_import(checker: &mut Checker, alias: &Alias) {
if let Some(asname) = &alias.asname {
if asname.as_str().is_ascii() {
return;
}

checker.diagnostics.push(Diagnostic::new(
NonAsciiImportName {
name: asname.to_string(),
kind: Kind::Aliased,
},
asname.range(),
));
} else {
if alias.name.as_str().is_ascii() {
return;
}

checker.diagnostics.push(Diagnostic::new(
NonAsciiImportName {
name: alias.name.to_string(),
kind: Kind::Unaliased,
},
alias.name.range(),
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
non_ascii_module_import.py:1:29: PLC2403 Module alias `łos` contains a non-ASCII character, use an ASCII-only alias
|
1 | from os.path import join as łos # Error
| ^^^ PLC2403
2 | from os.path import join as los # OK
|

non_ascii_module_import.py:4:24: PLC2403 Module alias `łos` contains a non-ASCII character, use an ASCII-only alias
|
2 | from os.path import join as los # OK
3 |
4 | import os.path.join as łos # Error
| ^^^ PLC2403
5 | import os.path.join as los # OK
|

non_ascii_module_import.py:7:8: PLC2403 Module name `os.path.łos` contains a non-ASCII character, use an ASCII-only alias
|
5 | import os.path.join as los # OK
6 |
7 | import os.pathos # Error (recommend an ASCII alias)
| ^^^^^^^^^^^ PLC2403
8 | import os.path.los # OK
|

non_ascii_module_import.py:10:21: PLC2403 Module name `łos` contains a non-ASCII character, use an ASCII-only alias
|
8 | import os.path.los # OK
9 |
10 | from os.path import łos # Error (recommend an ASCII alias)
| ^^^ PLC2403
11 | from os.path import los # OK
|


4 changes: 4 additions & 0 deletions ruff.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 7a5f988

Please sign in to comment.