add some diagnostic for unused ignores for tests typechecking (#191)

This commit is contained in:
Maxim Kurnikov
2019-09-28 21:38:56 +03:00
committed by GitHub
parent acc3ca4205
commit 32c0cbca45
2 changed files with 32 additions and 31 deletions

View File

@@ -15,19 +15,16 @@ MOCK_OBJECTS = ['MockRequest', 'MockCompiler', 'modelz', 'call_count', 'call_arg
EXTERNAL_MODULES = ['psycopg2', 'PIL', 'selenium', 'oracle', 'mysql', 'sqlite3', 'sqlparse', 'tblib', 'numpy', EXTERNAL_MODULES = ['psycopg2', 'PIL', 'selenium', 'oracle', 'mysql', 'sqlite3', 'sqlparse', 'tblib', 'numpy',
'bcrypt', 'argon2', 'xml.dom'] 'bcrypt', 'argon2', 'xml.dom']
IGNORED_ERRORS = { IGNORED_ERRORS = {
'__new_common__': [ '__common__': [
*MOCK_OBJECTS, *MOCK_OBJECTS,
*EXTERNAL_MODULES, *EXTERNAL_MODULES,
'Need type annotation for', 'Need type annotation for',
'has no attribute "getvalue"', 'has no attribute "getvalue"',
'Cannot assign to a method', 'Cannot assign to a method',
'Cannot infer type of lambda',
'already defined', 'already defined',
'Cannot assign to a type', 'Cannot assign to a type',
'"HttpResponse" has no attribute',
'"HttpResponseBase" has no attribute', '"HttpResponseBase" has no attribute',
'"object" has no attribute', '"object" has no attribute',
'defined in the current module',
re.compile(r'"Callable\[(\[(Any(, )?)*((, )?VarArg\(Any\))?((, )?KwArg\(Any\))?\]|\.\.\.), Any\]" ' re.compile(r'"Callable\[(\[(Any(, )?)*((, )?VarArg\(Any\))?((, )?KwArg\(Any\))?\]|\.\.\.), Any\]" '
r'has no attribute'), r'has no attribute'),
'has no attribute "deconstruct"', 'has no attribute "deconstruct"',
@@ -48,7 +45,6 @@ IGNORED_ERRORS = {
'gets multiple values for keyword argument', 'gets multiple values for keyword argument',
'"Handler" has no attribute', '"Handler" has no attribute',
'Module has no attribute', 'Module has no attribute',
"No installed app with label 'missing'",
'namedtuple', 'namedtuple',
'Lookups not supported yet', 'Lookups not supported yet',
# TODO: see test in managers/test_managers.yml # TODO: see test in managers/test_managers.yml
@@ -116,7 +112,6 @@ IGNORED_ERRORS = {
], ],
'custom_managers': [ 'custom_managers': [
'Unsupported dynamic base class', 'Unsupported dynamic base class',
'"Book" has no attribute "favorite_avg"',
'Incompatible types in assignment (expression has type "CharField', 'Incompatible types in assignment (expression has type "CharField',
'Item "Book" of "Optional[Book]" has no attribute "favorite_avg"' 'Item "Book" of "Optional[Book]" has no attribute "favorite_avg"'
], ],
@@ -151,13 +146,8 @@ IGNORED_ERRORS = {
'has incompatible type "str"' 'has incompatible type "str"'
], ],
'file_uploads': [ 'file_uploads': [
'"Iterable[Any]" has no attribute',
'"IO[Any]" has no attribute',
'has no attribute "content_type"', 'has no attribute "content_type"',
], ],
'file_storage': [
'Incompatible types in assignment (expression has type "Callable"'
],
'files': [ 'files': [
'Incompatible types in assignment (expression has type "IOBase", variable has type "File")', 'Incompatible types in assignment (expression has type "IOBase", variable has type "File")',
], ],
@@ -196,9 +186,6 @@ IGNORED_ERRORS = {
'middleware': [ 'middleware': [
'"HttpRequest" has no attribute' '"HttpRequest" has no attribute'
], ],
'managers_regress': [
'"Type[AbstractBase3]" has no attribute "objects"'
],
'many_to_one': [ 'many_to_one': [
'Incompatible type for "parent" of "Child" (got "None", expected "Union[Parent, Combinable]")', 'Incompatible type for "parent" of "Child" (got "None", expected "Union[Parent, Combinable]")',
'Incompatible type for "parent" of "Child" (got "Child", expected "Union[Parent, Combinable]")' 'Incompatible type for "parent" of "Child" (got "Child", expected "Union[Parent, Combinable]")'
@@ -208,7 +195,6 @@ IGNORED_ERRORS = {
], ],
'model_fields': [ 'model_fields': [
'Item "Field[Any, Any]" of "Union[Field[Any, Any], ForeignObjectRel]" has no attribute', 'Item "Field[Any, Any]" of "Union[Field[Any, Any], ForeignObjectRel]" has no attribute',
'has no attribute "field"',
'Incompatible types in assignment (expression has type "Type[Person', 'Incompatible types in assignment (expression has type "Type[Person',
'base class "IntegerFieldTests"', 'base class "IntegerFieldTests"',
'ImageFieldTestMixin', 'ImageFieldTestMixin',
@@ -252,9 +238,6 @@ IGNORED_ERRORS = {
re.compile(r'Incompatible types in assignment \(expression has type "Type\[.+?\]", ' re.compile(r'Incompatible types in assignment \(expression has type "Type\[.+?\]", '
r'base class "(UnaccentTest|TrigramTest)" defined the type as "Type\[CharFieldModel\]"\)'), r'base class "(UnaccentTest|TrigramTest)" defined the type as "Type\[CharFieldModel\]"\)'),
'("None" and "SearchQuery")', '("None" and "SearchQuery")',
# TODO:
'django.contrib.postgres.forms',
'django.contrib.postgres.aggregates',
], ],
'properties': [ 'properties': [
re.compile('Unexpected attribute "(full_name|full_name_2)" for model "Person"') re.compile('Unexpected attribute "(full_name|full_name_2)" for model "Person"')
@@ -278,8 +261,6 @@ IGNORED_ERRORS = {
'Unsupported operand types for & ("Manager[Author]" and "Manager[Tag]")', 'Unsupported operand types for & ("Manager[Author]" and "Manager[Tag]")',
'Unsupported operand types for | ("Manager[Author]" and "Manager[Tag]")', 'Unsupported operand types for | ("Manager[Author]" and "Manager[Tag]")',
'ObjectA', 'ObjectA',
'ObjectB',
'ObjectC',
"'flat' and 'named' can't be used together", "'flat' and 'named' can't be used together",
'"Collection[Any]" has no attribute "explain"' '"Collection[Any]" has no attribute "explain"'
], ],
@@ -337,7 +318,6 @@ IGNORED_ERRORS = {
'Argument 1 to "get_callable" has incompatible type "int"' 'Argument 1 to "get_callable" has incompatible type "int"'
], ],
'utils_tests': [ 'utils_tests': [
'Too few arguments for "__init__"',
'Argument 1 to "activate" has incompatible type "None"; expected "Union[tzinfo, str]"', 'Argument 1 to "activate" has incompatible type "None"; expected "Union[tzinfo, str]"',
'Incompatible types in assignment (expression has type "None", base class "object" defined the type as', 'Incompatible types in assignment (expression has type "None", base class "object" defined the type as',
'Class', 'Class',
@@ -352,3 +332,19 @@ IGNORED_ERRORS = {
"Module 'django.views.debug' has no attribute 'Path'" "Module 'django.views.debug' has no attribute 'Path'"
] ]
} }
def check_if_custom_ignores_are_covered_by_common() -> None:
from scripts.typecheck_tests import is_pattern_fits
common_ignore_patterns = IGNORED_ERRORS['__common__']
for module_name, patterns in IGNORED_ERRORS.items():
if module_name == '__common__':
continue
for pattern in patterns:
for common_pattern in common_ignore_patterns:
if isinstance(pattern, str) and is_pattern_fits(common_pattern, pattern):
print(f'pattern "{module_name}: {pattern!r}" is covered by pattern {common_pattern!r}')
check_if_custom_ignores_are_covered_by_common()

View File

@@ -4,7 +4,7 @@ import subprocess
import sys import sys
from collections import defaultdict from collections import defaultdict
from pathlib import Path from pathlib import Path
from typing import Dict, Pattern, Union from typing import Dict, List, Pattern, Union
from scripts.enabled_test_modules import ( from scripts.enabled_test_modules import (
EXTERNAL_MODULES, IGNORED_ERRORS, IGNORED_MODULES, MOCK_OBJECTS, EXTERNAL_MODULES, IGNORED_ERRORS, IGNORED_MODULES, MOCK_OBJECTS,
@@ -13,12 +13,14 @@ from scripts.enabled_test_modules import (
PROJECT_DIRECTORY = Path(__file__).parent.parent PROJECT_DIRECTORY = Path(__file__).parent.parent
def print_unused_ignores(ignored_message_freq): def get_unused_ignores(ignored_message_freq: Dict[str, Dict[Union[str, Pattern], int]]) -> List[str]:
unused_ignores = []
for root_key, patterns in IGNORED_ERRORS.items(): for root_key, patterns in IGNORED_ERRORS.items():
for pattern in patterns: for pattern in patterns:
if (ignored_message_freq[root_key][pattern] == 0 if (ignored_message_freq[root_key][pattern] == 0
and pattern not in itertools.chain(EXTERNAL_MODULES, MOCK_OBJECTS)): and pattern not in itertools.chain(EXTERNAL_MODULES, MOCK_OBJECTS)):
print(f'{root_key}: {pattern}') unused_ignores.append(f'{root_key}: {pattern}')
return unused_ignores
def is_pattern_fits(pattern: Union[Pattern, str], line: str): def is_pattern_fits(pattern: Union[Pattern, str], line: str):
@@ -38,16 +40,16 @@ def is_ignored(line: str, test_folder_name: str, *, ignored_message_freqs: Dict[
if test_folder_name in IGNORED_MODULES: if test_folder_name in IGNORED_MODULES:
return True return True
for pattern in IGNORED_ERRORS['__new_common__']:
if is_pattern_fits(pattern, line):
ignored_message_freqs['__new_common__'][pattern] += 1
return True
for pattern in IGNORED_ERRORS.get(test_folder_name, []): for pattern in IGNORED_ERRORS.get(test_folder_name, []):
if is_pattern_fits(pattern, line): if is_pattern_fits(pattern, line):
ignored_message_freqs[test_folder_name][pattern] += 1 ignored_message_freqs[test_folder_name][pattern] += 1
return True return True
for pattern in IGNORED_ERRORS['__common__']:
if is_pattern_fits(pattern, line):
ignored_message_freqs['__common__'][pattern] += 1
return True
return False return False
@@ -108,8 +110,11 @@ if __name__ == '__main__':
global_rc = 1 global_rc = 1
print(line) print(line)
print('UNUSED IGNORES ------------------------------------------------') unused_ignores = get_unused_ignores(ignored_message_freqs)
print_unused_ignores(ignored_message_freqs) if unused_ignores:
print('UNUSED IGNORES ------------------------------------------------')
print('\n'.join(unused_ignores))
sys.exit(global_rc) sys.exit(global_rc)
except BaseException as exc: except BaseException as exc: