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