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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ from ._reduce import reduce
2
+ from .abc import is_reducable
3
+
4
+ __all__ = ("is_reducable", "reduce")
5
+ __author__ = "Vizonex"
6
+ __version__ = "0.1.0"
@@ -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,3 @@
1
+
2
+ [:python_version < "3.14"]
3
+ typing_extensions
@@ -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