class DynamicMixinAcceptor:
# Declarations needed by mypy
_orig_class: Type
_orig_init_signature: inspect.Signature
_mixin_classes: list[Type]
_init_params_per_mixin_cls: DefaultDict[str, DefaultDict[str, inspect.Parameter]]
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if DynamicMixinAcceptor in get_original_bases(cls) and cls.__init__ is object.__init__:
raise TypeError(
'Dynamic mixin acceptor class is required to define a __init__() method.')
cls._orig_init_signature = inspect.signature(cls.__init__)
cls._mixin_classes = []
cls._init_params_per_mixin_cls = defaultdict(defaultdict)
@classmethod
def _get_mixin_init_kwarg_params(cls) -> dict[str, inspect.Parameter]:
return {
key: param for param_dict in cls._init_params_per_mixin_cls.values()
for key, param in param_dict.items()
}
@property
def _mixin_init_kwarg_params(self) -> dict[str, inspect.Parameter]:
return self._get_mixin_init_kwarg_params()
@classmethod
def _get_mixin_init_kwarg_params_including_bases(cls) -> dict[str, inspect.Parameter]:
all_mixin_init_kwarg_params: dict[str, inspect.Parameter] = {}
base_list = list(cls.__mro__)
skip_bases = {'DynamicMixinAcceptor'}
for base in base_list:
if base.__name__.endswith(WITH_MIXINS_CLS_SUFFIX):
skip_bases.add(base.__name__[:-len(WITH_MIXINS_CLS_SUFFIX)])
cleaned_base_list = [
base for base in base_list
if issubclass(base, DynamicMixinAcceptor) and base.__name__ not in skip_bases
]
for base in cleaned_base_list:
all_mixin_init_kwarg_params |= dict(base._get_mixin_init_kwarg_params().items())
return all_mixin_init_kwarg_params
@property
def _mixin_init_kwarg_params_including_bases(self) -> dict[str, inspect.Parameter]:
return self._get_mixin_init_kwarg_params_including_bases()
@classmethod
def accept_mixin(cls, mixin_cls: Type) -> None:
cls._accept_mixin(mixin_cls, update=True)
@classmethod
def _accept_mixin(cls, mixin_cls: Type, update: bool):
cls._mixin_classes.append(mixin_cls)
if '__init__' in mixin_cls.__dict__:
cls._store_init_signature_params_for_mixin(mixin_cls)
if update:
cls._update_cls_init_signature_with_kwargs_all_mixin_kwargs()
@classmethod
def _store_init_signature_params_for_mixin(cls, mixin_cls):
for key, param in inspect.signature(mixin_cls.__init__).parameters.items():
if key != 'self':
if param.kind not in (param.KEYWORD_ONLY, param.VAR_KEYWORD):
raise AttributeError(
'All params in the signature of the __init__() method in a dynamic '
'mixin class must be keyword-only or var-keyword '
f'(except for the "self" param). Failing param: {param.name}')
if param.kind == param.KEYWORD_ONLY:
cls._init_params_per_mixin_cls[mixin_cls.__name__][key] = param
@classmethod
def _update_cls_init_signature_with_kwargs_all_mixin_kwargs(cls):
updated_init_signature = cls._get_updated_cls_init_signature_with_all_mixin_kwargs()
cls.__init__.__signature__ = updated_init_signature
if hasattr(cls, '_orig_class'):
cls._orig_class.__init____signature__ = updated_init_signature
@classmethod
def _get_updated_cls_init_signature_with_all_mixin_kwargs(cls):
orig_init_param_dict = cls._orig_init_signature.parameters
init_params = list(orig_init_param_dict.values())
opt_var_keyword_param = []
if init_params[-1].kind == inspect.Parameter.VAR_KEYWORD:
opt_var_keyword_param = [init_params[-1]]
init_params = init_params[:-1]
only_mixin_params = [
val for (key, val) in cls._get_mixin_init_kwarg_params_including_bases().items()
if key not in orig_init_param_dict
]
updated_params = init_params + only_mixin_params + opt_var_keyword_param
return cls._orig_init_signature.replace(parameters=updated_params)
@classmethod
def reset_mixins(cls):
cls._mixin_classes.clear()
cls._init_params_per_mixin_cls.clear()
cls.__init__.__signature__ = cls._orig_init_signature
def __new__(cls, *args, **kwargs):
if not cls.__name__.endswith(WITH_MIXINS_CLS_SUFFIX):
cls_with_mixins = cls._create_subcls_inheriting_from_mixins_and_orig_cls()
obj = super(cls, cls_with_mixins).__new__(cls_with_mixins, *args, **kwargs)
else:
obj = object.__new__(cls)
cls._update_cls_init_signature_with_kwargs_all_mixin_kwargs()
return obj
@classmethod
def _create_subcls_inheriting_from_mixins_and_orig_cls(cls) -> type[Self]: # noqa: C901
# TODO: Refactor this, and possibly elsewhere in class
def __init__(self, *args, **kwargs):
# print(f'__init__ for obj of class: {self.__class__.__name__}')
cls.__init__(self, *args, **kwargs)
mixin_kwargs_defaults = {}
for base in cls_with_mixins.__mro__:
if base == cls \
or base.__name__.endswith(WITH_MIXINS_CLS_SUFFIX) \
or base.__init__ is object.__init__:
continue
mixin_kwargs = {}
contains_positional = False
for key, param in inspect.signature(base.__init__).parameters.items():
if key != 'self':
if param.kind == param.KEYWORD_ONLY:
if key in kwargs:
mixin_kwargs[key] = kwargs[key]
elif key in mixin_kwargs_defaults:
mixin_kwargs[key] = mixin_kwargs_defaults[key]
else:
mixin_kwargs_defaults[key] = param.default
elif param.kind in (param.POSITIONAL_ONLY,
param.POSITIONAL_OR_KEYWORD,
param.VAR_POSITIONAL):
contains_positional = True
if contains_positional:
# print(f'Calling... {base.__name__}(args={args}, kwargs={mixin_kwargs})')
base.__init__(self, *args, **mixin_kwargs)
else:
# print(f'Calling... {base.__name__}(kwargs={mixin_kwargs})')
base.__init__(self, **mixin_kwargs)
cls_bases = list(get_original_bases(cls))
if not cls.__name__.endswith(WITH_MIXINS_CLS_SUFFIX):
cls_bases = list(cls._mixin_classes) + cls_bases
cls_bases_with_mixins = []
for cls_base in cls_bases:
if cls._is_true_acceptor_subclass(cls_base):
cls_new_base = cls_base._create_subcls_inheriting_from_mixins_and_orig_cls()
cls_base = transfer_generic_args_to_cls(cls_new_base, cls_base)
cls_bases_with_mixins.append(cls_base)
def fill_ns(ns):
ns |= dict(__init__=__init__)
return ns
cls_with_mixins: type[Self] = types.new_class(
f'{cls.__name__}{WITH_MIXINS_CLS_SUFFIX}',
tuple([cls] + cls_bases_with_mixins),
{},
fill_ns,
)
cls_with_mixins.__module__ = cls.__module__
cls_with_mixins._orig_class = cls
cls_with_mixins._orig_init_signature = inspect.signature(cls.__init__)
for mixin_cls in cls._mixin_classes:
cls_with_mixins._accept_mixin(mixin_cls, update=False)
return cls_with_mixins
@staticmethod
def _is_true_acceptor_subclass(cls):
if cls == DynamicMixinAcceptor:
return False
return generic_aware_issubclass_ignore_args(cls, DynamicMixinAcceptor)