mirror of
https://github.com/davidhalter/django-stubs.git
synced 2025-12-07 12:44:29 +08:00
fix star import parsing for settings
This commit is contained in:
@@ -33,7 +33,7 @@ from django.db.models.fields.reverse_related import (
|
|||||||
)
|
)
|
||||||
from django.db.models.query_utils import PathInfo, Q
|
from django.db.models.query_utils import PathInfo, Q
|
||||||
|
|
||||||
from django.db.models.expressions import F
|
from django.db.models.expressions import Combinable
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django.db.models.manager import RelatedManager
|
from django.db.models.manager import RelatedManager
|
||||||
@@ -105,11 +105,12 @@ class ForeignObject(RelatedField):
|
|||||||
|
|
||||||
class ForeignKey(RelatedField, Generic[_T]):
|
class ForeignKey(RelatedField, Generic[_T]):
|
||||||
def __init__(self, to: Union[Type[_T], str], on_delete: Any, related_name: str = ..., **kwargs): ...
|
def __init__(self, to: Union[Type[_T], str], on_delete: Any, related_name: str = ..., **kwargs): ...
|
||||||
def __set__(self, instance, value: Union[Model, F]) -> None: ...
|
def __set__(self, instance, value: Union[Model, Combinable]) -> None: ...
|
||||||
def __get__(self, instance, owner) -> _T: ...
|
def __get__(self, instance, owner) -> _T: ...
|
||||||
|
|
||||||
class OneToOneField(RelatedField, Generic[_T]):
|
class OneToOneField(RelatedField, Generic[_T]):
|
||||||
def __init__(self, to: Union[Type[_T], str], on_delete: Any, related_name: str = ..., **kwargs): ...
|
def __init__(self, to: Union[Type[_T], str], on_delete: Any, related_name: str = ..., **kwargs): ...
|
||||||
|
def __set__(self, instance, value: Union[Model, Combinable]) -> None: ...
|
||||||
def __get__(self, instance, owner) -> _T: ...
|
def __get__(self, instance, owner) -> _T: ...
|
||||||
|
|
||||||
class ManyToManyField(RelatedField, Generic[_T]):
|
class ManyToManyField(RelatedField, Generic[_T]):
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from typing import Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
@@ -15,7 +15,11 @@ class Config:
|
|||||||
ini_config.read(fpath)
|
ini_config.read(fpath)
|
||||||
if not ini_config.has_section('mypy_django_plugin'):
|
if not ini_config.has_section('mypy_django_plugin'):
|
||||||
raise ValueError('Invalid config file: no [mypy_django_plugin] section')
|
raise ValueError('Invalid config file: no [mypy_django_plugin] section')
|
||||||
return Config(django_settings_module=ini_config.get('mypy_django_plugin', 'django_settings',
|
|
||||||
fallback=None),
|
django_settings = ini_config.get('mypy_django_plugin', 'django_settings',
|
||||||
|
fallback=None)
|
||||||
|
if django_settings:
|
||||||
|
django_settings = django_settings.strip()
|
||||||
|
return Config(django_settings_module=django_settings,
|
||||||
ignore_missing_settings=ini_config.get('mypy_django_plugin', 'ignore_missing_settings',
|
ignore_missing_settings=ini_config.get('mypy_django_plugin', 'ignore_missing_settings',
|
||||||
fallback=False))
|
fallback=False))
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import os
|
|||||||
from typing import Callable, Dict, Optional, cast
|
from typing import Callable, Dict, Optional, cast
|
||||||
|
|
||||||
from mypy.checker import TypeChecker
|
from mypy.checker import TypeChecker
|
||||||
from mypy.nodes import TypeInfo
|
from mypy.nodes import MemberExpr, TypeInfo
|
||||||
from mypy.options import Options
|
from mypy.options import Options
|
||||||
from mypy.plugin import ClassDefContext, FunctionContext, MethodContext, Plugin, AttributeContext
|
from mypy.plugin import AttributeContext, ClassDefContext, FunctionContext, MethodContext, Plugin
|
||||||
from mypy.types import Instance, Type, TypeType, AnyType, TypeOfAny
|
from mypy.types import AnyType, Instance, Type, TypeOfAny, TypeType
|
||||||
|
|
||||||
from mypy_django_plugin import helpers, monkeypatch
|
from mypy_django_plugin import helpers, monkeypatch
|
||||||
from mypy_django_plugin.config import Config
|
from mypy_django_plugin.config import Config
|
||||||
from mypy_django_plugin.plugins import init_create
|
from mypy_django_plugin.plugins import init_create
|
||||||
@@ -14,7 +13,7 @@ from mypy_django_plugin.plugins.fields import determine_type_of_array_field, rec
|
|||||||
from mypy_django_plugin.plugins.migrations import determine_model_cls_from_string_for_migrations, get_string_value_from_expr
|
from mypy_django_plugin.plugins.migrations import determine_model_cls_from_string_for_migrations, get_string_value_from_expr
|
||||||
from mypy_django_plugin.plugins.models import process_model_class
|
from mypy_django_plugin.plugins.models import process_model_class
|
||||||
from mypy_django_plugin.plugins.related_fields import extract_to_parameter_as_get_ret_type_for_related_field, reparametrize_with
|
from mypy_django_plugin.plugins.related_fields import extract_to_parameter_as_get_ret_type_for_related_field, reparametrize_with
|
||||||
from mypy_django_plugin.plugins.settings import AddSettingValuesToDjangoConfObject
|
from mypy_django_plugin.plugins.settings import AddSettingValuesToDjangoConfObject, SettingContext, get_settings_metadata
|
||||||
|
|
||||||
|
|
||||||
def transform_model_class(ctx: ClassDefContext) -> None:
|
def transform_model_class(ctx: ClassDefContext) -> None:
|
||||||
@@ -106,6 +105,25 @@ def extract_and_return_primary_key_of_bound_related_field_parameter(ctx: Attribu
|
|||||||
return ctx.default_attr_type
|
return ctx.default_attr_type
|
||||||
|
|
||||||
|
|
||||||
|
class ExtractSettingType:
|
||||||
|
def __init__(self, module_fullname: str):
|
||||||
|
self.module_fullname = module_fullname
|
||||||
|
|
||||||
|
def __call__(self, ctx: AttributeContext) -> Type:
|
||||||
|
api = cast(TypeChecker, ctx.api)
|
||||||
|
original_module = api.modules.get(self.module_fullname)
|
||||||
|
if original_module is None:
|
||||||
|
return ctx.default_attr_type
|
||||||
|
|
||||||
|
definition = ctx.context
|
||||||
|
if isinstance(definition, MemberExpr):
|
||||||
|
sym = original_module.names.get(definition.name)
|
||||||
|
if sym and sym.type:
|
||||||
|
return sym.type
|
||||||
|
|
||||||
|
return ctx.default_attr_type
|
||||||
|
|
||||||
|
|
||||||
class DjangoPlugin(Plugin):
|
class DjangoPlugin(Plugin):
|
||||||
def __init__(self, options: Options) -> None:
|
def __init__(self, options: Options) -> None:
|
||||||
super().__init__(options)
|
super().__init__(options)
|
||||||
@@ -116,17 +134,17 @@ class DjangoPlugin(Plugin):
|
|||||||
config_fpath = os.environ.get('MYPY_DJANGO_CONFIG', 'mypy_django.ini')
|
config_fpath = os.environ.get('MYPY_DJANGO_CONFIG', 'mypy_django.ini')
|
||||||
if config_fpath and os.path.exists(config_fpath):
|
if config_fpath and os.path.exists(config_fpath):
|
||||||
self.config = Config.from_config_file(config_fpath)
|
self.config = Config.from_config_file(config_fpath)
|
||||||
self.django_settings = self.config.django_settings_module
|
self.django_settings_module = self.config.django_settings_module
|
||||||
else:
|
else:
|
||||||
self.config = Config()
|
self.config = Config()
|
||||||
self.django_settings = None
|
self.django_settings_module = None
|
||||||
|
|
||||||
if 'DJANGO_SETTINGS_MODULE' in os.environ:
|
if 'DJANGO_SETTINGS_MODULE' in os.environ:
|
||||||
self.django_settings = os.environ['DJANGO_SETTINGS_MODULE']
|
self.django_settings_module = os.environ['DJANGO_SETTINGS_MODULE']
|
||||||
|
|
||||||
settings_modules = ['django.conf.global_settings']
|
settings_modules = ['django.conf.global_settings']
|
||||||
if self.django_settings:
|
if self.django_settings_module:
|
||||||
settings_modules.append(self.django_settings)
|
settings_modules.append(self.django_settings_module)
|
||||||
|
|
||||||
monkeypatch.add_modules_as_a_source_seed_files(settings_modules)
|
monkeypatch.add_modules_as_a_source_seed_files(settings_modules)
|
||||||
monkeypatch.inject_modules_as_dependencies_for_django_conf_settings(settings_modules)
|
monkeypatch.inject_modules_as_dependencies_for_django_conf_settings(settings_modules)
|
||||||
@@ -197,8 +215,8 @@ class DjangoPlugin(Plugin):
|
|||||||
|
|
||||||
if fullname == helpers.DUMMY_SETTINGS_BASE_CLASS:
|
if fullname == helpers.DUMMY_SETTINGS_BASE_CLASS:
|
||||||
settings_modules = ['django.conf.global_settings']
|
settings_modules = ['django.conf.global_settings']
|
||||||
if self.django_settings:
|
if self.django_settings_module:
|
||||||
settings_modules.append(self.django_settings)
|
settings_modules.append(self.django_settings_module)
|
||||||
return AddSettingValuesToDjangoConfObject(settings_modules,
|
return AddSettingValuesToDjangoConfObject(settings_modules,
|
||||||
self.config.ignore_missing_settings)
|
self.config.ignore_missing_settings)
|
||||||
|
|
||||||
@@ -209,11 +227,14 @@ class DjangoPlugin(Plugin):
|
|||||||
|
|
||||||
def get_attribute_hook(self, fullname: str
|
def get_attribute_hook(self, fullname: str
|
||||||
) -> Optional[Callable[[AttributeContext], Type]]:
|
) -> Optional[Callable[[AttributeContext], Type]]:
|
||||||
# sym = self.lookup_fully_qualified(helpers.MODEL_CLASS_FULLNAME)
|
module, _, name = fullname.rpartition('.')
|
||||||
# if sym and isinstance(sym.node, TypeInfo):
|
sym = self.lookup_fully_qualified('django.conf.LazySettings')
|
||||||
# if fullname.rpartition('.')[-1] in helpers.get_related_field_primary_key_names(sym.node):
|
if sym and isinstance(sym.node, TypeInfo):
|
||||||
return extract_and_return_primary_key_of_bound_related_field_parameter
|
metadata = get_settings_metadata(sym.node)
|
||||||
|
if module == 'builtins.object' and name in metadata:
|
||||||
|
return ExtractSettingType(module_fullname=metadata[name])
|
||||||
|
|
||||||
|
return extract_and_return_primary_key_of_bound_related_field_parameter
|
||||||
|
|
||||||
|
|
||||||
def plugin(version):
|
def plugin(version):
|
||||||
|
|||||||
@@ -127,16 +127,17 @@ def extract_expected_types(ctx: FunctionContext, model: TypeInfo) -> Dict[str, T
|
|||||||
if name in {'_meta', 'pk'}:
|
if name in {'_meta', 'pk'}:
|
||||||
continue
|
continue
|
||||||
if isinstance(sym.node, Var):
|
if isinstance(sym.node, Var):
|
||||||
if isinstance(sym.node.type, Instance):
|
if sym.node.type is None or isinstance(sym.node.type, AnyType):
|
||||||
|
# types are not ready, fallback to Any
|
||||||
|
expected_types[name] = AnyType(TypeOfAny.from_unimported_type)
|
||||||
|
expected_types[name + '_id'] = AnyType(TypeOfAny.from_unimported_type)
|
||||||
|
|
||||||
|
elif isinstance(sym.node.type, Instance):
|
||||||
tp = sym.node.type
|
tp = sym.node.type
|
||||||
field_type = extract_field_setter_type(tp)
|
field_type = extract_field_setter_type(tp)
|
||||||
if field_type is None:
|
if field_type is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
choices_type_fullname = extract_choices_type(model, name)
|
|
||||||
if choices_type_fullname:
|
|
||||||
field_type = UnionType([field_type, ctx.api.named_generic_type(choices_type_fullname, [])])
|
|
||||||
|
|
||||||
if tp.type.fullname() in {helpers.FOREIGN_KEY_FULLNAME, helpers.ONETOONE_FIELD_FULLNAME}:
|
if tp.type.fullname() in {helpers.FOREIGN_KEY_FULLNAME, helpers.ONETOONE_FIELD_FULLNAME}:
|
||||||
ref_to_model = tp.args[0]
|
ref_to_model = tp.args[0]
|
||||||
primary_key_type = AnyType(TypeOfAny.special_form)
|
primary_key_type = AnyType(TypeOfAny.special_form)
|
||||||
@@ -145,9 +146,7 @@ def extract_expected_types(ctx: FunctionContext, model: TypeInfo) -> Dict[str, T
|
|||||||
if typ:
|
if typ:
|
||||||
primary_key_type = typ
|
primary_key_type = typ
|
||||||
expected_types[name + '_id'] = primary_key_type
|
expected_types[name + '_id'] = primary_key_type
|
||||||
|
|
||||||
if field_type:
|
if field_type:
|
||||||
expected_types[name] = field_type
|
expected_types[name] = field_type
|
||||||
elif isinstance(sym.node.type, AnyType):
|
|
||||||
expected_types[name] = sym.node.type
|
|
||||||
return expected_types
|
return expected_types
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from typing import List, Optional, cast
|
from typing import Iterable, List, Optional, cast
|
||||||
|
|
||||||
from mypy.nodes import ClassDef, Context, MypyFile, SymbolNode, SymbolTableNode, Var
|
from mypy.nodes import ClassDef, Context, ImportAll, MypyFile, SymbolNode, SymbolTableNode, TypeInfo, Var
|
||||||
from mypy.plugin import ClassDefContext
|
from mypy.plugin import ClassDefContext
|
||||||
from mypy.semanal import SemanticAnalyzerPass2
|
from mypy.semanal import SemanticAnalyzerPass2
|
||||||
from mypy.types import Instance, NoneTyp, Type, UnionType
|
from mypy.types import AnyType, Instance, NoneTyp, Type, TypeOfAny, UnionType
|
||||||
|
from mypy.util import correct_relative_import
|
||||||
|
|
||||||
|
|
||||||
def get_error_context(node: SymbolNode) -> Context:
|
def get_error_context(node: SymbolNode) -> Context:
|
||||||
@@ -36,14 +37,44 @@ def make_sym_copy_of_setting(sym: SymbolTableNode) -> Optional[SymbolTableNode]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def load_settings_from_module(settings_classdef: ClassDef, module: MypyFile) -> None:
|
def get_settings_metadata(lazy_settings_info: TypeInfo):
|
||||||
for name, sym in module.names.items():
|
return lazy_settings_info.metadata.setdefault('django', {}).setdefault('settings', {})
|
||||||
if name.isupper() and isinstance(sym.node, Var):
|
|
||||||
if sym.type is not None:
|
|
||||||
copied = make_sym_copy_of_setting(sym)
|
def load_settings_from_names(settings_classdef: ClassDef,
|
||||||
if copied is None:
|
modules: Iterable[MypyFile],
|
||||||
continue
|
api: SemanticAnalyzerPass2) -> None:
|
||||||
settings_classdef.info.names[name] = copied
|
settings_metadata = get_settings_metadata(settings_classdef.info)
|
||||||
|
|
||||||
|
for module in modules:
|
||||||
|
for name, sym in module.names.items():
|
||||||
|
if name.isupper() and isinstance(sym.node, Var):
|
||||||
|
if sym.type is not None:
|
||||||
|
copied = make_sym_copy_of_setting(sym)
|
||||||
|
if copied is None:
|
||||||
|
continue
|
||||||
|
settings_classdef.info.names[name] = copied
|
||||||
|
else:
|
||||||
|
var = Var(name, AnyType(TypeOfAny.unannotated))
|
||||||
|
var.info = api.named_type('__builtins__.object').type
|
||||||
|
settings_classdef.info.names[name] = SymbolTableNode(sym.kind, var)
|
||||||
|
|
||||||
|
settings_metadata[name] = module.fullname()
|
||||||
|
|
||||||
|
|
||||||
|
def get_import_star_modules(api: SemanticAnalyzerPass2, module: MypyFile) -> List[str]:
|
||||||
|
import_star_modules = []
|
||||||
|
for module_import in module.imports:
|
||||||
|
# relative import * are not resolved by mypy
|
||||||
|
if isinstance(module_import, ImportAll) and module_import.relative:
|
||||||
|
absolute_import_path, correct = correct_relative_import(module.fullname(), module_import.relative, module_import.id,
|
||||||
|
is_cur_package_init_file=False)
|
||||||
|
if not correct:
|
||||||
|
return []
|
||||||
|
for path in [absolute_import_path] + get_import_star_modules(api, module=api.modules.get(absolute_import_path)):
|
||||||
|
if path not in import_star_modules:
|
||||||
|
import_star_modules.append(path)
|
||||||
|
return import_star_modules
|
||||||
|
|
||||||
|
|
||||||
class AddSettingValuesToDjangoConfObject:
|
class AddSettingValuesToDjangoConfObject:
|
||||||
@@ -55,7 +86,9 @@ class AddSettingValuesToDjangoConfObject:
|
|||||||
api = cast(SemanticAnalyzerPass2, ctx.api)
|
api = cast(SemanticAnalyzerPass2, ctx.api)
|
||||||
for module_name in self.settings_modules:
|
for module_name in self.settings_modules:
|
||||||
module = api.modules[module_name]
|
module = api.modules[module_name]
|
||||||
load_settings_from_module(ctx.cls, module=module)
|
star_deps = [api.modules[star_dep]
|
||||||
|
for star_dep in reversed(get_import_star_modules(api, module))]
|
||||||
|
load_settings_from_names(ctx.cls, modules=star_deps + [module], api=api)
|
||||||
|
|
||||||
if self.ignore_missing_settings:
|
if self.ignore_missing_settings:
|
||||||
ctx.cls.info.fallback_to_any = True
|
ctx.cls.info.fallback_to_any = True
|
||||||
|
|||||||
@@ -2,13 +2,18 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
reveal_type(settings.ROOT_DIR) # E: Revealed type is 'builtins.str'
|
reveal_type(settings.ROOT_DIR) # E: Revealed type is 'builtins.str'
|
||||||
|
reveal_type(settings.APPS_DIR) # E: Revealed type is 'pathlib.Path'
|
||||||
reveal_type(settings.OBJ) # E: Revealed type is 'django.utils.functional.LazyObject'
|
reveal_type(settings.OBJ) # E: Revealed type is 'django.utils.functional.LazyObject'
|
||||||
reveal_type(settings.NUMBERS) # E: Revealed type is 'builtins.list[builtins.str]'
|
reveal_type(settings.NUMBERS) # E: Revealed type is 'builtins.list[builtins.str]'
|
||||||
reveal_type(settings.DICT) # E: Revealed type is 'builtins.dict[Any, Any]'
|
reveal_type(settings.DICT) # E: Revealed type is 'builtins.dict[Any, Any]'
|
||||||
[env DJANGO_SETTINGS_MODULE=mysettings]
|
[env DJANGO_SETTINGS_MODULE=mysettings]
|
||||||
[file mysettings.py]
|
[file base.py]
|
||||||
SECRET_KEY = 112233
|
from pathlib import Path
|
||||||
ROOT_DIR = '/etc'
|
ROOT_DIR = '/etc'
|
||||||
|
APPS_DIR = Path(ROOT_DIR)
|
||||||
|
[file mysettings.py]
|
||||||
|
from base import *
|
||||||
|
SECRET_KEY = 112233
|
||||||
NUMBERS = ['one', 'two']
|
NUMBERS = ['one', 'two']
|
||||||
DICT = {} # type: ignore
|
DICT = {} # type: ignore
|
||||||
from django.utils.functional import LazyObject
|
from django.utils.functional import LazyObject
|
||||||
|
|||||||
Reference in New Issue
Block a user