Skip to content

Commit 0ae7cfd

Browse files
author
hyeongsam.kim
committed
cleanup
1 parent e794989 commit 0ae7cfd

File tree

10 files changed

+96
-88
lines changed

10 files changed

+96
-88
lines changed

CHANGELOG.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,41 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7-
## [0.2.0] - 2024-03-19
7+
## [0.3.0] - 2024-12-19
8+
9+
### Changed
10+
- Removed `initialize` and `finalize` methods from the worker thread.
11+
12+
### Added
13+
- Added the following examples:
14+
- `signal_function_slots.py` for demonstrating signal/slot connection with a regular function
15+
- `signal_lambda_slots.py` for demonstrating signal/slot connection with a lambda function
16+
- `stock_core.py` for demonstrating how to configure and communicate with a threaded backend using signal/slot and event queue
17+
- `stock_monitor_console.py` for demonstrating how to configure and communicate with a threaded backend in a command line interface
18+
- `stock_monitor_ui.py` for demonstrating how to configure and communicate with a threaded backend in a GUI.
19+
20+
### Fixed
21+
- Fixed issues with regular function and lambda function connections.
22+
- Fixed issues with the worker thread's event queue and graceful shutdown.
23+
24+
### Note
25+
Next steps before 1.0.0:
26+
- Strengthening Stability
27+
- Resource Cleanup Mechanism/Weak Reference Support:
28+
- Consider supporting weak references (weakref) that automatically release signal/slot connections when the object is GC'd.
29+
- Handling Slot Return Values in Async/Await Flow:
30+
- A mechanism may be needed to handle the values or exceptions returned by async slots.
31+
Example: If a slot call fails, the emit side can detect and return a callback or future.
32+
- Strengthening Type Hint-Based Verification:
33+
- The functionality of comparing the slot signature and the type of the passed argument when emitting can be further extended.
34+
- Read the type hint for the slot function, and if the number or type of the arguments does not match when emitting, raise a warning or exception.
35+
- Consider Additional Features
36+
- One-shot or Limited Connection Functionality:
37+
A "one-shot" slot feature that listens to a specific event only once and then disconnects can be provided.
38+
Example: Add the connect_one_shot method.
39+
Once the event is received, it will automatically disconnect.
40+
41+
## [0.2.0] - 2024-12-6
842

943
### Changed
1044
- Updated minimum Python version requirement to 3.10
File renamed without changes.

examples/signal_function_slots.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
@t_with_signals
2525
class Counter:
26+
"""A simple counter class that emits a signal when its count changes."""
27+
2628
def __init__(self):
2729
self.count = 0
2830

@@ -31,24 +33,31 @@ def count_changed(self):
3133
"""Emitted when the count changes."""
3234

3335
def increment(self):
36+
"""Increment the counter and emit the signal."""
37+
3438
self.count += 1
3539
print(f"Counter incremented to: {self.count}")
3640
self.count_changed.emit(self.count)
3741

3842

3943
def print_value(value):
4044
"""A standalone function acting as a slot."""
45+
4146
print(f"Function Slot received value: {value}")
4247

4348

4449
async def main():
50+
"""Main function to run the signal-function slots example."""
51+
4552
counter = Counter()
4653
# Connect the signal to the standalone function slot
4754
counter.count_changed.connect(print_value)
4855

4956
print("Press Enter to increment counter, or 'q' to quit.")
57+
5058
while True:
5159
line = input("> ")
60+
5261
if line.lower() == "q":
5362
break
5463
counter.increment()

examples/signal_lamba_slots.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
@t_with_signals
2424
class Counter:
25+
"""A simple counter class that emits a signal when its count changes."""
26+
2527
def __init__(self):
2628
self.count = 0
2729

@@ -30,19 +32,25 @@ def count_changed(self):
3032
"""Emitted when the count changes."""
3133

3234
def increment(self):
35+
"""Increment the counter and emit the signal."""
36+
3337
self.count += 1
3438
print(f"Counter incremented to: {self.count}")
3539
self.count_changed.emit(self.count)
3640

3741

3842
async def main():
43+
"""Main function to run the signal-lambda slots example."""
44+
3945
counter = Counter()
4046
# Connect the signal to a lambda slot
4147
counter.count_changed.connect(lambda v: print(f"Lambda Slot received: {v}"))
4248

4349
print("Press Enter to increment counter, or 'q' to quit.")
50+
4451
while True:
4552
line = input("> ")
53+
4654
if line.lower() == "q":
4755
break
4856
counter.increment()

examples/stock_core.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,18 @@ def __init__(self):
4141

