Files
django-stubs/scripts/catch_non_abstract_annotation.py
Maxim Kurnikov 8402e7c53e improve annotations in some places (#202)
* improve annotations in some places

* linting
2019-10-07 14:50:45 +03:00

89 lines
3.5 KiB
Python

import os
from typing import Optional
import libcst
from libcst import Annotation, BaseExpression, FunctionDef, Name, Subscript
from libcst.metadata import SyntacticPositionProvider
BASE_DIR = 'django-stubs'
fpath = os.path.join(BASE_DIR, 'core', 'checks', 'model_checks.pyi')
with open(fpath, 'r') as f:
contents = f.read()
tree = libcst.parse_module(contents)
class TypeAnnotationsAnalyzer(libcst.CSTVisitor):
METADATA_DEPENDENCIES = (SyntacticPositionProvider,)
def __init__(self, fpath: str):
super().__init__()
self.fpath = fpath
def get_node_location(self, node: FunctionDef) -> str:
start_line = self.get_metadata(SyntacticPositionProvider, node).start.line
return f'{self.fpath}:{start_line}'
def show_error_for_node(self, node: FunctionDef, error_message: str):
print(self.get_node_location(node), error_message)
def check_subscripted_annotation(self, annotation: BaseExpression) -> Optional[str]:
if isinstance(annotation, Subscript):
if isinstance(annotation.value, Name):
error_message = self.check_concrete_class_usage(annotation.value)
if error_message:
return error_message
if annotation.value.value == 'Union':
for slice_param in annotation.slice:
if isinstance(slice_param.slice.value, Name):
error_message = self.check_concrete_class_usage(annotation.value)
if error_message:
return error_message
def check_concrete_class_usage(self, name_node: Name) -> Optional[str]:
if name_node.value == 'List':
return (f'Concrete class {name_node.value!r} used for an iterable annotation. '
f'Use abstract collection (Iterable, Collection, Sequence) instead')
def visit_FunctionDef(self, node: FunctionDef) -> Optional[bool]:
params_node = node.params
for param_node in [*params_node.params, *params_node.default_params]:
param_name = param_node.name.value
annotation_node = param_node.annotation # type: Annotation
if annotation_node is not None:
annotation = annotation_node.annotation
if annotation.value == 'None':
self.show_error_for_node(node, f'"None" type annotation used for parameter {param_name!r}')
continue
error_message = self.check_subscripted_annotation(annotation)
if error_message is not None:
self.show_error_for_node(node, error_message)
continue
if node.returns is not None:
return_annotation = node.returns.annotation
if isinstance(return_annotation, Subscript) and return_annotation.value.value == 'Union':
self.show_error_for_node(node, 'Union is return type annotation')
return False
for dirpath, dirnames, filenames in os.walk(BASE_DIR):
for filename in filenames:
fpath = os.path.join(dirpath, filename)
# skip all other checks for now, low priority
if not fpath.startswith(('django-stubs/db', 'django-stubs/views', 'django-stubs/apps',
'django-stubs/http', 'django-stubs/contrib/postgres')):
continue
with open(fpath, 'r') as f:
contents = f.read()
tree = libcst.MetadataWrapper(libcst.parse_module(contents))
analyzer = TypeAnnotationsAnalyzer(fpath)
tree.visit(analyzer)