Skip to content

Commit cb665bc

Browse files
author
san-tekart
committed
Enhance code quality and linting across the project
- Updated connection type constants to use uppercase naming convention (e.g., `DirectConnection` to `DIRECT_CONNECTION`). - Improved logging statements for consistency and clarity, using formatted strings. - Added docstrings to classes and methods for better documentation and understanding of the code. - Removed unnecessary imports and simplified code where applicable. - Ensured all async methods are properly defined and documented. - Adjusted test cases to improve readability and maintainability. - Configured pylint to disable specific warnings to streamline code quality checks.
1 parent 51c2291 commit cb665bc

File tree

16 files changed

+254
-105
lines changed

16 files changed

+254
-105
lines changed

docs/api.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,8 @@ Emits the signal with the given arguments.
172172
Enum defining connection types.
173173

174174
#### Values:
175-
- `DirectConnection`: Slot is called directly in the emitting thread
176-
- `QueuedConnection`: Slot is queued in the receiver's event loop
175+
- `DIRECT_CONNECTION`: Slot is called directly in the emitting thread
176+
- `QUEUED_CONNECTION`: Slot is queued in the receiver's event loop
177177

178178
## Usage Examples
179179

@@ -222,14 +222,14 @@ asyncio.run(main())
222222
sender.value_changed.connect(
223223
receiver,
224224
receiver.on_value_changed,
225-
connection_type=TConnectionType.DirectConnection
225+
connection_type=TConnectionType.DIRECT_CONNECTION
226226
)
227227

