Skip to content

Commit a795e05

Browse files
committed
Pass ProjectVersionInfo as JSON-safe data
In order to safely pass the data across the celery task `defer` call, which implicitly JSON de/serializes data, the objects need to be converted to and from a JSON-compatible shape. As a simple paradigm for handling this, `serialize_many` and `parse_many` are added to `ProjectVersionInfo` as classmethods for converting to and from a list of dicts. By structuring `ProjectVersionInfo` as a decorator, it's possible to keep the code within the task definition the same as it was before.
1 parent fc13b1a commit a795e05

File tree

3 files changed

+49
-2
lines changed

3 files changed

+49
-2
lines changed

readthedocs/builds/tasks.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,10 @@ def delete_closed_external_versions(limit=200, days=30 * 3):
276276

277277

278278
@app.task(max_retries=1, default_retry_delay=60, queue="web")
279+
@ProjectVersionInfo.parse_many("tags_data", "branches_data")
279280
def sync_versions_task(
280281
project_pk: int,
282+
*,
281283
tags_data: list[ProjectVersionInfo],
282284
branches_data: list[ProjectVersionInfo],
283285
**kwargs: object,

readthedocs/projects/datatypes.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,19 @@
44
classes.
55
"""
66

7+
from __future__ import annotations
8+
79
import dataclasses
10+
import functools
11+
import sys
12+
import typing
13+
14+
15+
if typing.TYPE_CHECKING:
16+
if sys.version_info >= (3, 11):
17+
from typing import Self
18+
else:
19+
from typing_extensions import Self
820

921

1022
@dataclasses.dataclass
@@ -21,3 +33,36 @@ class ProjectVersionInfo:
2133

2234
verbose_name: str
2335
identifier: str
36+
37+
@classmethod
38+
def serialize_many(cls, data: typing.Sequence[Self]) -> list[dict[str, str]]:
39+
r"""
40+
A preparatory step which converts a sequence of ``ProjectVersionInfo``\s into a
41+
format suitable for JSON serialization.
42+
"""
43+
return [dataclasses.asdict(item) for item in data]
44+
45+
@classmethod
46+
def parse_many(cls, *argument_names: str) -> typing.Callable[..., typing.Any]:
47+
"""
48+
A decorator which adds a parsing step to deserialize JSON-ified data.
49+
50+
The keyword argument names which will be targetted for parsing must be supplied
51+
as strings to ``parse_many()``.
52+
"""
53+
54+
def decorator(func: typing.Callable[..., typing.Any]) -> typing.Callable[..., typing.Any]:
55+
@functools.wraps(func)
56+
def wrapper(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
57+
for name in argument_names:
58+
if name not in kwargs:
59+
raise TypeError(f"Missing required argument: {name}")
60+
kwargs[name] = [
61+
item if isinstance(item, ProjectVersionInfo) else cls(**item)
62+
for item in kwargs[name]
63+
]
64+
return func(*args, **kwargs)
65+
66+
return wrapper
67+
68+
return decorator

readthedocs/projects/tasks/mixins.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ def sync_versions(self, vcs_repository):
8282

8383
build_tasks.sync_versions_task.delay(
8484
project_pk=self.data.project.pk,
85-
tags_data=tags_data,
86-
branches_data=branches_data,
85+
tags_data=ProjectVersionInfo.serialize_many(tags_data),
86+
branches_data=ProjectVersionInfo.serialize_many(branches_data),
8787
)
8888

8989
def validate_duplicate_reserved_versions(

0 commit comments

Comments
 (0)