[`pyupgrade`] Implement import-replacement rule (`UP035`) (#2049)

This commit is contained in:
Colin Delahunty 2023-01-30 19:58:28 -05:00 committed by GitHub
parent 69e20c4554
commit ad8693e3de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1134 additions and 0 deletions

View File

@ -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)

View File

@ -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

View File

@ -1892,6 +1892,7 @@
"UP032",
"UP033",
"UP034",
"UP035",
"W",
"W2",
"W29",

View File

@ -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);

View File

@ -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,

View File

@ -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<Range> = vec![];
let mut commas: Vec<Range> = vec![];
let mut removal_indices: Vec<usize> = 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);
}
}

View File

@ -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(

View File

@ -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<String>,
pub fixable: bool,
}
);
impl Violation for ImportReplacements {
const AUTOFIX: Option<AutofixKind> = 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<fn(&Self) -> 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<String>,
}
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<Replacement> {
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<Replacement<'a>> {
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::<Vec<_>>(),
);
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<AliasData> = 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);
}
}

View File

@ -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;

View File

@ -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: ~