diff --git a/README.md b/README.md index 34d5c72..df8632b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Saleyo -Saleyo is a lightwight Python AOP framework, easy to use and integrate. +Saleyo is a lightwight scalable Python AOP framework, easy to use and integrate. ## Getting Start @@ -12,6 +12,8 @@ pip install saleyo ### Declear a `Mixin` class +If you don't like decorators, you can pass arguments to operations and call the `mixin` method manually. + ```python from saleyo import Mixin @@ -24,10 +26,11 @@ class MixinFoo:... ### Use `MixinOperation` +Here is a simple demo. + ```python from typing import Any -from saleyo import Mixin -from saleyo.operation import Accessor, OverWrite, Post, Pre, Intercept, InvokeEvent +from saleyo import Mixin, Accessor, OverWrite, Post, Pre, Intercept, InvokeEvent class Foo: @@ -51,7 +54,7 @@ class MixinFoo: @Intercept.configure(target_name="demo") @staticmethod def intercept_demo(_: InvokeEvent): - return InvokeEvent.from_call(lambda: print("hello world")) + return InvokeEvent(lambda: print("hello world")) # Will call before `demo` call @Pre.configure(target_name="demo") @@ -79,14 +82,39 @@ foo.demo() >>> post hello world ``` +### Custom ToolChain + +'ToolChain' determines the ability to modify the class. + +```python +from saleyo import Mixin, GCToolChain, Arguments, Pre + + +@Mixin(target=str, toolchain=GCToolChain) +class MixinStr: + @Pre.configure(target_name="format") + def pre_format(self, *args) -> Arguments[...]: + print(f"input args: {args}") + return Arguments(self, "saleyo") + + +print("hello world {}".format("python")) + +>>> input args: ('python',) +>>> hello world saleyo +``` + + ### Custom Operation +The default operations can't satify you? Why not try define a operation yourself! + ```python from typing import Any, Type -from saleyo.base.template import MixinOperation -from saleyo.base.toolchain import ToolChain +from saleyo import MixinOperation, ToolChain class MyOperation(MixinOperation[Any]): def mixin(self, target: Type, toolchain: ToolChain = ...) -> None: ... -``` \ No newline at end of file +``` + diff --git a/pyproject.toml b/pyproject.toml index f69a05e..c740b79 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "saleyo" -version = "0.1.2" -description = "Saleyo is a lightwight Python AOP framework, easy to use and integrate." +version = "0.1.3" +description = "Saleyo is a lightwight scalable Python AOP framework, easy to use and integrate." authors = [{ name = "H2Sxxa", email = "h2sxxa0w0@gmail.com" }] dependencies = [] requires-python = ">=3.8" diff --git a/src/saleyo/__init__.py b/src/saleyo/__init__.py index dc5fa80..d147d2d 100644 --- a/src/saleyo/__init__.py +++ b/src/saleyo/__init__.py @@ -1,16 +1,33 @@ """ Saleyo is a module to modify external python code in runtime. -The implements are in `mixin`. +The `operation` module defines some default `MixinOperation`. -If you want to call the method manually, you can try the `function` module. +The `base` module is used to extend your own `mixin` method. -If you want to use some decorators, please use the `decorator` module. +Don't know how to start? Please see the part of Basic Tutorial in README. -The `base` module is used to extend your own `mixin` method. +The two links below are available. + +https://github.com/H2Sxxa/saleyo/blob/main/README.md + +https://pypi.org/project/saleyo/ """ from . import operation as operation from . import base as base from . import mixin as mixin from .mixin import Mixin as Mixin +from .operation import Accessor as Accessor +from .operation import Processor as Processor +from .operation import Intercept as Intercept +from .operation import OverWrite as OverWrite +from .operation import Pre as Pre +from .operation import Post as Post +from .base.toolchain import ToolChain as ToolChain +from .base.toolchain import Arguments as Arguments +from .base.toolchain import InvokeEvent as InvokeEvent +from .base.toolchain import CPyToolChain as CPyToolChain +from .base.toolchain import GCToolChain as GCToolChain + +__version__ = (0, 1, 3) diff --git a/src/saleyo/base/template.py b/src/saleyo/base/template.py index 11a745b..214ac06 100644 --- a/src/saleyo/base/template.py +++ b/src/saleyo/base/template.py @@ -1,5 +1,5 @@ from abc import ABC -from typing import Generic, Type +from typing import Any, Generic, Type from .toolchain import ToolChain from .typing import T @@ -21,7 +21,7 @@ def __init__(self, argument: T, level=1) -> None: self.argument = argument self.level = level - def mixin(self, target: Type, toolchain: ToolChain = ToolChain()) -> None: + def mixin(self, target: Type[Any], toolchain: ToolChain = ToolChain()) -> None: raise NotImplementedError( f"Not Ready to use this Operation to modify '{target}' via '{toolchain}'" ) diff --git a/src/saleyo/base/toolchain.py b/src/saleyo/base/toolchain.py index d5fb1d2..5886e7b 100644 --- a/src/saleyo/base/toolchain.py +++ b/src/saleyo/base/toolchain.py @@ -1,9 +1,8 @@ -from dataclasses import dataclass -import ctypes as _ctypes +from ctypes import py_object as _py_object, POINTER as _POINTER, cast as _cast from types import FunctionType -from typing import Any, Callable, Dict, Generic, Optional +from dataclasses import dataclass from gc import get_referents as _get_referents - +from typing import Any, Callable, Dict, Generic, Optional from ..base.typing import P, RT, NameSpace @@ -11,9 +10,9 @@ @dataclass class ToolChain: """ - The tool to do mixin. + The tool class to do mixin. - Default to use `getattr`/`setattr`/`hasattr` + Default to use `getattr`/`setattr`/`hasattr`. """ tool_getattr: Callable[[Any, str], Any] = getattr @@ -36,8 +35,8 @@ class ToolChain: def _cpy_get_dict(_object: Any) -> Dict[str, Any]: - return _ctypes.cast( - id(_object) + type(_object).__dictoffset__, _ctypes.POINTER(_ctypes.py_object) + return _cast( + id(_object) + type(_object).__dictoffset__, _POINTER(_py_object) ).contents.value @@ -84,46 +83,39 @@ def define_function( )[function_name] -class Arugument(Generic[P]): +class Arguments(Generic[P]): """ `Argument` is used to call function, the Generic `P` is the params of target function. """ def __init__(self, *args: P.args, **kwargs: P.kwargs) -> None: - self.positional = args - self.keyword = kwargs + self.args = args + self.kwargs = kwargs def __str__(self) -> str: - return f"Arugument(positional: {self.positional}, keyword: {self.keyword} )" + return f"Arugument(positional: {self.args}, keyword: {self.kwargs} )" -@dataclass class InvokeEvent(Generic[P, RT]): """ A `InvokeEvent` includes the target function and the arguments to call this functions. - - Recommend to use the static method `InvokeEvent.from_call` to get a `InvokeEvent`. """ target: Callable[P, RT] - argument: Arugument[P] + argument: Arguments[P] - @staticmethod - def from_call( + def __init__( + self, target: Callable[P, RT], *args: P.args, **kwargs: P.kwargs, - ) -> "InvokeEvent[P, RT]": - return InvokeEvent( - target=target, - argument=Arugument( - *args, - **kwargs, - ), - ) + ) -> None: + super().__init__() + self.target = target + self.argument = Arguments(*args, **kwargs) def invoke(self, target: Callable[P, RT]) -> RT: - return target(*self.argument.positional, **self.argument.keyword) + return target(*self.argument.args, **self.argument.kwargs) def invoke_target(self) -> RT: - return self.target(*self.argument.positional, **self.argument.keyword) + return self.target(*self.argument.args, **self.argument.kwargs) diff --git a/src/saleyo/base/typing.py b/src/saleyo/base/typing.py index 2f0e0c9..0d3adc6 100644 --- a/src/saleyo/base/typing.py +++ b/src/saleyo/base/typing.py @@ -1,7 +1,22 @@ from typing import Any, Dict, List, ParamSpec, Type, TypeVar, Union -Target = Union[Type, List[Type]] +Target = Union[Type[Any], List[Type[Any]]] +""" +`Target` is the target of `@Mixin`, it's the alias of `Union[Type[Any], List[Type[Any]]]` +""" NameSpace = Dict[str, Any] +""" +`NameSpace` is the alias of `Dict[str, Any]` +""" RT = TypeVar("RT") +""" +`RT` means `Return Type` +""" T = TypeVar("T") +""" +`T` means `Type` +""" P = ParamSpec("P") +""" +`P` means `Params` +""" diff --git a/src/saleyo/mixin.py b/src/saleyo/mixin.py index 1fe2a33..c4e7281 100644 --- a/src/saleyo/mixin.py +++ b/src/saleyo/mixin.py @@ -1,4 +1,4 @@ -from typing import List, Type +from typing import List from .base.template import MixinOperation from .base.toolchain import ToolChain @@ -14,7 +14,7 @@ class Mixin: Allow to have more than one target, but that's not recommended. """ - target: List[Type] + target: Target toolchain: ToolChain reverse_level: bool diff --git a/src/saleyo/operation/__init__.py b/src/saleyo/operation/__init__.py index c675054..aa0f191 100644 --- a/src/saleyo/operation/__init__.py +++ b/src/saleyo/operation/__init__.py @@ -1,5 +1,6 @@ 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 +from .intercept import Intercept as Intercept +from .hook import Pre as Pre +from .hook import Post as Post diff --git a/src/saleyo/operation/accessor.py b/src/saleyo/operation/accessor.py index d6f6014..dd27e15 100644 --- a/src/saleyo/operation/accessor.py +++ b/src/saleyo/operation/accessor.py @@ -1,4 +1,4 @@ -from typing import Generic, Optional, Type +from typing import Any, Generic, Optional, Type from ..base.toolchain import ToolChain from ..base.typing import T @@ -9,6 +9,8 @@ class Accessor(Generic[T], MixinOperation[str]): """ Want to access and modify some private varibles or methods? Try use `Accessor`! + The Generic is the type of target varible. + Notice: The value only available after invoking the `mixin` method. If you use `@Mixin` and have more than one target classes, the `value` will always be the varible of latest target. @@ -16,6 +18,10 @@ class Accessor(Generic[T], MixinOperation[str]): _inner: Optional[T] + def __init__(self, argument: str, level=1) -> None: + super().__init__(argument, level) + self._inner = None + @staticmethod def configure(level: int = 1): return lambda argument: Accessor( @@ -23,7 +29,7 @@ def configure(level: int = 1): level=level, ) - def mixin(self, target: Type, toolchain: ToolChain = ToolChain()) -> None: + def mixin(self, target: Type[Any], toolchain: ToolChain = ToolChain()) -> None: self._inner = toolchain.tool_getattr( target, f"_{target.__name__}{self.argument}", diff --git a/src/saleyo/operation/hook.py b/src/saleyo/operation/hook.py index 50f0164..7c823e8 100644 --- a/src/saleyo/operation/hook.py +++ b/src/saleyo/operation/hook.py @@ -1,13 +1,15 @@ -from typing import Callable, Optional, Type +from typing import Any, Callable, Optional, Type, Union -from ..base.typing import RT, T -from ..base.toolchain import ToolChain +from ..base.typing import P, RT, T +from ..base.toolchain import ToolChain, Arguments from ..base.template import MixinOperation -class Post(MixinOperation[Callable[[T], RT]]): +class Post(MixinOperation[Callable[[T], Optional[RT]]]): """ `Post` will call after the target method, and the callable should be decorated as `@staticmethod` and have one argument to receive the result of target method. + + If the `post` function return value is not `None`, it will replace the original result. """ target_name: Optional[str] @@ -32,30 +34,34 @@ def configure( level=level, ) - def mixin(self, target: Type, toolchain: ToolChain = ToolChain()) -> None: + def mixin(self, target: Type[Any], 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) + native_function: Callable[..., T] = toolchain.tool_getattr(target, target_name) - def post(*args, **kwargs): + def post(*args, **kwargs) -> Union[T, RT]: result = native_function(*args, **kwargs) - self.argument(result) + post_result = self.argument(result) + if post_result is not None: + return post_result return result return toolchain.tool_setattr(target, target_name, post) -class Pre(MixinOperation[Callable[..., RT]]): +class Pre(MixinOperation[Callable[P, Optional[Arguments[P]]]]): """ `Pre` will call before the target method, and the callable should be decorated as `@staticmethod` and have `*args,**kwargs` to receive the arguments of target method. + + If the `pre` function return value is a `Aruguments`(not `None`), it will replace the original arguments. """ target_name: Optional[str] def __init__( self, - argument: Callable[[T], RT], + argument: Callable[P, Optional[Arguments[P]]], target_name: Optional[str] = None, level: int = 1, ) -> None: @@ -73,14 +79,16 @@ def configure( level=level, ) - def mixin(self, target: Type, toolchain: ToolChain = ToolChain()) -> None: + def mixin(self, target: Type[Any], 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) + def pre(*args: P.args, **kwargs: P.kwargs) -> Any: + arguments = self.argument(*args, **kwargs) + if arguments is not None: + return native_function(*arguments.args, **arguments.kwargs) return native_function(*args, **kwargs) return toolchain.tool_setattr(target, target_name, pre) diff --git a/src/saleyo/operation/intercept.py b/src/saleyo/operation/intercept.py index 77f3856..1274748 100644 --- a/src/saleyo/operation/intercept.py +++ b/src/saleyo/operation/intercept.py @@ -13,7 +13,9 @@ class Intercept(Generic[_PA, _PB], MixinOperation[Callable[[_A[_PA]], _B[_PB]]]) """ The `Intercept` allow you to intercept the arguments before invoking target method. - Then, you can handle these arguments in your own function. + Then, you can handle these arguments in your own function and make a redirect to another function. + + If you just want to modify the arguments or the result, please see `Pre` and `Post`. """ target_name: Optional[str] @@ -40,7 +42,7 @@ def configure( def mixin( self, - target: Type, + target: Type[Any], toolchain: ToolChain = ToolChain(), ) -> None: target_name = ( @@ -56,6 +58,6 @@ def mixin( target, target_name, lambda *args, **kwargs: self.argument( - InvokeEvent.from_call(native_function, *args, **kwargs) + InvokeEvent(native_function, *args, **kwargs) ).invoke_target(), ) diff --git a/src/saleyo/operation/overwrite.py b/src/saleyo/operation/overwrite.py index ad28280..9b54eb9 100644 --- a/src/saleyo/operation/overwrite.py +++ b/src/saleyo/operation/overwrite.py @@ -1,4 +1,4 @@ -from typing import Callable, Optional, Type +from typing import Any, Callable, Optional, Type from ..base.toolchain import ToolChain from ..base.template import MixinOperation @@ -32,11 +32,11 @@ def configure( target_name=target_name, ) - def mixin(self, target: Type, toolchain: ToolChain = ToolChain()) -> None: + def mixin(self, target: Type[Any], toolchain: ToolChain = ToolChain()) -> None: target_name = ( self.argument.__name__ if self.target_name is None else self.target_name ) - # self.argument.__qualname__ = f"{target.__name__}.{target_name}" + self.argument.__qualname__ = f"{target.__name__}.{target_name}" return toolchain.tool_setattr( target, target_name, diff --git a/src/saleyo/operation/processor.py b/src/saleyo/operation/processor.py index c78223f..aa00283 100644 --- a/src/saleyo/operation/processor.py +++ b/src/saleyo/operation/processor.py @@ -1,6 +1,6 @@ from inspect import getsource from types import ModuleType -from typing import Callable, Optional, Type +from typing import Any, Callable, Optional, Type from ..base.typing import NameSpace from ..base.toolchain import Container, ToolChain @@ -12,6 +12,8 @@ class Processor(MixinOperation[Callable[[str], str]]): If you want to get the soure code of a method and use `split` and `replace` to modify and redefine it,Try `Processor`. When you try to use this, please make sure you configure the correct module of your target, or you can use `extra_namespace` to supplement the missing things. + + Don't try to use it with external code, like the code of cpython, it will crash. """ module: Optional[ModuleType] @@ -53,7 +55,7 @@ def configure( def mixin( self, - target: Type, + target: Type[Any], toolchain: ToolChain = ToolChain(), ) -> None: target_name = ( diff --git a/tests/__init__.py b/tests/__init__.py index 4121273..b46b88e 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,2 +1,4 @@ import intercept as intercept import demo as demo +import gc_test as gc_test +import custom as custom diff --git a/tests/demo.py b/tests/demo.py index 2d61d1a..e403ef0 100644 --- a/tests/demo.py +++ b/tests/demo.py @@ -1,6 +1,5 @@ from typing import Any -from saleyo import Mixin -from saleyo.operation import Accessor, OverWrite, Post, Pre, Intercept, InvokeEvent +from saleyo import Mixin, Accessor, OverWrite, Post, Pre, Intercept, InvokeEvent class Foo: @@ -24,7 +23,7 @@ def func(self): @Intercept.configure(target_name="demo") @staticmethod def intercept_demo(_: InvokeEvent): - return InvokeEvent.from_call(lambda: print("hello world")) + return InvokeEvent(lambda: print("hello world")) # Will call before `demo` call @Pre.configure(target_name="demo") diff --git a/tests/gc.py b/tests/gc.py deleted file mode 100644 index 332ab18..0000000 --- a/tests/gc.py +++ /dev/null @@ -1,17 +0,0 @@ -from saleyo.base.toolchain import GCToolChain, InvokeEvent -from saleyo.mixin import Mixin -from saleyo.operation.intercept import Intercept - - -@Mixin(target=str, toolchain=GCToolChain) -class MixinStr: - @Intercept.configure( - target_name="format", - ) - @staticmethod - def test(_: InvokeEvent): - print(_.argument) - return _ - - -print("HELLO world".format()) diff --git a/tests/gc_test.py b/tests/gc_test.py new file mode 100644 index 0000000..d8e6529 --- /dev/null +++ b/tests/gc_test.py @@ -0,0 +1,12 @@ +from saleyo import Mixin, GCToolChain, Arguments, Pre + + +@Mixin(target=str, toolchain=GCToolChain) +class MixinStr: + @Pre.configure(target_name="format") + def pre_format(self, *args) -> Arguments[...]: + print(f"input args: {args}") + return Arguments(self, "python") + + +print("hello world {}".format("saleyo")) diff --git a/tests/intercept.py b/tests/intercept.py index 26aa326..c0ac514 100644 --- a/tests/intercept.py +++ b/tests/intercept.py @@ -12,7 +12,7 @@ class MixinFoo: @Intercept.configure(target_name="demo") @staticmethod def intercept_demo(_: InvokeEvent): - return InvokeEvent.from_call(lambda: print("hello world")) + return InvokeEvent(lambda: print("hello world")) @Pre.configure(target_name="demo") def pre_demo(*arg):