Skip to content

Commit f3e9bb3

Browse files
authored
Refactored worker intial content code (#98)
* Fixed paths in .github/workflows/docker.yml * Refactored worker intial content code * Added unit tests * Added new fixture: default_worker_app_module()
1 parent 3593a02 commit f3e9bb3

File tree

10 files changed

+383
-107
lines changed

10 files changed

+383
-107
lines changed

.github/workflows/docker.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ on:
44
pull_request:
55
branches: [ 'main']
66
paths:
7-
- '/src/pytest_celery/vendors/worker/**'
7+
- 'src/pytest_celery/vendors/worker/**'
88
- '.github/workflows/docker.yml'
99
- 'Dockerfile'
1010
push:
1111
branches: [ 'main']
1212
paths:
13-
- '/src/pytest_celery/vendors/worker/**'
13+
- 'src/pytest_celery/vendors/worker/**'
1414
- '.github/workflows/docker.yml'
1515
- 'Dockerfile'
1616

src/pytest_celery/vendors/worker/app.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
1+
""" Template for Celery worker application. """
2+
13
import json
24
import logging
35
import sys
46

57
from celery import Celery
68
from celery.signals import after_setup_logger
79

8-
config_updates = None
9-
name = "celery_test_app" # Default name if not provided by the initial content
10-
11-
# Will be populated accoring to the initial content
12-
{0}
13-
{1}
14-
app = Celery(name)
10+
imports = None
1511

16-
{2}
12+
app = Celery("celery_test_app")
13+
config = None
1714

18-
if config_updates:
19-
app.config_from_object(config_updates)
20-
print(f"Config updates from default_worker_app fixture: {json.dumps(config_updates, indent=4)}")
15+
if config:
16+
app.config_from_object(config)
17+
print(f"Changed worker configuration: {json.dumps(config, indent=4)}")
2118

2219

2320
@after_setup_logger.connect
Lines changed: 24 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
from __future__ import annotations
22

3-
import inspect
3+
from types import ModuleType
44

55
from celery import Celery
6-
from celery.app.base import PendingConfiguration
76

87
from pytest_celery.api.container import CeleryTestContainer
98
from pytest_celery.vendors.worker.defaults import DEFAULT_WORKER_ENV
109
from pytest_celery.vendors.worker.defaults import DEFAULT_WORKER_LOG_LEVEL
1110
from pytest_celery.vendors.worker.defaults import DEFAULT_WORKER_NAME
1211
from pytest_celery.vendors.worker.defaults import DEFAULT_WORKER_QUEUE
1312
from pytest_celery.vendors.worker.defaults import DEFAULT_WORKER_VERSION
13+
from pytest_celery.vendors.worker.volume import WorkerInitialContent
1414

1515

1616
class CeleryWorkerContainer(CeleryTestContainer):
@@ -37,9 +37,17 @@ def worker_name(cls) -> str:
3737
def worker_queue(cls) -> str:
3838
return DEFAULT_WORKER_QUEUE
3939

40+
@classmethod
41+
def app_module(cls) -> ModuleType:
42+
from pytest_celery.vendors.worker import app
43+
44+
return app
45+
4046
@classmethod
4147
def tasks_modules(cls) -> set:
42-
return set()
48+
from pytest_celery.vendors.worker import tasks
49+
50+
return {tasks}
4351

4452
@classmethod
4553
def signals_modules(cls) -> set:
@@ -76,96 +84,24 @@ def env(cls, celery_worker_cluster_config: dict, initial: dict | None = None) ->
7684
@classmethod
7785
def initial_content(
7886
cls,
79-
worker_tasks: set,
87+
worker_tasks: set | None = None,
8088
worker_signals: set | None = None,
8189
worker_app: Celery | None = None,
90+
app_module: ModuleType | None = None,
8291
) -> dict:
83-
from pytest_celery.vendors.worker import app as app_module
92+
if app_module is None:
93+
app_module = cls.app_module()
8494

85-
app_module_src = inspect.getsource(app_module)
95+
if worker_tasks is None:
96+
worker_tasks = cls.tasks_modules()
8697

87-
imports = dict()
88-
initial_content = cls._initial_content_worker_tasks(worker_tasks)
89-
imports["tasks_imports"] = initial_content.pop("tasks_imports")
98+
content = WorkerInitialContent()
99+
content.set_app_module(app_module)
100+
content.add_modules("tasks", worker_tasks)
90101
if worker_signals:
91-
initial_content.update(cls._initial_content_worker_signals(worker_signals))
92-
imports["signals_imports"] = initial_content.pop("signals_imports")
102+
content.add_modules("signals", worker_signals)
93103
if worker_app:
94-
# Accessing the worker_app.conf.changes.data property will trigger the PendingConfiguration to be resolved
95-
# and the changes will be applied to the worker_app.conf, so we make a clone app to avoid affecting the
96-
# original app object.
97-
app = Celery(worker_app.main)
98-
app.conf = worker_app.conf
99-
config_changes_from_defaults = app.conf.changes.copy()
100-
if isinstance(config_changes_from_defaults, PendingConfiguration):
101-
config_changes_from_defaults = config_changes_from_defaults.data.changes
102-
if not isinstance(config_changes_from_defaults, dict):
103-
raise TypeError(f"Unexpected type for config_changes: {type(config_changes_from_defaults)}")
104-
del config_changes_from_defaults["deprecated_settings"]
105-
106-
name_code = f'name = "{worker_app.main}"'
107-
else:
108-
config_changes_from_defaults = {}
109-
name_code = f'name = "{cls.worker_name()}"'
110-
111-
imports_format = "{%s}" % "}{".join(imports.keys())
112-
imports_format = imports_format.format(**imports)
113-
app_module_src = app_module_src.replace("{0}", imports_format)
114-
115-
app_module_src = app_module_src.replace("{1}", name_code)
116-
117-
config_items = (f" {repr(key)}: {repr(value)}" for key, value in config_changes_from_defaults.items())
118-
config_code = (
119-
"config_updates = {\n" + ",\n".join(config_items) + "\n}"
120-
if config_changes_from_defaults
121-
else "config_updates = {}"
122-
)
123-
app_module_src = app_module_src.replace("{2}", config_code)
124-
125-
initial_content["app.py"] = app_module_src.encode()
126-
return initial_content
127-
128-
@classmethod
129-
def _initial_content_worker_tasks(cls, worker_tasks: set) -> dict:
130-
from pytest_celery.vendors.worker import tasks
131-
132-
worker_tasks.add(tasks)
104+
content.set_app_name(worker_app.main)
105+
content.set_config_from_object(worker_app)
133106

134-
import_string = ""
135-
136-
for module in worker_tasks:
137-
import_string += f"from {module.__name__} import *\n"
138-
139-
initial_content = {
140-
"__init__.py": b"",
141-
"tasks_imports": import_string,
142-
}
143-
if worker_tasks:
144-
default_worker_tasks_src = {
145-
f"{module.__name__.replace('.', '/')}.py": inspect.getsource(module).encode() for module in worker_tasks
146-
}
147-
initial_content.update(default_worker_tasks_src)
148-
else:
149-
print("No tasks found")
150-
return initial_content
151-
152-
@classmethod
153-
def _initial_content_worker_signals(cls, worker_signals: set) -> dict:
154-
import_string = ""
155-
156-
for module in worker_signals:
157-
import_string += f"from {module.__name__} import *\n"
158-
159-
initial_content = {
160-
"__init__.py": b"",
161-
"signals_imports": import_string,
162-
}
163-
if worker_signals:
164-
default_worker_signals_src = {
165-
f"{module.__name__.replace('.', '/')}.py": inspect.getsource(module).encode()
166-
for module in worker_signals
167-
}
168-
initial_content.update(default_worker_signals_src)
169-
else:
170-
print("No signals found")
171-
return initial_content
107+
return content.generate()

src/pytest_celery/vendors/worker/fixtures.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
from types import ModuleType
6+
57
import pytest
68
from celery import Celery
79
from pytest_docker_tools import build
@@ -101,17 +103,24 @@ def default_worker_env(
101103
@pytest.fixture
102104
def default_worker_initial_content(
103105
default_worker_container_cls: type[CeleryWorkerContainer],
106+
default_worker_app_module: ModuleType,
104107
default_worker_tasks: set,
105108
default_worker_signals: set,
106109
default_worker_app: Celery,
107110
) -> dict:
108111
yield default_worker_container_cls.initial_content(
112+
app_module=default_worker_app_module,
109113
worker_tasks=default_worker_tasks,
110114
worker_signals=default_worker_signals,
111115
worker_app=default_worker_app,
112116
)
113117

114118

119+
@pytest.fixture
120+
def default_worker_app_module(default_worker_container_cls: type[CeleryWorkerContainer]) -> ModuleType:
121+
yield default_worker_container_cls.app_module()
122+
123+
115124
@pytest.fixture
116125
def default_worker_tasks(default_worker_container_cls: type[CeleryWorkerContainer]) -> set:
117126
yield default_worker_container_cls.tasks_modules()
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
from __future__ import annotations
2+
3+
import inspect
4+
from types import ModuleType
5+
from typing import Any
6+
7+
from celery import Celery
8+
from celery.app.base import PendingConfiguration
9+
10+
from pytest_celery.vendors.worker.defaults import DEFAULT_WORKER_APP_NAME
11+
12+
13+
class WorkerInitialContent:
14+
class Parser:
15+
def imports_str(self, modules: set[ModuleType]) -> str:
16+
return "".join(f"from {module.__name__} import *\n" for module in modules)
17+
18+
def imports_src(self, modules: set[ModuleType]) -> dict:
19+
src = dict()
20+
for module in modules:
21+
src[f"{module.__name__.replace('.', '/')}.py"] = inspect.getsource(module).encode()
22+
return src
23+
24+
def app_name(self, name: str | None = None) -> str:
25+
name = name or DEFAULT_WORKER_APP_NAME
26+
return f"app = Celery('{name}')"
27+
28+
def config(self, app: Celery | None = None) -> str:
29+
app = app or Celery(DEFAULT_WORKER_APP_NAME)
30+
31+
# Accessing the app.conf.changes.data property will trigger the PendingConfiguration to be resolved
32+
# and the changes will be applied to the app.conf, so we make a clone app to avoid affecting the
33+
# original app object.
34+
tmp_app = Celery(app.main)
35+
tmp_app.conf = app.conf
36+
37+
changes = tmp_app.conf.changes.copy()
38+
if isinstance(changes, PendingConfiguration):
39+
changes = changes.data.changes
40+
if not isinstance(changes, dict):
41+
raise TypeError(f"Unexpected type for app.conf.changes: {type(changes)}")
42+
del changes["deprecated_settings"]
43+
44+
if changes:
45+
changes = (f"\t{repr(key)}: {repr(value)}" for key, value in changes.items())
46+
config = "config = {\n" + ",\n".join(changes) + "\n}" if changes else "config = None"
47+
else:
48+
config = "config = None"
49+
return config
50+
51+
def __init__(self, app_module: ModuleType | None = None) -> None:
52+
self.parser = self.Parser()
53+
self._initial_content = {
54+
"__init__.py": b"",
55+
"imports": dict(),
56+
}
57+
self.set_app_module(app_module)
58+
self.set_app_name()
59+
self.set_config_from_object()
60+
61+
def __eq__(self, __value: object) -> bool:
62+
if not isinstance(__value, WorkerInitialContent):
63+
return False
64+
try:
65+
return self.generate() == __value.generate()
66+
except ValueError:
67+
return all(
68+
[
69+
self._app_module_src == __value._app_module_src,
70+
self._initial_content == __value._initial_content,
71+
self._app == __value._app,
72+
self._config == __value._config,
73+
]
74+
)
75+
76+
def set_app_module(self, app_module: ModuleType | None = None) -> None:
77+
self._app_module_src: str | None
78+
79+
if app_module:
80+
self._app_module_src = inspect.getsource(app_module)
81+
else:
82+
self._app_module_src = None
83+
84+
def add_modules(self, name: str, modules: set[ModuleType]) -> None:
85+
if not name:
86+
raise ValueError("name cannot be empty")
87+
88+
if not modules:
89+
raise ValueError("modules cannot be empty")
90+
91+
self._initial_content["imports"][name] = self.parser.imports_str(modules) # type: ignore
92+
self._initial_content.update(self.parser.imports_src(modules))
93+
94+
def set_app_name(self, name: str | None = None) -> None:
95+
name = name or DEFAULT_WORKER_APP_NAME
96+
self._app = self.parser.app_name(name)
97+
98+
def set_config_from_object(self, app: Celery | None = None) -> None:
99+
self._config = self.parser.config(app)
100+
101+
def generate(self) -> dict:
102+
if not self._app_module_src:
103+
raise ValueError("Please set_app_module() before calling generate()")
104+
105+
initial_content = self._initial_content.copy()
106+
107+
if not initial_content["imports"]:
108+
raise ValueError("Please add_modules() before calling generate()")
109+
110+
_imports: dict | Any = initial_content.pop("imports")
111+
imports = "{%s}" % "}{".join(_imports.keys())
112+
imports = imports.format(**_imports)
113+
114+
app, config = self._app, self._config
115+
116+
replacement_args = {
117+
"imports": "imports = None",
118+
"app": f'app = Celery("{DEFAULT_WORKER_APP_NAME}")',
119+
"config": "config = None",
120+
}
121+
self._app_module_src = self._app_module_src.replace(replacement_args["imports"], imports)
122+
self._app_module_src = self._app_module_src.replace(replacement_args["app"], app)
123+
self._app_module_src = self._app_module_src.replace(replacement_args["config"], config)
124+
125+
initial_content["app.py"] = self._app_module_src.encode()
126+
127+
return initial_content

tests/conftest.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33

44
@pytest.fixture
5-
def default_worker_tasks() -> set:
5+
def default_worker_tasks(default_worker_tasks: set) -> set:
66
from tests import tasks
77

8-
yield {tasks}
8+
default_worker_tasks.add(tasks)
9+
yield default_worker_tasks

tests/integration/vendors/test_worker.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from types import ModuleType
4+
35
import pytest
46
from pytest_lazyfixture import lazy_fixture
57

@@ -32,6 +34,19 @@ def test_disabling_backend_cluster(self, container: CeleryWorkerContainer):
3234
results = DEFAULT_WORKER_ENV["CELERY_BROKER_URL"]
3335
assert container.logs().count(f"transport: {results}")
3436

37+
class test_replacing_app_module:
38+
@pytest.fixture(params=["Default", "Custom"])
39+
def default_worker_app_module(self, request: pytest.FixtureRequest) -> ModuleType:
40+
if request.param == "Default":
41+
yield request.getfixturevalue("default_worker_app_module")
42+
else:
43+
from pytest_celery.vendors.worker import app
44+
45+
yield app
46+
47+
def test_replacing_app_module(self, container: CeleryWorkerContainer, default_worker_app_module: ModuleType):
48+
assert container.app_module() == default_worker_app_module
49+
3550

3651
@pytest.mark.parametrize("worker", [lazy_fixture(CELERY_SETUP_WORKER)])
3752
class test_base_test_worker:

tests/unit/vendors/test_worker/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)