From e20727d84fb8a5255e0574f454ddd2e1af43bbdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 13 Feb 2026 16:58:24 +0300 Subject: [PATCH 01/10] Add ThereWasNoSuchEvent exception and update was_event_locked to handle missing events gracefully --- locklib/__init__.py | 2 +- locklib/errors.py | 3 +++ locklib/locks/tracer/tracer.py | 21 +++++++++++++++------ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/locklib/__init__.py b/locklib/__init__.py index 767fef9..39b51c8 100644 --- a/locklib/__init__.py +++ b/locklib/__init__.py @@ -1,6 +1,6 @@ from locklib.errors import DeadLockError as DeadLockError from locklib.errors import ( - StrangeEventOrderError as StrangeEventOrderError, + StrangeEventOrderError as StrangeEventOrderError, ThereWasNoSuchEvent as ThereWasNoSuchEvent, ) from locklib.locks.smart_lock.lock import SmartLock as SmartLock from locklib.locks.tracer.tracer import ( diff --git a/locklib/errors.py b/locklib/errors.py index 17e4b2f..4205b17 100644 --- a/locklib/errors.py +++ b/locklib/errors.py @@ -3,3 +3,6 @@ class DeadLockError(Exception): class StrangeEventOrderError(Exception): pass + +class ThereWasNoSuchEvent(Exception): + pass diff --git a/locklib/locks/tracer/tracer.py b/locklib/locks/tracer/tracer.py index 27e120f..e8ec14d 100644 --- a/locklib/locks/tracer/tracer.py +++ b/locklib/locks/tracer/tracer.py @@ -3,7 +3,7 @@ from types import TracebackType from typing import Dict, List, Optional, Type -from locklib.errors import StrangeEventOrderError +from locklib.errors import StrangeEventOrderError, ThereWasNoSuchEvent from locklib.locks.tracer.events import TracerEvent, TracerEventType from locklib.protocols.lock import LockProtocol @@ -46,9 +46,11 @@ def notify(self, identifier: str) -> None: ), ) - def was_event_locked(self, identifier: str) -> bool: + def was_event_locked(self, identifier: str, raise_exception: bool = True) -> bool: stacks: Dict[int, List[TracerEvent]] = defaultdict(list) + there_was_action_with_this_identifier = False + for event in self.trace: stack = stacks[event.thread_id] @@ -57,11 +59,18 @@ def was_event_locked(self, identifier: str) -> bool: elif event.type == TracerEventType.RELEASE: if not stack: - raise StrangeEventOrderError('Release event without corresponding acquire event.') + if raise_exception: + raise StrangeEventOrderError('Release event without corresponding acquire event.') + return False stack.pop() elif event.type == TracerEventType.ACTION: - if event.identifier == identifier and not stack: - return False + if event.identifier == identifier: + there_was_action_with_this_identifier = True + if not stack: + return False + + if (not there_was_action_with_this_identifier) and raise_exception: + raise ThereWasNoSuchEvent(f'No events with identifier "{identifier}" occurred in any of the threads, so the question "was it thread-safe" is meaningless.') - return True + return there_was_action_with_this_identifier From 0e70ed7e27513b2a476adbc8fd821270fc853a44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 13 Feb 2026 16:58:42 +0300 Subject: [PATCH 02/10] Update test cases to handle missing events gracefully and add proper exception matching --- tests/units/locks/tracer/test_tracer.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/units/locks/tracer/test_tracer.py b/tests/units/locks/tracer/test_tracer.py index fcca42c..04af2da 100644 --- a/tests/units/locks/tracer/test_tracer.py +++ b/tests/units/locks/tracer/test_tracer.py @@ -3,8 +3,9 @@ from typing import List, Union import pytest +from full_match import match -from locklib import LockTraceWrapper, StrangeEventOrderError +from locklib import LockTraceWrapper, StrangeEventOrderError, ThereWasNoSuchEvent from locklib.locks.tracer.events import TracerEvent, TracerEventType @@ -76,7 +77,10 @@ def release(self): def test_event_is_locked_if_there_was_no_events(): wrapper = LockTraceWrapper(Lock()) - assert wrapper.was_event_locked('kek') + assert not wrapper.was_event_locked('kek', raise_exception=False) + + with pytest.raises(ThereWasNoSuchEvent, match=match('No events with identifier "kek" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')): + wrapper.was_event_locked('kek') def test_event_is_locked_if_there_are_only_opening_and_slosing_events(): @@ -85,7 +89,10 @@ def test_event_is_locked_if_there_are_only_opening_and_slosing_events(): with wrapper: pass - assert wrapper.was_event_locked('kek') + assert not wrapper.was_event_locked('kek', raise_exception=False) + + with pytest.raises(ThereWasNoSuchEvent, match=match('No events with identifier "kek" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')): + wrapper.was_event_locked('kek') def test_simple_case_of_locked_event(): @@ -208,4 +215,7 @@ def test_unknown_event_type(): wrapper.trace.append(TracerEvent('unknown', 1)) - assert wrapper.was_event_locked('lol') + assert not wrapper.was_event_locked('lol', raise_exception=False) + + with pytest.raises(ThereWasNoSuchEvent, match=match('No events with identifier "lol" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')): + wrapper.was_event_locked('lol') From e2b299f6558c2fd6e8cea44343827c37f59111b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 13 Feb 2026 16:59:02 +0300 Subject: [PATCH 03/10] Silence exceptions by replacing pass with ellipsis --- locklib/errors.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/locklib/errors.py b/locklib/errors.py index 4205b17..2ea185f 100644 --- a/locklib/errors.py +++ b/locklib/errors.py @@ -1,8 +1,8 @@ class DeadLockError(Exception): - pass + ... class StrangeEventOrderError(Exception): - pass + ... class ThereWasNoSuchEvent(Exception): - pass + ... From 5d8e22f9e1c4d58a28103c1f7d5d81108ef52cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 13 Feb 2026 16:59:28 +0300 Subject: [PATCH 04/10] Fix import order and remove duplicate alias in __init__.py --- locklib/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/locklib/__init__.py b/locklib/__init__.py index 39b51c8..971ea78 100644 --- a/locklib/__init__.py +++ b/locklib/__init__.py @@ -1,6 +1,9 @@ from locklib.errors import DeadLockError as DeadLockError from locklib.errors import ( - StrangeEventOrderError as StrangeEventOrderError, ThereWasNoSuchEvent as ThereWasNoSuchEvent, + StrangeEventOrderError as StrangeEventOrderError, +) +from locklib.errors import ( + ThereWasNoSuchEvent as ThereWasNoSuchEvent, ) from locklib.locks.smart_lock.lock import SmartLock as SmartLock from locklib.locks.tracer.tracer import ( From 622f6b092d5dcd58b5a25309f8514e0c819fe7f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 13 Feb 2026 17:00:17 +0300 Subject: [PATCH 05/10] Rename ThereWasNoSuchEvent to ThereWasNoSuchEventError --- locklib/__init__.py | 2 +- locklib/errors.py | 2 +- locklib/locks/tracer/tracer.py | 4 ++-- tests/units/locks/tracer/test_tracer.py | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/locklib/__init__.py b/locklib/__init__.py index 971ea78..47b3b05 100644 --- a/locklib/__init__.py +++ b/locklib/__init__.py @@ -3,7 +3,7 @@ StrangeEventOrderError as StrangeEventOrderError, ) from locklib.errors import ( - ThereWasNoSuchEvent as ThereWasNoSuchEvent, + ThereWasNoSuchEventError as ThereWasNoSuchEventError, ) from locklib.locks.smart_lock.lock import SmartLock as SmartLock from locklib.locks.tracer.tracer import ( diff --git a/locklib/errors.py b/locklib/errors.py index 2ea185f..90ffabb 100644 --- a/locklib/errors.py +++ b/locklib/errors.py @@ -4,5 +4,5 @@ class DeadLockError(Exception): class StrangeEventOrderError(Exception): ... -class ThereWasNoSuchEvent(Exception): +class ThereWasNoSuchEventError(Exception): ... diff --git a/locklib/locks/tracer/tracer.py b/locklib/locks/tracer/tracer.py index e8ec14d..aec7fc9 100644 --- a/locklib/locks/tracer/tracer.py +++ b/locklib/locks/tracer/tracer.py @@ -3,7 +3,7 @@ from types import TracebackType from typing import Dict, List, Optional, Type -from locklib.errors import StrangeEventOrderError, ThereWasNoSuchEvent +from locklib.errors import StrangeEventOrderError, ThereWasNoSuchEventError from locklib.locks.tracer.events import TracerEvent, TracerEventType from locklib.protocols.lock import LockProtocol @@ -71,6 +71,6 @@ def was_event_locked(self, identifier: str, raise_exception: bool = True) -> boo return False if (not there_was_action_with_this_identifier) and raise_exception: - raise ThereWasNoSuchEvent(f'No events with identifier "{identifier}" occurred in any of the threads, so the question "was it thread-safe" is meaningless.') + raise ThereWasNoSuchEventError(f'No events with identifier "{identifier}" occurred in any of the threads, so the question "was it thread-safe" is meaningless.') return there_was_action_with_this_identifier diff --git a/tests/units/locks/tracer/test_tracer.py b/tests/units/locks/tracer/test_tracer.py index 04af2da..a3546ed 100644 --- a/tests/units/locks/tracer/test_tracer.py +++ b/tests/units/locks/tracer/test_tracer.py @@ -5,7 +5,7 @@ import pytest from full_match import match -from locklib import LockTraceWrapper, StrangeEventOrderError, ThereWasNoSuchEvent +from locklib import LockTraceWrapper, StrangeEventOrderError, ThereWasNoSuchEventError from locklib.locks.tracer.events import TracerEvent, TracerEventType @@ -79,7 +79,7 @@ def test_event_is_locked_if_there_was_no_events(): assert not wrapper.was_event_locked('kek', raise_exception=False) - with pytest.raises(ThereWasNoSuchEvent, match=match('No events with identifier "kek" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')): + with pytest.raises(ThereWasNoSuchEventError, match=match('No events with identifier "kek" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')): wrapper.was_event_locked('kek') @@ -91,7 +91,7 @@ def test_event_is_locked_if_there_are_only_opening_and_slosing_events(): assert not wrapper.was_event_locked('kek', raise_exception=False) - with pytest.raises(ThereWasNoSuchEvent, match=match('No events with identifier "kek" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')): + with pytest.raises(ThereWasNoSuchEventError, match=match('No events with identifier "kek" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')): wrapper.was_event_locked('kek') @@ -217,5 +217,5 @@ def test_unknown_event_type(): assert not wrapper.was_event_locked('lol', raise_exception=False) - with pytest.raises(ThereWasNoSuchEvent, match=match('No events with identifier "lol" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')): + with pytest.raises(ThereWasNoSuchEventError, match=match('No events with identifier "lol" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')): wrapper.was_event_locked('lol') From cb8bcd8f0df9b0b85a61cdfce349d069ebcd48eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 13 Feb 2026 17:02:02 +0300 Subject: [PATCH 06/10] Bump version to 0.0.20 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a7944e9..df92a68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta' [project] name = 'locklib' -version = '0.0.19' +version = '0.0.20' authors = [ { name='Evgeniy Blinov', email='zheni-b@yandex.ru' }, ] From 43038c78ae826fe1adb489b4d55f017c3c0f426e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 13 Feb 2026 17:03:17 +0300 Subject: [PATCH 07/10] More test cases --- tests/units/locks/tracer/test_tracer.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/units/locks/tracer/test_tracer.py b/tests/units/locks/tracer/test_tracer.py index a3546ed..5a69c94 100644 --- a/tests/units/locks/tracer/test_tracer.py +++ b/tests/units/locks/tracer/test_tracer.py @@ -73,6 +73,11 @@ def release(self): with pytest.raises(StrangeEventOrderError): wrapper.was_event_locked('kek') + with pytest.raises(StrangeEventOrderError): + wrapper.was_event_locked('kek', raise_exception=True) + + assert not wrapper.was_event_locked('kek', raise_exception=False) + def test_event_is_locked_if_there_was_no_events(): wrapper = LockTraceWrapper(Lock()) @@ -82,6 +87,9 @@ def test_event_is_locked_if_there_was_no_events(): with pytest.raises(ThereWasNoSuchEventError, match=match('No events with identifier "kek" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')): wrapper.was_event_locked('kek') + with pytest.raises(ThereWasNoSuchEventError, match=match('No events with identifier "kek" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')): + wrapper.was_event_locked('kek', raise_exception=True) + def test_event_is_locked_if_there_are_only_opening_and_slosing_events(): wrapper = LockTraceWrapper(Lock()) @@ -94,6 +102,9 @@ def test_event_is_locked_if_there_are_only_opening_and_slosing_events(): with pytest.raises(ThereWasNoSuchEventError, match=match('No events with identifier "kek" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')): wrapper.was_event_locked('kek') + with pytest.raises(ThereWasNoSuchEventError, match=match('No events with identifier "kek" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')): + wrapper.was_event_locked('kek', raise_exception=True) + def test_simple_case_of_locked_event(): wrapper = LockTraceWrapper(Lock()) @@ -219,3 +230,6 @@ def test_unknown_event_type(): with pytest.raises(ThereWasNoSuchEventError, match=match('No events with identifier "lol" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')): wrapper.was_event_locked('lol') + + with pytest.raises(ThereWasNoSuchEventError, match=match('No events with identifier "lol" occurred in any of the threads, so the question "was it thread-safe" is meaningless.')): + wrapper.was_event_locked('lol', raise_exception=True) From 17e49c0e1ad4d5dc657827819e6ab4f1a96e9b4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 13 Feb 2026 17:10:04 +0300 Subject: [PATCH 08/10] Fix thread identification wording in warning note --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0769462..2f3dc07 100644 --- a/README.md +++ b/README.md @@ -219,4 +219,4 @@ If the `notify` method was called with the same parameter only when the lock act 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. -> ⚠️ The thread id is used to identify the streams. 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. +> ⚠️ 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. From 6f2399426a36dbb62dc3545f5ad2e94d3d14d919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 13 Feb 2026 17:13:16 +0300 Subject: [PATCH 09/10] Add option to suppress exception on missing event --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 2f3dc07..6bb283c 100644 --- a/README.md +++ b/README.md @@ -220,3 +220,5 @@ If the `notify` method was called with the same parameter only when the lock act 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. > ⚠️ 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. + +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`. From c011b13b26bb035c52f095e6a210ec6c54d50632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 13 Feb 2026 17:15:28 +0300 Subject: [PATCH 10/10] Update README to reflect new repository owner "mutating" --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 6bb283c..1668121 100644 --- a/README.md +++ b/README.md @@ -3,21 +3,21 @@ [![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) -[![Coverage Status](https://coveralls.io/repos/github/pomponchik/locklib/badge.svg?branch=main)](https://coveralls.io/github/pomponchik/locklib?branch=main) -[![Lines of code](https://sloc.xyz/github/pomponchik/locklib/?category=code?)](https://github.com/boyter/scc/) -[![Hits-of-Code](https://hitsofcode.com/github/pomponchik/locklib?branch=main)](https://hitsofcode.com/github/pomponchik/locklib/view?branch=main) -[![Test-Package](https://github.com/pomponchik/locklib/actions/workflows/tests_and_coverage.yml/badge.svg)](https://github.com/pomponchik/locklib/actions/workflows/tests_and_coverage.yml) +[![Coverage Status](https://coveralls.io/repos/github/mutating/locklib/badge.svg?branch=main)](https://coveralls.io/github/mutating/locklib?branch=main) +[![Lines of code](https://sloc.xyz/github/mutating/locklib/?category=code?)](https://github.com/boyter/scc/) +[![Hits-of-Code](https://hitsofcode.com/github/mutating/locklib?branch=main)](https://hitsofcode.com/github/mutating/locklib/view?branch=main) +[![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) [![Python versions](https://img.shields.io/pypi/pyversions/locklib.svg)](https://pypi.python.org/pypi/locklib) [![PyPI version](https://badge.fury.io/py/locklib.svg)](https://badge.fury.io/py/locklib) [![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/) [![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) +[![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/mutating/locklib)

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

@@ -43,7 +43,7 @@ pip install locklib ... or directly from git: ```bash -pip install git+https://github.com/pomponchik/locklib.git +pip install git+https://github.com/mutating/locklib.git ``` You can also quickly try out this and other packages without having to install using [instld](https://github.com/pomponchik/instld).