Skip to content

Commit 406c76c

Browse files
committed
Add barebones wiring.py implementation
1 parent 4fa1f46 commit 406c76c

File tree

6 files changed

+246
-1
lines changed

6 files changed

+246
-1
lines changed

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,29 @@
1-
# Injected
1+
# Python Injection Framework (PIF)
2+
3+
A simple Python dependency injection framework.
4+
5+
## Usage
6+
7+
**This project is under active development. The following example does not represent the final state for the project.**
8+
9+
```python
10+
from pif import wiring, providers
11+
12+
13+
def my_function(a: str = providers.Singleton[str](lambda: "hello wolrd")):
14+
return a
15+
16+
17+
if __name__ == '__main__':
18+
assert isinstance(my_function(), providers.Singleton)
19+
20+
wiring.wire([__name__])
21+
22+
assert "hello world" == my_function()
23+
```
24+
25+
## Authors
26+
27+
| [![Zac Scott](https://avatars.githubusercontent.com/u/38968222)](https://github.com/scottzach1) |
28+
|:------------------------------------------------------------------------------------------------|
29+
| [Zac Scott (scottzach1)](https://github.com/scottzach1) |

pif/__init__.py

Whitespace-only changes.

pif/providers.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# _ _ _ _
2+
# ___ ___ ___ | |_| |_ ______ _ ___| |__ / |
3+
# / __|/ __/ _ \| __| __|_ / _` |/ __| '_ \| |
4+
# \__ \ (_| (_) | |_| |_ / / (_| | (__| | | | |
5+
# |___/\___\___/ \__|\__/___\__,_|\___|_| |_|_|
6+
#
7+
# Zac Scott (github.com/scottzach1)
8+
#
9+
# https://github.com/scottzach1/python-injector-framework
10+
11+
import abc
12+
from typing import Callable
13+
14+
15+
class Provider[T](abc.ABC):
16+
"""
17+
Signposts something that can be injected.
18+
"""
19+
20+
@abc.abstractmethod
21+
def __call__(self, *args, **kwargs) -> T:
22+
"""
23+
Evaluate the value to provide.
24+
"""
25+
26+
27+
class ExistingSingleton[T](Provider):
28+
"""
29+
Provide an existing object instance.
30+
"""
31+
32+
def __init__(self, t: T):
33+
self.t = t
34+
35+
def __call__(self) -> T:
36+
return self.t
37+
38+
39+
UNSET = object()
40+
41+
42+
class Singleton[T](Provider):
43+
"""
44+
Provide a singleton instance.
45+
"""
46+
47+
def __init__(self, func: Callable[[...], T], *args, **kwargs):
48+
self._func = func
49+
self._args = args
50+
self._kwargs = kwargs
51+
self._result = UNSET
52+
53+
def __call__(self) -> T:
54+
if self._result is UNSET:
55+
self._result = self._func(*self._args, **self._kwargs)
56+
return self._result

pif/wiring.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# _ _ _ _
2+
# ___ ___ ___ | |_| |_ ______ _ ___| |__ / |
3+
# / __|/ __/ _ \| __| __|_ / _` |/ __| '_ \| |
4+
# \__ \ (_| (_) | |_| |_ / / (_| | (__| | | | |
5+
# |___/\___\___/ \__|\__/___\__,_|\___|_| |_|_|
6+
#
7+
# Zac Scott (github.com/scottzach1)
8+
#
9+
# https://github.com/scottzach1/python-injector-framework
10+
11+
import functools
12+
import importlib
13+
import inspect
14+
import types
15+
from typing import Any, Callable
16+
17+
from pif import providers
18+
19+
20+
def patch_args_decorator[T: Callable](func: T, patched_kwargs: dict[str, Any]) -> T:
21+
"""
22+
Get a decorated copy of `func` with patched arguments.
23+
24+
TODO(scottzach1) - add support for positional kwargs.
25+
26+
:param func: to decorate.
27+
:param patched_kwargs: the kwargs to patch.
28+
:return: the decorated function.
29+
"""
30+
31+
@functools.wraps(func)
32+
def wrapper(*args, **kwargs):
33+
patched_kwargs.update(kwargs)
34+
return func(*args, **patched_kwargs)
35+
36+
wrapper._patched_func = func
37+
return wrapper
38+
39+
40+
def is_patched(func: Callable | types.FunctionType) -> bool:
41+
"""
42+
Checks if a function has been "patched" by the `patch_args_decorator`
43+
44+
:param func: the function to check.
45+
:return: True if patched, False otherwise.
46+
"""
47+
return hasattr(func, "_patched_func")
48+
49+
50+
def patch_method[T: Callable | types.FunctionType](func: T) -> T:
51+
"""
52+
Return a "patched" version of the method provided.
53+
54+
If no values required patching, the provided function will be returned unchanged..
55+
56+
:param func: to patch default values.
57+
:return: a "patched" version of the method provided.
58+
"""
59+
patched_args = {}
60+
61+
for name, value in inspect.signature(func).parameters.items():
62+
if value.kind == inspect.Parameter.POSITIONAL_ONLY:
63+
continue # TODO(scottzach1) Add support for non keyword arguments.
64+
65+
if isinstance(value.default, providers.Provider):
66+
patched_args[name] = value.default()
67+
68+
if patched_args:
69+
return patch_args_decorator(func, patched_args)
70+
71+
return func
72+
73+
74+
def unpatch_method[T: Callable | types.FunctionType](func: T) -> T:
75+
"""
76+
Get an "unpatched" copy of a method.
77+
78+
If the value was not patched, the provided function will be returned unchanged.
79+
80+
:param func: the function to unpatch.
81+
:return: the unpatched provided function.
82+
"""
83+
return getattr(func, "_patched_func", func)
84+
85+
86+
def wire(modules: list[types.ModuleType | str]) -> None:
87+
"""
88+
Patch all methods in the module containing `Provide` default arguments.
89+
90+
:param modules: list of modules to wire.
91+
"""
92+
for module in modules:
93+
if isinstance(module, str):
94+
module = importlib.import_module(module)
95+
96+
for name, obj in inspect.getmembers(module):
97+
if inspect.isfunction(obj):
98+
if obj is not (patched := patch_method(obj)):
99+
setattr(module, name, patched)
100+
elif inspect.isclass(obj):
101+
for method_name, method in inspect.getmembers(obj, inspect.isfunction):
102+
if method is not (patched := patch_method(method)):
103+
setattr(obj, method_name, patched)
104+
105+
106+
def unwire(modules: list[types.ModuleType]) -> None:
107+
"""
108+
Unpatch all methods in the module containing `Provide` default arguments.
109+
110+
:param modules: list of modules to wire.
111+
"""
112+
for module in modules:
113+
if isinstance(module, str):
114+
module = importlib.import_module(module)
115+
116+
for name, obj in inspect.getmembers(module):
117+
if inspect.isfunction(obj):
118+
if obj is not (unpatched := unpatch_method(obj)):
119+
setattr(module, name, unpatched)
120+
elif inspect.isclass(obj):
121+
for method_name, method in inspect.getmembers(obj, inspect.isfunction):
122+
if method is not (unpatched := unpatch_method(method)):
123+
setattr(obj, method_name, unpatched)

tests/__init__.py

Whitespace-only changes.

tests/test_wiring.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import inspect
2+
3+
from pif import providers, wiring
4+
5+
provider = providers.Singleton[str](lambda: "hello")
6+
7+
8+
def my_func(a: str = provider):
9+
"""
10+
Our dummy method to test wiring for the module.
11+
"""
12+
return a
13+
14+
15+
def test_patch_kwarg():
16+
"""
17+
Test the very rudimentary wiring logic for a module.
18+
"""
19+
sig_before = inspect.signature(my_func)
20+
doc_before = my_func.__doc__
21+
assert provider == my_func()
22+
assert not wiring.is_patched(my_func)
23+
24+
wiring.wire([__name__])
25+
sig_wired = inspect.signature(my_func)
26+
doc_wired = my_func.__doc__
27+
assert my_func() == "hello"
28+
assert sig_before == sig_wired
29+
assert doc_before == doc_wired
30+
assert wiring.is_patched(my_func)
31+
32+
wiring.unwire([__name__])
33+
sig_unwired = inspect.signature(my_func)
34+
doc_unwired = my_func.__doc__
35+
assert provider == my_func()
36+
assert not wiring.is_patched(my_func)
37+
assert sig_before == sig_unwired
38+
assert doc_before == doc_unwired

0 commit comments

Comments
 (0)