mirror of
https://github.com/davidhalter/django-stubs.git
synced 2025-12-08 04:54:48 +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',
|
||||
'bcrypt', 'argon2', 'xml.dom']
|
||||
IGNORED_ERRORS = {
|
||||
'__new_common__': [
|
||||
'__common__': [
|
||||
*MOCK_OBJECTS,
|
||||
*EXTERNAL_MODULES,
|
||||
'Need type annotation for',
|
||||
'has no attribute "getvalue"',
|
||||
'Cannot assign to a method',
|
||||
'Cannot infer type of lambda',
|
||||
'already defined',
|
||||
'Cannot assign to a type',
|
||||
'"HttpResponse" has no attribute',
|
||||
'"HttpResponseBase" has no attribute',
|
||||
'"object" has no attribute',
|
||||
'defined in the current module',
|
||||
re.compile(r'"Callable\[(\[(Any(, )?)*((, )?VarArg\(Any\))?((, )?KwArg\(Any\))?\]|\.\.\.), Any\]" '
|
||||
r'has no attribute'),
|
||||
'has no attribute "deconstruct"',
|
||||
@@ -48,7 +45,6 @@ IGNORED_ERRORS = {
|
||||
'gets multiple values for keyword argument',
|
||||
'"Handler" has no attribute',
|
||||
'Module has no attribute',
|
||||
"No installed app with label 'missing'",
|
||||
'namedtuple',
|
||||
'Lookups not supported yet',
|
||||
# TODO: see test in managers/test_managers.yml
|
||||
@@ -116,7 +112,6 @@ IGNORED_ERRORS = {
|
||||
],
|
||||
'custom_managers': [
|
||||
'Unsupported dynamic base class',
|
||||
'"Book" has no attribute "favorite_avg"',
|
||||
'Incompatible types in assignment (expression has type "CharField',
|
||||
'Item "Book" of "Optional[Book]" has no attribute "favorite_avg"'
|
||||
],
|
||||
@@ -151,13 +146,8 @@ IGNORED_ERRORS = {
|
||||
'has incompatible type "str"'
|
||||
],
|
||||
'file_uploads': [
|
||||
'"Iterable[Any]" has no attribute',
|
||||
'"IO[Any]" has no attribute',
|
||||
'has no attribute "content_type"',
|
||||
],
|
||||
'file_storage': [
|
||||
'Incompatible types in assignment (expression has type "Callable"'
|
||||
],
|
||||
'files': [
|
||||
'Incompatible types in assignment (expression has type "IOBase", variable has type "File")',
|
||||
],
|
||||
@@ -196,9 +186,6 @@ IGNORED_ERRORS = {
|
||||
'middleware': [
|
||||
'"HttpRequest" has no attribute'
|
||||
],
|
||||
'managers_regress': [
|
||||
'"Type[AbstractBase3]" has no attribute "objects"'
|
||||
],
|
||||
'many_to_one': [
|
||||
'Incompatible type for "parent" of "Child" (got "None", expected "Union[Parent, Combinable]")',
|
||||
'Incompatible type for "parent" of "Child" (got "Child", expected "Union[Parent, Combinable]")'
|
||||
@@ -208,7 +195,6 @@ IGNORED_ERRORS = {
|
||||
],
|
||||
'model_fields': [
|
||||
'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',
|
||||
'base class "IntegerFieldTests"',
|
||||
'ImageFieldTestMixin',
|
||||
@@ -252,9 +238,6 @@ IGNORED_ERRORS = {
|
||||
re.compile(r'Incompatible types in assignment \(expression has type "Type\[.+?\]", '
|
||||
r'base class "(UnaccentTest|TrigramTest)" defined the type as "Type\[CharFieldModel\]"\)'),
|
||||
'("None" and "SearchQuery")',
|
||||
# TODO:
|
||||
'django.contrib.postgres.forms',
|
||||
'django.contrib.postgres.aggregates',
|
||||
],
|
||||
'properties': [
|
||||
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]")',
|
||||
'ObjectA',
|
||||
'ObjectB',
|
||||
'ObjectC',
|
||||
"'flat' and 'named' can't be used together",
|
||||
'"Collection[Any]" has no attribute "explain"'
|
||||
],
|
||||
@@ -337,7 +318,6 @@ IGNORED_ERRORS = {
|
||||
'Argument 1 to "get_callable" has incompatible type "int"'
|
||||
],
|
||||
'utils_tests': [
|
||||
'Too few arguments for "__init__"',
|
||||
'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',
|
||||
'Class',
|
||||
@@ -352,3 +332,19 @@ IGNORED_ERRORS = {
|
||||
"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
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Dict, Pattern, Union
|
||||
from typing import Dict, List, Pattern, Union
|
||||
|
||||
from scripts.enabled_test_modules import (
|
||||
EXTERNAL_MODULES, IGNORED_ERRORS, IGNORED_MODULES, MOCK_OBJECTS,
|
||||
@@ -13,12 +13,14 @@ from scripts.enabled_test_modules import (
|
||||
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 pattern in patterns:
|
||||
if (ignored_message_freq[root_key][pattern] == 0
|
||||
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):
|
||||
@@ -38,16 +40,16 @@ def is_ignored(line: str, test_folder_name: str, *, ignored_message_freqs: Dict[
|
||||
if test_folder_name in IGNORED_MODULES:
|
||||
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, []):
|
||||
if is_pattern_fits(pattern, line):
|
||||
ignored_message_freqs[test_folder_name][pattern] += 1
|
||||
return True
|
||||
|
||||
for pattern in IGNORED_ERRORS['__common__']:
|
||||
if is_pattern_fits(pattern, line):
|
||||
ignored_message_freqs['__common__'][pattern] += 1
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@@ -108,8 +110,11 @@ if __name__ == '__main__':
|
||||
global_rc = 1
|
||||
print(line)
|
||||
|
||||
print('UNUSED IGNORES ------------------------------------------------')
|
||||
print_unused_ignores(ignored_message_freqs)
|
||||
unused_ignores = get_unused_ignores(ignored_message_freqs)
|
||||
if unused_ignores:
|
||||
print('UNUSED IGNORES ------------------------------------------------')
|
||||
print('\n'.join(unused_ignores))
|
||||
|
||||
sys.exit(global_rc)
|
||||
|
||||
except BaseException as exc:
|
||||
|
||||
Reference in New Issue
Block a user