Skip to content

Commit

Permalink
Support PEP 593 annotations (#333)
Browse files Browse the repository at this point in the history
  • Loading branch information
adriangb authored Oct 6, 2022
1 parent a70624c commit df438ba
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 13 deletions.
33 changes: 33 additions & 0 deletions resources/test/fixtures/F821.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,36 @@ def update_tomato():
f'B'
f'{B}'
)


from typing import Annotated, Literal


def arbitrary_callable() -> None:
...


class PEP593Test:
field: Annotated[
int,
"base64",
arbitrary_callable(),
123,
(1, 2, 3),
]
field_with_stringified_type: Annotated[
"PEP593Test",
123,
]
field_with_undefined_stringified_type: Annotated[
"PEP593Test123",
123,
]
field_with_nested_subscript: Annotated[
dict[Literal["foo"], str],
123,
]
field_with_undefined_nested_subscript: Annotated[
dict["foo", "bar"], # Expected to fail as undefined.
123,
]
72 changes: 59 additions & 13 deletions src/check_ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,32 @@ fn match_name_or_attr(expr: &Expr, target: &str) -> bool {
}
}

fn is_annotated_subscript(expr: &Expr) -> bool {
enum SubscriptKind {
AnnotatedSubscript,
PEP593AnnotatedSubscript,
}

fn match_annotated_subscript(expr: &Expr) -> Option<SubscriptKind> {
match &expr.node {
ExprKind::Attribute { attr, .. } => typing::is_annotated_subscript(attr),
ExprKind::Name { id, .. } => typing::is_annotated_subscript(id),
_ => false,
ExprKind::Attribute { attr, .. } => {
if typing::is_annotated_subscript(attr) {
Some(SubscriptKind::AnnotatedSubscript)
} else if typing::is_pep593_annotated_subscript(attr) {
Some(SubscriptKind::PEP593AnnotatedSubscript)
} else {
None
}
}
ExprKind::Name { id, .. } => {
if typing::is_annotated_subscript(id) {
Some(SubscriptKind::AnnotatedSubscript)
} else if typing::is_pep593_annotated_subscript(id) {
Some(SubscriptKind::PEP593AnnotatedSubscript)
} else {
None
}
}
_ => None,
}
}

Expand Down Expand Up @@ -862,9 +883,11 @@ where
ExprKind::Constant {
value: Constant::Str(value),
..
} if self.in_annotation && !self.in_literal => {
self.deferred_string_annotations
.push((Range::from_located(expr), value));
} => {
if self.in_annotation && !self.in_literal {
self.deferred_string_annotations
.push((Range::from_located(expr), value));
}
}
ExprKind::Lambda { args, .. } => {
// Visit the arguments, but avoid the body, which will be deferred.
Expand Down Expand Up @@ -1015,12 +1038,35 @@ where
}
}
ExprKind::Subscript { value, slice, ctx } => {
if is_annotated_subscript(value) {
self.visit_expr(value);
self.visit_annotation(slice);
self.visit_expr_context(ctx);
} else {
visitor::walk_expr(self, expr);
match match_annotated_subscript(value) {
Some(subscript) => match subscript {
// Ex) Optional[int]
SubscriptKind::AnnotatedSubscript => {
self.visit_expr(value);
self.visit_annotation(slice);
self.visit_expr_context(ctx);
}
// Ex) Annotated[int, "Hello, world!"]
SubscriptKind::PEP593AnnotatedSubscript => {
// First argument is a type (including forward references); the rest are
// arbitrary Python objects.
self.visit_expr(value);
if let ExprKind::Tuple { elts, ctx } = &slice.node {
if let Some(expr) = elts.first() {
self.visit_expr(expr);
self.in_annotation = false;
for expr in elts.iter().skip(1) {
self.visit_expr(expr);
}
self.in_annotation = true;
self.visit_expr_context(ctx);
}
} else {
error!("Found non-ExprKind::Tuple argument to PEP 593 Annotation.")
}
}
},
None => visitor::walk_expr(self, expr),
}
}
_ => visitor::walk_expr(self, expr),
Expand Down
5 changes: 5 additions & 0 deletions src/python/typing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ static ANNOTATED_SUBSCRIPTS: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
"AbstractAsyncContextManager",
"AbstractContextManager",
"AbstractSet",
// "Annotated",
"AsyncContextManager",
"AsyncGenerator",
"AsyncIterable",
Expand Down Expand Up @@ -87,3 +88,7 @@ static ANNOTATED_SUBSCRIPTS: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
pub fn is_annotated_subscript(name: &str) -> bool {
ANNOTATED_SUBSCRIPTS.contains(name)
}

pub fn is_pep593_annotated_subscript(name: &str) -> bool {
name == "Annotated"
}
27 changes: 27 additions & 0 deletions src/snapshots/ruff__linter__tests__f821.snap
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,31 @@ expression: checks
row: 89
column: 9
fix: ~
- kind:
UndefinedName: PEP593Test123
location:
row: 114
column: 10
end_location:
row: 114
column: 24
fix: ~
- kind:
UndefinedName: foo
location:
row: 122
column: 15
end_location:
row: 122
column: 19
fix: ~
- kind:
UndefinedName: bar
location:
row: 122
column: 22
end_location:
row: 122
column: 26
fix: ~

0 comments on commit df438ba

Please sign in to comment.