From d42529f437efa4a39a4ad7afadf788a0f3bf6c12 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 18 Nov 2024 05:23:45 +0100 Subject: [PATCH 01/10] testing new release with CI --- portalocker/__about__.py | 2 +- portalocker/__main__.py | 17 ++-- portalocker/constants.py | 4 +- portalocker/exceptions.py | 4 +- portalocker/portalocker.py | 26 +++--- portalocker/redis.py | 42 ++++++---- portalocker/types.py | 113 ++++++++++++++++++++++++++ portalocker/utils.py | 140 ++++++++++++++++++-------------- portalocker_tests/conftest.py | 2 +- portalocker_tests/test_redis.py | 2 +- portalocker_tests/tests.py | 4 +- pyproject.toml | 1 + ruff.toml | 4 + tox.ini | 1 + 14 files changed, 260 insertions(+), 102 deletions(-) create mode 100644 portalocker/types.py diff --git a/portalocker/__about__.py b/portalocker/__about__.py index a0b817a..8353d60 100644 --- a/portalocker/__about__.py +++ b/portalocker/__about__.py @@ -2,5 +2,5 @@ __author__ = 'Rick van Hattem' __email__ = 'wolph@wol.ph' __version__ = '2.10.1' -__description__ = '''Wraps the portalocker recipe for easy usage''' +__description__ = """Wraps the portalocker recipe for easy usage""" __url__ = 'https://github.com/WoLpH/portalocker' diff --git a/portalocker/__main__.py b/portalocker/__main__.py index ecac207..21eca48 100644 --- a/portalocker/__main__.py +++ b/portalocker/__main__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import logging import os @@ -25,7 +27,7 @@ logger = logging.getLogger(__name__) -def main(argv=None): +def main(argv: typing.Sequence[str] | None = None) -> None: parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(required=True) @@ -46,11 +48,14 @@ def main(argv=None): args.func(args) -def _read_file(path: pathlib.Path, seen_files: typing.Set[pathlib.Path]): +def _read_file( + path: pathlib.Path, + seen_files: typing.Set[pathlib.Path], +) -> typing.Iterator[str]: if path in seen_files: return - names = set() + names: set[str] = set() seen_files.add(path) paren = False from_ = None @@ -82,17 +87,17 @@ def _read_file(path: pathlib.Path, seen_files: typing.Set[pathlib.Path]): yield _clean_line(line, names) -def _clean_line(line, names): +def _clean_line(line: str, names: set[str]): # Replace `some_import.spam` with `spam` if names: joined_names = '|'.join(names) - line = re.sub(fr'\b({joined_names})\.', '', line) + line = re.sub(rf'\b({joined_names})\.', '', line) # Replace useless assignments (e.g. `spam = spam`) return _USELESS_ASSIGNMENT_RE.sub('', line) -def combine(args): +def combine(args: argparse.Namespace): output_file = args.output_file pathlib.Path(output_file.name).parent.mkdir(parents=True, exist_ok=True) diff --git a/portalocker/constants.py b/portalocker/constants.py index 2099f1f..198571f 100644 --- a/portalocker/constants.py +++ b/portalocker/constants.py @@ -1,4 +1,4 @@ -''' +""" Locking constants Lock types: @@ -13,7 +13,7 @@ Manually unlock, only needed internally - `UNBLOCK` unlock -''' +""" import enum import os diff --git a/portalocker/exceptions.py b/portalocker/exceptions.py index e871d13..54d1bfa 100644 --- a/portalocker/exceptions.py +++ b/portalocker/exceptions.py @@ -1,5 +1,7 @@ import typing +from portalocker import types + class BaseLockException(Exception): # noqa: N818 # Error codes: @@ -8,7 +10,7 @@ class BaseLockException(Exception): # noqa: N818 def __init__( self, *args: typing.Any, - fh: typing.Union[typing.IO, None, int] = None, + fh: typing.Union[types.IO, None, int] = None, **kwargs: typing.Any, ) -> None: self.fh = fh diff --git a/portalocker/portalocker.py b/portalocker/portalocker.py index ceceeaa..32519fc 100644 --- a/portalocker/portalocker.py +++ b/portalocker/portalocker.py @@ -1,7 +1,9 @@ +from __future__ import annotations + import os import typing -from . import constants, exceptions +from . import constants, exceptions, types # Alias for readability. Due to import recursion issues we cannot do: # from .constants import LockFlags @@ -12,9 +14,9 @@ class HasFileno(typing.Protocol): def fileno(self) -> int: ... -LOCKER: typing.Optional[typing.Callable[ - [typing.Union[int, HasFileno], int], typing.Any]] = None - +LOCKER: typing.Optional[ + typing.Callable[[typing.Union[int, HasFileno], int], typing.Any] +] = None if os.name == 'nt': # pragma: no cover import msvcrt @@ -100,9 +102,9 @@ def unlock(file_: typing.IO): # The locking implementation. # Expected values are either fcntl.flock() or fcntl.lockf(), # but any callable that matches the syntax will be accepted. - LOCKER = fcntl.flock + LOCKER = fcntl.flock # pyright: ignore[reportConstantRedefinition] - def lock(file_: typing.Union[typing.IO, int], flags: LockFlags): + def lock(file: int | types.IO, flags: LockFlags): assert LOCKER is not None, 'We need a locking function in `LOCKER` ' # Locking with NON_BLOCKING without EXCLUSIVE or SHARED enabled # results in an error @@ -115,7 +117,7 @@ def lock(file_: typing.Union[typing.IO, int], flags: LockFlags): ) try: - LOCKER(file_, flags) + LOCKER(file, flags) except OSError as exc_value: # Python can use one of several different exception classes to # represent timeout (most likely is BlockingIOError and IOError), @@ -130,25 +132,25 @@ def lock(file_: typing.Union[typing.IO, int], flags: LockFlags): # again (if it wants to). raise exceptions.AlreadyLocked( exc_value, - fh=file_, + fh=file, ) from exc_value else: # Something else went wrong; don't wrap this so we stop # immediately. raise exceptions.LockException( exc_value, - fh=file_, + fh=file, ) from exc_value except EOFError as exc_value: # On NFS filesystems, flock can raise an EOFError raise exceptions.LockException( exc_value, - fh=file_, + fh=file, ) from exc_value - def unlock(file_: typing.IO): + def unlock(file: types.IO): assert LOCKER is not None, 'We need a locking function in `LOCKER` ' - LOCKER(file_.fileno(), LockFlags.UNBLOCK) + LOCKER(file.fileno(), LockFlags.UNBLOCK) else: # pragma: no cover raise RuntimeError('PortaLocker only defined for nt and posix platforms') diff --git a/portalocker/redis.py b/portalocker/redis.py index 11ee876..523f9d7 100644 --- a/portalocker/redis.py +++ b/portalocker/redis.py @@ -1,3 +1,4 @@ +# pyright: reportUnknownMemberType=false import _thread import json import logging @@ -5,7 +6,7 @@ import time import typing -from redis import client +import redis from . import exceptions, utils @@ -15,8 +16,8 @@ DEFAULT_THREAD_SLEEP_TIME = 0.1 -class PubSubWorkerThread(client.PubSubWorkerThread): # type: ignore - def run(self): +class PubSubWorkerThread(redis.client.PubSubWorkerThread): # type: ignore + def run(self) -> None: try: super().run() except Exception: # pragma: no cover @@ -25,7 +26,7 @@ def run(self): class RedisLock(utils.LockBase): - ''' + """ An extremely reliable Redis lock based on pubsub with a keep-alive thread As opposed to most Redis locking systems based on key/value pairs, @@ -59,30 +60,31 @@ class RedisLock(utils.LockBase): to override these you need to explicitly specify a value (e.g. `health_check_interval=0`) - ''' + """ redis_kwargs: typing.Dict[str, typing.Any] thread: typing.Optional[PubSubWorkerThread] channel: str timeout: float - connection: typing.Optional[client.Redis] - pubsub: typing.Optional[client.PubSub] = None + connection: typing.Optional[redis.client.Redis[str]] + pubsub: typing.Optional[redis.client.PubSub] = None close_connection: bool DEFAULT_REDIS_KWARGS: typing.ClassVar[typing.Dict[str, typing.Any]] = dict( health_check_interval=10, + decode_responses=True, ) def __init__( self, channel: str, - connection: typing.Optional[client.Redis] = None, + connection: typing.Optional[redis.client.Redis[str]] = None, timeout: typing.Optional[float] = None, check_interval: typing.Optional[float] = None, fail_when_locked: typing.Optional[bool] = False, thread_sleep_time: float = DEFAULT_THREAD_SLEEP_TIME, unavailable_timeout: float = DEFAULT_UNAVAILABLE_TIMEOUT, - redis_kwargs: typing.Optional[typing.Dict] = None, + redis_kwargs: typing.Optional[typing.Dict[str, typing.Any]] = None, ): # We don't want to close connections given as an argument self.close_connection = not connection @@ -103,18 +105,22 @@ def __init__( fail_when_locked=fail_when_locked, ) - def get_connection(self) -> client.Redis: + def get_connection(self) -> redis.client.Redis[str]: if not self.connection: - self.connection = client.Redis(**self.redis_kwargs) + self.connection = redis.client.Redis(**self.redis_kwargs) return self.connection - def channel_handler(self, message): + def channel_handler(self, message: typing.Dict[str, str]) -> None: if message.get('type') != 'message': # pragma: no cover return + raw_data = message.get('data') + if not raw_data: + return + try: - data = json.loads(message.get('data')) + data = json.loads(raw_data) except TypeError: # pragma: no cover logger.debug('TypeError while parsing: %r', message) return @@ -189,7 +195,11 @@ def acquire( # type: ignore[override] raise exceptions.AlreadyLocked(exceptions) - def check_or_kill_lock(self, connection, timeout): + def check_or_kill_lock( + self, + connection: redis.client.Redis[str], + timeout: float, + ): # Random channel name to get messages back from the lock response_channel = f'{self.channel}-{random.random()}' @@ -217,7 +227,9 @@ def check_or_kill_lock(self, connection, timeout): for client_ in connection.client_list('pubsub'): # pragma: no cover if client_.get('name') == self.client_name: logger.warning('Killing unavailable redis client: %r', client_) - connection.client_kill_filter(client_.get('id')) + connection.client_kill_filter( # pyright: ignore + client_.get('id'), + ) return None def release(self): diff --git a/portalocker/types.py b/portalocker/types.py new file mode 100644 index 0000000..8abcbe7 --- /dev/null +++ b/portalocker/types.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import pathlib +import typing +from typing import Union + +Mode = typing.Literal[ + # Text modes + 'r', + 'rt', + 'tr', # Read text + 'w', + 'wt', + 'tw', # Write text + 'a', + 'at', + 'ta', # Append text + 'x', + 'xt', + 'tx', # Exclusive creation text + 'r+', + '+r', + 'rt+', + 'r+t', + '+rt', + 'tr+', + 't+r', + '+tr', # Read and write text + 'w+', + '+w', + 'wt+', + 'w+t', + '+wt', + 'tw+', + 't+w', + '+tw', # Write and read text + 'a+', + '+a', + 'at+', + 'a+t', + '+at', + 'ta+', + 't+a', + '+ta', # Append and read text + 'x+', + '+x', + 'xt+', + 'x+t', + '+xt', + 'tx+', + 't+x', + '+tx', # Exclusive creation and read text + 'U', + 'rU', + 'Ur', + 'rtU', + 'rUt', + 'Urt', + 'trU', + 'tUr', + 'Utr', # Universal newline support + # Binary modes + 'rb', + 'br', # Read binary + 'wb', + 'bw', # Write binary + 'ab', + 'ba', # Append binary + 'xb', + 'bx', # Exclusive creation binary + 'rb+', + 'r+b', + '+rb', + 'br+', + 'b+r', + '+br', # Read and write binary + 'wb+', + 'w+b', + '+wb', + 'bw+', + 'b+w', + '+bw', # Write and read binary + 'ab+', + 'a+b', + '+ab', + 'ba+', + 'b+a', + '+ba', # Append and read binary + 'xb+', + 'x+b', + '+xb', + 'bx+', + 'b+x', + '+bx', # Exclusive creation and read binary + 'rbU', + 'rUb', + 'Urb', + 'brU', + 'bUr', + 'Ubr', + # Universal newline support in binary mode +] +Filename = Union[str, pathlib.Path] +IO: typing.TypeAlias = Union[typing.IO[str], typing.IO[bytes]] + + +class FileOpenKwargs(typing.TypedDict): + buffering: int | None + encoding: str | None + errors: str | None + newline: str | None + closefd: bool | None + opener: typing.Callable[[str, int], int] | None diff --git a/portalocker/utils.py b/portalocker/utils.py index 5115b0e..ab0b7b5 100644 --- a/portalocker/utils.py +++ b/portalocker/utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import abc import atexit import contextlib @@ -10,7 +12,8 @@ import typing import warnings -from . import constants, exceptions, portalocker +from . import constants, exceptions, portalocker, types +from .types import Filename, Mode logger = logging.getLogger(__name__) @@ -24,11 +27,9 @@ 'open_atomic', ] -Filename = typing.Union[str, pathlib.Path] - def coalesce(*args: typing.Any, test_value: typing.Any = None) -> typing.Any: - '''Simple coalescing function that returns the first value that is not + """Simple coalescing function that returns the first value that is not equal to the `test_value`. Or `None` if no value is valid. Usually this means that the last given value is the default value. @@ -48,7 +49,7 @@ def coalesce(*args: typing.Any, test_value: typing.Any = None) -> typing.Any: # This won't work because of the `is not test_value` type testing: >>> coalesce([], dict(spam='eggs'), test_value=[]) [] - ''' + """ return next((arg for arg in args if arg is not test_value), None) @@ -56,8 +57,8 @@ def coalesce(*args: typing.Any, test_value: typing.Any = None) -> typing.Any: def open_atomic( filename: Filename, binary: bool = True, -) -> typing.Iterator[typing.IO]: - '''Open a file for atomic writing. Instead of locking this method allows +) -> typing.Iterator[types.IO]: + """Open a file for atomic writing. Instead of locking this method allows you to write the entire file and move it to the actual location. Note that this makes the assumption that a rename is atomic on your platform which is generally the case but not a guarantee. @@ -80,9 +81,13 @@ def open_atomic( ... written = fh.write(b'test') >>> assert path_filename.exists() >>> path_filename.unlink() - ''' + """ # `pathlib.Path` cast in case `path` is a `str` - path: pathlib.Path = pathlib.Path(filename) + path: pathlib.Path + if isinstance(filename, pathlib.Path): + path = filename + else: + path = pathlib.Path(filename) assert not path.exists(), f'{path!r} exists' @@ -169,12 +174,12 @@ def __exit__( self.release() return None - def __delete__(self, instance): + def __delete__(self, instance: LockBase): instance.release() class Lock(LockBase): - '''Lock manager with built-in timeout + """Lock manager with built-in timeout Args: filename: filename @@ -192,21 +197,31 @@ class Lock(LockBase): Note that the file is opened first and locked later. So using 'w' as mode will result in truncate _BEFORE_ the lock is checked. - ''' + """ + + fh: types.IO | None + filename: str + mode: str + truncate: bool + timeout: float + check_interval: float + fail_when_locked: bool + flags: constants.LockFlags + file_open_kwargs: dict[str, typing.Any] def __init__( self, filename: Filename, - mode: str = 'a', - timeout: typing.Optional[float] = None, + mode: Mode = 'a', + timeout: float | None = None, check_interval: float = DEFAULT_CHECK_INTERVAL, fail_when_locked: bool = DEFAULT_FAIL_WHEN_LOCKED, flags: constants.LockFlags = LOCK_METHOD, - **file_open_kwargs, + **file_open_kwargs: typing.Any, ): if 'w' in mode: truncate = True - mode = mode.replace('w', 'a') + mode = typing.cast(Mode, mode.replace('w', 'a')) else: truncate = False @@ -218,15 +233,13 @@ def __init__( stacklevel=1, ) - self.fh: typing.Optional[typing.IO] = None - self.filename: str = str(filename) - self.mode: str = mode - self.truncate: bool = truncate - self.timeout: float = timeout - self.check_interval: float = check_interval - self.fail_when_locked: bool = fail_when_locked - self.flags: constants.LockFlags = flags + self.fh = None + self.filename = str(filename) + self.mode = mode + self.truncate = truncate + self.flags = flags self.file_open_kwargs = file_open_kwargs + super().__init__(timeout, check_interval, fail_when_locked) def acquire( self, @@ -234,7 +247,7 @@ def acquire( check_interval: typing.Optional[float] = None, fail_when_locked: typing.Optional[bool] = None, ) -> typing.IO[typing.AnyStr]: - '''Acquire the locked filehandle''' + """Acquire the locked filehandle""" fail_when_locked = coalesce(fail_when_locked, self.fail_when_locked) @@ -248,9 +261,10 @@ def acquire( ) # If we already have a filehandle, return it - fh: typing.Optional[typing.IO] = self.fh + fh = self.fh if fh: - return fh + # Due to type invariance we need to cast the type + return typing.cast(typing.IO[typing.AnyStr], fh) # Get a new filehandler fh = self._get_fh() @@ -296,41 +310,44 @@ def try_close(): # pragma: no cover fh = self._prepare_fh(fh) self.fh = fh - return fh + return typing.cast(typing.IO[typing.AnyStr], fh) def __enter__(self) -> typing.IO[typing.AnyStr]: return self.acquire() def release(self): - '''Releases the currently locked file handle''' + """Releases the currently locked file handle""" if self.fh: portalocker.unlock(self.fh) self.fh.close() self.fh = None - def _get_fh(self) -> typing.IO: - '''Get a new filehandle''' - return open( # noqa: SIM115 - self.filename, - self.mode, - **self.file_open_kwargs, + def _get_fh(self) -> types.IO: + """Get a new filehandle""" + return typing.cast( + types.IO, + open( # noqa: SIM115 + self.filename, + self.mode, + **self.file_open_kwargs, + ), ) - def _get_lock(self, fh: typing.IO) -> typing.IO: - ''' + def _get_lock(self, fh: types.IO) -> types.IO: + """ Try to lock the given filehandle - returns LockException if it fails''' + returns LockException if it fails""" portalocker.lock(fh, self.flags) return fh - def _prepare_fh(self, fh: typing.IO) -> typing.IO: - ''' + def _prepare_fh(self, fh: types.IO) -> types.IO: + """ Prepare the filehandle for usage If truncate is a number, the file will be truncated to that amount of bytes - ''' + """ if self.truncate: fh.seek(0) fh.truncate(0) @@ -339,20 +356,20 @@ def _prepare_fh(self, fh: typing.IO) -> typing.IO: class RLock(Lock): - ''' + """ A reentrant lock, functions in a similar way to threading.RLock in that it can be acquired multiple times. When the corresponding number of release() calls are made the lock will finally release the underlying file lock. - ''' + """ def __init__( self, - filename, - mode='a', - timeout=DEFAULT_TIMEOUT, - check_interval=DEFAULT_CHECK_INTERVAL, - fail_when_locked=False, - flags=LOCK_METHOD, + filename: Filename, + mode: Mode = 'a', + timeout: float = DEFAULT_TIMEOUT, + check_interval: float = DEFAULT_CHECK_INTERVAL, + fail_when_locked: bool = False, + flags: constants.LockFlags = LOCK_METHOD, ): super().__init__( filename, @@ -369,9 +386,10 @@ def acquire( timeout: typing.Optional[float] = None, check_interval: typing.Optional[float] = None, fail_when_locked: typing.Optional[bool] = None, - ) -> typing.IO: + ) -> typing.IO[typing.AnyStr]: + fh: typing.IO[typing.AnyStr] if self._acquire_count >= 1: - fh = self.fh + fh = typing.cast(typing.IO[typing.AnyStr], self.fh) else: fh = super().acquire(timeout, check_interval, fail_when_locked) self._acquire_count += 1 @@ -392,11 +410,11 @@ def release(self): class TemporaryFileLock(Lock): def __init__( self, - filename='.lock', - timeout=DEFAULT_TIMEOUT, - check_interval=DEFAULT_CHECK_INTERVAL, - fail_when_locked=True, - flags=LOCK_METHOD, + filename: str = '.lock', + timeout: float = DEFAULT_TIMEOUT, + check_interval: float = DEFAULT_CHECK_INTERVAL, + fail_when_locked: bool = True, + flags: constants.LockFlags = LOCK_METHOD, ): Lock.__init__( self, @@ -416,7 +434,7 @@ def release(self): class BoundedSemaphore(LockBase): - ''' + """ Bounded semaphore to prevent too many parallel processes from running This method is deprecated because multiple processes that are completely @@ -429,7 +447,7 @@ class BoundedSemaphore(LockBase): 'bounded_semaphore.00.lock' >>> str(sorted(semaphore.get_random_filenames())[1]) 'bounded_semaphore.01.lock' - ''' + """ lock: typing.Optional[Lock] @@ -470,7 +488,7 @@ def get_random_filenames(self) -> typing.Sequence[pathlib.Path]: random.shuffle(filenames) return filenames - def get_filename(self, number) -> pathlib.Path: + def get_filename(self, number: int) -> pathlib.Path: return pathlib.Path(self.directory) / self.filename_pattern.format( name=self.name, number=number, @@ -522,7 +540,7 @@ def release(self): # pragma: no cover class NamedBoundedSemaphore(BoundedSemaphore): - ''' + """ Bounded semaphore to prevent too many parallel processes from running It's also possible to specify a timeout when acquiring the lock to wait @@ -544,7 +562,7 @@ class NamedBoundedSemaphore(BoundedSemaphore): >>> 'bounded_semaphore' in str(semaphore.get_filenames()[0]) True - ''' + """ def __init__( self, diff --git a/portalocker_tests/conftest.py b/portalocker_tests/conftest.py index 5650288..ad2dc23 100644 --- a/portalocker_tests/conftest.py +++ b/portalocker_tests/conftest.py @@ -27,6 +27,6 @@ def pytest_sessionstart(session): @pytest.fixture(autouse=True) def reduce_timeouts(monkeypatch): - 'For faster testing we reduce the timeouts.' + "For faster testing we reduce the timeouts." monkeypatch.setattr(utils, 'DEFAULT_TIMEOUT', 0.1) monkeypatch.setattr(utils, 'DEFAULT_CHECK_INTERVAL', 0.05) diff --git a/portalocker_tests/test_redis.py b/portalocker_tests/test_redis.py index e9bec02..2f274a1 100644 --- a/portalocker_tests/test_redis.py +++ b/portalocker_tests/test_redis.py @@ -46,7 +46,7 @@ def test_redis_lock(): @pytest.mark.parametrize('timeout', [None, 0, 0.001]) @pytest.mark.parametrize('check_interval', [None, 0, 0.0005]) def test_redis_lock_timeout(timeout, check_interval): - connection = client.Redis() + connection: client.Redis[str] = client.Redis(decode_responses=True) channel = str(random.random()) lock_a = redis.RedisLock(channel) lock_a.acquire(timeout=timeout, check_interval=check_interval) diff --git a/portalocker_tests/tests.py b/portalocker_tests/tests.py index ee0d91b..d3b57cd 100644 --- a/portalocker_tests/tests.py +++ b/portalocker_tests/tests.py @@ -412,7 +412,7 @@ def test_lock_fileno(tmpfile, locker): ) @pytest.mark.parametrize('locker', LOCKERS, indirect=True) def test_locker_mechanism(tmpfile, locker): - '''Can we switch the locking mechanism?''' + """Can we switch the locking mechanism?""" # We can test for flock vs lockf based on their different behaviour re. # locking the same file. with portalocker.Lock(tmpfile, 'a+', flags=LockFlags.EXCLUSIVE): @@ -434,7 +434,7 @@ def test_locker_mechanism(tmpfile, locker): def test_exception(monkeypatch, tmpfile): - '''Do we stop immediately if the locking fails, even with a timeout?''' + """Do we stop immediately if the locking fails, even with a timeout?""" def patched_lock(*args, **kwargs): raise ValueError('Test exception') diff --git a/pyproject.toml b/pyproject.toml index 0c61e2b..08ea8ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,3 +98,4 @@ skip = '*/htmlcov,./docs/_build,*.asc' [tool.pyright] include = ['portalocker', 'portalocker_tests'] exclude = ['dist/*'] +strict = ['portalocker'] diff --git a/ruff.toml b/ruff.toml index e4180ff..194a455 100644 --- a/ruff.toml +++ b/ruff.toml @@ -8,6 +8,10 @@ exclude = ['docs'] line-length = 80 +[format] +quote-style = 'single' + + [lint] ignore = [ 'A001', # Variable {name} is shadowing a Python builtin diff --git a/tox.ini b/tox.ini index 29bf765..371099c 100644 --- a/tox.ini +++ b/tox.ini @@ -42,6 +42,7 @@ changedir = basepython = python3 deps = pyright + types-redis -e{toxinidir}[tests,redis] commands = pyright {toxinidir}/portalocker {toxinidir}/portalocker_tests From cada500eda2b40f23b487fb4dd8de5015bdbc728 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 18 Nov 2024 12:08:43 +0100 Subject: [PATCH 02/10] Fixed several bugs with the testing suite. --- portalocker/__about__.py | 2 +- portalocker/__main__.py | 6 ++++++ portalocker/constants.py | 4 ++-- portalocker/redis.py | 8 +++++--- portalocker/utils.py | 18 +++++++++--------- portalocker_tests/conftest.py | 2 +- portalocker_tests/tests.py | 4 ++-- 7 files changed, 26 insertions(+), 18 deletions(-) diff --git a/portalocker/__about__.py b/portalocker/__about__.py index 8353d60..a0b817a 100644 --- a/portalocker/__about__.py +++ b/portalocker/__about__.py @@ -2,5 +2,5 @@ __author__ = 'Rick van Hattem' __email__ = 'wolph@wol.ph' __version__ = '2.10.1' -__description__ = """Wraps the portalocker recipe for easy usage""" +__description__ = '''Wraps the portalocker recipe for easy usage''' __url__ = 'https://github.com/WoLpH/portalocker' diff --git a/portalocker/__main__.py b/portalocker/__main__.py index 21eca48..e777e50 100644 --- a/portalocker/__main__.py +++ b/portalocker/__main__.py @@ -60,6 +60,9 @@ def _read_file( paren = False from_ = None for line in path.open(): + if '__future__' in line: + continue + if paren: if ')' in line: line = line.split(')', 1)[1] @@ -101,6 +104,9 @@ def combine(args: argparse.Namespace): output_file = args.output_file pathlib.Path(output_file.name).parent.mkdir(parents=True, exist_ok=True) + # We're handling this separately because it has to be the first import. + output_file.write('from __future__ import annotations\n') + output_file.write( _TEXT_TEMPLATE.format((base_path / 'README.rst').read_text()), ) diff --git a/portalocker/constants.py b/portalocker/constants.py index 198571f..2099f1f 100644 --- a/portalocker/constants.py +++ b/portalocker/constants.py @@ -1,4 +1,4 @@ -""" +''' Locking constants Lock types: @@ -13,7 +13,7 @@ Manually unlock, only needed internally - `UNBLOCK` unlock -""" +''' import enum import os diff --git a/portalocker/redis.py b/portalocker/redis.py index 523f9d7..cb94adf 100644 --- a/portalocker/redis.py +++ b/portalocker/redis.py @@ -1,4 +1,6 @@ # pyright: reportUnknownMemberType=false +from __future__ import annotations + import _thread import json import logging @@ -26,7 +28,7 @@ def run(self) -> None: class RedisLock(utils.LockBase): - """ + ''' An extremely reliable Redis lock based on pubsub with a keep-alive thread As opposed to most Redis locking systems based on key/value pairs, @@ -60,7 +62,7 @@ class RedisLock(utils.LockBase): to override these you need to explicitly specify a value (e.g. `health_check_interval=0`) - """ + ''' redis_kwargs: typing.Dict[str, typing.Any] thread: typing.Optional[PubSubWorkerThread] @@ -137,7 +139,7 @@ def acquire( # type: ignore[override] timeout: typing.Optional[float] = None, check_interval: typing.Optional[float] = None, fail_when_locked: typing.Optional[bool] = None, - ) -> 'RedisLock': + ) -> RedisLock: timeout = utils.coalesce(timeout, self.timeout, 0.0) check_interval = utils.coalesce( check_interval, diff --git a/portalocker/utils.py b/portalocker/utils.py index ab0b7b5..ab065b0 100644 --- a/portalocker/utils.py +++ b/portalocker/utils.py @@ -247,7 +247,7 @@ def acquire( check_interval: typing.Optional[float] = None, fail_when_locked: typing.Optional[bool] = None, ) -> typing.IO[typing.AnyStr]: - """Acquire the locked filehandle""" + '''Acquire the locked filehandle''' fail_when_locked = coalesce(fail_when_locked, self.fail_when_locked) @@ -316,14 +316,14 @@ def __enter__(self) -> typing.IO[typing.AnyStr]: return self.acquire() def release(self): - """Releases the currently locked file handle""" + '''Releases the currently locked file handle''' if self.fh: portalocker.unlock(self.fh) self.fh.close() self.fh = None def _get_fh(self) -> types.IO: - """Get a new filehandle""" + '''Get a new filehandle''' return typing.cast( types.IO, open( # noqa: SIM115 @@ -334,20 +334,20 @@ def _get_fh(self) -> types.IO: ) def _get_lock(self, fh: types.IO) -> types.IO: - """ + ''' Try to lock the given filehandle - returns LockException if it fails""" + returns LockException if it fails''' portalocker.lock(fh, self.flags) return fh def _prepare_fh(self, fh: types.IO) -> types.IO: - """ + ''' Prepare the filehandle for usage If truncate is a number, the file will be truncated to that amount of bytes - """ + ''' if self.truncate: fh.seek(0) fh.truncate(0) @@ -356,11 +356,11 @@ def _prepare_fh(self, fh: types.IO) -> types.IO: class RLock(Lock): - """ + ''' A reentrant lock, functions in a similar way to threading.RLock in that it can be acquired multiple times. When the corresponding number of release() calls are made the lock will finally release the underlying file lock. - """ + ''' def __init__( self, diff --git a/portalocker_tests/conftest.py b/portalocker_tests/conftest.py index ad2dc23..5650288 100644 --- a/portalocker_tests/conftest.py +++ b/portalocker_tests/conftest.py @@ -27,6 +27,6 @@ def pytest_sessionstart(session): @pytest.fixture(autouse=True) def reduce_timeouts(monkeypatch): - "For faster testing we reduce the timeouts." + 'For faster testing we reduce the timeouts.' monkeypatch.setattr(utils, 'DEFAULT_TIMEOUT', 0.1) monkeypatch.setattr(utils, 'DEFAULT_CHECK_INTERVAL', 0.05) diff --git a/portalocker_tests/tests.py b/portalocker_tests/tests.py index d3b57cd..ee0d91b 100644 --- a/portalocker_tests/tests.py +++ b/portalocker_tests/tests.py @@ -412,7 +412,7 @@ def test_lock_fileno(tmpfile, locker): ) @pytest.mark.parametrize('locker', LOCKERS, indirect=True) def test_locker_mechanism(tmpfile, locker): - """Can we switch the locking mechanism?""" + '''Can we switch the locking mechanism?''' # We can test for flock vs lockf based on their different behaviour re. # locking the same file. with portalocker.Lock(tmpfile, 'a+', flags=LockFlags.EXCLUSIVE): @@ -434,7 +434,7 @@ def test_locker_mechanism(tmpfile, locker): def test_exception(monkeypatch, tmpfile): - """Do we stop immediately if the locking fails, even with a timeout?""" + '''Do we stop immediately if the locking fails, even with a timeout?''' def patched_lock(*args, **kwargs): raise ValueError('Test exception') From db8719379a6ac0394dd6710438767d3eaafeb93f Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 18 Nov 2024 12:14:01 +0100 Subject: [PATCH 03/10] made ruff happy, hopefully no issues with these updates. --- portalocker/__main__.py | 4 +-- portalocker/portalocker.py | 6 ++-- portalocker/redis.py | 28 +++++++-------- portalocker/utils.py | 72 +++++++++++++++++++------------------- portalocker_tests/tests.py | 2 +- ruff.toml | 2 +- 6 files changed, 56 insertions(+), 58 deletions(-) diff --git a/portalocker/__main__.py b/portalocker/__main__.py index e777e50..ed8cf4e 100644 --- a/portalocker/__main__.py +++ b/portalocker/__main__.py @@ -50,7 +50,7 @@ def main(argv: typing.Sequence[str] | None = None) -> None: def _read_file( path: pathlib.Path, - seen_files: typing.Set[pathlib.Path], + seen_files: set[pathlib.Path], ) -> typing.Iterator[str]: if path in seen_files: return @@ -114,7 +114,7 @@ def combine(args: argparse.Namespace): _TEXT_TEMPLATE.format((base_path / 'LICENSE').read_text()), ) - seen_files: typing.Set[pathlib.Path] = set() + seen_files: set[pathlib.Path] = set() for line in _read_file(src_path / '__init__.py', seen_files): output_file.write(line) diff --git a/portalocker/portalocker.py b/portalocker/portalocker.py index 32519fc..9bdfa52 100644 --- a/portalocker/portalocker.py +++ b/portalocker/portalocker.py @@ -14,9 +14,7 @@ class HasFileno(typing.Protocol): def fileno(self) -> int: ... -LOCKER: typing.Optional[ - typing.Callable[[typing.Union[int, HasFileno], int], typing.Any] -] = None +LOCKER: typing.Callable[[int | HasFileno, int], typing.Any] | None = None if os.name == 'nt': # pragma: no cover import msvcrt @@ -28,7 +26,7 @@ def fileno(self) -> int: ... __overlapped = pywintypes.OVERLAPPED() - def lock(file_: typing.Union[typing.IO, int], flags: LockFlags): + def lock(file_: typing.IO | int, flags: LockFlags): # Windows locking does not support locking through `fh.fileno()` so # we cast it to make mypy and pyright happy file_ = typing.cast(typing.IO, file_) diff --git a/portalocker/redis.py b/portalocker/redis.py index cb94adf..f58c492 100644 --- a/portalocker/redis.py +++ b/portalocker/redis.py @@ -64,15 +64,15 @@ class RedisLock(utils.LockBase): ''' - redis_kwargs: typing.Dict[str, typing.Any] - thread: typing.Optional[PubSubWorkerThread] + redis_kwargs: dict[str, typing.Any] + thread: PubSubWorkerThread | None channel: str timeout: float - connection: typing.Optional[redis.client.Redis[str]] - pubsub: typing.Optional[redis.client.PubSub] = None + connection: redis.client.Redis[str] | None + pubsub: redis.client.PubSub | None = None close_connection: bool - DEFAULT_REDIS_KWARGS: typing.ClassVar[typing.Dict[str, typing.Any]] = dict( + DEFAULT_REDIS_KWARGS: typing.ClassVar[dict[str, typing.Any]] = dict( health_check_interval=10, decode_responses=True, ) @@ -80,13 +80,13 @@ class RedisLock(utils.LockBase): def __init__( self, channel: str, - connection: typing.Optional[redis.client.Redis[str]] = None, - timeout: typing.Optional[float] = None, - check_interval: typing.Optional[float] = None, - fail_when_locked: typing.Optional[bool] = False, + connection: redis.client.Redis[str] | None = None, + timeout: float | None = None, + check_interval: float | None = None, + fail_when_locked: bool | None = False, thread_sleep_time: float = DEFAULT_THREAD_SLEEP_TIME, unavailable_timeout: float = DEFAULT_UNAVAILABLE_TIMEOUT, - redis_kwargs: typing.Optional[typing.Dict[str, typing.Any]] = None, + redis_kwargs: dict[str, typing.Any] | None = None, ): # We don't want to close connections given as an argument self.close_connection = not connection @@ -113,7 +113,7 @@ def get_connection(self) -> redis.client.Redis[str]: return self.connection - def channel_handler(self, message: typing.Dict[str, str]) -> None: + def channel_handler(self, message: dict[str, str]) -> None: if message.get('type') != 'message': # pragma: no cover return @@ -136,9 +136,9 @@ def client_name(self): def acquire( # type: ignore[override] self, - timeout: typing.Optional[float] = None, - check_interval: typing.Optional[float] = None, - fail_when_locked: typing.Optional[bool] = None, + timeout: float | None = None, + check_interval: float | None = None, + fail_when_locked: bool | None = None, ) -> RedisLock: timeout = utils.coalesce(timeout, self.timeout, 0.0) check_interval = utils.coalesce( diff --git a/portalocker/utils.py b/portalocker/utils.py index ab065b0..f2c630e 100644 --- a/portalocker/utils.py +++ b/portalocker/utils.py @@ -94,15 +94,15 @@ def open_atomic( # Create the parent directory if it doesn't exist path.parent.mkdir(parents=True, exist_ok=True) - temp_fh = tempfile.NamedTemporaryFile( + with tempfile.NamedTemporaryFile( mode=binary and 'wb' or 'w', dir=str(path.parent), delete=False, - ) - yield temp_fh - temp_fh.flush() - os.fsync(temp_fh.fileno()) - temp_fh.close() + ) as temp_fh: + yield temp_fh + temp_fh.flush() + os.fsync(temp_fh.fileno()) + try: os.rename(temp_fh.name, path) finally: @@ -120,9 +120,9 @@ class LockBase(abc.ABC): # pragma: no cover def __init__( self, - timeout: typing.Optional[float] = None, - check_interval: typing.Optional[float] = None, - fail_when_locked: typing.Optional[bool] = None, + timeout: float | None = None, + check_interval: float | None = None, + fail_when_locked: bool | None = None, ): self.timeout = coalesce(timeout, DEFAULT_TIMEOUT) self.check_interval = coalesce(check_interval, DEFAULT_CHECK_INTERVAL) @@ -134,15 +134,15 @@ def __init__( @abc.abstractmethod def acquire( self, - timeout: typing.Optional[float] = None, - check_interval: typing.Optional[float] = None, - fail_when_locked: typing.Optional[bool] = None, + timeout: float | None = None, + check_interval: float | None = None, + fail_when_locked: bool | None = None, ) -> typing.IO[typing.AnyStr]: ... def _timeout_generator( self, - timeout: typing.Optional[float], - check_interval: typing.Optional[float], + timeout: float | None, + check_interval: float | None, ) -> typing.Iterator[int]: f_timeout = coalesce(timeout, self.timeout, 0.0) f_check_interval = coalesce(check_interval, self.check_interval, 0.0) @@ -167,10 +167,10 @@ def __enter__(self) -> typing.IO[typing.AnyStr]: def __exit__( self, - exc_type: typing.Optional[typing.Type[BaseException]], - exc_value: typing.Optional[BaseException], + exc_type: type[BaseException] | None, + exc_value: BaseException | None, traceback: typing.Any, # Should be typing.TracebackType - ) -> typing.Optional[bool]: + ) -> bool | None: self.release() return None @@ -243,9 +243,9 @@ def __init__( def acquire( self, - timeout: typing.Optional[float] = None, - check_interval: typing.Optional[float] = None, - fail_when_locked: typing.Optional[bool] = None, + timeout: float | None = None, + check_interval: float | None = None, + fail_when_locked: bool | None = None, ) -> typing.IO[typing.AnyStr]: '''Acquire the locked filehandle''' @@ -383,9 +383,9 @@ def __init__( def acquire( self, - timeout: typing.Optional[float] = None, - check_interval: typing.Optional[float] = None, - fail_when_locked: typing.Optional[bool] = None, + timeout: float | None = None, + check_interval: float | None = None, + fail_when_locked: bool | None = None, ) -> typing.IO[typing.AnyStr]: fh: typing.IO[typing.AnyStr] if self._acquire_count >= 1: @@ -449,7 +449,7 @@ class BoundedSemaphore(LockBase): 'bounded_semaphore.01.lock' """ - lock: typing.Optional[Lock] + lock: Lock | None def __init__( self, @@ -457,15 +457,15 @@ def __init__( name: str = 'bounded_semaphore', filename_pattern: str = '{name}.{number:02d}.lock', directory: str = tempfile.gettempdir(), - timeout: typing.Optional[float] = DEFAULT_TIMEOUT, - check_interval: typing.Optional[float] = DEFAULT_CHECK_INTERVAL, - fail_when_locked: typing.Optional[bool] = True, + timeout: float | None = DEFAULT_TIMEOUT, + check_interval: float | None = DEFAULT_CHECK_INTERVAL, + fail_when_locked: bool | None = True, ): self.maximum = maximum self.name = name self.filename_pattern = filename_pattern self.directory = directory - self.lock: typing.Optional[Lock] = None + self.lock: Lock | None = None super().__init__( timeout=timeout, check_interval=check_interval, @@ -496,10 +496,10 @@ def get_filename(self, number: int) -> pathlib.Path: def acquire( # type: ignore[override] self, - timeout: typing.Optional[float] = None, - check_interval: typing.Optional[float] = None, - fail_when_locked: typing.Optional[bool] = None, - ) -> typing.Optional[Lock]: + timeout: float | None = None, + check_interval: float | None = None, + fail_when_locked: bool | None = None, + ) -> Lock | None: assert not self.lock, 'Already locked' filenames = self.get_filenames() @@ -567,12 +567,12 @@ class NamedBoundedSemaphore(BoundedSemaphore): def __init__( self, maximum: int, - name: typing.Optional[str] = None, + name: str | None = None, filename_pattern: str = '{name}.{number:02d}.lock', directory: str = tempfile.gettempdir(), - timeout: typing.Optional[float] = DEFAULT_TIMEOUT, - check_interval: typing.Optional[float] = DEFAULT_CHECK_INTERVAL, - fail_when_locked: typing.Optional[bool] = True, + timeout: float | None = DEFAULT_TIMEOUT, + check_interval: float | None = DEFAULT_CHECK_INTERVAL, + fail_when_locked: bool | None = True, ): if name is None: name = 'bounded_semaphore.%d' % random.randint(0, 1000000) diff --git a/portalocker_tests/tests.py b/portalocker_tests/tests.py index ee0d91b..f6ddb36 100644 --- a/portalocker_tests/tests.py +++ b/portalocker_tests/tests.py @@ -287,7 +287,7 @@ def exclusive_lock(filename, **kwargs): @dataclasses.dataclass(order=True) class LockResult: - exception_class: typing.Union[typing.Type[Exception], None] = None + exception_class: typing.Union[type[Exception], None] = None exception_message: typing.Union[str, None] = None exception_repr: typing.Union[str, None] = None diff --git a/ruff.toml b/ruff.toml index 194a455..c62d40f 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,7 +1,7 @@ # We keep the ruff configuration separate so it can easily be shared across # all projects -target-version = 'py38' +target-version = 'py39' src = ['portalocker'] exclude = ['docs'] From fdfd9cf6376ac61ba883a6230cf11dcc0db7db43 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 18 Nov 2024 12:16:59 +0100 Subject: [PATCH 04/10] made ruff happy, hopefully no issues with these updates. --- ruff.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/ruff.toml b/ruff.toml index c62d40f..065da24 100644 --- a/ruff.toml +++ b/ruff.toml @@ -30,6 +30,7 @@ ignore = [ 'SIM114', # Combine `if` branches using logical `or` operator 'RET506', # Unnecessary `else` after `raise` statement 'FA100', # Missing `from __future__ import annotations`, but uses `typing.Optional` + 'RUF100', # Unused noqa directives. Due to multiple Python versions, we need to keep them ] select = [ From 446d7a2d63d5c1dc5c4dc74da01d0a936ad4377b Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 18 Nov 2024 12:45:56 +0100 Subject: [PATCH 05/10] made mypy happy-ish --- portalocker/portalocker.py | 4 +- portalocker/types.py | 130 +++++++++++-------------------------- portalocker_tests/tests.py | 33 ++++++---- ruff.toml | 7 +- 4 files changed, 67 insertions(+), 107 deletions(-) diff --git a/portalocker/portalocker.py b/portalocker/portalocker.py index 9bdfa52..e9184d6 100644 --- a/portalocker/portalocker.py +++ b/portalocker/portalocker.py @@ -102,7 +102,7 @@ def unlock(file_: typing.IO): # but any callable that matches the syntax will be accepted. LOCKER = fcntl.flock # pyright: ignore[reportConstantRedefinition] - def lock(file: int | types.IO, flags: LockFlags): + def lock(file: int | types.IO, flags: LockFlags): # type: ignore[misc] assert LOCKER is not None, 'We need a locking function in `LOCKER` ' # Locking with NON_BLOCKING without EXCLUSIVE or SHARED enabled # results in an error @@ -146,7 +146,7 @@ def lock(file: int | types.IO, flags: LockFlags): fh=file, ) from exc_value - def unlock(file: types.IO): + def unlock(file: types.IO): # type: ignore[misc] assert LOCKER is not None, 'We need a locking function in `LOCKER` ' LOCKER(file.fileno(), LockFlags.UNBLOCK) diff --git a/portalocker/types.py b/portalocker/types.py index 8abcbe7..81814ae 100644 --- a/portalocker/types.py +++ b/portalocker/types.py @@ -4,104 +4,50 @@ import typing from typing import Union +# fmt: off Mode = typing.Literal[ # Text modes - 'r', - 'rt', - 'tr', # Read text - 'w', - 'wt', - 'tw', # Write text - 'a', - 'at', - 'ta', # Append text - 'x', - 'xt', - 'tx', # Exclusive creation text - 'r+', - '+r', - 'rt+', - 'r+t', - '+rt', - 'tr+', - 't+r', - '+tr', # Read and write text - 'w+', - '+w', - 'wt+', - 'w+t', - '+wt', - 'tw+', - 't+w', - '+tw', # Write and read text - 'a+', - '+a', - 'at+', - 'a+t', - '+at', - 'ta+', - 't+a', - '+ta', # Append and read text - 'x+', - '+x', - 'xt+', - 'x+t', - '+xt', - 'tx+', - 't+x', - '+tx', # Exclusive creation and read text - 'U', - 'rU', - 'Ur', - 'rtU', - 'rUt', - 'Urt', - 'trU', - 'tUr', - 'Utr', # Universal newline support + # Read text + 'r', 'rt', 'tr', + # Write text + 'w', 'wt', 'tw', + # Append text + 'a', 'at', 'ta', + # Exclusive creation text + 'x', 'xt', 'tx', + # Read and write text + 'r+', '+r', 'rt+', 'r+t', '+rt', 'tr+', 't+r', '+tr', + # Write and read text + 'w+', '+w', 'wt+', 'w+t', '+wt', 'tw+', 't+w', '+tw', + # Append and read text + 'a+', '+a', 'at+', 'a+t', '+at', 'ta+', 't+a', '+ta', + # Exclusive creation and read text + 'x+', '+x', 'xt+', 'x+t', '+xt', 'tx+', 't+x', '+tx', + # Universal newline support + 'U', 'rU', 'Ur', 'rtU', 'rUt', 'Urt', 'trU', 'tUr', 'Utr', + # Binary modes - 'rb', - 'br', # Read binary - 'wb', - 'bw', # Write binary - 'ab', - 'ba', # Append binary - 'xb', - 'bx', # Exclusive creation binary - 'rb+', - 'r+b', - '+rb', - 'br+', - 'b+r', - '+br', # Read and write binary - 'wb+', - 'w+b', - '+wb', - 'bw+', - 'b+w', - '+bw', # Write and read binary - 'ab+', - 'a+b', - '+ab', - 'ba+', - 'b+a', - '+ba', # Append and read binary - 'xb+', - 'x+b', - '+xb', - 'bx+', - 'b+x', - '+bx', # Exclusive creation and read binary - 'rbU', - 'rUb', - 'Urb', - 'brU', - 'bUr', - 'Ubr', + # Read binary + 'rb', 'br', + # Write binary + 'wb', 'bw', + # Append binary + 'ab', 'ba', + # Exclusive creation binary + 'xb', 'bx', + # Read and write binary + 'rb+', 'r+b', '+rb', 'br+', 'b+r', '+br', + # Write and read binary + 'wb+', 'w+b', '+wb', 'bw+', 'b+w', '+bw', + # Append and read binary + 'ab+', 'a+b', '+ab', 'ba+', 'b+a', '+ba', + # Exclusive creation and read binary + 'xb+', 'x+b', '+xb', 'bx+', 'b+x', '+bx', # Universal newline support in binary mode + 'rbU', 'rUb', 'Urb', 'brU', 'bUr', 'Ubr', ] Filename = Union[str, pathlib.Path] -IO: typing.TypeAlias = Union[typing.IO[str], typing.IO[bytes]] +IO: typing.TypeAlias = Union[typing.IO[str], typing.IO[bytes]] # type: ignore[name-defined] class FileOpenKwargs(typing.TypedDict): diff --git a/portalocker_tests/tests.py b/portalocker_tests/tests.py index f6ddb36..a875d4f 100644 --- a/portalocker_tests/tests.py +++ b/portalocker_tests/tests.py @@ -186,18 +186,24 @@ def test_exlusive(tmpfile): portalocker.lock(fh, portalocker.LOCK_EX | portalocker.LOCK_NB) # Make sure we can't read the locked file - with pytest.raises(portalocker.LockException), open( - tmpfile, - 'r+', - ) as fh2: + with ( + pytest.raises(portalocker.LockException), + open( + tmpfile, + 'r+', + ) as fh2, + ): portalocker.lock(fh2, portalocker.LOCK_EX | portalocker.LOCK_NB) assert fh2.read() == text_0 # Make sure we can't write the locked file - with pytest.raises(portalocker.LockException), open( - tmpfile, - 'w+', - ) as fh2: + with ( + pytest.raises(portalocker.LockException), + open( + tmpfile, + 'w+', + ) as fh2, + ): portalocker.lock(fh2, portalocker.LOCK_EX | portalocker.LOCK_NB) fh2.write('surprise and fear') @@ -218,10 +224,13 @@ def test_shared(tmpfile): assert fh2.read() == 'spam and eggs' # Make sure we can't write the locked file - with pytest.raises(portalocker.LockException), open( - tmpfile, - 'w+', - ) as fh2: + with ( + pytest.raises(portalocker.LockException), + open( + tmpfile, + 'w+', + ) as fh2, + ): portalocker.lock(fh2, portalocker.LOCK_EX | portalocker.LOCK_NB) fh2.write('surprise and fear') diff --git a/ruff.toml b/ruff.toml index 065da24..0853930 100644 --- a/ruff.toml +++ b/ruff.toml @@ -4,7 +4,12 @@ target-version = 'py39' src = ['portalocker'] -exclude = ['docs'] +exclude = [ + 'docs', + # Ignore local test files/directories/old-stuff + 'test.py', + '*_old.py', +] line-length = 80 From 7a8f74694fd066734d8ed269a5da216eef58d2c5 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 18 Nov 2024 12:53:49 +0100 Subject: [PATCH 06/10] updated github actions --- .github/workflows/lint.yml | 2 +- .github/workflows/python-package.yml | 4 ++-- .github/workflows/stale.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4c92829..108877c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.8', '3.8', '3.10', '3.11'] + python-version: ['3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 71209cc..f0acbb8 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.9', '3.10', '3.11', '3.12'] os: ['macos-latest', 'windows-latest'] steps: @@ -41,7 +41,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 7101b3f..b0efa53 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -12,6 +12,6 @@ jobs: - uses: actions/stale@v8 with: days-before-stale: 30 + days-before-pr-stale: -1 exempt-issue-labels: in-progress,help-wanted,pinned,security,enhancement exempt-all-pr-assignees: true - From 5f9061f1a8d4969839cee12c2a423d1f74f9862d Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 18 Nov 2024 12:59:30 +0100 Subject: [PATCH 07/10] fixed sphinx config to new format --- docs/conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 10570a6..3dd0173 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -350,4 +350,6 @@ # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/3/': None} +intersphinx_mapping = dict( + python=('http://docs.python.org/3/', None), +) From eb885f24c64fcd5ad336db176ab0c6d4273fae87 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 18 Nov 2024 13:05:24 +0100 Subject: [PATCH 08/10] made flake8 happy too --- portalocker/types.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/portalocker/types.py b/portalocker/types.py index 81814ae..c0ee5cc 100644 --- a/portalocker/types.py +++ b/portalocker/types.py @@ -47,7 +47,10 @@ 'rbU', 'rUb', 'Urb', 'brU', 'bUr', 'Ubr', ] Filename = Union[str, pathlib.Path] -IO: typing.TypeAlias = Union[typing.IO[str], typing.IO[bytes]] # type: ignore[name-defined] +IO: typing.TypeAlias = Union[ # type: ignore[name-defined] + typing.IO[str], + typing.IO[bytes], +] class FileOpenKwargs(typing.TypedDict): From 9d705ee88eb0b6cb975a4380a6ab34f32373a84e Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 18 Nov 2024 13:11:55 +0100 Subject: [PATCH 09/10] Dropped support for deprecated Python versions, only 3.9 and up are supported now. --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 08ea8ab..b537120 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,11 +37,11 @@ classifiers = [ 'Operating System :: Unix', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: IronPython', 'Programming Language :: Python :: Implementation :: PyPy', From 928b29d789bab337b4594e118e2d36f1079c79d0 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 18 Nov 2024 13:12:06 +0100 Subject: [PATCH 10/10] Incrementing version to v3.0.0 --- portalocker/__about__.py | 2 +- portalocker/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/portalocker/__about__.py b/portalocker/__about__.py index a0b817a..c4a0806 100644 --- a/portalocker/__about__.py +++ b/portalocker/__about__.py @@ -1,6 +1,6 @@ __package_name__ = 'portalocker' __author__ = 'Rick van Hattem' __email__ = 'wolph@wol.ph' -__version__ = '2.10.1' +__version__ = '3.0.0' __description__ = '''Wraps the portalocker recipe for easy usage''' __url__ = 'https://github.com/WoLpH/portalocker' diff --git a/portalocker/__init__.py b/portalocker/__init__.py index 7e757ef..ce03e83 100644 --- a/portalocker/__init__.py +++ b/portalocker/__init__.py @@ -20,7 +20,7 @@ #: Current author's email address __email__ = __about__.__email__ #: Version number -__version__ = '2.10.1' +__version__ = '3.0.0' #: Package description for Pypi __description__ = __about__.__description__ #: Package homepage