Skip to content

Instantly share code, notes, and snippets.

@Andrei-Pozolotin
Last active December 22, 2024 12:14
Show Gist options
  • Save Andrei-Pozolotin/7090f83e419dcfab9f5fcb67d5a4017c to your computer and use it in GitHub Desktop.
Save Andrei-Pozolotin/7090f83e419dcfab9f5fcb67d5a4017c to your computer and use it in GitHub Desktop.
#
# 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})"
#
#
#
#
#
#
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
#
#
#
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