Skip to content

Commit 726efbe

Browse files
committed
Initial commit
0 parents  commit 726efbe

6 files changed

Lines changed: 343 additions & 0 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# This workflow will upload a Python Package to PyPI when a release is created
2+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
3+
4+
# This workflow uses actions that are not certified by GitHub.
5+
# They are provided by a third-party and are governed by
6+
# separate terms of service, privacy policy, and support
7+
# documentation.
8+
9+
name: Upload Python Package
10+
11+
on:
12+
release:
13+
types: [published]
14+
15+
permissions:
16+
contents: read
17+
18+
jobs:
19+
release-build:
20+
runs-on: ubuntu-latest
21+
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- uses: actions/setup-python@v5
26+
with:
27+
python-version: "3.x"
28+
29+
- name: Build release distributions
30+
run: |
31+
# NOTE: put your own distribution build steps here.
32+
python -m pip install build
33+
python -m build
34+
35+
- name: Upload distributions
36+
uses: actions/upload-artifact@v4
37+
with:
38+
name: release-dists
39+
path: dist/
40+
41+
pypi-publish:
42+
runs-on: ubuntu-latest
43+
needs:
44+
- release-build
45+
permissions:
46+
# IMPORTANT: this permission is mandatory for trusted publishing
47+
id-token: write
48+
49+
# Dedicated environments with protections for publishing are strongly recommended.
50+
# For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
51+
environment:
52+
name: pypi
53+
# OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
54+
# url: https://pypi.org/p/YOURPROJECT
55+
#
56+
# ALTERNATIVE: if your GitHub Release name is the PyPI project version string
57+
# ALTERNATIVE: exactly, uncomment the following line instead:
58+
# url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }}
59+
60+
steps:
61+
- name: Retrieve release distributions
62+
uses: actions/download-artifact@v4
63+
with:
64+
name: release-dists
65+
path: dist/
66+
67+
- name: Publish release distributions to PyPI
68+
uses: pypa/gh-action-pypi-publish@release/v1
69+
with:
70+
packages-dir: dist/

.github/workflows/python-test.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3+
4+
name: Run Tests
5+
6+
on:
7+
push:
8+
branches: [ "main" ]
9+
pull_request:
10+
branches: [ "main" ]
11+
12+
jobs:
13+
build:
14+
15+
runs-on: ubuntu-latest
16+
strategy:
17+
fail-fast: false
18+
matrix:
19+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
20+
21+
steps:
22+
- uses: actions/checkout@v4
23+
- name: Set up Python ${{ matrix.python-version }}
24+
uses: actions/setup-python@v3
25+
with:
26+
python-version: ${{ matrix.python-version }}
27+
- name: Install dependencies
28+
run: |
29+
python -m pip install --upgrade pip
30+
python -m pip install pytest
31+
python -m pip install .
32+
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
33+
- name: Test with pytest
34+
run: |
35+
pytest