4242
self.prices: Dict[str, float] = {
4343
"AAPL": 180.0, # Apple Inc.
44-
# "GOOGL": 140.0, # Alphabet Inc.
45-
# "MSFT": 370.0, # Microsoft Corporation
46-
# "AMZN": 145.0, # Amazon.com Inc.
47-
# "TSLA": 240.0, # Tesla Inc.
44+
"GOOGL": 140.0, # Alphabet Inc.
45+
"MSFT": 370.0, # Microsoft Corporation
46+
"AMZN": 145.0, # Amazon.com Inc.
47+
"TSLA": 240.0, # Tesla Inc.
4848
}
4949
self._desc_lock = threading.RLock()
5050
self._descriptions = {
5151
"AAPL": "Apple Inc.",
52-
# "GOOGL": "Alphabet Inc.",
53-
# "MSFT": "Microsoft Corporation",
54-
# "AMZN": "Amazon.com Inc.",
55-
# "TSLA": "Tesla Inc.",
52+
"GOOGL": "Alphabet Inc.",
53+
"MSFT": "Microsoft Corporation",
54+
"AMZN": "Amazon.com Inc.",
55+
"TSLA": "Tesla Inc.",
5656
}
5757
self.last_prices = self.prices.copy()
5858
self._running = False
@@ -83,8 +83,10 @@ async def on_stopped(self):
8383

