Skip to content

Commit

Permalink
Drop support for python3.6
Browse files Browse the repository at this point in the history
And start testing with 3.11.

Signed-off-by: Bernát Gábor <gaborjbernat@gmail.com>
  • Loading branch information
gaborbernat committed Dec 26, 2021
1 parent 6823192 commit 530984d
Show file tree
Hide file tree
Showing 14 changed files with 67 additions and 45 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/check.yml
Expand Up @@ -24,11 +24,11 @@ jobs:
fail-fast: false
matrix:
py:
- "3.11-dev"
- "3.10"
- "3.9"
- "3.8"
- "3.7"
- "3.6"
- "pypy-3.7-v7.3.7"
os:
- ubuntu-20.04
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Expand Up @@ -43,7 +43,7 @@ repos:
rev: v1.20.0
hooks:
- id: setup-cfg-fmt
args: [ --min-py3-version, "3.6", "--max-py-version", "3.10" ]
args: [ --min-py3-version, "3.7", "--max-py-version", "3.11" ]
- repo: https://github.com/PyCQA/flake8
rev: 4.0.1
hooks:
Expand Down
6 changes: 5 additions & 1 deletion docs/changelog.rst
@@ -1,7 +1,11 @@
Changelog
=========

v3.4.1 (2021-13-16)
v3.4.2 (2021-12-16)
-------------------
- Drop support for python ``3.6``

v3.4.1 (2021-12-16)
-------------------
- Add ``stacklevel`` to deprecation warnings for argument name change

Expand Down
4 changes: 2 additions & 2 deletions setup.cfg
Expand Up @@ -16,11 +16,11 @@ classifiers =
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Topic :: Internet
Topic :: Software Development :: Libraries
Topic :: System
Expand All @@ -31,7 +31,7 @@ project_urls =

[options]
packages = find:
python_requires = >=3.6
python_requires = >=3.7
package_dir =
=src
zip_safe = True
Expand Down
2 changes: 2 additions & 0 deletions setup.py
@@ -1,3 +1,5 @@
from __future__ import annotations

from setuptools import setup

setup()
9 changes: 5 additions & 4 deletions src/filelock/__init__.py
Expand Up @@ -5,9 +5,10 @@
:no-value:
"""
from __future__ import annotations

import sys
import warnings
from typing import Type

from ._api import AcquireReturnProxy, BaseFileLock
from ._error import Timeout
Expand All @@ -21,18 +22,18 @@


if sys.platform == "win32": # pragma: win32 cover
_FileLock: Type[BaseFileLock] = WindowsFileLock
_FileLock: type[BaseFileLock] = WindowsFileLock
else: # pragma: win32 no cover
if has_fcntl:
_FileLock: Type[BaseFileLock] = UnixFileLock
_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: Type[BaseFileLock] = _FileLock
FileLock: type[BaseFileLock] = _FileLock


__all__ = [
Expand Down
32 changes: 17 additions & 15 deletions src/filelock/_api.py
@@ -1,11 +1,13 @@
from __future__ import annotations

import logging
import os
import time
import warnings
from abc import ABC, abstractmethod
from threading import Lock
from types import TracebackType
from typing import Any, Optional, Type, Union
from typing import Any

from ._error import Timeout

Expand All @@ -18,25 +20,25 @@
class AcquireReturnProxy:
"""A context aware object that will release the lock file when exiting."""

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

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

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


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

def __init__(self, lock_file: Union[str, "os.PathLike[Any]"], timeout: float = -1) -> None:
def __init__(self, lock_file: str | os.PathLike[Any], timeout: float = -1) -> None:
"""
Create a new lock object.
Expand All @@ -50,7 +52,7 @@ def __init__(self, lock_file: Union[str, "os.PathLike[Any]"], timeout: float = -

# 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: Optional[int] = None
self._lock_file_fd: int | None = None

# The default timeout value.
self.timeout: float = timeout
Expand All @@ -77,7 +79,7 @@ def timeout(self) -> float:
return self._timeout

@timeout.setter
def timeout(self, value: Union[float, str]) -> None:
def timeout(self, value: float | str) -> None:
"""
Change the default timeout value.
Expand Down Expand Up @@ -109,10 +111,10 @@ def is_locked(self) -> bool:

def acquire(
self,
timeout: Optional[float] = None,
timeout: float | None = None,
poll_interval: float = 0.05,
*,
poll_intervall: Optional[float] = None,
poll_intervall: float | None = None,
) -> AcquireReturnProxy:
"""
Try to acquire the file lock.
Expand Down Expand Up @@ -202,7 +204,7 @@ def release(self, force: bool = False) -> None:
self._lock_counter = 0
_LOGGER.debug("Lock %s released on %s", lock_id, lock_filename)

def __enter__(self) -> "BaseFileLock":
def __enter__(self) -> BaseFileLock:
"""
Acquire the lock.
Expand All @@ -213,9 +215,9 @@ def __enter__(self) -> "BaseFileLock":

def __exit__(
self,
exc_type: Optional[Type[BaseException]], # noqa: U100
exc_value: Optional[BaseException], # noqa: U100
traceback: Optional[TracebackType], # noqa: U100
exc_type: type[BaseException] | None, # noqa: U100
exc_value: BaseException | None, # noqa: U100
traceback: TracebackType | None, # noqa: U100
) -> None:
"""
Release the lock.
Expand Down
3 changes: 3 additions & 0 deletions src/filelock/_error.py
@@ -1,3 +1,6 @@
from __future__ import annotations


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

Expand Down
2 changes: 2 additions & 0 deletions src/filelock/_soft.py
@@ -1,3 +1,5 @@
from __future__ import annotations

import os
import sys
from errno import EACCES, EEXIST, ENOENT
Expand Down
2 changes: 2 additions & 0 deletions src/filelock/_unix.py
@@ -1,3 +1,5 @@
from __future__ import annotations

import os
import sys
from abc import ABC
Expand Down
2 changes: 2 additions & 0 deletions src/filelock/_util.py
@@ -1,3 +1,5 @@
from __future__ import annotations

import os
import stat

Expand Down
2 changes: 2 additions & 0 deletions src/filelock/_windows.py
@@ -1,3 +1,5 @@
from __future__ import annotations

import os
import sys
from abc import ABC
Expand Down
38 changes: 20 additions & 18 deletions tests/test_filelock.py
@@ -1,3 +1,5 @@
from __future__ import annotations

import logging
import sys
import threading
Expand All @@ -6,7 +8,7 @@
from pathlib import Path, PurePath
from stat import S_IWGRP, S_IWOTH, S_IWUSR
from types import TracebackType
from typing import Callable, Iterator, Optional, Tuple, Type, Union
from typing import Callable, Iterator, Tuple, Type, Union

import pytest
from _pytest.logging import LogCaptureFixture
Expand All @@ -26,7 +28,7 @@
],
)
def test_simple(
lock_type: Type[BaseFileLock], path_type: Union[Type[str], Type[Path]], tmp_path: Path, caplog: LogCaptureFixture
lock_type: type[BaseFileLock], path_type: type[str] | type[Path], tmp_path: Path, caplog: LogCaptureFixture
) -> None:
caplog.set_level(logging.DEBUG)

