Skip to content

Commit

Permalink
Drop python 2.7 and 3.5 support, add type hints (#100)
Browse files Browse the repository at this point in the history
  • Loading branch information
gaborbernat authored Oct 3, 2021
1 parent f4b4912 commit e5b4e10
Show file tree
Hide file tree
Showing 15 changed files with 245 additions and 205 deletions.
12 changes: 5 additions & 7 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,13 @@ jobs:
- 3.8
- 3.7
- 3.6
- 3.5
- pypy3
- 2.7
- pypy2

steps:
- name: Setup python for tox
uses: actions/setup-python@v2
with:
python-version: 3.9
python-version: 3.10.0-rc.2
- name: Install tox
run: python -m pip install tox
- uses: actions/checkout@v2
Expand Down Expand Up @@ -87,6 +84,7 @@ jobs:
- windows-latest
tox_env:
- dev
- type
- docs
- readme
exclude:
Expand All @@ -95,10 +93,10 @@ jobs:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Python 3.9
- name: Setup Python 3.10.0-rc.2
uses: actions/setup-python@v2
with:
python-version: 3.9
python-version: 3.10.0-rc.2
- name: Install tox
run: python -m pip install tox
- name: Setup test suite
Expand All @@ -114,7 +112,7 @@ jobs:
- name: Setup python to build package
uses: actions/setup-python@v2
with:
python-version: 3.9
python-version: 3.10.0-rc.2
- name: Install https://pypi.org/project/build
run: python -m pip install build
- uses: actions/checkout@v2
Expand Down
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ repos:
rev: v2.28.0
hooks:
- id: pyupgrade
args: [ "--py36-plus" ]
- repo: https://github.com/PyCQA/isort
rev: 5.9.3
hooks:
Expand Down Expand Up @@ -42,7 +43,7 @@ repos:
rev: v1.17.0
hooks:
- id: setup-cfg-fmt
args: [ --min-py3-version, "3.5", "--max-py-version", "3.10" ]
args: [ --min-py3-version, "3.6", "--max-py-version", "3.10" ]
- repo: https://github.com/PyCQA/flake8
rev: 3.9.2
hooks:
Expand Down
5 changes: 5 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
coverage:
status:
patch:
default:
informational: true
comment: false
28 changes: 17 additions & 11 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ classifiers =
License :: Public Domain
Operating System :: OS Independent
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Expand All @@ -33,7 +31,7 @@ project_urls =

[options]
packages = find:
python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
python_requires = >=3.6
package_dir =
=src
zip_safe = True
Expand All @@ -47,16 +45,18 @@ docs =
sphinx>=4.1
sphinx-autodoc-typehints>=1.12
testing =
covdefaults>=1.2.0
coverage>=4
pytest>=4
pytest-cov
pytest-timeout>=1.4.2

[bdist_wheel]
universal = true
[options.package_data]
filelock = py.typed

[coverage:report]
show_missing = True
[coverage:run]
plugins = covdefaults
parallel = true

[coverage:paths]
source =
Expand All @@ -67,6 +67,12 @@ source =
*/src
*\src

[coverage:run]
branch = true
parallel = true
[coverage:report]
fail_under = 88

[coverage:html]
show_contexts = true
skip_covered = false

[coverage:covdefaults]
subtract_omit = */.tox/*
26 changes: 14 additions & 12 deletions src/filelock/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"""
import sys
import warnings
from typing import Type

from ._api import AcquireReturnProxy, BaseFileLock
from ._error import Timeout
Expand All @@ -16,30 +17,31 @@
from .version import version

#: version of the project as a string
__version__ = version
__version__: str = version


if sys.platform == "win32":
_FileLock = WindowsFileLock
elif has_fcntl:
_FileLock = UnixFileLock
else:
_FileLock = SoftFileLock
if warnings is not None:
warnings.warn("only soft file lock is available")
if sys.platform == "win32": # pragma: win32 cover
_FileLock: Type[BaseFileLock] = WindowsFileLock
else: # pragma: win32 no cover
if has_fcntl:
_FileLock: Type[BaseFileLock] = UnixFileLock
else:
_FileLock = SoftFileLock
if warnings is not None:
warnings.warn("only soft file lock is available")

#: Alias for the lock, which should be used for the current platform. On Windows, this is an alias for
# :class:`WindowsFileLock`, on Unix for :class:`UnixFileLock` and otherwise for :class:`SoftFileLock`.
FileLock = _FileLock
FileLock: Type[BaseFileLock] = _FileLock


__all__ = [
"__version__",
"FileLock",
"SoftFileLock",
"WindowsFileLock",
"Timeout",
"UnixFileLock",
"WindowsFileLock",
"BaseFileLock",
"Timeout",
"AcquireReturnProxy",
]
59 changes: 37 additions & 22 deletions src/filelock/_api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import logging
import time
from abc import ABC, abstractmethod
from threading import Lock
from types import TracebackType
from typing import Optional, Type, Union

from ._error import Timeout

Expand All @@ -11,23 +14,28 @@
# This is a helper class which is returned by :meth:`BaseFileLock.acquire` and wraps the lock to make sure __enter__
# is not called twice when entering the with statement. If we would simply return *self*, the lock would be acquired
# again in the *__enter__* method of the BaseFileLock, but not released again automatically. issue #37 (memory leak)
class AcquireReturnProxy(object):
class AcquireReturnProxy:
"""A context aware object that will release the lock file when exiting."""

def __init__(self, lock):
def __init__(self, lock: "BaseFileLock") -> None:
self.lock = lock

def __enter__(self):
def __enter__(self) -> "BaseFileLock":
return self.lock

def __exit__(self, exc_type, exc_value, traceback): # noqa: U100
def __exit__(
self,
exc_type: Optional[Type[BaseException]], # noqa: U100
exc_value: Optional[BaseException], # noqa: U100
traceback: Optional[TracebackType], # noqa: U100
) -> None:
self.lock.release()


class BaseFileLock(object):
class BaseFileLock(ABC):
"""Abstract base class for a file lock object."""

def __init__(self, lock_file, timeout=-1):
def __init__(self, lock_file: str, timeout: float = -1) -> None:
"""
Create a new lock object.
Expand All @@ -37,29 +45,29 @@ def __init__(self, lock_file, timeout=-1):
A timeout of 0 means, that there is exactly one attempt to acquire the file lock.
"""
# The path to the lock file.
self._lock_file = lock_file
self._lock_file: str = lock_file

# The file descriptor for the *_lock_file* as it is returned by the os.open() function.
# This file lock is only NOT None, if the object currently holds the lock.
self._lock_file_fd = None
self._lock_file_fd: Optional[int] = None

# The default timeout value.
self.timeout = timeout
self.timeout: float = timeout

# We use this lock primarily for the lock counter.
self._thread_lock = Lock()
self._thread_lock: Lock = Lock()

# The lock counter is used for implementing the nested locking mechanism. Whenever the lock is acquired, the
# counter is increased and the lock is only released, when this value is 0 again.
self._lock_counter = 0
self._lock_counter: int = 0

@property
def lock_file(self):
def lock_file(self) -> str:
""":return: path to the lock file"""
return self._lock_file

@property
def timeout(self):
def timeout(self) -> float:
"""
:return: the default timeout value
Expand All @@ -68,24 +76,26 @@ def timeout(self):
return self._timeout

@timeout.setter
def timeout(self, value):
def timeout(self, value: Union[float, str]) -> None:
"""
Change the default timeout value.
:param value: the new value
"""
self._timeout = float(value)

def _acquire(self):
@abstractmethod
def _acquire(self) -> None:
"""If the file lock could be acquired, self._lock_file_fd holds the file descriptor of the lock file."""
raise NotImplementedError

def _release(self):
@abstractmethod
def _release(self) -> None:
"""Releases the lock and sets self._lock_file_fd to None."""
raise NotImplementedError

@property
def is_locked(self):
def is_locked(self) -> bool:
"""
:return: A boolean indicating if the lock file is holding the lock currently.
Expand All @@ -96,7 +106,7 @@ def is_locked(self):
"""
return self._lock_file_fd is not None

def acquire(self, timeout=None, poll_intervall=0.05):
def acquire(self, timeout: Optional[float] = None, poll_intervall: float = 0.05) -> AcquireReturnProxy:
"""
Try to acquire the file lock.
Expand Down Expand Up @@ -160,7 +170,7 @@ def acquire(self, timeout=None, poll_intervall=0.05):
raise
return AcquireReturnProxy(lock=self)

def release(self, force=False):
def release(self, force: bool = False) -> None:
"""
Releases the file lock. Please note, that the lock is only completely released, if the lock counter is 0. Also
note, that the lock file itself is not automatically deleted.
Expand All @@ -180,7 +190,7 @@ def release(self, force=False):
self._lock_counter = 0
_LOGGER.debug("Lock %s released on %s", lock_id, lock_filename)

def __enter__(self):
def __enter__(self) -> "BaseFileLock":
"""
Acquire the lock.
Expand All @@ -189,7 +199,12 @@ def __enter__(self):
self.acquire()
return self

def __exit__(self, exc_type, exc_value, traceback): # noqa: U100
def __exit__(
self,
exc_type: Optional[Type[BaseException]], # noqa: U100
exc_value: Optional[BaseException], # noqa: U100
traceback: Optional[TracebackType], # noqa: U100
) -> None:
"""
Release the lock.
Expand All @@ -199,7 +214,7 @@ def __exit__(self, exc_type, exc_value, traceback): # noqa: U100
"""
self.release()

def __del__(self):
def __del__(self) -> None:
"""Called when the lock object is deleted."""
self.release(force=True)

Expand Down
14 changes: 3 additions & 11 deletions src/filelock/_error.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
import sys

if sys.version[0] == 3:
TimeoutError = TimeoutError
else:
TimeoutError = OSError


class Timeout(TimeoutError):
"""Raised when the lock could not be acquired in *timeout* seconds."""

def __init__(self, lock_file):
def __init__(self, lock_file: str) -> None:
#: The path of the file lock.
self.lock_file = lock_file

def __str__(self):
return "The file lock '{}' could not be acquired.".format(self.lock_file)
def __str__(self) -> str:
return f"The file lock '{self.lock_file}' could not be acquired."


__all__ = [
Expand Down
9 changes: 5 additions & 4 deletions src/filelock/_soft.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
class SoftFileLock(BaseFileLock):
"""Simply watches the existence of the lock file."""

def _acquire(self):
def _acquire(self) -> None:
raise_on_exist_ro_file(self._lock_file)
# first check for exists and read-only mode as the open will mask this case as EEXIST
mode = (
Expand All @@ -25,13 +25,14 @@ def _acquire(self):
pass
elif exception.errno == ENOENT: # No such file or directory - parent directory is missing
raise
elif exception.errno == EACCES and sys.platform != "win32": # Permission denied - parent dir is R/O
elif exception.errno == EACCES and sys.platform != "win32": # pragma: win32 no cover
# Permission denied - parent dir is R/O
raise # note windows does not allow you to make a folder r/o only files
else:
self._lock_file_fd = fd

def _release(self):
os.close(self._lock_file_fd)
def _release(self) -> None:
os.close(self._lock_file_fd) # type: ignore # the lock file is definitely not None
self._lock_file_fd = None
try:
os.remove(self._lock_file)
Expand Down
Loading

0 comments on commit e5b4e10

Please sign in to comment.