diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 796b2a9..eeceb75 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ on: jobs: pypi-publish: - name: Test Lib & Publish + name: Publish runs-on: ubuntu-latest permissions: id-token: write diff --git a/README.md b/README.md index 1c814fd..073d41e 100644 --- a/README.md +++ b/README.md @@ -1 +1,92 @@ -# saleyo +# Saleyo + +Saleyo is a lightwight Python AOP framework, easy to use and integrate. + +## Getting Start + +```sh +pip install saleyo +``` + +## Basic Tutorial + +### Declear a `Mixin` class + +```python +from saleyo import Mixin + +class Foo:... + + +@Mixin(target = Foo) +class MixinFoo:... +``` + +### Use `MixinOperation` + +```python +from typing import Any +from saleyo import Mixin +from saleyo.operation import Accessor, OverWrite, Post, Pre, Intercept, InvokeEvent + + +class Foo: + __private = "private varible" + + def demo(self): + pass + + +@Mixin(target=Foo) +class MixinFoo: + # Will add a varible named `__private` to Foo and it has the same address with `_Foo__private` + private: Accessor[str] = Accessor("__private") + + # Will Add the `func` to `Foo` + @OverWrite + def func(self): + print("hello saleyo") + + # Will intercept the demo method and redirect to `lambda: print("hello world")` + @Intercept.configure(target_name="demo") + @staticmethod + def intercept_demo(_: InvokeEvent[None]): + return InvokeEvent.from_call(lambda: print("hello world")) + + # Will call before `demo` call + @Pre.configure(target_name="demo") + def pre_demo(*arg): + print("pre hello world") + + # Will call after `demo` call + @Post.configure(target_name="demo") + def post_demo(*arg): + print("post hello world") + + +foo: Any = ( + Foo() +) # Add the typing hint to avoid the error message from some IDE plugins. + +print(foo.__private) # Also `print(MixinFoo.private.value)` +foo.func() +foo.demo() + +>>> private varible +>>> hello saleyo +>>> hello world +>>> post hello world +>>> pre hello world +``` + +### Custom Operation + +```python +from typing import Any, Type +from saleyo.base.template import MixinOperation +from saleyo.base.toolchain import 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 0d30c9b..ab0f9a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,12 @@ [project] name = "saleyo" -version = "0.1.0" -description = "A Package to do mixin in Python" -authors = [ - {name = "H2Sxxa", email = "h2sxxa0w0@gmail.com"}, -] +version = "0.1.1" +description = "Saleyo is a lightwight Python AOP framework, easy to use and integrate." +authors = [{ name = "H2Sxxa", email = "h2sxxa0w0@gmail.com" }] dependencies = [] -requires-python = ">=3.11" +requires-python = ">=3.8" readme = "README.md" -license = {text = "MIT"} +license = { text = "MIT" } [build-system] requires = ["pdm-backend"] diff --git a/src/saleyo/base/toolchain.py b/src/saleyo/base/toolchain.py index 28f3d65..4296a9a 100644 --- a/src/saleyo/base/toolchain.py +++ b/src/saleyo/base/toolchain.py @@ -51,6 +51,12 @@ def define_function( @dataclass class InvokeEvent(Generic[T]): + """ + 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[..., T] argument: Dict[str, Any] diff --git a/src/saleyo/mixin.py b/src/saleyo/mixin.py index 63f1e69..e9d641e 100644 --- a/src/saleyo/mixin.py +++ b/src/saleyo/mixin.py @@ -7,22 +7,24 @@ class Mixin: """ - A `Mixin` Decorator is used to invoke all the `MixinOperation` in Mixin Class + A `Mixin` Decorator is used to invoke all the `MixinOperation` in Mixin Class. + + If the target is a special class, you should custom the toolchain yourself. + + Allow to have more than one target, but that's not recommended. """ - target_class: List[Type] + target: List[Type] toolchain: ToolChain reverse_level: bool def __init__( self, - target_class: Target, + target: Target, toolchain: ToolChain = ToolChain(), reverse_level: bool = False, ) -> None: - self.target_class = ( - target_class if isinstance(target_class, list) else [target_class] - ) + self.target = target if isinstance(target, list) else [target] self.toolchain = toolchain self.reverse_level = reverse_level @@ -40,7 +42,7 @@ def collect(self, mixin: T) -> T: ) for member in members: - for target in self.target_class: + for target in self.target: member.mixin(target=target, toolchain=self.toolchain) return mixin diff --git a/src/saleyo/operation/hook.py b/src/saleyo/operation/hook.py index c12ba86..2017370 100644 --- a/src/saleyo/operation/hook.py +++ b/src/saleyo/operation/hook.py @@ -6,6 +6,10 @@ class Post(MixinOperation[Callable[[T], 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. + """ + target_name: Optional[str] def __init__( @@ -43,6 +47,9 @@ def post(*args, **kwargs): class Pre(MixinOperation[Callable[..., RT]]): + """ + `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. + """ target_name: Optional[str] def __init__( diff --git a/src/saleyo/operation/intercept.py b/src/saleyo/operation/intercept.py index b3d09c7..54e2859 100644 --- a/src/saleyo/operation/intercept.py +++ b/src/saleyo/operation/intercept.py @@ -11,7 +11,7 @@ class Intercept( """ The `Intercept` allow you to intercept the arguments before invoking target method. - Then, you can handle these arguments in your own functions. + Then, you can handle these arguments in your own function. """ target_name: Optional[str] diff --git a/src/saleyo/operation/overwrite.py b/src/saleyo/operation/overwrite.py index 80ce12b..eae5665 100644 --- a/src/saleyo/operation/overwrite.py +++ b/src/saleyo/operation/overwrite.py @@ -5,16 +5,20 @@ from ..base.template import MixinOperation -class OverWrite(MixinOperation[MethodType]): +class OverWrite(MixinOperation[Callable]): """ OverWrite is rude and it will cover the target method. + + If the target method doesn't exist, overwrite will add overwrite method to target class. + + Try avoid using `OverWrite` with other `OverWrite`. """ target_name: Optional[str] def __init__( self, - argument: MethodType, + argument: Callable, target_name: Optional[str] = None, ) -> None: super().__init__(argument) diff --git a/src/saleyo/operation/processor.py b/src/saleyo/operation/processor.py index 269b53e..c78223f 100644 --- a/src/saleyo/operation/processor.py +++ b/src/saleyo/operation/processor.py @@ -10,6 +10,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. """ module: Optional[ModuleType] diff --git a/tests/__init__.py b/tests/__init__.py index d9879b7..4121273 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1,2 @@ import intercept as intercept +import demo as demo diff --git a/tests/custom.py b/tests/custom.py new file mode 100644 index 0000000..423782e --- /dev/null +++ b/tests/custom.py @@ -0,0 +1,7 @@ +from typing import Any, Type +from saleyo.base.template import MixinOperation +from saleyo.base.toolchain import ToolChain + +class MyOperation(MixinOperation[Any]): + def mixin(self, target: Type, toolchain: ToolChain = ...) -> None: + ... \ No newline at end of file diff --git a/tests/demo.py b/tests/demo.py new file mode 100644 index 0000000..76fc67b --- /dev/null +++ b/tests/demo.py @@ -0,0 +1,46 @@ +from typing import Any +from saleyo import Mixin +from saleyo.operation import Accessor, OverWrite, Post, Pre, Intercept, InvokeEvent + + +class Foo: + __private = "private varible" + + def demo(self): + pass + + +@Mixin(target=Foo) +class MixinFoo: + # Will add a varible named `__private` to Foo and it has the same address with `_Foo__private` + private: Accessor[str] = Accessor("__private") + + # Will Add the `func` to `Foo` + @OverWrite + def func(self): + print("hello saleyo") + + # Will intercept the demo method and redirect to `lambda: print("hello world")` + @Intercept.configure(target_name="demo") + @staticmethod + def intercept_demo(_: InvokeEvent[None]): + return InvokeEvent.from_call(lambda: print("hello world")) + + # Will call before `demo` call + @Pre.configure(target_name="demo") + def pre_demo(*arg): + print("pre hello world") + + # Will call after `demo` call + @Post.configure(target_name="demo") + def post_demo(*arg): + print("post hello world") + + +foo: Any = ( + Foo() +) # Add the typing hint to avoid the error message from some IDE plugins. + +print(foo.__private) # Also `print(MixinFoo.private.value)` +foo.func() +foo.demo() diff --git a/tests/intercept.py b/tests/intercept.py index 1866770..f9c0b12 100644 --- a/tests/intercept.py +++ b/tests/intercept.py @@ -7,7 +7,7 @@ def demo(self): print("goodbye~") -@Mixin(target_class=Foo) +@Mixin(target=Foo) class MixinFoo: @Intercept.configure(target_name="demo") @staticmethod