New Upstream Release - python-fasteners
Ready changes
Summary
Merged new upstream version: 0.18 (was: 0.17.3).
Resulting package
Built on 2023-01-31T17:26 (took 4m3s)
The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:
apt install -t fresh-releases python3-fasteners
Lintian Result
Diff
diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml
new file mode 100644
index 0000000..c697c89
--- /dev/null
+++ b/.github/workflows/build_docs.yml
@@ -0,0 +1,51 @@
+name: GitHub Pages
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+
+jobs:
+ deploy:
+ runs-on: ubuntu-20.04
+ concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Setup Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: '3.8'
+
+ - name: Upgrade pip
+ run: |
+ # install pip=>20.1 to use "pip cache dir"
+ python3 -m pip install --upgrade pip
+
+ - name: Get pip cache dir
+ id: pip-cache
+ run: echo "::set-output name=dir::$(pip cache dir)"
+
+ - name: Cache dependencies
+ uses: actions/cache@v2
+ with:
+ path: ${{ steps.pip-cache.outputs.dir }}
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+
+ - name: Install dependencies
+ run: python3 -m pip install -r ./requirements-docs.txt
+
+ - name: Install itself
+ run: python3 -m pip install -e .
+
+ - run: mkdocs build
+
+ - name: Deploy
+ uses: peaceiris/actions-gh-pages@v3
+ if: ${{ github.ref == 'refs/heads/main' }}
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ./site
\ No newline at end of file
diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml
index f498d48..85766b0 100644
--- a/.github/workflows/run_tests.yml
+++ b/.github/workflows/run_tests.yml
@@ -5,9 +5,9 @@ name: run tests
on:
push:
- branches: [ master ]
+ branches: [ main ]
pull_request:
- branches: [ master ]
+ branches: [ main ]
jobs:
build:
@@ -16,15 +16,12 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
- python-version: [3.6, 3.7, 3.8, 3.9, '3.10', pypy3]
- exclude:
- - os: macos-latest
- python-version: pypy3
+ python-version: [3.7, 3.8, 3.9, '3.10', 'pypy3.7', 'pypy3.9']
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
@@ -34,4 +31,5 @@ jobs:
python -m pip install -r requirements-test.txt
- name: Test with pytest
run: |
- pytest
+ pytest tests/
+ pytest tests_eventlet/
diff --git a/.gitignore b/.gitignore
index ca1e961..3895d47 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,6 @@
*.egg-info
__pycache__
-dist
-build
+dist/
+build/
+site/
+venv/
diff --git a/CHANGELOG b/CHANGELOG
deleted file mode 100644
index 2207e9c..0000000
--- a/CHANGELOG
+++ /dev/null
@@ -1,120 +0,0 @@
-0.17.3:
- - Allow writer to become a reader in thread ReaderWriter lock
-
-0.17.2:
- - Remove unnecessary setuptools pin
-
-0.17.1:
- - Switch to the modern python package build infrastructure
-
-0.17: [NEVER RELEASED]
- - Remove support for python 3.5 and earlier, including 2.7
- - Add support for python 3.9 and 3.10
- - Fix a conflict with django lock
- - Add __version__ and __all__ attributes
- - Fix a failure to parse README as utf-8
- - Move from nosetest to pytest and cleanup testing infrastructure
-
-0.16.3:
- - Fix a failure to parse README as utf-8 on python2
-
-0.16.2:
- - Fix a failure to parse README as utf-8
-
-0.16.1: [YANKED]
-
-0.16:
- - Move from travis and appveyor to github actions
- - Add interprocess reader writer lock
- - Improve README
- - remove unused eventlet import
- - use stdlib monotonic instead of external for python >= 3.4
-
-0.15:
- - Add testing for additional python versions
- - Remove python 2.6 support
- - Remove eventlet dependency and use
- threading.current_thread instead
-
-0.14:
- - Allow providing a custom exception logger
- to 'locked' decorator
- - Allow providing a custom logger to process
- lock class
- - Fix issue #12
-
-0.13:
- - Fix 'ensure_tree' check on freebsd
-
-0.12:
- - Use a tiny retry util helper class
- for performing process locking
- retries.
-
-0.11:
- - Directly use monotonic.monotonic.
- - Use BLATHER level for previously
- INFO/DEBUG statements.
-
-0.10:
- - Add LICENSE in generated source tarballs
- - Add a version.py file that can be used to
- extract the current version.
-
-0.9:
- - Allow providing a non-standard (eventlet or
- other condition class) to the r/w lock for
- cases where it is useful to do so.
- - Instead of having the r/w lock take a find
- eventlet keyword argument, allow for it to
- be provided a function that will be later
- called to get the current thread. This allows
- for the current *hack* to be easily removed
- by users (if they so desire).
-
-0.8:
- - Add fastener logo (from openclipart).
- - Ensure r/w writer -> reader -> writer
- lock acquisition.
- - Attempt to use the monotonic pypi module
- if its installed for monotonically increasing
- time on python versions where this is not
- built-in.
-
-0.7:
- - Add helpful `locked` decorator that can
- lock a method using a found attribute (a lock
- object or list of lock objects) in the
- instance the method is attached to.
- - Expose top level `try_lock` function.
-
-0.6:
- - Allow the sleep function to be provided (so that
- various alternatives other than time.sleep can
- be used), ie eventlet.sleep (or other).
- - Remove dependency on oslo.utils (replace with
- small utility code that achieves the same effect).
-
-0.5:
- - Make it possible to provide a acquisition timeout
- to the interprocess lock (which when acquisition
- can not complete in the desired time will return
- false).
-
-0.4:
- - Have the interprocess lock acquire take a blocking
- keyword argument (defaulting to true) that can avoid
- blocking trying to acquire the lock
-
-0.3:
- - Renamed from 'shared_lock' to 'fasteners'
-
-0.2.1
- - Fix delay not working as expected
-
-0.2:
- - Add a interprocess lock
-
-0.1:
- - Add travis yaml file
- - Initial commit/import
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..64641c4
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,117 @@
+# ChangeLog
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
+
+## [Unreleased]
+
+## [0.18]
+ - Reshuffle the process lock code and properly document it.
+ - Revamp the docs and switch from sphinx to mkdocs
+ - Remove difficult to use tread lock features from docs
+ - Bring back support for eventlet `spawn_n`
+ - Remove support for python3.6. It should still work, but is no longer tested.
+
+## [0.17.3]:
+ - Allow writer to become a reader in thread ReaderWriter lock
+
+## [0.17.2]:
+ - Remove unnecessary setuptools pin
+
+## [0.17.1]:
+ - Switch to the modern python package build infrastructure
+
+## [0.17]: [NEVER RELEASED]
+ - Remove support for python 3.5 and earlier, including 2.7
+ - Add support for python 3.9 and 3.10
+ - Fix a conflict with django lock
+ - Add `__version__` and `__all__` attributes
+ - Fix a failure to parse README as utf-8
+ - Move from nosetest to pytest and cleanup testing infrastructure
+
+## [0.16.3]:
+ - Fix a failure to parse README as utf-8 on python2
+
+## [0.16.2]:
+ - Fix a failure to parse README as utf-8
+
+## [0.16.1]: [YANKED]
+
+## [0.16]:
+ - Move from travis and appveyor to github actions
+ - Add interprocess reader writer lock
+ - Improve README
+ - remove unused eventlet import
+ - use stdlib monotonic instead of external for python >= 3.4
+
+## [0.15]:
+ - Add testing for additional python versions
+ - Remove python 2.6 support
+ - Remove eventlet dependency and use
+ threading.current_thread instead
+
+## [0.14]:
+ - Allow providing a custom exception logger to 'locked' decorator
+ - Allow providing a custom logger to process lock class
+ - Fix issue #12
+
+## [0.13]:
+ - Fix 'ensure_tree' check on freebsd
+
+## [0.12]:
+ - Use a tiny retry util helper class for performing process locking retries.
+
+## [0.11]:
+ - Directly use monotonic.monotonic.
+ - Use BLATHER level for previously INFO/DEBUG statements.
+
+## [0.10]:
+ - Add LICENSE in generated source tarballs
+ - Add a version.py file that can be used to extract the current version.
+
+## [0.9]:
+ - Allow providing a non-standard (eventlet or other condition class) to the
+ r/w lock for cases where it is useful to do so.
+ - Instead of having the r/w lock take a find eventlet keyword argument, allow
+ for it to be provided a function that will be later called to get the
+ current thread. This allows for the current *hack* to be easily removed
+ by users (if they so desire).
+
+## [0.8]:
+ - Add fastener logo (from openclipart).
+ - Ensure r/w writer -> reader -> writer lock acquisition.
+ - Attempt to use the monotonic pypi module if its installed for monotonically
+ increasing time on python versions where this is not built-in.
+
+## [0.7]:
+ - Add helpful `locked` decorator that can lock a method using a found
+ attribute (a lock object or list of lock objects) in the instance the method
+ is attached to.
+ - Expose top level `try_lock` function.
+
+## [0.6]:
+ - Allow the sleep function to be provided (so that various alternatives other
+ than time.sleep can be used), ie eventlet.sleep (or other).
+ - Remove dependency on oslo.utils (replace with small utility code that
+ achieves the same effect).
+
+## [0.5]:
+ - Make it possible to provide an acquisition timeout to the interprocess lock
+ (which when acquisition can not complete in the desired time will return
+ false).
+
+## [0.4]:
+ - Have the interprocess lock acquire take a blocking keyword argument
+ (defaulting to true) that can avoid blocking trying to acquire the lock
+
+## [0.3]:
+ - Renamed from 'shared_lock' to 'fasteners'
+
+## [0.2.1]
+ - Fix delay not working as expected
+
+## [0.2]:
+ - Add a interprocess lock
+
+## [0.1]:
+ - Add travis yaml file
+ - Initial commit/import
diff --git a/MANIFEST.in b/MANIFEST.in
index f7aec42..fc49293 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1 +1 @@
-include CHANGELOG
+include CHANGELOG.md
diff --git a/README.md b/README.md
index 65e5023..3504c65 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@ Fasteners
[![Downloads](https://img.shields.io/pypi/dm/fasteners.svg)](https://pypi.python.org/pypi/fasteners/)
[![Latest version](https://img.shields.io/pypi/v/fasteners.svg)](https://pypi.python.org/pypi/fasteners/)
-Cross platform locks for threads and processes.
+Cross-platform locks for threads and processes.
🔩 Install
----------
@@ -67,6 +67,8 @@ rw_lock.release_write_lock()
Python standard library provides a lock for threads (both a reentrant one, and a
non-reentrant one, see below). Fasteners extends this, and provides a lock for
processes, as well as Reader Writer locks for both threads and processes.
+Definitions of terms used in this overview can be found in the
+[glossary](https://fasteners.readthedocs.io/en/latest/guide/glossary/).
The specifics of the locks are as follows:
@@ -96,7 +98,7 @@ features are as follows:
### Thread locks
-Fasteners do not provide a simple thread lock, but for the sake of comparison note that the `threading` module
+Fasteners does not provide a simple thread lock, but for the sake of comparison note that the `threading` module
provides both a reentrant and non-reentrant locks:
| lock | reentrant | mandatory |
@@ -109,42 +111,8 @@ The `fasteners.ReaderWriterLock` at the moment is as follows:
| lock | reentrant | mandatory | upgradable | preference |
|------|-----------|-----------|-------------|------------|
-| fasteners.ReaderWriterLock | ✔ | ✘ | ✘ | reader |
-
-🔩 Glossary
------------
-To learn more about the various aspects of locks, check the wikipedia pages for
-[locks](https://en.wikipedia.org/wiki/Lock_(computer_science)) and
-[readers writer locks](https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock) as well as the
-[resources](https://github.com/harlowja/fasteners/blob/master/doc/source/api/process_lock.rst) listed in the
-documentation. Here we briefly mention the main notions used above.
-
-* **Lock** - a mechanism that prevents two or more threads or processes from running the same code at the same time.
-* **Readers writer lock** - a mechanism that prevents two or more threads from having write (or write and read) access,
-while allowing multiple readers.
-* **Reentrant lock** - a lock that can be acquired (and then released) multiple times, as in:
-
-```python
-with lock:
- with lock:
- ... # some code
-```
-* **Mandatory lock** (as opposed to advisory lock) - a lock that is enforced by the operating system, rather than
-by the cooperation between threads or processes
-* **Upgradable readers writer lock** - a readers writer lock that can be upgraded from reader to writer (or downgraded
-from writer to reader) without losing the lock that is already held, as in:
-```python
-with rw_lock.read_lock():
- ... # read access
- with rw_lock.write_lock():
- ... # write access
- ... # read access
-```
-* **Readers writer lock preference** - describes the behaviour when multiple threads or processes are waiting for
-access. Some of the patterns are:
- * **Reader preference** - If lock is held by readers, then new readers will get immediate access. This can result
- in writers waiting forever (writer starvation)
- * **Writer preference** - If writer is waiting for a lock, then all the new readers (and writers) will be queued
- after the writer.
- * **Phase fair** - Lock that alternates between readers and writers.
+| fasteners.ReaderWriterLock | ✔ | ✘ | ✘ | writer |
+If your threads are created by some other means than the standard library `threading`
+module (for example `eventlet`), you may need to provide the corresponding thread
+identification and synchronisation functions to the `ReaderWriterLock`.
\ No newline at end of file
diff --git a/debian/changelog b/debian/changelog
index 2df0601..0113eec 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+python-fasteners (0.18-1) UNRELEASED; urgency=low
+
+ * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk> Tue, 31 Jan 2023 17:23:13 -0000
+
python-fasteners (0.17.3-2) unstable; urgency=medium
* Uploading to unstable.
diff --git a/doc/source/api/lock.rst b/doc/source/api/lock.rst
deleted file mode 100644
index 0d625fe..0000000
--- a/doc/source/api/lock.rst
+++ /dev/null
@@ -1,26 +0,0 @@
-=======
- Lock
-=======
-
--------
-Classes
--------
-
-.. autoclass:: fasteners.lock.ReaderWriterLock
- :members:
-
-----------
-Decorators
-----------
-
-.. autofunction:: fasteners.lock.read_locked
-
-.. autofunction:: fasteners.lock.write_locked
-
-.. autofunction:: fasteners.lock.locked
-
-----------------
-Helper functions
-----------------
-
-.. autofunction:: fasteners.lock.try_lock
diff --git a/doc/source/api/process_lock.rst b/doc/source/api/process_lock.rst
deleted file mode 100644
index f2b4c93..0000000
--- a/doc/source/api/process_lock.rst
+++ /dev/null
@@ -1,83 +0,0 @@
-==============
- Process lock
-==============
-
-Fasteners inter-process locks are cross-platform and are released automatically
-if the process crashes. They are based on the platform specific locking
-mechanisms:
-
-* fcntl for posix (Linux and OSX)
-* LockFileEx (via pywin32) and \_locking (via msvcrt) for Windows
-
-The intersection of fcntl and LockFileEx features is quite small, hence you
-should always assume that:
-
-* Locks are advisory. They do not prevent the modification of the locked file
- by other processes.
-
-* Locks can be unintentionally released by simply opening and closing the file
- descriptor, so lock files must be accessed only using provided abstractions.
-
-* Locks are not reentrant_. An attempt to acquire a lock multiple times can
- result in a deadlock or a crash upon a release of the lock.
-
-* Reader writer locks are not upgradeable_. An attempt to get a reader's lock
- while holding a writer's lock (or vice versa) can result in a deadlock or a
- crash upon a release of the lock.
-
-* There are no guarantees regarding usage by multiple threads in a
- single process. The locks work only between processes.
-
-To learn more about the complications of locking on different platforms we
-recommend the following resources:
-
-* `File locking in Linux (blog post)`_
-
-* `On the Brokenness of File Locking (blog post)`_
-
-* `Everything you never wanted to know about file locking (blog post)`_
-
-* `Record Locking (course notes)`_
-
-* `Windows NT Files -- Locking (pywin32 docs)`_
-
-* `_locking (Windows Dev Center)`_
-
-* `LockFileEx function (Windows Dev Center)`_
-
-.. _upgradeable: https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock#Upgradable_RW_lock>
-.. _reentrant: https://en.wikipedia.org/wiki/Reentrant_mutex
-.. _File locking in Linux (blog post): https://gavv.github.io/articles/file-locks/
-.. _On the Brokenness of File Locking (blog post): http://0pointer.de/blog/projects/locking.html
-.. _Record Locking (course notes): http://poincare.matf.bg.ac.rs/~ivana/courses/ps/sistemi_knjige/pomocno/apue/APUE/0201433079/ch14lev1sec3.html
-.. _Windows NT Files -- Locking (pywin32 docs): http://timgolden.me.uk/pywin32-docs/Windows_NT_Files_.2d.2d_Locking.html
-.. _\_locking (Windows Dev Center): https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/locking?view=vs-2019
-.. _LockFileEx function (Windows Dev Center): https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex
-.. _Everything you never wanted to know about file locking (blog post): https://chris.improbable.org/2010/12/16/everything-you-never-wanted-to-know-about-file-locking/
-
--------
-Classes
--------
-
-.. autoclass:: fasteners.process_lock.InterProcessLock
- :members:
-
-.. autoclass:: fasteners.process_lock._InterProcessLock
- :members:
-
-.. autoclass:: fasteners.process_lock.InterProcessReaderWriterLock
- :members:
-
-.. autoclass:: fasteners.process_lock._InterProcessReaderWriterLock
- :members:
-
-
-----------
-Decorators
-----------
-
-.. autofunction:: fasteners.process_lock.interprocess_locked
-
-.. autofunction:: fasteners.process_lock.interprocess_read_locked
-
-.. autofunction:: fasteners.process_lock.interprocess_write_locked
diff --git a/doc/source/conf.py b/doc/source/conf.py
deleted file mode 100644
index dd20a54..0000000
--- a/doc/source/conf.py
+++ /dev/null
@@ -1,291 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Fasteners documentation build configuration file, created by
-# sphinx-quickstart on Fri Jun 5 14:47:29 2015.
-#
-# This file is execfile()d with the current directory set to its
-# containing dir.
-#
-# Note that not all possible configuration values are present in this
-# autogenerated file.
-#
-# All configuration values have a default; values that are commented out
-# serve to show the default.
-
-import sys
-import os
-import shlex
-
-from fasteners import version as fasteners_version
-
-# If extensions (or modules to document with autodoc) are in another directory,
-# add these directories to sys.path here. If the directory is relative to the
-# documentation root, use os.path.abspath to make it absolute, like shown here.
-#sys.path.insert(0, os.path.abspath('.'))
-
-# -- General configuration ------------------------------------------------
-
-# If your documentation needs a minimal Sphinx version, state it here.
-#needs_sphinx = '1.0'
-
-# Add any Sphinx extension module names here, as strings. They can be
-# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
-# ones.
-extensions = [
- 'sphinx.ext.doctest',
- 'sphinx.ext.autodoc',
- 'sphinx.ext.viewcode',
-]
-
-# Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
-
-# The suffix(es) of source filenames.
-# You can specify multiple suffix as a list of string:
-# source_suffix = ['.rst', '.md']
-source_suffix = '.rst'
-
-# The encoding of source files.
-#source_encoding = 'utf-8-sig'
-
-# The master toctree document.
-master_doc = 'index'
-
-# General information about the project.
-project = u'Fasteners'
-copyright = u'2015, OpenStack Foundation, Yahoo!'
-author = u'Joshua Harlow, OpenStack Developers'
-
-# The version info for the project you're documenting, acts as replacement for
-# |version| and |release|, also used in various other places throughout the
-# built documents.
-#
-# The short X.Y version.
-version = fasteners_version.version_string()
-
-# The full version, including alpha/beta/rc tags.
-release = fasteners_version.version_string()
-
-# The language for content autogenerated by Sphinx. Refer to documentation
-# for a list of supported languages.
-#
-# This is also used if you do content translation via gettext catalogs.
-# Usually you set "language" from the command line for these cases.
-language = None
-
-# There are two options for replacing |today|: either, you set today to some
-# non-false value, then it is used:
-#today = ''
-# Else, today_fmt is used as the format for a strftime call.
-#today_fmt = '%B %d, %Y'
-
-# List of patterns, relative to source directory, that match files and
-# directories to ignore when looking for source files.
-exclude_patterns = []
-
-# The reST default role (used for this markup: `text`) to use for all
-# documents.
-#default_role = None
-
-# If true, '()' will be appended to :func: etc. cross-reference text.
-#add_function_parentheses = True
-
-# If true, the current module name will be prepended to all description
-# unit titles (such as .. function::).
-#add_module_names = True
-
-# If true, sectionauthor and moduleauthor directives will be shown in the
-# output. They are ignored by default.
-#show_authors = False
-
-# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
-
-# A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
-
-# If true, keep warnings as "system message" paragraphs in the built documents.
-#keep_warnings = False
-
-# If true, `todo` and `todoList` produce output, else they produce nothing.
-todo_include_todos = False
-
-
-# -- Options for HTML output ----------------------------------------------
-
-# The theme to use for HTML and HTML Help pages. See the documentation for
-# a list of builtin themes.
-html_theme = 'alabaster'
-
-# Theme options are theme-specific and customize the look and feel of a theme
-# further. For a list of options available for each theme, see the
-# documentation.
-#html_theme_options = {}
-
-# Add any paths that contain custom themes here, relative to this directory.
-#html_theme_path = []
-
-# The name for this set of Sphinx documents. If None, it defaults to
-# "<project> v<release> documentation".
-#html_title = None
-
-# A shorter title for the navigation bar. Default is the same as html_title.
-#html_short_title = None
-
-# The name of an image file (relative to this directory) to place at the top
-# of the sidebar.
-html_logo = "img/safety-pin-small.png"
-
-# The name of an image file (within the static path) to use as favicon of the
-# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
-# pixels large.
-#html_favicon = None
-
-# Add any paths that contain custom static files (such as style sheets) here,
-# relative to this directory. They are copied after the builtin static files,
-# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
-
-# Add any extra paths that contain custom files (such as robots.txt or
-# .htaccess) here, relative to this directory. These files are copied
-# directly to the root of the documentation.
-#html_extra_path = []
-
-# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
-# using the given strftime format.
-#html_last_updated_fmt = '%b %d, %Y'
-
-# If true, SmartyPants will be used to convert quotes and dashes to
-# typographically correct entities.
-#html_use_smartypants = True
-
-# Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
-
-# Additional templates that should be rendered to pages, maps page names to
-# template names.
-#html_additional_pages = {}
-
-# If false, no module index is generated.
-#html_domain_indices = True
-
-# If false, no index is generated.
-#html_use_index = True
-
-# If true, the index is split into individual pages for each letter.
-#html_split_index = False
-
-# If true, links to the reST sources are added to the pages.
-#html_show_sourcelink = True
-
-# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
-#html_show_sphinx = True
-
-# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-#html_show_copyright = True
-
-# If true, an OpenSearch description file will be output, and all pages will
-# contain a <link> tag referring to it. The value of this option must be the
-# base URL from which the finished HTML is served.
-#html_use_opensearch = ''
-
-# This is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = None
-
-# Language to be used for generating the HTML full-text search index.
-# Sphinx supports the following languages:
-# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
-# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
-#html_search_language = 'en'
-
-# A dictionary with options for the search language support, empty by default.
-# Now only 'ja' uses this config value
-#html_search_options = {'type': 'default'}
-
-# The name of a javascript file (relative to the configuration directory) that
-# implements a search results scorer. If empty, the default will be used.
-#html_search_scorer = 'scorer.js'
-
-# Output file base name for HTML help builder.
-htmlhelp_basename = 'Fastenersdoc'
-
-# -- Options for LaTeX output ---------------------------------------------
-
-latex_elements = {
-# The paper size ('letterpaper' or 'a4paper').
-#'papersize': 'letterpaper',
-
-# The font size ('10pt', '11pt' or '12pt').
-#'pointsize': '10pt',
-
-# Additional stuff for the LaTeX preamble.
-#'preamble': '',
-
-# Latex figure (float) alignment
-#'figure_align': 'htbp',
-}
-
-# Grouping the document tree into LaTeX files. List of tuples
-# (source start file, target name, title,
-# author, documentclass [howto, manual, or own class]).
-latex_documents = [
- (master_doc, 'Fasteners.tex', u'Fasteners Documentation',
- u'Joshua Harlow', 'manual'),
-]
-
-# The name of an image file (relative to this directory) to place at the top of
-# the title page.
-#latex_logo = None
-
-# For "manual" documents, if this is true, then toplevel headings are parts,
-# not chapters.
-#latex_use_parts = False
-
-# If true, show page references after internal links.
-#latex_show_pagerefs = False
-
-# If true, show URL addresses after external links.
-#latex_show_urls = False
-
-# Documents to append as an appendix to all manuals.
-#latex_appendices = []
-
-# If false, no module index is generated.
-#latex_domain_indices = True
-
-
-# -- Options for manual page output ---------------------------------------
-
-# One entry per manual page. List of tuples
-# (source start file, name, description, authors, manual section).
-man_pages = [
- (master_doc, 'fasteners', u'Fasteners Documentation',
- [author], 1)
-]
-
-# If true, show URL addresses after external links.
-#man_show_urls = False
-
-
-# -- Options for Texinfo output -------------------------------------------
-
-# Grouping the document tree into Texinfo files. List of tuples
-# (source start file, target name, title, author,
-# dir menu entry, description, category)
-texinfo_documents = [
- (master_doc, 'Fasteners', u'Fasteners Documentation',
- author, 'Fasteners', 'One line description of project.',
- 'Miscellaneous'),
-]
-
-# Documents to append as an appendix to all manuals.
-#texinfo_appendices = []
-
-# If false, no module index is generated.
-#texinfo_domain_indices = True
-
-# How to display URL addresses: 'footnote', 'no', or 'inline'.
-#texinfo_show_urls = 'footnote'
-
-# If true, do not generate a @detailmenu in the "Top" node's menu.
-#texinfo_no_detailmenu = False
diff --git a/doc/source/examples.rst b/doc/source/examples.rst
deleted file mode 100644
index bad1cc7..0000000
--- a/doc/source/examples.rst
+++ /dev/null
@@ -1,250 +0,0 @@
-Examples
-========
-
--------------------
-Inter-process locks
--------------------
-
-.. note::
-
- Launch multiple of these at the same time to see the lock(s) in action.
-
-.. warning::
-
- There are no guarantees regarding usage by multiple threads in a
- single process with these locks (you will have to ensure single process
- safety yourself using traditional thread based locks). In other words this
- lock works **only** between processes.
-
-Lock API
---------
-
-Using a decorator:
-
-.. code-block:: python
-
- import time
-
- import fasteners
-
- @fasteners.interprocess_locked('/tmp/tmp_lock_file')
- def test():
- for i in range(10):
- print('I have the lock')
- time.sleep(1)
-
- print('Waiting for the lock')
- test()
-
-Using a context manager:
-
-.. code-block:: python
-
- import time
-
- import fasteners
-
- def test():
- for i in range(10):
- with fasteners.InterProcessLock('/tmp/tmp_lock_file'):
- print('I have the lock')
- time.sleep(1)
-
- test()
-
-Acquiring and releasing manually:
-
-.. code-block:: python
-
- import time
-
- import fasteners
-
- def test():
- a_lock = fasteners.InterProcessLock('/tmp/tmp_lock_file')
- for i in range(10):
- gotten = a_lock.acquire(blocking=False)
- try:
- if gotten:
- print('I have the lock')
- time.sleep(0.2)
- else:
- print('I do not have the lock')
- time.sleep(0.1)
- finally:
- if gotten:
- a_lock.release()
-
- test()
-
-
-Reader Writer Lock API
-----------------------
-
-Reader lock using a decorator:
-
-.. code-block:: python
-
- import time
-
- import fasteners
-
- @fasteners.interprocess_read_locked('/tmp/tmp_lock_file')
- def test():
- for i in range(10):
- print('I have the readers lock')
- time.sleep(1)
-
- print('Waiting for the lock')
- test()
-
-
-Writer lock using a context manager:
-
-.. code-block:: python
-
- import time
-
- import fasteners
-
- def test():
- for i in range(10):
- with fasteners.InterProcessReaderWriterLock('/tmp/tmp_lock_file').write_lock():
- print('I have the writers lock')
- time.sleep(1)
-
- test()
-
-
-Acquiring and releasing manually:
-
-.. code-block:: python
-
- import time
-
- import fasteners
-
- def test():
- a_lock = fasteners.InterProcessReaderWriterLock('/tmp/tmp_lock_file')
- for i in range(10):
- gotten = a_lock.acquire_read_lock(blocking=False)
- try:
- if gotten:
- print('I have the readers lock')
- time.sleep(0.2)
- else:
- print('I do not have the readers lock')
- time.sleep(0.1)
- finally:
- if gotten:
- a_lock.release_read_lock()
-
- test()
-
-
--------------------
-Inter-thread locks
--------------------
-
-Lock API
---------
-
-Using a decorator:
-
-.. code-block:: python
-
- import threading
-
- import fasteners
-
- class NotThreadSafeThing(object):
- def __init__(self):
- self._lock = threading.Lock()
-
- @fasteners.locked
- def do_something(self):
- print("Doing something in a thread safe manner")
-
- o = NotThreadSafeThing()
- o.do_something()
-
-
-Multiple locks using a single decorator:
-
-.. code-block:: python
-
- import threading
-
- import fasteners
-
- class NotThreadSafeThing(object):
- def __init__(self):
- self._locks = [threading.Lock(), threading.Lock()]
-
- @fasteners.locked(lock='_locks')
- def do_something(self):
- print("Doing something in a thread safe manner")
-
- o = NotThreadSafeThing()
- o.do_something()
-
-
-Manual lock without blocking:
-
-.. code-block:: python
-
- import threading
-
- import fasteners
-
- t = threading.Lock()
- with fasteners.try_lock(t) as gotten:
- if gotten:
- print("I got the lock")
- else:
- print("I did not get the lock")
-
-
-
-Reader Writer lock API
-----------------------
-
-Using a context manager:
-
-.. code-block:: python
-
- import random
- import threading
- import time
-
- import fasteners
-
- def read_something(ident, rw_lock):
- with rw_lock.read_lock():
- print("Thread %s is reading something" % ident)
- time.sleep(1)
-
- def write_something(ident, rw_lock):
- with rw_lock.write_lock():
- print("Thread %s is writing something" % ident)
- time.sleep(2)
-
- rw_lock = fasteners.ReaderWriterLock()
- threads = []
- for i in range(0, 10):
- is_writer = random.choice([True, False])
- if is_writer:
- threads.append(threading.Thread(target=write_something,
- args=(i, rw_lock)))
- else:
- threads.append(threading.Thread(target=read_something,
- args=(i, rw_lock)))
-
- try:
- for t in threads:
- t.start()
- finally:
- while threads:
- t = threads.pop()
- t.join()
-
diff --git a/doc/source/img/safety-pin-small.png b/doc/source/img/safety-pin-small.png
deleted file mode 100644
index a882be5..0000000
Binary files a/doc/source/img/safety-pin-small.png and /dev/null differ
diff --git a/doc/source/index.rst b/doc/source/index.rst
deleted file mode 100644
index 1f501f8..0000000
--- a/doc/source/index.rst
+++ /dev/null
@@ -1,24 +0,0 @@
-Welcome to Fasteners's documentation!
-=====================================
-
-A python `package`_ that provides useful locks.
-
-*Contents:*
-
-.. toctree::
- :maxdepth: 3
-
- api/lock
- api/process_lock
-
- examples
-
-Indices and tables
-==================
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
-
-.. _package: https://pypi.python.org/pypi/fasteners
-
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
new file mode 120000
index 0000000..04c99a5
--- /dev/null
+++ b/docs/CHANGELOG.md
@@ -0,0 +1 @@
+../CHANGELOG.md
\ No newline at end of file
diff --git a/docs/api/inter_process.md b/docs/api/inter_process.md
new file mode 100644
index 0000000..f926144
--- /dev/null
+++ b/docs/api/inter_process.md
@@ -0,0 +1,19 @@
+# Process Lock API
+
+::: fasteners.process_lock.InterProcessLock
+
+::: fasteners.process_lock.InterProcessReaderWriterLock
+
+## Decorators
+
+::: fasteners.process_lock.interprocess_locked
+ rendering:
+ heading_level: 3
+
+::: fasteners.process_lock.interprocess_read_locked
+ rendering:
+ heading_level: 3
+
+::: fasteners.process_lock.interprocess_write_locked
+ rendering:
+ heading_level: 3
\ No newline at end of file
diff --git a/docs/api/inter_thread.md b/docs/api/inter_thread.md
new file mode 100644
index 0000000..0f251ea
--- /dev/null
+++ b/docs/api/inter_thread.md
@@ -0,0 +1,3 @@
+# Thread lock API
+
+::: fasteners.lock.ReaderWriterLock
diff --git a/docs/guide/glossary.md b/docs/guide/glossary.md
new file mode 100644
index 0000000..f35dc1d
--- /dev/null
+++ b/docs/guide/glossary.md
@@ -0,0 +1,37 @@
+# Glossary
+
+To learn more about the various aspects of locks, check the wikipedia pages for
+[locks](https://en.wikipedia.org/wiki/Lock_(computer_science)) and
+[readers writer locks](https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock)
+Here we briefly mention the main notions used in the documentation.
+
+* **Lock** - a mechanism that prevents two or more threads or processes from running the same code at the same time.
+* **Readers writer lock** - a mechanism that prevents two or more threads from having write (or write and read) access,
+ while allowing multiple readers.
+* **Reentrant lock** - a lock that can be acquired (and then released) multiple times, as in:
+
+ ```python
+ with lock:
+ with lock:
+ ... # some code
+ ```
+
+* **Mandatory lock** (as opposed to advisory lock) - a lock that is enforced by the operating system, rather than
+ by the cooperation between threads or processes
+* **Upgradable readers writer lock** - a readers writer lock that can be upgraded from reader to writer (or downgraded
+ from writer to reader) without losing the lock that is already held, as in:
+```python
+with rw_lock.read_lock():
+ ... # read access
+ with rw_lock.write_lock():
+ ... # write access
+ ... # read access
+```
+* **Readers writer lock preference** - describes the behaviour when multiple threads or processes are waiting for
+ access. Some of the patterns are:
+ * **Reader preference** - If lock is held by readers, then new readers will get immediate access. This can result
+ in writers waiting forever (writer starvation).
+ * **Writer preference** - If writer is waiting for a lock, then all the new readers (and writers) will be queued
+ after it. This can result in readers waiting forever (reader starvation).
+ * **Phase fair** - Lock that alternates between readers and writers.
+
diff --git a/docs/guide/index.md b/docs/guide/index.md
new file mode 100644
index 0000000..ec09763
--- /dev/null
+++ b/docs/guide/index.md
@@ -0,0 +1,57 @@
+# User Guide
+
+## Basic Exclusive Lock Usage
+
+Exclusive Lock for independent processes has the same API as the
+[threading.Lock](https://docs.python.org/3/library/threading.html#threading.Lock)
+for threads:
+```python
+import fasteners
+import threading
+
+lock = threading.Lock() # for threads
+lock = fasteners.InterProcessLock('path/to/lock.file') # for processes
+
+with lock:
+ ... # exclusive access
+
+# or alternatively
+
+lock.acquire()
+... # exclusive access
+lock.release()
+```
+
+## Basic Reader Writer lock usage
+
+Reader Writer lock has a similar API, which is the same for threads or processes:
+
+```python
+import fasteners
+
+# for threads
+rw_lock = fasteners.ReaderWriterLock()
+# for processes
+rw_lock = fasteners.InterProcessReaderWriterLock('path/to/lock.file')
+
+with rw_lock.write_lock():
+ ... # write access
+
+with rw_lock.read_lock():
+ ... # read access
+
+# or alternatively, for processes only:
+
+rw_lock.acquire_read_lock()
+... # read access
+rw_lock.release_read_lock()
+
+rw_lock.acquire_write_lock()
+... # write access
+rw_lock.release_write_lock()
+```
+
+## Advanced usage
+
+For more details and options see [Process lock details](inter_process.md) and [Thread lock details](inter_thread.md).
+
diff --git a/docs/guide/inter_process.md b/docs/guide/inter_process.md
new file mode 100644
index 0000000..e38fbb4
--- /dev/null
+++ b/docs/guide/inter_process.md
@@ -0,0 +1,114 @@
+# Inter process locks
+
+Fasteners inter-process locks are cross-platform and are released automatically
+if the process crashes. They are based on the platform specific locking
+mechanisms:
+
+* fcntl for posix (Linux and OSX)
+* LockFileEx (via pywin32) and \_locking (via msvcrt) for Windows
+
+## Difference from `multiprocessing.Lock`
+
+Python standard library [multiprocessing.Lock] functions when the processes are
+launched by a single main process, who is responsible for managing the
+synchronization. `fasteners` locks use the operating system mechanisms for
+synchronization management, and hence work between processes that were launched
+independently.
+
+## Timeouts
+
+`fasteners` locks support timeouts, that can be used as follows:
+
+```python
+import fasteners
+
+lock = fasteners.InterProcessLock('path/to/lock.file')
+
+lock.acquire(timeout=10)
+... # exclusive access
+lock.release()
+```
+
+Equivalently for readers writer lock:
+
+
+```python
+import fasteners
+
+lock = fasteners.InterProcessReaderWriterLock('path/to/lock.file')
+
+lock.acquire_read_lock(timeout=10)
+... # exclusive access
+lock.release_read_lock()
+
+lock.acquire_write_lock(timeout=10)
+... # exclusive access
+lock.release_write_lock()
+```
+
+## Decorators
+
+For extra sugar, a function that always needs exclusive / read / write access
+can be decorated using one of the provided decorators. Note that they do not
+expose the timeout parameter, and always block until the lock is acquired.
+
+```python
+import fasteners
+
+
+@fasteners.interprocess_read_locked
+def read_file():
+ ...
+
+@fasteners.interprocess_write_locked
+def write_file():
+ ...
+
+@fasteners.interprocess_locked
+def do_something_exclusive():
+ ...
+```
+
+## (Lack of) Features
+
+The intersection of fcntl and LockFileEx features is quite small, hence you
+should always assume that:
+
+* Locks are advisory. They do not prevent the modification of the locked file
+ by other processes.
+
+* Locks can be unintentionally released by simply opening and closing the file
+ descriptor, so lock files must be accessed only using provided abstractions.
+
+* Locks are not [reentrant]. An attempt to acquire a lock multiple times can
+ result in a deadlock or a crash upon a release of the lock.
+
+* Reader writer locks are not [upgradeable]. An attempt to get a reader's lock
+ while holding a writer's lock (or vice versa) can result in a deadlock or a
+ crash upon a release of the lock.
+
+* There are no guarantees regarding usage by multiple threads in a
+ single process. The locks work only between processes.
+
+## Resources
+
+To learn more about the complications of locking on different platforms we
+recommend the following resources:
+
+* [File locking in Linux (blog post)](https://gavv.github.io/articles/file-locks/)
+
+* [On the Brokenness of File Locking (blog post)](http://0pointer.de/blog/projects/locking.html)
+
+* [Everything you never wanted to know about file locking (blog post)](https://chris.improbable.org/2010/12/16/everything-you-never-wanted-to-know-about-file-locking/)
+
+* [Record Locking (course notes)](http://poincare.matf.bg.ac.rs/~ivana/courses/ps/sistemi_knjige/pomocno/apue/APUE/0201433079/ch14lev1sec3.html)
+
+* [Windows NT Files -- Locking (pywin32 docs)](http://timgolden.me.uk/pywin32-docs/Windows_NT_Files_.2d.2d_Locking.html)
+
+* [_locking (Windows Dev Center)](https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/locking?view=vs-2019)
+
+* [LockFileEx function (Windows Dev Center)](https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex)
+
+[upgradeable]: https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock#Upgradable_RW_lock>
+[reentrant]: https://en.wikipedia.org/wiki/Reentrant_mutex
+[multiprocessing.Lock]: https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Lock
\ No newline at end of file
diff --git a/docs/guide/inter_thread.md b/docs/guide/inter_thread.md
new file mode 100644
index 0000000..4fa6062
--- /dev/null
+++ b/docs/guide/inter_thread.md
@@ -0,0 +1,50 @@
+# Inter thread locks
+
+Fasteners inter-thread locks were build specifically for the needs of
+`oslo.concurrency` project and thus have a rather peculiar API. We do not
+recommend using it fully, and it is hence not documented (but maintained until
+the end of time).
+
+Instead, we recommend limiting the use of fasteners inter-thread readers writer
+lock to the basic API:
+
+```python
+import fasteners
+
+# for threads
+rw_lock = fasteners.ReaderWriterLock()
+
+with rw_lock.write_lock():
+ ... # write access
+
+with rw_lock.read_lock():
+ ... # read access
+```
+
+## (Lack of) Features
+
+Fasteners inter-thread readers writer lock is
+
+* not [upgradeable]. An attempt to get a reader's lock while holding a writer's
+ lock (or vice versa) will raise an exception.
+
+* [reentrant] (!). You can acquire (and correspondingly release) the lock
+ multiple times.
+
+* has writer preference. Readers will queue after writers and pending writers.
+
+[upgradeable]: https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock#Upgradable_RW_lock>
+
+[reentrant]: https://en.wikipedia.org/wiki/Reentrant_mutex
+
+## Different thread creation mechanisms
+
+If your threads are created by some other means than the standard library `threading`
+module, you may need to provide corresponding thread identification and synchronisation
+functions to the `ReaderWriterLock`.
+
+### Eventlet
+
+In particular, in case of `eventlet` threads, you should monkey_patch the stdlib threads with `eventlet.monkey_patch(tread=True)`
+and initialise the `ReaderWriterLock` as `ReaderWriterLock(current_thread_functor=eventlet.getcurrent)`.
+
diff --git a/docs/img/favicon-192x192.png b/docs/img/favicon-192x192.png
new file mode 100644
index 0000000..4e5395d
Binary files /dev/null and b/docs/img/favicon-192x192.png differ
diff --git a/docs/img/favicon-32x32.png b/docs/img/favicon-32x32.png
new file mode 100644
index 0000000..818880b
Binary files /dev/null and b/docs/img/favicon-32x32.png differ
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..996d0c3
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,28 @@
+# Fasteners
+
+Python standard library provides an Exclusive Lock for threads and Exclusive Lock for processes
+spawned by `multiprocessing` module. `fasteners` provides additional three synchronization primitives:
+
+* Exclusive Lock for independent processes
+* Readers Writer Lock for independent processes
+* Readers Writer Lock for threads
+
+## Installation
+
+```
+pip install fasteners
+```
+
+## Usage
+
+See [User Guide](guide/index.md) for usage tips and examples and [Reference](api/inter_process.md) for detailed API.
+
+## Similar libraries
+
+[`portarlocker`](https://github.com/WoLpH/portalocker): readers writer lock and semaphore for
+independent processes, exclusive lock based on redis.
+
+[`py-filelock`](https://github.com/tox-dev/py-filelock): exclusive lock for independent processes.
+
+[`pyReaderWriterLock`](https://github.com/elarivie/pyReaderWriterLock): inter-thread readers writer
+locks, optionally downgradable, with various priorities (reader, writer, fair).
\ No newline at end of file
diff --git a/fasteners/lock.py b/fasteners/lock.py
index 2cb76f1..6c872d4 100644
--- a/fasteners/lock.py
+++ b/fasteners/lock.py
@@ -21,110 +21,53 @@ import collections
import contextlib
import functools
import threading
+from typing import Optional
from fasteners import _utils
-def read_locked(*args, **kwargs):
- """Acquires & releases a read lock around call into decorated method.
-
- NOTE(harlowja): if no attribute name is provided then by default the
- attribute named '_lock' is looked for (this attribute is expected to be
- a :py:class:`.ReaderWriterLock`) in the instance object this decorator
- is attached to.
- """
-
- def decorator(f):
- attr_name = kwargs.get('lock', '_lock')
-
- @functools.wraps(f)
- def wrapper(self, *args, **kwargs):
- rw_lock = getattr(self, attr_name)
- with rw_lock.read_lock():
- return f(self, *args, **kwargs)
-
- return wrapper
-
- # This is needed to handle when the decorator has args or the decorator
- # doesn't have args, python is rather weird here...
- if kwargs or not args:
- return decorator
- else:
- if len(args) == 1:
- return decorator(args[0])
- else:
- return decorator
-
-
-def write_locked(*args, **kwargs):
- """Acquires & releases a write lock around call into decorated method.
-
- NOTE(harlowja): if no attribute name is provided then by default the
- attribute named '_lock' is looked for (this attribute is expected to be
- a :py:class:`.ReaderWriterLock` object) in the instance object this
- decorator is attached to.
- """
-
- def decorator(f):
- attr_name = kwargs.get('lock', '_lock')
-
- @functools.wraps(f)
- def wrapper(self, *args, **kwargs):
- rw_lock = getattr(self, attr_name)
- with rw_lock.write_lock():
- return f(self, *args, **kwargs)
-
- return wrapper
-
- # This is needed to handle when the decorator has args or the decorator
- # doesn't have args, python is rather weird here...
- if kwargs or not args:
- return decorator
- else:
- if len(args) == 1:
- return decorator(args[0])
- else:
- return decorator
-
-
class ReaderWriterLock(object):
- """A reader/writer lock.
-
- This lock allows for simultaneous readers to exist but only one writer
- to exist for use-cases where it is useful to have such types of locks.
-
- Currently a reader can not escalate its read lock to a write lock and
- a writer can not acquire a read lock while it is waiting on the write
- lock.
+ """An inter-thread readers writer lock."""
- In the future these restrictions may be relaxed.
-
- This can be eventually removed if http://bugs.python.org/issue8800 ever
- gets accepted into the python standard threading library...
- """
-
- #: Writer owner type/string constant.
- WRITER = 'w'
-
- #: Reader owner type/string constant.
- READER = 'r'
+ WRITER = 'w' #: Writer owner type/string constant.
+ READER = 'r' #: Reader owner type/string constant.
def __init__(self,
condition_cls=threading.Condition,
- current_thread_functor=None):
+ current_thread_functor=threading.current_thread):
+ """
+ Args:
+ condition_cls:
+ Optional custom `Condition` primitive used for synchronization.
+ current_thread_functor:
+ Optional function that returns the identity of the thread in case
+ threads are not properly identified by threading.current_thread
+ """
self._writer = None
self._pending_writers = collections.deque()
self._readers = {}
self._cond = condition_cls()
- self._current_thread = threading.current_thread
+ self._current_thread = current_thread_functor
@property
- def has_pending_writers(self):
- """Returns if there are writers waiting to become the *one* writer."""
+ def has_pending_writers(self) -> bool:
+ """Check if there pending writers
+
+ Returns:
+ Whether there are pending writers.
+ """
return bool(self._pending_writers)
- def is_writer(self, check_pending=True):
- """Returns if the caller is the active writer or a pending writer."""
+ def is_writer(self, check_pending: bool = True) -> bool:
+ """Check if caller is a writer (optionally pending writer).
+
+ Args:
+ check_pending:
+ Whether to check for pending writer status.
+
+ Returns:
+ Whether the caller is the active (or optionally pending) writer.
+ """
me = self._current_thread()
if self._writer == me:
return True
@@ -133,8 +76,22 @@ class ReaderWriterLock(object):
else:
return False
+ def is_reader(self) -> bool:
+ """Check if caller is a reader.
+
+ Returns:
+ Whether the caller is an active reader.
+ """
+ me = self._current_thread()
+ return me in self._readers
+
@property
- def owner(self):
+ def owner(self) -> Optional[str]:
+ """Caller ownership (if any) of the lock
+
+ Returns:
+ `'w'` if caller is a writer, `'r'` if caller is a reader, None otherwise.
+ """
"""Returns whether the lock is locked by a writer or reader."""
if self._writer is not None:
return self.WRITER
@@ -142,19 +99,14 @@ class ReaderWriterLock(object):
return self.READER
return None
- def is_reader(self):
- """Returns if the caller is one of the readers."""
- me = self._current_thread()
- return me in self._readers
-
@contextlib.contextmanager
def read_lock(self):
"""Context manager that grants a read lock.
Will wait until no active or pending writers.
- Raises a ``RuntimeError`` if a pending writer tries to acquire
- a read lock.
+ Raises:
+ RuntimeError: if a pending writer tries to acquire a read lock.
"""
me = self._current_thread()
if me in self._pending_writers:
@@ -202,8 +154,8 @@ class ReaderWriterLock(object):
Guaranteed for locks to be processed in fair order (FIFO).
- Raises a ``RuntimeError`` if an active reader attempts to acquire
- a lock.
+ Raises:
+ RuntimeError: if an active reader attempts to acquire a lock.
"""
me = self._current_thread()
i_am_writer = self.is_writer(check_pending=False)
@@ -231,22 +183,6 @@ class ReaderWriterLock(object):
self._cond.notify_all()
-@contextlib.contextmanager
-def try_lock(lock):
- """Attempts to acquire a lock, and auto releases if acquired (on exit)."""
- # NOTE(harlowja): the keyword argument for 'blocking' does not work
- # in py2.x and only is fixed in py3.x (this adjustment is documented
- # and/or debated in http://bugs.python.org/issue10789); so we'll just
- # stick to the format that works in both (oddly the keyword argument
- # works in py2.x but only with reentrant locks).
- was_locked = lock.acquire(False)
- try:
- yield was_locked
- finally:
- if was_locked:
- lock.release()
-
-
def locked(*args, **kwargs):
"""A locking **method** decorator.
@@ -264,6 +200,10 @@ def locked(*args, **kwargs):
NOTE(harlowja): a custom logger (which will be used if lock release
failures happen) can be provided by passing a logger instance for keyword
argument ``logger``.
+
+ NOTE(paulius): This function is DEPRECATED and will be kept until the end
+ of time. It is potentially used by oslo, but too specific to be recommended
+ for other projects
"""
def decorator(f):
@@ -296,3 +236,103 @@ def locked(*args, **kwargs):
return decorator(args[0])
else:
return decorator
+
+
+def read_locked(*args, **kwargs):
+ """Acquires & releases a read lock around call into decorated method.
+
+ NOTE(harlowja): if no attribute name is provided then by default the
+ attribute named '_lock' is looked for (this attribute is expected to be
+ a :py:class:`.ReaderWriterLock`) in the instance object this decorator
+ is attached to.
+
+ NOTE(paulius): This function is DEPRECATED and will be kept until the end
+ of time. It is potentially used by oslo, but too specific to be recommended
+ for other projects
+ """
+
+ def decorator(f):
+ attr_name = kwargs.get('lock', '_lock')
+
+ @functools.wraps(f)
+ def wrapper(self, *args, **kwargs):
+ rw_lock = getattr(self, attr_name)
+ with rw_lock.read_lock():
+ return f(self, *args, **kwargs)
+
+ return wrapper
+
+ # This is needed to handle when the decorator has args or the decorator
+ # doesn't have args, python is rather weird here...
+ if kwargs or not args:
+ return decorator
+ else:
+ if len(args) == 1:
+ return decorator(args[0])
+ else:
+ return decorator
+
+
+def write_locked(*args, **kwargs):
+ """Acquires & releases a write lock around call into decorated method.
+
+ NOTE(harlowja): if no attribute name is provided then by default the
+ attribute named '_lock' is looked for (this attribute is expected to be
+ a :py:class:`.ReaderWriterLock` object) in the instance object this
+ decorator is attached to.
+
+ NOTE(paulius): This function is DEPRECATED and will be kept until the end
+ of time. It is potentially used by oslo, but too specific to be recommended
+ for other projects
+ """
+
+ def decorator(f):
+ attr_name = kwargs.get('lock', '_lock')
+
+ @functools.wraps(f)
+ def wrapper(self, *args, **kwargs):
+ rw_lock = getattr(self, attr_name)
+ with rw_lock.write_lock():
+ return f(self, *args, **kwargs)
+
+ return wrapper
+
+ # This is needed to handle when the decorator has args or the decorator
+ # doesn't have args, python is rather weird here...
+ if kwargs or not args:
+ return decorator
+ else:
+ if len(args) == 1:
+ return decorator(args[0])
+ else:
+ return decorator
+
+
+@contextlib.contextmanager
+def try_lock(lock: threading.Lock) -> bool:
+ """Context manager that attempts to acquire a lock without a timeout, and
+ releases it on exit (if acquired).
+
+ Args:
+ lock:
+ A lock to try to acquire.
+
+ Returns:
+ Whether the lock was acquired.
+
+ # NOTE(harlowja): the keyword argument for 'blocking' does not work
+ # in py2.x and only is fixed in py3.x (this adjustment is documented
+ # and/or debated in http://bugs.python.org/issue10789); so we'll just
+ # stick to the format that works in both (oddly the keyword argument
+ # works in py2.x but only with reentrant locks).
+
+ NOTE(paulius): This function is DEPRECATED and will be kept until the end
+ of time. It is potentially used by oslo, but too specific to be recommended
+ for other projects
+ """
+ was_locked = lock.acquire(False)
+ try:
+ yield was_locked
+ finally:
+ if was_locked:
+ lock.release()
diff --git a/fasteners/process_lock.py b/fasteners/process_lock.py
index fa4d789..1197731 100644
--- a/fasteners/process_lock.py
+++ b/fasteners/process_lock.py
@@ -20,10 +20,16 @@ import errno
import functools
import logging
import os
+from pathlib import Path
import threading
import time
+from typing import Callable
+from typing import Optional
+from typing import Union
from fasteners import _utils
+from fasteners.process_mechanism import _interprocess_mechanism
+from fasteners.process_mechanism import _interprocess_reader_writer_mechanism
LOG = logging.getLogger(__name__)
@@ -49,24 +55,25 @@ def _ensure_tree(path):
return True
-class _InterProcessLock(object):
+class InterProcessLock:
"""An interprocess lock."""
- MAX_DELAY = 0.1
- """
- Default maximum delay we will wait to try to acquire the lock (when
- it's busy/being held by another process).
- """
-
- DELAY_INCREMENT = 0.01
- """
- Default increment we will use (up to max delay) after each attempt before
- next attempt to acquire the lock. For example if 3 attempts have been made
- the calling thread will sleep (0.01 * 3) before the next attempt to
- acquire the lock (and repeat).
- """
+ MAX_DELAY = 0.1 # For backwards compatibility
+ DELAY_INCREMENT = 0.01 # For backwards compatibility
- def __init__(self, path, sleep_func=time.sleep, logger=None):
+ def __init__(self,
+ path: Union[Path, str],
+ sleep_func: Callable[[float], None] = time.sleep,
+ logger: Optional[logging.Logger] = None):
+ """
+ args:
+ path:
+ Path to the file that will be used for locking.
+ sleep_func:
+ Optional function to use for sleeping.
+ logger:
+ Optional logger to use for logging.
+ """
self.lockfile = None
self.path = _utils.canonicalize_path(path)
self.acquired = False
@@ -106,25 +113,27 @@ class _InterProcessLock(object):
if self.lockfile is None or self.lockfile.closed:
self.lockfile = open(self.path, 'a')
- def acquire(self, blocking=True,
- delay=DELAY_INCREMENT, max_delay=MAX_DELAY,
- timeout=None):
- """Attempt to acquire the given lock.
-
- :param blocking: whether to wait forever to try to acquire the lock
- :type blocking: bool
- :param delay: when blocking this is the delay time in seconds that
- will be added after each failed acquisition
- :type delay: int/float
- :param max_delay: the maximum delay to have (this limits the
- accumulated delay(s) added after each failed
- acquisition)
- :type max_delay: int/float
- :param timeout: an optional timeout (limits how long blocking
- will occur for)
- :type timeout: int/float
- :returns: whether or not the acquisition succeeded
- :rtype: bool
+ def acquire(self,
+ blocking: bool = True,
+ delay: float = 0.01,
+ max_delay: float = 0.1,
+ timeout: Optional[float] = None) -> bool:
+ """Attempt to acquire the lock.
+
+ Args:
+ blocking:
+ Whether to wait to try to acquire the lock.
+ delay:
+ When `blocking`, starting delay as well as the delay increment
+ (in seconds).
+ max_delay:
+ When `blocking` the maximum delay in between attempts to
+ acquire (in seconds).
+ timeout:
+ When `blocking`, maximal waiting time (in seconds).
+
+ Returns:
+ whether or not the acquisition succeeded
"""
if delay < 0:
raise ValueError("Delay must be greater than or equal to zero")
@@ -156,7 +165,7 @@ class _InterProcessLock(object):
def __enter__(self):
gotten = self.acquire()
if not gotten:
- # This shouldn't happen, but just incase...
+ # This shouldn't happen, but just in case...
raise threading.ThreadError("Unable to acquire a file lock"
" on `%s` (when used as a"
" context manager)" % self.path)
@@ -188,50 +197,70 @@ class _InterProcessLock(object):
self.release()
def exists(self):
- """Checks if the path that this lock exists at actually exists."""
return os.path.exists(self.path)
def trylock(self):
- self._trylock(self.lockfile)
+ _interprocess_mechanism.trylock(self.lockfile)
def unlock(self):
- self._unlock(self.lockfile)
+ _interprocess_mechanism.unlock(self.lockfile)
- @staticmethod
- def _trylock(lockfile):
- raise NotImplementedError()
- @staticmethod
- def _unlock(lockfile):
- raise NotImplementedError()
-
-
-class _InterProcessReaderWriterLock(object):
+class InterProcessReaderWriterLock:
"""An interprocess readers writer lock."""
- MAX_DELAY = 0.1
- """
- Default maximum delay we will wait to try to acquire the lock (when
- it's busy/being held by another process).
- """
-
- DELAY_INCREMENT = 0.01
- """
- Default increment we will use (up to max delay) after each attempt before
- next attempt to acquire the lock. For example if 3 attempts have been made
- the calling thread will sleep (0.01 * 3) before the next attempt to
- acquire the lock (and repeat).
- """
+ MAX_DELAY = 0.1 # for backwards compatibility
+ DELAY_INCREMENT = 0.01 # for backwards compatibility
- def __init__(self, path, sleep_func=time.sleep, logger=None):
+ def __init__(self,
+ path: Union[Path, str],
+ sleep_func: Callable[[float], None] = time.sleep,
+ logger: Optional[logging.Logger] = None):
+ """
+ Args:
+ path:
+ Path to the file that will be used for locking.
+ sleep_func:
+ Optional function to use for sleeping.
+ logger:
+ Optional logger to use for logging.
+ """
self.lockfile = None
self.path = _utils.canonicalize_path(path)
self.sleep_func = sleep_func
self.logger = _utils.pick_first_not_none(logger, LOG)
+ @contextmanager
+ def read_lock(self, delay=0.01, max_delay=0.1):
+ """Context manager that grans a read lock"""
+
+ self.acquire_read_lock(blocking=True, delay=delay,
+ max_delay=max_delay, timeout=None)
+ try:
+ yield
+ finally:
+ self.release_read_lock()
+
+ @contextmanager
+ def write_lock(self, delay=0.01, max_delay=0.1):
+ """Context manager that grans a write lock"""
+
+ gotten = self.acquire_write_lock(blocking=True, delay=delay,
+ max_delay=max_delay, timeout=None)
+
+ if not gotten:
+ # This shouldn't happen, but just in case...
+ raise threading.ThreadError("Unable to acquire a file lock"
+ " on `%s` (when used as a"
+ " context manager)" % self.path)
+ try:
+ yield
+ finally:
+ self.release_write_lock()
+
def _try_acquire(self, blocking, watch, exclusive):
try:
- gotten = self._trylock(self.lockfile, exclusive)
+ gotten = _interprocess_reader_writer_mechanism.trylock(self.lockfile, exclusive)
except Exception as e:
raise threading.ThreadError(
"Unable to acquire lock on {} due to {}!".format(self.path, e))
@@ -252,56 +281,58 @@ class _InterProcessReaderWriterLock(object):
self.logger.log(_utils.BLATHER,
'Created lock base path `%s`', basedir)
if self.lockfile is None:
- self.lockfile = self._get_handle(self.path)
-
- def acquire_read_lock(self, blocking=True,
- delay=DELAY_INCREMENT, max_delay=MAX_DELAY,
- timeout=None):
+ self.lockfile = _interprocess_reader_writer_mechanism.get_handle(self.path)
+ def acquire_read_lock(self,
+ blocking: bool = True,
+ delay: float = 0.01,
+ max_delay: float = 0.1,
+ timeout: float = None) -> bool:
"""Attempt to acquire a reader's lock.
- :param blocking: whether to wait forever to try to acquire the lock
- :type blocking: bool
- :param delay: when blocking this is the delay time in seconds that
- will be added after each failed acquisition
- :type delay: int/float
- :param max_delay: the maximum delay to have (this limits the
- accumulated delay(s) added after each failed
- acquisition)
- :type max_delay: int/float
- :param timeout: an optional timeout (limits how long blocking
- will occur for)
- :type timeout: int/float
- :returns: whether or not the acquisition succeeded
- :rtype: bool
+ Args:
+ blocking:
+ Whether to wait to try to acquire the lock.
+ delay:
+ When `blocking`, starting delay as well as the delay increment
+ (in seconds).
+ max_delay:
+ When `blocking` the maximum delay in between attempts to
+ acquire (in seconds).
+ timeout:
+ When `blocking`, maximal waiting time (in seconds).
+
+ Returns:
+ whether or not the acquisition succeeded
"""
return self._acquire(blocking, delay, max_delay, timeout, exclusive=False)
- def acquire_write_lock(self, blocking=True,
- delay=DELAY_INCREMENT, max_delay=MAX_DELAY,
- timeout=None):
-
+ def acquire_write_lock(self,
+ blocking: bool = True,
+ delay: float = 0.01,
+ max_delay: float = 0.1,
+ timeout: float = None) -> bool:
"""Attempt to acquire a writer's lock.
- :param blocking: whether to wait forever to try to acquire the lock
- :type blocking: bool
- :param delay: when blocking this is the delay time in seconds that
- will be added after each failed acquisition
- :type delay: int/float
- :param max_delay: the maximum delay to have (this limits the
- accumulated delay(s) added after each failed
- acquisition)
- :type max_delay: int/float
- :param timeout: an optional timeout (limits how long blocking
- will occur for)
- :type timeout: int/float
- :returns: whether or not the acquisition succeeded
- :rtype: bool
+ Args:
+ blocking:
+ Whether to wait to try to acquire the lock.
+ delay:
+ When `blocking`, starting delay as well as the delay increment
+ (in seconds).
+ max_delay:
+ When `blocking` the maximum delay in between attempts to
+ acquire (in seconds).
+ timeout:
+ When `blocking`, maximal waiting time (in seconds).
+
+ Returns:
+ whether or not the acquisition succeeded
"""
return self._acquire(blocking, delay, max_delay, timeout, exclusive=True)
def _acquire(self, blocking=True,
- delay=DELAY_INCREMENT, max_delay=MAX_DELAY,
+ delay=0.01, max_delay=0.1,
timeout=None, exclusive=True):
if delay < 0:
@@ -327,13 +358,13 @@ class _InterProcessReaderWriterLock(object):
def _do_close(self):
if self.lockfile is not None:
- self._close_handle(self.lockfile)
+ _interprocess_reader_writer_mechanism.close_handle(self.lockfile)
self.lockfile = None
def release_write_lock(self):
"""Release the writer's lock."""
try:
- self._unlock(self.lockfile)
+ _interprocess_reader_writer_mechanism.unlock(self.lockfile)
except IOError:
self.logger.exception("Could not unlock the acquired lock opened"
" on `%s`", self.path)
@@ -351,7 +382,7 @@ class _InterProcessReaderWriterLock(object):
def release_read_lock(self):
"""Release the reader's lock."""
try:
- self._unlock(self.lockfile)
+ _interprocess_reader_writer_mechanism.unlock(self.lockfile)
except IOError:
self.logger.exception("Could not unlock the acquired lock opened"
" on `%s`", self.path)
@@ -366,168 +397,14 @@ class _InterProcessReaderWriterLock(object):
"Unlocked and closed file lock open on"
" `%s`", self.path)
- @contextmanager
- def write_lock(self, delay=DELAY_INCREMENT, max_delay=MAX_DELAY):
-
- gotten = self.acquire_write_lock(blocking=True, delay=delay,
- max_delay=max_delay, timeout=None)
- if not gotten:
- # This shouldn't happen, but just in case...
- raise threading.ThreadError("Unable to acquire a file lock"
- " on `%s` (when used as a"
- " context manager)" % self.path)
- try:
- yield
- finally:
- self.release_write_lock()
-
- @contextmanager
- def read_lock(self, delay=DELAY_INCREMENT, max_delay=MAX_DELAY):
-
- self.acquire_read_lock(blocking=True, delay=delay,
- max_delay=max_delay, timeout=None)
- try:
- yield
- finally:
- self.release_read_lock()
-
- @staticmethod
- def _trylock(lockfile, exclusive):
- raise NotImplementedError()
-
- @staticmethod
- def _unlock(lockfile):
- raise NotImplementedError()
-
- @staticmethod
- def _get_handle(path):
- raise NotImplementedError()
-
- @staticmethod
- def _close_handle(lockfile):
- raise NotImplementedError()
-
-
-class _WindowsLock(_InterProcessLock):
- """Interprocess lock implementation that works on windows systems."""
-
- @staticmethod
- def _trylock(lockfile):
- fileno = lockfile.fileno()
- msvcrt.locking(fileno, msvcrt.LK_NBLCK, 1)
-
- @staticmethod
- def _unlock(lockfile):
- fileno = lockfile.fileno()
- msvcrt.locking(fileno, msvcrt.LK_UNLCK, 1)
-
-
-class _FcntlLock(_InterProcessLock):
- """Interprocess lock implementation that works on posix systems."""
-
- @staticmethod
- def _trylock(lockfile):
- fcntl.lockf(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
-
- @staticmethod
- def _unlock(lockfile):
- fcntl.lockf(lockfile, fcntl.LOCK_UN)
-
-
-class _WindowsInterProcessReaderWriterLock(_InterProcessReaderWriterLock):
- """Interprocess readers writer lock implementation that works on windows
- systems."""
-
- @staticmethod
- def _trylock(lockfile, exclusive):
-
- if exclusive:
- flags = win32con.LOCKFILE_FAIL_IMMEDIATELY | win32con.LOCKFILE_EXCLUSIVE_LOCK
- else:
- flags = win32con.LOCKFILE_FAIL_IMMEDIATELY
-
- handle = msvcrt.get_osfhandle(lockfile.fileno())
- ok = win32file.LockFileEx(handle, flags, 0, 1, 0, win32file.pointer(pywintypes.OVERLAPPED()))
- if ok:
- return True
- else:
- last_error = win32file.GetLastError()
- if last_error == win32file.ERROR_LOCK_VIOLATION:
- return False
- else:
- raise OSError(last_error)
-
- @staticmethod
- def _unlock(lockfile):
- handle = msvcrt.get_osfhandle(lockfile.fileno())
- ok = win32file.UnlockFileEx(handle, 0, 1, 0, win32file.pointer(pywintypes.OVERLAPPED()))
- if not ok:
- raise OSError(win32file.GetLastError())
-
- @staticmethod
- def _get_handle(path):
- return open(path, 'a+')
-
- @staticmethod
- def _close_handle(lockfile):
- lockfile.close()
-
-
-class _FcntlInterProcessReaderWriterLock(_InterProcessReaderWriterLock):
- """Interprocess readers writer lock implementation that works on posix
- systems."""
-
- @staticmethod
- def _trylock(lockfile, exclusive):
-
- if exclusive:
- flags = fcntl.LOCK_EX | fcntl.LOCK_NB
- else:
- flags = fcntl.LOCK_SH | fcntl.LOCK_NB
-
- try:
- fcntl.lockf(lockfile, flags)
- return True
- except (IOError, OSError) as e:
- if e.errno in (errno.EACCES, errno.EAGAIN):
- return False
- else:
- raise e
-
- @staticmethod
- def _unlock(lockfile):
- fcntl.lockf(lockfile, fcntl.LOCK_UN)
-
- @staticmethod
- def _get_handle(path):
- return open(path, 'a+')
-
- @staticmethod
- def _close_handle(lockfile):
- lockfile.close()
-
-
-if os.name == 'nt':
- import msvcrt
- import fasteners.pywin32.pywintypes as pywintypes
- import fasteners.pywin32.win32con as win32con
- import fasteners.pywin32.win32file as win32file
-
- InterProcessLock = _WindowsLock
- InterProcessReaderWriterLock = _WindowsInterProcessReaderWriterLock
-
-else:
- import fcntl
-
- InterProcessLock = _FcntlLock
- InterProcessReaderWriterLock = _FcntlInterProcessReaderWriterLock
-
-
-def interprocess_write_locked(path):
- """Acquires & releases an interprocess read lock around the call into
- the decorated function"""
+def interprocess_write_locked(path: Union[Path, str]):
+ """Acquires & releases an interprocess **write** lock around the call into
+ the decorated function
+ Args:
+ path: Path to the file used for locking.
+ """
lock = InterProcessReaderWriterLock(path)
def decorator(f):
@@ -541,10 +418,13 @@ def interprocess_write_locked(path):
return decorator
-def interprocess_read_locked(path):
- """Acquires & releases an interprocess read lock around the call into
- the decorated function"""
+def interprocess_read_locked(path: Union[Path, str]):
+ """Acquires & releases an interprocess **read** lock around the call into
+ the decorated function
+ Args:
+ path: Path to the file used for locking.
+ """
lock = InterProcessReaderWriterLock(path)
def decorator(f):
@@ -558,10 +438,13 @@ def interprocess_read_locked(path):
return decorator
-def interprocess_locked(path):
- """Acquires & releases a interprocess lock around call into
- decorated function."""
+def interprocess_locked(path: Union[Path, str]):
+ """Acquires & releases an interprocess lock around the call to the
+ decorated function.
+ Args:
+ path: Path to the file used for locking.
+ """
lock = InterProcessLock(path)
def decorator(f):
diff --git a/fasteners/process_mechanism.py b/fasteners/process_mechanism.py
new file mode 100644
index 0000000..39ccec6
--- /dev/null
+++ b/fasteners/process_mechanism.py
@@ -0,0 +1,154 @@
+from abc import ABC
+from abc import abstractmethod
+import errno
+import os
+
+
+class _InterProcessReaderWriterLockMechanism(ABC):
+
+ @staticmethod
+ @abstractmethod
+ def trylock(lockfile, exclusive):
+ ...
+
+ @staticmethod
+ @abstractmethod
+ def unlock(lockfile):
+ ...
+
+ @staticmethod
+ @abstractmethod
+ def get_handle(path):
+ ...
+
+ @staticmethod
+ @abstractmethod
+ def close_handle(lockfile):
+ ...
+
+
+class _InterProcessMechanism(ABC):
+ @staticmethod
+ @abstractmethod
+ def trylock(lockfile):
+ ...
+
+ @staticmethod
+ @abstractmethod
+ def unlock(lockfile):
+ ...
+
+
+class _WindowsInterProcessMechanism(_InterProcessMechanism):
+ """Interprocess lock implementation that works on windows systems."""
+
+ @staticmethod
+ def trylock(lockfile):
+ fileno = lockfile.fileno()
+ msvcrt.locking(fileno, msvcrt.LK_NBLCK, 1)
+
+ @staticmethod
+ def unlock(lockfile):
+ fileno = lockfile.fileno()
+ msvcrt.locking(fileno, msvcrt.LK_UNLCK, 1)
+
+
+class _FcntlInterProcessMechanism(_InterProcessMechanism):
+ """Interprocess lock implementation that works on posix systems."""
+
+ @staticmethod
+ def trylock(lockfile):
+ fcntl.lockf(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
+
+ @staticmethod
+ def unlock(lockfile):
+ fcntl.lockf(lockfile, fcntl.LOCK_UN)
+
+
+class _WindowsInterProcessReaderWriterLockMechanism(_InterProcessReaderWriterLockMechanism):
+ """Interprocess readers writer lock implementation that works on windows
+ systems."""
+
+ @staticmethod
+ def trylock(lockfile, exclusive):
+
+ if exclusive:
+ flags = win32con.LOCKFILE_FAIL_IMMEDIATELY | win32con.LOCKFILE_EXCLUSIVE_LOCK
+ else:
+ flags = win32con.LOCKFILE_FAIL_IMMEDIATELY
+
+ handle = msvcrt.get_osfhandle(lockfile.fileno())
+ ok = win32file.LockFileEx(handle, flags, 0, 1, 0, win32file.pointer(pywintypes.OVERLAPPED()))
+ if ok:
+ return True
+ else:
+ last_error = win32file.GetLastError()
+ if last_error == win32file.ERROR_LOCK_VIOLATION:
+ return False
+ else:
+ raise OSError(last_error)
+
+ @staticmethod
+ def unlock(lockfile):
+ handle = msvcrt.get_osfhandle(lockfile.fileno())
+ ok = win32file.UnlockFileEx(handle, 0, 1, 0, win32file.pointer(pywintypes.OVERLAPPED()))
+ if not ok:
+ raise OSError(win32file.GetLastError())
+
+ @staticmethod
+ def get_handle(path):
+ return open(path, 'a+')
+
+ @staticmethod
+ def close_handle(lockfile):
+ lockfile.close()
+
+
+class _FcntlInterProcessReaderWriterLockMechanism(_InterProcessReaderWriterLockMechanism):
+ """Interprocess readers writer lock implementation that works on posix
+ systems."""
+
+ @staticmethod
+ def trylock(lockfile, exclusive):
+
+ if exclusive:
+ flags = fcntl.LOCK_EX | fcntl.LOCK_NB
+ else:
+ flags = fcntl.LOCK_SH | fcntl.LOCK_NB
+
+ try:
+ fcntl.lockf(lockfile, flags)
+ return True
+ except (IOError, OSError) as e:
+ if e.errno in (errno.EACCES, errno.EAGAIN):
+ return False
+ else:
+ raise e
+
+ @staticmethod
+ def unlock(lockfile):
+ fcntl.lockf(lockfile, fcntl.LOCK_UN)
+
+ @staticmethod
+ def get_handle(path):
+ return open(path, 'a+')
+
+ @staticmethod
+ def close_handle(lockfile):
+ lockfile.close()
+
+
+if os.name == 'nt':
+ import msvcrt
+ import fasteners.pywin32.pywintypes as pywintypes
+ import fasteners.pywin32.win32con as win32con
+ import fasteners.pywin32.win32file as win32file
+
+ _interprocess_reader_writer_mechanism = _WindowsInterProcessReaderWriterLockMechanism()
+ _interprocess_mechanism = _WindowsInterProcessMechanism()
+
+else:
+ import fcntl
+
+ _interprocess_reader_writer_mechanism = _FcntlInterProcessReaderWriterLockMechanism()
+ _interprocess_mechanism = _FcntlInterProcessMechanism()
diff --git a/fasteners/version.py b/fasteners/version.py
index 88fc0e6..3ec34f5 100644
--- a/fasteners/version.py
+++ b/fasteners/version.py
@@ -17,7 +17,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-_VERSION = "0.17.3"
+_VERSION = "0.18"
def version_string():
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..cf38408
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,48 @@
+site_name: Fasteners
+site_url: https://fasteners.readthedocs.io/
+repo_url: https://github.com/harlowja/fasteners
+edit_uri: ''
+
+nav:
+ - Home: index.md
+ - User Guide:
+ - guide/index.md
+ - Process locks: guide/inter_process.md
+ - Thread locks: guide/inter_thread.md
+ - Glossary: guide/glossary.md
+ - Reference:
+ - Process locks: api/inter_process.md
+ - Thread locks: api/inter_thread.md
+ - Changelog: CHANGELOG.md
+
+theme:
+ name: material
+ features:
+ - navigation.indexes
+ - navigation.instant
+ - navigation.tracking
+ logo: img/favicon-192x192.png
+ favicon: img/favicon-32x32.png
+
+plugins:
+ - search
+ - mkdocstrings:
+ default_handler: python
+ handlers:
+ python:
+ rendering:
+ show_category_heading: true
+ show_source: false
+ members_order: source
+ show_if_no_docstring: false
+ show_root_full_path: false
+ show_root_heading: true
+ watch:
+ - fasteners
+
+markdown_extensions:
+ - pymdownx.highlight:
+ anchor_linenums: true
+ - pymdownx.inlinehilite
+ - pymdownx.snippets
+ - pymdownx.superfences
\ No newline at end of file
diff --git a/publish.md b/publish.md
index df0de81..e482dee 100644
--- a/publish.md
+++ b/publish.md
@@ -5,7 +5,7 @@
2. Update the version number:
setup.cfg
- fasteners/version
+ fasteners/version.py
4. Make sure that the working directory is clean.
@@ -22,4 +22,6 @@
twine upload dist/*
-7. Tag the git repo.
\ No newline at end of file
+7. Tag the git repo.
+
+8. Read the docs will be updated automatically.
diff --git a/requirements-docs.txt b/requirements-docs.txt
index cbf1e36..cd7299f 100644
--- a/requirements-docs.txt
+++ b/requirements-docs.txt
@@ -1,2 +1,3 @@
-sphinx
-sphinx-rtd-theme
+mkdocs
+mkdocstrings[python] >= 0.18
+mkdocs-material
diff --git a/requirements-test.txt b/requirements-test.txt
index 38cd410..8c77595 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -1,4 +1,4 @@
diskcache
+eventlet
more_itertools
-futures
pytest
diff --git a/setup.cfg b/setup.cfg
index 796790e..348890e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
[metadata]
name = fasteners
-version = 0.17.3
+version = 0.18
url = https://github.com/harlowja/fasteners
author = Joshua Harlow
@@ -33,7 +33,8 @@ license_files = LICENSE
[options]
packages = find:
python_requires = >=3.6
-include_package_data = True
[options.packages.find]
-exclude = tests
+exclude =
+ tests
+ tests_eventlet
diff --git a/tests/test_process_lock.py b/tests/test_process_lock.py
index 7aa61c7..c9b793e 100644
--- a/tests/test_process_lock.py
+++ b/tests/test_process_lock.py
@@ -28,6 +28,7 @@ import time
import pytest
from fasteners import process_lock as pl
+from fasteners.process_mechanism import _interprocess_mechanism
WIN32 = os.name == 'nt'
@@ -151,9 +152,9 @@ def _lock_files(lock_path, handles_dir, num_handles=50):
count = 0
for handle in handles:
try:
- pl.InterProcessLock._trylock(handle)
+ _interprocess_mechanism.trylock(handle)
count += 1
- pl.InterProcessLock._unlock(handle)
+ _interprocess_mechanism.unlock(handle)
except IOError:
sys.exit(2)
finally:
diff --git a/tests_eventlet/__init__.py b/tests_eventlet/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests_eventlet/test_eventlet.py b/tests_eventlet/test_eventlet.py
new file mode 100644
index 0000000..8d0f1f7
--- /dev/null
+++ b/tests_eventlet/test_eventlet.py
@@ -0,0 +1,41 @@
+import eventlet
+
+eventlet.monkey_patch(thread=True)
+
+from fasteners import ReaderWriterLock
+
+
+def test_eventlet_spawn_n_bug():
+ """Both threads run at the same time thru the lock"""
+ STARTED = eventlet.event.Event()
+ FINISHED = eventlet.event.Event()
+ lock = ReaderWriterLock()
+
+ def other():
+ STARTED.send('started')
+ with lock.write_lock():
+ FINISHED.send('finished')
+
+ with lock.write_lock():
+ eventlet.spawn_n(other)
+ STARTED.wait()
+ assert FINISHED.wait(1) == 'finished'
+
+
+def test_eventlet_spawn_n_bugfix():
+ """Threads wait for each other as they should"""
+ STARTED = eventlet.event.Event()
+ FINISHED = eventlet.event.Event()
+ lock = ReaderWriterLock(current_thread_functor=eventlet.getcurrent)
+
+ def other():
+ STARTED.send('started')
+ with lock.write_lock():
+ FINISHED.send('finished')
+
+ with lock.write_lock():
+ eventlet.spawn_n(other)
+ STARTED.wait()
+ assert FINISHED.wait(1) is None
+
+ assert FINISHED.wait(1) == 'finished'
Debdiff
[The following lists of changes regard files as different if they have different names, permissions or owners.]
Files in second set of .debs but not in first
-rw-r--r-- root/root /usr/lib/python3/dist-packages/fasteners-0.18.dist-info/METADATA -rw-r--r-- root/root /usr/lib/python3/dist-packages/fasteners-0.18.dist-info/RECORD -rw-r--r-- root/root /usr/lib/python3/dist-packages/fasteners-0.18.dist-info/WHEEL -rw-r--r-- root/root /usr/lib/python3/dist-packages/fasteners-0.18.dist-info/top_level.txt -rw-r--r-- root/root /usr/lib/python3/dist-packages/fasteners/process_mechanism.py
Files in first set of .debs but not in second
-rw-r--r-- root/root /usr/lib/python3/dist-packages/fasteners-0.17.3.dist-info/METADATA -rw-r--r-- root/root /usr/lib/python3/dist-packages/fasteners-0.17.3.dist-info/RECORD -rw-r--r-- root/root /usr/lib/python3/dist-packages/fasteners-0.17.3.dist-info/WHEEL -rw-r--r-- root/root /usr/lib/python3/dist-packages/fasteners-0.17.3.dist-info/top_level.txt
No differences were encountered in the control files