Skip to content

Commit

Permalink
Merge branch 'release/3.0.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
wolph committed Nov 18, 2024
2 parents 4d5905f + 928b29d commit 582adc2
Show file tree
Hide file tree
Showing 17 changed files with 287 additions and 157 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/stale.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

4 changes: 3 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)
2 changes: 1 addition & 1 deletion portalocker/__about__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
__package_name__ = 'portalocker'
__author__ = 'Rick van Hattem'
__email__ = '[email protected]'
__version__ = '2.10.1'
__version__ = '3.0.0'
__description__ = '''Wraps the portalocker recipe for easy usage'''
__url__ = 'https://github.com/WoLpH/portalocker'
2 changes: 1 addition & 1 deletion portalocker/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 18 additions & 7 deletions portalocker/__main__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import argparse
import logging
import os
Expand Down Expand Up @@ -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)
Expand All @@ -46,15 +48,21 @@ 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: 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
for line in path.open():
if '__future__' in line:
continue

if paren:
if ')' in line:
line = line.split(')', 1)[1]
Expand Down Expand Up @@ -82,28 +90,31 @@ 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)

# 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()),
)
output_file.write(
_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)

Expand Down
4 changes: 3 additions & 1 deletion portalocker/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import typing

from portalocker import types


class BaseLockException(Exception): # noqa: N818
# Error codes:
Expand All @@ -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
Expand Down
26 changes: 13 additions & 13 deletions portalocker/portalocker.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,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
Expand All @@ -26,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_)
Expand Down Expand Up @@ -100,9 +100,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): # 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
Expand All @@ -115,7 +115,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),
Expand All @@ -130,25 +130,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): # type: ignore[misc]
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')
60 changes: 37 additions & 23 deletions portalocker/redis.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
# pyright: reportUnknownMemberType=false
from __future__ import annotations

import _thread
import json
import logging
import random
import time
import typing

from redis import client
import redis

from . import exceptions, utils

Expand All @@ -15,8 +18,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
Expand Down Expand Up @@ -61,28 +64,29 @@ 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[client.Redis]
pubsub: typing.Optional[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,
)

def __init__(
self,
channel: str,
connection: typing.Optional[client.Redis] = 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] = 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
Expand All @@ -103,18 +107,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: 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
Expand All @@ -128,10 +136,10 @@ 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,
) -> 'RedisLock':
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(
check_interval,
Expand Down Expand Up @@ -189,7 +197,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()}'

Expand Down Expand Up @@ -217,7 +229,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):
Expand Down
Loading

0 comments on commit 582adc2

Please sign in to comment.