Expand Down Expand Up @@ -65,7 +67,7 @@ def tmp_path_ro(tmp_path: Path) -> Iterator[Path]:

@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
@pytest.mark.skipif(sys.platform == "win32", reason="Windows does not have read only folders")
def test_ro_folder(lock_type: Type[BaseFileLock], tmp_path_ro: Path) -> None:
def test_ro_folder(lock_type: type[BaseFileLock], tmp_path_ro: Path) -> None:
lock = lock_type(str(tmp_path_ro / "a"))
with pytest.raises(PermissionError, match="Permission denied"):
lock.acquire()
Expand All @@ -80,14 +82,14 @@ def tmp_file_ro(tmp_path: Path) -> Iterator[Path]:


@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
def test_ro_file(lock_type: Type[BaseFileLock], tmp_file_ro: Path) -> None:
def test_ro_file(lock_type: type[BaseFileLock], tmp_file_ro: Path) -> None:
lock = lock_type(str(tmp_file_ro))
with pytest.raises(PermissionError, match="Permission denied"):
lock.acquire()


@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
def test_missing_directory(lock_type: Type[BaseFileLock], tmp_path_ro: Path) -> None:
def test_missing_directory(lock_type: type[BaseFileLock], tmp_path_ro: Path) -> None:
lock_path = tmp_path_ro / "a" / "b"
lock = lock_type(str(lock_path))

Expand All @@ -96,7 +98,7 @@ def test_missing_directory(lock_type: Type[BaseFileLock], tmp_path_ro: Path) ->


@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
def test_nested_context_manager(lock_type: Type[BaseFileLock], tmp_path: Path) -> None:
def test_nested_context_manager(lock_type: type[BaseFileLock], tmp_path: Path) -> None:
# lock is not released before the most outer with statement that locked the lock, is left
lock_path = tmp_path / "a"
lock = lock_type(str(lock_path))
Expand All @@ -119,7 +121,7 @@ def test_nested_context_manager(lock_type: Type[BaseFileLock], tmp_path: Path) -


@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
def test_nested_acquire(lock_type: Type[BaseFileLock], tmp_path: Path) -> None:
def test_nested_acquire(lock_type: type[BaseFileLock], tmp_path: Path) -> None:
# lock is not released before the most outer with statement that locked the lock, is left
lock_path = tmp_path / "a"
lock = lock_type(str(lock_path))
Expand All @@ -142,7 +144,7 @@ def test_nested_acquire(lock_type: Type[BaseFileLock], tmp_path: Path) -> None:


