diff --git a/README.md b/README.md index ae5c5182f6..dcda1d6a21 100644 --- a/README.md +++ b/README.md @@ -847,6 +847,7 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/) on PyPI. | UP032 | f-string | Use f-string instead of `format` call | 🛠 | | UP033 | functools-cache | Use `@functools.cache` instead of `@functools.lru_cache(maxsize=None)` | 🛠 | | UP034 | extraneous-parentheses | Avoid extraneous parentheses | 🛠 | +| UP035 | import-replacements | Import from `{module}` instead: {names} | 🛠 | ### flake8-2020 (YTT) diff --git a/resources/test/fixtures/pyupgrade/UP035.py b/resources/test/fixtures/pyupgrade/UP035.py new file mode 100644 index 0000000000..a1ccf50443 --- /dev/null +++ b/resources/test/fixtures/pyupgrade/UP035.py @@ -0,0 +1,50 @@ +# UP035 +from collections import Mapping + +from collections import Mapping as MAP + +from collections import Mapping, Sequence + +from collections import Counter, Mapping + +from collections import (Counter, Mapping) + +from collections import (Counter, + Mapping) + +from collections import Counter, \ + Mapping + +from collections import Counter, Mapping, Sequence + +from collections import Mapping as mapping, Counter + +if True: + from collections import Mapping, Counter + +if True: + if True: + pass + from collections import Mapping, Counter + +if True: from collections import Mapping + +import os +from collections import Counter, Mapping +import sys + +if True: + from collections import ( + Mapping, + Callable, + Bad, + Good, + ) + +from typing import Callable, Match, Pattern, List + +if True: from collections import ( + Mapping, Counter) + +# OK +from a import b diff --git a/ruff.schema.json b/ruff.schema.json index e551bf5a73..16c4ceef3b 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1892,6 +1892,7 @@ "UP032", "UP033", "UP034", + "UP035", "W", "W2", "W29", diff --git a/src/checkers/ast.rs b/src/checkers/ast.rs index 6b6ad2aaf9..8f2bd34e79 100644 --- a/src/checkers/ast.rs +++ b/src/checkers/ast.rs @@ -1073,6 +1073,15 @@ where if self.settings.rules.enabled(&Rule::RewriteCElementTree) { pyupgrade::rules::replace_c_element_tree(self, stmt); } + if self.settings.rules.enabled(&Rule::ImportReplacements) { + pyupgrade::rules::import_replacements( + self, + stmt, + names, + module.as_ref().map(String::as_str), + level.as_ref(), + ); + } if self.settings.rules.enabled(&Rule::UnnecessaryBuiltinImport) { if let Some(module) = module.as_deref() { pyupgrade::rules::unnecessary_builtin_import(self, stmt, module, names); diff --git a/src/registry.rs b/src/registry.rs index 209869cf99..d90c7f3bb5 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -256,6 +256,7 @@ ruff_macros::define_rule_mapping!( UP032 => violations::FString, UP033 => violations::FunctoolsCache, UP034 => violations::ExtraneousParentheses, + UP035 => rules::pyupgrade::rules::ImportReplacements, // pydocstyle D100 => violations::PublicModule, D101 => violations::PublicClass, diff --git a/src/rules/pyupgrade/fixes.rs b/src/rules/pyupgrade/fixes.rs index 84b3cf4fcf..4546efdbd6 100644 --- a/src/rules/pyupgrade/fixes.rs +++ b/src/rules/pyupgrade/fixes.rs @@ -2,6 +2,8 @@ use libcst_native::{ Codegen, CodegenState, Expression, ParenthesizableWhitespace, SmallStatement, Statement, }; use rustpython_ast::{Expr, Keyword, Location}; +use rustpython_parser::lexer; +use rustpython_parser::lexer::Tok; use crate::ast::types::Range; use crate::autofix::helpers::remove_argument; @@ -58,3 +60,215 @@ pub fn remove_super_arguments(locator: &Locator, stylist: &Stylist, expr: &Expr) range.end_location, )) } + +/// Remove any imports matching `members` from an import-from statement. +pub fn remove_import_members(contents: &str, members: &[&str]) -> String { + let mut names: Vec = vec![]; + let mut commas: Vec = vec![]; + let mut removal_indices: Vec = vec![]; + + // Find all Tok::Name tokens that are not preceded by Tok::As, and all Tok::Comma tokens. + let mut prev_tok = None; + for (start, tok, end) in lexer::make_tokenizer(contents) + .flatten() + .skip_while(|(_, tok, _)| !matches!(tok, Tok::Import)) + { + if let Tok::Name { name } = &tok { + if matches!(prev_tok, Some(Tok::As)) { + // Adjust the location to take the alias into account. + names.last_mut().unwrap().end_location = end; + } else { + if members.contains(&name.as_str()) { + removal_indices.push(names.len()); + } + names.push(Range::new(start, end)); + } + } else if matches!(tok, Tok::Comma) { + commas.push(Range::new(start, end)); + } + prev_tok = Some(tok); + } + + // Reconstruct the source code by skipping any names that are in `members`. + let locator = Locator::new(contents); + let mut output = String::with_capacity(contents.len()); + let mut last_pos: Location = Location::new(1, 0); + let mut is_first = true; + for index in 0..names.len() { + if !removal_indices.contains(&index) { + is_first = false; + continue; + } + + let (start_location, end_location) = if is_first { + (names[index].location, names[index + 1].location) + } else { + (commas[index - 1].location, names[index].end_location) + }; + + // Add all contents from `last_pos` to `fix.location`. + // It's possible that `last_pos` is after `fix.location`, if we're removing the first _two_ + // members. + if start_location > last_pos { + let slice = locator.slice_source_code_range(&Range::new(last_pos, start_location)); + output.push_str(slice); + } + + last_pos = end_location; + } + + // Add the remaining content. + let slice = locator.slice_source_code_at(last_pos); + output.push_str(slice); + output +} + +#[cfg(test)] +mod test { + use crate::rules::pyupgrade::fixes::remove_import_members; + + #[test] + fn once() { + let source = r#"from foo import bar, baz, bop, qux as q"#; + let expected = r#"from foo import bar, baz, qux as q"#; + let actual = remove_import_members(source, &["bop"]); + assert_eq!(expected, actual); + } + + #[test] + fn twice() { + let source = r#"from foo import bar, baz, bop, qux as q"#; + let expected = r#"from foo import bar, qux as q"#; + let actual = remove_import_members(source, &["baz", "bop"]); + assert_eq!(expected, actual); + } + + #[test] + fn aliased() { + let source = r#"from foo import bar, baz, bop as boop, qux as q"#; + let expected = r#"from foo import bar, baz, qux as q"#; + let actual = remove_import_members(source, &["bop"]); + assert_eq!(expected, actual); + } + + #[test] + fn parenthesized() { + let source = r#"from foo import (bar, baz, bop, qux as q)"#; + let expected = r#"from foo import (bar, baz, qux as q)"#; + let actual = remove_import_members(source, &["bop"]); + assert_eq!(expected, actual); + } + + #[test] + fn last_import() { + let source = r#"from foo import bar, baz, bop, qux as q"#; + let expected = r#"from foo import bar, baz, bop"#; + let actual = remove_import_members(source, &["qux"]); + assert_eq!(expected, actual); + } + + #[test] + fn first_import() { + let source = r#"from foo import bar, baz, bop, qux as q"#; + let expected = r#"from foo import baz, bop, qux as q"#; + let actual = remove_import_members(source, &["bar"]); + assert_eq!(expected, actual); + } + + #[test] + fn first_two_imports() { + let source = r#"from foo import bar, baz, bop, qux as q"#; + let expected = r#"from foo import bop, qux as q"#; + let actual = remove_import_members(source, &["bar", "baz"]); + assert_eq!(expected, actual); + } + + #[test] + fn first_two_imports_multiline() { + let source = r#"from foo import ( + bar, + baz, + bop, + qux as q +)"#; + let expected = r#"from foo import ( + bop, + qux as q +)"#; + let actual = remove_import_members(source, &["bar", "baz"]); + assert_eq!(expected, actual); + } + + #[test] + fn multiline_once() { + let source = r#"from foo import ( + bar, + baz, + bop, + qux as q, +)"#; + let expected = r#"from foo import ( + bar, + baz, + qux as q, +)"#; + let actual = remove_import_members(source, &["bop"]); + assert_eq!(expected, actual); + } + + #[test] + fn multiline_twice() { + let source = r#"from foo import ( + bar, + baz, + bop, + qux as q, +)"#; + let expected = r#"from foo import ( + bar, + qux as q, +)"#; + let actual = remove_import_members(source, &["baz", "bop"]); + assert_eq!(expected, actual); + } + + #[test] + fn multiline_comment() { + let source = r#"from foo import ( + bar, + baz, + # This comment should be removed. + bop, + # This comment should be retained. + qux as q, +)"#; + let expected = r#"from foo import ( + bar, + baz, + # This comment should be retained. + qux as q, +)"#; + let actual = remove_import_members(source, &["bop"]); + assert_eq!(expected, actual); + } + + #[test] + fn multi_comment_first_import() { + let source = r#"from foo import ( + # This comment should be retained. + bar, + # This comment should be removed. + baz, + bop, + qux as q, +)"#; + let expected = r#"from foo import ( + # This comment should be retained. + baz, + bop, + qux as q, +)"#; + let actual = remove_import_members(source, &["bar"]); + assert_eq!(expected, actual); + } +} diff --git a/src/rules/pyupgrade/mod.rs b/src/rules/pyupgrade/mod.rs index 2d86fc8859..e04a6a63c0 100644 --- a/src/rules/pyupgrade/mod.rs +++ b/src/rules/pyupgrade/mod.rs @@ -57,6 +57,7 @@ mod tests { #[test_case(Rule::FString, Path::new("UP032.py"); "UP032")] #[test_case(Rule::FunctoolsCache, Path::new("UP033.py"); "UP033")] #[test_case(Rule::ExtraneousParentheses, Path::new("UP034.py"); "UP034")] + #[test_case(Rule::ImportReplacements, Path::new("UP035.py"); "UP035")] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/src/rules/pyupgrade/rules/import_replacements.rs b/src/rules/pyupgrade/rules/import_replacements.rs new file mode 100644 index 0000000000..f3ca2a1494 --- /dev/null +++ b/src/rules/pyupgrade/rules/import_replacements.rs @@ -0,0 +1,465 @@ +use itertools::Itertools; +use rustpython_ast::{Alias, AliasData, Stmt}; + +use ruff_macros::derive_message_formats; + +use crate::ast::types::Range; +use crate::ast::whitespace::indentation; +use crate::checkers::ast::Checker; +use crate::fix::Fix; +use crate::registry::{Diagnostic, Rule}; +use crate::rules::pyupgrade::fixes; +use crate::settings::types::PythonVersion; +use crate::source_code::{Locator, Stylist}; +use crate::violation::{Availability, Violation}; +use crate::{define_violation, AutofixKind}; + +define_violation!( + pub struct ImportReplacements { + pub module: String, + pub members: Vec, + pub fixable: bool, + } +); +impl Violation for ImportReplacements { + const AUTOFIX: Option = Some(AutofixKind::new(Availability::Always)); + + #[derive_message_formats] + fn message(&self) -> String { + let ImportReplacements { + module, members, .. + } = self; + let names = members.iter().map(|name| format!("`{name}`")).join(", "); + format!("Import from `{module}` instead: {names}") + } + + fn autofix_title_formatter(&self) -> Option String> { + let ImportReplacements { fixable, .. } = self; + if *fixable { + Some(|ImportReplacements { module, .. }| format!("Import from `{module}`")) + } else { + None + } + } +} + +// A list of modules that may involve import rewrites. +const RELEVANT_MODULES: &[&str] = &[ + "collections", + "pipes", + "mypy_extensions", + "typing_extensions", + "typing", + "typing.re", +]; + +// Members of `collections` that have been moved to `collections.abc`. +const COLLECTIONS_TO_ABC: &[&str] = &[ + "AsyncGenerator", + "AsyncIterable", + "AsyncIterator", + "Awaitable", + "ByteString", + "Callable", + "Collection", + "Container", + "Coroutine", + "Generator", + "Hashable", + "ItemsView", + "Iterable", + "Iterator", + "KeysView", + "Mapping", + "MappingView", + "MutableMapping", + "MutableSequence", + "MutableSet", + "Reversible", + "Sequence", + "Set", + "Sized", + "ValuesView", +]; + +// Members of `pipes` that have been moved to `shlex`. +const PIPES_TO_SHLEX: &[&str] = &["quote"]; + +// Members of `typing_extensions` that have been moved to `typing`. +const TYPING_EXTENSIONS_TO_TYPING: &[&str] = &[ + "AsyncIterable", + "AsyncIterator", + "Awaitable", + "ClassVar", + "ContextManager", + "Coroutine", + "DefaultDict", + "NewType", + "TYPE_CHECKING", + "Text", + "Type", +]; + +// Python 3.7+ + +// Members of `mypy_extensions` that have been moved to `typing`. +const MYPY_EXTENSIONS_TO_TYPING_37: &[&str] = &["NoReturn"]; + +// Members of `typing_extensions` that have been moved to `typing`. +const TYPING_EXTENSIONS_TO_TYPING_37: &[&str] = &[ + "AsyncContextManager", + "AsyncGenerator", + "ChainMap", + "Counter", + "Deque", + "NoReturn", +]; + +// Python 3.8+ + +// Members of `mypy_extensions` that have been moved to `typing`. +const MYPY_EXTENSIONS_TO_TYPING_38: &[&str] = &["TypedDict"]; + +// Members of `typing_extensions` that have been moved to `typing`. +const TYPING_EXTENSIONS_TO_TYPING_38: &[&str] = &[ + "Final", + "Literal", + "OrderedDict", + "Protocol", + "SupportsIndex", + "runtime_checkable", +]; + +// Python 3.9+ + +// Members of `typing` that have been moved to `collections.abc`. +const TYPING_TO_COLLECTIONS_ABC_39: &[&str] = &[ + "AsyncGenerator", + "AsyncIterable", + "AsyncIterator", + "Awaitable", + "ByteString", + "ChainMap", + "Collection", + "Container", + "Coroutine", + "Counter", + "Generator", + "Hashable", + "ItemsView", + "Iterable", + "Iterator", + "KeysView", + "Mapping", + "MappingView", + "MutableMapping", + "MutableSequence", + "MutableSet", + "Reversible", + "Sequence", + "Sized", + "ValuesView", +]; + +// Members of `typing` that have been moved to `typing.re`. +const TYPING_TO_RE_39: &[&str] = &["Match", "Pattern"]; + +// Members of `typing.re` that have been moved to `re`. +const TYPING_RE_TO_RE_39: &[&str] = &["Match", "Pattern"]; + +// Members of `typing_extensions` that have been moved to `typing`. +const TYPING_EXTENSIONS_TO_TYPING_39: &[&str] = &["Annotated", "get_type_hints"]; + +// Python 3.10+ + +// Members of `typing` that have been moved to `collections.abc`. +const TYPING_TO_COLLECTIONS_ABC_310: &[&str] = &["Callable"]; + +// Members of `typing_extensions` that have been moved to `typing`. +const TYPING_EXTENSIONS_TO_TYPING_310: &[&str] = &[ + "Concatenate", + "ParamSpecArgs", + "ParamSpecKwargs", + "TypeAlias", + "TypeGuard", + "get_args", + "get_origin", + "is_typeddict", +]; + +// Python 3.11+ + +// Members of `typing_extensions` that have been moved to `typing`. +const TYPING_EXTENSIONS_TO_TYPING_311: &[&str] = &[ + "Any", + "LiteralString", + "NamedTuple", + "Never", + "NotRequired", + "Required", + "Self", + "TypedDict", + "Unpack", + "assert_never", + "assert_type", + "clear_overloads", + "dataclass_transform", + "final", + "get_overloads", + "overload", + "reveal_type", +]; + +struct Replacement<'a> { + module: &'a str, + members: Vec<&'a AliasData>, + content: Option, +} + +struct ImportReplacer<'a> { + stmt: &'a Stmt, + module: &'a str, + members: &'a [AliasData], + locator: &'a Locator<'a>, + stylist: &'a Stylist<'a>, + version: PythonVersion, +} + +impl<'a> ImportReplacer<'a> { + fn new( + stmt: &'a Stmt, + module: &'a str, + members: &'a [AliasData], + locator: &'a Locator<'a>, + stylist: &'a Stylist<'a>, + version: PythonVersion, + ) -> Self { + Self { + stmt, + module, + members, + locator, + stylist, + version, + } + } + + fn replacements(&self) -> Vec { + let mut replacements = vec![]; + match self.module { + "collections" => { + if let Some(replacement) = self.try_replace(COLLECTIONS_TO_ABC, "collections.abc") { + replacements.push(replacement); + } + } + "pipes" => { + if let Some(replacement) = self.try_replace(PIPES_TO_SHLEX, "shlex") { + replacements.push(replacement); + } + } + "typing_extensions" => { + let mut typing_extensions_to_typing = TYPING_EXTENSIONS_TO_TYPING.to_vec(); + if self.version >= PythonVersion::Py37 { + typing_extensions_to_typing.extend(TYPING_EXTENSIONS_TO_TYPING_37); + } + if self.version >= PythonVersion::Py38 { + typing_extensions_to_typing.extend(TYPING_EXTENSIONS_TO_TYPING_38); + } + if self.version >= PythonVersion::Py39 { + typing_extensions_to_typing.extend(TYPING_EXTENSIONS_TO_TYPING_39); + } + if self.version >= PythonVersion::Py310 { + typing_extensions_to_typing.extend(TYPING_EXTENSIONS_TO_TYPING_310); + } + if self.version >= PythonVersion::Py311 { + typing_extensions_to_typing.extend(TYPING_EXTENSIONS_TO_TYPING_311); + } + if let Some(replacement) = self.try_replace(&typing_extensions_to_typing, "typing") + { + replacements.push(replacement); + } + } + "mypy_extensions" => { + let mut mypy_extensions_to_typing = vec![]; + if self.version >= PythonVersion::Py37 { + mypy_extensions_to_typing.extend(MYPY_EXTENSIONS_TO_TYPING_37); + } + if self.version >= PythonVersion::Py38 { + mypy_extensions_to_typing.extend(MYPY_EXTENSIONS_TO_TYPING_38); + } + if let Some(replacement) = self.try_replace(&mypy_extensions_to_typing, "typing") { + replacements.push(replacement); + } + } + "typing" => { + // `typing` to `collections.abc` + let mut typing_to_collections_abc = vec![]; + if self.version >= PythonVersion::Py39 { + typing_to_collections_abc.extend(TYPING_TO_COLLECTIONS_ABC_39); + } + if self.version >= PythonVersion::Py310 { + typing_to_collections_abc.extend(TYPING_TO_COLLECTIONS_ABC_310); + } + if let Some(replacement) = + self.try_replace(&typing_to_collections_abc, "collections.abc") + { + replacements.push(replacement); + } + + // `typing` to `re` + let mut typing_to_re = vec![]; + if self.version >= PythonVersion::Py39 { + typing_to_re.extend(TYPING_TO_RE_39); + } + if let Some(replacement) = self.try_replace(&typing_to_re, "re") { + replacements.push(replacement); + } + } + "typing.re" if self.version >= PythonVersion::Py39 => { + if let Some(replacement) = self.try_replace(TYPING_RE_TO_RE_39, "re") { + replacements.push(replacement); + } + } + _ => {} + } + replacements + } + + fn try_replace(&'a self, candidates: &[&str], target: &'a str) -> Option> { + let (matched_names, unmatched_names) = self.partition_imports(candidates); + + // If we have no matched names, we don't need to do anything. + if matched_names.is_empty() { + return None; + } + + if unmatched_names.is_empty() { + let matched = ImportReplacer::format_import_from(&matched_names, target); + Some(Replacement { + module: target, + members: matched_names, + content: Some(matched), + }) + } else { + let indentation = indentation(self.locator, self.stmt); + + // If we have matched _and_ unmatched names, but the import is not on its own line, we + // can't add a statement after it. For example, if we have `if True: import foo`, we can't + // add a statement to the next line. + let Some(indentation) = indentation else { + return Some(Replacement { + module: target, + members: matched_names, + content: None, + }); + }; + + let matched = ImportReplacer::format_import_from(&matched_names, target); + let unmatched = fixes::remove_import_members( + self.locator + .slice_source_code_range(&Range::from_located(self.stmt)), + &matched_names + .iter() + .map(|name| name.name.as_str()) + .collect::>(), + ); + + Some(Replacement { + module: target, + members: matched_names, + content: Some(format!( + "{unmatched}{}{}{matched}", + self.stylist.line_ending().as_str(), + indentation, + )), + }) + } + } + + /// Partitions imports into matched and unmatched names. + fn partition_imports(&self, candidates: &[&str]) -> (Vec<&AliasData>, Vec<&AliasData>) { + let mut matched_names = vec![]; + let mut unmatched_names = vec![]; + for name in self.members { + if candidates.contains(&name.name.as_str()) { + matched_names.push(name); + } else { + unmatched_names.push(name); + } + } + (matched_names, unmatched_names) + } + + /// Converts a list of names and a module into an `import from`-style import. + fn format_import_from(names: &[&AliasData], module: &str) -> String { + // Construct the whitespace strings. + // Generate the formatted names. + let full_names: String = names + .iter() + .map(|name| match &name.asname { + Some(asname) => format!("{} as {asname}", name.name), + None => format!("{}", name.name), + }) + .join(", "); + format!("from {module} import {full_names}") + } +} + +/// UP035 +pub fn import_replacements( + checker: &mut Checker, + stmt: &Stmt, + names: &[Alias], + module: Option<&str>, + level: Option<&usize>, +) { + // Avoid relative and star imports. + if level.map_or(false, |level| *level > 0) { + return; + } + if names.first().map_or(false, |name| name.node.name == "*") { + return; + } + let Some(module) = module else { + return; + }; + + if !RELEVANT_MODULES.contains(&module) { + return; + } + + let members: Vec = names.iter().map(|alias| alias.node.clone()).collect(); + let fixer = ImportReplacer::new( + stmt, + module, + &members, + checker.locator, + checker.stylist, + checker.settings.target_version, + ); + + for replacement in fixer.replacements() { + let mut diagnostic = Diagnostic::new( + ImportReplacements { + module: replacement.module.to_string(), + members: replacement + .members + .iter() + .map(|name| name.name.to_string()) + .collect(), + fixable: replacement.content.is_some(), + }, + Range::from_located(stmt), + ); + if checker.patch(&Rule::ImportReplacements) { + if let Some(content) = replacement.content { + diagnostic.amend(Fix::replacement( + content, + stmt.location, + stmt.end_location.unwrap(), + )); + } + } + checker.diagnostics.push(diagnostic); + } +} diff --git a/src/rules/pyupgrade/rules/mod.rs b/src/rules/pyupgrade/rules/mod.rs index 3cc503d266..89117f6d08 100644 --- a/src/rules/pyupgrade/rules/mod.rs +++ b/src/rules/pyupgrade/rules/mod.rs @@ -6,6 +6,7 @@ pub(crate) use extraneous_parentheses::extraneous_parentheses; pub(crate) use f_strings::f_strings; pub(crate) use format_literals::format_literals; pub(crate) use functools_cache::functools_cache; +pub(crate) use import_replacements::{import_replacements, ImportReplacements}; pub(crate) use lru_cache_without_parameters::lru_cache_without_parameters; pub(crate) use native_literals::native_literals; use once_cell::sync::Lazy; @@ -48,6 +49,7 @@ mod extraneous_parentheses; mod f_strings; mod format_literals; mod functools_cache; +mod import_replacements; mod lru_cache_without_parameters; mod native_literals; mod open_alias; diff --git a/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP035_UP035.py.snap b/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP035_UP035.py.snap new file mode 100644 index 0000000000..e1b7d2c483 --- /dev/null +++ b/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP035_UP035.py.snap @@ -0,0 +1,390 @@ +--- +source: src/rules/pyupgrade/mod.rs +expression: diagnostics +--- +- kind: + ImportReplacements: + module: collections.abc + members: + - Mapping + fixable: true + location: + row: 2 + column: 0 + end_location: + row: 2 + column: 31 + fix: + content: + - from collections.abc import Mapping + location: + row: 2 + column: 0 + end_location: + row: 2 + column: 31 + parent: ~ +- kind: + ImportReplacements: + module: collections.abc + members: + - Mapping + fixable: true + location: + row: 4 + column: 0 + end_location: + row: 4 + column: 38 + fix: + content: + - from collections.abc import Mapping as MAP + location: + row: 4 + column: 0 + end_location: + row: 4 + column: 38 + parent: ~ +- kind: + ImportReplacements: + module: collections.abc + members: + - Mapping + - Sequence + fixable: true + location: + row: 6 + column: 0 + end_location: + row: 6 + column: 41 + fix: + content: + - "from collections.abc import Mapping, Sequence" + location: + row: 6 + column: 0 + end_location: + row: 6 + column: 41 + parent: ~ +- kind: + ImportReplacements: + module: collections.abc + members: + - Mapping + fixable: true + location: + row: 8 + column: 0 + end_location: + row: 8 + column: 40 + fix: + content: + - from collections import Counter + - from collections.abc import Mapping + location: + row: 8 + column: 0 + end_location: + row: 8 + column: 40 + parent: ~ +- kind: + ImportReplacements: + module: collections.abc + members: + - Mapping + fixable: true + location: + row: 10 + column: 0 + end_location: + row: 10 + column: 42 + fix: + content: + - from collections import (Counter) + - from collections.abc import Mapping + location: + row: 10 + column: 0 + end_location: + row: 10 + column: 42 + parent: ~ +- kind: + ImportReplacements: + module: collections.abc + members: + - Mapping + fixable: true + location: + row: 12 + column: 0 + end_location: + row: 13 + column: 33 + fix: + content: + - from collections import (Counter) + - from collections.abc import Mapping + location: + row: 12 + column: 0 + end_location: + row: 13 + column: 33 + parent: ~ +- kind: + ImportReplacements: + module: collections.abc + members: + - Mapping + fixable: true + location: + row: 15 + column: 0 + end_location: + row: 16 + column: 32 + fix: + content: + - from collections import Counter + - from collections.abc import Mapping + location: + row: 15 + column: 0 + end_location: + row: 16 + column: 32 + parent: ~ +- kind: + ImportReplacements: + module: collections.abc + members: + - Mapping + - Sequence + fixable: true + location: + row: 18 + column: 0 + end_location: + row: 18 + column: 50 + fix: + content: + - from collections import Counter + - "from collections.abc import Mapping, Sequence" + location: + row: 18 + column: 0 + end_location: + row: 18 + column: 50 + parent: ~ +- kind: + ImportReplacements: + module: collections.abc + members: + - Mapping + fixable: true + location: + row: 20 + column: 0 + end_location: + row: 20 + column: 51 + fix: + content: + - from collections import Counter + - from collections.abc import Mapping as mapping + location: + row: 20 + column: 0 + end_location: + row: 20 + column: 51 + parent: ~ +- kind: + ImportReplacements: + module: collections.abc + members: + - Mapping + fixable: true + location: + row: 23 + column: 4 + end_location: + row: 23 + column: 44 + fix: + content: + - from collections import Counter + - " from collections.abc import Mapping" + location: + row: 23 + column: 4 + end_location: + row: 23 + column: 44 + parent: ~ +- kind: + ImportReplacements: + module: collections.abc + members: + - Mapping + fixable: true + location: + row: 28 + column: 4 + end_location: + row: 28 + column: 44 + fix: + content: + - from collections import Counter + - " from collections.abc import Mapping" + location: + row: 28 + column: 4 + end_location: + row: 28 + column: 44 + parent: ~ +- kind: + ImportReplacements: + module: collections.abc + members: + - Mapping + fixable: true + location: + row: 30 + column: 9 + end_location: + row: 30 + column: 40 + fix: + content: + - from collections.abc import Mapping + location: + row: 30 + column: 9 + end_location: + row: 30 + column: 40 + parent: ~ +- kind: + ImportReplacements: + module: collections.abc + members: + - Mapping + fixable: true + location: + row: 33 + column: 0 + end_location: + row: 33 + column: 40 + fix: + content: + - from collections import Counter + - from collections.abc import Mapping + location: + row: 33 + column: 0 + end_location: + row: 33 + column: 40 + parent: ~ +- kind: + ImportReplacements: + module: collections.abc + members: + - Mapping + - Callable + fixable: true + location: + row: 37 + column: 4 + end_location: + row: 42 + column: 5 + fix: + content: + - from collections import ( + - " Bad," + - " Good," + - " )" + - " from collections.abc import Mapping, Callable" + location: + row: 37 + column: 4 + end_location: + row: 42 + column: 5 + parent: ~ +- kind: + ImportReplacements: + module: collections.abc + members: + - Callable + fixable: true + location: + row: 44 + column: 0 + end_location: + row: 44 + column: 49 + fix: + content: + - "from typing import Match, Pattern, List" + - from collections.abc import Callable + location: + row: 44 + column: 0 + end_location: + row: 44 + column: 49 + parent: ~ +- kind: + ImportReplacements: + module: re + members: + - Match + - Pattern + fixable: true + location: + row: 44 + column: 0 + end_location: + row: 44 + column: 49 + fix: + content: + - "from typing import Callable, List" + - "from re import Match, Pattern" + location: + row: 44 + column: 0 + end_location: + row: 44 + column: 49 + parent: ~ +- kind: + ImportReplacements: + module: collections.abc + members: + - Mapping + fixable: false + location: + row: 46 + column: 9 + end_location: + row: 47 + column: 21 + fix: ~ + parent: ~ +