Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 0 additions & 34 deletions .github/workflows/publish.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/tests_and_coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion .ruff.toml

This file was deleted.

11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
![logo](https://raw.githubusercontent.com/pomponchik/locklib/develop/docs/assets/logo_7.svg)
<details>
<summary>ⓘ</summary>

[![Downloads](https://static.pepy.tech/badge/locklib/month)](https://pepy.tech/project/locklib)
[![Downloads](https://static.pepy.tech/badge/locklib)](https://pepy.tech/project/locklib)
Expand All @@ -12,6 +13,14 @@
[![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)
[![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/pomponchik/locklib)

</details>

<p align="center">

![logo](https://raw.githubusercontent.com/pomponchik/locklib/develop/docs/assets/logo_7.svg)

</p>

It contains several useful additions to the standard thread synchronization tools, such as lock protocols and locks with advanced functionality.


Expand Down
22 changes: 15 additions & 7 deletions locklib/__init__.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions locklib/locks/smart_lock/graph.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
61 changes: 31 additions & 30 deletions locklib/locks/smart_lock/lock.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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()
4 changes: 2 additions & 2 deletions locklib/locks/tracer/events.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
16 changes: 8 additions & 8 deletions locklib/locks/tracer/tracer.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -25,7 +25,7 @@ def acquire(self) -> None:
TracerEvent(
TracerEventType.ACQUIRE,
thread_id=get_ident(),
)
),
)

def release(self) -> None:
Expand All @@ -34,7 +34,7 @@ def release(self) -> None:
TracerEvent(
TracerEventType.RELEASE,
thread_id=get_ident(),
)
),
)

def notify(self, identifier: str) -> None:
Expand All @@ -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:
Expand Down
4 changes: 1 addition & 3 deletions locklib/protocols/async_context_lock.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
7 changes: 5 additions & 2 deletions locklib/protocols/context_lock.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
6 changes: 5 additions & 1 deletion locklib/protocols/lock.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
12 changes: 11 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
]
Expand All @@ -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',
Expand All @@ -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'
2 changes: 0 additions & 2 deletions pytest.ini

This file was deleted.

2 changes: 1 addition & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 10 additions & 4 deletions tests/documentation/test_readme.py
Original file line number Diff line number Diff line change
@@ -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():
Expand Down
4 changes: 2 additions & 2 deletions tests/units/locks/smart_lock/test_graph.py
Original file line number Diff line number Diff line change
@@ -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():
Expand Down Expand Up @@ -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():
Expand Down
Loading
Loading