Enhance PEP 604 annotation handling by skipping dynamic Union creation in non-type definition contexts. Added support for implicit type aliases at the module level.

This commit is contained in:
Dan 2025-11-11 17:22:44 -05:00
parent 160b5f257b
commit bb61ed58cc
3 changed files with 54 additions and 15 deletions

View File

@ -147,6 +147,18 @@ def f(types: tuple[type, ...]):
return Union[types]
# Don't emit lint for dynamic Union creation with function calls (e.g., Union[foo()])
def get_types():
return (int, str, float)
def g():
return Union[get_types()]
# Implicit type alias at module level - should be flagged
IntOrStr = Union[int, str]
if __name__ == "__main__":
u = f((int, str, float))
print(u) # typing.Union[int, str, float]

View File

@ -205,9 +205,15 @@ pub(crate) fn non_pep604_annotation(
return;
}
// Skip dynamic Union creation (e.g., `Union[types]` where `types` is a variable).
// This cannot be converted to PEP 604 syntax because the types are determined at runtime.
if matches!(slice, Expr::Name(_)) {
// Only apply this rule in type annotation/definition positions, including implicit
// type aliases (e.g., `X = Union[int, str]` at module or class level).
// Skip dynamic Union creation (e.g., `Union[types]` or `Union[foo()]`) when not in
// type annotations, as these cannot be converted to PEP 604 syntax since the types
// are determined at runtime.
let in_type_definition = checker.semantic().in_type_definition();
let in_implicit_type_alias = is_implicit_type_alias(checker, expr);
if !in_type_definition && !in_implicit_type_alias {
return;
}
@ -327,3 +333,24 @@ fn is_named_tuple(checker: &Checker, expr: &Expr) -> bool {
fn is_optional_none(operator: Pep604Operator, slice: &Expr) -> bool {
matches!(operator, Pep604Operator::Optional) && matches!(slice, Expr::NoneLiteral(_))
}
/// Return `true` if the expression is part of an implicit type alias (e.g., `X = Union[int, str]`
/// at module or class level).
fn is_implicit_type_alias(checker: &Checker, expr: &Expr) -> bool {
let semantic = checker.semantic();
let scope = semantic.current_scope();
// Only consider module-level or class-level assignments as potential type aliases
if scope.kind.is_function() {
return false;
}
// Check if the current statement is a simple assignment
if let ast::Stmt::Assign(ast::StmtAssign { value, .. }) = semantic.current_statement() {
// Check if the Union expression is part of the assignment value
// We check if the expression's range is within the value's range
expr.range().start() >= value.range().start() && expr.range().end() <= value.range().end()
} else {
false
}
}

View File

@ -205,17 +205,6 @@ help: Convert to `X | Y`
43 |
44 |
UP007 Use `X | Y` for type annotations
--> UP007.py:46:9
|
45 | def f() -> None:
46 | x = Union[str, int]
| ^^^^^^^^^^^^^^^
47 | x = Union["str", "int"]
48 | x: Union[str, int]
|
help: Convert to `X | Y`
UP007 [*] Use `X | Y` for type annotations
--> UP007.py:48:8
|
@ -343,3 +332,14 @@ help: Convert to `X | Y`
92 | print(param)
93 |
94 |
UP007 Use `X | Y` for type annotations
--> UP007.py:160:12
|
159 | # Implicit type alias at module level - should be flagged
160 | IntOrStr = Union[int, str]
| ^^^^^^^^^^^^^^^
161 |
162 | if __name__ == "__main__":
|
help: Convert to `X | Y`