locklib 0.0.20__tar.gz → 0.0.22__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. locklib-0.0.22/PKG-INFO +281 -0
  2. locklib-0.0.22/README.md +251 -0
  3. {locklib-0.0.20 → locklib-0.0.22}/locklib/__init__.py +4 -0
  4. locklib-0.0.22/locklib/locks/empty/async_empty_lock.py +24 -0
  5. locklib-0.0.22/locklib/locks/empty/empty_lock.py +23 -0
  6. locklib-0.0.22/locklib/py.typed +0 -0
  7. locklib-0.0.22/locklib.egg-info/PKG-INFO +281 -0
  8. {locklib-0.0.20 → locklib-0.0.22}/locklib.egg-info/SOURCES.txt +3 -0
  9. {locklib-0.0.20 → locklib-0.0.22}/pyproject.toml +5 -3
  10. locklib-0.0.20/PKG-INFO +0 -253
  11. locklib-0.0.20/README.md +0 -224
  12. locklib-0.0.20/locklib.egg-info/PKG-INFO +0 -253
  13. {locklib-0.0.20 → locklib-0.0.22}/LICENSE +0 -0
  14. {locklib-0.0.20 → locklib-0.0.22}/locklib/errors.py +0 -0
  15. {locklib-0.0.20 → locklib-0.0.22}/locklib/locks/__init__.py +0 -0
  16. {locklib-0.0.20/locklib/locks/smart_lock → locklib-0.0.22/locklib/locks/empty}/__init__.py +0 -0
  17. {locklib-0.0.20/locklib/locks/tracer → locklib-0.0.22/locklib/locks/smart_lock}/__init__.py +0 -0
  18. {locklib-0.0.20 → locklib-0.0.22}/locklib/locks/smart_lock/graph.py +0 -0
  19. {locklib-0.0.20 → locklib-0.0.22}/locklib/locks/smart_lock/lock.py +0 -0
  20. {locklib-0.0.20/locklib/protocols → locklib-0.0.22/locklib/locks/tracer}/__init__.py +0 -0
  21. {locklib-0.0.20 → locklib-0.0.22}/locklib/locks/tracer/events.py +0 -0
  22. {locklib-0.0.20 → locklib-0.0.22}/locklib/locks/tracer/tracer.py +0 -0
  23. /locklib-0.0.20/locklib/py.typed → /locklib-0.0.22/locklib/protocols/__init__.py +0 -0
  24. {locklib-0.0.20 → locklib-0.0.22}/locklib/protocols/async_context_lock.py +0 -0
  25. {locklib-0.0.20 → locklib-0.0.22}/locklib/protocols/context_lock.py +0 -0
  26. {locklib-0.0.20 → locklib-0.0.22}/locklib/protocols/lock.py +0 -0
  27. {locklib-0.0.20 → locklib-0.0.22}/locklib.egg-info/dependency_links.txt +0 -0
  28. {locklib-0.0.20 → locklib-0.0.22}/locklib.egg-info/top_level.txt +0 -0
  29. {locklib-0.0.20 → locklib-0.0.22}/setup.cfg +0 -0
