Skip to content

Commit e5b4e10

Browse files
authored
Drop python 2.7 and 3.5 support, add type hints (#100)
1 parent f4b4912 commit e5b4e10

File tree

15 files changed

+245
-205
lines changed

15 files changed

+245
-205
lines changed

.github/workflows/check.yml

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,13 @@ jobs:
2929
- 3.8
3030
- 3.7
3131
- 3.6
32-
- 3.5
3332
- pypy3
34-
- 2.7
35-
- pypy2
3633

3734
steps:
3835
- name: Setup python for tox
3936
uses: actions/setup-python@v2
4037
with:
41-
python-version: 3.9
38+
python-version: 3.10.0-rc.2
4239
- name: Install tox
4340
run: python -m pip install tox
4441
- uses: actions/checkout@v2
@@ -87,6 +84,7 @@ jobs:
8784
- windows-latest
8885
tox_env:
8986
- dev
87+
- type
9088
- docs
9189
- readme
9290
exclude:
@@ -95,10 +93,10 @@ jobs:
9593
- uses: actions/checkout@v2
9694
with:
9795
fetch-depth: 0
98-
- name: Setup Python 3.9
96+
- name: Setup Python 3.10.0-rc.2
9997
uses: actions/setup-python@v2
10098
with:
101-
python-version: 3.9
99+
python-version: 3.10.0-rc.2
102100
- name: Install tox
103101
run: python -m pip install tox
104102
- name: Setup test suite
@@ -114,7 +112,7 @@ jobs:
114112
- name: Setup python to build package
115113
uses: actions/setup-python@v2
116114
with:
117-
python-version: 3.9
115+
python-version: 3.10.0-rc.2
118116
- name: Install https://pypi.org/project/build
119117
run: python -m pip install build
120118
- uses: actions/checkout@v2

.pre-commit-config.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ repos:
1515
rev: v2.28.0
1616
hooks:
1717
- id: pyupgrade
18+
args: [ "--py36-plus" ]
1819
- repo: https://github.com/PyCQA/isort
1920
rev: 5.9.3
2021
hooks:
@@ -42,7 +43,7 @@ repos:
4243
rev: v1.17.0
4344
hooks:
4445
- id: setup-cfg-fmt
45-
args: [ --min-py3-version, "3.5", "--max-py-version", "3.10" ]
46+
args: [ --min-py3-version, "3.6", "--max-py-version", "3.10" ]
4647
- repo: https://github.com/PyCQA/flake8
4748
rev: 3.9.2
4849
hooks:

codecov.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1+
coverage:
2+
status:
3+
patch:
4+
default:
5+
informational: true
16
comment: false

setup.cfg

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,8 @@ classifiers =
1414
License :: Public Domain
1515
Operating System :: OS Independent
1616
Programming Language :: Python
17-
Programming Language :: Python :: 2
18-
Programming Language :: Python :: 2.7
1917
Programming Language :: Python :: 3
20-
Programming Language :: Python :: 3.5
18+
Programming Language :: Python :: 3 :: Only
2119
Programming Language :: Python :: 3.6
2220
Programming Language :: Python :: 3.7
2321
Programming Language :: Python :: 3.8
@@ -33,7 +31,7 @@ project_urls =
3331

3432
[options]
3533
packages = find:
36-
python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
34+
python_requires = >=3.6
3735
package_dir =
3836
=src
3937
zip_safe = True
@@ -47,16 +45,18 @@ docs =
4745
sphinx>=4.1
4846
sphinx-autodoc-typehints>=1.12
4947
testing =
48+
covdefaults>=1.2.0
5049
coverage>=4
5150
pytest>=4
5251
pytest-cov
5352
pytest-timeout>=1.4.2
5453

55-
[bdist_wheel]
56-
universal = true
54+
[options.package_data]
55+
filelock = py.typed
5756

58-
[coverage:report]
59-
show_missing = True
57+
[coverage:run]
58+
plugins = covdefaults
59+
parallel = true
6060

6161
[coverage:paths]
6262
source =
@@ -67,6 +67,12 @@ source =
6767
*/src
6868
*\src
6969

70-
[coverage:run]
71-
branch = true
72-
parallel = true
70+
[coverage:report]
71+
fail_under = 88
72+
73+
[coverage:html]
74+
show_contexts = true
75+
skip_covered = false
76+
77+
[coverage:covdefaults]
78+
subtract_omit = */.tox/*

src/filelock/__init__.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88
import sys
99
import warnings
10+
from typing import Type
1011

1112
from ._api import AcquireReturnProxy, BaseFileLock
1213
from ._error import Timeout
@@ -16,30 +17,31 @@
1617
from .version import version
1718

1819
#: version of the project as a string
19-
__version__ = version
20+
__version__: str = version
2021

2122

22-
if sys.platform == "win32":
23-
_FileLock = WindowsFileLock
24-
elif has_fcntl:
25-
_FileLock = UnixFileLock
26-
else:
27-
_FileLock = SoftFileLock
28-
if warnings is not None:
29-
warnings.warn("only soft file lock is available")
23+
if sys.platform == "win32": # pragma: win32 cover
24+
_FileLock: Type[BaseFileLock] = WindowsFileLock
25+
else: # pragma: win32 no cover
26+
if has_fcntl:
27+
_FileLock: Type[BaseFileLock] = UnixFileLock
28+
else:
29+
_FileLock = SoftFileLock
30+
if warnings is not None:
31+
warnings.warn("only soft file lock is available")
3032

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

3537

3638
__all__ = [
3739
"__version__",
3840
"FileLock",
3941
"SoftFileLock",
40-
"WindowsFileLock",
42+
"Timeout",
4143
"UnixFileLock",
44+
"WindowsFileLock",
4245
"BaseFileLock",
43-
"Timeout",
4446
"AcquireReturnProxy",
4547
]

src/filelock/_api.py

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import logging
22
import time
3+
from abc import ABC, abstractmethod
34
from threading import Lock
5+
from types import TracebackType
6+
from typing import Optional, Type, Union
47

58
from ._error import Timeout
69

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

17-
def __init__(self, lock):
20+
def __init__(self, lock: "BaseFileLock") -> None:
1821
self.lock = lock
1922

20-
def __enter__(self):
23+
def __enter__(self) -> "BaseFileLock":
2124
return self.lock
2225

23-
def __exit__(self, exc_type, exc_value, traceback): # noqa: U100
26+
def __exit__(
27+
self,
28+
exc_type: Optional[Type[BaseException]], # noqa: U100
29+
exc_value: Optional[BaseException], # noqa: U100
30+
traceback: Optional[TracebackType], # noqa: U100
31+
) -> None:
2432
self.lock.release()
2533

2634

27-
class BaseFileLock(object):
35+
class BaseFileLock(ABC):
2836
"""Abstract base class for a file lock object."""
2937

30-
def __init__(self, lock_file, timeout=-1):
38+
def __init__(self, lock_file: str, timeout: float = -1) -> None:
3139
"""
3240
Create a new lock object.
3341
@@ -37,29 +45,29 @@ def __init__(self, lock_file, timeout=-1):
3745
A timeout of 0 means, that there is exactly one attempt to acquire the file lock.
3846
"""
3947
# The path to the lock file.
40-
self._lock_file = lock_file
48+
self._lock_file: str = lock_file
4149

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

4654
# The default timeout value.
47-
self.timeout = timeout
55+
self.timeout: float = timeout
4856

4957
# We use this lock primarily for the lock counter.
50-
self._thread_lock = Lock()
58+
self._thread_lock: Lock = Lock()
5159

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

5664
@property
57-
def lock_file(self):
65+
def lock_file(self) -> str:
5866
""":return: path to the lock file"""
5967
return self._lock_file
6068

6169
@property
62-
def timeout(self):
70+
def timeout(self) -> float:
6371
"""
6472
:return: the default timeout value
6573
@@ -68,24 +76,26 @@ def timeout(self):
6876
return self._timeout
6977

7078
@timeout.setter
71-
def timeout(self, value):
79+
def timeout(self, value: Union[float, str]) -> None:
7280
"""
7381
Change the default timeout value.
7482
7583
:param value: the new value
7684
"""
7785
self._timeout = float(value)
7886

79-
def _acquire(self):
87+
@abstractmethod
88+
def _acquire(self) -> None:
8089
"""If the file lock could be acquired, self._lock_file_fd holds the file descriptor of the lock file."""
8190
raise NotImplementedError
8291

83-
def _release(self):
92+
@abstractmethod
93+
def _release(self) -> None:
8494
"""Releases the lock and sets self._lock_file_fd to None."""
8595
raise NotImplementedError
8696

8797
@property
88-
def is_locked(self):
98+
def is_locked(self) -> bool:
8999
"""
90100
91101
:return: A boolean indicating if the lock file is holding the lock currently.
@@ -96,7 +106,7 @@ def is_locked(self):
96106
"""
97107
return self._lock_file_fd is not None
98108

99-
def acquire(self, timeout=None, poll_intervall=0.05):
109+
def acquire(self, timeout: Optional[float] = None, poll_intervall: float = 0.05) -> AcquireReturnProxy:
100110
"""
101111
Try to acquire the file lock.
102112
@@ -160,7 +170,7 @@ def acquire(self, timeout=None, poll_intervall=0.05):
160170
raise
161171
return AcquireReturnProxy(lock=self)
162172

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

183-
def __enter__(self):
193+
def __enter__(self) -> "BaseFileLock":
184194
"""
185195
Acquire the lock.
186196
@@ -189,7 +199,12 @@ def __enter__(self):
189199
self.acquire()
190200
return self
191201

192-
def __exit__(self, exc_type, exc_value, traceback): # noqa: U100
202+
def __exit__(
203+
self,
204+
exc_type: Optional[Type[BaseException]], # noqa: U100
205+
exc_value: Optional[BaseException], # noqa: U100
206+
traceback: Optional[TracebackType], # noqa: U100
207+
) -> None:
193208
"""
194209
Release the lock.
195210
@@ -199,7 +214,7 @@ def __exit__(self, exc_type, exc_value, traceback): # noqa: U100
199214
"""
200215
self.release()
201216

202-
def __del__(self):
217+
def __del__(self) -> None:
203218
"""Called when the lock object is deleted."""
204219
self.release(force=True)
205220

src/filelock/_error.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,12 @@
1-
import sys
2-
3-
if sys.version[0] == 3:
4-
TimeoutError = TimeoutError
5-
else:
6-
TimeoutError = OSError
7-
8-
91
class Timeout(TimeoutError):
102
"""Raised when the lock could not be acquired in *timeout* seconds."""
113

12-
def __init__(self, lock_file):
4+
def __init__(self, lock_file: str) -> None:
135
#: The path of the file lock.
146
self.lock_file = lock_file
157

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

1911

2012
__all__ = [

src/filelock/_soft.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
class SoftFileLock(BaseFileLock):
1010
"""Simply watches the existence of the lock file."""
1111

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

33-
def _release(self):
34-
os.close(self._lock_file_fd)
34+
def _release(self) -> None:
35+
os.close(self._lock_file_fd) # type: ignore # the lock file is definitely not None
3536
self._lock_file_fd = None
3637
try:
3738
os.remove(self._lock_file)

0 commit comments

Comments
 (0)