Skip to content

Commit

Permalink
Merge pull request #959 from procrastinate-org/django-app-proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
ewjoachim committed Mar 2, 2024
2 parents 62a5cf2 + b6035f1 commit fa5ee02
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 22 deletions.
13 changes: 1 addition & 12 deletions procrastinate/contrib/django/__init__.py
@@ -1,10 +1,6 @@
from __future__ import annotations

from typing import cast

from procrastinate import app as app_module

from .placeholder import FutureApp
from .procrastinate_app import app
from .router import ProcrastinateReadOnlyRouter
from .utils import connector_params

Expand All @@ -14,10 +10,3 @@
"ProcrastinateReadOnlyRouter",
]
default_app_config = "procrastinate.contrib.django.apps.ProcrastinateConfig"

# Before the Django app is ready, we're defining the app as a blueprint so
# that tasks can be registered. The real app will be initialized in the
# ProcrastinateConfig.ready() method.
# This blueprint has special implementations for App method so that if
# users try to use the app before it's ready, they get a helpful error message.
app: app_module.App = cast(app_module.App, FutureApp())
10 changes: 5 additions & 5 deletions procrastinate/contrib/django/apps.py
Expand Up @@ -6,10 +6,8 @@
from django.utils import module_loading

import procrastinate
from procrastinate.contrib import django as contrib_django
from procrastinate.contrib.django import migrations_magic, utils

from . import django_connector
from . import django_connector, migrations_magic, procrastinate_app, utils


class ProcrastinateConfig(apps.AppConfig):
Expand All @@ -18,11 +16,13 @@ class ProcrastinateConfig(apps.AppConfig):

def ready(self) -> None:
migrations_magic.load()
contrib_django.app = create_app(blueprint=contrib_django.app)
procrastinate_app._current_app = create_app(
blueprint=procrastinate_app._current_app
)

@property
def app(self) -> procrastinate.App:
return contrib_django.app
return procrastinate_app.app


def get_import_paths() -> Iterable[str]:
Expand Down
72 changes: 72 additions & 0 deletions procrastinate/contrib/django/procrastinate_app.py
@@ -0,0 +1,72 @@
from __future__ import annotations

import functools
from typing import NoReturn, cast

from procrastinate import app as app_module
from procrastinate import blueprints

from . import exceptions


def _not_ready(_method: str, *args, **kwargs) -> NoReturn:
base_text = (
f"Cannot call procrastinate.contrib.app.{_method}() before "
"the 'procrastinate.contrib.django' django app is ready."
)
details = (
"If this message appears at import time, the app is not ready yet: "
"move the corresponding code in an app's `AppConfig.ready()` method. "
"If this message appears in an app's `AppConfig.ready()` method, "
'make sure `"procrastinate.contrib.django"` appears before '
"that app when ordering apps in the Django setting `INSTALLED_APPS`. "
"Alternatively, use the Django setting "
"PROCRASTINATE_ON_APP_READY (see the doc)."
)
raise exceptions.DjangoNotReady(base_text + "\n\n" + details)


class FutureApp(blueprints.Blueprint):
_shadowed_methods = frozenset(
[
"__enter__",
"__exit__",
"_register_builtin_tasks",
"_worker",
"check_connection_async",
"check_connection",
"close_async",
"close",
"configure_task",
"open_async",
"open",
"perform_import_paths",
"run_worker_async",
"run_worker",
"schema_manager",
"with_connector",
"will_configure_task",
]
)
for method in _shadowed_methods:
locals()[method] = functools.partial(_not_ready, method)


class ProxyApp:
def __repr__(self) -> str:
return repr(_current_app)

def __getattr__(self, name):
return getattr(_current_app, name)


# Users may import the app before it's ready, so we're defining a proxy
# that references either the pre-app or the real app.
app: app_module.App = cast(app_module.App, ProxyApp())

# Before the Django app is ready, we're defining the app as a blueprint so
# that tasks can be registered. The real app will be initialized in the
# ProcrastinateConfig.ready() method.
# This blueprint has special implementations for App methods so that if
# users try to use the app before it's ready, they get a helpful error message.
_current_app: app_module.App = cast(app_module.App, FutureApp())
10 changes: 5 additions & 5 deletions tests/unit/contrib/django/test_placeholder.py
Expand Up @@ -3,7 +3,7 @@
import pytest

from procrastinate import app, blueprints
from procrastinate.contrib.django import exceptions, placeholder
from procrastinate.contrib.django import exceptions, procrastinate_app


def test__not_ready():
Expand All @@ -15,16 +15,16 @@ def test__not_ready():
exceptions.DjangoNotReady,
match=message,
):
placeholder._not_ready("foo")
procrastinate_app._not_ready("foo")


def test_FutureApp__not_ready():
with pytest.raises(exceptions.DjangoNotReady):
placeholder.FutureApp().open()
procrastinate_app.FutureApp().open()


def test_FutureApp__defer():
app = placeholder.FutureApp()
app = procrastinate_app.FutureApp()

@app.task
def foo():
Expand All @@ -38,4 +38,4 @@ def test_FutureApp__shadowed_methods():
ignored = {"from_path"}
added = {"will_configure_task"}
app_methods = set(dir(app.App)) - set(dir(blueprints.Blueprint)) - ignored | added
assert sorted(placeholder.FutureApp._shadowed_methods) == sorted(app_methods)
assert sorted(procrastinate_app.FutureApp._shadowed_methods) == sorted(app_methods)

0 comments on commit fa5ee02

Please sign in to comment.