diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index c3a1b2c..83d9f60 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
- python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"]
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
deleted file mode 100644
index 8ebe0bb..0000000
--- a/.github/workflows/publish.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-name: Publish the package
-
-on:
- push:
- branches:
- - main
-
-jobs:
- pypi-publish:
- name: upload release to PyPI
- runs-on: ubuntu-latest
- # Specifying a GitHub environment is optional, but strongly encouraged
- environment: release
- permissions:
- # IMPORTANT: this permission is mandatory for trusted publishing
- id-token: write
- steps:
- - uses: actions/checkout@v4
-
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5
- with:
- python-version: ${{ matrix.python-version }}
-
- - name: Install dependencies
- shell: bash
- run: pip install -r requirements_dev.txt
-
- - name: Build the project
- shell: bash
- run: python -m build .
-
- - name: Publish package distributions to PyPI
- uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/.github/workflows/tests_and_coverage.yml b/.github/workflows/tests_and_coverage.yml
index 25dbd0b..45db3bf 100644
--- a/.github/workflows/tests_and_coverage.yml
+++ b/.github/workflows/tests_and_coverage.yml
@@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
- python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"]
steps:
- uses: actions/checkout@v4
diff --git a/.ruff.toml b/.ruff.toml
deleted file mode 100644
index ec5b9a4..0000000
--- a/.ruff.toml
+++ /dev/null
@@ -1 +0,0 @@
-ignore = ['E501', 'E712']
diff --git a/README.md b/README.md
index 299a561..0769462 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
-
+ⓘ
[](https://pepy.tech/project/locklib)
[](https://pepy.tech/project/locklib)
@@ -12,6 +13,14 @@
[](https://github.com/astral-sh/ruff)
[](https://deepwiki.com/pomponchik/locklib)
+
+ + + +
+ It contains several useful additions to the standard thread synchronization tools, such as lock protocols and locks with advanced functionality. diff --git a/locklib/__init__.py b/locklib/__init__.py index f1c8e84..767fef9 100644 --- a/locklib/__init__.py +++ b/locklib/__init__.py @@ -1,7 +1,15 @@ -from locklib.locks.smart_lock.lock import SmartLock as SmartLock # noqa: F401 -from locklib.errors import DeadLockError as DeadLockError # noqa: F401 -from locklib.protocols.lock import LockProtocol as LockProtocol # noqa: F401 -from locklib.protocols.context_lock import ContextLockProtocol as ContextLockProtocol # noqa: F401 -from locklib.protocols.async_context_lock import AsyncContextLockProtocol as AsyncContextLockProtocol # noqa: F401 -from locklib.locks.tracer.tracer import LockTraceWrapper as LockTraceWrapper # noqa: F401 -from locklib.errors import StrangeEventOrderError as StrangeEventOrderError # noqa: F401 +from locklib.errors import DeadLockError as DeadLockError +from locklib.errors import ( + StrangeEventOrderError as StrangeEventOrderError, +) +from locklib.locks.smart_lock.lock import SmartLock as SmartLock +from locklib.locks.tracer.tracer import ( + LockTraceWrapper as LockTraceWrapper, +) +from locklib.protocols.async_context_lock import ( + AsyncContextLockProtocol as AsyncContextLockProtocol, +) +from locklib.protocols.context_lock import ( + ContextLockProtocol as ContextLockProtocol, +) +from locklib.protocols.lock import LockProtocol as LockProtocol diff --git a/locklib/locks/smart_lock/graph.py b/locklib/locks/smart_lock/graph.py index d30792b..15293d9 100644 --- a/locklib/locks/smart_lock/graph.py +++ b/locklib/locks/smart_lock/graph.py @@ -1,6 +1,6 @@ -from threading import Lock from collections import defaultdict -from typing import List, Set, DefaultDict, Optional +from threading import Lock +from typing import DefaultDict, List, Optional, Set from locklib.errors import DeadLockError diff --git a/locklib/locks/smart_lock/lock.py b/locklib/locks/smart_lock/lock.py index 93c4c94..3410f69 100644 --- a/locklib/locks/smart_lock/lock.py +++ b/locklib/locks/smart_lock/lock.py @@ -1,15 +1,18 @@ try: - from threading import Lock, get_native_id # type: ignore[attr-defined, unused-ignore] + from threading import ( # type: ignore[attr-defined, unused-ignore] + Lock, + get_native_id, + ) except ImportError: # pragma: no cover - from threading import Lock, get_ident as get_native_id # get_native_id is available only since python 3.8 + from threading import Lock # get_native_id is available only since python 3.8 + from threading import get_ident as get_native_id from collections import deque -from typing import Type, Deque, Dict, Optional from types import TracebackType +from typing import Deque, Dict, Optional, Type from locklib.locks.smart_lock.graph import LocksGraph - graph = LocksGraph() class SmartLock: @@ -26,40 +29,38 @@ def __exit__(self, exception_type: Optional[Type[BaseException]], exception_valu self.release() def acquire(self) -> None: - id = get_native_id() + thread_id = get_native_id() previous_element_lock = None - with self.lock: - with self.graph.lock: - if not self.deque: - self.deque.appendleft(id) - self.local_locks[id] = Lock() - self.local_locks[id].acquire() - else: - previous_element = self.deque[0] - self.graph.add_link(id, previous_element) - self.deque.appendleft(id) - self.local_locks[id] = Lock() - self.local_locks[id].acquire() - previous_element_lock = self.local_locks[previous_element] + with self.lock, self.graph.lock: + if not self.deque: + self.deque.appendleft(thread_id) + self.local_locks[thread_id] = Lock() + self.local_locks[thread_id].acquire() + else: + previous_element = self.deque[0] + self.graph.add_link(thread_id, previous_element) + self.deque.appendleft(thread_id) + self.local_locks[thread_id] = Lock() + self.local_locks[thread_id].acquire() + previous_element_lock = self.local_locks[previous_element] if previous_element_lock is not None: previous_element_lock.acquire() def release(self) -> None: - id = get_native_id() + thread_id = get_native_id() - with self.lock: - with self.graph.lock: - if id not in self.local_locks: - raise RuntimeError('Release unlocked lock.') + with self.lock, self.graph.lock: + if thread_id not in self.local_locks: + raise RuntimeError('Release unlocked lock.') - self.deque.pop() - lock = self.local_locks[id] - del self.local_locks[id] + self.deque.pop() + lock = self.local_locks[thread_id] + del self.local_locks[thread_id] - if len(self.deque) != 0: - next_element = self.deque[-1] - self.graph.delete_link(next_element, id) + if len(self.deque) != 0: + next_element = self.deque[-1] + self.graph.delete_link(next_element, thread_id) - lock.release() + lock.release() diff --git a/locklib/locks/tracer/events.py b/locklib/locks/tracer/events.py index fc5daf3..8d10901 100644 --- a/locklib/locks/tracer/events.py +++ b/locklib/locks/tracer/events.py @@ -1,6 +1,6 @@ -from typing import Optional -from enum import Enum from dataclasses import dataclass +from enum import Enum +from typing import Optional class TracerEventType(Enum): diff --git a/locklib/locks/tracer/tracer.py b/locklib/locks/tracer/tracer.py index 2811680..27e120f 100644 --- a/locklib/locks/tracer/tracer.py +++ b/locklib/locks/tracer/tracer.py @@ -1,11 +1,11 @@ -from typing import List, Dict, Optional, Type -from types import TracebackType -from threading import get_ident from collections import defaultdict +from threading import get_ident +from types import TracebackType +from typing import Dict, List, Optional, Type -from locklib.protocols.lock import LockProtocol -from locklib.locks.tracer.events import TracerEvent, TracerEventType from locklib.errors import StrangeEventOrderError +from locklib.locks.tracer.events import TracerEvent, TracerEventType +from locklib.protocols.lock import LockProtocol class LockTraceWrapper: @@ -25,7 +25,7 @@ def acquire(self) -> None: TracerEvent( TracerEventType.ACQUIRE, thread_id=get_ident(), - ) + ), ) def release(self) -> None: @@ -34,7 +34,7 @@ def release(self) -> None: TracerEvent( TracerEventType.RELEASE, thread_id=get_ident(), - ) + ), ) def notify(self, identifier: str) -> None: @@ -43,7 +43,7 @@ def notify(self, identifier: str) -> None: TracerEventType.ACTION, thread_id=get_ident(), identifier=identifier, - ) + ), ) def was_event_locked(self, identifier: str) -> bool: diff --git a/locklib/protocols/async_context_lock.py b/locklib/protocols/async_context_lock.py index 2c18a58..70e4e55 100644 --- a/locklib/protocols/async_context_lock.py +++ b/locklib/protocols/async_context_lock.py @@ -1,7 +1,5 @@ -from typing import Type, Optional, Any from types import TracebackType - -from typing import Protocol, runtime_checkable +from typing import Any, Optional, Protocol, Type, runtime_checkable from locklib.protocols.lock import LockProtocol diff --git a/locklib/protocols/context_lock.py b/locklib/protocols/context_lock.py index 4bb3e05..9ec0afb 100644 --- a/locklib/protocols/context_lock.py +++ b/locklib/protocols/context_lock.py @@ -1,10 +1,13 @@ -from typing import Type, Optional, Any from types import TracebackType +from typing import Any, Optional, Type try: from typing import Protocol, runtime_checkable except ImportError: # pragma: no cover - from typing_extensions import Protocol, runtime_checkable # type: ignore[assignment] + from typing_extensions import ( # type: ignore[assignment] + Protocol, + runtime_checkable, + ) from locklib.protocols.lock import LockProtocol diff --git a/locklib/protocols/lock.py b/locklib/protocols/lock.py index 61d291e..ebe98df 100644 --- a/locklib/protocols/lock.py +++ b/locklib/protocols/lock.py @@ -1,10 +1,14 @@ try: from typing import Protocol, runtime_checkable except ImportError: # pragma: no cover - from typing_extensions import Protocol, runtime_checkable # type: ignore[assignment] + from typing_extensions import ( # type: ignore[assignment] + Protocol, + runtime_checkable, + ) from typing import Any + @runtime_checkable class LockProtocol(Protocol): def acquire(self) -> Any: diff --git a/pyproject.toml b/pyproject.toml index 700c7a4..a7944e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta' [project] name = 'locklib' -version = '0.0.18' +version = '0.0.19' authors = [ { name='Evgeniy Blinov', email='zheni-b@yandex.ru' }, ] @@ -23,6 +23,8 @@ classifiers = [ 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: 3.14', + 'Programming Language :: Python :: Free Threading', + 'Programming Language :: Python :: Free Threading :: 3 - Stable', 'License :: OSI Approved :: MIT License', 'Topic :: Software Development :: Libraries', 'Intended Audience :: Developers', @@ -42,6 +44,14 @@ keywords = [ paths_to_mutate="locklib" runner="pytest" +[tool.ruff] +lint.ignore = ['E501', 'E712', 'PTH123', 'PTH118', 'PLR2004', 'PTH107', 'SIM105', 'SIM102', 'RET503', 'PLR0912', 'C901'] +lint.select = ["ERA001", "YTT", "ASYNC", "BLE", "B", "A", "COM", "INP", "PIE", "T20", "PT", "RSE", "RET", "SIM", "SLOT", "TID252", "ARG", "PTH", "I", "C90", "N", "E", "W", "D201", "D202", "D419", "F", "PL", "PLE", "PLR", "PLW", "RUF", "TRY201", "TRY400", "TRY401"] +format.quote-style = "single" + +[tool.pytest.ini_options] +addopts = "-p no:warnings" + [project.urls] 'Source' = 'https://github.com/pomponchik/locklib' 'Tracker' = 'https://github.com/pomponchik/locklib/issues' diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 1ceab94..0000000 --- a/pytest.ini +++ /dev/null @@ -1,2 +0,0 @@ -[pytest] -addopts = -p no:warnings diff --git a/requirements_dev.txt b/requirements_dev.txt index 30dca14..89f294b 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -4,7 +4,7 @@ coverage==7.6.1 twine==6.1.0 wheel==0.41.2 build==1.2.2.post1 -ruff==0.9.9 +ruff==0.14.6 mypy==1.14.1 mutmut==3.2.3 full_match==0.0.3 diff --git a/tests/documentation/test_readme.py b/tests/documentation/test_readme.py index 46edd5d..be80115 100644 --- a/tests/documentation/test_readme.py +++ b/tests/documentation/test_readme.py @@ -1,8 +1,14 @@ -from multiprocessing import Lock as MLock -from threading import Lock as TLock, RLock as TRLock from asyncio import Lock as ALock - -from locklib import SmartLock, LockProtocol, ContextLockProtocol, AsyncContextLockProtocol +from multiprocessing import Lock as MLock +from threading import Lock as TLock +from threading import RLock as TRLock + +from locklib import ( + AsyncContextLockProtocol, + ContextLockProtocol, + LockProtocol, + SmartLock, +) def test_lock_protocols_basic(): diff --git a/tests/units/locks/smart_lock/test_graph.py b/tests/units/locks/smart_lock/test_graph.py index 32e2196..a31bba6 100644 --- a/tests/units/locks/smart_lock/test_graph.py +++ b/tests/units/locks/smart_lock/test_graph.py @@ -1,7 +1,7 @@ import pytest -from locklib.locks.smart_lock.graph import LocksGraph from locklib.errors import DeadLockError +from locklib.locks.smart_lock.graph import LocksGraph def test_multiple_set_and_get(): @@ -59,7 +59,7 @@ def test_delete_non_existing_link(): graph.delete_link(1, 3) - assert graph.get_links_from(1) == {2,} + assert graph.get_links_from(1) == {2} def test_detect_simple_cycle(): diff --git a/tests/units/locks/smart_lock/test_lock.py b/tests/units/locks/smart_lock/test_lock.py index 1127fd5..fd1949f 100644 --- a/tests/units/locks/smart_lock/test_lock.py +++ b/tests/units/locks/smart_lock/test_lock.py @@ -1,11 +1,11 @@ -from time import sleep from queue import Queue -from threading import Thread, Lock +from threading import Lock, Thread +from time import sleep import pytest from full_match import match -from locklib import SmartLock, DeadLockError +from locklib import DeadLockError, SmartLock def test_release_unlocked(): @@ -54,10 +54,9 @@ def function_1(): nonlocal flag try: while True: - with lock_1: - with lock_2: - if flag: - break + with lock_1, lock_2: + if flag: + break except DeadLockError: flag = True queue.put(True) @@ -66,10 +65,9 @@ def function_2(): nonlocal flag try: while True: - with lock_2: - with lock_1: - if flag: - break + with lock_2, lock_1: + if flag: + break except DeadLockError: flag = True queue.put(True) @@ -86,7 +84,7 @@ def function_2(): @pytest.mark.timeout(5) -def test_raise_when_not_so_simple_deadlock(): +def test_raise_when_not_so_simple_deadlock(): # noqa: PLR0915 number_of_attempts = 50 lock_1 = SmartLock() @@ -112,7 +110,7 @@ def function_1(): if flag: break except DeadLockError: - with lock: + with lock: # noqa: B023 cycles += 1 if cycles == 2: flag = True @@ -131,7 +129,7 @@ def function_2(): if flag: break except DeadLockError: - with lock: + with lock: # noqa: B023 cycles += 1 if cycles == 2: flag = True @@ -150,7 +148,7 @@ def function_3(): if flag: break except DeadLockError: - with lock: + with lock: # noqa: B023 cycles += 1 if cycles == 2: flag = True diff --git a/tests/units/locks/tracer/test_tracer.py b/tests/units/locks/tracer/test_tracer.py index 4ed9ee4..fcca42c 100644 --- a/tests/units/locks/tracer/test_tracer.py +++ b/tests/units/locks/tracer/test_tracer.py @@ -1,6 +1,6 @@ -from typing import List, Union from threading import Lock, Thread, get_ident from time import sleep +from typing import List, Union import pytest diff --git a/tests/units/protocols/test_async_context_lock.py b/tests/units/protocols/test_async_context_lock.py index ca2d825..4286623 100644 --- a/tests/units/protocols/test_async_context_lock.py +++ b/tests/units/protocols/test_async_context_lock.py @@ -1,7 +1,8 @@ -from multiprocessing import Lock as MLock -from threading import Lock as TLock, RLock as TRLock from asyncio import Lock as ALock from contextlib import asynccontextmanager +from multiprocessing import Lock as MLock +from threading import Lock as TLock +from threading import RLock as TRLock import pytest from full_match import match diff --git a/tests/units/protocols/test_context_lock.py b/tests/units/protocols/test_context_lock.py index 90bb1d7..de51a7a 100644 --- a/tests/units/protocols/test_context_lock.py +++ b/tests/units/protocols/test_context_lock.py @@ -1,8 +1,9 @@ import sys -from multiprocessing import Lock as MLock -from threading import Lock as TLock, RLock as TRLock from asyncio import Lock as ALock from contextlib import contextmanager +from multiprocessing import Lock as MLock +from threading import Lock as TLock +from threading import RLock as TRLock import pytest from full_match import match @@ -44,7 +45,6 @@ def test_asyncio_lock_is_not_just_context_lock(): # type: ignore[no-untyped-def asyncio lock is an instance of the AsyncContextLockProtocol, not just ContextLockProtocol. But! In python 3.8 it is both. """ - print(sys.version_info) assert not isinstance(ALock(), ContextLockProtocol) diff --git a/tests/units/protocols/test_lock.py b/tests/units/protocols/test_lock.py index a904711..c641c97 100644 --- a/tests/units/protocols/test_lock.py +++ b/tests/units/protocols/test_lock.py @@ -1,6 +1,7 @@ -from multiprocessing import Lock as MLock -from threading import Lock as TLock, RLock as TRLock from asyncio import Lock as ALock +from multiprocessing import Lock as MLock +from threading import Lock as TLock +from threading import RLock as TRLock import pytest from full_match import match diff --git a/tests/units/test_errors.py b/tests/units/test_errors.py index 7120015..e4dc03b 100644 --- a/tests/units/test_errors.py +++ b/tests/units/test_errors.py @@ -1,8 +1,9 @@ import pytest +from full_match import match from locklib.errors import DeadLockError def test_raise(): - with pytest.raises(DeadLockError): - raise DeadLockError() + with pytest.raises(DeadLockError, match=match('some message')): + raise DeadLockError('some message')