diff --git a/src/saleyo/__init__.py b/src/saleyo/__init__.py index f70cb75..dc5fa80 100644 --- a/src/saleyo/__init__.py +++ b/src/saleyo/__init__.py @@ -1,5 +1,5 @@ """ -Saleyo is A module to modify external python code in runtime. +Saleyo is a module to modify external python code in runtime. The implements are in `mixin`. @@ -10,11 +10,7 @@ The `base` module is used to extend your own `mixin` method. """ -from . import decorator, function, mixin, base - -__all__ = [ - "decorator", - "function", - "mixin", - "base", -] +from . import operation as operation +from . import base as base +from . import mixin as mixin +from .mixin import Mixin as Mixin diff --git a/src/saleyo/base/__init__.py b/src/saleyo/base/__init__.py index 8bad462..90722a1 100644 --- a/src/saleyo/base/__init__.py +++ b/src/saleyo/base/__init__.py @@ -1,7 +1,3 @@ -from . import typing, template, toolchain - -__all__ = [ - "typing", - "template", - "toolchain", -] +from . import typing as typing +from . import template as template +from . import toolchain as toolchain diff --git a/src/saleyo/base/template.py b/src/saleyo/base/template.py index e552836..11a745b 100644 --- a/src/saleyo/base/template.py +++ b/src/saleyo/base/template.py @@ -9,13 +9,17 @@ class MixinOperation(Generic[T], ABC): """ The MixinOperation is the base of All Operation. - The generic `MixinOperation` is the type of argument + The generic `MixinOperation` is the type of argument. + + `level` will affect to the mixin order, default to `1`. """ argument: T + level: int - def __init__(self, argument: T) -> None: + def __init__(self, argument: T, level=1) -> None: self.argument = argument + self.level = level def mixin(self, target: Type, toolchain: ToolChain = ToolChain()) -> None: raise NotImplementedError( diff --git a/src/saleyo/base/toolchain.py b/src/saleyo/base/toolchain.py index eaf63e4..28f3d65 100644 --- a/src/saleyo/base/toolchain.py +++ b/src/saleyo/base/toolchain.py @@ -55,7 +55,7 @@ class InvokeEvent(Generic[T]): argument: Dict[str, Any] @staticmethod - def from_call(target: Callable[..., Any], *args, **kwargs) -> "InvokeEvent": + def from_call(target: Callable[..., T], *args, **kwargs) -> "InvokeEvent[T]": argument = {} function_parameters = signature(target).parameters arg_names = list(function_parameters.keys()) diff --git a/src/saleyo/decorator.py b/src/saleyo/decorator.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/saleyo/function.py b/src/saleyo/function.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/saleyo/mixin/mixin.py b/src/saleyo/mixin.py similarity index 51% rename from src/saleyo/mixin/mixin.py rename to src/saleyo/mixin.py index 1da5388..63f1e69 100644 --- a/src/saleyo/mixin/mixin.py +++ b/src/saleyo/mixin.py @@ -1,8 +1,8 @@ from typing import List, Type -from ..base.template import MixinOperation -from ..base.toolchain import ToolChain -from ..base.typing import T, Target +from .base.template import MixinOperation +from .base.toolchain import ToolChain +from .base.typing import T, Target class Mixin: @@ -12,25 +12,34 @@ class Mixin: target_class: List[Type] toolchain: ToolChain + reverse_level: bool def __init__( self, target_class: Target, toolchain: ToolChain = ToolChain(), + reverse_level: bool = False, ) -> None: self.target_class = ( target_class if isinstance(target_class, list) else [target_class] ) self.toolchain = toolchain + self.reverse_level = reverse_level def collect(self, mixin: T) -> T: - for member in map( - lambda name: self.toolchain.tool_getattr(mixin, name), - filter(lambda name: not name.startswith("_"), dir(mixin)), - ): - if not isinstance(member, MixinOperation): - continue - + members: List[MixinOperation] = sorted( + filter( + lambda member: isinstance(member, MixinOperation), + map( + lambda name: self.toolchain.tool_getattr(mixin, name), + filter(lambda name: not name.startswith("_"), dir(mixin)), + ), + ), + key=lambda member: member.level, + reverse=self.reverse_level, + ) + + for member in members: for target in self.target_class: member.mixin(target=target, toolchain=self.toolchain) return mixin diff --git a/src/saleyo/mixin/__init__.py b/src/saleyo/mixin/__init__.py deleted file mode 100644 index d817830..0000000 --- a/src/saleyo/mixin/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -from .mixin import Mixin -from .accessor import Accessor -from .overwrite import OverWrite -from .processor import Processor -from .intercept import Intercept - -__all__ = [ - "Mixin", - "Accessor", - "OverWrite", - "Processor", - "Intercept", -] diff --git a/src/saleyo/mixin/inject.py b/src/saleyo/mixin/inject.py deleted file mode 100644 index 67f9b18..0000000 --- a/src/saleyo/mixin/inject.py +++ /dev/null @@ -1,13 +0,0 @@ -from types import MethodType -from typing import Type -from saleyo.base.template import MixinOperation -from saleyo.base.toolchain import ToolChain - - -class Inject(MixinOperation[MethodType]): - def mixin( - self, - target: Type, - toolchain: ToolChain = ToolChain(), - ) -> None: - return super().mixin(target, toolchain) diff --git a/src/saleyo/mixin/intercept.py b/src/saleyo/mixin/intercept.py deleted file mode 100644 index a929c4d..0000000 --- a/src/saleyo/mixin/intercept.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import Any, Callable, Type - -from ..base.toolchain import InvokeEvent, ToolChain -from ..base.template import MixinOperation - - -class Intercept(MixinOperation[Callable[[InvokeEvent], InvokeEvent]]): - """ - The `Intercept` allow you to intercept the arguments before invoking target method. - - Then, you can handle these arguments in your own functions. - """ - - def mixin( - self, - target: Type, - toolchain: ToolChain = ToolChain(), - ) -> None: - native_function = toolchain.tool_getattr( - target, - self.argument.__name__, - ) - - def invoke(*args, **kwargs) -> Any: - return self.argument( - InvokeEvent.from_call(native_function, *args, **kwargs) - ).invoke() - - return toolchain.tool_setattr( - target, - self.argument.__name__, - invoke, - ) diff --git a/src/saleyo/operation/__init__.py b/src/saleyo/operation/__init__.py new file mode 100644 index 0000000..c675054 --- /dev/null +++ b/src/saleyo/operation/__init__.py @@ -0,0 +1,5 @@ +from .accessor import Accessor as Accessor +from .overwrite import OverWrite as OverWrite +from .processor import Processor as Processor +from .intercept import Intercept as Intercept, InvokeEvent as InvokeEvent +from .hook import Pre as Pre, Post as Post diff --git a/src/saleyo/mixin/accessor.py b/src/saleyo/operation/accessor.py similarity index 86% rename from src/saleyo/mixin/accessor.py rename to src/saleyo/operation/accessor.py index 5f37e63..d6f6014 100644 --- a/src/saleyo/mixin/accessor.py +++ b/src/saleyo/operation/accessor.py @@ -16,6 +16,13 @@ class Accessor(Generic[T], MixinOperation[str]): _inner: Optional[T] + @staticmethod + def configure(level: int = 1): + return lambda argument: Accessor( + argument=argument, + level=level, + ) + def mixin(self, target: Type, toolchain: ToolChain = ToolChain()) -> None: self._inner = toolchain.tool_getattr( target, diff --git a/src/saleyo/operation/hook.py b/src/saleyo/operation/hook.py new file mode 100644 index 0000000..c12ba86 --- /dev/null +++ b/src/saleyo/operation/hook.py @@ -0,0 +1,79 @@ +from typing import Callable, Optional, Type + +from ..base.typing import RT, T +from ..base.toolchain import ToolChain +from ..base.template import MixinOperation + + +class Post(MixinOperation[Callable[[T], RT]]): + target_name: Optional[str] + + def __init__( + self, + argument: Callable[[T], RT], + target_name: Optional[str] = None, + level: int = 1, + ) -> None: + super().__init__(argument, level) + self.target_name = target_name + + @staticmethod + def configure( + level: int = 1, + target_name: Optional[str] = None, + ): + return lambda argument: Post( + argument=argument, + target_name=target_name, + level=level, + ) + + def mixin(self, target: Type, toolchain: ToolChain = ToolChain()) -> None: + target_name = ( + self.target_name if self.target_name is not None else self.argument.__name__ + ) + native_function = toolchain.tool_getattr(target, target_name) + + def post(*args, **kwargs): + result = native_function(*args, **kwargs) + self.argument(result) + return result + + return toolchain.tool_setattr(target, target_name, post) + + +class Pre(MixinOperation[Callable[..., RT]]): + target_name: Optional[str] + + def __init__( + self, + argument: Callable[[T], RT], + target_name: Optional[str] = None, + level: int = 1, + ) -> None: + super().__init__(argument, level) + self.target_name = target_name + + @staticmethod + def configure( + level: int = 1, + target_name: Optional[str] = None, + ): + return lambda argument: Post( + argument=argument, + target_name=target_name, + level=level, + ) + + def mixin(self, target: Type, toolchain: ToolChain = ToolChain()) -> None: + target_name = ( + self.target_name if self.target_name is not None else self.argument.__name__ + ) + native_function = toolchain.tool_getattr(target, target_name) + + def pre(*args, **kwargs): + self.argument(*args, **kwargs) + result = native_function(*args, **kwargs) + return result + + return toolchain.tool_setattr(target, target_name, pre) diff --git a/src/saleyo/operation/intercept.py b/src/saleyo/operation/intercept.py new file mode 100644 index 0000000..b3d09c7 --- /dev/null +++ b/src/saleyo/operation/intercept.py @@ -0,0 +1,61 @@ +from typing import Any, Callable, Generic, Optional, Type + +from ..base.typing import RT, T +from ..base.toolchain import InvokeEvent, ToolChain +from ..base.template import MixinOperation + + +class Intercept( + Generic[T, RT], MixinOperation[Callable[[InvokeEvent[T]], InvokeEvent[RT]]] +): + """ + The `Intercept` allow you to intercept the arguments before invoking target method. + + Then, you can handle these arguments in your own functions. + """ + + target_name: Optional[str] + + def __init__( + self, + argument: Callable[[InvokeEvent[T]], InvokeEvent[RT]], + level: int = 1, + target_name: Optional[str] = None, + ) -> None: + super().__init__(argument, level) + self.target_name = target_name + + @staticmethod + def configure( + level: int = 1, + target_name: Optional[str] = None, + ) -> Callable[[Callable[[InvokeEvent[T]], InvokeEvent[RT]]], "Intercept[T, RT]"]: + return lambda argument: Intercept( + argument=argument, + level=level, + target_name=target_name, + ) + + def mixin( + self, + target: Type, + toolchain: ToolChain = ToolChain(), + ) -> None: + target_name = ( + self.target_name if self.target_name is not None else self.argument.__name__ + ) + native_function = toolchain.tool_getattr( + target, + target_name, + ) + + def invoke(*args, **kwargs) -> Any: + return self.argument( + InvokeEvent.from_call(native_function, *args, **kwargs) + ).invoke() + + return toolchain.tool_setattr( + target, + target_name, + invoke, + ) diff --git a/src/saleyo/mixin/overwrite.py b/src/saleyo/operation/overwrite.py similarity index 99% rename from src/saleyo/mixin/overwrite.py rename to src/saleyo/operation/overwrite.py index b6d5f42..80ce12b 100644 --- a/src/saleyo/mixin/overwrite.py +++ b/src/saleyo/operation/overwrite.py @@ -1,5 +1,6 @@ from types import MethodType from typing import Callable, Optional, Type + from ..base.toolchain import ToolChain from ..base.template import MixinOperation diff --git a/src/saleyo/mixin/processor.py b/src/saleyo/operation/processor.py similarity index 95% rename from src/saleyo/mixin/processor.py rename to src/saleyo/operation/processor.py index 07f6d44..269b53e 100644 --- a/src/saleyo/mixin/processor.py +++ b/src/saleyo/operation/processor.py @@ -20,12 +20,13 @@ class Processor(MixinOperation[Callable[[str], str]]): def __init__( self, argument: Callable[[str], str], + level: int = 1, target_name: Optional[str] = None, prefix_indent: Optional[int] = None, module: Optional[ModuleType] = None, extra_namespace: Optional[NameSpace] = None, ) -> None: - super().__init__(argument) + super().__init__(argument, level) self.target_name = target_name self.prefix_indent = prefix_indent self.module = module @@ -33,6 +34,7 @@ def __init__( @staticmethod def configure( + level: int = 1, target_name: Optional[str] = None, prefix_indent: Optional[int] = None, module: Optional[ModuleType] = None, @@ -40,6 +42,7 @@ def configure( ) -> Callable[[Callable[[str], str]], "Processor"]: return lambda argument: Processor( argument=argument, + level=level, prefix_indent=prefix_indent, target_name=target_name, module=module, diff --git a/tests/__init__.py b/tests/__init__.py index 7931fcf..74f9b7d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,19 +1 @@ -from saleyo.base.toolchain import InvokeEvent -from saleyo.mixin import Mixin -from saleyo.mixin.intercept import Intercept - - -class Foo: - def demo(self, hello, bad=0): - print("goodbye~") - - -@Mixin(target_class=Foo) -class A: - @Intercept - @staticmethod - def demo(_: InvokeEvent[None]): - return InvokeEvent.from_call(lambda: print("hello world")) - - -Foo().demo(1) +from . import intercept as intercept diff --git a/tests/intercept.py b/tests/intercept.py new file mode 100644 index 0000000..1866770 --- /dev/null +++ b/tests/intercept.py @@ -0,0 +1,26 @@ +from saleyo import Mixin +from saleyo.operation import Intercept, InvokeEvent, Post, Pre + + +class Foo: + def demo(self): + print("goodbye~") + + +@Mixin(target_class=Foo) +class MixinFoo: + @Intercept.configure(target_name="demo") + @staticmethod + def intercept_demo(_: InvokeEvent[None]): + return InvokeEvent.from_call(lambda: print("hello world")) + + @Pre.configure(target_name="demo") + def pre_demo(*arg): + print("pre hello world") + + @Post.configure(target_name="demo") + def post_demo(*arg): + print("post hello world") + + +Foo().demo()