rework settings, add loading of the django.conf.global_settings, cleanups

This commit is contained in:
Maxim Kurnikov
2019-02-07 19:13:39 +03:00
parent faee26703e
commit d4cb729c93
11 changed files with 180 additions and 361 deletions

View File

@@ -1,18 +1,18 @@
import os
from typing import Callable, Optional, cast, Dict
from typing import Callable, Dict, Optional, cast
from mypy.checker import TypeChecker
from mypy.nodes import TypeInfo
from mypy.options import Options
from mypy.plugin import Plugin, FunctionContext, ClassDefContext, MethodContext
from mypy.types import Type, Instance
from mypy.plugin import ClassDefContext, FunctionContext, MethodContext, Plugin
from mypy.types import Instance, Type
from mypy_django_plugin import helpers, monkeypatch
from mypy_django_plugin.plugins.fields import determine_type_of_array_field
from mypy_django_plugin.plugins.migrations import determine_model_cls_from_string_for_migrations
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.settings import DjangoConfSettingsInitializerHook
from mypy_django_plugin.plugins.settings import AddSettingValuesToDjangoConfObject
def transform_model_class(ctx: ClassDefContext) -> None:
@@ -55,19 +55,21 @@ def determine_proper_manager_type(ctx: FunctionContext) -> Type:
class DjangoPlugin(Plugin):
def __init__(self,
options: Options) -> None:
def __init__(self, options: Options) -> None:
super().__init__(options)
monkeypatch.restore_original_load_graph()
monkeypatch.restore_original_dependencies_handling()
settings_modules = ['django.conf.global_settings']
self.django_settings = os.environ.get('DJANGO_SETTINGS_MODULE')
if self.django_settings:
monkeypatch.load_graph_to_add_settings_file_as_a_source_seed(self.django_settings)
monkeypatch.inject_dependencies(self.django_settings)
else:
monkeypatch.restore_original_load_graph()
monkeypatch.restore_original_dependencies_handling()
settings_modules.append(self.django_settings)
def get_current_model_bases(self) -> Dict[str, int]:
monkeypatch.add_modules_as_a_source_seed_files(settings_modules)
monkeypatch.inject_modules_as_dependencies_for_django_conf_settings(settings_modules)
def _get_current_model_bases(self) -> Dict[str, int]:
model_sym = self.lookup_fully_qualified(helpers.MODEL_CLASS_FULLNAME)
if model_sym is not None and isinstance(model_sym.node, TypeInfo):
if 'django' not in model_sym.node.metadata:
@@ -78,7 +80,7 @@ class DjangoPlugin(Plugin):
else:
return {}
def get_current_manager_bases(self) -> Dict[str, int]:
def _get_current_manager_bases(self) -> Dict[str, int]:
manager_sym = self.lookup_fully_qualified(helpers.MANAGER_CLASS_FULLNAME)
if manager_sym is not None and isinstance(manager_sym.node, TypeInfo):
if 'django' not in manager_sym.node.metadata:
@@ -99,7 +101,7 @@ class DjangoPlugin(Plugin):
if fullname == 'django.contrib.postgres.fields.array.ArrayField':
return determine_type_of_array_field
manager_bases = self.get_current_manager_bases()
manager_bases = self._get_current_manager_bases()
if fullname in manager_bases:
return determine_proper_manager_type
@@ -112,13 +114,16 @@ class DjangoPlugin(Plugin):
def get_base_class_hook(self, fullname: str
) -> Optional[Callable[[ClassDefContext], None]]:
if fullname in self.get_current_model_bases():
if fullname in self._get_current_model_bases():
return transform_model_class
if fullname == helpers.DUMMY_SETTINGS_BASE_CLASS:
return DjangoConfSettingsInitializerHook(settings_module=self.django_settings)
settings_modules = ['django.conf.global_settings']
if self.django_settings:
settings_modules.append(self.django_settings)
return AddSettingValuesToDjangoConfObject(settings_modules)
if fullname in self.get_current_manager_bases():
if fullname in self._get_current_manager_bases():
return transform_manager_class
return None

View File

@@ -1,5 +1,4 @@
from .dependencies import (load_graph_to_add_settings_file_as_a_source_seed,
inject_dependencies,
from .dependencies import (add_modules_as_a_source_seed_files,
inject_modules_as_dependencies_for_django_conf_settings,
restore_original_load_graph,
restore_original_dependencies_handling,
process_settings_before_dependants)
restore_original_dependencies_handling)

View File

