1from __future__ import annotations
2
3from collections.abc import Callable
4from importlib import import_module
5from typing import Any, overload
6
7
8@overload
9def deconstructible[T](cls: type[T], /) -> type[T]: ...
10@overload
11def deconstructible[T](*, path: str | None = ...) -> Callable[[type[T]], type[T]]: ...
12def deconstructible[T](
13 *args: type[T], path: str | None = None
14) -> Callable[[type[T]], type[T]] | type[T]:
15 """
16 Class decorator that allows the decorated class to be serialized
17 by the migrations subsystem.
18
19 The `path` kwarg specifies the import path.
20 """
21
22 def decorator(klass: type[T]) -> type[T]:
23 def __new__(cls: type[T], *args: Any, **kwargs: Any) -> T:
24 # We capture the arguments to make returning them trivial
25 obj = super(klass, cls).__new__(cls) # ty: ignore[unresolved-attribute]
26 obj._constructor_args = (args, kwargs)
27 return obj
28
29 def deconstruct(obj: Any) -> tuple[str, tuple[Any, ...], dict[str, Any]]:
30 """
31 Return a 3-tuple of class import path, positional arguments,
32 and keyword arguments.
33 """
34 # Fallback version
35 if path and type(obj) is klass:
36 module_name, _, name = path.rpartition(".")
37 else:
38 module_name = obj.__module__
39 name = obj.__class__.__name__
40 # Make sure it's actually there and not an inner class
41 module = import_module(module_name)
42 if not hasattr(module, name):
43 raise ValueError(
44 f"Could not find object {name} in {module_name}.\n"
45 "Please note that you cannot serialize things like inner "
46 "classes. Please move the object into the main module "
47 "body to use migrations."
48 )
49 return (
50 path
51 if path and type(obj) is klass
52 else f"{obj.__class__.__module__}.{name}",
53 obj._constructor_args[0],
54 obj._constructor_args[1],
55 )
56
57 setattr(klass, "__new__", staticmethod(__new__))
58 setattr(klass, "deconstruct", deconstruct)
59
60 return klass
61
62 if not args:
63 return decorator
64 return decorator(*args)