Skip to content

Commit

Permalink
test: write tests for different features of the api
Browse files Browse the repository at this point in the history
refactor: rename certain names in the api to better reflect their job
refactor: store_snapshot now puts snapshot files in a hierarchical directory structure based on the test module and test name
  • Loading branch information
sassanh committed Mar 17, 2024
1 parent fcc6d6e commit 7d64e10
Show file tree
Hide file tree
Showing 89 changed files with 1,394 additions and 242 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/integration_delivery.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
workflow_dispatch:

env:
PYTHON_VERSION: '3.11'
PYTHON_VERSION: "3.11"

jobs:
dependencies:
Expand Down Expand Up @@ -121,7 +121,7 @@ jobs:
key: poetry-${{ hashFiles('poetry.lock') }}

- name: Test
run: poetry run poe test --cov-report=xml --cov-report=html
run: poetry run poe test

- name: Prepare list of JSON files with mismatching pairs
if: failure()
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## Version 0.12.3

- test: write tests for different features of the api
- refactor: rename certain names in the api to better reflect their job
- refactor: store_snapshot now puts snapshot files in a hierarchical directory structure
based on the test module and test name
- fix: sort JSON keys in `snapshot_store`'s `json_snapshot`
- test: cover most features with tests

## Version 0.12.2

- docs: update path of demos migrated to tests in `README.md`
Expand Down
16 changes: 15 additions & 1 deletion poetry.lock

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

18 changes: 15 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "python-redux"
version = "0.12.2"
version = "0.12.3"
description = "Redux implementation for Python"
authors = ["Sassan Haradji <[email protected]>"]
license = "Apache-2.0"
Expand All @@ -11,6 +11,7 @@ packages = [{ include = "redux" }]
python = "^3.11"
python-immutable = "^1.0.5"
typing-extensions = "^4.9.0"
pytest-timeout = "^2.3.1"

[tool.poetry.group.dev]
optional = true
Expand All @@ -33,7 +34,7 @@ todo_demo = "todo_demo:main"
[tool.poe.tasks]
lint = "ruff check . --unsafe-fixes"
typecheck = "pyright -p pyproject.toml ."
test = "pytest --cov=redux --cov-report=term-missing"
test = "pytest --cov=redux --cov-report=term-missing --cov-report=html --cov-report=xml"
sanity = ["typecheck", "lint", "test"]

[tool.ruff]
Expand All @@ -48,7 +49,7 @@ inline-quotes = "single"
multiline-quotes = "double"