8484
logger.info("[StockService][on_stopped] stopped")
8585
self._running = False
86+
8687
if hasattr(self, "_update_task"):
8788
self._update_task.cancel()
89+
8890
try:
8991
await self._update_task
9092
except asyncio.CancelledError:
@@ -104,6 +106,7 @@ async def update_prices(self):
104106
change=((self.prices[code] / self.last_prices[code]) - 1) * 100,
105107
timestamp=time.time(),
106108
)
109+
107110
logger.debug(
108111
"[StockService][update_prices] price_data: %s",
109112
price_data,
@@ -148,13 +151,15 @@ def remove_alert(self):
148151
@t_slot
149152
def on_price_processed(self, price_data: StockPrice):
150153
"""Receive processed stock price data from StockProcessor"""
154+
151155
logger.debug("[StockViewModel][on_price_processed] price_data: %s", price_data)
152156
self.current_prices[price_data.code] = price_data
153157
self.prices_updated.emit(dict(self.current_prices))
154158

155159
@t_slot
156160
def on_alert_triggered(self, code: str, alert_type: str, price: float):
157161
"""Receive alert trigger from StockProcessor"""
162+
158163
self.alerts.append((code, alert_type, price))
159164
self.alert_added.emit(code, alert_type, price)
160165

@@ -163,6 +168,7 @@ def on_alert_settings_changed(
163168
self, code: str, lower: Optional[float], upper: Optional[float]
164169
):
165170
"""Receive alert settings change notification from StockProcessor"""
171+
166172
if lower is None and upper is None:
167173
self.alert_settings.pop(code, None)
168174
else:
@@ -209,19 +215,22 @@ async def on_set_price_alert(
209215
self, code: str, lower: Optional[float], upper: Optional[float]
210216
):
211217
"""Receive price alert setting request from main thread"""
218+
212219
self.price_alerts[code] = (lower, upper)
213220
self.alert_settings_changed.emit(code, lower, upper)
214221

215222
@t_slot
216223
async def on_remove_price_alert(self, code: str):
217224
"""Receive price alert removal request from main thread"""
225+
218226
if code in self.price_alerts:
219227
del self.price_alerts[code]
220228
self.alert_settings_changed.emit(code, None, None)
221229

222230
@t_slot
223231
async def on_price_updated(self, price_data: StockPrice):
224232
"""Receive stock price update from StockService"""
233+
225234
logger.debug("[StockProcessor][on_price_updated] price_data: %s", price_data)
226235

227236
try:
@@ -232,17 +241,22 @@ async def on_price_updated(self, price_data: StockPrice):
232241

233242
async def process_price(self, price_data: StockPrice):
234243
"""Process stock price data"""
244+
235245
logger.debug("[StockProcessor][process_price] price_data: %s", price_data)
246+
236247
try:
237248
if price_data.code in self.price_alerts:
238249
logger.debug(
239250
"[process_price] Process price event loop: %s",
240251
asyncio.get_running_loop(),
241252
)
253+
242254
if price_data.code in self.price_alerts:
243255
lower, upper = self.price_alerts[price_data.code]
256+
244257
if lower and price_data.price <= lower:
245258
self.alert_triggered.emit(price_data.code, "LOW", price_data.price)
259+
246260
if upper and price_data.price >= upper:
247261
self.alert_triggered.emit(price_data.code, "HIGH", price_data.price)
248262

examples/stock_monitor_console.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,25 +68,30 @@ def on_prices_updated(self, prices: Dict[str, StockPrice]):
6868
print("\n(Press Enter to return to menu)")
6969

7070
alerts = []
71+
7172
for code, data in prices.items():
7273
if code in self.view_model.alert_settings:
7374
lower, upper = self.view_model.alert_settings[code]
75+
7476
if lower and data.price <= lower:
7577
alerts.append(
7678
f"{code} price (${data.price:.2f}) below ${lower:.2f}"
7779
)
80+
7881
if upper and data.price >= upper:
7982
alerts.append(
8083
f"{code} price (${data.price:.2f}) above ${upper:.2f}"
8184
)
8285

8386
if alerts:
8487
print("\nAlerts:")
88+
8589
for alert in alerts:
8690
print(alert)
8791

8892
async def process_command(self, command: str):
8993
"""Process command"""
94+
9095
parts = command.strip().split()
9196

9297
if not parts:
@@ -123,6 +128,7 @@ async def process_command(self, command: str):
123128

124129
elif parts[0] == "remove" and len(parts) == 2:
125130
code = parts[1].upper()
131+
126132
if code in self.view_model.alert_settings:
127133
self.view_model.remove_alert.emit(code)
128134
print(f"Alert removed for {code}")
@@ -140,6 +146,7 @@ async def process_command(self, command: str):
140146

141147
async def run(self):
142148
"""CLI execution"""
149+
143150
logger.debug(
144151
"[StockMonitorCLI][run] started current loop: %s %s",
145152
id(asyncio.get_running_loop()),
@@ -152,11 +159,15 @@ async def run(self):
152159

153160
# Connect service.start to processor's started signal
154161
def on_processor_started():
162+
"""Processor started"""
163+
155164
logger.debug("[StockMonitorCLI][run] processor started, starting service")
156165
self.service.start()
157166

158167
# Set processor_started future to True in the main loop
159168
def set_processor_started_true():
169+
"""Set processor started"""
170+
160171
logger.debug(
161172
"[StockMonitorCLI][run] set_processor_started_true current loop: %s %s",
162173
id(asyncio.get_running_loop()),

examples/stock_monitor_simple.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ async def run(self, *args, **kwargs):
4141
await self._tsignal_stopping.wait()
4242
# Clean up
4343
self._running = False
44+
4445
if self._update_task:
4546
self._update_task.cancel()
47+
4648
try:
4749
await self._update_task
4850
except asyncio.CancelledError:
@@ -52,10 +54,13 @@ async def update_loop(self):
5254
"""Update loop"""
5355

5456
count = 0
57+
5558
while self._running:
5659
logger.debug("[Worker] Processing data %d", count)
5760
self.data_processed.emit(count)
61+
5862
count += 1
63+
5964
await asyncio.sleep(1)
6065

6166

examples/stock_monitor_ui.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ def update_prices(self, prices: Dict[str, StockPrice]):
106106
@t_slot
107107
def on_alert_added(self, code: str, alert_type: str, price: float):
108108
"""Update UI when alert is triggered"""
109+
109110
self.alert_label.text = f"ALERT: {code} {alert_type} {price:.2f}"
110111

111112

@@ -201,6 +202,7 @@ def _remove_alert(self, instance):
201202
"""Alert removal button handler"""
202203

203204
code = self.view.stock_spinner.text
205+
204206
if not code:
205207
self.view.alert_label.text = "No stock selected"
206208
return
@@ -230,6 +232,7 @@ async def cleanup(self):
230232
for task in self.tasks:
231233
if not task.done():
232234
task.cancel()
235+
233236
try:
234237
await task
235238
except asyncio.CancelledError:
@@ -240,6 +243,7 @@ async def async_run(self, async_lib=None):
240243
"""Async run"""
241244

242245
self._async_lib = async_lib or asyncio
246+
243247
return await self._async_lib.gather(
244248
self._async_lib.create_task(super().async_run(async_lib=async_lib))
245249
)
@@ -262,6 +266,7 @@ async def main():
262266
for task in app.tasks:
263267
if not task.done():
264268
task.cancel()
269+
265270
try:
266271
await task
267272
except asyncio.CancelledError:

src/tsignal/contrib/patterns/worker/decorators.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,14 @@ def t_with_worker(cls):
3333
class WorkerClass(cls):
3434
"""
3535
Worker class for the worker pattern.
36-
37-
_worker_lock:
38-
A re-entrant lock that protects access to both the event loop (`_tsignal_loop`)
39-
and the worker thread (`_tsignal_thread`). All operations that set or get
40-
`_tsignal_loop` or `_tsignal_thread` must be done within `with self._worker_lock:`.
4136
"""
4237

4338
def __init__(self):
4439
self._tsignal_loop = None
4540
self._tsignal_thread = None
4641

4742
"""
48-
_lifecycle_lock:
43+
_tsignal_lifecycle_lock:
4944
A re-entrant lock that protects worker's lifecycle state (event loop and thread).
5045
All operations that access or modify worker's lifecycle state must be
5146
performed while holding this lock.

0 commit comments

Comments
 (0)