@@ -0,0 +1,281 @@
1
+ Metadata-Version: 2.1
2
+ Name: locklib
3
+ Version: 0.0.22
4
+ Summary: When there are not enough locks from the standard library
5
+ Author-email: Evgeniy Blinov <zheni-b@yandex.ru>
6
+ Project-URL: Source, https://github.com/mutating/locklib
7
+ Project-URL: Tracker, https://github.com/mutating/locklib/issues
8
+ Keywords: locks,mutexes,threading,protocols
9
+ Classifier: Operating System :: MacOS :: MacOS X
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Classifier: Operating System :: POSIX
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Programming Language :: Python :: 3.15
21
+ Classifier: Programming Language :: Python :: Free Threading
22
+ Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
23
+ Classifier: License :: OSI Approved :: MIT License
24
+ Classifier: Topic :: Software Development :: Libraries
25
+ Classifier: Intended Audience :: Developers
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.8
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+
31
+ <details>
32
+ <summary>ⓘ</summary>
33
+
34
+ [![Downloads](https://static.pepy.tech/badge/locklib/month)](https://pepy.tech/project/locklib)
35
+ [![Downloads](https://static.pepy.tech/badge/locklib)](https://pepy.tech/project/locklib)
36
+ [![Coverage Status](https://coveralls.io/repos/github/mutating/locklib/badge.svg?branch=main)](https://coveralls.io/github/mutating/locklib?branch=main)
37
+ [![Lines of code](https://sloc.xyz/github/mutating/locklib/?category=code?)](https://github.com/boyter/scc/)
38
+ [![Hits-of-Code](https://hitsofcode.com/github/mutating/locklib?branch=main)](https://hitsofcode.com/github/mutating/locklib/view?branch=main)
39
+ [![Test-Package](https://github.com/mutating/locklib/actions/workflows/tests_and_coverage.yml/badge.svg)](https://github.com/mutating/locklib/actions/workflows/tests_and_coverage.yml)
40
+ [![Python versions](https://img.shields.io/pypi/pyversions/locklib.svg)](https://pypi.python.org/pypi/locklib)
41
+ [![PyPI version](https://badge.fury.io/py/locklib.svg)](https://badge.fury.io/py/locklib)
42
+ [![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)
43
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
44
+ [![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/mutating/locklib)
45
+
46
+ </details>
47
+
48
+ <p align="center">
49
+
50
+ ![logo](https://raw.githubusercontent.com/mutating/locklib/develop/docs/assets/logo_7.svg)
51
+
52
+ </p>
53
+
54
+ It adds several useful features to Python’s standard synchronization primitives, including lock protocols and enhanced lock implementations.
55
+
56
+
57
+ ## Table of contents
58
+
59
+ - [**Installation**](#installation)
60
+ - [**Lock protocols**](#lock-protocols)
61
+ - [**Empty locks**](#empty-locks)
62
+ - [**`SmartLock` turns deadlocks into exceptions**](#smartlock-turns-deadlocks-into-exceptions)
63
+ - [**Test your locks**](#test-your-locks)
64
+
65
+
66
+ ## Installation
67
+
68
+ Install [`locklib`](https://pypi.org/project/locklib/) with `pip`:
69
+
70
+ ```bash
71
+ pip install locklib
72
+ ```
73
+
74
+ ... or directly from the Git repository:
75
+
76
+ ```bash
77
+ pip install git+https://github.com/mutating/locklib.git
78
+ ```
79
+
80
+ You can also use [`instld`](https://github.com/pomponchik/instld) to quickly try out this package and others without installing them.
81
+
82
+
83
+ ## Lock protocols
84
+
85
+ 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.
86
+
87
+ At a minimum, a lock object should provide two methods:
88
+
89
+ ```python
90
+ def acquire(self) -> None: ...
91
+ def release(self) -> None: ...
92
+ ```
93
+
94
+ All standard library locks conform to this, as do the locks provided by this library.
95
+
96
+ To check for compliance with this minimum standard, `locklib` contains the `LockProtocol`. You can verify that all of these locks satisfy it:
97
+
98
+ ```python
99
+ from multiprocessing import Lock as MLock
100
+ from threading import Lock as TLock, RLock as TRLock
101
+ from asyncio import Lock as ALock
102
+
103
+ from locklib import SmartLock, LockProtocol
104
+
105
+ print(isinstance(MLock(), LockProtocol)) # True
106
+ print(isinstance(TLock(), LockProtocol)) # True
107
+ print(isinstance(TRLock(), LockProtocol)) # True
108
+ print(isinstance(ALock(), LockProtocol)) # True
109
+ print(isinstance(SmartLock(), LockProtocol)) # True
110
+ ```
111
+
112
+ 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:
113
+
114
+ ```
115
+ LockProtocol
116
+ ├── ContextLockProtocol
117
+ └── AsyncContextLockProtocol
118
+ ```
119
+
120
+ `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.
121
+
122
+ Almost all standard library locks, as well as `SmartLock`, satisfy `ContextLockProtocol`:
123
+
124
+ ```python
125
+ from multiprocessing import Lock as MLock
126
+ from threading import Lock as TLock, RLock as TRLock
127
+
128
+ from locklib import SmartLock, ContextLockProtocol
129
+
130
+ print(isinstance(MLock(), ContextLockProtocol)) # True
131
+ print(isinstance(TLock(), ContextLockProtocol)) # True
132
+ print(isinstance(TRLock(), ContextLockProtocol)) # True
133
+ print(isinstance(SmartLock(), ContextLockProtocol)) # True
134
+ ```
135
+
136
+ 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:
137
+
138
+ ```python
139
+ from asyncio import Lock
140
+ from locklib import AsyncContextLockProtocol
141
+
142
+ print(isinstance(Lock(), AsyncContextLockProtocol)) # True
143
+ ```
144
+
145
+ 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.
146
+
147
+
148
+ ## Empty locks
149
+
150
+ Sometimes a piece of code expects a lock, but in a particular case no synchronization is actually needed. Instead of branching on whether to lock, you can inject a lock that does nothing. `locklib` provides two such no-op locks: `EmptyLock` and its asynchronous counterpart `AsyncEmptyLock`. Their `acquire`/`release` methods and context-manager forms return immediately and never block:
151
+
152
+ ```python
153
+ from locklib import EmptyLock
154
+
155
+ lock = EmptyLock()
156
+
157
+ with lock:
158
+ ... # nothing is actually locked
159
+ ```
160
+
161
+ ```python
162
+ from locklib import AsyncEmptyLock
163
+
164
+ lock = AsyncEmptyLock()
165
+
166
+ async def function():
167
+ async with lock:
168
+ ... # nothing is actually locked
169
+ ```
170
+
171
+ `EmptyLock` implements `ContextLockProtocol` and `AsyncEmptyLock` implements `AsyncContextLockProtocol` (and both implement `LockProtocol`), so each one is a drop-in substitute wherever the corresponding protocol is expected.
172
+
173
+
174
+ ## `SmartLock` turns deadlocks into exceptions
175
+
176
+ `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:
177
+
178
+ ```python
179
+ from threading import Thread
180
+ from locklib import SmartLock
181
+
182
+ lock = SmartLock()
183
+ counter = 0
184
+
185
+ def function():
186
+ global counter
187
+
188
+ for _ in range(1000):
189
+ with lock:
190
+ counter += 1
191
+
192
+ thread_1 = Thread(target=function)
193
+ thread_2 = Thread(target=function)
194
+ thread_1.start()
195
+ thread_2.start()
196
+
197
+ assert counter == 2000
198
+ ```
199
+
200
+ As expected, this lock prevents race conditions just like the standard `Lock`. Now let’s deliberately trigger a deadlock and see what happens:
201
+
202
+ ```python
203
+ from threading import Thread
204
+ from locklib import SmartLock
205
+
206
+ lock_1 = SmartLock()
207
+ lock_2 = SmartLock()
208
+
209
+ def function_1():
210
+ while True:
211
+ with lock_1:
212
+ with lock_2:
213
+ pass
214
+
215
+ def function_2():
216
+ while True:
217
+ with lock_2:
218
+ with lock_1:
219
+ pass
220
+
221
+ thread_1 = Thread(target=function_1)
222
+ thread_2 = Thread(target=function_2)
223
+ thread_1.start()
224
+ thread_2.start()
225
+ ```
226
+
227
+ This raises an exception like the following:
228
+
229
+ ```
230
+ ...
231
+ locklib.errors.DeadLockError: A cycle between 1970256th and 1970257th threads has been detected.
232
+ ```
233
+
234
+ So, with this lock, a deadlock results in an exception instead of blocking forever.
235
+
236
+ If you want to catch this exception, you can also import it from `locklib`:
237
+
238
+ ```python
239
+ from locklib import DeadLockError
240
+ ```
241
+
242
+
243
+ ## Test your locks
244
+
245
+ 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.
246
+
247
+ Creating such a wrapper is easy. Just pass any lock to the constructor:
248
+
249
+ ```python
250
+ from threading import Lock
251
+ from locklib import LockTraceWrapper
252
+
253
+ lock = LockTraceWrapper(Lock())
254
+ ```
255
+
256
+ You can use it exactly like the wrapped lock:
257
+
258
+ ```python
259
+ with lock:
260
+ ...
261
+ ```
262
+
263
+ Anywhere in your program, you can record that a specific event occurred:
264
+
265
+ ```python
266
+ lock.notify('event_name')
267
+ ```
268
+
269
+ 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:
270
+
271
+ ```python
272
+ lock.was_event_locked('event_name')
273
+ ```
274
+
275
+ 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.
276
+
277
+ 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.
278
+
279
+ > ⚠️ 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.
280
+
281
+ 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`.
@@ -0,0 +1,251 @@
1
+ <details>
2
+ <summary>ⓘ</summary>
3
+
4
+ [![Downloads](https://static.pepy.tech/badge/locklib/month)](https://pepy.tech/project/locklib)
5
+ [![Downloads](https://static.pepy.tech/badge/locklib)](https://pepy.tech/project/locklib)
6
+ [![Coverage Status](https://coveralls.io/repos/github/mutating/locklib/badge.svg?branch=main)](https://coveralls.io/github/mutating/locklib?branch=main)
7
+ [![Lines of code](https://sloc.xyz/github/mutating/locklib/?category=code?)](https://github.com/boyter/scc/)
8
+ [![Hits-of-Code](https://hitsofcode.com/github/mutating/locklib?branch=main)](https://hitsofcode.com/github/mutating/locklib/view?branch=main)
9
+ [![Test-Package](https://github.com/mutating/locklib/actions/workflows/tests_and_coverage.yml/badge.svg)](https://github.com/mutating/locklib/actions/workflows/tests_and_coverage.yml)
10
+ [![Python versions](https://img.shields.io/pypi/pyversions/locklib.svg)](https://pypi.python.org/pypi/locklib)
11
+ [![PyPI version](https://badge.fury.io/py/locklib.svg)](https://badge.fury.io/py/locklib)
12
+ [![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)
13
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
14
+ [![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/mutating/locklib)
15
+
16
+ </details>
17
+
18
+ <p align="center">
19
+
20
+ ![logo](https://raw.githubusercontent.com/mutating/locklib/develop/docs/assets/logo_7.svg)
21
+
22
+ </p>
23
+
24
+ It adds several useful features to Python’s standard synchronization primitives, including lock protocols and enhanced lock implementations.
25
+
26
+
27
+ ## Table of contents
28
+
29
+ - [**Installation**](#installation)
30
+ - [**Lock protocols**](#lock-protocols)
31
+ - [**Empty locks**](#empty-locks)
32
+ - [**`SmartLock` turns deadlocks into exceptions**](#smartlock-turns-deadlocks-into-exceptions)
33
+ - [**Test your locks**](#test-your-locks)
34
+
35
+
36
+ ## Installation
37
+
38
+ Install [`locklib`](https://pypi.org/project/locklib/) with `pip`:
39
+
40
+ ```bash
41
+ pip install locklib
42
+ ```
43
+
44
+ ... or directly from the Git repository:
45
+
46
+ ```bash
47
+ pip install git+https://github.com/mutating/locklib.git
48
+ ```
49
+
50
+ You can also use [`instld`](https://github.com/pomponchik/instld) to quickly try out this package and others without installing them.
51
+
52
+
53
+ ## Lock protocols
54
+
55
+ 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.
56
+
57
+ At a minimum, a lock object should provide two methods:
58
+
59
+ ```python
60
+ def acquire(self) -> None: ...
61
+ def release(self) -> None: ...
62
+ ```
63
+
64
+ All standard library locks conform to this, as do the locks provided by this library.
65
+
66
+ To check for compliance with this minimum standard, `locklib` contains the `LockProtocol`. You can verify that all of these locks satisfy it:
67
+
68
+ ```python
69
+ from multiprocessing import Lock as MLock
70
+ from threading import Lock as TLock, RLock as TRLock
71
+ from asyncio import Lock as ALock
72
+
73
+ from locklib import SmartLock, LockProtocol
74
+
75
+ print(isinstance(MLock(), LockProtocol)) # True
76
+ print(isinstance(TLock(), LockProtocol)) # True
77
+ print(isinstance(TRLock(), LockProtocol)) # True
78
+ print(isinstance(ALock(), LockProtocol)) # True
79
+ print(isinstance(SmartLock(), LockProtocol)) # True
80
+ ```
81
+
82
+ 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:
83
+
84
+ ```
85
+ LockProtocol
86
+ ├── ContextLockProtocol
87
+ └── AsyncContextLockProtocol
88
+ ```
89
+
90
+ `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.
91
+
92
+ Almost all standard library locks, as well as `SmartLock`, satisfy `ContextLockProtocol`:
93
+
94
+ ```python
95
+ from multiprocessing import Lock as MLock
96
+ from threading import Lock as TLock, RLock as TRLock
97
+
98
+ from locklib import SmartLock, ContextLockProtocol
99
+
100
+ print(isinstance(MLock(), ContextLockProtocol)) # True
101
+ print(isinstance(TLock(), ContextLockProtocol)) # True
102
+ print(isinstance(TRLock(), ContextLockProtocol)) # True
103
+ print(isinstance(SmartLock(), ContextLockProtocol)) # True
104
+ ```
105
+
106
+ 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:
107
+
108
+ ```python
109
+ from asyncio import Lock
110
+ from locklib import AsyncContextLockProtocol
111
+
112
+ print(isinstance(Lock(), AsyncContextLockProtocol)) # True
113
+ ```
114
+
115
+ 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.
116
+
117
+
118
+ ## Empty locks
119
+
120
+ Sometimes a piece of code expects a lock, but in a particular case no synchronization is actually needed. Instead of branching on whether to lock, you can inject a lock that does nothing. `locklib` provides two such no-op locks: `EmptyLock` and its asynchronous counterpart `AsyncEmptyLock`. Their `acquire`/`release` methods and context-manager forms return immediately and never block:
121
+
122
+ ```python
123
+ from locklib import EmptyLock
124
+
125
+ lock = EmptyLock()
126
+
127
+ with lock:
128
+ ... # nothing is actually locked
129
+ ```
130
+
131
+ ```python
132
+ from locklib import AsyncEmptyLock
133
+
134
+ lock = AsyncEmptyLock()
135
+
136
+ async def function():
137
+ async with lock:
138
+ ... # nothing is actually locked
139
+ ```
140
+
141
+ `EmptyLock` implements `ContextLockProtocol` and `AsyncEmptyLock` implements `AsyncContextLockProtocol` (and both implement `LockProtocol`), so each one is a drop-in substitute wherever the corresponding protocol is expected.
142
+
143
+
144
+ ## `SmartLock` turns deadlocks into exceptions
145
+
146
+ `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:
147
+
148
+ ```python
149
+ from threading import Thread
150
+ from locklib import SmartLock
151
+
152
+ lock = SmartLock()
153
+ counter = 0
154
+
155
+ def function():
156
+ global counter
157
+
158
+ for _ in range(1000):
159
+ with lock:
160
+ counter += 1
161
+
162
+ thread_1 = Thread(target=function)
163
+ thread_2 = Thread(target=function)
164
+ thread_1.start()
165
+ thread_2.start()
166
+
167
+ assert counter == 2000
168
+ ```
169
+
170
+ As expected, this lock prevents race conditions just like the standard `Lock`. Now let’s deliberately trigger a deadlock and see what happens:
171
+
172
+ ```python
173
+ from threading import Thread
174
+ from locklib import SmartLock
175
+
176
+ lock_1 = SmartLock()
177
+ lock_2 = SmartLock()
178
+
179
+ def function_1():
180
+ while True:
181
+ with lock_1:
182
+ with lock_2:
183
+ pass
184
+
185
+ def function_2():
186
+ while True:
187
+ with lock_2:
188
+ with lock_1:
189
+ pass
190
+
191
+ thread_1 = Thread(target=function_1)
192
+ thread_2 = Thread(target=function_2)
193
+ thread_1.start()
194
+ thread_2.start()
195
+ ```
196
+
197
+ This raises an exception like the following:
198
+
199
+ ```
200
+ ...
201
+ locklib.errors.DeadLockError: A cycle between 1970256th and 1970257th threads has been detected.
202
+ ```
203
+
204
+ So, with this lock, a deadlock results in an exception instead of blocking forever.
205
+
206
+ If you want to catch this exception, you can also import it from `locklib`:
207
+
208
+ ```python
209
+ from locklib import DeadLockError
210
+ ```
211
+
212
+
213
+ ## Test your locks
214
+
215
+ 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.
216
+
217
+ Creating such a wrapper is easy. Just pass any lock to the constructor:
218
+
219
+ ```python
220
+ from threading import Lock
221
+ from locklib import LockTraceWrapper
222
+
223
+ lock = LockTraceWrapper(Lock())
224
+ ```
225
+
226
+ You can use it exactly like the wrapped lock:
227
+
228
+ ```python
229
+ with lock:
230
+ ...
231
+ ```
232
+
233
+ Anywhere in your program, you can record that a specific event occurred:
234
+
235
+ ```python
236
+ lock.notify('event_name')
237
+ ```
238
+
239
+ 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:
240
+
241
+ ```python
242
+ lock.was_event_locked('event_name')
243
+ ```
244
+
245
+ 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.
246
+
247
+ 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.
248
+
249
+ > ⚠️ 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.
250
+
251
+ 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`.
@@ -5,6 +5,10 @@ from locklib.errors import (
5
5
  from locklib.errors import (
6
6
  ThereWasNoSuchEventError as ThereWasNoSuchEventError,
7
7
  )
8
+ from locklib.locks.empty.async_empty_lock import (
9
+ AsyncEmptyLock as AsyncEmptyLock,
10
+ )
11
+ from locklib.locks.empty.empty_lock import EmptyLock as EmptyLock
8
12
  from locklib.locks.smart_lock.lock import SmartLock as SmartLock
9
13
  from locklib.locks.tracer.tracer import (
10
14
  LockTraceWrapper as LockTraceWrapper,
@@ -0,0 +1,24 @@
1
+ from types import TracebackType
2
+ from typing import Optional, Type
3
+
4
+
5
+ class AsyncEmptyLock:
6
+ """Provide the async-context-lock interface while deliberately doing no locking.
7
+
8
+ The asynchronous counterpart of ``EmptyLock``: it mirrors the shape of
9
+ ``asyncio.Lock`` (an awaitable ``acquire`` and a synchronous ``release``)
10
+ but performs no synchronization. It is stateless, so it never blocks and
11
+ can be reused freely.
12
+ """
13
+
14
+ async def __aenter__(self) -> None:
15
+ await self.acquire()
16
+
17
+ async def __aexit__(self, exception_type: Optional[Type[BaseException]], exception_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None:
18
+ self.release()
19
+
20
+ async def acquire(self) -> None:
21
+ ...
22
+
23
+ def release(self) -> None:
24
+ ...
@@ -0,0 +1,23 @@
1
+ from types import TracebackType
2
+ from typing import Optional, Type
3
+
4
+
5
+ class EmptyLock:
6
+ """Provide the context-lock interface while deliberately doing no locking.
7
+
8
+ Useful when some code expects a lock but no synchronization is actually
9
+ needed, so a no-op lock can be injected instead of branching on whether to
10
+ lock. It is stateless, so it never blocks and can be reused freely.
11
+ """
12
+
13
+ def __enter__(self) -> None:
14
+ self.acquire()
15
+
16
+ def __exit__(self, exception_type: Optional[Type[BaseException]], exception_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None:
17
+ self.release()
18
+
19
+ def acquire(self) -> None:
20
+ ...
21
+
22
+ def release(self) -> None:
23
+ ...
File without changes