|
| 1 | +import sys |
1 | 2 | from typing import (
|
2 | 3 | Any,
|
| 4 | + Generic, |
3 | 5 | Tuple,
|
4 | 6 | Type,
|
5 | 7 | TypeVar,
|
6 | 8 | )
|
7 | 9 |
|
8 | 10 | from pydantic import BaseModel
|
9 | 11 | from pydantic.json_schema import GenerateJsonSchema
|
| 12 | +from typing_extensions import override |
10 | 13 |
|
11 | 14 | from galaxy.schema.fields import (
|
12 | 15 | DecodedDatabaseIdField,
|
@@ -50,3 +53,46 @@ def get_defs_ref(self, core_mode_ref):
|
50 | 53 | for i, choice in enumerate(choices):
|
51 | 54 | choices[i] = choice.replace(choices[0], ref_to_name[ref]) # type: ignore[call-overload]
|
52 | 55 | 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 |
0 commit comments