Skip to content

Commit c2bc7aa

Browse files
authored
Merge pull request #24 from mutating/develop
0.0.21
2 parents 3acf932 + 310a08e commit c2bc7aa

File tree

4 files changed

+49
-48
lines changed

4 files changed

+49
-48
lines changed

.github/workflows/lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
strategy:
99
matrix:
1010
os: [ubuntu-latest]
11-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"]
11+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t", "3.15.0-alpha.1"]
1212

1313
steps:
1414
- uses: actions/checkout@v4

.github/workflows/tests_and_coverage.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
strategy:
99
matrix:
1010
os: [ubuntu-latest, windows-latest, macos-latest]
11-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"]
11+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t", "3.15.0-alpha.1"]
1212

1313
steps:
1414
- uses: actions/checkout@v4

README.md

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -21,48 +21,48 @@
2121

2222
</p>
2323

24-
It contains several useful additions to the standard thread synchronization tools, such as lock protocols and locks with advanced functionality.
24+
It adds several useful features to Python’s standard synchronization primitives, including lock protocols and enhanced lock implementations.
2525

2626

2727
## Table of contents
2828

2929
- [**Installation**](#installation)
3030
- [**Lock protocols**](#lock-protocols)
31-
- [**SmartLock - deadlock is impossible with it**](#smartlock---deadlock-is-impossible-with-it)
31+
- [**`SmartLock` turns deadlocks into exceptions**](#smartlock-turns-deadlocks-into-exceptions)
3232
- [**Test your locks**](#test-your-locks)
3333

3434

3535
## Installation
3636

37-
Get the `locklib` from the [pypi](https://pypi.org/project/locklib/):
37+
Install [`locklib`](https://pypi.org/project/locklib/) with `pip`:
3838

3939
```bash
4040
pip install locklib
4141
```
4242

43-
... or directly from git:
43+
... or directly from the Git repository:
4444

4545
```bash
4646
pip install git+https://github.com/mutating/locklib.git
4747
```
4848

49-
You can also quickly try out this and other packages without having to install using [instld](https://github.com/pomponchik/instld).
49+
You can also use [`instld`](https://github.com/pomponchik/instld) to quickly try out this package and others without installing them.
5050

5151

5252
## Lock protocols
5353

54-
Protocols are needed so that you can write typed code without being bound to specific classes. Protocols from this library allow you to "equalize" locks from the standard library and third-party locks, including those provided by this library.
54+
Protocols let you write type-annotated code without depending on concrete classes. The protocols in this library let you treat lock implementations from the standard library, third-party packages, and this library uniformly.
5555

56-
We consider the basic characteristic of the lock protocol to be the presence of two methods for an object:
56+
At a minimum, a lock object should provide two methods:
5757

5858
```python
59-
def acquire() -> None: pass
60-
def release() -> None: pass
59+
def acquire(self) -> None: ...
60+
def release(self) -> None: ...
6161
```
6262

63-
All the locks from the standard library correspond to this, as well as the locks presented in this one.
63+
All standard library locks conform to this, as do the locks provided by this library.
6464

65-
To check for compliance with this minimum standard, `locklib` contains the `LockProtocol`. You can check for yourself that all the locks match it:
65+
To check for compliance with this minimum standard, `locklib` contains the `LockProtocol`. You can verify that all of these locks satisfy it:
6666

6767
```python
6868
from multiprocessing import Lock as MLock
@@ -78,17 +78,17 @@ print(isinstance(ALock(), LockProtocol)) # True
7878
print(isinstance(SmartLock(), LockProtocol)) # True
7979
```
8080

81-
However! Most idiomatic python code using locks uses them as context managers. If your code is like that too, you can use one of the two inheritors of the regular `LockProtocol`: `ContextLockProtocol` or `AsyncContextLockProtocol`. Thus, the protocol inheritance hierarchy looks like this:
81+
However, most idiomatic Python code uses locks as context managers. If your code does too, you can use one of the two protocols derived from the base `LockProtocol`: `ContextLockProtocol` or `AsyncContextLockProtocol`. Thus, the protocol hierarchy looks like this:
8282

8383
```
8484
LockProtocol
8585
├── ContextLockProtocol
8686
└── AsyncContextLockProtocol
8787
```
8888

89-
`ContextLockProtocol` describes the objects described by `LockProtocol`, which are also [context managers](https://docs.python.org/3/library/stdtypes.html#typecontextmanager). `AsyncContextLockProtocol`, by analogy, describes objects that are instances of `LockProtocol`, as well as [asynchronous context managers](https://docs.python.org/3/reference/datamodel.html#async-context-managers).
89+
`ContextLockProtocol` describes objects that satisfy `LockProtocol` and also implement the [context manager protocol](https://docs.python.org/3/library/stdtypes.html#typecontextmanager). Similarly,`AsyncContextLockProtocol` describes objects that satisfy `LockProtocol` and implement the [asynchronous context manager](https://docs.python.org/3/reference/datamodel.html#async-context-managers) protocol.
9090

91-
Almost all the locks from the standard library are instances of `ContextLockProtocol`, as well as `SmartLock`.
91+
Almost all standard library locks, as well as `SmartLock`, satisfy `ContextLockProtocol`:
9292

9393
```python
9494
from multiprocessing import Lock as MLock
@@ -102,7 +102,7 @@ print(isinstance(TRLock(), ContextLockProtocol)) # True
102102
print(isinstance(SmartLock(), ContextLockProtocol)) # True
103103
```
104104

105-
However, the [`Lock` from asyncio](https://docs.python.org/3/library/asyncio-sync.html#asyncio.Lock) belongs to a separate category and `AsyncContextLockProtocol` is needed to describe it:
105+
However, the [`asyncio.Lock`](https://docs.python.org/3/library/asyncio-sync.html#asyncio.Lock) belongs to a separate category and `AsyncContextLockProtocol` is needed to describe it:
106106

107107
```python
108108
from asyncio import Lock
@@ -111,12 +111,12 @@ from locklib import AsyncContextLockProtocol
111111
print(isinstance(Lock(), AsyncContextLockProtocol)) # True
112112
```
113113

114-
If you use type hints and static verification tools like [mypy](https://github.com/python/mypy), we highly recommend using the narrowest of the presented categories for lock protocols, which describe the requirements for your locales.
114+
If you use type hints and static verification tools like [mypy](https://github.com/python/mypy), we highly recommend using the narrowest applicable protocol for your use case.
115115

116116

117-
## `SmartLock` - deadlock is impossible with it
117+
## `SmartLock` turns deadlocks into exceptions
118118

119-
`locklib` contains a lock that cannot get into the [deadlock](https://en.wikipedia.org/wiki/Deadlock) - `SmartLock`, based on [Wait-for Graph](https://en.wikipedia.org/wiki/Wait-for_graph). You can use it as a usual [```Lock``` from the standard library](https://docs.python.org/3/library/threading.html#lock-objects). Let's check that it can protect us from the [race condition](https://en.wikipedia.org/wiki/Race_condition) in the same way:
119+
`locklib` includes a lock that prevents [deadlocks](https://en.wikipedia.org/wiki/Deadlock) `SmartLock`, based on [Wait-for Graph](https://en.wikipedia.org/wiki/Wait-for_graph). You can use it like a regular [`Lock` from the standard library](https://docs.python.org/3/library/threading.html#lock-objects). Let’s verify that it prevents [race conditions](https://en.wikipedia.org/wiki/Race_condition) in the same way:
120120

121121
```python
122122
from threading import Thread
@@ -126,11 +126,11 @@ lock = SmartLock()
126126
counter = 0
127127

128128
def function():
129-
global counter
129+
global counter
130130

131-
for _ in range(1000):
132-
with lock:
133-
counter += 1
131+
for _ in range(1000):
132+
with lock:
133+
counter += 1
134134

135135
thread_1 = Thread(target=function)
136136
thread_2 = Thread(target=function)
@@ -140,7 +140,7 @@ thread_2.start()
140140
assert counter == 2000
141141
```
142142

143-
Yeah, in this case the lock helps us not to get a race condition, as the standard ```Lock``` does. But! Let's trigger a deadlock and look what happens:
143+
As expected, this lock prevents race conditions just like the standard `Lock`. Now let’s deliberately trigger a deadlock and see what happens:
144144

145145
```python
146146
from threading import Thread
@@ -150,33 +150,33 @@ lock_1 = SmartLock()
150150
lock_2 = SmartLock()
151151

152152
def function_1():
153-
while True:
154-
with lock_1:
155-
with lock_2:
156-
pass
153+
while True:
154+
with lock_1:
155+
with lock_2:
156+
pass
157157

158158
def function_2():
159-
while True:
160-
with lock_2:
161-
with lock_1:
162-
pass
159+
while True:
160+
with lock_2:
161+
with lock_1:
162+
pass
163163

164164
thread_1 = Thread(target=function_1)
165165
thread_2 = Thread(target=function_2)
166166
thread_1.start()
167167
thread_2.start()
168168
```
169169

170-
And... We have an exception like this:
170+
This raises an exception like the following:
171171

172172
```
173173
...
174174
locklib.errors.DeadLockError: A cycle between 1970256th and 1970257th threads has been detected.
175175
```
176176

177-
Deadlocks are impossible for this lock!
177+
So, with this lock, a deadlock results in an exception instead of blocking forever.
178178

179-
If you want to catch the exception, import this from the `locklib` too:
179+
If you want to catch this exception, you can also import it from `locklib`:
180180

181181
```python
182182
from locklib import DeadLockError
@@ -185,9 +185,9 @@ from locklib import DeadLockError
185185

186186
## Test your locks
187187

188-
Sometimes, when testing a code, you may need to detect if some action is taking place inside the lock. How to do this with a minimum of code? There is the `LockTraceWrapper` for this. It is a wrapper around a regular lock, which records it every time the code takes a lock or releases it. At the same time, the functionality of the wrapped lock is fully preserved.
188+
Sometimes, when testing code, you may need to detect whether some action occurs while the lock is held. How can you do this with minimal boilerplate? Use `LockTraceWrapper`. It is a wrapper around a regular lock that records every acquisition and release. At the same time, it fully preserves the wrapped lock’s behavior.
189189

190-
It's easy to create an object of such a lock. Just pass any other lock to the class constructor:
190+
Creating such a wrapper is easy. Just pass any lock to the constructor:
191191

192192
```python
193193
from threading import Lock
@@ -196,29 +196,29 @@ from locklib import LockTraceWrapper
196196
lock = LockTraceWrapper(Lock())
197197
```
198198

199-
You can use it in the same way as the wrapped lock:
199+
You can use it exactly like the wrapped lock:
200200

201201
```python
202202
with lock:
203203
...
204204
```
205205

206-
Anywhere in your program, you can "inform" the lock that the action you need is being performed here:
206+
Anywhere in your program, you can record that a specific event occurred:
207207

208208
```python
209209
lock.notify('event_name')
210210
```
211211

212-
And! Now you can easily identify if there were cases when an event with this identifier did not occur under the mutex. To do this, use the `was_event_locked` method:
212+
You can then easily check whether an event with this identifier ever occurred outside the lock. To do this, use the `was_event_locked` method:
213213

214214
```python
215215
lock.was_event_locked('event_name')
216216
```
217217

218-
If the `notify` method was called with the same parameter only when the lock activated, it will return `True`. If not, that is, if there was at least one case when the c method was called with such an identifier without an activated mutex, `False` will be returned.
218+
If the `notify` method was called with the same parameter only while the lock was held, it will return `True`. If not, that is, if there was at least one case when the `notify` method was called with that identifier without the lock being held, `False` will be returned.
219219

220-
How does it work? A modified [algorithm for determining the correct parenthesis sequence](https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B0%D0%B2%D0%B8%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F_%D1%81%D0%BA%D0%BE%D0%B1%D0%BE%D1%87%D0%BD%D0%B0%D1%8F_%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%D0%B4%D0%BE%D0%B2%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D1%81%D1%82%D1%8C) is used here. For each thread for which any events were registered (taking the mutex, releasing the mutex, and also calling the `notify` method), the check takes place separately, that is, we determine that it was the same thread that held the mutex when `notify` was called, and not some other one.
220+
How does it work? It uses a modified [balanced-parentheses algorithm](https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B0%D0%B2%D0%B8%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F_%D1%81%D0%BA%D0%BE%D0%B1%D0%BE%D1%87%D0%BD%D0%B0%D1%8F_%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%D0%B4%D0%BE%D0%B2%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D1%81%D1%82%D1%8C). For each thread for which any events were registered (taking the mutex, releasing the mutex, and also calling the `notify` method), the check takes place separately, that is, we determine that it was the same thread that held the mutex when `notify` was called, and not some other one.
221221

222-
> ⚠️ The thread id is used to identify the threads. This id may be reused if the current thread ends, which in some cases may lead to incorrect identification of lock coverage for operations that were not actually covered by the lock. Make sure that this cannot happen during your test.
222+
> ⚠️ The thread id is used to identify the threads. A thread ID may be reused after a thread exits, which may in some cases cause the wrapper to incorrectly report that an operation was protected by the lock. Make sure this cannot happen during your tests.
223223
224-
If no event with the specified identifier was recorded in any of the threads, the `ThereWasNoSuchEventError` exception will be raised by default. If you want to disable this so that the method simply returns `False` in such situations, pass the additional argument `raise_exception=False` to `was_event_locked`.
224+
If no event with the specified identifier was recorded in any thread, the `ThereWasNoSuchEventError` exception will be raised by default. If you want to disable this so that the method simply returns `False` in such situations, pass the keyword argument `raise_exception=False` to `was_event_locked`.

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta'
44

55
[project]
66
name = 'locklib'
7-
version = '0.0.20'
7+
version = '0.0.21'
88
authors = [
99
{ name='Evgeniy Blinov', email='zheni-b@yandex.ru' },
1010
]
@@ -23,6 +23,7 @@ classifiers = [
2323
'Programming Language :: Python :: 3.12',
2424
'Programming Language :: Python :: 3.13',
2525
'Programming Language :: Python :: 3.14',
26+
'Programming Language :: Python :: 3.15',
2627
'Programming Language :: Python :: Free Threading',
2728
'Programming Language :: Python :: Free Threading :: 3 - Stable',
2829
'License :: OSI Approved :: MIT License',
@@ -53,5 +54,5 @@ format.quote-style = "single"
5354
addopts = "-p no:warnings"
5455

5556
[project.urls]
56-
'Source' = 'https://github.com/pomponchik/locklib'
57-
'Tracker' = 'https://github.com/pomponchik/locklib/issues'
57+
'Source' = 'https://github.com/mutating/locklib'
58+
'Tracker' = 'https://github.com/mutating/locklib/issues'

0 commit comments

Comments
 (0)