use crate::fixtures::imports::is_stdlib_module;
use crate::fixtures::string_utils::replace_identifier;
use crate::fixtures::types::TypeImportSpec;
use rustpython_parser::ast::{Mod, Stmt};
use rustpython_parser::Mode;
use std::collections::HashMap;
use tracing::{debug, info, warn};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImportKind {
Future,
Stdlib,
ThirdParty,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ImportGroup {
pub first_line: usize,
pub last_line: usize,
pub kind: ImportKind,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ImportedName {
pub name: String,
pub alias: Option<String>,
}
impl ImportedName {
pub fn as_import_str(&self) -> String {
match &self.alias {
Some(alias) => format!("{} as {}", self.name, alias),
None => self.name.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ParsedFromImport {
pub line: usize,
pub end_line: usize,
pub module: String,
pub names: Vec<ImportedName>,
pub is_multiline: bool,
}
impl ParsedFromImport {
pub fn name_strings(&self) -> Vec<String> {
self.names.iter().map(|n| n.as_import_str()).collect()
}
pub fn has_star(&self) -> bool {
self.names.iter().any(|n| n.name == "*")
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ParsedBareImport {
pub line: usize,
pub module: String,
pub alias: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParseSource {
Ast,
StringFallback,
}
pub struct ImportLayout {
pub groups: Vec<ImportGroup>,
pub from_imports: Vec<ParsedFromImport>,
#[allow(dead_code)]
pub bare_imports: Vec<ParsedBareImport>,
#[allow(dead_code)]
pub source: ParseSource,
lines: Vec<String>,
}
impl ImportLayout {
fn new(
groups: Vec<ImportGroup>,
from_imports: Vec<ParsedFromImport>,
bare_imports: Vec<ParsedBareImport>,
source: ParseSource,
content: &str,
) -> Self {
let lines = content.lines().map(|l| l.to_string()).collect();
Self {
groups,
from_imports,
bare_imports,
source,
lines,
}
}
pub fn line_strs(&self) -> Vec<&str> {
self.lines.iter().map(|s| s.as_str()).collect()
}
pub fn line(&self, idx: usize) -> &str {
self.lines.get(idx).map(|s| s.as_str()).unwrap_or("")
}
pub fn find_matching_from_import(&self, module: &str) -> Option<&ParsedFromImport> {
self.from_imports
.iter()
.find(|fi| fi.module == module && !fi.has_star())
}
}
pub fn parse_import_layout(content: &str) -> ImportLayout {
match rustpython_parser::parse(content, Mode::Module, "") {
Ok(ast) => parse_layout_from_ast(&ast, content),
Err(e) => {
warn!("AST parse failed ({e}), using string fallback for import layout");
parse_layout_from_str(content)
}
}
}
pub fn classify_import_statement(statement: &str) -> ImportKind {
classify_module(top_level_module(statement).unwrap_or(""))
}
pub fn import_sort_key(name: &str) -> &str {
match name.find(" as ") {
Some(pos) => name[..pos].trim(),
None => name.trim(),
}
}
pub fn import_line_sort_key(line: &str) -> (u8, String) {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("import ") {
let module = rest.split_whitespace().next().unwrap_or("");
(0, module.to_lowercase())
} else if let Some(rest) = trimmed.strip_prefix("from ") {
let module = rest.split(" import ").next().unwrap_or("").trim();
(1, module.to_lowercase())
} else {
(2, String::new())
}
}
pub fn find_sorted_insert_position(
lines: &[&str],
group: &ImportGroup,
sort_key: &(u8, String),
) -> u32 {
for (i, line) in lines
.iter()
.enumerate()
.take(group.last_line + 1)
.skip(group.first_line)
{
let existing_key = import_line_sort_key(line);
if *sort_key < existing_key {
return i as u32;
}
}
(group.last_line + 1) as u32
}
pub fn adapt_type_for_consumer(
return_type: &str,
fixture_imports: &[TypeImportSpec],
consumer_import_map: &HashMap<String, TypeImportSpec>,
) -> (String, Vec<TypeImportSpec>) {
let mut adapted = return_type.to_string();
let mut remaining = Vec::new();
for spec in fixture_imports {
if spec.import_statement.starts_with("import ") {
let bare_module = spec
.import_statement
.strip_prefix("import ")
.unwrap()
.split(" as ")
.next()
.unwrap_or("")
.trim();
if bare_module.is_empty() {
remaining.push(spec.clone());
continue;
}
let prefix = format!("{}.", spec.check_name);
if !adapted.contains(&prefix) {
remaining.push(spec.clone());
continue;
}
let mut rewrites: Vec<(String, String)> = Vec::new(); let mut all_rewritable = true;
let mut pos = 0;
while let Some(hit) = adapted[pos..].find(&prefix) {
let abs = pos + hit;
if abs > 0 {
let prev = adapted.as_bytes()[abs - 1];
if prev.is_ascii_alphanumeric() || prev == b'_' {
pos = abs + prefix.len();
continue;
}
}
let name_start = abs + prefix.len();
let rest = &adapted[name_start..];
let name_end = rest
.find(|c: char| !c.is_alphanumeric() && c != '_')
.unwrap_or(rest.len());
let name = &rest[..name_end];
if name.is_empty() {
pos = name_start;
continue;
}
if let Some(consumer_spec) = consumer_import_map.get(name) {
let expected = format!("from {} import", bare_module);
if consumer_spec.import_statement.starts_with(&expected) {
let dotted = format!("{}.{}", spec.check_name, name);
if !rewrites.iter().any(|(d, _)| d == &dotted) {
rewrites.push((dotted, consumer_spec.check_name.clone()));
}
} else {
all_rewritable = false;
break;
}
} else {
all_rewritable = false;
break;
}
pos = name_start + name_end;
}
if all_rewritable && !rewrites.is_empty() {
for (dotted, short) in &rewrites {
adapted = adapted.replace(dotted.as_str(), short.as_str());
}
info!(
"Adapted type '{}' → '{}' (consumer already imports short names)",
return_type, adapted
);
} else {
debug!(
"adapt_type_for_consumer: cannot fully rewrite '{}' in '{}' \
(not all dotted names have matching from-imports in consumer) \
— keeping bare-import spec",
spec.check_name, return_type,
);
remaining.push(spec.clone());
}
} else if let Some((module, name_part)) = split_from_import(&spec.import_statement) {
let original_name = name_part.split(" as ").next().unwrap_or(name_part).trim();
if let Some(consumer_module_name) =
find_consumer_bare_import(consumer_import_map, module)
{
let dotted = format!("{}.{}", consumer_module_name, original_name);
let new_adapted = replace_identifier(&adapted, &spec.check_name, &dotted);
if new_adapted != adapted {
info!(
"Adapted type: '{}' → '{}' (consumer has bare import for '{}')",
spec.check_name, dotted, module
);
adapted = new_adapted;
} else {
remaining.push(spec.clone());
}
} else {
remaining.push(spec.clone());
}
} else {
remaining.push(spec.clone());
}
}
(adapted, remaining)
}
pub(crate) fn find_consumer_bare_import<'a>(
consumer_import_map: &'a HashMap<String, TypeImportSpec>,
module: &str,
) -> Option<&'a str> {
for spec in consumer_import_map.values() {
if let Some(rest) = spec.import_statement.strip_prefix("import ") {
let module_part = rest.split(" as ").next().unwrap_or("").trim();
if module_part == module {
return Some(&spec.check_name);
}
}
}
None
}
pub(crate) fn can_merge_into(fi: &ParsedFromImport) -> bool {
!(fi.has_star() || fi.is_multiline && fi.names.is_empty())
}
fn classify_module(module: &str) -> ImportKind {
if module == "__future__" {
ImportKind::Future
} else if is_stdlib_module(module) {
ImportKind::Stdlib
} else {
ImportKind::ThirdParty
}
}
fn merge_kinds(a: ImportKind, b: ImportKind) -> ImportKind {
match (a, b) {
(ImportKind::Future, _) | (_, ImportKind::Future) => ImportKind::Future,
(ImportKind::ThirdParty, _) | (_, ImportKind::ThirdParty) => ImportKind::ThirdParty,
_ => ImportKind::Stdlib,
}
}
fn classify_import_line(line: &str) -> ImportKind {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("from ") {
let module = rest.split_whitespace().next().unwrap_or("");
classify_module(module.split('.').next().unwrap_or(""))
} else if let Some(rest) = trimmed.strip_prefix("import ") {
rest.split(',')
.filter_map(|part| {
let name = part.split_whitespace().next()?;
Some(classify_module(name.split('.').next().unwrap_or("")))
})
.fold(ImportKind::Stdlib, merge_kinds)
} else {
ImportKind::ThirdParty
}
}
fn top_level_module(line: &str) -> Option<&str> {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("from ") {
let module = rest.split_whitespace().next()?;
module.split('.').next()
} else if let Some(rest) = trimmed.strip_prefix("import ") {
let first = rest.split(',').next()?.trim();
let first = first.split_whitespace().next()?;
first.split('.').next()
} else {
None
}
}
fn split_from_import(statement: &str) -> Option<(&str, &str)> {
let rest = statement.strip_prefix("from ")?;
let (module, rest) = rest.split_once(" import ")?;
let module = module.trim();
let name = rest.trim();
if module.is_empty() || name.is_empty() {
None
} else {
Some((module, name))
}
}
fn parse_layout_from_ast(ast: &rustpython_parser::ast::Mod, content: &str) -> ImportLayout {
let line_starts = build_line_starts(content);
let offset_to_line = |offset: usize| -> usize {
line_starts
.partition_point(|&s| s <= offset)
.saturating_sub(1)
};
let mut from_imports: Vec<ParsedFromImport> = Vec::new();
let mut bare_imports: Vec<ParsedBareImport> = Vec::new();
let body = match ast {
Mod::Module(m) => &m.body,
_ => return parse_layout_from_str(content),
};
for stmt in body {
match stmt {
Stmt::ImportFrom(import_from) => {
let start_byte = import_from.range.start().to_usize();
let end_byte = import_from.range.end().to_usize();
let line = offset_to_line(start_byte);
let end_line = offset_to_line(end_byte.saturating_sub(1));
let mut module = import_from
.module
.as_ref()
.map(|m| m.to_string())
.unwrap_or_default();
if let Some(ref level) = import_from.level {
let level_val = level.to_usize();
if level_val > 0 {
let dots = ".".repeat(level_val);
module = dots + &module;
}
}
let names: Vec<ImportedName> = import_from
.names
.iter()
.map(|alias| ImportedName {
name: alias.name.to_string(),
alias: alias.asname.as_ref().map(|a| a.to_string()),
})
.collect();
let is_multiline = end_line > line;
from_imports.push(ParsedFromImport {
line,
end_line,
module,
names,
is_multiline,
});
}
Stmt::Import(import_stmt) => {
let start_byte = import_stmt.range.start().to_usize();
let line = offset_to_line(start_byte);
for alias in &import_stmt.names {
bare_imports.push(ParsedBareImport {
line,
module: alias.name.to_string(),
alias: alias.asname.as_ref().map(|a| a.to_string()),
});
}
}
_ => {}
}
}
let groups = build_groups_from_ast(&from_imports, &bare_imports);
ImportLayout::new(
groups,
from_imports,
bare_imports,
ParseSource::Ast,
content,
)
}
fn build_line_starts(content: &str) -> Vec<usize> {
let bytes = content.as_bytes();
let mut starts = vec![0usize];
for (i, &b) in bytes.iter().enumerate() {
if b == b'\n' {
starts.push(i + 1);
}
}
starts
}
struct ImportEvent {
first_line: usize,
last_line: usize,
top_module: String,
}
fn build_groups_from_ast(
from_imports: &[ParsedFromImport],
bare_imports: &[ParsedBareImport],
) -> Vec<ImportGroup> {
let mut events: Vec<ImportEvent> = Vec::new();
for fi in from_imports {
let top = fi
.module
.trim_start_matches('.')
.split('.')
.next()
.unwrap_or("")
.to_string();
events.push(ImportEvent {
first_line: fi.line,
last_line: fi.end_line,
top_module: top,
});
}
for bi in bare_imports {
let top = bi.module.split('.').next().unwrap_or("").to_string();
events.push(ImportEvent {
first_line: bi.line,
last_line: bi.line,
top_module: top,
});
}
events.sort_by_key(|e| e.first_line);
let mut groups: Vec<ImportGroup> = Vec::new();
for event in events {
match groups.last_mut() {
Some(g) if event.first_line <= g.last_line + 1 => {
g.last_line = g.last_line.max(event.last_line);
g.kind = merge_kinds(g.kind, classify_module(&event.top_module));
}
_ => {
let kind = classify_module(&event.top_module);
groups.push(ImportGroup {
first_line: event.first_line,
last_line: event.last_line,
kind,
});
}
}
}
groups
}
fn parse_layout_from_str(content: &str) -> ImportLayout {
let lines: Vec<&str> = content.lines().collect();
let mut groups: Vec<ImportGroup> = Vec::new();
let mut from_imports: Vec<ParsedFromImport> = Vec::new();
let mut bare_imports: Vec<ParsedBareImport> = Vec::new();
let mut current_start: Option<usize> = None;
let mut current_last: usize = 0;
let mut current_kind = ImportKind::ThirdParty;
let mut seen_any_import = false;
let mut in_multiline = false;
let mut multiline_start: usize = 0;
let mut multiline_module: String = String::new();
for (i, &line) in lines.iter().enumerate() {
if in_multiline {
current_last = i;
let line_no_comment = line.split('#').next().unwrap_or("").trim_end();
if line_no_comment.contains(')') {
from_imports.push(ParsedFromImport {
line: multiline_start,
end_line: i,
module: multiline_module.clone(),
names: vec![],
is_multiline: true,
});
in_multiline = false;
}
continue;
}
if line.starts_with("import ") || line.starts_with("from ") {
seen_any_import = true;
if current_start.is_none() {
current_start = Some(i);
current_kind = classify_import_line(line);
} else {
current_kind = merge_kinds(current_kind, classify_import_line(line));
}
current_last = i;
if let Some(rest) = line.strip_prefix("from ") {
let module = rest
.split(" import ")
.next()
.unwrap_or("")
.trim()
.to_string();
let line_no_comment = line.split('#').next().unwrap_or("").trim_end();
if line_no_comment.contains('(') && !line_no_comment.contains(')') {
in_multiline = true;
multiline_start = i;
multiline_module = module;
} else {
if let Some(names_raw) = rest.split(" import ").nth(1) {
let names_str = names_raw.split('#').next().unwrap_or("").trim_end();
let names: Vec<ImportedName> = names_str
.split(',')
.filter_map(|n| {
let n = n.trim();
if n.is_empty() {
return None;
}
if let Some((name, alias)) = n.split_once(" as ") {
Some(ImportedName {
name: name.trim().to_string(),
alias: Some(alias.trim().to_string()),
})
} else {
Some(ImportedName {
name: n.to_string(),
alias: None,
})
}
})
.collect();
from_imports.push(ParsedFromImport {
line: i,
end_line: i,
module,
names,
is_multiline: false,
});
}
}
} else if let Some(rest) = line.strip_prefix("import ") {
for part in rest.split(',') {
let part = part.trim();
let (module_str, alias) = if let Some((m, a)) = part.split_once(" as ") {
(m.trim().to_string(), Some(a.trim().to_string()))
} else {
let m = part.split_whitespace().next().unwrap_or(part);
(m.to_string(), None)
};
if !module_str.is_empty() {
bare_imports.push(ParsedBareImport {
line: i,
module: module_str,
alias,
});
}
}
}
continue;
}
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
if let Some(start) = current_start.take() {
groups.push(ImportGroup {
first_line: start,
last_line: current_last,
kind: current_kind,
});
}
continue;
}
if seen_any_import {
if let Some(start) = current_start.take() {
groups.push(ImportGroup {
first_line: start,
last_line: current_last,
kind: current_kind,
});
}
break;
}
}
if let Some(start) = current_start {
groups.push(ImportGroup {
first_line: start,
last_line: current_last,
kind: current_kind,
});
}
ImportLayout::new(
groups,
from_imports,
bare_imports,
ParseSource::StringFallback,
content,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fixtures::types::TypeImportSpec;
use std::collections::HashMap;
fn spec(check_name: &str, import_statement: &str) -> TypeImportSpec {
TypeImportSpec {
check_name: check_name.to_string(),
import_statement: import_statement.to_string(),
}
}
fn layout(lines: &[&str]) -> ImportLayout {
parse_import_layout(&lines.join("\n"))
}
#[test]
fn test_classify_future() {
assert_eq!(
classify_import_statement("from __future__ import annotations"),
ImportKind::Future
);
}
#[test]
fn test_classify_stdlib() {
assert_eq!(
classify_import_statement("from typing import Any"),
ImportKind::Stdlib
);
assert_eq!(
classify_import_statement("import pathlib"),
ImportKind::Stdlib
);
assert_eq!(
classify_import_statement("from collections.abc import Sequence"),
ImportKind::Stdlib
);
}
#[test]
fn test_classify_third_party() {
assert_eq!(
classify_import_statement("import pytest"),
ImportKind::ThirdParty
);
assert_eq!(
classify_import_statement("from myapp.db import Database"),
ImportKind::ThirdParty
);
}
#[test]
fn test_classify_comma_separated_stdlib() {
assert_eq!(
classify_import_statement("import os, sys"),
ImportKind::Stdlib
);
}
#[test]
fn test_classify_comma_separated_mixed_kinds_first_module_wins() {
assert_eq!(
classify_import_statement("import os, pytest"),
ImportKind::Stdlib );
assert_eq!(
classify_import_statement("import pytest, os"),
ImportKind::ThirdParty );
}
#[test]
fn test_merge_kinds_future_wins_over_all() {
assert_eq!(
merge_kinds(ImportKind::Future, ImportKind::Stdlib),
ImportKind::Future
);
assert_eq!(
merge_kinds(ImportKind::Future, ImportKind::ThirdParty),
ImportKind::Future
);
assert_eq!(
merge_kinds(ImportKind::Stdlib, ImportKind::Future),
ImportKind::Future
);
}
#[test]
fn test_merge_kinds_third_party_wins_over_stdlib() {
assert_eq!(
merge_kinds(ImportKind::ThirdParty, ImportKind::Stdlib),
ImportKind::ThirdParty
);
assert_eq!(
merge_kinds(ImportKind::Stdlib, ImportKind::ThirdParty),
ImportKind::ThirdParty
);
}
#[test]
fn test_merge_kinds_same_kind_unchanged() {
assert_eq!(
merge_kinds(ImportKind::Stdlib, ImportKind::Stdlib),
ImportKind::Stdlib
);
assert_eq!(
merge_kinds(ImportKind::ThirdParty, ImportKind::ThirdParty),
ImportKind::ThirdParty
);
}
#[test]
fn test_classify_import_line_all_stdlib() {
assert_eq!(classify_import_line("import os, sys"), ImportKind::Stdlib);
}
#[test]
fn test_classify_import_line_all_third_party() {
assert_eq!(
classify_import_line("import pytest, flask"),
ImportKind::ThirdParty
);
}
#[test]
fn test_classify_import_line_mixed_stdlib_first() {
assert_eq!(
classify_import_line("import os, pytest"),
ImportKind::ThirdParty
);
}
#[test]
fn test_classify_import_line_mixed_third_party_first() {
assert_eq!(
classify_import_line("import pytest, os"),
ImportKind::ThirdParty
);
}
#[test]
fn test_classify_import_line_three_modules_mixed() {
assert_eq!(
classify_import_line("import os, sys, pytest"),
ImportKind::ThirdParty
);
}
#[test]
fn test_classify_import_line_four_modules_stdlib_only() {
assert_eq!(
classify_import_line("import os, sys, re, pathlib"),
ImportKind::Stdlib
);
}
#[test]
fn test_classify_import_line_four_modules_third_party_last() {
assert_eq!(
classify_import_line("import os, sys, re, pytest"),
ImportKind::ThirdParty
);
}
#[test]
fn test_parse_groups_three_module_mixed_bare_import() {
let l = layout(&["import os, sys, pytest", "", "def test(): pass"]);
assert_eq!(l.groups.len(), 1);
assert_eq!(l.groups[0].kind, ImportKind::ThirdParty);
}
#[test]
fn test_classify_import_line_from_import_unaffected() {
assert_eq!(
classify_import_line("from typing import Any"),
ImportKind::Stdlib
);
assert_eq!(
classify_import_line("from flask import Flask"),
ImportKind::ThirdParty
);
}
#[test]
fn test_parse_groups_mixed_bare_import_classified_as_third_party() {
let l = layout(&["import os, pytest", "", "def test(): pass"]);
assert_eq!(l.groups.len(), 1);
assert_eq!(l.groups[0].kind, ImportKind::ThirdParty);
}
#[test]
fn test_parse_groups_mixed_bare_import_order_independent() {
let l = layout(&["import pytest, os", "", "def test(): pass"]);
assert_eq!(l.groups.len(), 1);
assert_eq!(l.groups[0].kind, ImportKind::ThirdParty);
}
#[test]
fn test_parse_groups_all_stdlib_bare_import_unchanged() {
let l = layout(&["import os, sys", "", "def test(): pass"]);
assert_eq!(l.groups.len(), 1);
assert_eq!(l.groups[0].kind, ImportKind::Stdlib);
}
#[test]
fn test_parse_groups_fallback_mixed_bare_import() {
let l = parse_import_layout("import os, pytest\ndef test(:\n pass");
assert_eq!(l.source, ParseSource::StringFallback);
assert_eq!(l.groups.len(), 1);
assert_eq!(l.groups[0].kind, ImportKind::ThirdParty);
}
#[test]
fn test_parse_layout_uses_ast_for_valid_python() {
let l = layout(&["import os", "", "def test(): pass"]);
assert_eq!(l.source, ParseSource::Ast);
}
#[test]
fn test_parse_layout_falls_back_for_invalid_python() {
let l = parse_import_layout("import os\ndef test(:\n pass");
assert_eq!(l.source, ParseSource::StringFallback);
}
#[test]
fn test_parse_groups_stdlib_and_third_party() {
let l = layout(&[
"import time",
"",
"import pytest",
"from vcc.framework import fixture",
"",
"LOGGING_TIME = 2",
]);
assert_eq!(l.groups.len(), 2);
assert_eq!(l.groups[0].first_line, 0);
assert_eq!(l.groups[0].last_line, 0);
assert_eq!(l.groups[0].kind, ImportKind::Stdlib);
assert_eq!(l.groups[1].first_line, 2);
assert_eq!(l.groups[1].last_line, 3);
assert_eq!(l.groups[1].kind, ImportKind::ThirdParty);
}
#[test]
fn test_parse_groups_single_third_party() {
let l = layout(&["import pytest", "", "def test(): pass"]);
assert_eq!(l.groups.len(), 1);
assert_eq!(l.groups[0].kind, ImportKind::ThirdParty);
assert_eq!(l.groups[0].first_line, 0);
assert_eq!(l.groups[0].last_line, 0);
}
#[test]
fn test_parse_groups_no_imports() {
let l = layout(&["def test(): pass"]);
assert!(l.groups.is_empty());
}
#[test]
fn test_parse_groups_empty_file() {
let l = layout(&[]);
assert!(l.groups.is_empty());
}
#[test]
fn test_parse_groups_with_docstring_preamble() {
let l = layout(&[
r#""""Module docstring.""""#,
"",
"import pytest",
"from pathlib import Path",
"",
"def test(): pass",
]);
assert_eq!(l.groups.len(), 1);
assert_eq!(l.groups[0].first_line, 2);
assert_eq!(l.groups[0].last_line, 3);
assert_eq!(l.groups[0].kind, ImportKind::ThirdParty);
}
#[test]
fn test_parse_groups_ignores_indented_imports() {
let l = layout(&[
"import pytest",
"",
"def test():",
" from .utils import helper",
" import os",
]);
assert_eq!(l.groups.len(), 1);
assert_eq!(l.groups[0].first_line, 0);
assert_eq!(l.groups[0].last_line, 0);
}
#[test]
fn test_parse_groups_future_then_stdlib_then_third_party() {
let l = layout(&[
"from __future__ import annotations",
"",
"import os",
"import time",
"",
"import pytest",
"",
"def test(): pass",
]);
assert_eq!(l.groups.len(), 3);
assert_eq!(l.groups[0].kind, ImportKind::Future);
assert_eq!(l.groups[1].kind, ImportKind::Stdlib); assert_eq!(l.groups[2].kind, ImportKind::ThirdParty); }
#[test]
fn test_parse_groups_with_comments_between() {
let l = layout(&[
"import os",
"# stdlib above, third-party below",
"import pytest",
"",
"def test(): pass",
]);
assert_eq!(l.groups.len(), 2);
assert_eq!(l.groups[0].kind, ImportKind::Stdlib);
assert_eq!(l.groups[0].last_line, 0);
assert_eq!(l.groups[1].kind, ImportKind::ThirdParty);
assert_eq!(l.groups[1].first_line, 2);
}
#[test]
fn test_parse_groups_comma_separated_import_is_stdlib() {
let l = layout(&[
"import os, sys",
"",
"import pytest",
"",
"def test(): pass",
]);
assert_eq!(l.groups.len(), 2);
assert_eq!(l.groups[0].kind, ImportKind::Stdlib);
assert_eq!(l.groups[0].first_line, 0);
assert_eq!(l.groups[0].last_line, 0);
assert_eq!(l.groups[1].kind, ImportKind::ThirdParty);
}
#[test]
fn test_parse_groups_multiline_import_single_group() {
let l = layout(&["from liba import (", " moda,", " modb", ")"]);
assert_eq!(l.groups.len(), 1);
assert_eq!(l.groups[0].first_line, 0);
assert_eq!(l.groups[0].last_line, 3);
assert_eq!(l.groups[0].kind, ImportKind::ThirdParty);
}
#[test]
fn test_parse_groups_multiline_import_followed_by_third_party() {
let l = layout(&[
"from liba import (",
" moda,",
" modb",
")",
"",
"import pytest",
"",
"def test(): pass",
]);
assert_eq!(l.groups.len(), 2);
assert_eq!(l.groups[0].first_line, 0);
assert_eq!(l.groups[0].last_line, 3);
assert_eq!(l.groups[1].first_line, 5);
assert_eq!(l.groups[1].last_line, 5);
assert_eq!(l.groups[1].kind, ImportKind::ThirdParty);
}
#[test]
fn test_parse_groups_multiline_stdlib_then_third_party() {
let l = layout(&[
"from typing import (",
" Any,",
" Optional,",
")",
"",
"import pytest",
"",
"def test(): pass",
]);
assert_eq!(l.groups.len(), 2);
assert_eq!(l.groups[0].kind, ImportKind::Stdlib);
assert_eq!(l.groups[0].first_line, 0);
assert_eq!(l.groups[0].last_line, 3);
assert_eq!(l.groups[1].kind, ImportKind::ThirdParty);
assert_eq!(l.groups[1].first_line, 5);
assert_eq!(l.groups[1].last_line, 5);
}
#[test]
fn test_parse_groups_inline_multiline_import() {
let l = layout(&[
"from typing import (Any,",
" Optional)",
"",
"import pytest",
]);
assert_eq!(l.groups.len(), 2);
assert_eq!(l.groups[0].kind, ImportKind::Stdlib);
assert_eq!(l.groups[0].first_line, 0);
assert_eq!(l.groups[0].last_line, 1);
assert_eq!(l.groups[1].kind, ImportKind::ThirdParty);
assert_eq!(l.groups[1].first_line, 3);
assert_eq!(l.groups[1].last_line, 3);
}
#[test]
fn test_from_imports_single_line() {
let l = layout(&["from typing import Any, Optional"]);
assert_eq!(l.from_imports.len(), 1);
let fi = &l.from_imports[0];
assert_eq!(fi.module, "typing");
assert_eq!(fi.line, 0);
assert_eq!(fi.end_line, 0);
assert!(!fi.is_multiline);
assert_eq!(fi.name_strings(), vec!["Any", "Optional"]);
}
#[test]
fn test_from_imports_with_alias() {
let l = layout(&["from pathlib import Path as P"]);
let fi = &l.from_imports[0];
assert_eq!(fi.module, "pathlib");
assert_eq!(fi.name_strings(), vec!["Path as P"]);
}
#[test]
fn test_from_imports_multiline_has_correct_end_line() {
let l = layout(&["from typing import (", " Any,", " Optional,", ")"]);
assert_eq!(l.from_imports.len(), 1);
let fi = &l.from_imports[0];
assert_eq!(fi.line, 0);
assert_eq!(fi.end_line, 3);
assert!(fi.is_multiline);
if l.source == ParseSource::Ast {
assert_eq!(fi.name_strings(), vec!["Any", "Optional"]);
}
}
#[test]
fn test_bare_imports_comma_separated() {
let l = layout(&["import os, sys"]);
assert_eq!(l.bare_imports.len(), 2);
assert_eq!(l.bare_imports[0].module, "os");
assert_eq!(l.bare_imports[1].module, "sys");
assert_eq!(l.bare_imports[0].line, 0);
assert_eq!(l.bare_imports[1].line, 0);
}
#[test]
fn test_bare_import_with_alias() {
let l = layout(&["import pathlib as pl"]);
assert_eq!(l.bare_imports.len(), 1);
assert_eq!(l.bare_imports[0].module, "pathlib");
assert_eq!(l.bare_imports[0].alias, Some("pl".to_string()));
}
#[test]
fn test_find_matching_found() {
let l = layout(&[
"import pytest",
"from typing import Optional",
"",
"def test(): pass",
]);
let fi = l.find_matching_from_import("typing");
assert!(fi.is_some());
assert_eq!(fi.unwrap().name_strings(), vec!["Optional"]);
}
#[test]
fn test_find_matching_multiple_names() {
let l = layout(&["from typing import Any, Optional, Union"]);
let fi = l.find_matching_from_import("typing").unwrap();
assert_eq!(fi.name_strings(), vec!["Any", "Optional", "Union"]);
}
#[test]
fn test_find_matching_not_found() {
let l = layout(&["import pytest", "from pathlib import Path"]);
assert!(l.find_matching_from_import("typing").is_none());
}
#[test]
fn test_find_matching_returns_multiline() {
let l = layout(&["from typing import (", " Any,", " Optional,", ")"]);
let fi = l.find_matching_from_import("typing");
assert!(fi.is_some(), "multiline match should be returned");
assert!(fi.unwrap().is_multiline);
}
#[test]
fn test_find_matching_skips_star() {
let l = layout(&["from typing import *"]);
assert!(l.find_matching_from_import("typing").is_none());
}
#[test]
fn test_find_matching_ignores_indented() {
let l = layout(&[
"import pytest",
"",
"def test():",
" from typing import Any",
]);
assert!(l.find_matching_from_import("typing").is_none());
}
#[test]
fn test_find_matching_with_inline_comment() {
let l = layout(&["from typing import Any # comment"]);
let fi = l.find_matching_from_import("typing").unwrap();
assert_eq!(fi.name_strings(), vec!["Any"]);
}
#[test]
fn test_find_matching_aliases_preserved() {
let l = layout(&["from os import path as p, getcwd as cwd"]);
let fi = l.find_matching_from_import("os").unwrap();
assert_eq!(fi.name_strings(), vec!["path as p", "getcwd as cwd"]);
}
#[test]
fn test_can_merge_single_line() {
let fi = ParsedFromImport {
line: 0,
end_line: 0,
module: "typing".to_string(),
names: vec![ImportedName {
name: "Any".to_string(),
alias: None,
}],
is_multiline: false,
};
assert!(can_merge_into(&fi));
}
#[test]
fn test_can_merge_multiline_with_names() {
let fi = ParsedFromImport {
line: 0,
end_line: 3,
module: "typing".to_string(),
names: vec![ImportedName {
name: "Any".to_string(),
alias: None,
}],
is_multiline: true,
};
assert!(can_merge_into(&fi));
}
#[test]
fn test_cannot_merge_multiline_without_names() {
let fi = ParsedFromImport {
line: 0,
end_line: 3,
module: "typing".to_string(),
names: vec![],
is_multiline: true,
};
assert!(!can_merge_into(&fi));
}
#[test]
fn test_cannot_merge_star() {
let fi = ParsedFromImport {
line: 0,
end_line: 0,
module: "typing".to_string(),
names: vec![ImportedName {
name: "*".to_string(),
alias: None,
}],
is_multiline: false,
};
assert!(!can_merge_into(&fi));
}
#[test]
fn test_import_sort_key_plain() {
assert_eq!(import_sort_key("Path"), "Path");
}
#[test]
fn test_import_sort_key_alias() {
assert_eq!(import_sort_key("Path as P"), "Path");
}
#[test]
fn test_import_line_sort_key_bare_before_from() {
let bare = import_line_sort_key("import os");
let from = import_line_sort_key("from typing import Any");
assert!(bare < from, "bare imports should sort before from-imports");
}
#[test]
fn test_import_line_sort_key_alphabetical_bare() {
let a = import_line_sort_key("import os");
let b = import_line_sort_key("import pathlib");
let c = import_line_sort_key("import time");
assert!(a < b);
assert!(b < c);
}
#[test]
fn test_import_line_sort_key_alphabetical_from() {
let a = import_line_sort_key("from pathlib import Path");
let b = import_line_sort_key("from typing import Any");
assert!(a < b);
}
#[test]
fn test_import_line_sort_key_dotted_module_ordering() {
let short = import_line_sort_key("from vcc import conx_canoe");
let long = import_line_sort_key("from vcc.conxtfw.framework import fixture");
assert!(
short < long,
"shorter module path should sort before longer"
);
}
#[test]
fn test_sorted_position_bare_before_existing_bare() {
let lines = vec!["import os", "import time"];
let group = ImportGroup {
first_line: 0,
last_line: 1,
kind: ImportKind::Stdlib,
};
let key = import_line_sort_key("import pathlib");
assert_eq!(find_sorted_insert_position(&lines, &group, &key), 1);
}
#[test]
fn test_sorted_position_from_after_all_bare() {
let lines = vec!["import os", "import time"];
let group = ImportGroup {
first_line: 0,
last_line: 1,
kind: ImportKind::Stdlib,
};
let key = import_line_sort_key("from typing import Any");
assert_eq!(find_sorted_insert_position(&lines, &group, &key), 2);
}
#[test]
fn test_sorted_position_from_between_existing_froms() {
let lines = vec!["import pytest", "from aaa import X", "from zzz import Y"];
let group = ImportGroup {
first_line: 0,
last_line: 2,
kind: ImportKind::ThirdParty,
};
let key = import_line_sort_key("from mmm import Z");
assert_eq!(find_sorted_insert_position(&lines, &group, &key), 2);
}
#[test]
fn test_sorted_position_before_everything() {
let lines = vec!["import time", "from typing import Any"];
let group = ImportGroup {
first_line: 0,
last_line: 1,
kind: ImportKind::Stdlib,
};
let key = import_line_sort_key("import os");
assert_eq!(find_sorted_insert_position(&lines, &group, &key), 0);
}
#[test]
fn test_adapt_dotted_to_short_when_consumer_has_from_import() {
let fixture_imports = vec![spec("pathlib", "import pathlib")];
let mut consumer_map = HashMap::new();
consumer_map.insert("Path".to_string(), spec("Path", "from pathlib import Path"));
let (adapted, remaining) =
adapt_type_for_consumer("pathlib.Path", &fixture_imports, &consumer_map);
assert_eq!(adapted, "Path");
assert!(
remaining.is_empty(),
"No import should remain: {:?}",
remaining
);
}
#[test]
fn test_adapt_no_rewrite_when_consumer_lacks_from_import() {
let fixture_imports = vec![spec("pathlib", "import pathlib")];
let consumer_map = HashMap::new();
let (adapted, remaining) =
adapt_type_for_consumer("pathlib.Path", &fixture_imports, &consumer_map);
assert_eq!(adapted, "pathlib.Path");
assert_eq!(remaining.len(), 1);
assert_eq!(remaining[0].import_statement, "import pathlib");
}
#[test]
fn test_adapt_no_rewrite_when_consumer_imports_from_different_module() {
let fixture_imports = vec![spec("pathlib", "import pathlib")];
let mut consumer_map = HashMap::new();
consumer_map.insert("Path".to_string(), spec("Path", "from mylib import Path"));
let (adapted, remaining) =
adapt_type_for_consumer("pathlib.Path", &fixture_imports, &consumer_map);
assert_eq!(adapted, "pathlib.Path");
assert_eq!(remaining.len(), 1);
}
#[test]
fn test_adapt_from_import_specs_pass_through_unchanged() {
let fixture_imports = vec![spec("Path", "from pathlib import Path")];
let consumer_map = HashMap::new();
let (adapted, remaining) = adapt_type_for_consumer("Path", &fixture_imports, &consumer_map);
assert_eq!(adapted, "Path");
assert_eq!(remaining.len(), 1);
assert_eq!(remaining[0].check_name, "Path");
}
#[test]
fn test_adapt_complex_generic_with_dotted_and_from() {
let fixture_imports = vec![
spec("Optional", "from typing import Optional"),
spec("pathlib", "import pathlib"),
];
let mut consumer_map = HashMap::new();
consumer_map.insert("Path".to_string(), spec("Path", "from pathlib import Path"));
consumer_map.insert(
"Optional".to_string(),
spec("Optional", "from typing import Optional"),
);
let (adapted, remaining) =
adapt_type_for_consumer("Optional[pathlib.Path]", &fixture_imports, &consumer_map);
assert_eq!(adapted, "Optional[Path]");
assert_eq!(remaining.len(), 1);
assert_eq!(remaining[0].check_name, "Optional");
}
#[test]
fn test_adapt_multiple_dotted_refs_same_module() {
let fixture_imports = vec![spec("pathlib", "import pathlib")];
let mut consumer_map = HashMap::new();
consumer_map.insert("Path".to_string(), spec("Path", "from pathlib import Path"));
consumer_map.insert(
"PurePath".to_string(),
spec("PurePath", "from pathlib import PurePath"),
);
let (adapted, remaining) = adapt_type_for_consumer(
"tuple[pathlib.Path, pathlib.PurePath]",
&fixture_imports,
&consumer_map,
);
assert_eq!(adapted, "tuple[Path, PurePath]");
assert!(remaining.is_empty());
}
#[test]
fn test_adapt_partial_match_one_name_missing() {
let fixture_imports = vec![spec("pathlib", "import pathlib")];
let mut consumer_map = HashMap::new();
consumer_map.insert("Path".to_string(), spec("Path", "from pathlib import Path"));
let (adapted, remaining) = adapt_type_for_consumer(
"tuple[pathlib.Path, pathlib.PurePath]",
&fixture_imports,
&consumer_map,
);
assert_eq!(adapted, "tuple[pathlib.Path, pathlib.PurePath]");
assert_eq!(remaining.len(), 1);
}
#[test]
fn test_adapt_aliased_bare_import() {
let fixture_imports = vec![spec("pl", "import pathlib as pl")];
let mut consumer_map = HashMap::new();
consumer_map.insert("Path".to_string(), spec("Path", "from pathlib import Path"));
let (adapted, remaining) =
adapt_type_for_consumer("pl.Path", &fixture_imports, &consumer_map);
assert_eq!(adapted, "Path");
assert!(remaining.is_empty());
}
#[test]
fn test_adapt_no_false_match_on_prefix_substring() {
let fixture_imports = vec![spec("pathlib", "import pathlib")];
let mut consumer_map = HashMap::new();
consumer_map.insert("Path".to_string(), spec("Path", "from pathlib import Path"));
let (adapted, remaining) =
adapt_type_for_consumer("mypathlib.Path", &fixture_imports, &consumer_map);
assert_eq!(adapted, "mypathlib.Path");
assert_eq!(remaining.len(), 1);
}
#[test]
fn test_adapt_dotted_module_collections_abc() {
let fixture_imports = vec![spec("collections.abc", "import collections.abc")];
let mut consumer_map = HashMap::new();
consumer_map.insert(
"Iterable".to_string(),
spec("Iterable", "from collections.abc import Iterable"),
);
let (adapted, remaining) = adapt_type_for_consumer(
"collections.abc.Iterable[str]",
&fixture_imports,
&consumer_map,
);
assert_eq!(adapted, "Iterable[str]");
assert!(remaining.is_empty());
}
#[test]
fn test_adapt_consumer_has_bare_import_no_rewrite() {
let fixture_imports = vec![spec("pathlib", "import pathlib")];
let mut consumer_map = HashMap::new();
consumer_map.insert("pathlib".to_string(), spec("pathlib", "import pathlib"));
let (adapted, remaining) =
adapt_type_for_consumer("pathlib.Path", &fixture_imports, &consumer_map);
assert_eq!(adapted, "pathlib.Path");
assert_eq!(remaining.len(), 1);
}
#[test]
fn test_adapt_short_to_dotted_when_consumer_has_bare_import() {
let fixture_imports = vec![spec("Path", "from pathlib import Path")];
let mut consumer_map = HashMap::new();
consumer_map.insert("pathlib".to_string(), spec("pathlib", "import pathlib"));
let (adapted, remaining) = adapt_type_for_consumer("Path", &fixture_imports, &consumer_map);
assert_eq!(adapted, "pathlib.Path");
assert!(
remaining.is_empty(),
"No import should remain: {:?}",
remaining
);
}
#[test]
fn test_adapt_short_to_dotted_consumer_has_aliased_bare_import() {
let fixture_imports = vec![spec("Path", "from pathlib import Path")];
let mut consumer_map = HashMap::new();
consumer_map.insert("pl".to_string(), spec("pl", "import pathlib as pl"));
let (adapted, remaining) = adapt_type_for_consumer("Path", &fixture_imports, &consumer_map);
assert_eq!(adapted, "pl.Path");
assert!(remaining.is_empty());
}
#[test]
fn test_adapt_short_no_rewrite_when_consumer_lacks_bare_import() {
let fixture_imports = vec![spec("Path", "from pathlib import Path")];
let consumer_map = HashMap::new();
let (adapted, remaining) = adapt_type_for_consumer("Path", &fixture_imports, &consumer_map);
assert_eq!(adapted, "Path");
assert_eq!(remaining.len(), 1);
assert_eq!(remaining[0].check_name, "Path");
}
#[test]
fn test_adapt_short_to_dotted_generic_type() {
let fixture_imports = vec![
spec("Optional", "from typing import Optional"),
spec("Path", "from pathlib import Path"),
];
let mut consumer_map = HashMap::new();
consumer_map.insert("pathlib".to_string(), spec("pathlib", "import pathlib"));
let (adapted, remaining) =
adapt_type_for_consumer("Optional[Path]", &fixture_imports, &consumer_map);
assert_eq!(adapted, "Optional[pathlib.Path]");
assert_eq!(remaining.len(), 1);
assert_eq!(remaining[0].check_name, "Optional");
}
#[test]
fn test_adapt_short_to_dotted_word_boundary_safety() {
let fixture_imports = vec![spec("Path", "from pathlib import Path")];
let mut consumer_map = HashMap::new();
consumer_map.insert("pathlib".to_string(), spec("pathlib", "import pathlib"));
let (adapted, remaining) =
adapt_type_for_consumer("PathLike", &fixture_imports, &consumer_map);
assert_eq!(adapted, "PathLike");
assert_eq!(remaining.len(), 1);
assert_eq!(remaining[0].check_name, "Path");
}
#[test]
fn test_adapt_short_to_dotted_multiple_occurrences() {
let fixture_imports = vec![spec("Path", "from pathlib import Path")];
let mut consumer_map = HashMap::new();
consumer_map.insert("pathlib".to_string(), spec("pathlib", "import pathlib"));
let (adapted, remaining) =
adapt_type_for_consumer("tuple[Path, Path]", &fixture_imports, &consumer_map);
assert_eq!(adapted, "tuple[pathlib.Path, pathlib.Path]");
assert!(remaining.is_empty());
}
#[test]
fn test_adapt_short_to_dotted_aliased_from_import() {
let fixture_imports = vec![spec("P", "from pathlib import Path as P")];
let mut consumer_map = HashMap::new();
consumer_map.insert("pathlib".to_string(), spec("pathlib", "import pathlib"));
let (adapted, remaining) = adapt_type_for_consumer("P", &fixture_imports, &consumer_map);
assert_eq!(adapted, "pathlib.Path");
assert!(remaining.is_empty());
}
#[test]
fn test_adapt_short_to_dotted_collections_abc() {
let fixture_imports = vec![spec("Iterable", "from collections.abc import Iterable")];
let mut consumer_map = HashMap::new();
consumer_map.insert(
"collections.abc".to_string(),
spec("collections.abc", "import collections.abc"),
);
let (adapted, remaining) =
adapt_type_for_consumer("Iterable[str]", &fixture_imports, &consumer_map);
assert_eq!(adapted, "collections.abc.Iterable[str]");
assert!(remaining.is_empty());
}
#[test]
fn test_adapt_both_directions_in_one_call() {
let fixture_imports = vec![
spec("Sequence", "from typing import Sequence"),
spec("pathlib", "import pathlib"),
];
let mut consumer_map = HashMap::new();
consumer_map.insert("Path".to_string(), spec("Path", "from pathlib import Path"));
consumer_map.insert("typing".to_string(), spec("typing", "import typing"));
let (adapted, remaining) =
adapt_type_for_consumer("Sequence[pathlib.Path]", &fixture_imports, &consumer_map);
assert_eq!(adapted, "typing.Sequence[Path]");
assert!(
remaining.is_empty(),
"Both specs should be dropped: {:?}",
remaining
);
}
}