Skip to content

Commit

Permalink
feat: add keep_ref parameter to subscriptions and autoruns, default…
Browse files Browse the repository at this point in the history
…ing to `True`, if set to `False`, the subscription/autorun will not keep a reference to the callback

refacotr: general housekeeping
  • Loading branch information
sassanh committed Mar 14, 2024
1 parent cdfaccf commit 521bb85
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 71 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Version 0.11.0

- feat: add `keep_ref` parameter to subscriptions and autoruns, defaulting to `True`,
if set to `False`, the subscription/autorun will not keep a reference to the callback
- refacotr: general housekeeping

## Version 0.10.7

- fix: autorun now correctly updates its value when the store is updated
Expand Down
44 changes: 22 additions & 22 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "python-redux"
version = "0.10.7"
version = "0.11.0"
description = "Redux implementation for Python"
authors = ["Sassan Haradji <[email protected]>"]
license = "Apache-2.0"
Expand All @@ -17,8 +17,8 @@ optional = true

[tool.poetry.group.dev.dependencies]
poethepoet = "^0.24.4"
pyright = "^1.1.350"
ruff = "^0.2.2"
pyright = "^1.1.354"
ruff = "^0.3.2"

[build-system]
requires = ["poetry-core"]
Expand All @@ -29,7 +29,7 @@ demo = "demo:main"
todo_demo = "todo_demo:main"

[tool.poe.tasks]
lint = "ruff . --unsafe-fixes"
lint = "ruff check . --unsafe-fixes"
typecheck = "pyright -p pyproject.toml ."
sanity = ["typecheck", "lint"]

Expand Down
123 changes: 90 additions & 33 deletions redux/autorun.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# ruff: noqa: D100, D101, D102, D103, D104, D105, D107
from __future__ import annotations

import inspect
import weakref
from asyncio import iscoroutinefunction
from inspect import signature
from types import MethodType
from typing import TYPE_CHECKING, Any, Callable, Generic, cast
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Generic, cast

from redux.basic_types import (
Action,
Expand All @@ -17,6 +18,8 @@
)

if TYPE_CHECKING:
from types import MethodType

from redux.main import Store