@@ -1,26 +1,25 @@
from typing import List, Optional, AbstractSet, MutableSet, Set
from typing import List, Optional
from mypy.build import BuildManager, Graph, State, PRI_ALL
from mypy import build
from mypy.build import BuildManager, Graph, State
from mypy.modulefinder import BuildSource
old_load_graph = build.load_graph
OldState = build.State
def is_module_present_in_sources(module_name: str, sources: List[BuildSource]):
return any([source.module == module_name for source in sources])
from mypy import build
old_load_graph = build.load_graph
OldState = build.State
old_sorted_components = build.sorted_components
def load_graph_to_add_settings_file_as_a_source_seed(settings_module: str):
def add_modules_as_a_source_seed_files(modules: List[str]) -> None:
def patched_load_graph(sources: List[BuildSource], manager: BuildManager,
old_graph: Optional[Graph] = None,
new_modules: Optional[List[State]] = None):
if not is_module_present_in_sources(settings_module, sources):
sources.append(BuildSource(None, settings_module, None))
# add global settings
for module_name in modules:
if not is_module_present_in_sources(module_name, sources):
sources.append(BuildSource(None, module_name, None))
return old_load_graph(sources=sources, manager=manager,
old_graph=old_graph,
@@ -35,14 +34,14 @@ def restore_original_load_graph():
build.load_graph = old_load_graph
def inject_dependencies(settings_module: str):
def inject_modules_as_dependencies_for_django_conf_settings(modules: List[str]) -> None:
from mypy import build
class PatchedState(build.State):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.id == 'django.conf':
self.dependencies.append(settings_module)
self.dependencies.extend(modules)
build.State = PatchedState
@@ -51,40 +50,3 @@ def restore_original_dependencies_handling():
from mypy import build
build.State = OldState
def _extract_dependencies(graph: Graph, state_id: str, visited_modules: Set[str]) -> Set[str]:
visited_modules.add(state_id)
dependencies = set(graph[state_id].dependencies)
for new_dep_id in dependencies.copy():
if new_dep_id not in visited_modules:
dependencies.update(_extract_dependencies(graph, new_dep_id, visited_modules))
return dependencies
def extract_module_dependencies(graph: Graph, state_id: str) -> Set[str]:
visited_modules = set()
return _extract_dependencies(graph, state_id, visited_modules=visited_modules)
def process_settings_before_dependants(settings_module: str):
def patched_sorted_components(graph: Graph,
vertices: Optional[AbstractSet[str]] = None,
pri_max: int = PRI_ALL) -> List[AbstractSet[str]]:
sccs = old_sorted_components(graph,
vertices=vertices,
pri_max=pri_max)
for i, scc in enumerate(sccs.copy()):
if 'django.conf' in scc:
django_conf_deps = set(extract_module_dependencies(graph, 'django.conf')).union({'django.conf'})
old_scc_modified = scc.difference(django_conf_deps)
new_scc = scc.difference(old_scc_modified)
if not old_scc_modified:
# already processed
break
sccs[i] = frozenset(old_scc_modified)
sccs.insert(i, frozenset(new_scc))
break
return sccs
build.sorted_components = patched_sorted_components

View File

@@ -1,9 +1,9 @@
import dataclasses
from abc import abstractmethod, ABCMeta
from typing import cast, Iterator, Tuple, Optional, Dict
from abc import ABCMeta, abstractmethod
from typing import Dict, Iterator, Optional, Tuple, cast
from mypy.nodes import ClassDef, AssignmentStmt, CallExpr, MemberExpr, StrExpr, NameExpr, MDEF, TypeInfo, Var, SymbolTableNode, \
Lvalue, Expression, MypyFile, Context
import dataclasses
from mypy.nodes import AssignmentStmt, CallExpr, ClassDef, Context, Expression, Lvalue, MDEF, MemberExpr, MypyFile, NameExpr, \
StrExpr, SymbolTableNode, TypeInfo, Var
from mypy.plugin import ClassDefContext
from mypy.semanal import SemanticAnalyzerPass2
from mypy.types import Instance
@@ -45,6 +45,7 @@ class ModelClassInitializer(metaclass=ABCMeta):
var._fullname = self.model_classdef.info.fullname() + '.' + name
var.is_inferred = True
var.is_initialized_in_class = True
var.is_classvar = True
self.model_classdef.info.names[name] = SymbolTableNode(MDEF, var)
@abstractmethod

View File

@@ -1,9 +1,9 @@
from typing import cast, List, Optional
from typing import List, Optional, cast
from mypy.nodes import Var, Context, SymbolNode, SymbolTableNode
from mypy.nodes import ClassDef, Context, MypyFile, SymbolNode, SymbolTableNode, Var
from mypy.plugin import ClassDefContext
from mypy.semanal import SemanticAnalyzerPass2
from mypy.types import Instance, UnionType, NoneTyp, Type
from mypy.types import Instance, NoneTyp, Type, UnionType
def get_error_context(node: SymbolNode) -> Context:
@@ -36,39 +36,22 @@ def make_sym_copy_of_setting(sym: SymbolTableNode) -> Optional[SymbolTableNode]:
return None
def add_settings_to_django_conf_object(ctx: ClassDefContext,
settings_module: str) -> None:
api = cast(SemanticAnalyzerPass2, ctx.api)
if settings_module not in api.modules:
return None
settings_file = api.modules[settings_module]
for name, sym in settings_file.names.items():
def load_settings_from_module(settings_classdef: ClassDef, module: MypyFile) -> None:
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
ctx.cls.info.names[name] = copied
# else:
# TODO: figure out suggestion to add type annotation
# context = Context()
# module, node_name = sym.node.fullname().rsplit('.', 1)
# module_file = api.modules.get(module)
# if module_file is None:
# return None
# context.set_line(sym.node)
# api.msg.report(f"Need type annotation for '{sym.node.name()}'", context,
# severity='error', file=module_file.path)
ctx.cls.info.fallback_to_any = True
settings_classdef.info.names[name] = copied
class DjangoConfSettingsInitializerHook(object):
def __init__(self, settings_module: Optional[str]):
self.settings_module = settings_module
class AddSettingValuesToDjangoConfObject:
def __init__(self, settings_modules: List[str]):
self.settings_modules = settings_modules
def __call__(self, ctx: ClassDefContext) -> None:
if not self.settings_module:
return
add_settings_to_django_conf_object(ctx, self.settings_module)
api = cast(SemanticAnalyzerPass2, ctx.api)
for module_name in self.settings_modules:
module = api.modules[module_name]
load_settings_from_module(ctx.cls, module=module)