mirror of
https://github.com/davidhalter/django-stubs.git
synced 2025-12-08 21:14:49 +08:00
add some diagnostic for unused ignores for tests typechecking (#191)
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user