Skip to content

Commit

Permalink
Fix: Semi string third-party generic type annotations get ignored (#155)
Browse files Browse the repository at this point in the history
* Fix: detect generic semi string type annotations.

* Add: CHANGELOG

* Fix: nested-string only supported by PY38+

* drawback to more clean method.
  • Loading branch information
hadialqattan committed Jul 16, 2022
1 parent 70874fe commit 907b865
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 118 deletions.
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

### Fixed

- [Semi string third-party generic type annotations get ignored by @hadialqattan](https://github.com/hadialqattan/pycln/pull/155)

## [2.0.2] - 2022-07-13

### Added
Expand Down
116 changes: 68 additions & 48 deletions pycln/utils/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,12 +226,13 @@ def visit_Call(self, node: ast.Call):

@recursive
def visit_Subscript(self, node: ast.Subscript) -> None:
#: Support semi string type hints.
#: >>> from ast import Import
#: >>> from typing import List
#: >>> def foo(bar: List["Import"]):
#: >>> pass
#: Issue: https://github.com/hadialqattan/pycln/issues/32
#: Support semi string type assigment
#:
#: >>> from ast import Import, ImportFrom
#: >>> from typing import Union, List
#: >>>
#: >>> bar = List['Import']
#: >>> foo = Union['Import', 'ImportFrom']
value = getattr(node, "value", "")
if getattr(value, "id", "") in SUBSCRIPT_TYPE_VARIABLE or (
hasattr(value, "value") and getattr(value.value, "id", "") == "typing"
Expand All @@ -241,57 +242,58 @@ def visit_Subscript(self, node: ast.Subscript) -> None:
else:
s_val = node.slice.value # type: ignore
for elt in getattr(s_val, "elts", ()) or (s_val,):
try:
self._parse_string(elt) # type: ignore
except UnparsableFile:
#: Ignore errors when parsing Literal
#: that are not valid identifiers.
#:
#: >>> from typing import Literal
#: >>> L: Literal[" "] = " "
#:
#: Issue: https://github.com/hadialqattan/pycln/issues/41
pass
self._parse_string(elt) # type: ignore

@recursive
def visit_AnnAssign(self, node: ast.AnnAssign):
#: Support (nested)string type annotations.
#: >>> from ast import Import
#: >>> from typing import Tuple
#: >>>
#: >>> foo: "List[Import]" = []
#: >>>
#: >>> bar: "List['Import']" = []
#: Support all
#:
#: 1) string type annotations:
#: >>> foo: "Bar[Baz]" = []
#:
#: 2) nested string type annotations:
#: >>> bar: "Bar['Baz']" = []
#:
#: 3) semi string type annotations:
#: >>> foo: Bar["Baz"] = []
self._visit_string_type_annotation(node)

@recursive
def visit_arg(self, node: ast.arg):
# Support Python ^3.8 type comments.
self._visit_type_comment(node)
#: Support arg (nested)string type annotations.
#: >>> from ast import Import
#: >>> from typing import Tuple
#: >>>
#: >>> def foo(bar: "Tuple[Import]"):
#: ... pass
#: >>>
#: >>> def bar(foo: "Tuple['Import']"):
#: ... pass
#: Support all
#:
#: 1) string type annotations:
#: >>> def foo(bar: "Baz[X]"):
#: ... pass
#:
#: 2) nested string type annotations:
#: >>> def foo(bar: "Baz['X']"):
#: ... pass
#:
#: 3) semi string type annotations:
#: >>> def foo(bar: Baz['X']):
#: ... pass
self._visit_string_type_annotation(node)

@recursive
def visit_FunctionDef(self, node: FunctionDefT):
# Support Python ^3.8 type comments.
self._visit_type_comment(node)
#: Support (nested)string type annotations.
#: >>> from ast import Import
#: >>> from typing import List
#: >>>
#: >>> def foo() -> "List[Import]":
#: >>> pass
#: >>>
#: >>> def bar() -> "List['Import']":
#: >>> pass
#: Support all
#:
#: 1) string type annotations:
#: >>> def foo() -> "Baz[X]":
#: ... pass
#:
#: 2) nested string type annotations:
#: >>> def foo() -> "Baz['X']":
#: ... pass
#:
#: 3) semi string type annotations:
#: >>> def foo() -> Baz['X']:
#: ... pass
self._visit_string_type_annotation(node)

# Support `ast.AsyncFunctionDef`.
Expand Down Expand Up @@ -384,6 +386,13 @@ def _visit_string_type_annotation(
annotation = node.annotation
else:
annotation = node.returns

if isinstance(annotation, ast.Subscript):
if isinstance(annotation.slice, ast.Constant):
annotation = annotation.slice
else:
annotation = annotation.slice.value # type: ignore

self._parse_string(annotation, True) # type: ignore

def _visit_type_comment(
Expand Down Expand Up @@ -417,12 +426,23 @@ def _visit_type_comment(
def _parse_string(
self, node: Union[ast.Constant, ast.Str], is_str_annotation: bool = False
) -> None:
# Parse string names/attrs.
if isinstance(node, (ast.Constant, ast.Str)):
val = getattr(node, "value", "") or getattr(node, "s", "")
if val and isinstance(val, str):
tree = parse_ast(val, mode="eval")
self._add_name_attr_const(tree, is_str_annotation)
try:
# Parse string names/attrs.
if isinstance(node, (ast.Constant, ast.Str)):
val = getattr(node, "value", "") or getattr(node, "s", "")
if val and isinstance(val, str):
val = val.strip()
tree = parse_ast(val, mode="eval")
self._add_name_attr_const(tree, is_str_annotation)
except UnparsableFile:
#: Ignore errors when parsing Literals
#: that are not valid identifiers (e.g. contain white-spaces).
#:
#: >>> from typing import Literal
#: >>> L: Literal[" "] = " "
#:
#: Issue: https://github.com/hadialqattan/pycln/issues/41
pass

def _add_concatenated_list_names(self, node: ast.BinOp) -> None:
#: Safely add `["x", "y"] + ["i", "j"]`
Expand Down
Loading

0 comments on commit 907b865

Please sign in to comment.