mirror of
https://github.com/davidhalter/django-stubs.git
synced 2025-12-07 04:34:29 +08:00
add support for django.conf.settings.SETTING_NAME
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
pytest_plugins = [
|
pytest_plugins = [
|
||||||
'mypy.test.data'
|
'test.data'
|
||||||
]
|
]
|
||||||
@@ -1,4 +1,14 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from django.utils.functional import LazyObject
|
||||||
|
|
||||||
settings = ... # type: Any
|
|
||||||
|
# required for plugin to be able to distinguish this specific instance of LazySettings from others
|
||||||
|
class _DjangoConfLazyObject(LazyObject): ...
|
||||||
|
|
||||||
|
|
||||||
|
class LazySettings(_DjangoConfLazyObject):
|
||||||
|
configured: bool
|
||||||
|
def configure(self, default_settings: Any = ..., **options: Any) -> Any: ...
|
||||||
|
|
||||||
|
settings: LazySettings = ...
|
||||||
57
django-stubs/utils/functional.pyi
Normal file
57
django-stubs/utils/functional.pyi
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
|
from django.db.models.base import Model
|
||||||
|
|
||||||
|
|
||||||
|
def curry(_curried_func: Any, *args: Any, **kwargs: Any): ...
|
||||||
|
|
||||||
|
class cached_property:
|
||||||
|
func: Callable = ...
|
||||||
|
__doc__: Any = ...
|
||||||
|
name: str = ...
|
||||||
|
def __init__(self, func: Callable, name: Optional[str] = ...) -> None: ...
|
||||||
|
def __get__(self, instance: Any, cls: Type[Any] = ...) -> Any: ...
|
||||||
|
|
||||||
|
class Promise: ...
|
||||||
|
|
||||||
|
def lazy(func: Union[Callable, Type[str]], *resultclasses: Any) -> Callable: ...
|
||||||
|
def lazystr(text: Any): ...
|
||||||
|
def keep_lazy(*resultclasses: Any) -> Callable: ...
|
||||||
|
def keep_lazy_text(func: Callable) -> Callable: ...
|
||||||
|
|
||||||
|
empty: Any
|
||||||
|
|
||||||
|
def new_method_proxy(func: Any): ...
|
||||||
|
|
||||||
|
class LazyObject:
|
||||||
|
def __init__(self) -> None: ...
|
||||||
|
__getattr__: Any = ...
|
||||||
|
def __setattr__(self, name: str, value: Any) -> None: ...
|
||||||
|
def __delattr__(self, name: str) -> None: ...
|
||||||
|
def __reduce__(self) -> Tuple[Callable, Tuple[Model]]: ...
|
||||||
|
def __copy__(self): ...
|
||||||
|
def __deepcopy__(self, memo: Any): ...
|
||||||
|
__bytes__: Any = ...
|
||||||
|
__bool__: Any = ...
|
||||||
|
__dir__: Any = ...
|
||||||
|
__class__: Any = ...
|
||||||
|
__eq__: Any = ...
|
||||||
|
__ne__: Any = ...
|
||||||
|
__hash__: Any = ...
|
||||||
|
__getitem__: Any = ...
|
||||||
|
__setitem__: Any = ...
|
||||||
|
__delitem__: Any = ...
|
||||||
|
__iter__: Any = ...
|
||||||
|
__len__: Any = ...
|
||||||
|
__contains__: Any = ...
|
||||||
|
|
||||||
|
def unpickle_lazyobject(wrapped: Model) -> Model: ...
|
||||||
|
|
||||||
|
class SimpleLazyObject(LazyObject):
|
||||||
|
def __init__(self, func: Callable) -> None: ...
|
||||||
|
def __copy__(self) -> List[int]: ...
|
||||||
|
def __deepcopy__(self, memo: Dict[Any, Any]) -> List[int]: ...
|
||||||
|
|
||||||
|
def partition(
|
||||||
|
predicate: Callable, values: List[Model]
|
||||||
|
) -> Tuple[List[Model], List[Model]]: ...
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
from typing import Dict, Optional, Type, Tuple, NamedTuple
|
from typing import Dict, Optional, NamedTuple
|
||||||
|
|
||||||
from mypy.nodes import SymbolTableNode, Var, Expression, MemberExpr
|
from mypy.semanal import SemanticAnalyzerPass2
|
||||||
|
from mypy.types import Type
|
||||||
|
from mypy.nodes import SymbolTableNode, Var, Expression
|
||||||
from mypy.plugin import FunctionContext
|
from mypy.plugin import FunctionContext
|
||||||
from mypy.types import Instance
|
from mypy.types import Instance, UnionType, NoneTyp
|
||||||
|
|
||||||
MODEL_CLASS_FULLNAME = 'django.db.models.base.Model'
|
MODEL_CLASS_FULLNAME = 'django.db.models.base.Model'
|
||||||
QUERYSET_CLASS_FULLNAME = 'django.db.models.query.QuerySet'
|
QUERYSET_CLASS_FULLNAME = 'django.db.models.query.QuerySet'
|
||||||
@@ -13,7 +15,6 @@ ONETOONE_FIELD_FULLNAME = 'django.db.models.fields.related.OneToOneField'
|
|||||||
def create_new_symtable_node(name: str, kind: int, instance: Instance) -> SymbolTableNode:
|
def create_new_symtable_node(name: str, kind: int, instance: Instance) -> SymbolTableNode:
|
||||||
new_var = Var(name, instance)
|
new_var = Var(name, instance)
|
||||||
new_var.info = instance.type
|
new_var.info = instance.type
|
||||||
|
|
||||||
return SymbolTableNode(kind, new_var,
|
return SymbolTableNode(kind, new_var,
|
||||||
plugin_generated=True)
|
plugin_generated=True)
|
||||||
|
|
||||||
@@ -53,3 +54,14 @@ def get_call_signature_or_none(ctx: FunctionContext) -> Optional[Dict[str, Argum
|
|||||||
result[arg_name] = (arg[0], arg_type[0])
|
result[arg_name] = (arg[0], arg_type[0])
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def make_optional(typ: Type) -> Type:
|
||||||
|
return UnionType.make_simplified_union([typ, NoneTyp()])
|
||||||
|
|
||||||
|
|
||||||
|
def make_required(typ: Type) -> Type:
|
||||||
|
if not isinstance(typ, UnionType):
|
||||||
|
return typ
|
||||||
|
items = [item for item in typ.items if not isinstance(item, NoneTyp)]
|
||||||
|
return UnionType.make_union(items)
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import os
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
from django.conf import Settings
|
||||||
|
from mypy.options import Options
|
||||||
from mypy.plugin import Plugin, FunctionContext, ClassDefContext
|
from mypy.plugin import Plugin, FunctionContext, ClassDefContext
|
||||||
from mypy.types import Type
|
from mypy.types import Type
|
||||||
|
|
||||||
@@ -8,6 +11,7 @@ from mypy_django_plugin.plugins.objects_queryset import set_objects_queryset_to_
|
|||||||
from mypy_django_plugin.plugins.postgres_fields import determine_type_of_array_field
|
from mypy_django_plugin.plugins.postgres_fields import determine_type_of_array_field
|
||||||
from mypy_django_plugin.plugins.related_fields import set_related_name_instance_for_onetoonefield, \
|
from mypy_django_plugin.plugins.related_fields import set_related_name_instance_for_onetoonefield, \
|
||||||
set_related_name_manager_for_foreign_key, set_fieldname_attrs_for_related_fields
|
set_related_name_manager_for_foreign_key, set_fieldname_attrs_for_related_fields
|
||||||
|
from mypy_django_plugin.plugins.setup_settings import DjangoConfSettingsInitializerHook
|
||||||
|
|
||||||
|
|
||||||
base_model_classes = {helpers.MODEL_CLASS_FULLNAME}
|
base_model_classes = {helpers.MODEL_CLASS_FULLNAME}
|
||||||
@@ -21,6 +25,15 @@ def transform_model_class(ctx: ClassDefContext) -> None:
|
|||||||
|
|
||||||
|
|
||||||
class DjangoPlugin(Plugin):
|
class DjangoPlugin(Plugin):
|
||||||
|
def __init__(self,
|
||||||
|
options: Options) -> None:
|
||||||
|
super().__init__(options)
|
||||||
|
self.django_settings = None
|
||||||
|
|
||||||
|
django_settings_module = os.environ.get('DJANGO_SETTINGS_MODULE')
|
||||||
|
if django_settings_module:
|
||||||
|
self.django_settings = Settings(django_settings_module)
|
||||||
|
|
||||||
def get_function_hook(self, fullname: str
|
def get_function_hook(self, fullname: str
|
||||||
) -> Optional[Callable[[FunctionContext], Type]]:
|
) -> Optional[Callable[[FunctionContext], Type]]:
|
||||||
if fullname == helpers.FOREIGN_KEY_FULLNAME:
|
if fullname == helpers.FOREIGN_KEY_FULLNAME:
|
||||||
@@ -37,6 +50,8 @@ class DjangoPlugin(Plugin):
|
|||||||
) -> Optional[Callable[[ClassDefContext], None]]:
|
) -> Optional[Callable[[ClassDefContext], None]]:
|
||||||
if fullname in base_model_classes:
|
if fullname in base_model_classes:
|
||||||
return transform_model_class
|
return transform_model_class
|
||||||
|
if fullname == 'django.conf._DjangoConfLazyObject':
|
||||||
|
return DjangoConfSettingsInitializerHook(settings=self.django_settings)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
34
mypy_django_plugin/plugins/setup_settings.py
Normal file
34
mypy_django_plugin/plugins/setup_settings.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from typing import cast, Any
|
||||||
|
|
||||||
|
from django.conf import Settings
|
||||||
|
from mypy.nodes import MDEF, TypeInfo, SymbolTable
|
||||||
|
from mypy.plugin import ClassDefContext
|
||||||
|
from mypy.semanal import SemanticAnalyzerPass2
|
||||||
|
from mypy.types import Instance, AnyType, TypeOfAny
|
||||||
|
|
||||||
|
from mypy_django_plugin import helpers
|
||||||
|
|
||||||
|
|
||||||
|
def get_obj_type_name(value: Any) -> str:
|
||||||
|
return type(value).__module__ + '.' + type(value).__qualname__
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoConfSettingsInitializerHook(object):
|
||||||
|
def __init__(self, settings: Settings):
|
||||||
|
self.settings = settings
|
||||||
|
|
||||||
|
def __call__(self, ctx: ClassDefContext) -> None:
|
||||||
|
api = cast(SemanticAnalyzerPass2, ctx.api)
|
||||||
|
for name, value in self.settings.__dict__.items():
|
||||||
|
if name.isupper():
|
||||||
|
if value is None:
|
||||||
|
ctx.cls.info.names[name] = helpers.create_new_symtable_node(name, MDEF,
|
||||||
|
instance=api.builtin_type('builtins.object'))
|
||||||
|
continue
|
||||||
|
|
||||||
|
type_fullname = get_obj_type_name(value)
|
||||||
|
sym = api.lookup_fully_qualified_or_none(type_fullname)
|
||||||
|
if sym is not None:
|
||||||
|
args = len(sym.node.type_vars) * [AnyType(TypeOfAny.from_omitted_generics)]
|
||||||
|
ctx.cls.info.names[name] = helpers.create_new_symtable_node(name, MDEF,
|
||||||
|
instance=Instance(sym.node, args))
|
||||||
0
test/__init__.py
Normal file
0
test/__init__.py
Normal file
244
test/data.py
Normal file
244
test/data.py
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
import os
|
||||||
|
import posixpath
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from typing import Any, Optional, Iterator, Dict, List, Tuple, Set
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from mypy.test.config import test_temp_dir
|
||||||
|
from mypy.test.data import DataDrivenTestCase, DataSuite, add_test_name_suffix, parse_test_data, \
|
||||||
|
expand_errors, expand_variables, fix_win_path
|
||||||
|
|
||||||
|
|
||||||
|
def parse_test_case(case: 'DataDrivenTestCase') -> None:
|
||||||
|
"""Parse and prepare a single case from suite with test case descriptions.
|
||||||
|
|
||||||
|
This method is part of the setup phase, just before the test case is run.
|
||||||
|
"""
|
||||||
|
test_items = parse_test_data(case.data, case.name)
|
||||||
|
base_path = case.suite.base_path
|
||||||
|
if case.suite.native_sep:
|
||||||
|
join = os.path.join
|
||||||
|
else:
|
||||||
|
join = posixpath.join # type: ignore
|
||||||
|
|
||||||
|
out_section_missing = case.suite.required_out_section
|
||||||
|
|
||||||
|
files = [] # type: List[Tuple[str, str]] # path and contents
|
||||||
|
output_files = [] # type: List[Tuple[str, str]] # path and contents for output files
|
||||||
|
output = [] # type: List[str] # Regular output errors
|
||||||
|
output2 = {} # type: Dict[int, List[str]] # Output errors for incremental, runs 2+
|
||||||
|
deleted_paths = {} # type: Dict[int, Set[str]] # from run number of paths
|
||||||
|
stale_modules = {} # type: Dict[int, Set[str]] # from run number to module names
|
||||||
|
rechecked_modules = {} # type: Dict[ int, Set[str]] # from run number module names
|
||||||
|
triggered = [] # type: List[str] # Active triggers (one line per incremental step)
|
||||||
|
|
||||||
|
# Process the parsed items. Each item has a header of form [id args],
|
||||||
|
# optionally followed by lines of text.
|
||||||
|
item = first_item = test_items[0]
|
||||||
|
for item in test_items[1:]:
|
||||||
|
if item.id == 'file' or item.id == 'outfile':
|
||||||
|
# Record an extra file needed for the test case.
|
||||||
|
assert item.arg is not None
|
||||||
|
contents = expand_variables('\n'.join(item.data))
|
||||||
|
file_entry = (join(base_path, item.arg), contents)
|
||||||
|
if item.id == 'file':
|
||||||
|
files.append(file_entry)
|
||||||
|
else:
|
||||||
|
output_files.append(file_entry)
|
||||||
|
elif item.id in ('builtins', 'builtins_py2'):
|
||||||
|
# Use an alternative stub file for the builtins module.
|
||||||
|
assert item.arg is not None
|
||||||
|
mpath = join(os.path.dirname(case.file), item.arg)
|
||||||
|
fnam = 'builtins.pyi' if item.id == 'builtins' else '__builtin__.pyi'
|
||||||
|
with open(mpath) as f:
|
||||||
|
files.append((join(base_path, fnam), f.read()))
|
||||||
|
elif item.id == 'typing':
|
||||||
|
# Use an alternative stub file for the typing module.
|
||||||
|
assert item.arg is not None
|
||||||
|
src_path = join(os.path.dirname(case.file), item.arg)
|
||||||
|
with open(src_path) as f:
|
||||||
|
files.append((join(base_path, 'typing.pyi'), f.read()))
|
||||||
|
elif re.match(r'stale[0-9]*$', item.id):
|
||||||
|
passnum = 1 if item.id == 'stale' else int(item.id[len('stale'):])
|
||||||
|
assert passnum > 0
|
||||||
|
modules = (set() if item.arg is None else {t.strip() for t in item.arg.split(',')})
|
||||||
|
stale_modules[passnum] = modules
|
||||||
|
elif re.match(r'rechecked[0-9]*$', item.id):
|
||||||
|
passnum = 1 if item.id == 'rechecked' else int(item.id[len('rechecked'):])
|
||||||
|
assert passnum > 0
|
||||||
|
modules = (set() if item.arg is None else {t.strip() for t in item.arg.split(',')})
|
||||||
|
rechecked_modules[passnum] = modules
|
||||||
|
elif item.id == 'delete':
|
||||||
|
# File to delete during a multi-step test case
|
||||||
|
assert item.arg is not None
|
||||||
|
m = re.match(r'(.*)\.([0-9]+)$', item.arg)
|
||||||
|
assert m, 'Invalid delete section: {}'.format(item.arg)
|
||||||
|
num = int(m.group(2))
|
||||||
|
assert num >= 2, "Can't delete during step {}".format(num)
|
||||||
|
full = join(base_path, m.group(1))
|
||||||
|
deleted_paths.setdefault(num, set()).add(full)
|
||||||
|
elif re.match(r'out[0-9]*$', item.id):
|
||||||
|
tmp_output = [expand_variables(line) for line in item.data]
|
||||||
|
if os.path.sep == '\\':
|
||||||
|
tmp_output = [fix_win_path(line) for line in tmp_output]
|
||||||
|
if item.id == 'out' or item.id == 'out1':
|
||||||
|
output = tmp_output
|
||||||
|
else:
|
||||||
|
passnum = int(item.id[len('out'):])
|
||||||
|
assert passnum > 1
|
||||||
|
output2[passnum] = tmp_output
|
||||||
|
out_section_missing = False
|
||||||
|
elif item.id == 'triggered' and item.arg is None:
|
||||||
|
triggered = item.data
|
||||||
|
elif item.id == 'env':
|
||||||
|
env_vars_to_set = item.arg
|
||||||
|
for env in env_vars_to_set.split(';'):
|
||||||
|
try:
|
||||||
|
name, value = env.split('=')
|
||||||
|
os.environ[name] = value
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
'Invalid section header {} in {} at line {}'.format(
|
||||||
|
item.id, case.file, item.line))
|
||||||
|
|
||||||
|
if out_section_missing:
|
||||||
|
raise ValueError(
|
||||||
|
'{}, line {}: Required output section not found'.format(
|
||||||
|
case.file, first_item.line))
|
||||||
|
|
||||||
|
for passnum in stale_modules.keys():
|
||||||
|
if passnum not in rechecked_modules:
|
||||||
|
# If the set of rechecked modules isn't specified, make it the same as the set
|
||||||
|
# of modules with a stale public interface.
|
||||||
|
rechecked_modules[passnum] = stale_modules[passnum]
|
||||||
|
if (passnum in stale_modules
|
||||||
|
and passnum in rechecked_modules
|
||||||
|
and not stale_modules[passnum].issubset(rechecked_modules[passnum])):
|
||||||
|
raise ValueError(
|
||||||
|
('Stale modules after pass {} must be a subset of rechecked '
|
||||||
|
'modules ({}:{})').format(passnum, case.file, first_item.line))
|
||||||
|
|
||||||
|
input = first_item.data
|
||||||
|
expand_errors(input, output, 'main')
|
||||||
|
for file_path, contents in files:
|
||||||
|
expand_errors(contents.split('\n'), output, file_path)
|
||||||
|
|
||||||
|
case.input = input
|
||||||
|
case.output = output
|
||||||
|
case.output2 = output2
|
||||||
|
case.lastline = item.line
|
||||||
|
case.files = files
|
||||||
|
case.output_files = output_files
|
||||||
|
case.expected_stale_modules = stale_modules
|
||||||
|
case.expected_rechecked_modules = rechecked_modules
|
||||||
|
case.deleted_paths = deleted_paths
|
||||||
|
case.triggered = triggered or []
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoDataDrivenTestCase(DataDrivenTestCase):
|
||||||
|
def setup(self) -> None:
|
||||||
|
self.old_environ = os.environ.copy()
|
||||||
|
|
||||||
|
parse_test_case(case=self)
|
||||||
|
self.old_cwd = os.getcwd()
|
||||||
|
|
||||||
|
self.tmpdir = tempfile.TemporaryDirectory(prefix='mypy-test-')
|
||||||
|
|
||||||
|
os.chdir(self.tmpdir.name)
|
||||||
|
os.mkdir(test_temp_dir)
|
||||||
|
encountered_files = set()
|
||||||
|
self.clean_up = []
|
||||||
|
for paths in self.deleted_paths.values():
|
||||||
|
for path in paths:
|
||||||
|
self.clean_up.append((False, path))
|
||||||
|
encountered_files.add(path)
|
||||||
|
for path, content in self.files:
|
||||||
|
dir = os.path.dirname(path)
|
||||||
|
for d in self.add_dirs(dir):
|
||||||
|
self.clean_up.append((True, d))
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
if path not in encountered_files:
|
||||||
|
self.clean_up.append((False, path))
|
||||||
|
encountered_files.add(path)
|
||||||
|
if re.search(r'\.[2-9]$', path):
|
||||||
|
# Make sure new files introduced in the second and later runs are accounted for
|
||||||
|
renamed_path = path[:-2]
|
||||||
|
if renamed_path not in encountered_files:
|
||||||
|
encountered_files.add(renamed_path)
|
||||||
|
self.clean_up.append((False, renamed_path))
|
||||||
|
for path, _ in self.output_files:
|
||||||
|
# Create directories for expected output and mark them to be cleaned up at the end
|
||||||
|
# of the test case.
|
||||||
|
dir = os.path.dirname(path)
|
||||||
|
for d in self.add_dirs(dir):
|
||||||
|
self.clean_up.append((True, d))
|
||||||
|
self.clean_up.append((False, path))
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(self.tmpdir.name, 'tmp'))
|
||||||
|
|
||||||
|
def teardown(self):
|
||||||
|
if hasattr(self, 'old_environ'):
|
||||||
|
os.environ = self.old_environ
|
||||||
|
super().teardown()
|
||||||
|
|
||||||
|
|
||||||
|
def split_test_cases(parent: 'DataSuiteCollector', suite: 'DataSuite',
|
||||||
|
file: str) -> Iterator[DjangoDataDrivenTestCase]:
|
||||||
|
"""Iterate over raw test cases in file, at collection time, ignoring sub items.
|
||||||
|
|
||||||
|
The collection phase is slow, so any heavy processing should be deferred to after
|
||||||
|
uninteresting tests are filtered (when using -k PATTERN switch).
|
||||||
|
"""
|
||||||
|
with open(file, encoding='utf-8') as f:
|
||||||
|
data = f.read()
|
||||||
|
cases = re.split(r'^\[case ([a-zA-Z_0-9]+)'
|
||||||
|
r'(-writescache)?'
|
||||||
|
r'(-only_when_cache|-only_when_nocache)?'
|
||||||
|
r'(-skip)?'
|
||||||
|
r'\][ \t]*$\n', data,
|
||||||
|
flags=re.DOTALL | re.MULTILINE)
|
||||||
|
line_no = cases[0].count('\n') + 1
|
||||||
|
|
||||||
|
for i in range(1, len(cases), 5):
|
||||||
|
name, writescache, only_when, skip, data = cases[i:i + 5]
|
||||||
|
yield DjangoDataDrivenTestCase(parent, suite, file,
|
||||||
|
name=add_test_name_suffix(name, suite.test_name_suffix),
|
||||||
|
writescache=bool(writescache),
|
||||||
|
only_when=only_when,
|
||||||
|
skip=bool(skip),
|
||||||
|
data=data,
|
||||||
|
line=line_no)
|
||||||
|
line_no += data.count('\n') + 1
|
||||||
|
|
||||||
|
|
||||||
|
class DataSuiteCollector(pytest.Class): # type: ignore # inheriting from Any
|
||||||
|
def collect(self) -> Iterator[pytest.Item]: # type: ignore
|
||||||
|
"""Called by pytest on each of the object returned from pytest_pycollect_makeitem"""
|
||||||
|
|
||||||
|
# obj is the object for which pytest_pycollect_makeitem returned self.
|
||||||
|
suite = self.obj # type: DataSuite
|
||||||
|
for f in suite.files:
|
||||||
|
yield from split_test_cases(self, suite, os.path.join(suite.data_prefix, f))
|
||||||
|
|
||||||
|
|
||||||
|
# This function name is special to pytest. See
|
||||||
|
# http://doc.pytest.org/en/latest/writing_plugins.html#collection-hooks
|
||||||
|
def pytest_pycollect_makeitem(collector: Any, name: str,
|
||||||
|
obj: object) -> 'Optional[Any]':
|
||||||
|
"""Called by pytest on each object in modules configured in conftest.py files.
|
||||||
|
|
||||||
|
collector is pytest.Collector, returns Optional[pytest.Class]
|
||||||
|
"""
|
||||||
|
if isinstance(obj, type):
|
||||||
|
# Only classes derived from DataSuite contain test cases, not the DataSuite class itself
|
||||||
|
if issubclass(obj, DataSuite) and obj is not DataSuite:
|
||||||
|
# Non-None result means this obj is a test case.
|
||||||
|
# The collect method of the returned DataSuiteCollector instance will be called later,
|
||||||
|
# with self.obj being obj.
|
||||||
|
return DataSuiteCollector(name, parent=collector)
|
||||||
|
return None
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
[mypy]
|
[mypy]
|
||||||
plugins =
|
plugins =
|
||||||
mypy_django_plugin.plugin
|
mypy_django_plugin.main
|
||||||
|
|||||||
31
test/test-data/check-parse-settings.test
Normal file
31
test/test-data/check-parse-settings.test
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
[case testParseSettingsFromFile]
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
reveal_type(settings.ROOT_DIR) # E: Revealed type is 'builtins.str'
|
||||||
|
reveal_type(settings.OBJ) # E: Revealed type is 'django.utils.functional.LazyObject'
|
||||||
|
reveal_type(settings.NUMBERS) # E: Revealed type is 'builtins.list[Any]'
|
||||||
|
reveal_type(settings.DICT) # E: Revealed type is 'builtins.dict[Any, Any]'
|
||||||
|
|
||||||
|
[file mysettings.py]
|
||||||
|
SECRET_KEY = 112233
|
||||||
|
ROOT_DIR = '/etc'
|
||||||
|
NUMBERS = ['one', 'two']
|
||||||
|
DICT = {}
|
||||||
|
|
||||||
|
from django.utils.functional import LazyObject
|
||||||
|
OBJ = LazyObject()
|
||||||
|
|
||||||
|
[env DJANGO_SETTINGS_MODULE=mysettings]
|
||||||
|
[out]
|
||||||
|
|
||||||
|
[case testSettingIsInitializedToNone]
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
reveal_type(settings.NONE_SETTING) # E: Revealed type is 'builtins.object'
|
||||||
|
|
||||||
|
[file mysettings_not_none.py]
|
||||||
|
SECRET_KEY = 112233
|
||||||
|
NONE_SETTING = None
|
||||||
|
|
||||||
|
[env DJANGO_SETTINGS_MODULE=mysettings_not_none]
|
||||||
|
[out]
|
||||||
@@ -17,7 +17,8 @@ class DjangoTestSuite(DataSuite):
|
|||||||
'check-objects-queryset.test',
|
'check-objects-queryset.test',
|
||||||
'check-model-fields.test',
|
'check-model-fields.test',
|
||||||
'check-postgres-fields.test',
|
'check-postgres-fields.test',
|
||||||
'check-model-relations.test'
|
'check-model-relations.test',
|
||||||
|
'check-parse-settings.test'
|
||||||
]
|
]
|
||||||
data_prefix = str(TEST_DATA_DIR)
|
data_prefix = str(TEST_DATA_DIR)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user