228228
# Force queued connection
229229
sender.value_changed.connect(
230230
receiver,
231231
receiver.on_value_changed,
232-
connection_type=TConnectionType.QueuedConnection
232+
connection_type=TConnectionType.QUEUED_CONNECTION
233233
)
234234
```
235235

docs/usage.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,18 +75,18 @@ class DataProcessor:
7575
## Connection Types
7676
TSignal supports two types of connections:
7777

78-
### DirectConnection
78+
### DIRECT_CONNECTION
7979
- Signal and slot execute in the same thread
8080
- Slot is called immediately when signal is emitted
8181
```python
82-
signal.connect(receiver, slot, connection_type=TConnectionType.DirectConnection)
82+
signal.connect(receiver, slot, connection_type=TConnectionType.DIRECT_CONNECTION)
8383
```
8484

85-
### QueuedConnection
85+
### QUEUED_CONNECTION
8686
- Signal and slot can execute in different threads
8787
- Slot execution is queued in receiver's event loop
8888
```python
89-
signal.connect(receiver, slot, connection_type=TConnectionType.QueuedConnection)
89+
signal.connect(receiver, slot, connection_type=TConnectionType.QUEUED_CONNECTION)
9090
```
9191

9292
Connection type is automatically determined based on:

pyproject.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ asyncio_default_fixture_loop_scope = "function"
5656
source = ["tsignal"]
5757
branch = true
5858

59+
[tool.pylint]
60+
disable = [
61+
"protected-access",
62+
"too-few-public-methods",
63+
"too-many-statements",
64+
"broad-exception-caught"
65+
]
66+
5967
[tool.coverage.report]
6068
exclude_lines = [
6169
"pragma: no cover",

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

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
1+
"""
2+
Decorator for the worker pattern.
3+
4+
This decorator enhances a class to support a worker pattern, allowing for
5+
asynchronous task processing in a separate thread. It ensures that the
6+
class has the required asynchronous `initialize` and `finalize` methods,
7+
facilitating the management of worker threads and task queues.
8+
"""
9+
110
import asyncio
211
import threading
312
import logging
4-
import time
5-
import sys
613

714
logger = logging.getLogger(__name__)
815

916

1017
def t_with_worker(cls):
18+
"""Decorator for the worker pattern."""
1119
if not asyncio.iscoroutinefunction(getattr(cls, "initialize", None)):
1220
raise TypeError(f"{cls.__name__}.initialize must be an async function")
1321
if not asyncio.iscoroutinefunction(getattr(cls, "finalize", None)):
1422
raise TypeError(f"{cls.__name__}.finalize must be an async function")
1523

1624
class WorkerClass(cls):
25+
"""Worker class for the worker pattern."""
1726
def __init__(self):
1827
self._worker_loop = None
1928
self._worker_thread = None
@@ -22,10 +31,7 @@ def __init__(self):
2231
self._thread = threading.current_thread()
2332
try:
2433
self._loop = asyncio.get_event_loop()
25-
if sys.version_info < (3, 9):
26-
self._stopping = asyncio.Event(loop=self._loop)
27-
else:
28-
self._stopping = asyncio.Event()
34+
self._stopping = asyncio.Event()
2935
except RuntimeError:
3036
self._loop = asyncio.new_event_loop()
3137
asyncio.set_event_loop(self._loop)
@@ -45,9 +51,10 @@ async def _process_queue(self):
4551
except asyncio.TimeoutError:
4652
continue
4753
except Exception as e:
48-
logger.error(f"Error processing queued task: {e}")
54+
logger.error("Error processing queued task: %s", e)
4955

5056
def start(self, *args, **kwargs):
57+
"""Start the worker thread."""
5158
if self._worker_thread:
5259
raise RuntimeError("Worker already started")
5360

@@ -78,12 +85,6 @@ async def run_loop():
7885
if hasattr(self, "finalize"):
7986
await self.finalize()
8087

81-
initial_pending = [
82-
task
83-
for task in asyncio.all_tasks(self._worker_loop)
84-
if task is not asyncio.current_task()
85-
]
86-
8788
# Wait until all callbacks in the current event loop are processed
8889
while self._worker_loop.is_running():
8990
pending_tasks = [
@@ -115,6 +116,7 @@ async def run_loop():
115116
self._worker_thread.start()
116117

117118
def stop(self):
119+
"""Stop the worker thread."""
118120
if (
119121
self._worker_loop
120122
and self._worker_thread
@@ -125,7 +127,8 @@ def stop(self):
125127

126128
if self._worker_thread and self._worker_thread.is_alive():
127129
logger.warning(
128-
f"Worker thread {self._worker_thread.name} did not stop gracefully"
130+
"Worker thread %s did not stop gracefully",
131+
self._worker_thread.name,
129132
)
130133
self._worker_thread = None
131134

src/tsignal/core.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
# Standard library imports
1+
"""
2+
Implementation of the Signal class for tsignal.
3+
4+
Provides signal-slot communication pattern for event handling, supporting both
5+
synchronous and asynchronous operations in a thread-safe manner.
6+
"""
7+
28
import asyncio
39
import functools
410
import logging
@@ -8,11 +14,13 @@
814

915

1016
class TConnectionType(Enum):
11-
DirectConnection = 1
12-
QueuedConnection = 2
17+
"""Connection type for signal-slot connections."""
18+
DIRECT_CONNECTION = 1
19+
QUEUED_CONNECTION = 2
1320

1421

1522
class _SignalConstants:
23+
"""Constants for signal-slot communication."""
1624
FROM_EMIT = "_from_emit"
1725
THREAD = "_thread"
1826
LOOP = "_loop"
@@ -28,10 +36,11 @@ def _wrap_direct_function(func):
2836

2937
@functools.wraps(func)
3038
def wrapper(*args, **kwargs):
39+
"""Wrapper for directly connected functions"""
3140
# Remove FROM_EMIT
3241
kwargs.pop(_SignalConstants.FROM_EMIT, False)
3342

34-
# DirectConnection executes immediately regardless of thread
43+
# DIRECT_CONNECTION executes immediately regardless of thread
3544
if is_coroutine:
3645
try:
3746
loop = asyncio.get_event_loop()
@@ -45,8 +54,8 @@ def wrapper(*args, **kwargs):
4554

4655

4756
class TSignal:
57+
"""Signal class for tsignal."""
4858
def __init__(self):
49-
"""Initialize signal"""
5059
self.connections: List[Tuple[Optional[object], Callable, TConnectionType]] = []
5160

5261
def connect(
@@ -56,7 +65,8 @@ def connect(
5665
if slot is None:
5766
if not callable(receiver_or_slot):
5867
logger.error(
59-
f"Invalid connection attempt - receiver_or_slot is not callable: {receiver_or_slot}"
68+
"Invalid connection attempt - receiver_or_slot is not callable: %s",
69+
receiver_or_slot,
6070
)
6171
raise TypeError("When slot is not provided, receiver must be callable")
6272

@@ -80,15 +90,16 @@ def connect(
8090
receiver = receiver_or_slot
8191
if not callable(slot):
8292
logger.error(
83-
f"Invalid connection attempt - slot is not callable: {slot}"
93+
"Invalid connection attempt - slot is not callable: %s",
94+
slot,
8495
)
8596
raise TypeError("Slot must be callable")
8697

8798
is_coroutine = asyncio.iscoroutinefunction(slot)
8899
conn_type = (
89-
TConnectionType.QueuedConnection
100+
TConnectionType.QUEUED_CONNECTION
90101
if is_coroutine
91-
else TConnectionType.DirectConnection
102+
else TConnectionType.DIRECT_CONNECTION
92103
)
93104
self.connections.append((receiver, slot, conn_type))
94105

@@ -114,18 +125,18 @@ def disconnect(self, receiver: object = None, slot: Callable = None) -> int:
114125

115126
self.connections = new_connections
116127
disconnected = original_count - len(self.connections)
117-
logger.debug(f"Disconnected {disconnected} connection(s)")
128+
logger.debug("Disconnected %d connection(s)", disconnected)
118129
return disconnected
119130

120131
def emit(self, *args, **kwargs):
132+
"""Emit signal to connected slots."""
121133
logger.debug("Signal emission started")
122134

123-
current_loop = asyncio.get_event_loop()
124135
for receiver, slot, conn_type in self.connections:
125136
try:
126-
if conn_type == TConnectionType.DirectConnection:
137+
if conn_type == TConnectionType.DIRECT_CONNECTION:
127138
slot(*args, **kwargs)
128-
else: # QueuedConnection
139+
else: # QUEUED_CONNECTION
129140
receiver_loop = getattr(receiver, "_loop", None)
130141
if not receiver_loop:
131142
logger.error("No event loop found for receiver")
@@ -171,6 +182,7 @@ def t_slot(func):
171182

172183
@functools.wraps(func)
173184
async def wrapper(self, *args, **kwargs):
185+
"""Wrapper for coroutine slots"""
174186
from_emit = kwargs.pop(_SignalConstants.FROM_EMIT, False)
175187

176188
if not hasattr(self, _SignalConstants.THREAD):
@@ -198,6 +210,7 @@ async def wrapper(self, *args, **kwargs):
198210

199211
@functools.wraps(func)
200212
def wrapper(self, *args, **kwargs):
213+
"""Wrapper for regular slots"""
201214
from_emit = kwargs.pop(_SignalConstants.FROM_EMIT, False)
202215

203216
if not hasattr(self, _SignalConstants.THREAD):
@@ -215,7 +228,7 @@ def wrapper(self, *args, **kwargs):
215228
if current_thread != self._thread:
216229
logger.debug("Executing regular slot from different thread")
217230
self._loop.call_soon_threadsafe(lambda: func(self, *args, **kwargs))
218-
return
231+
return None
219232

220233
return func(self, *args, **kwargs)
221234

tests/conftest.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,88 @@
1-
import pytest
1+
"""
2+
Shared fixtures for tests.
3+
"""
4+
5+
# pylint: disable=no-member
6+
# pylint: disable=redefined-outer-name
7+
# pylint: disable=unused-variable
8+
# pylint: disable=unused-argument
9+
# pylint: disable=property-with-parameters
10+
11+
import os
12+
import sys
213
import asyncio
314
import threading
4-
from tsignal import t_with_signals, t_signal, t_slot
515
import logging
6-
import sys
7-
import os
16+
import pytest
17+
from tsignal import t_with_signals, t_signal, t_slot
818

919
# Only creating the logger without configuration
1020
logger = logging.getLogger(__name__)
1121

1222

1323
@pytest.fixture(scope="function")
1424
def event_loop():
25+
"""Create an event loop"""
1526
loop = asyncio.new_event_loop()
1627
asyncio.set_event_loop(loop)
1728
yield loop
1829
loop.close()
1930

20-
2131
@t_with_signals
2232
class Sender:
33+
"""Sender class"""
2334
@t_signal
2435
def value_changed(self, value):
2536
"""Signal for value changes"""
26-
pass
2737

2838
def emit_value(self, value):
39+
"""Emit a value change signal"""
2940
self.value_changed.emit(value)
3041

31-
3242
@t_with_signals
3343
class Receiver:
44+
"""Receiver class"""
3445
def __init__(self):
3546
super().__init__()
3647
self.received_value = None
3748
self.received_count = 0
3849
self.id = id(self)
39-
logger.info(f"Created Receiver[{self.id}]")
50+
logger.info("Created Receiver[%d]", self.id)
4051

4152
@t_slot
4253
async def on_value_changed(self, value: int):
43-
logger.info(f"Receiver[{self.id}] on_value_changed called with value: {value}")
44-
logger.info(f"Current thread: {threading.current_thread().name}")
45-
logger.info(f"Current event loop: {asyncio.get_running_loop()}")
54+
"""Slot for value changes"""
55+
logger.info("Receiver[%d] on_value_changed called with value: %d", self.id, value)
56+
logger.info("Current thread: %s", threading.current_thread().name)
57+
logger.info("Current event loop: %s", asyncio.get_running_loop())
4658
self.received_value = value
4759
self.received_count += 1
4860
logger.info(
49-
f"Receiver[{self.id}] updated: value={self.received_value}, count={self.received_count}"
61+
"Receiver[%d] updated: value=%d, count=%d",
62+
self.id, self.received_value, self.received_count
5063
)
5164

5265
@t_slot
5366
def on_value_changed_sync(self, value: int):
54-
print(f"Receiver[{self.id}] received value (sync): {value}")
67+
"""Sync slot for value changes"""
68+
logger.info("Receiver[%d] on_value_changed_sync called with value: %d", self.id, value)
5569
self.received_value = value
5670
self.received_count += 1
57-
print(
58-
f"Receiver[{self.id}] updated (sync): value={self.received_value}, count={self.received_count}"
71+
logger.info(
72+
"Receiver[%d] updated (sync): value=%d, count=%d",
73+
self.id, self.received_value, self.received_count
5974
)
6075

6176

6277
@pytest.fixture
6378
def receiver(event_loop):
79+
"""Create a receiver"""
6480
return Receiver()
6581

6682

6783
@pytest.fixture
6884
def sender(event_loop):
85+
"""Create a sender"""
6986
return Sender()
7087

7188

@@ -80,9 +97,8 @@ def setup_logging():
8097

8198
# Can enable DEBUG mode via environment variable
8299

83-
default_level = logging.DEBUG
84-
# if os.environ.get("TSIGNAL_DEBUG"):
85-
# default_level = logging.DEBUG
100+
if os.environ.get("TSIGNAL_DEBUG"):
101+
default_level = logging.DEBUG
86102

87103
root.setLevel(default_level)
88104

0 commit comments

Comments
 (0)