@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
def test_nested_forced_release(lock_type: Type[BaseFileLock], tmp_path: Path) -> None:
def test_nested_forced_release(lock_type: type[BaseFileLock], tmp_path: Path) -> None:
# acquires the lock using a with-statement and releases the lock before leaving the with-statement
lock_path = tmp_path / "a"
lock = lock_type(str(lock_path))
Expand All @@ -164,23 +166,23 @@ def test_nested_forced_release(lock_type: Type[BaseFileLock], tmp_path: Path) ->
class ExThread(threading.Thread):
def __init__(self, target: Callable[[], None], name: str) -> None:
super().__init__(target=target, name=name)
self.ex: Optional[_ExcInfoType] = None
self.ex: _ExcInfoType | None = None

def run(self) -> None:
try:
super().run()
except Exception: # pragma: no cover
self.ex = sys.exc_info() # pragma: no cover

def join(self, timeout: Optional[float] = None) -> None:
def join(self, timeout: float | None = None) -> None:
super().join(timeout=timeout)
if self.ex is not None:
print(f"fail from thread {self.name}") # pragma: no cover
raise RuntimeError from self.ex[1] # pragma: no cover


@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
def test_threaded_shared_lock_obj(lock_type: Type[BaseFileLock], tmp_path: Path) -> None:
def test_threaded_shared_lock_obj(lock_type: type[BaseFileLock], tmp_path: Path) -> None:
# Runs 100 threads, which need the filelock. The lock must be acquired if at least one thread required it and
# released, as soon as all threads stopped.
lock_path = tmp_path / "a"
Expand All @@ -202,7 +204,7 @@ def thread_work() -> None:

@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
@pytest.mark.skipif(hasattr(sys, "pypy_version_info") and sys.platform == "win32", reason="deadlocks randomly")
def test_threaded_lock_different_lock_obj(lock_type: Type[BaseFileLock], tmp_path: Path) -> None:
def test_threaded_lock_different_lock_obj(lock_type: type[BaseFileLock], tmp_path: Path) -> None:
# Runs multiple threads, which acquire the same lock file with a different FileLock object. When thread group 1
# acquired the lock, thread group 2 must not hold their lock.

Expand Down Expand Up @@ -234,7 +236,7 @@ def t_2() -> None:


@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
def test_timeout(lock_type: Type[BaseFileLock], tmp_path: Path) -> None:
def test_timeout(lock_type: type[BaseFileLock], tmp_path: Path) -> None:
# raises Timeout error when the lock cannot be acquired
lock_path = tmp_path / "a"
lock_1, lock_2 = lock_type(str(lock_path)), lock_type(str(lock_path))
Expand All @@ -257,7 +259,7 @@ def test_timeout(lock_type: Type[BaseFileLock], tmp_path: Path) -> None:


@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
def test_default_timeout(lock_type: Type[BaseFileLock], tmp_path: Path) -> None:
def test_default_timeout(lock_type: type[BaseFileLock], tmp_path: Path) -> None:
# test if the default timeout parameter works
lock_path = tmp_path / "a"
lock_1, lock_2 = lock_type(str(lock_path)), lock_type(str(lock_path), timeout=0.1)
Expand Down Expand Up @@ -289,7 +291,7 @@ def test_default_timeout(lock_type: Type[BaseFileLock], tmp_path: Path) -> None:


@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
def test_context_release_on_exc(lock_type: Type[BaseFileLock], tmp_path: Path) -> None:
def test_context_release_on_exc(lock_type: type[BaseFileLock], tmp_path: Path) -> None:
# lock is released when an exception is thrown in a with-statement
lock_path = tmp_path / "a"
lock = lock_type(str(lock_path))
Expand All @@ -304,7 +306,7 @@ def test_context_release_on_exc(lock_type: Type[BaseFileLock], tmp_path: Path) -


@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
def test_acquire_release_on_exc(lock_type: Type[BaseFileLock], tmp_path: Path) -> None:
def test_acquire_release_on_exc(lock_type: type[BaseFileLock], tmp_path: Path) -> None:
# lock is released when an exception is thrown in a acquire statement
lock_path = tmp_path / "a"
lock = lock_type(str(lock_path))
Expand All @@ -320,7 +322,7 @@ def test_acquire_release_on_exc(lock_type: Type[BaseFileLock], tmp_path: Path) -

@pytest.mark.skipif(hasattr(sys, "pypy_version_info"), reason="del() does not trigger GC in PyPy")
@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
def test_del(lock_type: Type[BaseFileLock], tmp_path: Path) -> None:
def test_del(lock_type: type[BaseFileLock], tmp_path: Path) -> None:
# lock is released when the object is deleted
lock_path = tmp_path / "a"
lock_1, lock_2 = lock_type(str(lock_path)), lock_type(str(lock_path))
Expand Down Expand Up @@ -354,7 +356,7 @@ def test_cleanup_soft_lock(tmp_path: Path) -> None:


@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock])
def test_poll_intervall_deprecated(lock_type: Type[BaseFileLock], tmp_path: Path) -> None:
def test_poll_intervall_deprecated(lock_type: type[BaseFileLock], tmp_path: Path) -> None:
lock_path = tmp_path / "a"
lock = lock_type(str(lock_path))

Expand Down

0 comments on commit 530984d

Please sign in to comment.