diff --git a/.markdownlint.yaml b/.markdownlint.yaml deleted file mode 100644 index f30749b7..00000000 --- a/.markdownlint.yaml +++ /dev/null @@ -1,8 +0,0 @@ -MD013: - code_blocks: false - headers: false - line_length: 120 - tables: false - -MD046: - style: fenced diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 22d7030c..a8319fac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,77 +2,31 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - - id: check-ast - - id: check-builtin-literals - - id: check-docstring-first - - id: check-merge-conflict - - id: check-yaml - - id: check-toml - - id: debug-statements - id: end-of-file-fixer - id: trailing-whitespace - - repo: https://github.com/asottile/add-trailing-comma - rev: v2.4.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.0.272" hooks: - - id: add-trailing-comma - args: [--py36-plus] - - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 - hooks: - - id: pyupgrade - args: ["--py37-plus"] - - repo: https://github.com/PyCQA/isort - rev: 5.12.0 - hooks: - - id: isort + - id: ruff + args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/psf/black rev: 23.3.0 hooks: - id: black - args: [--safe] - - repo: https://github.com/asottile/blacken-docs - rev: 1.13.0 - hooks: - - id: blacken-docs - additional_dependencies: [black==23.3] - - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.10.0 - hooks: - - id: rst-backticks - repo: https://github.com/tox-dev/tox-ini-fmt rev: "1.3.0" hooks: - id: tox-ini-fmt args: ["-p", "fix"] - repo: https://github.com/tox-dev/pyproject-fmt - rev: "0.9.2" + rev: "0.11.2" hooks: - id: pyproject-fmt - - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 - hooks: - - id: flake8 - additional_dependencies: - - flake8-bugbear==23.3.23 - - flake8-comprehensions==3.12 - - flake8-pytest-style==1.7.2 - - flake8-spellcheck==0.28 - - flake8-unused-arguments==0.0.13 - - flake8-noqa==1.3.1 - - pep8-naming==0.13.3 - - flake8-pyproject==1.2.3 - repo: https://github.com/pre-commit/mirrors-prettier - rev: "v2.7.1" + rev: "v3.0.0-alpha.9-for-vscode" hooks: - id: prettier - additional_dependencies: - - prettier@2.7.1 - - "@prettier/plugin-xml@2.2" args: ["--print-width=120", "--prose-wrap=always"] - - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.33.0 - hooks: - - id: markdownlint - repo: meta hooks: - id: check-hooks-apply diff --git a/docs/conf.py b/docs/conf.py index 449ac3d9..97ec078f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,19 +1,24 @@ +# noqa: INP001 +"""Configuration for Sphinx.""" from __future__ import annotations -from datetime import date, datetime +from datetime import datetime, timezone +from typing import TYPE_CHECKING from docutils.nodes import Element, Text -from sphinx.addnodes import pending_xref -from sphinx.application import Sphinx -from sphinx.builders import Builder +from filelock import __version__ from sphinx.domains.python import PythonDomain -from sphinx.environment import BuildEnvironment -from filelock import __version__ +if TYPE_CHECKING: + from sphinx.addnodes import pending_xref + from sphinx.application import Sphinx + from sphinx.builders import Builder + from sphinx.environment import BuildEnvironment name, company = "filelock", "tox-dev" +now = datetime.now(tz=timezone.utc) version, release = ".".join(__version__.split(".")[:2]), __version__ -copyright = f"2014-{date.today().year}, {company}" +copyright = f"2014-{now.date().year}, {company}" # noqa: A001 extensions = [ "sphinx.ext.autodoc", "sphinx.ext.autosectionlabel", @@ -23,7 +28,7 @@ "sphinx_autodoc_typehints", ] html_theme = "furo" -html_title, html_last_updated_fmt = name, datetime.now().isoformat() +html_title, html_last_updated_fmt = name, now.isoformat() pygments_style, pygments_dark_style = "sphinx", "monokai" autoclass_content, autodoc_member_order, autodoc_typehints = "class", "bysource", "none" autodoc_default_options = {"member-order": "bysource", "undoc-members": True, "show-inheritance": True} @@ -40,13 +45,19 @@ def setup(app: Sphinx) -> None: + """ + Setup app. + + :param app: the app + """ + class PatchedPythonDomain(PythonDomain): - def resolve_xref( + def resolve_xref( # noqa: PLR0913 self, env: BuildEnvironment, fromdocname: str, builder: Builder, - type: str, + type: str, # noqa: A002 target: str, node: pending_xref, contnode: Element, diff --git a/pyproject.toml b/pyproject.toml index ccb2ca13..701118a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ build-backend = "hatchling.build" requires = [ "hatch-vcs>=0.3", - "hatchling>=1.14", + "hatchling>=1.17", ] [project] @@ -25,8 +25,12 @@ classifiers = [ "License :: OSI Approved :: The Unlicense (Unlicense)", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", + "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", @@ -35,16 +39,16 @@ dynamic = [ "version", ] optional-dependencies.docs = [ - "furo>=2023.3.27", - "sphinx>=6.1.3", + "furo>=2023.5.20", + "sphinx>=7.0.1", "sphinx-autodoc-typehints!=1.23.4,>=1.23", ] optional-dependencies.testing = [ "covdefaults>=2.3", - "coverage>=7.2.3", + "coverage>=7.2.7", "diff-cover>=7.5", "pytest>=7.3.1", - "pytest-cov>=4", + "pytest-cov>=4.1", "pytest-mock>=3.10", "pytest-timeout>=2.1", ] @@ -61,18 +65,6 @@ version.source = "vcs" [tool.black] line-length = 120 -[tool.isort] -profile = "black" -known_first_party = ["filelock"] -add_imports = ["from __future__ import annotations"] - -[tool.flake8] -max-complexity = 22 -max-line-length = 120 -unused-arguments-ignore-abstract-functions = true -noqa-require-code = true -dictionaries = ["en_US", "python", "technical", "django"] - [tool.coverage] html.show_contexts = true html.skip_covered = false @@ -88,5 +80,26 @@ show_error_codes = true strict = true overrides = [{ module = ["appdirs.*", "jnius.*"], ignore_missing_imports = true }] -[tool.pep8] -max-line-length = "120" +[tool.ruff] +select = ["ALL"] +line-length = 120 +target-version = "py37" +isort = {known-first-party = ["platformdirs", "tests"], required-imports = ["from __future__ import annotations"]} +ignore = [ + "ANN101", # Missing type annotation for `self` in method + "D301", # Use `r"""` if any backslashes in a docstring + "D205", # 1 blank line required between summary line and description + "D401", # First line of docstring should be in imperative mood + "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible + "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible + "S104", # Possible binding to all interface +] +[tool.ruff.per-file-ignores] +"tests/**/*.py" = [ + "S101", # asserts allowed in tests... + "FBT", # don"t care about booleans as positional arguments in tests + "INP001", # no implicit namespace + "D", # don"t care about documentation in tests + "S603", # `subprocess` call: check for execution of untrusted input + "PLR2004", # Magic value used in comparison, consider replacing with a constant variable +] diff --git a/src/filelock/__init__.py b/src/filelock/__init__.py index c7492ba5..2b36c340 100644 --- a/src/filelock/__init__.py +++ b/src/filelock/__init__.py @@ -25,18 +25,16 @@ if sys.platform == "win32": # pragma: win32 cover _FileLock: type[BaseFileLock] = WindowsFileLock else: # pragma: win32 no cover - if has_fcntl: + if has_fcntl: # noqa: PLR5501 _FileLock: type[BaseFileLock] = UnixFileLock else: _FileLock = SoftFileLock if warnings is not None: warnings.warn("only soft file lock is available", stacklevel=2) -if TYPE_CHECKING: - FileLock = SoftFileLock -else: - #: Alias for the lock, which should be used for the current platform. - FileLock = _FileLock + +#: Alias for the lock, which should be used for the current platform. +FileLock: type[BaseFileLock] = SoftFileLock if TYPE_CHECKING else _FileLock # type: ignore[assignment] __all__ = [ diff --git a/src/filelock/_api.py b/src/filelock/_api.py index 66710cc5..7754f084 100644 --- a/src/filelock/_api.py +++ b/src/filelock/_api.py @@ -8,11 +8,13 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from threading import local -from types import TracebackType -from typing import Any +from typing import TYPE_CHECKING, Any from ._error import Timeout +if TYPE_CHECKING: + from types import TracebackType + _LOGGER = logging.getLogger("filelock") @@ -30,18 +32,16 @@ def __enter__(self) -> BaseFileLock: def __exit__( self, - exc_type: type[BaseException] | None, # noqa: U100 - exc_value: BaseException | None, # noqa: U100 - traceback: TracebackType | None, # noqa: U100 + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, ) -> None: self.lock.release() @dataclass class FileLockContext: - """ - A dataclass which holds the context for a ``BaseFileLock`` object. - """ + """A dataclass which holds the context for a ``BaseFileLock`` object.""" # The context is held in a separate class to allow optional use of thread local storage via the # ThreadLocalFileContext class. @@ -63,9 +63,7 @@ class FileLockContext: class ThreadLocalFileContext(FileLockContext, local): - """ - A thread local version of the ``FileLockContext`` class. - """ + """A thread local version of the ``FileLockContext`` class.""" class BaseFileLock(ABC, contextlib.ContextDecorator): @@ -76,7 +74,7 @@ def __init__( lock_file: str | os.PathLike[Any], timeout: float = -1, mode: int = 0o644, - thread_local: bool = True, + thread_local: bool = True, # noqa: FBT001, FBT002 ) -> None: """ Create a new lock object. @@ -151,9 +149,7 @@ def is_locked(self) -> bool: @property def lock_counter(self) -> int: - """ - :return: The number of times this lock has been acquired (but not yet released). - """ + """:return: The number of times this lock has been acquired (but not yet released).""" return self._context.lock_counter def acquire( @@ -218,22 +214,21 @@ def acquire( if self.is_locked: _LOGGER.debug("Lock %s acquired on %s", lock_id, lock_filename) break - elif blocking is False: + if blocking is False: _LOGGER.debug("Failed to immediately acquire lock %s on %s", lock_id, lock_filename) - raise Timeout(lock_filename) - elif 0 <= timeout < time.perf_counter() - start_time: + raise Timeout(lock_filename) # noqa: TRY301 + if 0 <= timeout < time.perf_counter() - start_time: _LOGGER.debug("Timeout on acquiring lock %s on %s", lock_id, lock_filename) - raise Timeout(lock_filename) - else: - msg = "Lock %s not acquired on %s, waiting %s seconds ..." - _LOGGER.debug(msg, lock_id, lock_filename, poll_interval) - time.sleep(poll_interval) + raise Timeout(lock_filename) # noqa: TRY301 + msg = "Lock %s not acquired on %s, waiting %s seconds ..." + _LOGGER.debug(msg, lock_id, lock_filename, poll_interval) + time.sleep(poll_interval) except BaseException: # Something did go wrong, so decrement the counter. self._context.lock_counter = max(0, self._context.lock_counter - 1) raise return AcquireReturnProxy(lock=self) - def release(self, force: bool = False) -> None: + def release(self, force: bool = False) -> None: # noqa: FBT001, FBT002 """ 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. @@ -262,9 +257,9 @@ def __enter__(self) -> BaseFileLock: def __exit__( self, - exc_type: type[BaseException] | None, # noqa: U100 - exc_value: BaseException | None, # noqa: U100 - traceback: TracebackType | None, # noqa: U100 + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, ) -> None: """ Release the lock. diff --git a/src/filelock/_error.py b/src/filelock/_error.py index e2bd6530..f7ff08c0 100644 --- a/src/filelock/_error.py +++ b/src/filelock/_error.py @@ -3,7 +3,7 @@ from typing import Any -class Timeout(TimeoutError): +class Timeout(TimeoutError): # noqa: N818 """Raised when the lock could not be acquired in *timeout* seconds.""" def __init__(self, lock_file: str) -> None: diff --git a/src/filelock/_soft.py b/src/filelock/_soft.py index 57a6c04d..b99912bc 100644 --- a/src/filelock/_soft.py +++ b/src/filelock/_soft.py @@ -2,7 +2,9 @@ import os import sys +from contextlib import suppress from errno import EACCES, EEXIST +from pathlib import Path from ._api import BaseFileLock from ._util import raise_on_not_writable_file @@ -32,12 +34,11 @@ def _acquire(self) -> None: self._context.lock_file_fd = file_handler def _release(self) -> None: - os.close(self._context.lock_file_fd) # type: ignore # the lock file is definitely not None + assert self._context.lock_file_fd is not None # noqa: S101 + os.close(self._context.lock_file_fd) # the lock file is definitely not None self._context.lock_file_fd = None - try: - os.remove(self.lock_file) - except OSError: # the file is already deleted and that's what we want - pass + with suppress(OSError): # the file is already deleted and that's what we want + Path(self.lock_file).unlink() __all__ = [ diff --git a/src/filelock/_unix.py b/src/filelock/_unix.py index 641bd8d9..40cec0ab 100644 --- a/src/filelock/_unix.py +++ b/src/filelock/_unix.py @@ -2,6 +2,7 @@ import os import sys +from contextlib import suppress from errno import ENOSYS from typing import cast @@ -34,16 +35,15 @@ class UnixFileLock(BaseFileLock): def _acquire(self) -> None: open_flags = os.O_RDWR | os.O_CREAT | os.O_TRUNC fd = os.open(self.lock_file, open_flags, self._context.mode) - try: + with suppress(PermissionError): # This locked is not owned by this UID os.fchmod(fd, self._context.mode) - except PermissionError: - pass # This locked is not owned by this UID try: fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) except OSError as exception: os.close(fd) if exception.errno == ENOSYS: # NotImplemented error - raise NotImplementedError("FileSystem does not appear to support flock; user SoftFileLock instead") + msg = "FileSystem does not appear to support flock; user SoftFileLock instead" + raise NotImplementedError(msg) from exception else: self._context.lock_file_fd = fd diff --git a/src/filelock/_util.py b/src/filelock/_util.py index 81cbeaf2..3d95731f 100644 --- a/src/filelock/_util.py +++ b/src/filelock/_util.py @@ -12,12 +12,12 @@ def raise_on_not_writable_file(filename: str) -> None: This is done so files that will never be writable can be separated from files that are writable but currently locked :param filename: file to check - :raises OSError: as if the file was opened for writing + :raises OSError: as if the file was opened for writing. """ - try: - file_stat = os.stat(filename) # use stat to do exists + can write to check without race condition + try: # use stat to do exists + can write to check without race condition + file_stat = os.stat(filename) # noqa: PTH116 except OSError: - return None # swallow does not exist or other errors + return # swallow does not exist or other errors if file_stat.st_mtime != 0: # if os.stat returns but modification is zero that's an invalid os.stat - ignore it if not (file_stat.st_mode & stat.S_IWUSR): @@ -27,7 +27,7 @@ def raise_on_not_writable_file(filename: str) -> None: if sys.platform == "win32": # pragma: win32 cover # On Windows, this is PermissionError raise PermissionError(EACCES, "Permission denied", filename) - else: # pragma: win32 no cover + else: # pragma: win32 no cover # noqa: RET506 # On linux / macOS, this is IsADirectoryError raise IsADirectoryError(EISDIR, "Is a directory", filename) diff --git a/src/filelock/_windows.py b/src/filelock/_windows.py index 799644c8..41683f48 100644 --- a/src/filelock/_windows.py +++ b/src/filelock/_windows.py @@ -2,7 +2,9 @@ import os import sys +from contextlib import suppress from errno import EACCES +from pathlib import Path from typing import cast from ._api import BaseFileLock @@ -42,11 +44,8 @@ def _release(self) -> None: msvcrt.locking(fd, msvcrt.LK_UNLCK, 1) os.close(fd) - try: - os.remove(self.lock_file) - # Probably another instance of the application hat acquired the file lock. - except OSError: - pass + with suppress(OSError): # Probably another instance of the application hat acquired the file lock. + Path(self.lock_file).unlink() else: # pragma: win32 no cover diff --git a/tests/test_error.py b/tests/test_error.py index 45e940e4..1d884a75 100644 --- a/tests/test_error.py +++ b/tests/test_error.py @@ -22,7 +22,7 @@ def test_timeout_lock_file() -> None: def test_timeout_pickle() -> None: timeout = Timeout("/path/to/lock") - timeout_loaded = pickle.loads(pickle.dumps(timeout)) + timeout_loaded = pickle.loads(pickle.dumps(timeout)) # noqa: S301 assert timeout.__class__ == timeout_loaded.__class__ assert str(timeout) == str(timeout_loaded) diff --git a/tests/test_filelock.py b/tests/test_filelock.py index 4ed4c3ca..afe89110 100644 --- a/tests/test_filelock.py +++ b/tests/test_filelock.py @@ -12,21 +12,14 @@ from pathlib import Path, PurePath from stat import S_IWGRP, S_IWOTH, S_IWUSR, filemode from types import TracebackType -from typing import Callable, Iterator, Tuple, Type, Union +from typing import TYPE_CHECKING, Callable, Iterator, Tuple, Type, Union from uuid import uuid4 import pytest -from _pytest.logging import LogCaptureFixture -from pytest_mock import MockerFixture - -from filelock import ( - BaseFileLock, - FileLock, - SoftFileLock, - Timeout, - UnixFileLock, - WindowsFileLock, -) +from filelock import BaseFileLock, FileLock, SoftFileLock, Timeout, UnixFileLock, WindowsFileLock + +if TYPE_CHECKING: + from pytest_mock import MockerFixture @pytest.mark.parametrize( @@ -44,7 +37,7 @@ def test_simple( lock_type: type[BaseFileLock], path_type: type[str] | type[Path], tmp_path: Path, - caplog: LogCaptureFixture, + caplog: pytest.LogCaptureFixture, ) -> None: caplog.set_level(logging.DEBUG) @@ -84,7 +77,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") @pytest.mark.skipif( - sys.platform != "win32" and os.geteuid() == 0, # noqa: SC200 + sys.platform != "win32" and os.geteuid() == 0, reason="Cannot make a read only file (that the current user: root can't read)", ) def test_ro_folder(lock_type: type[BaseFileLock], tmp_path_ro: Path) -> None: @@ -103,7 +96,7 @@ def tmp_file_ro(tmp_path: Path) -> Iterator[Path]: @pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) @pytest.mark.skipif( - sys.platform != "win32" and os.geteuid() == 0, # noqa: SC200 + sys.platform != "win32" and os.geteuid() == 0, reason="Cannot make a read only file (that the current user: root can't read)", ) def test_ro_file(lock_type: type[BaseFileLock], tmp_file_ro: Path) -> None: @@ -219,13 +212,12 @@ def __init__(self, target: Callable[[], None], name: str) -> None: def run(self) -> None: try: super().run() - except Exception: # pragma: no cover + except Exception: # noqa: BLE001 # pragma: no cover self.ex = sys.exc_info() # pragma: no cover 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 @@ -371,7 +363,7 @@ def test_context_release_on_exc(lock_type: type[BaseFileLock], tmp_path: Path) - with lock as lock_1: assert lock is lock_1 assert lock.is_locked - raise ValueError + raise ValueError # noqa: TRY301 except ValueError: assert not lock.is_locked @@ -386,7 +378,7 @@ def test_acquire_release_on_exc(lock_type: type[BaseFileLock], tmp_path: Path) - with lock.acquire() as lock_1: assert lock is lock_1 assert lock.is_locked - raise ValueError + raise ValueError # noqa: TRY301 except ValueError: assert not lock.is_locked @@ -465,7 +457,7 @@ def test_lock_mode(tmp_path: Path) -> None: lock.acquire() assert lock.is_locked - mode = filemode(os.stat(lock_path).st_mode) + mode = filemode(lock_path.stat().st_mode) assert mode == "-rw-rw-rw-" finally: os.umask(initial_umask) @@ -484,7 +476,7 @@ def test_lock_mode_soft(tmp_path: Path) -> None: lock.acquire() assert lock.is_locked - mode = filemode(os.stat(lock_path).st_mode) + mode = filemode(lock_path.stat().st_mode) if sys.platform == "win32": assert mode == "-rw-rw-rw-" else: @@ -540,15 +532,14 @@ def test_wrong_platform(tmp_path: Path) -> None: with pytest.raises(NotImplementedError): lock.acquire() with pytest.raises(NotImplementedError): - lock._release() + lock._release() # noqa: SLF001 @pytest.mark.skipif(sys.platform == "win32", reason="flock not run on windows") def test_flock_not_implemented_unix(tmp_path: Path, mocker: MockerFixture) -> None: mocker.patch("fcntl.flock", side_effect=OSError(ENOSYS, "mock error")) - with pytest.raises(NotImplementedError): - with FileLock(tmp_path / "a.lock"): - pass + with pytest.raises(NotImplementedError), FileLock(tmp_path / "a.lock"): + pass def test_soft_errors(tmp_path: Path, mocker: MockerFixture) -> None: diff --git a/tox.ini b/tox.ini index e032debd..0326eab3 100644 --- a/tox.ini +++ b/tox.ini @@ -38,7 +38,7 @@ description = format the code base to adhere to our styles, and complain about w base_python = python3.10 skip_install = true deps = - pre-commit>=3.2.2 + pre-commit>=3.3.2 commands = pre-commit run --all-files --show-diff-on-failure python -c 'import pathlib; print("hint: run \{\} install to add checks as pre-commit hook".format(pathlib.Path(r"{envdir}") / "bin" / "pre-commit"))' @@ -46,7 +46,7 @@ commands = [testenv:type] description = run type check on code base deps = - mypy==1.2 + mypy==1.3 set_env = {tty:MYPY_FORCE_COLOR = 1} commands = @@ -58,7 +58,7 @@ description = combine coverage files and generate diff (against DIFF_AGAINST def skip_install = true deps = covdefaults>=2.3 - coverage[toml]>=7.2.3 + coverage[toml]>=7.2.7 diff-cover>=7.5 extras = parallel_show_output = true