Skip to content

Commit 32f2c73

Browse files
committed
feat: introduce grace_time_in_seconds parameter to Store to allow a grace period for the store to finish its work before calling cleanup and on_finish
1 parent 714dc21 commit 32f2c73

File tree

8 files changed

+129
-78
lines changed

8 files changed

+129
-78
lines changed

.github/workflows/integration_delivery.yml

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
- uses: actions/checkout@v4
1717
name: Checkout
1818

19-
- name: Save Cached Poetry
19+
- name: Load Cached Poetry
2020
id: cached-poetry
2121
uses: actions/cache@v4
2222
with:
@@ -112,36 +112,23 @@ jobs:
112112
architecture: x64
113113

114114
- name: Load Cached Poetry
115-
id: cached-poetry
116115
uses: actions/cache/restore@v4
116+
id: cached-poetry
117117
with:
118118
path: |
119119
~/.cache
120120
~/.local
121121
key: poetry-${{ hashFiles('poetry.lock') }}
122122

123-
- name: Test
123+
- name: Run Tests
124124
run: poetry run poe test
125125

126-
- name: Prepare list of JSON files with mismatching pairs
127-
if: failure()
128-
run: |
129-
mkdir -p artifacts
130-
for file in $(find tests/ -name "*.mismatch.json"); do
131-
base=${file%.mismatch.json}.json
132-
if [[ -f "$base" ]]; then
133-
echo "$file" >> artifacts/files_to_upload.txt
134-
echo "$base" >> artifacts/files_to_upload.txt
135-
fi
136-
done
137-
138-
- name: Collect Mismatching Store Snapshots
139-
if: failure()
126+
- name: Collect Store Snapshots
140127
uses: actions/upload-artifact@v4
128+
if: always()
141129
with:
142-
name: mismatching-snapshots
143-
path: |
144-
@artifacts/files_to_upload.txt
130+
name: snapshots
131+
path: tests/**/results/**/*.jsonc
145132

146133
- name: Collect HTML Coverage Report
147134
uses: actions/upload-artifact@v4
@@ -163,8 +150,8 @@ jobs:
163150
- dependencies
164151
runs-on: ubuntu-latest
165152
outputs:
166-
version: ${{ steps.extract_version.outputs.version }}
167-
name: ${{ steps.extract_version.outputs.name }}
153+
version: ${{ steps.extract_version.outputs.VERSION }}
154+
name: ${{ steps.extract_version.outputs.NAME }}
168155
steps:
169156
- uses: actions/checkout@v4
170157
name: Checkout
@@ -184,14 +171,41 @@ jobs:
184171
~/.local
185172
key: poetry-${{ hashFiles('poetry.lock') }}
186173

187-
- name: Build
188-
run: poetry build
189-
190174
- name: Extract Version
191175
id: extract_version
192176
run: |
193-
echo "version=$(poetry version --short)" >> "$GITHUB_OUTPUT"
194-
echo "name=$(poetry version | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
177+
echo "VERSION=$(poetry version --short)" >> "$GITHUB_OUTPUT"
178+
echo "NAME=$(poetry version | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
179+
echo "VERSION=$(poetry version --short)"
180+
echo "NAME=$(poetry version | cut -d' ' -f1)"
181+
182+
- name: Extract Version from CHANGELOG.md
183+
id: extract_changelog_version
184+
run: |
185+
VERSION_CHANGELOG=$(sed -n '3 s/## Version //p' CHANGELOG.md)
186+
echo "VERSION_CHANGELOG=$VERSION_CHANGELOG"
187+
if [ "${{ steps.extract_version.outputs.VERSION }}" != "$VERSION_CHANGELOG" ]; then
188+
echo "Error: Version extracted from CHANGELOG.md does not match the version in pyproject.toml"
189+
exit 1
190+
else
191+
echo "Versions are consistent."
192+
fi
193+
194+
- name: Extract Version from Tag
195+
if: startsWith(github.ref, 'refs/tags/v')
196+
id: extract_tag_version
197+
run: |
198+
VERSION_TAG=$(sed 's/^v//' <<< ${{ github.ref_name }})
199+
echo "VERSION_TAG=$VERSION_TAG"
200+
if [ "${{ steps.extract_version.outputs.VERSION }}" != "$VERSION_TAG" ]; then
201+
echo "Error: Version extracted from tag does not match the version in pyproject.toml"
202+
exit 1
203+
else
204+
echo "Versions are consistent."
205+
fi
206+
207+
- name: Build
208+
run: poetry build
195209

196210
- name: Upload wheel
197211
uses: actions/upload-artifact@v4
@@ -240,19 +254,20 @@ jobs:
240254
verbose: true
241255

242256
release:
257+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
243258
name: Release
244259
needs:
245260
- type-check
246261
- lint
247262
- test
248263
- build
249264
- pypi-publish
265+
runs-on: ubuntu-latest
250266
environment:
251267
name: release
252-
runs-on: ubuntu-latest
268+
url: https://pypi.org/p/${{ needs.build.outputs.name }}
253269
permissions:
254270
contents: write
255-
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
256271
steps:
257272
- name: Procure Wheel
258273
uses: actions/download-artifact@v4

CHANGELOG.md

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

3+
## Version 0.14.1
4+
5+
- feat: introduce `grace_time_in_seconds` parameter to `Store` to allow a grace
6+
period for the store to finish its work before calling `cleanup` and `on_finish`
7+
38
## Version 0.14.0
49

