Skip to content

Commit d5433c1

Browse files
committed
fix: add __qualname__ to WithStore instances so that they play nice when passed as a function to something assuming they are normal functions and have a __qualname__ attribute
1 parent a08e6db commit d5433c1

File tree

5 files changed

+104
-40
lines changed

5 files changed

+104
-40
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## Upcoming
4+
5+
- fix: add `__qualname__`, `__annotations__`, `__module__`, `__defaults__` and `__kwdefaults__` to `Autorun` and `WithStore` instances so that they play nice when passed as a function to something assuming they are normal function having these properties.
6+
37
## Version 0.22.1
48

59
- fix: add `__name__` to `WithStore` instances so that they play nice when passed as a function to something assuming they are functions and have a `__name__` attribute

redux/autorun.py

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class Autorun(
7171
):
7272
"""Run a wrapped function in response to specific state changes in the store."""
7373

74-
def __init__(
74+
def __init__( # noqa: C901, PLR0912
7575
self: Autorun,
7676
*,
7777
store: Store[State, Action, Event],
@@ -88,18 +88,31 @@ def __init__(
8888
self.__name__ = f'Autorun:{func.__name__}'
8989
else:
9090
self.__name__ = f'Autorun:{func}'
91-
self._store = store
92-
self._selector = selector
93-
self._comparator = comparator
94-
self._should_be_called = False
91+
if hasattr(func, '__qualname__'):
92+
self.__qualname__ = f'Autorun:{func.__qualname__}'
93+
else:
94+
self.__qualname__ = f'Autorun:{func}'
9595
signature = inspect.signature(func)
9696
parameters = list(signature.parameters.values())
9797
if parameters and parameters[0].kind in [
9898
inspect.Parameter.POSITIONAL_ONLY,
9999
inspect.Parameter.POSITIONAL_OR_KEYWORD,
100100
]:
101101
parameters = parameters[1:]
102-
self._signature = signature.replace(parameters=parameters)
102+
self.__signature__ = signature.replace(parameters=parameters)
103+
self.__module__ = func.__module__
104+
if (annotations := getattr(func, '__annotations__', None)) is not None:
105+
self.__annotations__ = annotations
106+
if (defaults := getattr(func, '__defaults__', None)) is not None:
107+
self.__defaults__ = defaults
108+
if (kwdefaults := getattr(func, '__kwdefaults__', None)) is not None:
109+
self.__kwdefaults__ = kwdefaults
110+
111+
self._store = store
112+
self._selector = selector
113+
self._comparator = comparator
114+
self._should_be_called = False
115+
103116
if options.keep_ref:
104117
self._func = func
105118
elif inspect.ismethod(func):
@@ -370,18 +383,3 @@ def unsubscribe() -> None:
370383
self._subscriptions.discard(callback_ref)
371384

372385
return unsubscribe
373-
374-
@property
375-
def __signature__(
376-
self: Autorun[
377-
State,
378-
Action,
379-
Event,
380-
SelectorOutput,
381-
ComparatorOutput,
382-
Args,
383-
ReturnType,
384-
],
385-
) -> inspect.Signature:
386-
"""Get the signature of the wrapped function."""
387-
return self._signature

redux/with_state.py

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,29 @@ def __init__(
3131
self.__name__ = f'WithState:{func.__name__}'
3232
else:
3333
self.__name__ = f'WithState:{func}'
34-
self._store = store
35-
self._selector = selector
36-
self._func = func
34+
if hasattr(func, '__qualname__'):
35+
self.__qualname__ = f'WithState:{func.__qualname__}'
36+
else:
37+
self.__qualname__ = f'WithState:{func}'
3738
signature = inspect.signature(func)
3839
parameters = list(signature.parameters.values())
3940
if parameters and parameters[0].kind in [
4041
inspect.Parameter.POSITIONAL_ONLY,
4142
inspect.Parameter.POSITIONAL_OR_KEYWORD,
4243
]:
4344
parameters = parameters[1:]
44-
self._signature = signature.replace(parameters=parameters)
45+
self.__signature__ = signature.replace(parameters=parameters)
46+
self.__module__ = func.__module__
47+
if (annotations := getattr(func, '__annotations__', None)) is not None:
48+
self.__annotations__ = annotations
49+
if (defaults := getattr(func, '__defaults__', None)) is not None:
50+
self.__defaults__ = defaults
51+
if (kwdefaults := getattr(func, '__kwdefaults__', None)) is not None:
52+
self.__kwdefaults__ = kwdefaults
53+
54+
self._store = store
55+
self._selector = selector
56+
self._func = func
4557

4658
def __call__(
4759
self: WithState[
@@ -73,17 +85,3 @@ def __repr__(
7385
) -> str:
7486
"""Return the string representation of the WithState instance."""
7587
return super().__repr__() + f'(func: {self._func})'
76-
77-
@property
78-
def __signature__(
79-
self: WithState[
80-
State,
81-
Action,
82-
Event,
83-
SelectorOutput,
84-
ReturnType,
85-
Args,
86-
],
87-
) -> inspect.Signature:
88-
"""Get the signature of the wrapped function."""
89-
return self._signature

tests/test_autorun.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# ruff: noqa: D100, D101, D102, D103, D104, D107
22
from __future__ import annotations
33

4+
import inspect
45
import re
56
from dataclasses import replace
67
from typing import TYPE_CHECKING, Any, cast
@@ -89,14 +90,15 @@ def _(value: int) -> int:
8990
return value
9091

9192

92-
def test_name(store: StoreType) -> None:
93+
def test_name_attr(store: StoreType) -> None:
9394
"""Test `autorun` decorator name attribute."""
9495

9596
@store.autorun(lambda state: state.value)
9697
def decorated(value: int) -> int:
9798
return value
9899

99100
assert decorated.__name__ == 'Autorun:decorated'
101+
assert decorated.__qualname__ == 'Autorun:test_name_attr.<locals>.decorated'
100102

101103
inline_decorated = store.autorun(lambda state: state.value)(
102104
lambda value: value,
@@ -116,6 +118,67 @@ def __repr__(self) -> str:
116118
assert decorated_instance.__name__ == 'Autorun:Decorated'
117119

118120

121+
def test_signature(store: StoreType) -> None:
122+
"""Test `with_state` decorator `__signature__` attribute."""
123+
124+
@store.autorun(
125+
lambda state: state.value,
126+
options=AutorunOptions(
127+
initial_call=False,
128+
reactive=False,
129+
),
130+
)
131+
def func(
132+
value: int,
133+
some_positional_parameter: str,
134+
some_positional_parameter_with_default: int = 0,
135+
*,
136+
some_keyword_parameter: bool,
137+
some_keyword_parameter_with_default: int = 1,
138+
) -> int:
139+
_ = (
140+
some_positional_parameter,
141+
some_positional_parameter_with_default,
142+
some_keyword_parameter,
143+
some_keyword_parameter_with_default,
144+
)
145+
return value
146+
147+
signature = inspect.signature(func)
148+
assert len(signature.parameters) == 4
149+
150+
assert 'some_positional_parameter' in signature.parameters
151+
assert (
152+
signature.parameters['some_positional_parameter'].default
153+
is inspect.Parameter.empty
154+
)
155+
assert signature.parameters['some_positional_parameter'].annotation == 'str'
156+
157+
assert 'some_positional_parameter_with_default' in signature.parameters
158+
assert signature.parameters['some_positional_parameter_with_default'].default == 0
159+
assert (
160+
signature.parameters['some_positional_parameter_with_default'].annotation
161+
== 'int'
162+
)
163+
164+
assert 'some_keyword_parameter' in signature.parameters
165+
assert (
166+
signature.parameters['some_keyword_parameter'].default
167+
is inspect.Parameter.empty
168+
)
169+
assert signature.parameters['some_keyword_parameter'].annotation == 'bool'
170+
171+
assert 'some_keyword_parameter_with_default' in signature.parameters
172+
assert signature.parameters['some_keyword_parameter_with_default'].default == 1
173+
assert (
174+
signature.parameters['some_keyword_parameter_with_default'].annotation == 'int'
175+
)
176+
177+
assert 'value' not in signature.parameters
178+
179+
assert signature.return_annotation == 'int'
180+
181+
119182
def test_ignore_attribute_error_in_selector(store: StoreType) -> None:
120183
@store.autorun(lambda state: cast('Any', state).non_existing)
121184
def _(_: int) -> None:

tests/test_with_state.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def decorated(value: int) -> int:
6464
return value
6565

6666
assert decorated.__name__ == 'WithState:decorated'
67+
assert decorated.__qualname__ == 'WithState:test_name_attr.<locals>.decorated'
6768

6869
inline_decorated = store.with_state(lambda state: state.value)(
6970
lambda value: value,

0 commit comments

Comments
 (0)