Skip to content
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

Make procrastinate.contrib.django.app a proxy #959

Merged
merged 1 commit into from
Mar 2, 2024
Merged
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
13 changes: 1 addition & 12 deletions procrastinate/contrib/django/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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)
Loading