Skip to content

Skip executing apps.ready for installed apps #1341

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions mypy_django_plugin/django/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import sys
from collections import defaultdict
from contextlib import contextmanager
from typing import TYPE_CHECKING, Any, Dict, Iterable, Iterator, Optional, Set, Tuple, Type, Union
from typing import TYPE_CHECKING, Any, Dict, Generator, Iterable, Iterator, Optional, Set, Tuple, Type, Union
from unittest import mock

from django.core.exceptions import FieldError
from django.db import models
Expand Down Expand Up @@ -32,6 +33,7 @@ class ArrayField: # type: ignore


if TYPE_CHECKING:
from django.apps.config import AppConfig
from django.apps.registry import Apps # noqa: F401
from django.conf import LazySettings # noqa: F401
from django.contrib.contenttypes.fields import GenericForeignKey
Expand All @@ -48,6 +50,42 @@ def temp_environ() -> Iterator[None]:
os.environ.update(environ)


class AppConfigs(Dict[str, "AppConfig"]):
"""
A mapping for 'AppConfig' that monkey patches 'ready' method on insert
"""

def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.patches: Dict[str, mock._patch[mock.MagicMock]] = {}

def __setitem__(self, key: str, value: "AppConfig") -> None:
self.patches[key] = mock.patch.object(value, "ready", autospec=True)
super().__setitem__(key, value)
self.patches[key].start()


@contextmanager
def skip_apps_ready(apps: "Apps") -> Generator[None, None, None]:
"""
Context manager that monkey patches the apps registry's container for app configs so
that we avoid executing custom 'ready' methods. This both saves time but more
importantly avoids executing custom runtime code for arbitrary apps. There should be
no need for django-stubs plugin to have triggered 'ready' in order to type check
correctly.
"""
if apps.ready:
# Don't overwrite a readied apps instance. Calling '.ready' will be a noop.
yield None
else:
apps.app_configs = AppConfigs()
try:
yield None
finally:
for patch in apps.app_configs.patches.values():
patch.stop()


def initialize_django(settings_module: str) -> Tuple["Apps", "LazySettings"]:
with temp_environ():
os.environ["DJANGO_SETTINGS_MODULE"] = settings_module
Expand All @@ -64,7 +102,8 @@ def initialize_django(settings_module: str) -> Tuple["Apps", "LazySettings"]:
if not settings.configured:
settings._setup() # type: ignore

apps.populate(settings.INSTALLED_APPS)
with skip_apps_ready(apps):
apps.populate(settings.INSTALLED_APPS)

assert apps.apps_ready, "Apps are not ready"
assert settings.configured, "Settings are not configured"
Expand Down
17 changes: 17 additions & 0 deletions tests/typecheck/test_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,20 @@
content: |
def extra_fn() -> None:
pass

- case: test_skips_executing_custom_app_config_ready
main:
installed_apps:
- myapp
files:
- path: myapp/__init__.py
- path: myapp/apps.py
content: |
from typing import NoReturn
from django.apps.config import AppConfig
class MyAppConfig(AppConfig):
name = "myapp"
default = True

def ready(self) -> NoReturn:
raise Exception("Don't execute ready")