diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..d8330cb --- /dev/null +++ b/.coveragerc @@ -0,0 +1,8 @@ +[run] +# _connections.py only builds real Redis clients / connection pools; the test +# suite mocks `get_redis` instead of talking to a live server, so measuring it +# would report permanently uncovered lines. The pre-pyproject CI passed this as +# `coverage run --omit=...`; the rendered workflow has no such flag, so it lives +# here now. +omit = + concurrency_limit/_connections.py diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 97892da..a6ff77c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,24 +8,23 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 + with: + fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v6 with: - python-version: '3.10' - architecture: 'x64' + python-version: '3.x' - - name: Install dependencies and package + - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt + pip install ".[build]" - name: Build source and binary distribution package run: | - python setup.py sdist bdist_wheel - env: - PACKAGE_VERSION: ${{ github.ref }} + python -m build - name: Check distribution package run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d551b8b..4bd8b90 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,42 +2,39 @@ name: Run linter and tests on: [push, pull_request] jobs: - test: + build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: - - "3.9" - - "3.10" - - "3.11" - - "3.12" + - '3.10' + - '3.11' + - '3.12' + - '3.13' + - '3.14' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} - - name: Install dependencies and package - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt + - name: Install dependencies and package + run: | + python -m pip install --upgrade pip + pip install -e ".[test,dev]" - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 './concurrency_limit' --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 './concurrency_limit' --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Run lint and code review + run: | + pre-commit run --all-files - - name: Run tests - run: | - # run tests with coverage - coverage run --source='./concurrency_limit' --omit='./concurrency_limit/_connections.py' -m pytest - coverage xml + - name: Run tests with coverage + run: | + coverage run --source='./concurrency_limit' -m pytest + coverage xml - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 diff --git a/.gitignore b/.gitignore index 0904884..a9cba74 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ coverage.xml *.egg-info *.pyc *.pyo +*.sqlite* +build/ +dist/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..ff4f1b6 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-merge-conflict + - id: end-of-file-fixer + - id: requirements-txt-fixer + - id: trailing-whitespace + args: ["--markdown-linebreak-ext=md"] + + - repo: https://github.com/asottile/pyupgrade + rev: v3.21.2 + hooks: + - id: pyupgrade + args: ["--py310-plus"] + + - repo: https://github.com/asottile/add-trailing-comma + rev: v4.0.0 + hooks: + - id: add-trailing-comma + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.14 + hooks: + - id: ruff + args: [ --fix ] + - id: ruff-format diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..b167a98 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,13 @@ +# Project-owned ruff settings, layered on top of the template-managed +# [tool.ruff] section in pyproject.toml (which is overwritten on every copier +# apply, so project-specific rules cannot live there). +# +# This package re-exports its public API with `from .module import *` in every +# submodule, not just in __init__.py: each module declares __all__, so the +# surface is explicit even though ruff cannot resolve it across modules. The +# template only ignores F403/F405 for the package __init__. +extend = "pyproject.toml" + +[lint.extend-per-file-ignores] +"concurrency_limit/*.py" = ["F403", "F405"] +"tests/test_limit.py" = ["F403", "F405"] diff --git a/LICENSE b/LICENSE index 1e7f292..30fccbf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2021 ANEXIA Internetdienstleisungs GmbH +Copyright (c) 2021-2026 Anexia Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 3de031b..1f92040 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,13 @@ pip install concurrency-limit # Getting started ## How it works -- A limit of concurrently running scopes of a concurrency group can be defined using the `limit` decorator. A +- A limit of concurrently running scopes of a concurrency group can be defined using the `limit` decorator. A concurrency group is defined by the `key` attribute of the `LimitConfiguration` instance. -- If the count of concurrently running scopes of a concurrency group is below the configured limit, the scope is +- If the count of concurrently running scopes of a concurrency group is below the configured limit, the scope is executed immediately. -- If the count of concurrently running scopes of a concurrency group exceeds the configured limit, the context manager +- If the count of concurrently running scopes of a concurrency group exceeds the configured limit, the context manager wait unit it goes below the configured limit. -- If the count of concurrently running scopes of a concurrency group does not go below the configured limit with the +- If the count of concurrently running scopes of a concurrency group does not go below the configured limit with the configured timeout, a `ConcurrencyLimitExceededException` exception is raised. ## Usage @@ -41,7 +41,7 @@ pip install concurrency-limit ### Example 1 Limit the concurrency group `"example-1"` to `100` concurrently running scopes. If there are already `100` running -scopes, wait until the count of concurrently running scopes go below `100`. Fail if this does not happen within `10` +scopes, wait until the count of concurrently running scopes go below `100`. Fail if this does not happen within `10` seconds by raising a `ConcurrencyLimitExceededException` exception. ```python @@ -64,8 +64,8 @@ with concurrency_limit.limit(redis_configuration, limit_configuration): ### Example 2 Limit the concurrency group `"example-1"` to `100` concurrently running scopes. If there are already `100` running -scopes, wait until the count of concurrently running scopes go below `100`. Fail if this does not happen within `10` -seconds by raising a `ConcurrencyLimitExceededException` exception. Check if the concurrently running scopes +scopes, wait until the count of concurrently running scopes go below `100`. Fail if this does not happen within `10` +seconds by raising a `ConcurrencyLimitExceededException` exception. Check if the concurrently running scopes are below the limit every `1` second. ```python @@ -110,7 +110,7 @@ with concurrency_limit.limit(redis_configuration, limit_configuration): ### Example 4 -Limit the concurrency group `"example-4"` to `100` concurrently running scopes. The implementation of the +Limit the concurrency group `"example-4"` to `100` concurrently running scopes. The implementation of the concurrency group needs to now the number of the concurrently running scope. ```python @@ -262,7 +262,7 @@ fail by raising a `ConcurrencyLimitExceededException` exception. Default: `10` The timeout that defines how long to wait for the concurrency count to go below the configured limit. The timeout -is configured in seconds. Set to `0` if you want to raise a `ConcurrencyLimitExceededException` exception immediately +is configured in seconds. Set to `0` if you want to raise a `ConcurrencyLimitExceededException` exception immediately if there are too many concurrent executions. #### `limit_interval: float` @@ -270,24 +270,25 @@ if there are too many concurrent executions. Default: `0.1` If there are too many concurrent executions of a scope, and a `limit_timeout` is set to a value greater than `0`, this -configuration defines the interval to re-check the current concurrency count. As soon as the concurrency count is +configuration defines the interval to re-check the current concurrency count. As soon as the concurrency count is below the configured limit, the execution of the scope starts. #### `limit_expire: int` Default: `60` -The expiry time of the concurrency count key, configured in seconds. If a concurrency count is untouched for the +The expiry time of the concurrency count key, configured in seconds. If a concurrency count is untouched for the configured time, it will be deleted. # Supported versions | | Supported | |-------------|-----------| -| Python 3.9 | ✓ | | Python 3.10 | ✓ | | Python 3.11 | ✓ | | Python 3.12 | ✓ | +| Python 3.13 | ✓ | +| Python 3.14 | ✓ | # List of developers diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..5949fc2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Reporting Security Issues + +Please report any security issues you discovered to opensource[at]anexia-it[dot]com + +We will assess the risk, plus make a fix available before we create a GitHub issue. + +Thank you for your contribution. diff --git a/concurrency_limit/configuration.py b/concurrency_limit/configuration.py index bb0313b..af19ed0 100644 --- a/concurrency_limit/configuration.py +++ b/concurrency_limit/configuration.py @@ -1,5 +1,4 @@ import dataclasses -import typing import redis import redis.connection @@ -43,13 +42,13 @@ class RedisConfiguration: unix_socket: bool = False "Use UNIX socket connection to Redis server. Will ignore `secure`, if set." - connection_class: typing.Type[redis.connection.AbstractConnection] = None + connection_class: type[redis.connection.AbstractConnection] = None "Redis connection class to use instead of `secure` and `unix_socket` fields, if set." connection_pool: redis.ConnectionPool = None "Use this connection pool instance instead of the other fields, if set." - def get_connection_class(self) -> typing.Type[redis.connection.AbstractConnection]: + def get_connection_class(self) -> type[redis.connection.AbstractConnection]: """ Returns the `redis.Connection` class to use based on this configuration. diff --git a/concurrency_limit/context_managers.py b/concurrency_limit/context_managers.py index 5c19a7d..ea24845 100644 --- a/concurrency_limit/context_managers.py +++ b/concurrency_limit/context_managers.py @@ -13,7 +13,8 @@ @contextlib.contextmanager def limit( - redis_configuration: RedisConfiguration, limit_configuration: LimitConfiguration + redis_configuration: RedisConfiguration, + limit_configuration: LimitConfiguration, ): """ The `limit` method is a context manager that allows for executing a scoped block of code under a concurrency limit. @@ -100,7 +101,8 @@ class _LockAcquireException(Exception): # exception. Executing the context manager's scope failed in this case. if elapsed > lock_timeout: raise ConcurrencyLimitExceededException( - limit=lock_limit, timeout=lock_timeout + limit=lock_limit, + timeout=lock_timeout, ) # We failed to acquire an execution slot for the context manager's scope, but we want to try again. diff --git a/concurrency_limit/exceptions.py b/concurrency_limit/exceptions.py index b13a881..3bd249f 100644 --- a/concurrency_limit/exceptions.py +++ b/concurrency_limit/exceptions.py @@ -10,7 +10,7 @@ class ConcurrencyLimitException(Exception): def __init__(self, *args, **kwargs): super().__init__( - self._msg_template.format(*args, **kwargs) if self._msg_template else None + self._msg_template.format(*args, **kwargs) if self._msg_template else None, ) diff --git a/concurrency_limit/utils.py b/concurrency_limit/utils.py index 2a4cf5e..6446a8f 100644 --- a/concurrency_limit/utils.py +++ b/concurrency_limit/utils.py @@ -9,7 +9,8 @@ def limit_clean( - redis_configuration: RedisConfiguration, limit_configuration: LimitConfiguration + redis_configuration: RedisConfiguration, + limit_configuration: LimitConfiguration, ): """ Cleans stale limit locks in the hash for the given limit configuration. diff --git a/pyproject.toml b/pyproject.toml index 3181290..057812e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,59 @@ [build-system] -requires = [ - "setuptools>=58", - "wheel", -] +requires = ["setuptools>=75", "wheel", "setuptools_scm[toml]>=8"] build-backend = "setuptools.build_meta" + +[project] +name = "concurrency-limit" +dynamic = ["version"] +description = "A library that implements a distributed concurrency limiting mechanism using Redis as a backend." +readme = "README.md" +requires-python = ">=3.10" +license = {file = "LICENSE"} +authors = [{name = "Anexia", email = "opensource@anexia-it.com"}] +urls = {Homepage = "https://github.com/anexia/python-concurrency-limit"} +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] +dependencies = [ + "redis>=4.0", +] + +[project.optional-dependencies] +test = [ + "coverage>=7.13.2", + "pytest>=8", + "pytest-mock>=3.14", +] +dev = [ + "pre-commit>=4.5,<4.6", +] +build = [ + "build>=1.4.0", + "twine>=6.2.0", + "wheel>=0.46.3", +] + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +where = ["."] +include = ["concurrency_limit*"] + +[tool.setuptools_scm] + +[tool.ruff.lint.per-file-ignores] +# Package __init__ files re-export the public API with `from .module import *`; +# every submodule defines __all__, so the surface is explicit even though ruff +# cannot resolve it across modules. +"concurrency_limit/__init__.py" = ["F403", "F405"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 35ff4e2..0000000 --- a/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -# Package and package dependencies --e . - -# Development dependencies -pytest>=6.2,<6.3 -pytest-mock>=3.6,<3.7 -flake8>=5.0.0,<5.1.0 -codecov>=2.1,<2.2 -setuptools>=42 -wheel>=0.37 -twine>=3.4 diff --git a/setup.py b/setup.py deleted file mode 100644 index 125692b..0000000 --- a/setup.py +++ /dev/null @@ -1,37 +0,0 @@ -import os - -from setuptools import find_packages, setup - -with open(os.path.join(os.path.dirname(__file__), "README.md")) as fh: - readme = fh.read() - -# allow setup.py to be run from any path -os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) - -setup( - name="concurrency-limit", - version=os.getenv("PACKAGE_VERSION", "0.0.0").replace("refs/tags/", ""), - packages=find_packages(), - include_package_data=True, - license="MIT", - description="A library that implements a distributed concurrency limiting mechanism using Redis as a backend.", - long_description=readme, - long_description_content_type="text/markdown", - url="https://github.com/anexia/python-concurrency-limit", - author="Andreas Stocker", - author_email="AStocker@anexia.com", - install_requires=["redis>=4.0"], - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Topic :: Software Development", - ], -) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_base.py b/tests/test_base.py index e881227..9524384 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -13,13 +13,12 @@ class RedisMock: def __init__(self): self._lock = threading.Lock() self._keys = collections.defaultdict(lambda: None) - self._hashes = collections.defaultdict(lambda: {}) - self._expires = collections.defaultdict(lambda: time.time() + 2 ** 32) + self._hashes = collections.defaultdict(dict) + self._expires = collections.defaultdict(lambda: time.time() + 2**32) def scan_iter(self, match): keys = {*self._keys.keys(), *self._hashes.keys()} - for key in fnmatch.filter(keys, match): - yield key + yield from fnmatch.filter(keys, match) def set(self, name, value): with self._lock: @@ -60,8 +59,7 @@ def hscan_iter(self, name): self._clean_expired(name) _hash = dict(self._hashes[name]) - for hkey, hvalue in _hash.items(): - yield hkey, hvalue + yield from _hash.items() def expire(self, name, _time): with self._lock: @@ -94,7 +92,7 @@ def execute(self): def _ensure_type_hash(self, name): if name in self._keys: raise redis.ResponseError( - "WRONGTYPE Operation against a key holding the wrong kind of value" + "WRONGTYPE Operation against a key holding the wrong kind of value", ) def _clean_expired(self, name): diff --git a/tests/test_configuration.py b/tests/test_configuration.py index e8e8bb6..57532b2 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1,5 +1,3 @@ -import typing - import redis.connection import pytest @@ -27,7 +25,10 @@ ( "rediss://127.0.0.1:6379/1", RedisConfiguration( - host="127.0.0.1", port=6379, db=1, connection_class=redis.SSLConnection + host="127.0.0.1", + port=6379, + db=1, + connection_class=redis.SSLConnection, ), ), ( @@ -74,7 +75,10 @@ def test_configuration_from_url(url: str, config: RedisConfiguration): "redis://127.0.0.1:6379/1", {"connection_class": redis.SSLConnection}, RedisConfiguration( - host="127.0.0.1", port=6379, db=1, connection_class=redis.SSLConnection + host="127.0.0.1", + port=6379, + db=1, + connection_class=redis.SSLConnection, ), ), ( @@ -90,7 +94,9 @@ def test_configuration_from_url(url: str, config: RedisConfiguration): ], ) def test_configuration_from_url_with_kwargs( - url: str, kwargs: dict, config: RedisConfiguration + url: str, + kwargs: dict, + config: RedisConfiguration, ): assert RedisConfiguration.from_url(url, **kwargs) == config @@ -113,13 +119,16 @@ def test_configuration_from_url_with_kwargs( ), ( RedisConfiguration( - secure=True, unix_socket=True, connection_class=redis.Connection + secure=True, + unix_socket=True, + connection_class=redis.Connection, ), redis.Connection, ), ], ) def test_configuration_get_connection_class( - config: RedisConfiguration, expected_class: typing.Type[redis.Connection] + config: RedisConfiguration, + expected_class: type[redis.Connection], ): assert config.get_connection_class() == expected_class diff --git a/tests/test_limit.py b/tests/test_limit.py index 1455de6..b89bfce 100644 --- a/tests/test_limit.py +++ b/tests/test_limit.py @@ -5,12 +5,13 @@ import concurrency_limit -from test_base import * +from tests.test_base import * def test_limit_without_concurrency(mocker: pytest_mock.MockerFixture): mocker.patch( - "concurrency_limit.context_managers.get_redis", return_value=RedisMock() + "concurrency_limit.context_managers.get_redis", + return_value=RedisMock(), ) with concurrency_limit.limit( @@ -22,7 +23,8 @@ def test_limit_without_concurrency(mocker: pytest_mock.MockerFixture): def test_limit_slot_ids(mocker: pytest_mock.MockerFixture): mocker.patch( - "concurrency_limit.context_managers.get_redis", return_value=RedisMock() + "concurrency_limit.context_managers.get_redis", + return_value=RedisMock(), ) slot_ids = [] @@ -32,7 +34,9 @@ def _concurrent_function(): with concurrency_limit.limit( concurrency_limit.RedisConfiguration(), concurrency_limit.LimitConfiguration( - key="key-1", limit=10, limit_timeout=0 + key="key-1", + limit=10, + limit_timeout=0, ), ) as slot_id: slot_ids.append(slot_id) @@ -46,7 +50,8 @@ def _concurrent_function(): def test_limit_within_limit(mocker: pytest_mock.MockerFixture): mocker.patch( - "concurrency_limit.context_managers.get_redis", return_value=RedisMock() + "concurrency_limit.context_managers.get_redis", + return_value=RedisMock(), ) @concurrent(threads=1) @@ -62,7 +67,8 @@ def _concurrent_function(): def test_limit_exceeded_limit_without_timeout(mocker: pytest_mock.MockerFixture): mocker.patch( - "concurrency_limit.context_managers.get_redis", return_value=RedisMock() + "concurrency_limit.context_managers.get_redis", + return_value=RedisMock(), ) @concurrent(threads=10) @@ -79,7 +85,8 @@ def _concurrent_function(): def test_limit_exceeded_limit_within_timeout(mocker: pytest_mock.MockerFixture): mocker.patch( - "concurrency_limit.context_managers.get_redis", return_value=RedisMock() + "concurrency_limit.context_managers.get_redis", + return_value=RedisMock(), ) @concurrent(threads=10) @@ -95,7 +102,8 @@ def _concurrent_function(): def test_limit_exceeded_limit_exceeded_timeout(mocker: pytest_mock.MockerFixture): mocker.patch( - "concurrency_limit.context_managers.get_redis", return_value=RedisMock() + "concurrency_limit.context_managers.get_redis", + return_value=RedisMock(), ) @concurrent(threads=10) @@ -112,13 +120,17 @@ def _concurrent_function(): def test_limit_within_limit_expire(mocker: pytest_mock.MockerFixture): mocker.patch( - "concurrency_limit.context_managers.get_redis", return_value=RedisMock() + "concurrency_limit.context_managers.get_redis", + return_value=RedisMock(), ) with concurrency_limit.limit( concurrency_limit.RedisConfiguration(), concurrency_limit.LimitConfiguration( - key="key-1", limit=1, limit_expire=5, limit_timeout=0 + key="key-1", + limit=1, + limit_expire=5, + limit_timeout=0, ), ): time.sleep(1) @@ -127,7 +139,10 @@ def test_limit_within_limit_expire(mocker: pytest_mock.MockerFixture): with concurrency_limit.limit( concurrency_limit.RedisConfiguration(), concurrency_limit.LimitConfiguration( - key="key-1", limit=1, limit_expire=5, limit_timeout=0 + key="key-1", + limit=1, + limit_expire=5, + limit_timeout=0, ), ): time.sleep(1) @@ -135,13 +150,17 @@ def test_limit_within_limit_expire(mocker: pytest_mock.MockerFixture): def test_limit_exceeded_limit_expire(mocker: pytest_mock.MockerFixture): mocker.patch( - "concurrency_limit.context_managers.get_redis", return_value=RedisMock() + "concurrency_limit.context_managers.get_redis", + return_value=RedisMock(), ) with concurrency_limit.limit( concurrency_limit.RedisConfiguration(), concurrency_limit.LimitConfiguration( - key="key-1", limit=1, limit_expire=5, limit_timeout=0 + key="key-1", + limit=1, + limit_expire=5, + limit_timeout=0, ), ): time.sleep(10) @@ -149,7 +168,10 @@ def test_limit_exceeded_limit_expire(mocker: pytest_mock.MockerFixture): with concurrency_limit.limit( concurrency_limit.RedisConfiguration(), concurrency_limit.LimitConfiguration( - key="key-1", limit=1, limit_expire=5, limit_timeout=0 + key="key-1", + limit=1, + limit_expire=5, + limit_timeout=0, ), ): time.sleep(1) @@ -157,7 +179,8 @@ def test_limit_exceeded_limit_expire(mocker: pytest_mock.MockerFixture): def test_limit_with_high_load(mocker: pytest_mock.MockerFixture): mocker.patch( - "concurrency_limit.context_managers.get_redis", return_value=RedisMock() + "concurrency_limit.context_managers.get_redis", + return_value=RedisMock(), ) counter = 0 @@ -167,7 +190,9 @@ def _concurrent_function(): with concurrency_limit.limit( concurrency_limit.RedisConfiguration(), concurrency_limit.LimitConfiguration( - key="key-1", limit=500, limit_timeout=1 + key="key-1", + limit=500, + limit_timeout=1, ), ) as slot_id: nonlocal counter @@ -217,7 +242,8 @@ def test_limit_clean(mocker: pytest_mock.MockerFixture): assert client.hlen("key-1") == 2 for scan_key in concurrency_limit.limit_iter( - concurrency_limit.RedisConfiguration(), "key-*" + concurrency_limit.RedisConfiguration(), + "key-*", ): for scan_lock_id, _ in client.hscan_iter(scan_key): assert scan_lock_id.startswith("unexpired-") @@ -243,7 +269,8 @@ def test_limit_clean_expire_wrong_type(mocker: pytest_mock.MockerFixture): assert client.hlen("key-1") == 2 for scan_key in concurrency_limit.limit_iter( - concurrency_limit.RedisConfiguration(), "key-*" + concurrency_limit.RedisConfiguration(), + "key-*", ): for scan_lock_id, _ in client.hscan_iter(scan_key): assert scan_lock_id.startswith("valid-")