510
- refactor: `Store` no longer aggregates changes, it now calls listeners with every

poetry.lock

Lines changed: 22 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "python-redux"
3-
version = "0.14.0"
3+
version = "0.14.1"
44
description = "Redux implementation for Python"
55
authors = ["Sassan Haradji <[email protected]>"]
66
license = "Apache-2.0"
@@ -16,8 +16,8 @@ optional = true
1616

1717
[tool.poetry.group.dev.dependencies]
1818
poethepoet = "^0.24.4"
19-
pyright = "^1.1.354"
20-
ruff = "^0.3.3"
19+
pyright = "^1.1.357"
20+
ruff = "^0.3.5"
2121
pytest = "^8.1.1"
2222
pytest-cov = "^4.1.0"
2323
pytest-timeout = "^2.3.1"

redux/basic_types.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ class BaseEvent(Immutable): ...
4242

4343
class CompleteReducerResult(Immutable, Generic[State, Action, Event]):
4444
state: State
45-
actions: list[Action] | None = None
46-
events: list[Event] | None = None
45+
actions: Sequence[Action] | None = None
46+
events: Sequence[Event] | None = None
4747

4848

4949
ReducerResult = CompleteReducerResult[State, Action, Event] | State
@@ -112,6 +112,7 @@ class CreateStoreOptions(Immutable, Generic[Action, Event]):
112112
event_middlewares: Sequence[EventMiddleware[Event]] = field(default_factory=list)
113113
task_creator: TaskCreator | None = None
114114
on_finish: Callable[[], Any] | None = None
115+
grace_time_in_seconds: float = 1
115116

116117

117118
class AutorunOptions(Immutable, Generic[AutorunOriginalReturnType]):

redux/main.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import queue
88
import weakref
99
from collections import defaultdict
10-
from threading import Lock
10+
from threading import Lock, Thread
1111
from typing import Any, Callable, Generic, cast
1212

1313
from redux.autorun import Autorun
@@ -50,7 +50,6 @@ def __init__(
5050
options: CreateStoreOptions[Action, Event] | None = None,
5151
) -> None:
5252
"""Create a new store."""
53-
self.finished = False
5453
self.store_options = options or CreateStoreOptions()
5554
self.reducer = reducer
5655
self._create_task = self.store_options.task_creator
@@ -125,8 +124,8 @@ def _run_actions(self: Store[State, Action, Event]) -> None:
125124

126125
def _run_event_handlers(self: Store[State, Action, Event]) -> None:
127126
event = self._events.pop(0)
128-
for event_handler_ in self._event_handlers[type(event)].copy():
129-
self._event_handlers_queue.put_nowait((event_handler_, event))
127+
for event_handler in self._event_handlers[type(event)].copy():
128+
self._event_handlers_queue.put_nowait((event_handler, event))
130129

131130
def run(self: Store[State, Action, Event]) -> None:
132131
"""Run the store."""
@@ -137,13 +136,6 @@ def run(self: Store[State, Action, Event]) -> None:
137136

138137
if len(self._events) > 0:
139138
self._run_event_handlers()
140-
if (
141-
self.finished
142-
and self._actions == []
143-
and self._events == []
144-
and not any(worker.is_alive() for worker in self._workers)
145-
):
146-
self.clean_up()
147139

148140
def clean_up(self: Store[State, Action, Event]) -> None:
149141
"""Clean up the store."""
@@ -152,8 +144,6 @@ def clean_up(self: Store[State, Action, Event]) -> None:
152144
self._workers.clear()
153145
self._listeners.clear()
154146
self._event_handlers.clear()
155-
if self.store_options.on_finish:
156-
self.store_options.on_finish()
157147

158148
def dispatch(
159149
self: Store[State, Action, Event],
@@ -225,10 +215,30 @@ def unsubscribe() -> None:
225215

226216
return unsubscribe
227217

218+
def wait_for_store_to_finish(self: Store[State, Action, Event]) -> None:
219+
"""Wait for the store to finish."""
220+
import time
221+
222+
while True:
223+
if (
224+
self._actions == []
225+
and self._events == []
226+
and self._event_handlers_queue.qsize() == 0
227+
):
228+
time.sleep(self.store_options.grace_time_in_seconds)
229+
self._event_handlers_queue.join()
230+
for _ in range(self.store_options.threads):
231+
self._event_handlers_queue.put_nowait(None)
232+
self._event_handlers_queue.join()
233+
self.clean_up()
234+
if self.store_options.on_finish:
235+
self.store_options.on_finish()
236+
break
237+
time.sleep(0.1)
238+
228239
def _handle_finish_event(self: Store[State, Action, Event]) -> None:
229-
for _ in range(self.store_options.threads):
230-
self._event_handlers_queue.put_nowait(None)
231-
self.finished = True
240+
thread = Thread(target=self.wait_for_store_to_finish)
241+
thread.start()
232242

233243
def autorun(
234244
self: Store[State, Action, Event],

redux_pytest/fixtures/snapshot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def __init__(
3939
self.results_dir = path.parent / 'results' / file / test_id.split('::')[-1][5:]
4040
if self.results_dir.exists():
4141
for file in self.results_dir.glob(
42-
'store-*' if override else 'store-*.mismatch.json',
42+
'store-*.jsonc' if override else 'store-*.mismatch.jsonc',
4343
):
4444
file.unlink() # pragma: no cover
4545
self.results_dir.mkdir(parents=True, exist_ok=True)

0 commit comments

Comments
 (0)