Skip to content

Commit

Permalink
✨ Optimize typing hints and add toolchain
Browse files Browse the repository at this point in the history
  • Loading branch information
H2Sxxa committed Mar 16, 2024
1 parent 9d2d655 commit dd43266
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 40 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class MixinFoo:
# Will intercept the demo method and redirect to `lambda: print("hello world")`
@Intercept.configure(target_name="demo")
@staticmethod
def intercept_demo(_: InvokeEvent[None]):
def intercept_demo(_: InvokeEvent):
return InvokeEvent.from_call(lambda: print("hello world"))

# Will call before `demo` call
Expand Down
88 changes: 71 additions & 17 deletions src/saleyo/base/toolchain.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from dataclasses import dataclass
from inspect import signature
import ctypes as _ctypes
from types import FunctionType
from typing import Any, Callable, Dict, Generic, Optional
from gc import get_referents as _get_referents


from ..base.typing import T, NameSpace
from ..base.typing import P, RT, NameSpace


@dataclass
Expand All @@ -20,6 +21,40 @@ class ToolChain:
tool_hasattr: Callable[[Any, str], bool] = hasattr


GCToolChain = ToolChain(
tool_getattr=lambda _object, _name: _get_referents(_object.__dict__)[0][_name],
tool_hasattr=lambda _object, _name: _name in _get_referents(_object.__dict__)[0],
tool_setattr=lambda _object, _name, _attr: _get_referents(_object.__dict__)[
0
].update({_name: _attr}),
)
"""
GC ToolChain use the `get_referents` functions in `gc` and it can modify some special class.
Notice: There is no guarantee that it can modify any class, and this method is rude and danger, avoid using it in produce environment.
"""


def _cpy_get_dict(_object: Any) -> Dict[str, Any]:
return _ctypes.cast(
id(_object) + type(_object).__dictoffset__, _ctypes.POINTER(_ctypes.py_object)
).contents.value


CPyToolChain = ToolChain(
tool_getattr=lambda _object, _name: _cpy_get_dict(_object)[_name],
tool_hasattr=lambda _object, _name: _name in _cpy_get_dict(_object),
tool_setattr=lambda _object, _name, _attr: _cpy_get_dict(_object).update(
{_name: _attr}
),
)
"""
`CPyToolChain` use the `ctypes` to modify class, it's danger than other default toolchains.
Notice: There is no guarantee that it can modify any class, and this method is rude and danger, avoid using it in produce environment.
"""


class Container:
"""
Container is A Class to keep a namespace and use this namespace to define function / variable / ...
Expand Down Expand Up @@ -49,27 +84,46 @@ def define_function(
)[function_name]


class Arugument(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

def __str__(self) -> str:
return f"Arugument(positional: {self.positional}, keyword: {self.keyword} )"


@dataclass
class InvokeEvent(Generic[T]):
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[..., T]
argument: Dict[str, Any]
target: Callable[P, RT]
argument: Arugument[P]

@staticmethod
def from_call(target: Callable[..., T], *args, **kwargs) -> "InvokeEvent[T]":
argument = {}
function_parameters = signature(target).parameters
arg_names = list(function_parameters.keys())
argument.update({k: v.default for k, v in function_parameters.items()})
for arg in args:
argument[arg_names.pop(0)] = arg
argument.update(kwargs)
return InvokeEvent(target=target, argument=argument)

def invoke(self) -> T:
return self.target(**self.argument)
def from_call(
target: Callable[P, RT],
*args: P.args,
**kwargs: P.kwargs,
) -> "InvokeEvent[P, RT]":
return InvokeEvent(
target=target,
argument=Arugument(
*args,
**kwargs,
),
)

def invoke(self, target: Callable[P, RT]) -> RT:
return target(*self.argument.positional, **self.argument.keyword)

def invoke_target(self) -> RT:
return self.target(*self.argument.positional, **self.argument.keyword)
3 changes: 2 additions & 1 deletion src/saleyo/base/typing.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Any, Dict, List, Type, TypeVar, Union
from typing import Any, Dict, List, ParamSpec, Type, TypeVar, Union

Target = Union[Type, List[Type]]
NameSpace = Dict[str, Any]
RT = TypeVar("RT")
T = TypeVar("T")
P = ParamSpec("P")
4 changes: 2 additions & 2 deletions src/saleyo/mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
class Mixin:
"""
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.
"""

Expand Down
28 changes: 14 additions & 14 deletions src/saleyo/operation/intercept.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
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

from typing import Any, Callable, Generic, Optional, ParamSpec, Type

_PA = ParamSpec("_PA")
_PB = ParamSpec("_PB")
_A = InvokeEvent[_PA, Any]
_B = InvokeEvent[_PB, Any]

class Intercept(
Generic[T, RT], MixinOperation[Callable[[InvokeEvent[T]], InvokeEvent[RT]]]
):

class Intercept(Generic[_PA, _PB], MixinOperation[Callable[[_A[_PA]], _B[_PB]]]):
"""
The `Intercept` allow you to intercept the arguments before invoking target method.
Expand All @@ -18,7 +20,7 @@ class Intercept(

def __init__(
self,
argument: Callable[[InvokeEvent[T]], InvokeEvent[RT]],
argument: Callable[[_A[_PA]], _B[_PB]],
level: int = 1,
target_name: Optional[str] = None,
) -> None:
Expand All @@ -29,7 +31,7 @@ def __init__(
def configure(
level: int = 1,
target_name: Optional[str] = None,
) -> Callable[[Callable[[InvokeEvent[T]], InvokeEvent[RT]]], "Intercept[T, RT]"]:
) -> Callable[[Callable[[_A[_PA]], _B[_PB]]], "Intercept[_PA, _PB]"]:
return lambda argument: Intercept(
argument=argument,
level=level,
Expand All @@ -44,18 +46,16 @@ def mixin(
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,
lambda *args, **kwargs: self.argument(
InvokeEvent.from_call(native_function, *args, **kwargs)
).invoke_target(),
)
5 changes: 2 additions & 3 deletions src/saleyo/operation/overwrite.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from types import MethodType
from typing import Callable, Optional, Type

from ..base.toolchain import ToolChain
Expand All @@ -10,7 +9,7 @@ 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`.
"""

Expand All @@ -27,7 +26,7 @@ def __init__(
@staticmethod
def configure(
target_name: Optional[str] = None,
) -> Callable[[MethodType], "OverWrite"]:
) -> Callable[[Callable], "OverWrite"]:
return lambda argument: OverWrite(
argument=argument,
target_name=target_name,
Expand Down
2 changes: 1 addition & 1 deletion tests/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def func(self):
# Will intercept the demo method and redirect to `lambda: print("hello world")`
@Intercept.configure(target_name="demo")
@staticmethod
def intercept_demo(_: InvokeEvent[None]):
def intercept_demo(_: InvokeEvent):
return InvokeEvent.from_call(lambda: print("hello world"))

# Will call before `demo` call
Expand Down
17 changes: 17 additions & 0 deletions tests/gc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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())
2 changes: 1 addition & 1 deletion tests/intercept.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def demo(self):
class MixinFoo:
@Intercept.configure(target_name="demo")
@staticmethod
def intercept_demo(_: InvokeEvent[None]):
def intercept_demo(_: InvokeEvent):
return InvokeEvent.from_call(lambda: print("hello world"))

@Pre.configure(target_name="demo")
Expand Down

0 comments on commit dd43266

Please sign in to comment.