Skip to content

Commit f96111a

Browse files
committed
Add PatchGenericPickle to fix pickle issue in celery notifications
1 parent f382305 commit f96111a

File tree

2 files changed

+48
-1
lines changed

2 files changed

+48
-1
lines changed

lib/galaxy/schema/generics.py

+46
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
import sys
12
from typing import (
23
Any,
4+
Generic,
35
Tuple,
46
Type,
57
TypeVar,
68
)
79

810
from pydantic import BaseModel
911
from pydantic.json_schema import GenerateJsonSchema
12+
from typing_extensions import override
1013

1114
from galaxy.schema.fields import (
1215
DecodedDatabaseIdField,
@@ -50,3 +53,46 @@ def get_defs_ref(self, core_mode_ref):
5053
for i, choice in enumerate(choices):
5154
choices[i] = choice.replace(choices[0], ref_to_name[ref]) # type: ignore[call-overload]
5255
return full_def
56+
57+
58+
class PatchGenericPickle:
59+
"""A mixin that allows generic pydantic models to be serialized and deserialized with pickle.
60+
61+
Notes
62+
----
63+
In general, pickle shouldn't be encouraged as a means of serialization since there are better,
64+
safer options. In some cases e.g. Streamlit's `@st.cache_data there's no getting around
65+
needing to use pickle.
66+
67+
As of Pydantic 2.7, generics don't properly work with pickle. The core issue is the following
68+
1. For each specialized generic, pydantic creates a new subclass at runtime. This class
69+
has a `__qualname__` that contains the type var argument e.g. `"MyGeneric[str]"` for a
70+
`class MyGeneric(BaseModel, Generic[T])`.
71+
2. Pickle attempts to find a symbol with the value of `__qualname__` in the module where the
72+
class was defined, which fails since Pydantic defines that class dynamically at runtime.
73+
Pydantic does attempt to register these dynamic classes but currently only for classes
74+
defined at the top-level of the interpreter.
75+
76+
See Also
77+
--------
78+
- https://github.com/pydantic/pydantic/issues/9390
79+
"""
80+
81+
@classmethod
82+
@override
83+
def __init_subclass__(cls, **kwargs):
84+
# Note: we're still in __init_subclass__, not yet in __pydantic_init_subclass__
85+
# not all model_fields are available at this point.
86+
super().__init_subclass__(**kwargs)
87+
88+
if not issubclass(cls, BaseModel):
89+
raise TypeError("PatchGenericPickle can only be used with subclasses of pydantic.BaseModel")
90+
if not issubclass(cls, Generic): # type: ignore [arg-type]
91+
raise TypeError("PatchGenericPickle can only be used with Generic models")
92+
93+
qualname = cls.__qualname__
94+
declaring_module = sys.modules[cls.__module__]
95+
if qualname not in declaring_module.__dict__:
96+
# This should work in all cases, but we might need to make this check and update more
97+
# involved e.g. see pydantic._internal._generics.create_generic_submodel
98+
declaring_module.__dict__[qualname] = cls

lib/galaxy/schema/notifications.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from galaxy.schema.generics import (
2727
DatabaseIdT,
2828
GenericModel,
29+
PatchGenericPickle,
2930
)
3031
from galaxy.schema.schema import Model
3132
from galaxy.schema.types import (
@@ -264,7 +265,7 @@ class NotificationCreateData(Model):
264265
)
265266

266267

267-
class GenericNotificationRecipients(GenericModel, Generic[DatabaseIdT]):
268+
class GenericNotificationRecipients(GenericModel, Generic[DatabaseIdT], PatchGenericPickle):
268269
"""The recipients of a notification. Can be a combination of users, groups and roles."""
269270

270271
user_ids: List[DatabaseIdT] = Field(

0 commit comments

Comments
 (0)