Pyupgrade: import mock to from unittest import mock (#1488)

This commit is contained in:
Colin Delahunty
2023-01-01 02:25:06 +00:00
committed by GitHub
parent f2c9f94f73
commit 70895a8f1e
14 changed files with 710 additions and 4 deletions

View File

@@ -643,6 +643,9 @@ where
if self.settings.enabled.contains(&CheckCode::UP023) {
pyupgrade::plugins::replace_c_element_tree(self, stmt);
}
if self.settings.enabled.contains(&CheckCode::UP026) {
pyupgrade::plugins::rewrite_mock_import(self, stmt);
}
for alias in names {
if alias.node.name.contains('.') && alias.node.asname.is_none() {
@@ -854,6 +857,9 @@ where
pyupgrade::plugins::unnecessary_future_import(self, stmt, names);
}
}
if self.settings.enabled.contains(&CheckCode::UP026) {
pyupgrade::plugins::rewrite_mock_import(self, stmt);
}
if self.settings.enabled.contains(&CheckCode::TID251) {
if let Some(module) = module {

View File

@@ -238,6 +238,7 @@ pub enum CheckCode {
UP023,
UP024,
UP025,
UP026,
// pydocstyle
D100,
D101,
@@ -910,6 +911,7 @@ pub enum CheckKind {
RewriteCElementTree,
OSErrorAlias(Option<String>),
RewriteUnicodeLiteral,
RewriteMockImport,
// pydocstyle
BlankLineAfterLastSection(String),
BlankLineAfterSection(String),
@@ -1305,6 +1307,7 @@ impl CheckCode {
CheckCode::UP023 => CheckKind::RewriteCElementTree,
CheckCode::UP024 => CheckKind::OSErrorAlias(None),
CheckCode::UP025 => CheckKind::RewriteUnicodeLiteral,
CheckCode::UP026 => CheckKind::RewriteMockImport,
// pydocstyle
CheckCode::D100 => CheckKind::PublicModule,
CheckCode::D101 => CheckKind::PublicClass,
@@ -1738,6 +1741,7 @@ impl CheckCode {
CheckCode::UP023 => CheckCategory::Pyupgrade,
CheckCode::UP024 => CheckCategory::Pyupgrade,
CheckCode::UP025 => CheckCategory::Pyupgrade,
CheckCode::UP026 => CheckCategory::Pyupgrade,
CheckCode::W292 => CheckCategory::Pycodestyle,
CheckCode::W605 => CheckCategory::Pycodestyle,
CheckCode::YTT101 => CheckCategory::Flake82020,
@@ -1961,6 +1965,7 @@ impl CheckKind {
CheckKind::RewriteCElementTree => &CheckCode::UP023,
CheckKind::OSErrorAlias(..) => &CheckCode::UP024,
CheckKind::RewriteUnicodeLiteral => &CheckCode::UP025,
CheckKind::RewriteMockImport => &CheckCode::UP026,
// pydocstyle
CheckKind::BlankLineAfterLastSection(..) => &CheckCode::D413,
CheckKind::BlankLineAfterSection(..) => &CheckCode::D410,
@@ -2722,6 +2727,7 @@ impl CheckKind {
}
CheckKind::OSErrorAlias(..) => "Replace aliased errors with `OSError`".to_string(),
CheckKind::RewriteUnicodeLiteral => "Remove unicode literals from strings".to_string(),
CheckKind::RewriteMockImport => "`mock` is deprecated, use `unittest.mock`".to_string(),
// pydocstyle
CheckKind::FitsOnOneLine => "One-line docstring should fit on one line".to_string(),
CheckKind::BlankLineAfterSummary => {
@@ -3190,6 +3196,7 @@ impl CheckKind {
| CheckKind::ReplaceStdoutStderr
| CheckKind::ReplaceUniversalNewlines
| CheckKind::RewriteCElementTree
| CheckKind::RewriteMockImport
| CheckKind::RewriteUnicodeLiteral
| CheckKind::SectionNameEndsInColon(..)
| CheckKind::SectionNotOverIndented(..)
@@ -3302,6 +3309,7 @@ impl CheckKind {
}
CheckKind::RewriteCElementTree => Some("Replace with `ElementTree`".to_string()),
CheckKind::RewriteUnicodeLiteral => Some("Remove unicode prefix".to_string()),
CheckKind::RewriteMockImport => Some("Import from `unittest.mock` instead".to_string()),
CheckKind::NewLineAfterSectionName(name) => {
Some(format!("Add newline after \"{name}\""))
}

View File

@@ -542,6 +542,7 @@ pub enum CheckCodePrefix {
UP023,
UP024,
UP025,
UP026,
W,
W2,
W29,
@@ -776,6 +777,7 @@ impl CheckCodePrefix {
CheckCode::UP023,
CheckCode::UP024,
CheckCode::UP025,
CheckCode::UP026,
CheckCode::D100,
CheckCode::D101,
CheckCode::D102,
@@ -2456,6 +2458,7 @@ impl CheckCodePrefix {
CheckCode::UP023,
CheckCode::UP024,
CheckCode::UP025,
CheckCode::UP026,
]
}
CheckCodePrefix::U0 => {
@@ -2490,6 +2493,7 @@ impl CheckCodePrefix {
CheckCode::UP023,
CheckCode::UP024,
CheckCode::UP025,
CheckCode::UP026,
]
}
CheckCodePrefix::U00 => {
@@ -2708,6 +2712,7 @@ impl CheckCodePrefix {
CheckCode::UP023,
CheckCode::UP024,
CheckCode::UP025,
CheckCode::UP026,
],
CheckCodePrefix::UP0 => vec![
CheckCode::UP001,
@@ -2734,6 +2739,7 @@ impl CheckCodePrefix {
CheckCode::UP023,
CheckCode::UP024,
CheckCode::UP025,
CheckCode::UP026,
],
CheckCodePrefix::UP00 => vec![
CheckCode::UP001,
@@ -2782,6 +2788,7 @@ impl CheckCodePrefix {
CheckCode::UP023,
CheckCode::UP024,
CheckCode::UP025,
CheckCode::UP026,
],
CheckCodePrefix::UP020 => vec![CheckCode::UP020],
CheckCodePrefix::UP021 => vec![CheckCode::UP021],
@@ -2789,6 +2796,7 @@ impl CheckCodePrefix {
CheckCodePrefix::UP023 => vec![CheckCode::UP023],
CheckCodePrefix::UP024 => vec![CheckCode::UP024],
CheckCodePrefix::UP025 => vec![CheckCode::UP025],
CheckCodePrefix::UP026 => vec![CheckCode::UP026],
CheckCodePrefix::W => vec![CheckCode::W292, CheckCode::W605],
CheckCodePrefix::W2 => vec![CheckCode::W292],
CheckCodePrefix::W29 => vec![CheckCode::W292],
@@ -3362,6 +3370,7 @@ impl CheckCodePrefix {
CheckCodePrefix::UP023 => SuffixLength::Three,
CheckCodePrefix::UP024 => SuffixLength::Three,
CheckCodePrefix::UP025 => SuffixLength::Three,
CheckCodePrefix::UP026 => SuffixLength::Three,
CheckCodePrefix::W => SuffixLength::Zero,
CheckCodePrefix::W2 => SuffixLength::One,
CheckCodePrefix::W29 => SuffixLength::Two,

View File

@@ -1,5 +1,5 @@
use anyhow::{bail, Result};
use libcst_native::{Expr, Module, SmallStatement, Statement};
use libcst_native::{Expr, Import, ImportFrom, Module, SmallStatement, Statement};
pub fn match_module(module_text: &str) -> Result<Module> {
match libcst_native::parse_module(module_text, None) {
@@ -19,3 +19,27 @@ pub fn match_expr<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut Expr<'b>
bail!("Expected Statement::Simple")
}
}
pub fn match_import<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut Import<'b>> {
if let Some(Statement::Simple(expr)) = module.body.first_mut() {
if let Some(SmallStatement::Import(expr)) = expr.body.first_mut() {
Ok(expr)
} else {
bail!("Expected SmallStatement::Expr")
}
} else {
bail!("Expected Statement::Simple")
}
}
pub fn match_import_from<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut ImportFrom<'b>> {
if let Some(Statement::Simple(expr)) = module.body.first_mut() {
if let Some(SmallStatement::ImportFrom(expr)) = expr.body.first_mut() {
Ok(expr)
} else {
bail!("Expected SmallStatement::Expr")
}
} else {
bail!("Expected Statement::Simple")
}
}

View File

@@ -23,12 +23,12 @@ use crate::SourceCodeLocator;
mod categorize;
mod comments;
pub mod format;
mod helpers;
pub mod helpers;
pub mod plugins;
pub mod settings;
mod sorting;
pub mod track;
mod types;
pub mod types;
#[derive(Debug)]
pub struct AnnotatedAliasData<'a> {

View File

@@ -46,6 +46,7 @@ mod tests {
#[test_case(CheckCode::UP024, Path::new("UP024_1.py"); "UP024_1")]
#[test_case(CheckCode::UP024, Path::new("UP024_2.py"); "UP024_2")]
#[test_case(CheckCode::UP025, Path::new("UP025.py"); "UP025")]
#[test_case(CheckCode::UP026, Path::new("UP026.py"); "UP026")]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
let checks = test_path(

View File

@@ -10,6 +10,7 @@ pub use remove_six_compat::remove_six_compat;
pub use replace_stdout_stderr::replace_stdout_stderr;
pub use replace_universal_newlines::replace_universal_newlines;
pub use rewrite_c_element_tree::replace_c_element_tree;
pub use rewrite_mock_import::rewrite_mock_import;
pub use rewrite_unicode_literal::rewrite_unicode_literal;
pub use super_call_with_parameters::super_call_with_parameters;
pub use type_of_primitive::type_of_primitive;
@@ -34,6 +35,7 @@ mod remove_six_compat;
mod replace_stdout_stderr;
mod replace_universal_newlines;
mod rewrite_c_element_tree;
mod rewrite_mock_import;
mod rewrite_unicode_literal;
mod super_call_with_parameters;
mod type_of_primitive;

View File

@@ -0,0 +1,249 @@
use anyhow::Result;
use libcst_native::{
AsName, AssignTargetExpression, Attribute, Codegen, CodegenState, Dot, Expression, Import,
ImportAlias, ImportFrom, ImportNames, Name, NameOrAttribute, ParenthesizableWhitespace,
};
use log::error;
use rustpython_ast::{Stmt, StmtKind};
use crate::ast::types::Range;
use crate::ast::whitespace::indentation;
use crate::autofix::Fix;
use crate::checkers::ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::cst::matchers::{match_import, match_import_from, match_module};
use crate::source_code_locator::SourceCodeLocator;
use crate::source_code_style::SourceCodeStyleDetector;
/// Return a vector of all non-`mock` imports.
fn clean_import_aliases(aliases: Vec<ImportAlias>) -> (Vec<ImportAlias>, Vec<Option<AsName>>) {
let mut clean_aliases: Vec<ImportAlias> = vec![];
let mut mock_aliases: Vec<Option<AsName>> = vec![];
for alias in aliases {
match &alias.name {
// Ex) `import mock`
NameOrAttribute::N(name_struct) => {
if name_struct.value == "mock" {
mock_aliases.push(alias.asname.clone());
continue;
}
clean_aliases.push(alias);
}
// Ex) `import mock.mock`
NameOrAttribute::A(attribute_struct) => {
if let Expression::Name(name_struct) = &*attribute_struct.value {
if name_struct.value == "mock" && attribute_struct.attr.value == "mock" {
mock_aliases.push(alias.asname.clone());
continue;
}
}
clean_aliases.push(alias);
}
}
}
(clean_aliases, mock_aliases)
}
/// Return `true` if the aliases contain `mock`.
fn includes_mock_member(aliases: &[ImportAlias]) -> bool {
for alias in aliases {
let ImportAlias { name, .. } = &alias;
// Ex) `import mock.mock`
if let NameOrAttribute::A(attribute_struct) = name {
if let Expression::Name(name_struct) = &*attribute_struct.value {
if name_struct.value == "mock" && attribute_struct.attr.value == "mock" {
return true;
}
}
}
}
false
}
fn format_mocks(
aliases: Vec<Option<AsName>>,
indent: &str,
stylist: &SourceCodeStyleDetector,
) -> String {
let mut content = String::new();
for alias in aliases {
match alias {
None => {
if !content.is_empty() {
content.push_str(stylist.line_ending());
content.push_str(indent);
}
content.push_str("from unittest import mock");
}
Some(as_name) => {
if let AssignTargetExpression::Name(name) = as_name.name {
if !content.is_empty() {
content.push_str(stylist.line_ending());
content.push_str(indent);
}
content.push_str("from unittest import mock as ");
content.push_str(name.value);
}
}
}
}
content
}
/// Format the `import mock` rewrite.
fn format_import(
stmt: &Stmt,
indent: &str,
locator: &SourceCodeLocator,
stylist: &SourceCodeStyleDetector,
) -> Result<String> {
let module_text = locator.slice_source_code_range(&Range::from_located(stmt));
let mut tree = match_module(&module_text)?;
let mut import = match_import(&mut tree)?;
let Import { names, .. } = import.clone();
let (clean_aliases, mock_aliases) = clean_import_aliases(names);
Ok(if clean_aliases.is_empty() {
format_mocks(mock_aliases, indent, stylist)
} else {
import.names = clean_aliases;
let mut state = CodegenState::default();
tree.codegen(&mut state);
let mut content = state.to_string();
content.push_str(stylist.line_ending());
content.push_str(indent);
content.push_str(&format_mocks(mock_aliases, indent, stylist));
content
})
}
/// Format the `from mock import ...` rewrite.
fn format_import_from(
stmt: &Stmt,
indent: &str,
locator: &SourceCodeLocator,
stylist: &SourceCodeStyleDetector,
) -> Result<String> {
let module_text = locator.slice_source_code_range(&Range::from_located(stmt));
let mut tree = match_module(&module_text).unwrap();
let mut import = match_import_from(&mut tree)?;
let ImportFrom {
names: ImportNames::Aliases(names),
..
} = import.clone() else {
unreachable!("Expected ImportNames::Aliases");
};
let has_mock_member = includes_mock_member(&names);
let (clean_aliases, mock_aliases) = clean_import_aliases(names);
Ok(if clean_aliases.is_empty() {
format_mocks(mock_aliases, indent, stylist)
} else {
import.names = ImportNames::Aliases(clean_aliases);
import.module = Some(NameOrAttribute::A(Box::new(Attribute {
value: Box::new(Expression::Name(Box::new(Name {
value: "unittest",
lpar: vec![],
rpar: vec![],
}))),
attr: Name {
value: "mock",
lpar: vec![],
rpar: vec![],
},
dot: Dot {
whitespace_before: ParenthesizableWhitespace::default(),
whitespace_after: ParenthesizableWhitespace::default(),
},
lpar: vec![],
rpar: vec![],
})));
let mut state = CodegenState::default();
tree.codegen(&mut state);
let mut content = state.to_string();
if has_mock_member {
content.push_str(stylist.line_ending());
content.push_str(indent);
content.push_str(&format_mocks(mock_aliases, indent, stylist));
}
content
})
}
/// UP026
pub fn rewrite_mock_import(checker: &mut Checker, stmt: &Stmt) {
match &stmt.node {
StmtKind::Import { names } => {
// Find all `mock` imports.
if names
.iter()
.any(|name| name.node.name == "mock" || name.node.name == "mock.mock")
{
// Generate the fix, if needed, which is shared between all `mock` imports.
let content = if checker.patch(&CheckCode::UP026) {
let indent = indentation(checker, stmt);
match format_import(stmt, &indent, checker.locator, checker.style) {
Ok(content) => Some(content),
Err(e) => {
error!("Failed to rewrite `mock` import: {e}");
None
}
}
} else {
None
};
// Add a `Check` for each `mock` import.
for name in names {
if name.node.name == "mock" || name.node.name == "mock.mock" {
let mut check =
Check::new(CheckKind::RewriteMockImport, Range::from_located(name));
if let Some(content) = content.as_ref() {
check.amend(Fix::replacement(
content.clone(),
stmt.location,
stmt.end_location.unwrap(),
));
}
checker.add_check(check);
}
}
}
}
StmtKind::ImportFrom {
module: Some(module),
level,
..
} => {
if level.map_or(false, |level| level > 0) {
return;
}
if module == "mock" {
let mut check = Check::new(CheckKind::RewriteMockImport, Range::from_located(stmt));
if checker.patch(&CheckCode::UP026) {
let indent = indentation(checker, stmt);
match format_import_from(stmt, &indent, checker.locator, checker.style) {
Ok(content) => {
check.amend(Fix::replacement(
content,
stmt.location,
stmt.end_location.unwrap(),
));
}
Err(e) => error!("Failed to rewrite `mock` import: {e}"),
}
}
checker.add_check(check);
}
}
_ => (),
}
}

View File

@@ -80,7 +80,7 @@ pub fn unnecessary_future_import(checker: &mut Checker, stmt: &Stmt, names: &[Lo
}
check.amend(fix);
}
Err(e) => error!("Failed to remove __future__ import: {e}"),
Err(e) => error!("Failed to remove `__future__` import: {e}"),
}
}
checker.add_check(check);

View File

@@ -0,0 +1,325 @@
---
source: src/pyupgrade/mod.rs
expression: checks
---
- kind: RewriteMockImport
location:
row: 3
column: 11
end_location:
row: 3
column: 15
fix:
content: from unittest import mock
location:
row: 3
column: 4
end_location:
row: 3
column: 15
parent: ~
- kind: RewriteMockImport
location:
row: 6
column: 11
end_location:
row: 6
column: 15
fix:
content: "import sys\n from unittest import mock"
location:
row: 6
column: 4
end_location:
row: 6
column: 20
parent: ~
- kind: RewriteMockImport
location:
row: 9
column: 7
end_location:
row: 9
column: 16
fix:
content: from unittest import mock
location:
row: 9
column: 0
end_location:
row: 9
column: 16
parent: ~
- kind: RewriteMockImport
location:
row: 12
column: 19
end_location:
row: 12
column: 23
fix:
content: "import contextlib, sys\nfrom unittest import mock"
location:
row: 12
column: 0
end_location:
row: 12
column: 28
parent: ~
- kind: RewriteMockImport
location:
row: 15
column: 7
end_location:
row: 15
column: 11
fix:
content: "import sys\nfrom unittest import mock"
location:
row: 15
column: 0
end_location:
row: 15
column: 16
parent: ~
- kind: RewriteMockImport
location:
row: 19
column: 0
end_location:
row: 19
column: 21
fix:
content: from unittest import mock
location:
row: 19
column: 0
end_location:
row: 19
column: 21
parent: ~
- kind: RewriteMockImport
location:
row: 22
column: 0
end_location:
row: 27
column: 1
fix:
content: "from unittest.mock import (\n a,\n b,\n c,\n)"
location:
row: 22
column: 0
end_location:
row: 27
column: 1
parent: ~
- kind: RewriteMockImport
location:
row: 30
column: 0
end_location:
row: 35
column: 1
fix:
content: "from unittest.mock import (\n a,\n b,\n c\n)"
location:
row: 30
column: 0
end_location:
row: 35
column: 1
parent: ~
- kind: RewriteMockImport
location:
row: 39
column: 8
end_location:
row: 44
column: 9
fix:
content: "from unittest.mock import (\n a,\n b,\n c\n )"
location:
row: 39
column: 8
end_location:
row: 44
column: 9
parent: ~
- kind: RewriteMockImport
location:
row: 50
column: 7
end_location:
row: 50
column: 11
fix:
content: "from unittest import mock\nfrom unittest import mock"
location:
row: 50
column: 0
end_location:
row: 50
column: 17
parent: ~
- kind: RewriteMockImport
location:
row: 50
column: 13
end_location:
row: 50
column: 17
fix:
content: "from unittest import mock\nfrom unittest import mock"
location:
row: 50
column: 0
end_location:
row: 50
column: 17
parent: ~
- kind: RewriteMockImport
location:
row: 53
column: 7
end_location:
row: 53
column: 18
fix:
content: from unittest import mock as foo
location:
row: 53
column: 0
end_location:
row: 53
column: 18
parent: ~
- kind: RewriteMockImport
location:
row: 56
column: 0
end_location:
row: 56
column: 28
fix:
content: from unittest import mock as foo
location:
row: 56
column: 0
end_location:
row: 56
column: 28
parent: ~
- kind: RewriteMockImport
location:
row: 60
column: 11
end_location:
row: 60
column: 22
fix:
content: "from unittest import mock as foo\n from unittest import mock as bar\n from unittest import mock"
location:
row: 60
column: 4
end_location:
row: 60
column: 41
parent: ~
- kind: RewriteMockImport
location:
row: 60
column: 24
end_location:
row: 60
column: 35
fix:
content: "from unittest import mock as foo\n from unittest import mock as bar\n from unittest import mock"
location:
row: 60
column: 4
end_location:
row: 60
column: 41
parent: ~
- kind: RewriteMockImport
location:
row: 60
column: 37
end_location:
row: 60
column: 41
fix:
content: "from unittest import mock as foo\n from unittest import mock as bar\n from unittest import mock"
location:
row: 60
column: 4
end_location:
row: 60
column: 41
parent: ~
- kind: RewriteMockImport
location:
row: 63
column: 11
end_location:
row: 63
column: 22
fix:
content: "import os\n from unittest import mock as foo\n from unittest import mock as bar\n from unittest import mock"
location:
row: 63
column: 4
end_location:
row: 63
column: 45
parent: ~
- kind: RewriteMockImport
location:
row: 63
column: 24
end_location:
row: 63
column: 35
fix:
content: "import os\n from unittest import mock as foo\n from unittest import mock as bar\n from unittest import mock"
location:
row: 63
column: 4
end_location:
row: 63
column: 45
parent: ~
- kind: RewriteMockImport
location:
row: 63
column: 37
end_location:
row: 63
column: 41
fix:
content: "import os\n from unittest import mock as foo\n from unittest import mock as bar\n from unittest import mock"
location:
row: 63
column: 4
end_location:
row: 63
column: 45
parent: ~
- kind: RewriteMockImport
location:
row: 67
column: 4
end_location:
row: 67
column: 51
fix:
content: "from unittest import mock as foo\n from unittest import mock as bar\n from unittest import mock"
location:
row: 67
column: 4
end_location:
row: 67
column: 51
parent: ~