Injectinator 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: Injectinator
3
+ Version: 0.1.0
4
+ Summary: Very simple dependency injection
5
+ Author-email: Chris Read <centurix@gmail.com>
6
+ License: MIT
7
+ Project-URL: homepage, https://github.com/Centurix/injectinator
8
+ Project-URL: repository, https://github.com/Centurix/injectinator
9
+ Keywords: dependency,injection,di,decorator
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Natural Language :: English
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: Microsoft :: Windows
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+
22
+ ![build workflow](https://github.com/Centurix/injectinator/actions/workflows/test.yml/badge.svg)
23
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
24
+ ![uv-managed](https://img.shields.io/badge/uv-managed-blueviolet)
25
+
26
+ INJECTINATOR
27
+ =
28
+
29
+ Very Simple Dependency Injection.
30
+
31
+ Here's the function, just paste it where you need it:
32
+
33
+ ```
34
+ def injectinator(func):
35
+ def wrapper(*args, **kwargs):
36
+ replacements = dict(zip(
37
+ func.__code__.co_varnames[:func.__code__.co_argcount],
38
+ list(([None] * func.__code__.co_argcount) + list(func.__defaults__ or []))[-func.__code__.co_argcount:]
39
+ ))
40
+ for position, default in enumerate(replacements.keys()):
41
+ if position >= len(args):
42
+ if default not in kwargs and isinstance(replacements[default], type):
43
+ kwargs[default] = replacements[default]()
44
+ return func(*args, **kwargs)
45
+ return wrapper
46
+ ```
47
+
48
+ Need some kind of dependency injection but don't want a massive library adding to your
49
+ dependency graph that introduces potential supply chain attacks? Well, this is what you need.
50
+
51
+ This also doesn't need any imports to work. Just paste the script above here's an example of how to use it:
52
+
53
+ ```
54
+ class BaseClass:
55
+ # You don't really need this base class, but for the sake of method contracts it's here
56
+ def print(self):
57
+ ...
58
+
59
+ class InjectedClass(BaseClass):
60
+ # This will be the default injected class if nothing is provided in a call
61
+ def print(self):
62
+ print("Default class")
63
+
64
+
65
+ class SuppliedClass(BaseClass):
66
+ # This will be a supplied class
67
+ def print(self):
68
+ print("Supplied class")
69
+
70
+
71
+ @injectinator
72
+ def test(injected=InjectedClass):
73
+ # Can have as many parameters as you like, note that the class is a reference to the class, not an instance
74
+ injected.print()
75
+
76
+ # Two calls, first creates an instance of InjectedClass and passes it to the function, second supplies an instance of SuppliedClass
77
+ test()
78
+ test(SuppliedClass())
79
+ ```
80
+
81
+ Wait I hear you say: Can't we just create an instance of the class in the function specification like this?
82
+
83
+ ```
84
+ def test(injected=InjectedClass()):
85
+ ...
86
+ ```
87
+
88
+ You can, but the instantiation is performed when Python creates the definition of the function, not at the time you run
89
+ the function. Which means that the object it creates is re-used every time you call the function, which is not desirable.
90
+
91
+ Also, if you need to debug this, you might need to change it to:
92
+
93
+ ```
94
+ import functools
95
+
96
+ def injectinator(func):
97
+ @functools.wraps(func)
98
+ def wrapper(*args, **kwargs):
99
+ ...
100
+ ```
101
+
102
+ This will retain the call stack and debugging easier. But as the script is below, you can add it without imports.
103
+
104
+ Notes on this script
105
+ -
106
+
107
+ - It relies on dunder methods on `func` itself, so these _could_ change in the future. They've all been clumped up together for this reason to make it obvious when it fails because the Python foundation changed their usage of dunders
108
+ - There is no parameter support on the injected class, it could be adjusted to support them
109
+ - All the config for the injection is in the function specification rather than the decorator
110
+ - There's no types. It should have types I guess. Probably added to the two function specifications. Could be lazy and just make it all `Any` but then you'd have to drag in `typing` as an import for this gist
111
+ - I have not timed this. It could be wildly inefficient. There's an iteration enumerating over the function parameter list so its O(n)
112
+ - This script does work with arbitrary length argument specifications like adding `*args` to the end
113
+ - Although it is DI in 11 lines, I've no interest in code golf.
114
+
115
+ Operation
116
+ -
117
+
118
+ 1. Figure out what the default values are for each argument, dump the results in `replacements`
119
+ 1. Skip supplied positional values
120
+ 1. For arguments with a default value that do not have a keyword supplied value and the default is a class type, create an instance and add to the keyword argument dictionary
121
+ 1. Invoke the wrapped function/method with positional and keyword values
@@ -0,0 +1,100 @@
1
+ ![build workflow](https://github.com/Centurix/injectinator/actions/workflows/test.yml/badge.svg)
2
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
3
+ ![uv-managed](https://img.shields.io/badge/uv-managed-blueviolet)
4
+
5
+ INJECTINATOR
6
+ =
7
+
8
+ Very Simple Dependency Injection.
9
+
10
+ Here's the function, just paste it where you need it:
11
+
12
+ ```
13
+ def injectinator(func):
14
+ def wrapper(*args, **kwargs):
15
+ replacements = dict(zip(
16
+ func.__code__.co_varnames[:func.__code__.co_argcount],
17
+ list(([None] * func.__code__.co_argcount) + list(func.__defaults__ or []))[-func.__code__.co_argcount:]
18
+ ))
19
+ for position, default in enumerate(replacements.keys()):
20
+ if position >= len(args):
21
+ if default not in kwargs and isinstance(replacements[default], type):
22
+ kwargs[default] = replacements[default]()
23
+ return func(*args, **kwargs)
24
+ return wrapper
25
+ ```
26
+
27
+ Need some kind of dependency injection but don't want a massive library adding to your
28
+ dependency graph that introduces potential supply chain attacks? Well, this is what you need.
29
+
30
+ This also doesn't need any imports to work. Just paste the script above here's an example of how to use it:
31
+
32
+ ```
33
+ class BaseClass:
34
+ # You don't really need this base class, but for the sake of method contracts it's here
35
+ def print(self):
36
+ ...
37
+
38
+ class InjectedClass(BaseClass):
39
+ # This will be the default injected class if nothing is provided in a call
40
+ def print(self):
41
+ print("Default class")
42
+
43
+
44
+ class SuppliedClass(BaseClass):
45
+ # This will be a supplied class
46
+ def print(self):
47
+ print("Supplied class")
48
+
49
+
50
+ @injectinator
51
+ def test(injected=InjectedClass):
52
+ # Can have as many parameters as you like, note that the class is a reference to the class, not an instance
53
+ injected.print()
54
+
55
+ # Two calls, first creates an instance of InjectedClass and passes it to the function, second supplies an instance of SuppliedClass
56
+ test()
57
+ test(SuppliedClass())
58
+ ```
59
+
60
+ Wait I hear you say: Can't we just create an instance of the class in the function specification like this?
61
+
62
+ ```
63
+ def test(injected=InjectedClass()):
64
+ ...
65
+ ```
66
+
67
+ You can, but the instantiation is performed when Python creates the definition of the function, not at the time you run
68
+ the function. Which means that the object it creates is re-used every time you call the function, which is not desirable.
69
+
70
+ Also, if you need to debug this, you might need to change it to:
71
+
72
+ ```
73
+ import functools
74
+
75
+ def injectinator(func):
76
+ @functools.wraps(func)
77
+ def wrapper(*args, **kwargs):
78
+ ...
79
+ ```
80
+
81
+ This will retain the call stack and debugging easier. But as the script is below, you can add it without imports.
82
+
83
+ Notes on this script
84
+ -
85
+
86
+ - It relies on dunder methods on `func` itself, so these _could_ change in the future. They've all been clumped up together for this reason to make it obvious when it fails because the Python foundation changed their usage of dunders
87
+ - There is no parameter support on the injected class, it could be adjusted to support them
88
+ - All the config for the injection is in the function specification rather than the decorator
89
+ - There's no types. It should have types I guess. Probably added to the two function specifications. Could be lazy and just make it all `Any` but then you'd have to drag in `typing` as an import for this gist
90
+ - I have not timed this. It could be wildly inefficient. There's an iteration enumerating over the function parameter list so its O(n)
91
+ - This script does work with arbitrary length argument specifications like adding `*args` to the end
92
+ - Although it is DI in 11 lines, I've no interest in code golf.
93
+
94
+ Operation
95
+ -
96
+
97
+ 1. Figure out what the default values are for each argument, dump the results in `replacements`
98
+ 1. Skip supplied positional values
99
+ 1. For arguments with a default value that do not have a keyword supplied value and the default is a class type, create an instance and add to the keyword argument dictionary
100
+ 1. Invoke the wrapped function/method with positional and keyword values
@@ -0,0 +1,38 @@
1
+ [project]
2
+ name = "Injectinator"
3
+ version = "0.1.0"
4
+ description = "Very simple dependency injection"
5
+ authors = [
6
+ {name = "Chris Read",email = "centurix@gmail.com"},
7
+ ]
8
+ readme = "README.md"
9
+ requires-python = ">=3.10"
10
+ dependencies = []
11
+ license = {text = "MIT"}
12
+ keywords = ["dependency", "injection", "di", "decorator"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Environment :: Console",
16
+ "Intended Audience :: Developers",
17
+ "Natural Language :: English",
18
+ "Operating System :: POSIX :: Linux",
19
+ "Operating System :: MacOS",
20
+ "Operating System :: Microsoft :: Windows",
21
+ "Programming Language :: Python :: 3",
22
+ "Topic :: Software Development :: Libraries"
23
+ ]
24
+
25
+ [project.urls]
26
+ homepage = "https://github.com/Centurix/injectinator"
27
+ repository = "https://github.com/Centurix/injectinator"
28
+
29
+ [dependency-groups]
30
+ dev = [
31
+ "pytest>=9.0.3",
32
+ ]
33
+
34
+ [tool.setuptools]
35
+ license-files = []
36
+
37
+ [tool.pytest.ini_options]
38
+ pythonpath = [".", "src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: Injectinator
3
+ Version: 0.1.0
4
+ Summary: Very simple dependency injection
5
+ Author-email: Chris Read <centurix@gmail.com>
6
+ License: MIT
7
+ Project-URL: homepage, https://github.com/Centurix/injectinator
8
+ Project-URL: repository, https://github.com/Centurix/injectinator
9
+ Keywords: dependency,injection,di,decorator
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Natural Language :: English
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: Microsoft :: Windows
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+
22
+ ![build workflow](https://github.com/Centurix/injectinator/actions/workflows/test.yml/badge.svg)
23
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
24
+ ![uv-managed](https://img.shields.io/badge/uv-managed-blueviolet)
25
+
26
+ INJECTINATOR
27
+ =
28
+
29
+ Very Simple Dependency Injection.
30
+
31
+ Here's the function, just paste it where you need it:
32
+
33
+ ```
34
+ def injectinator(func):
35
+ def wrapper(*args, **kwargs):
36
+ replacements = dict(zip(
37
+ func.__code__.co_varnames[:func.__code__.co_argcount],
38
+ list(([None] * func.__code__.co_argcount) + list(func.__defaults__ or []))[-func.__code__.co_argcount:]
39
+ ))
40
+ for position, default in enumerate(replacements.keys()):
41
+ if position >= len(args):
42
+ if default not in kwargs and isinstance(replacements[default], type):
43
+ kwargs[default] = replacements[default]()
44
+ return func(*args, **kwargs)
45
+ return wrapper
46
+ ```
47
+
48
+ Need some kind of dependency injection but don't want a massive library adding to your
49
+ dependency graph that introduces potential supply chain attacks? Well, this is what you need.
50
+
51
+ This also doesn't need any imports to work. Just paste the script above here's an example of how to use it:
52
+
53
+ ```
54
+ class BaseClass:
55
+ # You don't really need this base class, but for the sake of method contracts it's here
56
+ def print(self):
57
+ ...
58
+
59
+ class InjectedClass(BaseClass):
60
+ # This will be the default injected class if nothing is provided in a call
61
+ def print(self):
62
+ print("Default class")
63
+
64
+
65
+ class SuppliedClass(BaseClass):
66
+ # This will be a supplied class
67
+ def print(self):
68
+ print("Supplied class")
69
+
70
+
71
+ @injectinator
72
+ def test(injected=InjectedClass):
73
+ # Can have as many parameters as you like, note that the class is a reference to the class, not an instance
74
+ injected.print()
75
+
76
+ # Two calls, first creates an instance of InjectedClass and passes it to the function, second supplies an instance of SuppliedClass
77
+ test()
78
+ test(SuppliedClass())
79
+ ```
80
+
81
+ Wait I hear you say: Can't we just create an instance of the class in the function specification like this?
82
+
83
+ ```
84
+ def test(injected=InjectedClass()):
85
+ ...
86
+ ```
87
+
88
+ You can, but the instantiation is performed when Python creates the definition of the function, not at the time you run
89
+ the function. Which means that the object it creates is re-used every time you call the function, which is not desirable.
90
+
91
+ Also, if you need to debug this, you might need to change it to:
92
+
93
+ ```
94
+ import functools
95
+
96
+ def injectinator(func):
97
+ @functools.wraps(func)
98
+ def wrapper(*args, **kwargs):
99
+ ...
100
+ ```
101
+
102
+ This will retain the call stack and debugging easier. But as the script is below, you can add it without imports.
103
+
104
+ Notes on this script
105
+ -
106
+
107
+ - It relies on dunder methods on `func` itself, so these _could_ change in the future. They've all been clumped up together for this reason to make it obvious when it fails because the Python foundation changed their usage of dunders
108
+ - There is no parameter support on the injected class, it could be adjusted to support them
109
+ - All the config for the injection is in the function specification rather than the decorator
110
+ - There's no types. It should have types I guess. Probably added to the two function specifications. Could be lazy and just make it all `Any` but then you'd have to drag in `typing` as an import for this gist
111
+ - I have not timed this. It could be wildly inefficient. There's an iteration enumerating over the function parameter list so its O(n)
112
+ - This script does work with arbitrary length argument specifications like adding `*args` to the end
113
+ - Although it is DI in 11 lines, I've no interest in code golf.
114
+
115
+ Operation
116
+ -
117
+
118
+ 1. Figure out what the default values are for each argument, dump the results in `replacements`
119
+ 1. Skip supplied positional values
120
+ 1. For arguments with a default value that do not have a keyword supplied value and the default is a class type, create an instance and add to the keyword argument dictionary
121
+ 1. Invoke the wrapped function/method with positional and keyword values
@@ -0,0 +1,8 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/Injectinator.egg-info/PKG-INFO
4
+ src/Injectinator.egg-info/SOURCES.txt
5
+ src/Injectinator.egg-info/dependency_links.txt
6
+ src/Injectinator.egg-info/top_level.txt
7
+ src/injectinator/__init__.py
8
+ tests/test_inject.py
@@ -0,0 +1 @@
1
+ injectinator
@@ -0,0 +1,15 @@
1
+ import functools # This is only needed for debugging, you can remove
2
+
3
+ def injectinator(func):
4
+ @functools.wraps(func) # This is only needed for debugging, you can remove
5
+ def wrapper(*args, **kwargs):
6
+ replacements = dict(zip(
7
+ func.__code__.co_varnames[:func.__code__.co_argcount],
8
+ list(([None] * func.__code__.co_argcount) + list(func.__defaults__ or []))[-func.__code__.co_argcount:]
9
+ ))
10
+ for position, default in enumerate(replacements.keys()):
11
+ if position >= len(args):
12
+ if default not in kwargs and isinstance(replacements[default], type):
13
+ kwargs[default] = replacements[default]()
14
+ return func(*args, **kwargs)
15
+ return wrapper
@@ -0,0 +1,68 @@
1
+ from src.injectinator import injectinator
2
+ from abc import ABC, abstractmethod
3
+
4
+
5
+ # Classes to test injection with
6
+ class BaseClass(ABC):
7
+ @abstractmethod
8
+ def test_method(self, arg1: int = 1) -> int:
9
+ raise NotImplemented("test_method Not implemented")
10
+
11
+ class ClassA(BaseClass):
12
+ def test_method(self, arg1: int = 1) -> int:
13
+ return arg1 + 1
14
+
15
+ class ClassB(BaseClass):
16
+ def test_method(self, arg1: int = 1) -> int:
17
+ return arg1 + 2
18
+
19
+ def test_inject_with_no_arguments_no_return():
20
+ """No arguments, no return"""
21
+ @injectinator
22
+ def function_to_test() -> None:
23
+ pass
24
+
25
+ result = function_to_test()
26
+ assert result is None
27
+
28
+ def test_inject_with_no_arguments_int_return():
29
+ """No arguments, integer return"""
30
+ @injectinator
31
+ def function_to_test() -> int:
32
+ return 1
33
+
34
+ result = function_to_test()
35
+ assert type(result) == int
36
+ assert result == 1
37
+
38
+ def test_inject_with_positional_arguments_int_return():
39
+ """One positional argument, integer return"""
40
+ @injectinator
41
+ def function_to_test(arg1: int) -> int:
42
+ return arg1
43
+
44
+ result = function_to_test(2)
45
+ assert type(result) == int
46
+ assert result == 2
47
+
48
+ def test_inject_with_injected_argument_int_return():
49
+ """Positional argument with injected class and integer return"""
50
+ @injectinator
51
+ def function_to_test(arg1: int, arg2: BaseClass=ClassA) -> int:
52
+ return arg2.test_method(arg1)
53
+
54
+ result = function_to_test(0)
55
+ assert type(result) == int
56
+ assert result == 1
57
+
58
+ result = function_to_test(0, ClassB())
59
+ assert type(result) == int
60
+ assert result == 2
61
+
62
+ result = function_to_test(arg1=0)
63
+ assert type(result) == int
64
+ assert result == 1
65
+
66
+ result = function_to_test(arg1=0, arg2=ClassB())
67
+ assert type(result) == int
68
+ assert result == 2