README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Dirlock
2+
3+
[![PyPi badge](https://img.shields.io/pypi/v/dirlock)](https://pypi.org/project/dirlock/)
4+
![pytest badge](https://github.com/z3rone-org/dirlock/actions/workflows/python-test.yml/badge.svg)
5+
6+
A simple directory-based lock implementation for Python. This package provides a lightweight and effective way
7+
to coordinate access to shared resources using a lock directory mechanism.
8+
This does not require any file locking capabilities of the underlying filesystem or network share.
9+
10+
## Installation
11+
12+
Install the package via pip:
13+
14+
```bash
15+
pip install dirlock
16+
```
17+
18+
## Usage
19+
20+
You can use the `DirLock` class to acquire and release locks in your Python code. The class also supports usage within a `with` clause for convenience.
21+
22+
### Example: Using the Lock Explicitly
23+
24+
```python
25+
import time
26+
from dirlock import DirLock
27+
28+
lock_dir_path = "/tmp/mylockdir.lock"
29+
lock = DirLock(lock_dir_path)
30+
31+
print("Acquire lock...")
32+
lock.acquire()
33+
print("Lock acquired!")
34+
35+
# Perform critical section tasks here
36+
# Simulating work
37+
time.sleep(5)
38+
39+
# Release the lock
40+
lock.release()
41+
print("Lock released.")
42+
```
43+
44+
### Example: Using the Lock in a `with` Clause
45+
46+
```python
47+
import time
48+
from dirlock import DirLock
49+
50+
lock_dir_path = "/tmp/mylockdir.lock"
51+
52+
with DirLock(lock_dir_path) as lock:
53+
print("Lock acquired!")
54+
# Perform critical section tasks
55+
time.sleep(5) # Simulate work
56+
print("Work done!")
57+
58+
# The lock is automatically released when the block exits.
59+
print("Lock released.")
60+
```
61+
62+
## Parameters
63+
64+
- `lock_dir` (str): The path to the lock directory.
65+
- `retry_interval` (float, default=0.1): Time to wait before retrying if the lock cannot be acquired.
66+
- `timeout_interval` (float, default=-1): Timeout for acquiring lock. Set to negative value for no timeout.
67+
68+
## Change Default Values
69+
You can change the default value for `retry_interval`
70+
via `DirLock.retry_interval=<new_value>`.
71+
72+
## License
73+
74+
This package is licensed under the MIT License.
75+

dirlock/__init__.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import os
2+
import time
3+
from datetime import datetime
4+
5+
6+
class DirLock:
7+
default_retry_interval: float = 0.1
8+
def __init__(self,
9+
lock_dir: str,
10+
retry_interval: float = None,
11+
timeout_interval: float = -1):
12+
"""
13+
Initialize the DirLock.
14+
15+
Args:
16+
lock_dir (str): Path to the lock directory.
17+
retry_interval (float): Time to wait before retrying (in seconds).
18+
timeout_interval (float): Timeout
19+
"""
20+
self.lock_dir = str(lock_dir)
21+
self.timeout_interval = timeout_interval
22+
23+
if retry_interval is None:
24+
self.retry_interval = DirLock.default_retry_interval
25+
else:
26+
self.retry_interval = retry_interval
27+
self.acquired = False
28+
29+
def acquire(self):
30+
"""
31+
Acquire the lock by following the directory-based lock mechanism.
32+
"""
33+
start_time = datetime.now()
34+
35+
while True:
36+
try:
37+
os.mkdir(self.lock_dir)
38+
self.acquired = True
39+
break
40+
except FileExistsError:
41+
pass
42+
43+
if self.timeout_interval >= 0.0:
44+
if (datetime.now() - start_time).total_seconds() > self.timeout_interval:
45+
raise LockTimeoutException()
46+
47+
# If the lock directory exists, retry
48+
time.sleep(self.retry_interval)
49+
50+
def release(self):
51+
"""
52+
Release the lock if it is held by this instance.
53+
"""
54+
try:
55+
os.rmdir(self.lock_dir)
56+
except FileNotFoundError:
57+
pass
58+
self.acquired = False
59+
60+
def __enter__(self):
61+
"""
62+
Context management entry point.
63+
"""
64+
self.acquire()
65+
return self
66+
67+
def __exit__(self, exc_type, exc_value, traceback):
68+
"""
69+
Context management exit point.
70+
"""
71+
self.release()
72+
73+
class LockTimeoutException(Exception):
74+
def __init__(self, message="acquiring lock timed out"):
75+
super().__init__(message)

setup.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from setuptools import setup, find_packages
2+
3+
setup(
4+
name="dirlock",
5+
description="A simple directory based lock implementation for Python.",
6+
use_scm_version=True,
7+
long_description=open("README.md", encoding="utf-8").read(),
8+
long_description_content_type="text/markdown",
9+
author="Falk B. Schimweg",
10+
author_email="git@falk.schimweg.de",
11+
url="https://github.com/z3rone-org/dirlock",
12+
license="MIT",
13+
packages=find_packages(),
14+
classifiers=[
15+
"Programming Language :: Python :: 3",
16+
"License :: OSI Approved :: MIT License",
17+
"Operating System :: OS Independent",
18+
],
19+
extras_require={
20+
"dev": [
21+
"pytest",
22+
]
23+
},
24+
setup_requires=['setuptools_scm'],
25+
python_requires=">=3.8",
26+
)
27+

tests/test_lock.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from datetime import datetime
2+
import os
3+
from dirlock import DirLock, LockTimeoutException
4+
5+
6+
def test_two_locks(tmpdir):
7+
lockdir = os.path.join(tmpdir, '.lock')
8+
lock1 = DirLock(lockdir)
9+
lock2 = DirLock(lockdir)
10+
11+
assert not lock1.acquired
12+
assert not lock2.acquired
13+
14+
# Acquire the lock
15+
lock1.acquire()
16+
17+
assert lock1.acquired
18+
assert not lock2.acquired
19+
20+
lock1.release()
21+
lock2.acquire()
22+
23+
assert not lock1.acquired
24+
assert lock2.acquired
25+
26+
lock2.release()
27+
28+
assert not lock1.acquired
29+
assert not lock2.acquired
30+
31+
32+
def test_timeout(tmpdir):
33+
lockdir = os.path.join(tmpdir, '.lock')
34+
lock1 = DirLock(lockdir)
35+
lock2 = DirLock(lockdir, timeout_interval=3)
36+
37+
lock1.acquire()
38+
assert lock1.acquired
39+
40+
try:
41+
lock2.acquire()
42+
assert False
43+
except LockTimeoutException:
44+
assert not lock2.acquired
45+
46+
lock1.release()
47+
assert not lock1.acquired
48+
assert not lock2.acquired
49+
50+
51+
def test_context_manager(tmpdir):
52+
lockdir = os.path.join(tmpdir, '.lock')
53+
lock = DirLock(str(lockdir))
54+
55+
# Use the context manager to acquire and release the lock
56+
with lock:
57+
# Check that the lock directory was created with the correct UUID
58+
assert os.path.exists(str(lockdir))
59+
60+
# Ensure that the lock directory is removed after exiting the context
61+
assert not os.path.exists(str(lockdir))

0 commit comments

Comments
 (0)