reductable-params 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.
- reductable_params-0.1.0/LICENSE +21 -0
- reductable_params-0.1.0/PKG-INFO +64 -0
- reductable_params-0.1.0/README.md +52 -0
- reductable_params-0.1.0/pyproject.toml +48 -0
- reductable_params-0.1.0/setup.cfg +4 -0
- reductable_params-0.1.0/src/reductable_params/__init__.py +6 -0
- reductable_params-0.1.0/src/reductable_params/_reduce.py +23 -0
- reductable_params-0.1.0/src/reductable_params/_reduce_c.pyi +11 -0
- reductable_params-0.1.0/src/reductable_params/_reduce_c.pyx +153 -0
- reductable_params-0.1.0/src/reductable_params/_reduce_py.py +119 -0
- reductable_params-0.1.0/src/reductable_params/abc.py +38 -0
- reductable_params-0.1.0/src/reductable_params/py.typed +0 -0
- reductable_params-0.1.0/src/reductable_params/utils.py +94 -0
- reductable_params-0.1.0/src/reductable_params.egg-info/PKG-INFO +64 -0
- reductable_params-0.1.0/src/reductable_params.egg-info/SOURCES.txt +17 -0
- reductable_params-0.1.0/src/reductable_params.egg-info/dependency_links.txt +1 -0
- reductable_params-0.1.0/src/reductable_params.egg-info/requires.txt +3 -0
- reductable_params-0.1.0/src/reductable_params.egg-info/top_level.txt +1 -0
- reductable_params-0.1.0/tests/test_reduce.py +103 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vizonex
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reductable-params
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: filters arbitrary paremeters before sending them off to be called
|
|
5
|
+
Author-email: Vizonex <VizonexBusiness@gmail.com>
|
|
6
|
+
Project-URL: repository, https://github.com/Vizonex/reductable-params.git
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: typing_extensions; python_version < "3.14"
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# reductable-params
|
|
14
|
+
A low level Function packer & sender inspired by pluggy that is designed for mass sending
|
|
15
|
+
as well as ignoring unneeded parameters before sending to a function allowing for large chainable callbacks to be possible in any configuration order.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from reductable_params import reduce
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test(a: int, b: str | None = None):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
def child(a: int):
|
|
26
|
+
print(f"GOT {a}")
|
|
27
|
+
|
|
28
|
+
def what_is_b(b: str | None):
|
|
29
|
+
print(f"B Is {b}")
|
|
30
|
+
|
|
31
|
+
def main():
|
|
32
|
+
func = reduce(test)
|
|
33
|
+
# defaults can be installed before possibly sending
|
|
34
|
+
# these to a child function.
|
|
35
|
+
data = func.install(1) # {"a": 1, b: None}
|
|
36
|
+
|
|
37
|
+
child_func = reduce(child)
|
|
38
|
+
|
|
39
|
+
# Calling child_func will send itself off.
|
|
40
|
+
# You can customize the data after installing it too.
|
|
41
|
+
# allowing for tons of creatives uses.
|
|
42
|
+
child_func(data) # B Parameter is ignored here.
|
|
43
|
+
# "GOT 1"
|
|
44
|
+
|
|
45
|
+
child_2 = reduce(what_is_b)
|
|
46
|
+
child_2(data)
|
|
47
|
+
# B Is None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
main()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Benchmarks
|
|
55
|
+
I decided to run benchmarks incase mainly to prove that this could be better than using `inspect.signature` alone and better for use on a mass scale when running the `install()` function.
|
|
56
|
+
Note that smaller is better and that the timer is measured in seconds running `bench.py` which I have in this repo incase you want to see for yourself.
|
|
57
|
+
|
|
58
|
+
<img src="install-benchmark.png"/>
|
|
59
|
+
|
|
60
|
+
I'll add a comparison with pluggy in the future as soon as I get around to doing so.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# reductable-params
|
|
2
|
+
A low level Function packer & sender inspired by pluggy that is designed for mass sending
|
|
3
|
+
as well as ignoring unneeded parameters before sending to a function allowing for large chainable callbacks to be possible in any configuration order.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from reductable_params import reduce
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test(a: int, b: str | None = None):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
def child(a: int):
|
|
14
|
+
print(f"GOT {a}")
|
|
15
|
+
|
|
16
|
+
def what_is_b(b: str | None):
|
|
17
|
+
print(f"B Is {b}")
|
|
18
|
+
|
|
19
|
+
def main():
|
|
20
|
+
func = reduce(test)
|
|
21
|
+
# defaults can be installed before possibly sending
|
|
22
|
+
# these to a child function.
|
|
23
|
+
data = func.install(1) # {"a": 1, b: None}
|
|
24
|
+
|
|
25
|
+
child_func = reduce(child)
|
|
26
|
+
|
|
27
|
+
# Calling child_func will send itself off.
|
|
28
|
+
# You can customize the data after installing it too.
|
|
29
|
+
# allowing for tons of creatives uses.
|
|
30
|
+
child_func(data) # B Parameter is ignored here.
|
|
31
|
+
# "GOT 1"
|
|
32
|
+
|
|
33
|
+
child_2 = reduce(what_is_b)
|
|
34
|
+
child_2(data)
|
|
35
|
+
# B Is None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
main()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Benchmarks
|
|
43
|
+
I decided to run benchmarks incase mainly to prove that this could be better than using `inspect.signature` alone and better for use on a mass scale when running the `install()` function.
|
|
44
|
+
Note that smaller is better and that the timer is measured in seconds running `bench.py` which I have in this repo incase you want to see for yourself.
|
|
45
|
+
|
|
46
|
+
<img src="install-benchmark.png"/>
|
|
47
|
+
|
|
48
|
+
I'll add a comparison with pluggy in the future as soon as I get around to doing so.
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "reductable-params"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "filters arbitrary paremeters before sending them off to be called"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Vizonex", email = "VizonexBusiness@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
dependencies = [
|
|
11
|
+
'typing_extensions; python_version<"3.14"'
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["setuptools", "cython", "wheel"]
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.dynamic]
|
|
18
|
+
version = {attr = "reductable_params.__version__"}
|
|
19
|
+
|
|
20
|
+
[tool.setuptools]
|
|
21
|
+
|
|
22
|
+
ext-modules = [
|
|
23
|
+
{name="reductable_params._reduce_c", sources = ["src/reductable_params/_reduce_c.pyx"]}
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[tool.ruff]
|
|
27
|
+
line-length = 79
|
|
28
|
+
target-version = "py310"
|
|
29
|
+
exclude = [
|
|
30
|
+
".git",
|
|
31
|
+
".ruff_cache",
|
|
32
|
+
".venv",
|
|
33
|
+
".vscode",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[tool.ruff.lint]
|
|
37
|
+
select = [
|
|
38
|
+
"E", # pycodestyle errors
|
|
39
|
+
"F", # pyflakes
|
|
40
|
+
"I", # isort (import sorting)
|
|
41
|
+
"RUF", # Ruff-specific rules
|
|
42
|
+
"W", # pycodestyle warnings
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
repository = "https://github.com/Vizonex/reductable-params.git"
|
|
48
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
NO_EXTENSIONS = bool(os.environ.get("REDUCTABLE_PARAMS_NO_EXTENSIONS")) # type: bool
|
|
5
|
+
if sys.implementation.name != "cpython":
|
|
6
|
+
NO_EXTENSIONS = True
|
|
7
|
+
|
|
8
|
+
# isort: off
|
|
9
|
+
if not NO_EXTENSIONS: # pragma: no branch
|
|
10
|
+
try:
|
|
11
|
+
from ._reduce_c import reduce as reduce_c
|
|
12
|
+
|
|
13
|
+
reduce = reduce_c
|
|
14
|
+
except ImportError: # pragma: no cover
|
|
15
|
+
from ._reduce_py import reduce as reduce_py
|
|
16
|
+
|
|
17
|
+
reduce = reduce_py
|
|
18
|
+
|
|
19
|
+
else:
|
|
20
|
+
from ._reduce_py import reduce as reduce_py
|
|
21
|
+
|
|
22
|
+
reduce = reduce_py
|
|
23
|
+
# isort: on
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from collections.abc import Callable as Callable
|
|
2
|
+
from typing import Any, Generic, ParamSpec, TypeVar
|
|
3
|
+
|
|
4
|
+
T = TypeVar("T")
|
|
5
|
+
P = ParamSpec("P")
|
|
6
|
+
|
|
7
|
+
class reduce(Generic[P, T]):
|
|
8
|
+
__wrapped__: Callable[P, T]
|
|
9
|
+
def __init__(self, func: Callable[P, T]) -> None: ...
|
|
10
|
+
def install(self, *args: P.args, **kwargs: P.kwargs) -> dict[str, Any]: ...
|
|
11
|
+
def __call__(self, /, kwds: dict[str, Any]) -> T: ...
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# cython: freethreading_compatible = True
|
|
2
|
+
|
|
3
|
+
from types import GenericAlias
|
|
4
|
+
|
|
5
|
+
cimport cython
|
|
6
|
+
from cpython.dict cimport (
|
|
7
|
+
PyDict_Contains,
|
|
8
|
+
PyDict_Copy,
|
|
9
|
+
PyDict_GetItem,
|
|
10
|
+
PyDict_GetItemWithError,
|
|
11
|
+
PyDict_SetItem
|
|
12
|
+
)
|
|
13
|
+
from cpython.object cimport PyObject, PyObject_Call
|
|
14
|
+
from cpython.set cimport PySet_Contains
|
|
15
|
+
from cpython.tuple cimport (
|
|
16
|
+
PyTuple_GET_ITEM,
|
|
17
|
+
PyTuple_GET_SIZE,
|
|
18
|
+
PyTuple_New
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from .abc import Reducable
|
|
22
|
+
from .utils import varnames
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
cdef extern from "Python.h":
|
|
26
|
+
object PyTuple_GetItemObj "PyTuple_GET_ITEM"(object p, Py_ssize_t pos) noexcept
|
|
27
|
+
|
|
28
|
+
void PyTuple_SetItemPtr "PyTuple_SET_ITEM"(object p, Py_ssize_t pos, PyObject* val) noexcept
|
|
29
|
+
int PyDict_SetItemPtr "PyDict_SetItem"(object p, object key, PyObject* val) except -1
|
|
30
|
+
Py_ssize_t PyDict_GET_SIZE(dict p)
|
|
31
|
+
|
|
32
|
+
# This should be enough callables for any decently sized application.
|
|
33
|
+
DEF REDUCE_FREELIST_SIZE = 250
|
|
34
|
+
|
|
35
|
+
@cython.freelist(REDUCE_FREELIST_SIZE)
|
|
36
|
+
cdef class reduce:
|
|
37
|
+
cdef:
|
|
38
|
+
public object __wrapped__
|
|
39
|
+
dict _defaults
|
|
40
|
+
str _name
|
|
41
|
+
Py_ssize_t _nargs, _nparams
|
|
42
|
+
tuple _optional
|
|
43
|
+
tuple _params
|
|
44
|
+
frozenset _param_set
|
|
45
|
+
tuple _required
|
|
46
|
+
|
|
47
|
+
__class_getitem__ = classmethod(GenericAlias)
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
object func
|
|
52
|
+
) -> None:
|
|
53
|
+
cdef tuple required
|
|
54
|
+
cdef dict optional
|
|
55
|
+
# The only bottlekneck is here when ititalizing althought this part is not planned to be benchmarked.
|
|
56
|
+
required, optional = varnames(func)
|
|
57
|
+
|
|
58
|
+
if name := getattr(func, "__name__", None):
|
|
59
|
+
self._name = f"{name}()"
|
|
60
|
+
else:
|
|
61
|
+
self._name = "function"
|
|
62
|
+
|
|
63
|
+
self.__wrapped__ = func
|
|
64
|
+
self._defaults = optional
|
|
65
|
+
self._nargs = len(required)
|
|
66
|
+
self._optional = tuple(optional.keys())
|
|
67
|
+
self._params = required + self._optional
|
|
68
|
+
self._param_set = frozenset(self._params)
|
|
69
|
+
self._nparams = len(self._params)
|
|
70
|
+
self._required = required
|
|
71
|
+
|
|
72
|
+
def install(self, *args, **kwargs):
|
|
73
|
+
r"""Simillar to `inspect.BoundArguments` but a little bit faster,
|
|
74
|
+
it is based off CPython's getargs.c's algorythms, this will also attempt to
|
|
75
|
+
install defaults if any are needed. However this does not allow arbitrary
|
|
76
|
+
arguments to be passed through. Instead, this should primarly be
|
|
77
|
+
used for writing callback utilities that require a parent function's signature.
|
|
78
|
+
|
|
79
|
+
:raises TypeError: if argument parsing fails or has a argument that overlaps in either args or kwargs.
|
|
80
|
+
"""
|
|
81
|
+
# Mimics checks from vgetargskeywordsfast_impl in getargs.c
|
|
82
|
+
cdef Py_ssize_t nargs = PyTuple_GET_SIZE(args)
|
|
83
|
+
cdef Py_ssize_t ntotal = nargs + PyDict_GET_SIZE(kwargs)
|
|
84
|
+
cdef Py_ssize_t n
|
|
85
|
+
cdef frozenset params = self._param_set
|
|
86
|
+
cdef object k, v
|
|
87
|
+
cdef dict output
|
|
88
|
+
|
|
89
|
+
if ntotal < self._nargs:
|
|
90
|
+
raise TypeError(f"Not enough params in {self._name}")
|
|
91
|
+
|
|
92
|
+
elif ntotal > self._nparams:
|
|
93
|
+
raise TypeError(
|
|
94
|
+
"%.200s takes at most %d %sargument%s (%i given)" % (
|
|
95
|
+
self._name,
|
|
96
|
+
self._nparams,
|
|
97
|
+
"keyword" if not self._nargs else "",
|
|
98
|
+
"" if self._nparams == 1 else "s",
|
|
99
|
+
ntotal
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Begin parsing while checking for overlapping arguments and copy off all the defaults.
|
|
104
|
+
|
|
105
|
+
output = PyDict_Copy(self._defaults)
|
|
106
|
+
for n in range(nargs):
|
|
107
|
+
k = self._params[n]
|
|
108
|
+
if PyDict_Contains(kwargs, k):
|
|
109
|
+
# arg present in tuple and dict
|
|
110
|
+
raise TypeError(
|
|
111
|
+
"argument for %.200s given by name ('%s') and position (%d)" % (
|
|
112
|
+
self._name, k, n + 1
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
PyDict_SetItemPtr(output, k, PyTuple_GET_ITEM(args, n))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# replace rest of the defaults with keyword arguments
|
|
119
|
+
for k, v in kwargs.items():
|
|
120
|
+
# force up a keyerror if object is not present in the
|
|
121
|
+
# actual defaults
|
|
122
|
+
if not PySet_Contains(params, k):
|
|
123
|
+
raise KeyError(k)
|
|
124
|
+
PyDict_SetItem(output, k, v)
|
|
125
|
+
|
|
126
|
+
return output
|
|
127
|
+
|
|
128
|
+
@cython.nonecheck(False)
|
|
129
|
+
def __call__(self, dict kwds):
|
|
130
|
+
"""Calls reduction wrapper and calls function
|
|
131
|
+
while ignoring any unwanted arguments. This is useful
|
|
132
|
+
when chaining together callbacks with different function
|
|
133
|
+
formations."""
|
|
134
|
+
|
|
135
|
+
cdef dict kwargs = PyDict_Copy(self._defaults)
|
|
136
|
+
cdef tuple args = PyTuple_New(self._nargs)
|
|
137
|
+
cdef PyObject* v
|
|
138
|
+
cdef Py_ssize_t k
|
|
139
|
+
|
|
140
|
+
for k, key in enumerate(self._required):
|
|
141
|
+
v = PyDict_GetItemWithError(kwds, key)
|
|
142
|
+
PyTuple_SetItemPtr(args, k, v)
|
|
143
|
+
|
|
144
|
+
for key in self._params[self._nargs:]:
|
|
145
|
+
v = PyDict_GetItem(kwds, key)
|
|
146
|
+
if v != NULL:
|
|
147
|
+
PyDict_SetItemPtr(kwargs, key, v)
|
|
148
|
+
|
|
149
|
+
return PyObject_Call(self.__wrapped__, args, kwargs)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
Reducable.register(reduce)
|
|
153
|
+
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from types import GenericAlias
|
|
3
|
+
from typing import Any, Generic
|
|
4
|
+
|
|
5
|
+
from .abc import P, Reducable, T
|
|
6
|
+
from .utils import varnames
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class reduce(Generic[P, T]):
|
|
10
|
+
r"""reduceses arbitrary arguments being sent by only selecting
|
|
11
|
+
ones that make sense on sending. Useful when chaining together
|
|
12
|
+
callbacks where function's children may not need all arguments
|
|
13
|
+
incase callback signatures differ from the parent."""
|
|
14
|
+
|
|
15
|
+
__slots__ = (
|
|
16
|
+
"__wrapped__",
|
|
17
|
+
"_defaults",
|
|
18
|
+
"_name",
|
|
19
|
+
"_nargs",
|
|
20
|
+
"_nparams",
|
|
21
|
+
"_optional",
|
|
22
|
+
"_params",
|
|
23
|
+
"_params_set",
|
|
24
|
+
"_required",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__class_getitem__ = classmethod(GenericAlias)
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
func: Callable[P, T],
|
|
32
|
+
) -> None:
|
|
33
|
+
# if for some reason inspect wants to grab it, let it do so...
|
|
34
|
+
required, optional = varnames(func)
|
|
35
|
+
|
|
36
|
+
if name := getattr(func, "__name__", None):
|
|
37
|
+
self._name = f"{name}()"
|
|
38
|
+
else:
|
|
39
|
+
self._name = "function"
|
|
40
|
+
|
|
41
|
+
self.__wrapped__ = func
|
|
42
|
+
self._defaults = optional
|
|
43
|
+
self._nargs = len(required)
|
|
44
|
+
self._optional = tuple(optional.keys())
|
|
45
|
+
self._params = required + self._optional
|
|
46
|
+
self._nparams = len(self._params)
|
|
47
|
+
self._params_set = frozenset(self._params)
|
|
48
|
+
self._required = required
|
|
49
|
+
|
|
50
|
+
def install(self, *args: P.args, **kwargs: P.kwargs) -> dict[str, Any]:
|
|
51
|
+
r"""Simillar to `inspect.BoundArguments` but a little bit faster,
|
|
52
|
+
it is based off CPython's getargs.c's algorythms, this will also
|
|
53
|
+
attemptto install defaults if any are needed. However this does not
|
|
54
|
+
allow arbitrary arguments to be passed through. Instead, this should
|
|
55
|
+
primarlybe used for writing callback utilities that require a parent
|
|
56
|
+
function's signature.
|
|
57
|
+
|
|
58
|
+
:raises TypeError: if argument parsing fails or has a argument that
|
|
59
|
+
overlaps in either args or kwargs.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
# Mimics checks from vgetargskeywordsfast_impl in getargs.c
|
|
63
|
+
ntotal = len(args) + len(kwargs)
|
|
64
|
+
if ntotal < self._nargs:
|
|
65
|
+
raise TypeError(f"Not enough params in {self._name}")
|
|
66
|
+
|
|
67
|
+
elif ntotal > self._nparams:
|
|
68
|
+
raise TypeError(
|
|
69
|
+
"%.200s takes at most %d %sargument%s (%i given)"
|
|
70
|
+
% (
|
|
71
|
+
self._name,
|
|
72
|
+
self._nparams,
|
|
73
|
+
"keyword" if not self._nargs else "",
|
|
74
|
+
"" if self._nparams == 1 else "s",
|
|
75
|
+
ntotal,
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Begin parsing while checking for overlapping arguments and copy off
|
|
80
|
+
# all the defaults.
|
|
81
|
+
|
|
82
|
+
output = self._defaults.copy()
|
|
83
|
+
for n, v in enumerate(args):
|
|
84
|
+
k = self._params[n]
|
|
85
|
+
if k in kwargs:
|
|
86
|
+
# arg present in tuple and dict
|
|
87
|
+
raise TypeError(
|
|
88
|
+
"argument for %.200s given by name ('%s') and position "
|
|
89
|
+
"(%d)"
|
|
90
|
+
% (self._name, k, n + 1)
|
|
91
|
+
)
|
|
92
|
+
output[k] = v
|
|
93
|
+
|
|
94
|
+
# replace rest of the defaults with keyword arguments
|
|
95
|
+
for k, v in kwargs.items():
|
|
96
|
+
# force up a keyerror if object is somehow
|
|
97
|
+
# not present in the actual defaults
|
|
98
|
+
if k not in self._params_set:
|
|
99
|
+
raise KeyError(k)
|
|
100
|
+
output[k] = v
|
|
101
|
+
return output
|
|
102
|
+
|
|
103
|
+
def __call__(self, kwds: dict[str, Any]) -> T:
|
|
104
|
+
"""Calls reduction wrapper and calls function
|
|
105
|
+
while ignoring any unwanted arguments. This is useful
|
|
106
|
+
when chaining together callbacks with different function
|
|
107
|
+
formations."""
|
|
108
|
+
|
|
109
|
+
kwargs = self._defaults.copy()
|
|
110
|
+
args = [kwds[key] for key in self._required]
|
|
111
|
+
|
|
112
|
+
for k in self._params[self._nargs :]:
|
|
113
|
+
if k in kwargs:
|
|
114
|
+
kwargs[k] = kwds[k]
|
|
115
|
+
|
|
116
|
+
return self.__wrapped__(*args, **kwargs)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
Reducable.register(reduce)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any, Generic, TypeVar
|
|
5
|
+
|
|
6
|
+
if sys.version_info < (3, 10):
|
|
7
|
+
from typing_extensions import ParamSpec
|
|
8
|
+
else:
|
|
9
|
+
from typing import ParamSpec
|
|
10
|
+
|
|
11
|
+
if sys.version_info < (3, 13):
|
|
12
|
+
from typing_extensions import TypeIs
|
|
13
|
+
else:
|
|
14
|
+
from typing import TypeIs
|
|
15
|
+
|
|
16
|
+
T = TypeVar("T")
|
|
17
|
+
P = ParamSpec("P")
|
|
18
|
+
|
|
19
|
+
# Mostly added for pytest's sake but also to prevent breaking
|
|
20
|
+
# if cython is utilized with reducable params but a python version
|
|
21
|
+
# is exposed sort of scenario.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Reducable(Generic[P, T], ABC):
|
|
25
|
+
__wrapped__: Callable[P, T]
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def __init__(self, func: Callable[P, T]) -> None: ...
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def install(self, *args: P.args, **kwargs: P.kwargs) -> dict[str, Any]: ...
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def __call__(self, /, kwds: dict[str, Any]) -> T: ...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def is_reducable(obj: object) -> TypeIs[Reducable]:
|
|
36
|
+
"""Used for inspecting to see if a type belongs to a `reduce`
|
|
37
|
+
class-like object"""
|
|
38
|
+
return isinstance(obj, Reducable)
|
|
File without changes
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import sys
|
|
3
|
+
from types import CodeType
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
_PYPY = hasattr(sys, "pypy_version_info")
|
|
7
|
+
|
|
8
|
+
# Forked from pluggy with my own modification.
|
|
9
|
+
# This is also a PR/idea based off this that I have for pluggy to try.
|
|
10
|
+
# SEE: https://github.com/pytest-dev/pluggy/pull/659
|
|
11
|
+
|
|
12
|
+
_POSITONAL_ONLY = inspect.Parameter.POSITIONAL_ONLY
|
|
13
|
+
_POSITONAL_OR_KW = inspect.Parameter.POSITIONAL_OR_KEYWORD
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _varnames_from_code(
|
|
17
|
+
func: object,
|
|
18
|
+
) -> tuple[tuple[str, ...], dict[str, Any]]:
|
|
19
|
+
"""Faster shortcut than needing to parse a function's given signature."""
|
|
20
|
+
code: CodeType = getattr(func, "__code__")
|
|
21
|
+
args = code.co_varnames[: code.co_argcount]
|
|
22
|
+
|
|
23
|
+
if defaults := getattr(func, "__defaults__", None):
|
|
24
|
+
index = -len(defaults)
|
|
25
|
+
return args[:index], dict(zip(args[index:], defaults))
|
|
26
|
+
else:
|
|
27
|
+
return args, {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _varnames_from_signature(
|
|
31
|
+
func: object,
|
|
32
|
+
) -> tuple[tuple[str, ...], dict[str, Any]]:
|
|
33
|
+
"""extracts from a function's given signature but is slightly slower"""
|
|
34
|
+
sig = inspect.signature(func)
|
|
35
|
+
parameters = sig.parameters
|
|
36
|
+
required_args = [
|
|
37
|
+
k for k, v in parameters.items() if v.kind == _POSITONAL_ONLY
|
|
38
|
+
]
|
|
39
|
+
optional = {
|
|
40
|
+
k: v for k, v in parameters.items() if v.kind == _POSITONAL_OR_KW
|
|
41
|
+
}
|
|
42
|
+
return tuple(required_args), optional
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def varnames(func: object):
|
|
46
|
+
"""Return tuple of positional and keywrord argument names along with
|
|
47
|
+
defaults for a function, method, class or callable.
|
|
48
|
+
|
|
49
|
+
In case of a class, its ``__init__`` method is considered.
|
|
50
|
+
For methods the ``self`` parameter is not included.
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
if inspect.isclass(func):
|
|
54
|
+
try:
|
|
55
|
+
func = func.__init__
|
|
56
|
+
except AttributeError: # pragma: no cover - pypy special case
|
|
57
|
+
return (), {}
|
|
58
|
+
elif not inspect.isroutine(func): # callable object?
|
|
59
|
+
try:
|
|
60
|
+
func = getattr(func, "__call__", func)
|
|
61
|
+
except Exception: # pragma: no cover - pypy special case
|
|
62
|
+
return (), {}
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
# func MUST be a function or method here or we won't parse any args.
|
|
66
|
+
func = func.__func__ if inspect.ismethod(func) else func
|
|
67
|
+
if hasattr(func, "__code__") and inspect.isroutine(func):
|
|
68
|
+
# Take the optimized approch rather than sit and parse the given
|
|
69
|
+
# signature.
|
|
70
|
+
args, kwargs = _varnames_from_code(
|
|
71
|
+
func
|
|
72
|
+
if not hasattr(func, "__wrapped__")
|
|
73
|
+
else inspect.unwrap(func)
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
# Fallback
|
|
77
|
+
args, kwargs = _varnames_from_signature(func)
|
|
78
|
+
except TypeError: # pragma: no cover
|
|
79
|
+
return (), {}
|
|
80
|
+
|
|
81
|
+
# strip any implicit instance arg
|
|
82
|
+
# pypy3 uses "obj" instead of "self" for default dunder methods
|
|
83
|
+
if not _PYPY:
|
|
84
|
+
implicit_names: tuple[str, ...] = ("self",)
|
|
85
|
+
else: # pragma: no cover
|
|
86
|
+
implicit_names = ("self", "obj")
|
|
87
|
+
if args:
|
|
88
|
+
qualname: str = getattr(func, "__qualname__", "")
|
|
89
|
+
if inspect.ismethod(func) or (
|
|
90
|
+
"." in qualname and args[0] in implicit_names
|
|
91
|
+
):
|
|
92
|
+
args = args[1:]
|
|
93
|
+
|
|
94
|
+
return args, kwargs
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reductable-params
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: filters arbitrary paremeters before sending them off to be called
|
|
5
|
+
Author-email: Vizonex <VizonexBusiness@gmail.com>
|
|
6
|
+
Project-URL: repository, https://github.com/Vizonex/reductable-params.git
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: typing_extensions; python_version < "3.14"
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# reductable-params
|
|
14
|
+
A low level Function packer & sender inspired by pluggy that is designed for mass sending
|
|
15
|
+
as well as ignoring unneeded parameters before sending to a function allowing for large chainable callbacks to be possible in any configuration order.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from reductable_params import reduce
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test(a: int, b: str | None = None):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
def child(a: int):
|
|
26
|
+
print(f"GOT {a}")
|
|
27
|
+
|
|
28
|
+
def what_is_b(b: str | None):
|
|
29
|
+
print(f"B Is {b}")
|
|
30
|
+
|
|
31
|
+
def main():
|
|
32
|
+
func = reduce(test)
|
|
33
|
+
# defaults can be installed before possibly sending
|
|
34
|
+
# these to a child function.
|
|
35
|
+
data = func.install(1) # {"a": 1, b: None}
|
|
36
|
+
|
|
37
|
+
child_func = reduce(child)
|
|
38
|
+
|
|
39
|
+
# Calling child_func will send itself off.
|
|
40
|
+
# You can customize the data after installing it too.
|
|
41
|
+
# allowing for tons of creatives uses.
|
|
42
|
+
child_func(data) # B Parameter is ignored here.
|
|
43
|
+
# "GOT 1"
|
|
44
|
+
|
|
45
|
+
child_2 = reduce(what_is_b)
|
|
46
|
+
child_2(data)
|
|
47
|
+
# B Is None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
main()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Benchmarks
|
|
55
|
+
I decided to run benchmarks incase mainly to prove that this could be better than using `inspect.signature` alone and better for use on a mass scale when running the `install()` function.
|
|
56
|
+
Note that smaller is better and that the timer is measured in seconds running `bench.py` which I have in this repo incase you want to see for yourself.
|
|
57
|
+
|
|
58
|
+
<img src="install-benchmark.png"/>
|
|
59
|
+
|
|
60
|
+
I'll add a comparison with pluggy in the future as soon as I get around to doing so.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/reductable_params/__init__.py
|
|
5
|
+
src/reductable_params/_reduce.py
|
|
6
|
+
src/reductable_params/_reduce_c.pyi
|
|
7
|
+
src/reductable_params/_reduce_c.pyx
|
|
8
|
+
src/reductable_params/_reduce_py.py
|
|
9
|
+
src/reductable_params/abc.py
|
|
10
|
+
src/reductable_params/py.typed
|
|
11
|
+
src/reductable_params/utils.py
|
|
12
|
+
src/reductable_params.egg-info/PKG-INFO
|
|
13
|
+
src/reductable_params.egg-info/SOURCES.txt
|
|
14
|
+
src/reductable_params.egg-info/dependency_links.txt
|
|
15
|
+
src/reductable_params.egg-info/requires.txt
|
|
16
|
+
src/reductable_params.egg-info/top_level.txt
|
|
17
|
+
tests/test_reduce.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
reductable_params
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from reductable_params._reduce_c import reduce as reduce_c
|
|
6
|
+
from reductable_params._reduce_py import reduce as reduce_py
|
|
7
|
+
from reductable_params.abc import Reducable, is_reducable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture(
|
|
11
|
+
params=[
|
|
12
|
+
(85022, 43727, 128749),
|
|
13
|
+
(14444, 84793, 99237),
|
|
14
|
+
(54194, 7697, 61891),
|
|
15
|
+
(89370, 55869, 145239),
|
|
16
|
+
(81581, 85501, 167082),
|
|
17
|
+
(64190, 90931, 155121),
|
|
18
|
+
(24752, 78572, 103324),
|
|
19
|
+
(31516, 8151, 39667),
|
|
20
|
+
(67001, 96812, 163813),
|
|
21
|
+
(31638, 95508, 127146),
|
|
22
|
+
]
|
|
23
|
+
)
|
|
24
|
+
def addition_cases(request: pytest.FixtureRequest) -> tuple[int, ...]:
|
|
25
|
+
return request.param
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BaseTestReduce:
|
|
29
|
+
reduce: type[Reducable]
|
|
30
|
+
|
|
31
|
+
def make_test_1(self) -> Reducable[[int, str | None], None]:
|
|
32
|
+
def sig(a: str, b: int | None = None):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
return self.reduce(sig)
|
|
36
|
+
|
|
37
|
+
def test_install(self) -> None:
|
|
38
|
+
func = self.make_test_1()
|
|
39
|
+
assert {"a": "1", "b": 2} == func.install(a="1", b=2)
|
|
40
|
+
assert {"a": "1", "b": 2} == func.install("1", b=2)
|
|
41
|
+
assert {"a": "1", "b": 2} == func.install("1", 2)
|
|
42
|
+
assert {"a": "1", "b": None} == func.install(a="1")
|
|
43
|
+
assert {"a": "1", "b": None} == func.install("1")
|
|
44
|
+
|
|
45
|
+
def test_bad_install_too_many_arguments(self):
|
|
46
|
+
func = self.make_test_1()
|
|
47
|
+
with pytest.raises(TypeError):
|
|
48
|
+
# Too Many Arguments
|
|
49
|
+
func.install("1", 2, 3)
|
|
50
|
+
|
|
51
|
+
def test_bad_install_too_little_arguments(self):
|
|
52
|
+
func = self.make_test_1()
|
|
53
|
+
with pytest.raises(BaseException):
|
|
54
|
+
# Too Little Arguments
|
|
55
|
+
func.install()
|
|
56
|
+
|
|
57
|
+
def test_bad_install_overlapping(self):
|
|
58
|
+
func = self.make_test_1()
|
|
59
|
+
with pytest.raises(TypeError):
|
|
60
|
+
# Overlapping tuple and dict keywords
|
|
61
|
+
func.install("1", a="10")
|
|
62
|
+
|
|
63
|
+
def test_calling_returnables(self, addition_cases: tuple[int, ...]):
|
|
64
|
+
def addition_cb(a: int, b: int):
|
|
65
|
+
return a + b
|
|
66
|
+
|
|
67
|
+
a, b, c = addition_cases
|
|
68
|
+
func = self.reduce(addition_cb)
|
|
69
|
+
assert func({"a": a, "b": b}) == c
|
|
70
|
+
|
|
71
|
+
def test_call_raises(self):
|
|
72
|
+
class Problem(Exception):
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
def raise_me():
|
|
76
|
+
raise Problem("problem...")
|
|
77
|
+
|
|
78
|
+
func = self.reduce(raise_me)
|
|
79
|
+
|
|
80
|
+
with pytest.raises(Problem, match=r"problem..."):
|
|
81
|
+
func({})
|
|
82
|
+
|
|
83
|
+
def test_call_with_unwanted_arguments(self):
|
|
84
|
+
def i_require_foo(foo: str):
|
|
85
|
+
assert foo == "SPAM"
|
|
86
|
+
|
|
87
|
+
func = self.reduce(i_require_foo)
|
|
88
|
+
# It's what reduce's job is intended to do.
|
|
89
|
+
# Allow creating pluggy-like systems but at a
|
|
90
|
+
# very low level...
|
|
91
|
+
func({"foo": "SPAM", "extra": "BLAH BLAH BLAH"})
|
|
92
|
+
|
|
93
|
+
def test_is_reducable_check(self):
|
|
94
|
+
assert is_reducable(self.make_test_1())
|
|
95
|
+
assert is_reducable(0xDEAD) is False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class TestPyReduce(BaseTestReduce):
|
|
99
|
+
reduce = reduce_py
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TestCReduce(BaseTestReduce):
|
|
103
|
+
reduce = reduce_c
|