[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"]
"tests/*" = ["S101", "PLR0915"]

[tool.ruff.format]
quote-style = 'single'
Expand All @@ -58,3 +59,14 @@ profile = "black"

[tool.pyright]
exclude = ['typings']

[tool.pytest.ini_options]
log_cli = 1
log_cli_level = 'ERROR'
timeout = 4

[tool.coverage.report]
exclude_also = ["if TYPE_CHECKING:"]

[tool.coverage.run]
omit = ['redux/test.py']
1 change: 1 addition & 0 deletions redux/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Redux-like state management for Python."""

from .basic_types import (
AutorunDecorator,
AutorunOptions,
Expand Down
66 changes: 39 additions & 27 deletions redux/autorun.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,11 @@ def __init__( # noqa: PLR0913
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

store.subscribe(self._check_and_call)
store.subscribe(self._check_and_call, keep_ref=options.keep_ref)

def inform_subscribers(
self: Autorun[
Expand Down Expand Up @@ -142,21 +137,31 @@ def _check_and_call(
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:
comparator_result = self._comparator(state)
if comparator_result != self._last_comparator_result:
previous_result = self._last_selector_result
self._last_selector_result = selector_result
self._last_comparator_result = comparator_result
self._latest_value = self.call_func(selector_result, previous_result, func)
if self._immediate_run:
self.inform_subscribers()
if func:
if self._comparator is None:
comparator_result = cast(ComparatorOutput, selector_result)
else:
self._store._create_task(cast(Coroutine, self._latest_value)) # noqa: SLF001
try:
comparator_result = self._comparator(state)
except AttributeError:
return
if comparator_result != self._last_comparator_result:
previous_result = self._last_selector_result
self._last_selector_result = selector_result
self._last_comparator_result = comparator_result
self._latest_value = self.call_func(
selector_result,
previous_result,
func,
)
if iscoroutinefunction(func):
task = self._store._async_loop.create_task( # noqa: SLF001
cast(Coroutine, self._latest_value),
)
task.add_done_callback(lambda _: self.inform_subscribers())
self._latest_value = cast(AutorunOriginalReturnType, task)
else:
self.inform_subscribers()

def __call__(
self: Autorun[
Expand All @@ -168,8 +173,9 @@ def __call__(
AutorunOriginalReturnType,
],
) -> AutorunOriginalReturnType:
if self._store._state is not None: # noqa: SLF001
self._check_and_call(self._store._state) # noqa: SLF001
state = self._store._state # noqa: SLF001
if state is not None:
self._check_and_call(state)
return cast(AutorunOriginalReturnType, self._latest_value)

def __repr__(
Expand Down Expand Up @@ -209,11 +215,11 @@ def subscribe(
],
callback: Callable[[AutorunOriginalReturnType], Any],
*,
immediate_run: bool | None = None,
initial_run: bool | None = None,
keep_ref: bool | None = None,
) -> Callable[[], None]:
if immediate_run is None:
immediate_run = self._options.subscribers_immediate_run
if initial_run is None:
initial_run = self._options.subscribers_initial_run
if keep_ref is None:
keep_ref = self._options.subscribers_keep_ref
if keep_ref:
Expand All @@ -224,10 +230,16 @@ def subscribe(
callback_ref = weakref.ref(callback)
self._subscriptions.add(callback_ref)

if immediate_run:
if initial_run:
callback(self.value)

def unsubscribe() -> None:
self._subscriptions.discard(callback_ref)
callback = (
callback_ref()
if isinstance(callback_ref, weakref.ref)
else callback_ref
)
if callback is not None:
self._subscriptions.discard(callback)

return unsubscribe
50 changes: 20 additions & 30 deletions redux/basic_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@
from __future__ import annotations

from types import NoneType
from typing import Any, Callable, Coroutine, Generic, Protocol, TypeAlias, TypeGuard
from typing import TYPE_CHECKING, Any, Callable, Generic, Protocol, TypeAlias, TypeGuard

from immutable import Immutable
from typing_extensions import TypeVar

if TYPE_CHECKING:
import asyncio

class BaseAction(Immutable):
...

class BaseAction(Immutable): ...

class BaseEvent(Immutable):
...

class BaseEvent(Immutable): ...


class EventSubscriptionOptions(Immutable):
Expand Down Expand Up @@ -51,16 +52,13 @@ def __init__(self: InitializationActionError, action: BaseAction) -> None:
)


class InitAction(BaseAction):
...
class InitAction(BaseAction): ...


class FinishAction(BaseAction):
...
class FinishAction(BaseAction): ...


class FinishEvent(BaseEvent):
...
class FinishEvent(BaseEvent): ...


def is_complete_reducer_result(
Expand All @@ -76,8 +74,7 @@ def is_state_reducer_result(


class Scheduler(Protocol):
def __call__(self: Scheduler, callback: Callable, *, interval: bool) -> None:
...
def __call__(self: Scheduler, callback: Callable, *, interval: bool) -> None: ...


class CreateStoreOptions(Immutable):
Expand All @@ -86,14 +83,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
async_loop: asyncio.AbstractEventLoop | None = None


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


Expand All @@ -108,8 +105,7 @@ def __call__(
State,
SelectorOutput,
AutorunOriginalReturnType,
]:
...
]: ...


class AutorunDecorator(
Expand All @@ -124,29 +120,25 @@ def __call__(
self: AutorunDecorator,
func: Callable[[SelectorOutput], AutorunOriginalReturnType]
| Callable[[SelectorOutput, SelectorOutput], AutorunOriginalReturnType],
) -> AutorunReturnType[AutorunOriginalReturnType]:
...
) -> AutorunReturnType[AutorunOriginalReturnType]: ...


class AutorunReturnType(
Protocol,
Generic[AutorunOriginalReturnType],
):
def __call__(self: AutorunReturnType) -> AutorunOriginalReturnType:
...
def __call__(self: AutorunReturnType) -> AutorunOriginalReturnType: ...

@property
def value(self: AutorunReturnType) -> AutorunOriginalReturnType:
...
def value(self: AutorunReturnType) -> AutorunOriginalReturnType: ...

def subscribe(
self: AutorunReturnType,
callback: Callable[[AutorunOriginalReturnType], Any],
*,
immediate_run: bool | None = None,
initial_run: bool | None = None,
keep_ref: bool | None = None,
) -> Callable[[], None]:
...
) -> Callable[[], None]: ...


class EventSubscriber(Protocol):
Expand All @@ -156,8 +148,7 @@ def __call__(
handler: EventHandler[Event],
*,
options: EventSubscriptionOptions | None = None,
) -> Callable[[], None]:
...
) -> Callable[[], None]: ...


DispatchParameters: TypeAlias = Action | Event | list[Action | Event]
Expand All @@ -169,8 +160,7 @@ def __call__(
*items: Action | Event | list[Action | Event],
with_state: Callable[[State | None], Action | Event | list[Action | Event]]
| None = None,
) -> None:
...
) -> None: ...


class BaseCombineReducerState(Immutable):
Expand Down
Loading

0 comments on commit 7d64e10

Please sign in to comment.