From 9532f342a6d9e4990b9f431b717d4a14387847dc Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 9 Jan 2023 18:17:50 -0500 Subject: [PATCH] Enable project-specific typing module re-exports (#1754) Resolves #1744. --- README.md | 24 ++ flake8_to_ruff/src/converter.rs | 7 + .../test/fixtures/pyflakes/typing_modules.py | 7 + ruff.schema.json | 10 + src/checkers/ast.rs | 24 +- src/lib_wasm.rs | 1 + src/pyflakes/mod.rs | 23 ++ ...flakes__tests__default_typing_modules.snap | 15 + ...pyflakes__tests__extra_typing_modules.snap | 15 + src/python/typing.rs | 330 ++++++++++-------- src/settings/configuration.rs | 9 +- src/settings/mod.rs | 11 +- src/settings/options.rs | 34 +- src/settings/pyproject.rs | 6 + 14 files changed, 345 insertions(+), 171 deletions(-) create mode 100644 resources/test/fixtures/pyflakes/typing_modules.py create mode 100644 src/pyflakes/snapshots/ruff__pyflakes__tests__default_typing_modules.snap create mode 100644 src/pyflakes/snapshots/ruff__pyflakes__tests__extra_typing_modules.snap diff --git a/README.md b/README.md index 976aa1a116..97e988c58d 100644 --- a/README.md +++ b/README.md @@ -2288,6 +2288,30 @@ task-tags = ["HACK"] --- +#### [`typing-modules`](#typing-modules) + +A list of modules whose imports should be treated equivalently to +members of the `typing` module. + +This is useful for ensuring proper type annotation inference for +projects that re-export `typing` and `typing_extensions` members +from a compatibility module. If omitted, any members imported from +modules apart from `typing` and `typing_extensions` will be treated +as ordinary Python objects. + +**Default value**: `[]` + +**Type**: `Vec` + +**Example usage**: + +```toml +[tool.ruff] +typing-modules = ["airflow.typing_compat"] +``` + +--- + #### [`unfixable`](#unfixable) A list of check code prefixes to consider un-autofix-able. diff --git a/flake8_to_ruff/src/converter.rs b/flake8_to_ruff/src/converter.rs index 920f2797bc..8afb643ae4 100644 --- a/flake8_to_ruff/src/converter.rs +++ b/flake8_to_ruff/src/converter.rs @@ -435,6 +435,7 @@ mod tests { src: None, target_version: None, unfixable: None, + typing_modules: None, task_tags: None, update_check: None, flake8_annotations: None, @@ -499,6 +500,7 @@ mod tests { src: None, target_version: None, unfixable: None, + typing_modules: None, task_tags: None, update_check: None, flake8_annotations: None, @@ -563,6 +565,7 @@ mod tests { src: None, target_version: None, unfixable: None, + typing_modules: None, task_tags: None, update_check: None, flake8_annotations: None, @@ -627,6 +630,7 @@ mod tests { src: None, target_version: None, unfixable: None, + typing_modules: None, task_tags: None, update_check: None, flake8_annotations: None, @@ -691,6 +695,7 @@ mod tests { src: None, target_version: None, unfixable: None, + typing_modules: None, task_tags: None, update_check: None, flake8_annotations: None, @@ -764,6 +769,7 @@ mod tests { src: None, target_version: None, unfixable: None, + typing_modules: None, task_tags: None, update_check: None, flake8_annotations: None, @@ -831,6 +837,7 @@ mod tests { src: None, target_version: None, unfixable: None, + typing_modules: None, task_tags: None, update_check: None, flake8_annotations: None, diff --git a/resources/test/fixtures/pyflakes/typing_modules.py b/resources/test/fixtures/pyflakes/typing_modules.py new file mode 100644 index 0000000000..dc154c39e1 --- /dev/null +++ b/resources/test/fixtures/pyflakes/typing_modules.py @@ -0,0 +1,7 @@ +from typing import Union + +from airflow.typing_compat import Literal, Optional + + +X = Union[Literal[False], Literal["db"]] +y = Optional["Class"] diff --git a/ruff.schema.json b/ruff.schema.json index 8285df0705..b81ac12018 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -408,6 +408,16 @@ "type": "string" } }, + "typing-modules": { + "description": "A list of modules whose imports should be treated equivalently to members of the `typing` module.\n\nThis is useful for ensuring proper type annotation inference for projects that re-export `typing` and `typing_extensions` members from a compatibility module. If omitted, any members imported from modules apart from `typing` and `typing_extensions` will be treated as ordinary Python objects.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, "unfixable": { "description": "A list of check code prefixes to consider un-autofix-able.", "type": [ diff --git a/src/checkers/ast.rs b/src/checkers/ast.rs index 7082a6e01a..15f9f6a12c 100644 --- a/src/checkers/ast.rs +++ b/src/checkers/ast.rs @@ -174,9 +174,26 @@ impl<'a> Checker<'a> { /// Return `true` if the call path is a reference to `typing.${target}`. pub fn match_typing_call_path(&self, call_path: &[&str], target: &str) -> bool { - match_call_path(call_path, "typing", target, &self.from_imports) - || (typing::in_extensions(target) - && match_call_path(call_path, "typing_extensions", target, &self.from_imports)) + if match_call_path(call_path, "typing", target, &self.from_imports) { + return true; + } + + if typing::TYPING_EXTENSIONS.contains(target) { + if match_call_path(call_path, "typing_extensions", target, &self.from_imports) { + return true; + } + } + + if self + .settings + .typing_modules + .iter() + .any(|module| match_call_path(call_path, module, target, &self.from_imports)) + { + return true; + } + + false } /// Return the current `Binding` for a given `name`. @@ -2954,6 +2971,7 @@ where value, &self.from_imports, &self.import_aliases, + self.settings.typing_modules.iter().map(String::as_str), |member| self.is_builtin(member), ) { Some(subscript) => { diff --git a/src/lib_wasm.rs b/src/lib_wasm.rs index 13d6ca66da..9223ecb985 100644 --- a/src/lib_wasm.rs +++ b/src/lib_wasm.rs @@ -112,6 +112,7 @@ pub fn defaultSettings() -> Result { show_source: None, src: None, unfixable: None, + typing_modules: None, task_tags: None, update_check: None, // Use default options for all plugins. diff --git a/src/pyflakes/mod.rs b/src/pyflakes/mod.rs index 0cf882f20b..290ef25e79 100644 --- a/src/pyflakes/mod.rs +++ b/src/pyflakes/mod.rs @@ -163,6 +163,29 @@ mod tests { Ok(()) } + #[test] + fn default_typing_modules() -> Result<()> { + let diagnostics = test_path( + Path::new("./resources/test/fixtures/pyflakes/typing_modules.py"), + &settings::Settings::for_rules(vec![RuleCode::F821]), + )?; + insta::assert_yaml_snapshot!(diagnostics); + Ok(()) + } + + #[test] + fn extra_typing_modules() -> Result<()> { + let diagnostics = test_path( + Path::new("./resources/test/fixtures/pyflakes/typing_modules.py"), + &settings::Settings { + typing_modules: vec!["airflow.typing_compat".to_string()], + ..settings::Settings::for_rules(vec![RuleCode::F821]) + }, + )?; + insta::assert_yaml_snapshot!(diagnostics); + Ok(()) + } + #[test] fn future_annotations() -> Result<()> { let diagnostics = test_path( diff --git a/src/pyflakes/snapshots/ruff__pyflakes__tests__default_typing_modules.snap b/src/pyflakes/snapshots/ruff__pyflakes__tests__default_typing_modules.snap new file mode 100644 index 0000000000..7bc2c8eb11 --- /dev/null +++ b/src/pyflakes/snapshots/ruff__pyflakes__tests__default_typing_modules.snap @@ -0,0 +1,15 @@ +--- +source: src/pyflakes/mod.rs +expression: diagnostics +--- +- kind: + UndefinedName: db + location: + row: 6 + column: 34 + end_location: + row: 6 + column: 38 + fix: ~ + parent: ~ + diff --git a/src/pyflakes/snapshots/ruff__pyflakes__tests__extra_typing_modules.snap b/src/pyflakes/snapshots/ruff__pyflakes__tests__extra_typing_modules.snap new file mode 100644 index 0000000000..e715a5c7ca --- /dev/null +++ b/src/pyflakes/snapshots/ruff__pyflakes__tests__extra_typing_modules.snap @@ -0,0 +1,15 @@ +--- +source: src/pyflakes/mod.rs +expression: diagnostics +--- +- kind: + UndefinedName: Class + location: + row: 7 + column: 13 + end_location: + row: 7 + column: 20 + fix: ~ + parent: ~ + diff --git a/src/python/typing.rs b/src/python/typing.rs index 947a28893c..d372b91205 100644 --- a/src/python/typing.rs +++ b/src/python/typing.rs @@ -5,7 +5,7 @@ use rustpython_ast::{Expr, ExprKind}; use crate::ast::helpers::{collect_call_paths, dealias_call_path, match_call_path}; // See: https://pypi.org/project/typing-extensions/ -static TYPING_EXTENSIONS: Lazy> = Lazy::new(|| { +pub static TYPING_EXTENSIONS: Lazy> = Lazy::new(|| { FxHashSet::from_iter([ "Annotated", "Any", @@ -61,159 +61,157 @@ static TYPING_EXTENSIONS: Lazy> = Lazy::new(|| { ]) }); -pub fn in_extensions(name: &str) -> bool { - TYPING_EXTENSIONS.contains(name) -} +// See: https://docs.python.org/3/library/typing.html +static SUBSCRIPTS: Lazy>> = Lazy::new(|| { + let mut subscripts: FxHashMap<&'static str, Vec<&'static str>> = FxHashMap::default(); + for (module, name) in [ + // builtins + ("", "dict"), + ("", "frozenset"), + ("", "list"), + ("", "set"), + ("", "tuple"), + ("", "type"), + // `collections` + ("collections", "ChainMap"), + ("collections", "Counter"), + ("collections", "OrderedDict"), + ("collections", "defaultdict"), + ("collections", "deque"), + // `collections.abc` + ("collections.abc", "AsyncGenerator"), + ("collections.abc", "AsyncIterable"), + ("collections.abc", "AsyncIterator"), + ("collections.abc", "Awaitable"), + ("collections.abc", "ByteString"), + ("collections.abc", "Callable"), + ("collections.abc", "Collection"), + ("collections.abc", "Container"), + ("collections.abc", "Coroutine"), + ("collections.abc", "Generator"), + ("collections.abc", "ItemsView"), + ("collections.abc", "Iterable"), + ("collections.abc", "Iterator"), + ("collections.abc", "KeysView"), + ("collections.abc", "Mapping"), + ("collections.abc", "MappingView"), + ("collections.abc", "MutableMapping"), + ("collections.abc", "MutableSequence"), + ("collections.abc", "MutableSet"), + ("collections.abc", "Reversible"), + ("collections.abc", "Sequence"), + ("collections.abc", "Set"), + ("collections.abc", "ValuesView"), + // `contextlib` + ("contextlib", "AbstractAsyncContextManager"), + ("contextlib", "AbstractContextManager"), + // `re` + ("re", "Match"), + ("re", "Pattern"), + // `typing` + ("typing", "AbstractSet"), + ("typing", "AsyncContextManager"), + ("typing", "AsyncGenerator"), + ("typing", "AsyncIterator"), + ("typing", "Awaitable"), + ("typing", "BinaryIO"), + ("typing", "ByteString"), + ("typing", "Callable"), + ("typing", "ChainMap"), + ("typing", "ClassVar"), + ("typing", "Collection"), + ("typing", "Concatenate"), + ("typing", "Container"), + ("typing", "ContextManager"), + ("typing", "Coroutine"), + ("typing", "Counter"), + ("typing", "DefaultDict"), + ("typing", "Deque"), + ("typing", "Dict"), + ("typing", "Final"), + ("typing", "FrozenSet"), + ("typing", "Generator"), + ("typing", "Generic"), + ("typing", "IO"), + ("typing", "ItemsView"), + ("typing", "Iterable"), + ("typing", "Iterator"), + ("typing", "KeysView"), + ("typing", "List"), + ("typing", "Mapping"), + ("typing", "Match"), + ("typing", "MutableMapping"), + ("typing", "MutableSequence"), + ("typing", "MutableSet"), + ("typing", "Optional"), + ("typing", "OrderedDict"), + ("typing", "Pattern"), + ("typing", "Reversible"), + ("typing", "Sequence"), + ("typing", "Set"), + ("typing", "TextIO"), + ("typing", "Tuple"), + ("typing", "Type"), + ("typing", "TypeGuard"), + ("typing", "Union"), + ("typing", "Unpack"), + ("typing", "ValuesView"), + // `typing.io` + ("typing.io", "BinaryIO"), + ("typing.io", "IO"), + ("typing.io", "TextIO"), + // `typing.re` + ("typing.re", "Match"), + ("typing.re", "Pattern"), + // `typing_extensions` + ("typing_extensions", "AsyncContextManager"), + ("typing_extensions", "AsyncGenerator"), + ("typing_extensions", "AsyncIterable"), + ("typing_extensions", "AsyncIterator"), + ("typing_extensions", "Awaitable"), + ("typing_extensions", "ChainMap"), + ("typing_extensions", "ClassVar"), + ("typing_extensions", "Concatenate"), + ("typing_extensions", "ContextManager"), + ("typing_extensions", "Coroutine"), + ("typing_extensions", "Counter"), + ("typing_extensions", "DefaultDict"), + ("typing_extensions", "Deque"), + ("typing_extensions", "Type"), + // `weakref` + ("weakref", "WeakKeyDictionary"), + ("weakref", "WeakSet"), + ("weakref", "WeakValueDictionary"), + ] { + subscripts.entry(name).or_default().push(module); + } + subscripts +}); // See: https://docs.python.org/3/library/typing.html -const SUBSCRIPTS: &[(&str, &str)] = &[ - // builtins - ("", "dict"), - ("", "frozenset"), - ("", "list"), - ("", "set"), - ("", "tuple"), - ("", "type"), - // `collections` - ("collections", "ChainMap"), - ("collections", "Counter"), - ("collections", "OrderedDict"), - ("collections", "defaultdict"), - ("collections", "deque"), - // `collections.abc` - ("collections.abc", "AsyncGenerator"), - ("collections.abc", "AsyncIterable"), - ("collections.abc", "AsyncIterator"), - ("collections.abc", "Awaitable"), - ("collections.abc", "ByteString"), - ("collections.abc", "Callable"), - ("collections.abc", "Collection"), - ("collections.abc", "Container"), - ("collections.abc", "Coroutine"), - ("collections.abc", "Generator"), - ("collections.abc", "ItemsView"), - ("collections.abc", "Iterable"), - ("collections.abc", "Iterator"), - ("collections.abc", "KeysView"), - ("collections.abc", "Mapping"), - ("collections.abc", "MappingView"), - ("collections.abc", "MutableMapping"), - ("collections.abc", "MutableSequence"), - ("collections.abc", "MutableSet"), - ("collections.abc", "Reversible"), - ("collections.abc", "Sequence"), - ("collections.abc", "Set"), - ("collections.abc", "ValuesView"), - // `contextlib` - ("contextlib", "AbstractAsyncContextManager"), - ("contextlib", "AbstractContextManager"), - // `re` - ("re", "Match"), - ("re", "Pattern"), - // `typing` - ("typing", "AbstractSet"), - ("typing", "AsyncContextManager"), - ("typing", "AsyncGenerator"), - ("typing", "AsyncIterator"), - ("typing", "Awaitable"), - ("typing", "BinaryIO"), - ("typing", "ByteString"), - ("typing", "Callable"), - ("typing", "ChainMap"), - ("typing", "ClassVar"), - ("typing", "Collection"), - ("typing", "Concatenate"), - ("typing", "Container"), - ("typing", "ContextManager"), - ("typing", "Coroutine"), - ("typing", "Counter"), - ("typing", "DefaultDict"), - ("typing", "Deque"), - ("typing", "Dict"), - ("typing", "Final"), - ("typing", "FrozenSet"), - ("typing", "Generator"), - ("typing", "Generic"), - ("typing", "IO"), - ("typing", "ItemsView"), - ("typing", "Iterable"), - ("typing", "Iterator"), - ("typing", "KeysView"), - ("typing", "List"), - ("typing", "Mapping"), - ("typing", "Match"), - ("typing", "MutableMapping"), - ("typing", "MutableSequence"), - ("typing", "MutableSet"), - ("typing", "Optional"), - ("typing", "OrderedDict"), - ("typing", "Pattern"), - ("typing", "Reversible"), - ("typing", "Sequence"), - ("typing", "Set"), - ("typing", "TextIO"), - ("typing", "Tuple"), - ("typing", "Type"), - ("typing", "TypeGuard"), - ("typing", "Union"), - ("typing", "Unpack"), - ("typing", "ValuesView"), - // `typing.io` - ("typing.io", "BinaryIO"), - ("typing.io", "IO"), - ("typing.io", "TextIO"), - // `typing.re` - ("typing.re", "Match"), - ("typing.re", "Pattern"), - // `typing_extensions` - ("typing_extensions", "AsyncContextManager"), - ("typing_extensions", "AsyncGenerator"), - ("typing_extensions", "AsyncIterable"), - ("typing_extensions", "AsyncIterator"), - ("typing_extensions", "Awaitable"), - ("typing_extensions", "ChainMap"), - ("typing_extensions", "ClassVar"), - ("typing_extensions", "Concatenate"), - ("typing_extensions", "ContextManager"), - ("typing_extensions", "Coroutine"), - ("typing_extensions", "Counter"), - ("typing_extensions", "DefaultDict"), - ("typing_extensions", "Deque"), - ("typing_extensions", "Type"), - // `weakref` - ("weakref", "WeakKeyDictionary"), - ("weakref", "WeakSet"), - ("weakref", "WeakValueDictionary"), -]; - -// See: https://docs.python.org/3/library/typing.html -const PEP_583_SUBSCRIPTS: &[(&str, &str)] = &[ - // `typing` - ("typing", "Annotated"), - // `typing_extensions` - ("typing_extensions", "Annotated"), -]; - -// See: https://peps.python.org/pep-0585/ -const PEP_585_BUILTINS_ELIGIBLE: &[(&str, &str)] = &[ - ("typing", "Dict"), - ("typing", "FrozenSet"), - ("typing", "List"), - ("typing", "Set"), - ("typing", "Tuple"), - ("typing", "Type"), - ("typing_extensions", "Type"), -]; +static PEP_593_SUBSCRIPTS: Lazy>> = Lazy::new(|| { + let mut subscripts: FxHashMap<&'static str, Vec<&'static str>> = FxHashMap::default(); + for (module, name) in [ + // `typing` + ("typing", "Annotated"), + // `typing_extensions` + ("typing_extensions", "Annotated"), + ] { + subscripts.entry(name).or_default().push(module); + } + subscripts +}); pub enum SubscriptKind { AnnotatedSubscript, PEP593AnnotatedSubscript, } -pub fn match_annotated_subscript( +pub fn match_annotated_subscript<'a, F>( expr: &Expr, from_imports: &FxHashMap<&str, FxHashSet<&str>>, import_aliases: &FxHashMap<&str, &str>, + typing_modules: impl Iterator, is_builtin: F, ) -> Option where @@ -226,23 +224,49 @@ where return None; } let call_path = dealias_call_path(collect_call_paths(expr), import_aliases); - if !call_path.is_empty() { - for (module, member) in SUBSCRIPTS { - if match_call_path(&call_path, module, member, from_imports) - && (!module.is_empty() || is_builtin(member)) - { - return Some(SubscriptKind::AnnotatedSubscript); + if let Some(member) = call_path.last() { + if let Some(modules) = SUBSCRIPTS.get(member) { + for module in modules { + if match_call_path(&call_path, module, member, from_imports) + && (!module.is_empty() || is_builtin(member)) + { + return Some(SubscriptKind::AnnotatedSubscript); + } } - } - for (module, member) in PEP_583_SUBSCRIPTS { - if match_call_path(&call_path, module, member, from_imports) { - return Some(SubscriptKind::PEP593AnnotatedSubscript); + for module in typing_modules { + if match_call_path(&call_path, module, member, from_imports) { + return Some(SubscriptKind::AnnotatedSubscript); + } + } + } else if let Some(modules) = PEP_593_SUBSCRIPTS.get(member) { + for module in modules { + if match_call_path(&call_path, module, member, from_imports) + && (!module.is_empty() || is_builtin(member)) + { + return Some(SubscriptKind::PEP593AnnotatedSubscript); + } + } + for module in typing_modules { + if match_call_path(&call_path, module, member, from_imports) { + return Some(SubscriptKind::PEP593AnnotatedSubscript); + } } } } None } +// See: https://peps.python.org/pep-0585/ +const PEP_585_BUILTINS_ELIGIBLE: &[(&str, &str)] = &[ + ("typing", "Dict"), + ("typing", "FrozenSet"), + ("typing", "List"), + ("typing", "Set"), + ("typing", "Tuple"), + ("typing", "Type"), + ("typing_extensions", "Type"), +]; + /// Returns `true` if `Expr` represents a reference to a typing object with a /// PEP 585 built-in. pub fn is_pep585_builtin( diff --git a/src/settings/configuration.rs b/src/settings/configuration.rs index cb6c71aaeb..4fbd970eea 100644 --- a/src/settings/configuration.rs +++ b/src/settings/configuration.rs @@ -52,8 +52,9 @@ pub struct Configuration { pub show_source: Option, pub src: Option>, pub target_version: Option, - pub unfixable: Option>, pub task_tags: Option>, + pub typing_modules: Option>, + pub unfixable: Option>, pub update_check: Option, // Plugins pub flake8_annotations: Option, @@ -153,8 +154,9 @@ impl Configuration { .map(|src| resolve_src(&src, project_root)) .transpose()?, target_version: options.target_version, - unfixable: options.unfixable, task_tags: options.task_tags, + typing_modules: options.typing_modules, + unfixable: options.unfixable, update_check: options.update_check, // Plugins flake8_annotations: options.flake8_annotations, @@ -217,8 +219,9 @@ impl Configuration { show_source: self.show_source.or(config.show_source), src: self.src.or(config.src), target_version: self.target_version.or(config.target_version), - unfixable: self.unfixable.or(config.unfixable), task_tags: self.task_tags.or(config.task_tags), + typing_modules: self.typing_modules.or(config.typing_modules), + unfixable: self.unfixable.or(config.unfixable), update_check: self.update_check.or(config.update_check), // Plugins flake8_annotations: self.flake8_annotations.or(config.flake8_annotations), diff --git a/src/settings/mod.rs b/src/settings/mod.rs index ba986dbbc9..e04c058c53 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -61,6 +61,7 @@ pub struct Settings { pub src: Vec, pub target_version: PythonVersion, pub task_tags: Vec, + pub typing_modules: Vec, pub update_check: bool, // Plugins pub flake8_annotations: flake8_annotations::settings::Settings, @@ -180,6 +181,7 @@ impl Settings { task_tags: config.task_tags.unwrap_or_else(|| { vec!["TODO".to_string(), "FIXME".to_string(), "XXX".to_string()] }), + typing_modules: config.typing_modules.unwrap_or_default(), update_check: config.update_check.unwrap_or(true), // Plugins flake8_annotations: config @@ -238,7 +240,8 @@ impl Settings { show_source: false, src: vec![path_dedot::CWD.clone()], target_version: PythonVersion::Py310, - task_tags: vec!["TODO".to_string(), "FIXME".to_string()], + task_tags: vec!["TODO".to_string(), "FIXME".to_string(), "XXX".to_string()], + typing_modules: vec![], update_check: false, flake8_annotations: flake8_annotations::settings::Settings::default(), flake8_bandit: flake8_bandit::settings::Settings::default(), @@ -281,7 +284,8 @@ impl Settings { show_source: false, src: vec![path_dedot::CWD.clone()], target_version: PythonVersion::Py310, - task_tags: vec!["TODO".to_string()], + task_tags: vec!["TODO".to_string(), "FIXME".to_string(), "XXX".to_string()], + typing_modules: vec![], update_check: false, flake8_annotations: flake8_annotations::settings::Settings::default(), flake8_bandit: flake8_bandit::settings::Settings::default(), @@ -321,6 +325,7 @@ impl Hash for Settings { for confusable in &self.allowed_confusables { confusable.hash(state); } + self.builtins.hash(state); self.dummy_variable_rgx.as_str().hash(state); for value in self.enabled.iter().sorted() { value.hash(state); @@ -343,6 +348,8 @@ impl Hash for Settings { self.show_source.hash(state); self.src.hash(state); self.target_version.hash(state); + self.task_tags.hash(state); + self.typing_modules.hash(state); // Add plugin properties in alphabetical order. self.flake8_annotations.hash(state); self.flake8_bandit.hash(state); diff --git a/src/settings/options.rs b/src/settings/options.rs index a541cc8cf8..8fd368baed 100644 --- a/src/settings/options.rs +++ b/src/settings/options.rs @@ -339,16 +339,6 @@ pub struct Options { /// version will _not_ be inferred from the _current_ Python version, /// and instead must be specified explicitly (as seen below). pub target_version: Option, - #[option( - default = "[]", - value_type = "Vec", - example = r#" - # Disable autofix for unused imports (`F401`). - unfixable = ["F401"] - "# - )] - /// A list of check code prefixes to consider un-autofix-able. - pub unfixable: Option>, #[option( default = r#"["TODO", "FIXME", "XXX"]"#, value_type = "Vec", @@ -360,6 +350,30 @@ pub struct Options { /// detection (`ERA`), and skipped by line-length checks (`E501`) if /// `ignore-overlong-task-comments` is set to `true`. pub task_tags: Option>, + #[option( + default = r#"[]"#, + value_type = "Vec", + example = r#"typing-modules = ["airflow.typing_compat"]"# + )] + /// A list of modules whose imports should be treated equivalently to + /// members of the `typing` module. + /// + /// This is useful for ensuring proper type annotation inference for + /// projects that re-export `typing` and `typing_extensions` members + /// from a compatibility module. If omitted, any members imported from + /// modules apart from `typing` and `typing_extensions` will be treated + /// as ordinary Python objects. + pub typing_modules: Option>, + #[option( + default = "[]", + value_type = "Vec", + example = r#" + # Disable autofix for unused imports (`F401`). + unfixable = ["F401"] + "# + )] + /// A list of check code prefixes to consider un-autofix-able. + pub unfixable: Option>, #[option( default = "true", value_type = "bool", diff --git a/src/settings/pyproject.rs b/src/settings/pyproject.rs index 4cd7ac55e2..200d01c13f 100644 --- a/src/settings/pyproject.rs +++ b/src/settings/pyproject.rs @@ -189,6 +189,7 @@ mod tests { src: None, target_version: None, unfixable: None, + typing_modules: None, task_tags: None, update_check: None, flake8_annotations: None, @@ -246,6 +247,7 @@ line-length = 79 src: None, target_version: None, unfixable: None, + typing_modules: None, task_tags: None, update_check: None, cache_dir: None, @@ -305,6 +307,7 @@ exclude = ["foo.py"] src: None, target_version: None, unfixable: None, + typing_modules: None, task_tags: None, update_check: None, flake8_annotations: None, @@ -363,6 +366,7 @@ select = ["E501"] src: None, target_version: None, unfixable: None, + typing_modules: None, task_tags: None, update_check: None, flake8_annotations: None, @@ -422,6 +426,7 @@ ignore = ["E501"] src: None, target_version: None, unfixable: None, + typing_modules: None, task_tags: None, update_check: None, flake8_annotations: None, @@ -511,6 +516,7 @@ other-attribute = 1 format: None, force_exclude: None, unfixable: None, + typing_modules: None, task_tags: None, update_check: None, cache_dir: None,