From 4d52ea87efeb6313f52d6b34beb1f1d8faad860e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 26 Jan 2023 16:55:32 -0500 Subject: [PATCH] Implement `exempt-modules` setting from flake8-type-checking (#2230) --- README.md | 18 ++++++++++++ .../flake8_type_checking/exempt_modules.py | 16 +++++++++++ ruff.schema.json | 10 +++++++ src/rules/flake8_type_checking/mod.rs | 23 ++++++++++++++- .../rules/typing_only_runtime_import.rs | 28 +++++++++++++++++++ src/rules/flake8_type_checking/settings.rs | 13 +++++++++ ..._type_checking__tests__exempt_modules.snap | 16 +++++++++++ 7 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 resources/test/fixtures/flake8_type_checking/exempt_modules.py create mode 100644 src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__exempt_modules.snap diff --git a/README.md b/README.md index 6912f20e88..1382a7a697 100644 --- a/README.md +++ b/README.md @@ -3108,6 +3108,24 @@ and can be circumvented via `eval` or `importlib`. ### `flake8-type-checking` +#### [`exempt-modules`](#exempt-modules) + +Exempt certain modules from needing to be moved into type-checking +blocks. + +**Default value**: `[]` + +**Type**: `Vec` + +**Example usage**: + +```toml +[tool.ruff.flake8-type-checking] +exempt-modules = ["typing_extensions"] +``` + +--- + #### [`strict`](#strict) Enforce TC001, TC002, and TC003 rules even when valid runtime imports diff --git a/resources/test/fixtures/flake8_type_checking/exempt_modules.py b/resources/test/fixtures/flake8_type_checking/exempt_modules.py new file mode 100644 index 0000000000..cd24c8e2a6 --- /dev/null +++ b/resources/test/fixtures/flake8_type_checking/exempt_modules.py @@ -0,0 +1,16 @@ +def f(): + import pandas as pd + + x: pd.DataFrame + + +def f(): + import pandas.core.frame as pd + + x: pd.DataFrame + + +def f(): + import flask + + x: flask diff --git a/ruff.schema.json b/ruff.schema.json index 80c672cdec..79ba54e760 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -836,6 +836,16 @@ "Flake8TypeCheckingOptions": { "type": "object", "properties": { + "exempt-modules": { + "description": "Exempt certain modules from needing to be moved into type-checking blocks.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, "strict": { "description": "Enforce TC001, TC002, and TC003 rules even when valid runtime imports are present for the same module. See: https://github.com/snok/flake8-type-checking#strict.", "type": [ diff --git a/src/rules/flake8_type_checking/mod.rs b/src/rules/flake8_type_checking/mod.rs index d0b72d6bf6..50255da1ed 100644 --- a/src/rules/flake8_type_checking/mod.rs +++ b/src/rules/flake8_type_checking/mod.rs @@ -47,7 +47,28 @@ mod tests { .join(path) .as_path(), &settings::Settings { - flake8_type_checking: super::settings::Settings { strict: true }, + flake8_type_checking: super::settings::Settings { + strict: true, + ..Default::default() + }, + ..settings::Settings::for_rule(rule_code) + }, + )?; + assert_yaml_snapshot!(diagnostics); + Ok(()) + } + + #[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("exempt_modules.py"); "exempt_modules")] + fn exempt_modules(rule_code: Rule, path: &Path) -> Result<()> { + let diagnostics = test_path( + Path::new("./resources/test/fixtures/flake8_type_checking") + .join(path) + .as_path(), + &settings::Settings { + flake8_type_checking: super::settings::Settings { + exempt_modules: vec!["pandas".to_string()], + ..Default::default() + }, ..settings::Settings::for_rule(rule_code) }, )?; diff --git a/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index e25ac9560b..808d4b02ae 100644 --- a/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -95,6 +95,22 @@ fn is_implicit_import(this: &Binding, that: &Binding) -> bool { } } +/// Return `true` if `name` is exempt from typing-only enforcement. +fn is_exempt(name: &str, exempt_modules: &[&str]) -> bool { + let mut name = name; + loop { + if exempt_modules.contains(&name) { + return true; + } + match name.rfind('.') { + Some(idx) => { + name = &name[..idx]; + } + None => return false, + } + } +} + /// TCH001 pub fn typing_only_runtime_import( binding: &Binding, @@ -120,6 +136,18 @@ pub fn typing_only_runtime_import( _ => return None, }; + if is_exempt( + full_name, + &settings + .flake8_type_checking + .exempt_modules + .iter() + .map(String::as_str) + .collect::>(), + ) { + return None; + } + let defined_in_type_checking = blocks .iter() .any(|block| Range::from_located(block).contains(&binding.range)); diff --git a/src/rules/flake8_type_checking/settings.rs b/src/rules/flake8_type_checking/settings.rs index 85f8792a03..4512cadfe1 100644 --- a/src/rules/flake8_type_checking/settings.rs +++ b/src/rules/flake8_type_checking/settings.rs @@ -24,17 +24,29 @@ pub struct Options { /// are present for the same module. /// See: https://github.com/snok/flake8-type-checking#strict. pub strict: Option, + #[option( + default = "[]", + value_type = "Vec", + example = r#" + exempt-modules = ["typing_extensions"] + "# + )] + /// Exempt certain modules from needing to be moved into type-checking + /// blocks. + pub exempt_modules: Option>, } #[derive(Debug, Hash, Default)] pub struct Settings { pub strict: bool, + pub exempt_modules: Vec, } impl From for Settings { fn from(options: Options) -> Self { Self { strict: options.strict.unwrap_or_default(), + exempt_modules: options.exempt_modules.unwrap_or_default(), } } } @@ -43,6 +55,7 @@ impl From for Options { fn from(settings: Settings) -> Self { Self { strict: Some(settings.strict), + exempt_modules: Some(settings.exempt_modules), } } } diff --git a/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__exempt_modules.snap b/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__exempt_modules.snap new file mode 100644 index 0000000000..22d2b14a9d --- /dev/null +++ b/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__exempt_modules.snap @@ -0,0 +1,16 @@ +--- +source: src/rules/flake8_type_checking/mod.rs +expression: diagnostics +--- +- kind: + TypingOnlyThirdPartyImport: + full_name: flask + location: + row: 14 + column: 11 + end_location: + row: 14 + column: 16 + fix: ~ + parent: ~ +