Expand All @@ -43,30 +46,104 @@ def __init__( # noqa: PLR0913
self._store = store
self._selector = selector
self._comparator = comparator
self._func = func
if options.keep_ref:
self._func = func
elif inspect.ismethod(func):
self._func = weakref.WeakMethod(func)
else:
self._func = weakref.ref(func)
self._options = options

self._last_selector_result: SelectorOutput | None = None
self._last_comparator_result: ComparatorOutput = cast(
ComparatorOutput,
object(),
)
self._latest_value: AutorunOriginalReturnType | None = options.default_value
self._latest_value: AutorunOriginalReturnType = options.default_value
self._subscriptions: set[
Callable[[AutorunOriginalReturnType], Any]
| weakref.ref[Callable[[AutorunOriginalReturnType], Any]]
] = set()
self._immediate_run = (
not iscoroutinefunction(func)
if options.subscribers_immediate_run is None
else options.subscribers_immediate_run
)

if self._options.initial_run and store._state is not None: # noqa: SLF001
self.check_and_call(store._state) # noqa: SLF001
self._check_and_call(store._state) # noqa: SLF001

store.subscribe(self.check_and_call)
store.subscribe(self._check_and_call)

def check_and_call(self: Autorun, state: State) -> None:
def inform_subscribers(
self: Autorun[
State,
Action,
Event,
SelectorOutput,
ComparatorOutput,
AutorunOriginalReturnType,
],
) -> None:
for subscriber_ in self._subscriptions.copy():
if isinstance(subscriber_, weakref.ref):
subscriber = subscriber_()
if subscriber is None:
self._subscriptions.discard(subscriber_)
continue
else:
subscriber = subscriber_
subscriber(self._latest_value)

def call_func(
self: Autorun[
State,
Action,
Event,
SelectorOutput,
ComparatorOutput,
AutorunOriginalReturnType,
],
selector_result: SelectorOutput,
previous_result: SelectorOutput | None,
func: Callable[
[SelectorOutput, SelectorOutput],
AutorunOriginalReturnType,
]
| Callable[[SelectorOutput], AutorunOriginalReturnType]
| MethodType,
) -> AutorunOriginalReturnType:
if len(signature(func).parameters) == 1:
return cast(
Callable[[SelectorOutput], AutorunOriginalReturnType],
func,
)(selector_result)
return cast(
Callable[
[SelectorOutput, SelectorOutput | None],
AutorunOriginalReturnType,
],
func,
)(selector_result, previous_result)

def _check_and_call(
self: Autorun[
State,
Action,
Event,
SelectorOutput,
ComparatorOutput,
AutorunOriginalReturnType,
],
state: State,
) -> None:
try:
selector_result = self._selector(state)
except AttributeError:
return
func = self._func() if isinstance(self._func, weakref.ref) else self._func
if func is None:
return
if self._comparator is None:
comparator_result = cast(ComparatorOutput, selector_result)
else:
Expand All @@ -75,31 +152,11 @@ def check_and_call(self: Autorun, state: State) -> None:
previous_result = self._last_selector_result
self._last_selector_result = selector_result
self._last_comparator_result = comparator_result
if len(signature(self._func).parameters) == 1:
self._latest_value = cast(
Callable[[SelectorOutput], AutorunOriginalReturnType],
self._func,
)(selector_result)
self._latest_value = self.call_func(selector_result, previous_result, func)
if self._immediate_run:
self.inform_subscribers()
else:
self._latest_value = cast(
Callable[
[SelectorOutput, SelectorOutput | None],
AutorunOriginalReturnType,
],
self._func,
)(
selector_result,
previous_result,
)
for subscriber_ in self._subscriptions.copy():
if isinstance(subscriber_, weakref.ref):
subscriber = subscriber_()
if subscriber is None:
self._subscriptions.discard(subscriber_)
continue
else:
subscriber = subscriber_
subscriber(self._latest_value)
self._store._create_task(cast(Coroutine, self._latest_value)) # noqa: SLF001

def __call__(
self: Autorun[
Expand All @@ -112,7 +169,7 @@ def __call__(
],
) -> AutorunOriginalReturnType:
if self._store._state is not None: # noqa: SLF001
self.check_and_call(self._store._state) # noqa: SLF001
self._check_and_call(self._store._state) # noqa: SLF001
return cast(AutorunOriginalReturnType, self._latest_value)

def __repr__(
Expand Down Expand Up @@ -161,7 +218,7 @@ def subscribe(
keep_ref = self._options.subscribers_keep_ref
if keep_ref:
callback_ref = callback
elif isinstance(callback, MethodType):
elif inspect.ismethod(callback):
callback_ref = weakref.WeakMethod(callback)
else:
callback_ref = weakref.ref(callback)
Expand Down
8 changes: 5 additions & 3 deletions redux/basic_types.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ruff: noqa: A003, D100, D101, D102, D103, D104, D105, D107
from __future__ import annotations

from typing import Any, Callable, Generic, Protocol, TypeAlias, TypeGuard
from typing import Any, Callable, Coroutine, Generic, Protocol, TypeAlias, TypeGuard

from immutable import Immutable
from typing_extensions import TypeVar
Expand All @@ -16,7 +16,7 @@ class BaseEvent(Immutable):


class EventSubscriptionOptions(Immutable):
run_async: bool = True
immediate_run: bool = False
keep_ref: bool = True


Expand Down Expand Up @@ -85,12 +85,14 @@ class CreateStoreOptions(Immutable):
scheduler: Scheduler | None = None
action_middleware: Callable[[BaseAction], Any] | None = None
event_middleware: Callable[[BaseEvent], Any] | None = None
task_creator: Callable[[Coroutine], Any] | None = None


class AutorunOptions(Immutable, Generic[AutorunOriginalReturnType]):
default_value: AutorunOriginalReturnType | None = None
initial_run: bool = True
subscribers_immediate_run: bool = True
keep_ref: bool = True
subscribers_immediate_run: bool | None = None
subscribers_keep_ref: bool = True


Expand Down
Loading

0 comments on commit 521bb85

Please sign in to comment.