Last active
December 22, 2024 12:14
-
-
Save Andrei-Pozolotin/7090f83e419dcfab9f5fcb67d5a4017c to your computer and use it in GitHub Desktop.
Cooperative Multiple Inheritance in Python: https://stackoverflow.com/questions/16305177/cooperative-multiple-inheritance-issue
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# https://stackoverflow.com/questions/16305177/cooperative-multiple-inheritance-issue | |
# | |
import inspect | |
from typing import Any | |
from typing import MutableMapping # @UnusedImport | |
from typing import Type | |
from typing import _AnnotatedAlias # type:ignore[reportAttributeAccessIssue] @UnresolvedImport | |
from typing import _UnionGenericAlias # type:ignore[reportAttributeAccessIssue] @UnresolvedImport | |
class BasicHabit_SYS: # cooperative multiple inheritance trait | |
_USE_DEV_HABIT_ = False | |
_USE_HABIT_INJECT_ = False | |
@property # inheritance stops before python::object | |
def habit_base_type_list(self) -> tuple[type, ...]: | |
return self.__class__.__mro__[0:-1] | |
# permit attribute override in __init__() | |
def habit_has_inject_ignore(self, attr_name:str, attr_type:Type) -> bool: | |
if hasattr(self, attr_name): # contract: alredy was set | |
return True | |
match attr_type: | |
case _AnnotatedAlias(): # contract: promise to be set | |
return True | |
return False | |
# permit inject None for missing context atruments | |
def habit_has_inject_optional(self, attr_name:str, attr_type:Type) -> bool: # @UnusedVariable | |
match attr_type: | |
case _UnionGenericAlias(_name=type_name) if type_name == "Optional": | |
return True | |
return False | |
# auto-inject instance properties with simple type annotations | |
def habit_inject_init_args(self, **init_args_dict:MutableMapping) -> None: | |
self_type = self.__class__.__name__ | |
for base_type in self.habit_base_type_list: | |
for attr_name, attr_type in base_type.__annotations__.items(): | |
if self.habit_has_inject_ignore(attr_name, attr_type): | |
continue | |
if self._USE_DEV_HABIT_: print(f"HABIT INJECT: {self_type=} {attr_name=} {attr_type=}") | |
if self.habit_has_inject_optional(attr_name, attr_type): | |
attr_value = init_args_dict.get(attr_name, None) | |
else: | |
attr_value = init_args_dict[attr_name] # inject required | |
setattr(self, attr_name, attr_value) | |
# collect consumed arguments and call next __init__ in __mro__ chain | |
def habit_invoke_next_init(self, super_entry:Any) -> None: | |
assert isinstance(super_entry, super), f"expecting super(): {super_entry=}" | |
stack_frame_list = inspect.stack() | |
stack_frame_back = stack_frame_list[1] # contract: call here in derived __init__ | |
origin_value_dict = stack_frame_back.frame.f_locals | |
origin_class = super_entry.__thisclass__ # type:ignore[reportAttributeAccessIssue] super() internal | |
origin_init_func = origin_class.__init__ | |
origin_signature = inspect.signature(origin_init_func) | |
origin_param_dict = origin_signature.parameters | |
result_param_dict = dict() | |
self_type = self.__class__.__name__ | |
origin_type = origin_class.__name__ | |
if self._USE_DEV_HABIT_: print(f"HABIT SUPER: {self_type=} {origin_type=}") | |
for param_entry in origin_param_dict.values(): | |
param_name = param_entry.name | |
param_kind = param_entry.kind | |
if param_name == "self": | |
continue | |
if self._USE_DEV_HABIT_: print(f"HABIT SUPER: {self_type=} {param_name=} {param_kind=}") | |
match param_kind: | |
case inspect.Parameter.KEYWORD_ONLY: | |
result_param_dict[param_name] = origin_value_dict[param_name] | |
case inspect.Parameter.VAR_KEYWORD: | |
result_param_dict.update(origin_value_dict[param_name]) | |
case _: | |
raise ValueError(f"expecting keyword argument: {param_name=} {param_kind=}") | |
super_entry.__init__(**result_param_dict) | |
def __init__(self, **habit_args_dict:MutableMapping) -> None: | |
if self._USE_HABIT_INJECT_: | |
self.habit_inject_init_args(**habit_args_dict) | |
__init_next__ = habit_invoke_next_init | |
def __repr__(self) -> str: | |
klaz_name = self.__class__.__name__ | |
args_list = [] | |
for base_type in self.habit_base_type_list: | |
for attr_name in base_type.__annotations__: | |
attr_value = getattr(self, attr_name, None) | |
args_entry = f"{attr_name}='{attr_value}'" | |
args_list.append(args_entry) | |
args_text = ",".join(args_list) | |
return f"{klaz_name}({args_text})" | |
# | |
# | |
# |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# | |
# | |
from typing import Annotated | |
from typing import Optional | |
from habit import BasicHabit_SYS | |
class ColorKind( | |
BasicHabit_SYS, | |
): | |
color_name:str | |
color_size:Optional[int] | |
def __init__(self, *, | |
color_name:str, | |
**_context_ | |
): | |
self.color_name = color_name | |
self.__init_next__(super()) | |
class SpaceKind( | |
BasicHabit_SYS, | |
): | |
space_name:Optional[str] | |
space_kind:Annotated[str, "skip this"] | |
space_size:int | |
def __init__(self, *, | |
space_size, | |
**_context_ | |
): | |
self.space_size = space_size | |
self.__init_next__(super()) | |
class MatterCompound( | |
ColorKind, | |
SpaceKind, | |
BasicHabit_SYS, | |
): | |
matter_name:str | |
matter_volume:float | |
def __init__(self, *, | |
matter_name:str, | |
matter_volume:float, | |
**_context_ | |
): | |
self.matter_name = matter_name | |
self.matter_volume = matter_volume | |
self.__init_next__(super()) | |
BasicHabit_SYS._USE_DEV_HABIT_ = True # enable debug print | |
BasicHabit_SYS._USE_HABIT_INJECT_ = True # enable magic inject | |
color_result = ColorKind( | |
color_name="red", | |
) | |
print(color_result) | |
assert color_result.color_name == "red" | |
assert color_result.color_size == None # magic inject | |
matter_result = MatterCompound( | |
color_name="green", # used by ColorKind | |
color_size=12, # used by ColorKind, magic inject | |
space_name="empty", # used by SpaceKind, magic inject | |
space_size=7, # used by SpaceKind | |
# space_kind # magic inject to None | |
matter_name="steel", # used by MatterCompound | |
matter_volume="expanding", # used by MatterCompound | |
extra_param="unused", # ignored by this class family | |
) | |
print(matter_result) | |
assert matter_result.color_size == 12 # magic inject | |
assert matter_result.space_name == "empty" # magic inject | |
# | |
# | |
# |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
HABIT SUPER: self_type='ColorKind' origin_type='ColorKind' | |
HABIT SUPER: self_type='ColorKind' param_name='color_name' param_kind=<_ParameterKind.KEYWORD_ONLY: 3> | |
HABIT SUPER: self_type='ColorKind' param_name='_context_' param_kind=<_ParameterKind.VAR_KEYWORD: 4> | |
HABIT INJECT: self_type='ColorKind' attr_name='color_size' attr_type=typing.Optional[int] | |
ColorKind(color_name='red',color_size='None') | |
HABIT SUPER: self_type='MatterCompound' origin_type='MatterCompound' | |
HABIT SUPER: self_type='MatterCompound' param_name='matter_name' param_kind=<_ParameterKind.KEYWORD_ONLY: 3> | |
HABIT SUPER: self_type='MatterCompound' param_name='matter_volume' param_kind=<_ParameterKind.KEYWORD_ONLY: 3> | |
HABIT SUPER: self_type='MatterCompound' param_name='_context_' param_kind=<_ParameterKind.VAR_KEYWORD: 4> | |
HABIT SUPER: self_type='MatterCompound' origin_type='ColorKind' | |
HABIT SUPER: self_type='MatterCompound' param_name='color_name' param_kind=<_ParameterKind.KEYWORD_ONLY: 3> | |
HABIT SUPER: self_type='MatterCompound' param_name='_context_' param_kind=<_ParameterKind.VAR_KEYWORD: 4> | |
HABIT SUPER: self_type='MatterCompound' origin_type='SpaceKind' | |
HABIT SUPER: self_type='MatterCompound' param_name='space_size' param_kind=<_ParameterKind.KEYWORD_ONLY: 3> | |
HABIT SUPER: self_type='MatterCompound' param_name='_context_' param_kind=<_ParameterKind.VAR_KEYWORD: 4> | |
HABIT INJECT: self_type='MatterCompound' attr_name='color_size' attr_type=typing.Optional[int] | |
HABIT INJECT: self_type='MatterCompound' attr_name='space_name' attr_type=typing.Optional[str] | |
MatterCompound(matter_name='steel',matter_volume='expanding',color_name='green',color_size='12',space_name='empty',space_kind='None',space_size='7') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment