From 13af6ad81505116e2c1366d15c98c38b3b8f0d65 Mon Sep 17 00:00:00 2001 From: Christian Hartung Date: Fri, 3 May 2024 18:06:51 -0300 Subject: [PATCH] style: add hatch static analysis --- .github/workflows/tests.yml | 47 +- docs/source/conf.py | 5 +- docs/source/exceptions.rst | 2 +- hatch.toml | 21 - pyproject.toml | 39 ++ ruff_defaults.toml | 534 ++++++++++++++++++++++ src/loafer/dispatchers.py | 28 +- src/loafer/exceptions.py | 4 +- src/loafer/ext/aws/bases.py | 2 +- src/loafer/ext/aws/handlers.py | 14 +- src/loafer/ext/aws/message_translators.py | 13 +- src/loafer/ext/aws/providers.py | 19 +- src/loafer/ext/aws/routes.py | 3 +- src/loafer/ext/sentry.py | 2 +- src/loafer/managers.py | 8 +- src/loafer/message_translators.py | 2 +- src/loafer/providers.py | 2 - src/loafer/routes.py | 33 +- src/loafer/runners.py | 10 +- src/loafer/utils.py | 8 +- tests/conftest.py | 17 +- tests/ext/aws/conftest.py | 10 +- tests/ext/aws/test_handlers.py | 4 +- tests/ext/aws/test_providers.py | 24 +- tests/ext/aws/test_routes.py | 12 +- tests/test_dispatchers.py | 7 +- tests/test_managers.py | 4 +- tests/test_routes.py | 24 +- tests/test_runners.py | 5 +- tests/test_utils.py | 2 +- 30 files changed, 712 insertions(+), 193 deletions(-) delete mode 100644 hatch.toml create mode 100644 ruff_defaults.toml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6e0a271..650a0ad 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,52 +17,27 @@ concurrency: cancel-in-progress: true jobs: - lint: - name: lint + tests: + name: Run tests on ${{ matrix.python-version }} runs-on: ubuntu-latest - env: - HATCH_ENV: lint - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup Hatch - run: pipx install hatch - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.12" - - - name: Run linters - run: | - hatch env run lint - - - name: Check dead fixtures - run: | - hatch env run check-fixtures - - test: - name: Run tests - runs-on: ubuntu-latest - env: - HATCH_ENV: test strategy: fail-fast: false matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup Hatch - run: pipx install hatch + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Setup Hatch + run: pipx install hatch + + - name: Run static analysis + run: hatch fmt --check + - name: Run tests run: | - hatch env run -i py=${{ matrix.python-version }} test + hatch env run -e tests -i py=${{ matrix.python-version }} test diff --git a/docs/source/conf.py b/docs/source/conf.py index 99e0108..3e06dd5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,5 +1,4 @@ -#!/usr/bin/env python3 -# +# # noqa: INP001 # Loafer documentation build configuration file, created by # sphinx-quickstart on Tue Apr 12 03:26:33 2016. # @@ -45,7 +44,7 @@ # General information about the project. project = "Loafer" -copyright = "2016-2019, George Y. Kussumoto" +copyright = "2016-2019, George Y. Kussumoto" # noqa: A001 author = "George Y. Kussumoto" # The version info for the project you're documenting, acts as replacement for diff --git a/docs/source/exceptions.rst b/docs/source/exceptions.rst index 6a542b6..ad5c5dd 100644 --- a/docs/source/exceptions.rst +++ b/docs/source/exceptions.rst @@ -15,4 +15,4 @@ A description of all the exceptions: * ``DeleteMessage``: if any :doc:`handlers` raises this exception, the message will be rejected and acknowledged (the message will be deleted). -* ``LoaferException``: the base exception for ``DeleteMessage``. +* ``LoaferError``: the base exception for ``DeleteMessage``. diff --git a/hatch.toml b/hatch.toml deleted file mode 100644 index 1098ff7..0000000 --- a/hatch.toml +++ /dev/null @@ -1,21 +0,0 @@ -[envs.default] -dependencies = [ - "pre-commit", - "pytest", - "pytest-asyncio", - "pytest-cov", - "pytest-deadfixtures", -] - -[envs.lint.env-vars] -SKIP = "no-commit-to-branch" - -[envs.lint.scripts] -lint = "pre-commit run -a -v" -check-fixtures = "pytest --dead-fixtures" - -[envs.test.scripts] -test = "pytest -vv --cov=loafer --cov-report=term-missing" - -[[envs.test.matrix]] -python = ["3.8", "3.9", "3.10", "3.11", "3.12"] diff --git a/pyproject.toml b/pyproject.toml index af537f7..89ca46f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,45 @@ packages = ["src/loafer"] [tool.hatch.version] source = "vcs" +[tool.hatch.envs.default] +dependencies = [ + "coverage[toml]>=6.5", + "pytest", + "pytest-asyncio", + "pytest-deadfixtures", +] + +[tool.hatch.envs.default.scripts] +test = "pytest {args:tests}" +test-cov = "coverage run -m pytest {args:tests}" +cov-report = [ + "- coverage combine", + "coverage report", +] +cov = [ + "test-cov", + "cov-report", +] + +[[tool.hatch.envs.all.matrix]] +python = ["3.8", "3.9", "3.10", "3.11", "3.12"] + +[tool.hatch.envs.types] +dependencies = [ + "mypy>=1.0.0", +] +[tool.hatch.envs.types.scripts] +check = "mypy --install-types --non-interactive {args:src/loafer tests}" + +[tool.hatch.envs.hatch-static-analysis] +config-path = "ruff_defaults.toml" + [tool.pytest.ini_options] testpaths = ["tests"] asyncio_mode = "strict" + +[tool.ruff] +extend = "ruff_defaults.toml" + +[tool.ruff.lint.flake8-tidy-imports] +ban-relative-imports = "parents" diff --git a/ruff_defaults.toml b/ruff_defaults.toml new file mode 100644 index 0000000..07911f0 --- /dev/null +++ b/ruff_defaults.toml @@ -0,0 +1,534 @@ +line-length = 120 + +[format] +docstring-code-format = true +docstring-code-line-length = 80 + +[lint] +select = [ + "A001", + "A002", + "A003", + "ARG001", + "ARG002", + "ARG003", + "ARG004", + "ARG005", + "ASYNC100", + "ASYNC101", + "ASYNC102", + "B002", + "B003", + "B004", + "B005", + "B006", + "B007", + "B008", + "B009", + "B010", + "B011", + "B012", + "B013", + "B014", + "B015", + "B016", + "B017", + "B018", + "B019", + "B020", + "B021", + "B022", + "B023", + "B024", + "B025", + "B026", + "B028", + "B029", + "B030", + "B031", + "B032", + "B033", + "B034", + "B904", + "B905", + "BLE001", + "C400", + "C401", + "C402", + "C403", + "C404", + "C405", + "C406", + "C408", + "C409", + "C410", + "C411", + "C413", + "C414", + "C415", + "C416", + "C417", + "C418", + "C419", + "COM818", + "DTZ001", + "DTZ002", + "DTZ003", + "DTZ004", + "DTZ005", + "DTZ006", + "DTZ007", + "DTZ011", + "DTZ012", + "E101", + "E401", + "E402", + "E501", + "E701", + "E702", + "E703", + "E711", + "E712", + "E713", + "E714", + "E721", + "E722", + "E731", + "E741", + "E742", + "E743", + "E902", + "E999", + "EM101", + "EM102", + "EM103", + "EXE001", + "EXE002", + "EXE003", + "EXE004", + "EXE005", + "F401", + "F402", + "F403", + "F404", + "F405", + "F406", + "F407", + "F501", + "F502", + "F503", + "F504", + "F505", + "F506", + "F507", + "F508", + "F509", + "F521", + "F522", + "F523", + "F524", + "F525", + "F541", + "F601", + "F602", + "F621", + "F622", + "F631", + "F632", + "F633", + "F634", + "F701", + "F702", + "F704", + "F706", + "F707", + "F722", + "F811", + "F821", + "F822", + "F823", + "F841", + "F842", + "F901", + "FA100", + "FA102", + "FBT001", + "FBT002", + "FLY002", + "G001", + "G002", + "G003", + "G004", + "G010", + "G101", + "G201", + "G202", + "I001", + "I002", + "ICN001", + "ICN002", + "ICN003", + "INP001", + "INT001", + "INT002", + "INT003", + "ISC003", + "N801", + "N802", + "N803", + "N804", + "N805", + "N806", + "N807", + "N811", + "N812", + "N813", + "N814", + "N815", + "N816", + "N817", + "N818", + "N999", + "PERF101", + "PERF102", + "PERF401", + "PERF402", + "PGH001", + "PGH002", + "PGH005", + "PIE790", + "PIE794", + "PIE796", + "PIE800", + "PIE804", + "PIE807", + "PIE808", + "PIE810", + "PLC0105", + "PLC0131", + "PLC0132", + "PLC0205", + "PLC0208", + "PLC0414", + "PLC3002", + "PLE0100", + "PLE0101", + "PLE0116", + "PLE0117", + "PLE0118", + "PLE0241", + "PLE0302", + "PLE0307", + "PLE0604", + "PLE0605", + "PLE1142", + "PLE1205", + "PLE1206", + "PLE1300", + "PLE1307", + "PLE1310", + "PLE1507", + "PLE1700", + "PLE2502", + "PLE2510", + "PLE2512", + "PLE2513", + "PLE2514", + "PLE2515", + "PLR0124", + "PLR0133", + "PLR0206", + "PLR0402", + "PLR1701", + "PLR1711", + "PLR1714", + "PLR1722", + "PLR2004", + "PLR5501", + "PLW0120", + "PLW0127", + "PLW0129", + "PLW0131", + "PLW0406", + "PLW0602", + "PLW0603", + "PLW0711", + "PLW1508", + "PLW1509", + "PLW1510", + "PLW2901", + "PLW3301", + "PT001", + "PT002", + "PT003", + "PT006", + "PT007", + "PT008", + "PT009", + "PT010", + "PT011", + "PT012", + "PT013", + "PT014", + "PT015", + "PT016", + "PT017", + "PT018", + "PT019", + "PT020", + "PT021", + "PT022", + "PT023", + "PT024", + "PT025", + "PT026", + "PT027", + "PYI001", + "PYI002", + "PYI003", + "PYI004", + "PYI005", + "PYI006", + "PYI007", + "PYI008", + "PYI009", + "PYI010", + "PYI011", + "PYI012", + "PYI013", + "PYI014", + "PYI015", + "PYI016", + "PYI017", + "PYI018", + "PYI019", + "PYI020", + "PYI021", + "PYI024", + "PYI025", + "PYI026", + "PYI029", + "PYI030", + "PYI032", + "PYI033", + "PYI034", + "PYI035", + "PYI036", + "PYI041", + "PYI042", + "PYI043", + "PYI044", + "PYI045", + "PYI046", + "PYI047", + "PYI048", + "PYI049", + "PYI050", + "PYI051", + "PYI052", + "PYI053", + "PYI054", + "PYI055", + "PYI056", + "RET503", + "RET504", + "RET505", + "RET506", + "RET507", + "RET508", + "RSE102", + "RUF001", + "RUF002", + "RUF003", + "RUF005", + "RUF006", + "RUF007", + "RUF008", + "RUF009", + "RUF010", + "RUF011", + "RUF012", + "RUF013", + "RUF015", + "RUF016", + "RUF100", + "RUF200", + "S101", + "S102", + "S103", + "S104", + "S105", + "S106", + "S107", + "S108", + "S110", + "S112", + "S113", + "S301", + "S302", + "S303", + "S304", + "S305", + "S306", + "S307", + "S308", + "S310", + "S311", + "S312", + "S313", + "S314", + "S315", + "S316", + "S317", + "S318", + "S319", + "S320", + "S321", + "S323", + "S324", + "S501", + "S506", + "S508", + "S509", + "S601", + "S602", + "S604", + "S605", + "S606", + "S607", + "S608", + "S609", + "S612", + "S701", + "SIM101", + "SIM102", + "SIM103", + "SIM105", + "SIM107", + "SIM108", + "SIM109", + "SIM110", + "SIM112", + "SIM114", + "SIM115", + "SIM116", + "SIM117", + "SIM118", + "SIM201", + "SIM202", + "SIM208", + "SIM210", + "SIM211", + "SIM212", + "SIM220", + "SIM221", + "SIM222", + "SIM223", + "SIM300", + "SIM910", + "SLF001", + "SLOT000", + "SLOT001", + "SLOT002", + "T100", + "T201", + "T203", + "TCH001", + "TCH002", + "TCH003", + "TCH004", + "TCH005", + "TD004", + "TD005", + "TD006", + "TD007", + "TID251", + "TID252", + "TID253", + "TRY002", + "TRY003", + "TRY004", + "TRY200", + "TRY201", + "TRY300", + "TRY301", + "TRY302", + "TRY400", + "TRY401", + "UP001", + "UP003", + "UP004", + "UP005", + "UP006", + "UP007", + "UP008", + "UP009", + "UP010", + "UP011", + "UP012", + "UP013", + "UP014", + "UP015", + "UP017", + "UP018", + "UP019", + "UP020", + "UP021", + "UP022", + "UP023", + "UP024", + "UP025", + "UP026", + "UP027", + "UP028", + "UP029", + "UP030", + "UP031", + "UP032", + "UP033", + "UP034", + "UP035", + "UP036", + "UP037", + "UP038", + "UP039", + "UP040", + "W291", + "W292", + "W293", + "W505", + "W605", + "YTT101", + "YTT102", + "YTT103", + "YTT201", + "YTT202", + "YTT203", + "YTT204", + "YTT301", + "YTT302", + "YTT303", +] + +[lint.per-file-ignores] +"**/scripts/*" = [ + "INP001", + "T201", +] +"**/tests/**/*" = [ + "PLC1901", + "PLR2004", + "PLR6301", + "S", + "TID252", +] + +[lint.flake8-tidy-imports] +ban-relative-imports = "all" + +[lint.isort] +known-first-party = ["loafer"] + +[lint.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false diff --git a/src/loafer/dispatchers.py b/src/loafer/dispatchers.py index ba57275..d1491d4 100644 --- a/src/loafer/dispatchers.py +++ b/src/loafer/dispatchers.py @@ -1,11 +1,15 @@ +from __future__ import annotations + import asyncio import logging import sys -from typing import Any, Optional, Sequence +from typing import TYPE_CHECKING, Any, Sequence from .compat import TaskGroup from .exceptions import DeleteMessage -from .routes import Route + +if TYPE_CHECKING: + from .routes import Route logger = logging.getLogger(__name__) @@ -14,28 +18,28 @@ class LoaferDispatcher: def __init__( self, routes: Sequence[Route], - max_concurrency: Optional[int] = None, + max_concurrency: int | None = None, ) -> None: self.routes = routes self.max_concurrency = max_concurrency if max_concurrency is not None else max(len(routes), 5) async def dispatch_message(self, message: Any, route: Route) -> bool: - logger.debug(f"dispatching message to route={route}") + logger.debug("dispatching message to route=%s", route) confirm_message = False if not message: - logger.warning(f"message will be ignored:\n{message!r}\n") + logger.warning("message will be ignored:\n%r\n", message) return confirm_message try: confirm_message = await route.deliver(message) except DeleteMessage: - logger.info(f"explicit message deletion\n{message}\n") + logger.info("explicit message deletion\n%s\n", message) confirm_message = True except asyncio.CancelledError: msg = '"{!r}" was cancelled, the message will not be acknowledged:\n{}\n' logger.warning(msg.format(route.handler, message)) except Exception as exc: - logger.exception(f"{exc!r}") + logger.exception("%r", exc) # noqa: TRY401 exc_info = sys.exc_info() confirm_message = await route.error_handler(exc_info, message) @@ -54,9 +58,9 @@ async def _fetch_messages( self, processing_queue: asyncio.Queue, tg: TaskGroup, - forever: bool = True, + forever: bool = True, # noqa: FBT001, FBT002 ) -> None: - routes = [route for route in self.routes] + routes = list(self.routes) tasks = [tg.create_task(route.provider.fetch_messages()) for route in routes] while routes or tasks: @@ -66,8 +70,8 @@ async def _fetch_messages( new_tasks = [] for task, route in zip(tasks, routes): if task.done(): - if task.exception(): - raise task.exception() + if exc := task.exception(): + raise exc for message in task.result(): await processing_queue.put((message, route)) @@ -89,7 +93,7 @@ async def _consume_messages(self, processing_queue: asyncio.Queue) -> None: await self._process_message(message, route) processing_queue.task_done() - async def dispatch_providers(self, forever: bool = True) -> None: + async def dispatch_providers(self, forever: bool = True) -> None: # noqa: FBT001, FBT002 processing_queue = asyncio.Queue(self.max_concurrency) async with TaskGroup() as tg: diff --git a/src/loafer/exceptions.py b/src/loafer/exceptions.py index a230a33..7c46c45 100644 --- a/src/loafer/exceptions.py +++ b/src/loafer/exceptions.py @@ -6,9 +6,9 @@ class ProviderRuntimeError(ProviderError): pass -class LoaferException(Exception): +class LoaferError(Exception): pass -class DeleteMessage(LoaferException): +class DeleteMessage(LoaferError): # noqa: N818 pass diff --git a/src/loafer/ext/aws/bases.py b/src/loafer/ext/aws/bases.py index 6671d5e..f0b18ce 100644 --- a/src/loafer/ext/aws/bases.py +++ b/src/loafer/ext/aws/bases.py @@ -34,7 +34,7 @@ def __init__(self, *args, **kwargs): self._cached_queue_urls = {} async def get_queue_url(self, queue): - if queue and (queue.startswith("http://") or queue.startswith("https://")): + if queue and (queue.startswith(("http://", "https://"))): name = queue.split("/")[-1] self._cached_queue_urls[name] = queue queue = name diff --git a/src/loafer/ext/aws/handlers.py b/src/loafer/ext/aws/handlers.py index f8afdb8..84fd6ef 100644 --- a/src/loafer/ext/aws/handlers.py +++ b/src/loafer/ext/aws/handlers.py @@ -18,18 +18,19 @@ def __str__(self): async def publish(self, message, encoder=json.dumps): if not self.queue_name: - raise ValueError(f"{type(self).__name__}: missing queue_name attribute") + msg = f"{type(self).__name__}: missing queue_name attribute" + raise ValueError(msg) if encoder: message = encoder(message) - logger.debug(f"publishing, queue={self.queue_name}, message={message}") + logger.debug("publishing, queue=%s, message=%s", self.queue_name, message) queue_url = await self.get_queue_url(self.queue_name) async with self.get_client() as client: return await client.send_message(QueueUrl=queue_url, MessageBody=message) - async def handle(self, message, *args): + async def handle(self, message, *args): # noqa: ARG002 return await self.publish(message) @@ -45,17 +46,18 @@ def __str__(self): async def publish(self, message, encoder=json.dumps): if not self.topic: - raise ValueError(f"{type(self).__name__}: missing topic attribute") + msg = f"{type(self).__name__}: missing topic attribute" + raise ValueError(msg) if encoder: message = encoder(message) topic_arn = await self.get_topic_arn(self.topic) - logger.debug(f"publishing, topic={topic_arn}, message={message}") + logger.debug("publishing, topic=%s, message=%s", topic_arn, message) msg = json.dumps({"default": message}) async with self.get_client() as client: return await client.publish(TopicArn=topic_arn, MessageStructure="json", Message=msg) - async def handle(self, message, *args): + async def handle(self, message, *args): # noqa: ARG002 return await self.publish(message) diff --git a/src/loafer/ext/aws/message_translators.py b/src/loafer/ext/aws/message_translators.py index 3250878..07c7320 100644 --- a/src/loafer/ext/aws/message_translators.py +++ b/src/loafer/ext/aws/message_translators.py @@ -12,15 +12,13 @@ def translate(self, message): try: body = message["Body"] except (KeyError, TypeError): - logger.error( - "missing Body key in SQS message. It really came from SQS ?" "\nmessage={!r}".format(message) - ) + logger.exception("missing Body key in SQS message. It really came from SQS ?\nmessage=%r", message) return translated try: translated["content"] = json.loads(body) except json.decoder.JSONDecodeError as exc: - logger.error(f"error={exc!r}, message={message!r}") + logger.exception("error=%r, message=%r", exc, message) # noqa: TRY401 return translated message.pop("Body") @@ -35,9 +33,8 @@ def translate(self, message): body = json.loads(message["Body"]) message_body = body.pop("Message") except (KeyError, TypeError): - logger.error( - "Missing Body or Message key in SQS message. It really came from SNS ?" - "\nmessage={!r}".format(message) + logger.exception( + "Missing Body or Message key in SQS message. It really came from SNS ?\nmessage=%r", message ) return translated @@ -47,7 +44,7 @@ def translate(self, message): try: translated["content"] = json.loads(message_body) except (json.decoder.JSONDecodeError, TypeError) as exc: - logger.error(f"error={exc!r}, message={message!r}") + logger.exception("error=%r, message=%r", exc, message) # noqa: TRY401 return translated translated["metadata"].update(body) diff --git a/src/loafer/ext/aws/providers.py b/src/loafer/ext/aws/providers.py index bf0f84b..d651ae1 100644 --- a/src/loafer/ext/aws/providers.py +++ b/src/loafer/ext/aws/providers.py @@ -1,12 +1,14 @@ import logging +from http import HTTPStatus import botocore.exceptions -from .bases import BaseSQSClient from loafer.exceptions import ProviderError from loafer.providers import AbstractProvider from loafer.utils import calculate_backoff_multiplier +from .bases import BaseSQSClient + logger = logging.getLogger(__name__) @@ -28,14 +30,14 @@ def __str__(self): async def confirm_message(self, message): receipt = message["ReceiptHandle"] - logger.info(f"confirm message (ack/deletion), receipt={receipt!r}") + logger.info("confirm message (ack/deletion), receipt=%r", receipt) queue_url = await self.get_queue_url(self.queue_name) try: async with self.get_client() as client: return await client.delete_message(QueueUrl=queue_url, ReceiptHandle=receipt) except botocore.exceptions.ClientError as exc: - if exc.response["ResponseMetadata"]["HTTPStatusCode"] == 404: + if exc.response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.NOT_FOUND: return True raise @@ -50,8 +52,7 @@ async def message_not_processed(self, message): custom_visibility_timeout = round(backoff_multiplier * self._options.get("VisibilityTimeout", 30)) logger.info( - f"message not processed, receipt={receipt!r}, " - f"custom_visibility_timeout={custom_visibility_timeout!r}" + "message not processed, receipt=%r, custom_visibility_timeout=%r", receipt, custom_visibility_timeout ) queue_url = await self.get_queue_url(self.queue_name) try: @@ -64,18 +65,20 @@ async def message_not_processed(self, message): except botocore.exceptions.ClientError as exc: if "InvalidParameterValue" not in str(exc): raise + return None async def fetch_messages(self): - logger.debug(f"fetching messages on {self.queue_name}") + logger.debug("fetching messages on %s", self.queue_name) try: queue_url = await self.get_queue_url(self.queue_name) async with self.get_client() as client: response = await client.receive_message(QueueUrl=queue_url, **self._options) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as exc: - raise ProviderError(f"error fetching messages from queue={self.queue_name}: {str(exc)}") from exc + msg = f"error fetching messages from queue={self.queue_name}: {exc!s}" + raise ProviderError(msg) from exc return response.get("Messages", []) def stop(self): - logger.info(f"stopping {self}") + logger.info("stopping %s", self) return super().stop() diff --git a/src/loafer/ext/aws/routes.py b/src/loafer/ext/aws/routes.py index 43093c1..1ee4fcc 100644 --- a/src/loafer/ext/aws/routes.py +++ b/src/loafer/ext/aws/routes.py @@ -1,4 +1,5 @@ -from ...routes import Route +from loafer.routes import Route + from .message_translators import SNSMessageTranslator, SQSMessageTranslator from .providers import SQSProvider diff --git a/src/loafer/ext/sentry.py b/src/loafer/ext/sentry.py index 8ca5cd3..747a305 100644 --- a/src/loafer/ext/sentry.py +++ b/src/loafer/ext/sentry.py @@ -1,7 +1,7 @@ # TODO: it should be async -def sentry_handler(sdk_or_hub, delete_message=False): +def sentry_handler(sdk_or_hub, delete_message=False): # noqa: FBT002 def send_to_sentry(exc_info, message): with sdk_or_hub.push_scope() as scope: scope.set_extra("message", message) diff --git a/src/loafer/managers.py b/src/loafer/managers.py index e49831c..59ff4d1 100644 --- a/src/loafer/managers.py +++ b/src/loafer/managers.py @@ -18,7 +18,7 @@ def __init__(self, routes, runner=None, max_concurrency=None): self.routes = routes self.dispatcher = LoaferDispatcher(self.routes, max_concurrency) - def run(self, forever=True, debug=False): + def run(self, forever=True, debug=False): # noqa: FBT002 loop = self.runner.loop self._future = asyncio.ensure_future( self.dispatcher.dispatch_providers(forever=forever), @@ -44,10 +44,12 @@ def on_future__errors(self, future): exc = future.exception() # Unhandled errors crashes the event loop execution if isinstance(exc, BaseException): - logger.critical(f"fatal error caught: {exc!r}") + logger.critical("fatal error caught: %r", exc) self.runner.prepare_stop() + return None + return None - def on_loop__stop(self, *args, **kwargs): + def on_loop__stop(self): logger.info("cancel dispatcher operations ...") if hasattr(self, "_future"): diff --git a/src/loafer/message_translators.py b/src/loafer/message_translators.py index 87190bd..c060ea4 100644 --- a/src/loafer/message_translators.py +++ b/src/loafer/message_translators.py @@ -18,5 +18,5 @@ def translate(self, message): class StringMessageTranslator(AbstractMessageTranslator): def translate(self, message): - logger.debug(f"{type(self).__name__!r} will translate {message!r}") + logger.debug("%r will translate %r", type(self).__name__, message) return {"content": str(message), "metadata": {}} diff --git a/src/loafer/providers.py b/src/loafer/providers.py index 0da67c4..3b721ff 100644 --- a/src/loafer/providers.py +++ b/src/loafer/providers.py @@ -19,7 +19,6 @@ async def confirm_message(self, message): async def message_not_processed(self, message): """Perform actions when a message was not processed.""" - pass def stop(self): """Stop the provider. @@ -27,4 +26,3 @@ def stop(self): If needed, the provider should perform clean-up actions. This method is called whenever we need to shutdown the provider. """ - pass diff --git a/src/loafer/routes.py b/src/loafer/routes.py index 6fbb838..993e1ad 100644 --- a/src/loafer/routes.py +++ b/src/loafer/routes.py @@ -12,19 +12,20 @@ def __init__(self, provider, handler, name="default", message_translator=None, e self.name = name if not isinstance(provider, AbstractProvider): - raise TypeError(f"invalid provider instance: {provider!r}") + msg = f"invalid provider instance: {provider!r}" + raise TypeError(msg) self.provider = provider - if message_translator: - if not isinstance(message_translator, AbstractMessageTranslator): - raise TypeError(f"invalid message translator instance: {message_translator!r}") + if message_translator and not isinstance(message_translator, AbstractMessageTranslator): + msg = f"invalid message translator instance: {message_translator!r}" + raise TypeError(msg) self.message_translator = message_translator - if error_handler: - if not callable(error_handler): - raise TypeError(f"error_handler must be a callable object: {error_handler!r}") + if error_handler and not callable(error_handler): + msg = f"error_handler must be a callable object: {error_handler!r}" + raise TypeError(msg) self._error_handler = error_handler @@ -36,14 +37,11 @@ def __init__(self, provider, handler, name="default", message_translator=None, e self._handler_instance = handler if not self.handler: - raise ValueError( - f"handler must be a callable object or implement `handle` method: {self.handler!r}" - ) + msg = f"handler must be a callable object or implement `handle` method: {self.handler!r}" + raise ValueError(msg) def __str__(self): - return "<{}(name={} provider={!r} handler={!r})>".format( - type(self).__name__, self.name, self.provider, self.handler - ) + return f"<{type(self).__name__}(name={self.name} provider={self.provider!r} handler={self.handler!r})>" def apply_message_translator(self, message): processed_message = {"content": message, "metadata": {}} @@ -54,17 +52,18 @@ def apply_message_translator(self, message): processed_message["metadata"].update(translated.get("metadata", {})) processed_message["content"] = translated["content"] if not processed_message["content"]: - raise ValueError(f"{self.message_translator} failed to translate message={message}") + msg = f"{self.message_translator} failed to translate message={message}" + raise ValueError(msg) return processed_message async def deliver(self, raw_message): message = self.apply_message_translator(raw_message) - logger.info(f"delivering message route={self}, message={message!r}") + logger.info("delivering message route=%s, message=%r", self, message) return await ensure_coroutinefunction(self.handler, message["content"], message["metadata"]) async def error_handler(self, exc_info, message): - logger.info(f"error handler process originated by message={message}") + logger.info("error handler process originated by message=%s", message) if self._error_handler is not None: return await ensure_coroutinefunction(self._error_handler, exc_info, message) @@ -72,7 +71,7 @@ async def error_handler(self, exc_info, message): return False def stop(self): - logger.info(f"stopping route {self}") + logger.info("stopping route %s", self) self.provider.stop() # only for class-based handlers if hasattr(self._handler_instance, "stop"): diff --git a/src/loafer/runners.py b/src/loafer/runners.py index f26271b..fe50935 100644 --- a/src/loafer/runners.py +++ b/src/loafer/runners.py @@ -15,7 +15,7 @@ def __init__(self, on_stop_callback=None): def loop(self): return asyncio.get_event_loop() - def start(self, debug=False): + def start(self, debug=False): # noqa: FBT002 if debug: self.loop.set_debug(enabled=debug) @@ -27,10 +27,10 @@ def start(self, debug=False): finally: self.stop() self.loop.close() - logger.debug(f"loop.is_running={self.loop.is_running()}") - logger.debug(f"loop.is_closed={self.loop.is_closed()}") + logger.debug("loop.is_running=%s", self.loop.is_running()) + logger.debug("loop.is_closed=%s", self.loop.is_closed()) - def prepare_stop(self, *args): + def prepare_stop(self, *args): # noqa: ARG002 if self.loop.is_running(): # signals loop.run_forever to exit in the next iteration self.loop.stop() @@ -55,7 +55,7 @@ def _cancel_all_tasks(self): } ) - def stop(self, *args, **kwargs): + def stop(self): logger.info("stopping Loafer ...") if callable(self._on_stop_callback): self._on_stop_callback() diff --git a/src/loafer/utils.py b/src/loafer/utils.py index 6fa07ad..88109df 100644 --- a/src/loafer/utils.py +++ b/src/loafer/utils.py @@ -7,14 +7,12 @@ async def ensure_coroutinefunction(func, *args): if iscoroutinefunction(func): - logger.debug(f"handler is coroutine! {func!r}") + logger.debug("handler is coroutine! %r", func) return await func(*args) - logger.debug(f"handler will run in a separate thread: {func!r}") + logger.debug("handler will run in a separate thread: %r", func) return await to_thread(func, *args) def calculate_backoff_multiplier(number_of_tries, backoff_factor): - exponential_factor = backoff_factor**number_of_tries - - return exponential_factor + return backoff_factor**number_of_tries diff --git a/tests/conftest.py b/tests/conftest.py index a89e3fe..5b2b4c6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,8 +5,9 @@ @pytest.fixture def dummy_handler(): - def handler(message, *args): - raise AssertionError("I should not be called") + def handler(message, *args): # noqa: ARG001 + msg = "I should not be called" + raise AssertionError(msg) return handler @@ -15,15 +16,19 @@ def handler(message, *args): def dummy_provider(): class Dummy(AbstractProvider): async def fetch_messages(self): - raise AssertionError("I should not be called") + msg = "I should not be called" + raise AssertionError(msg) async def confirm_message(self): - raise AssertionError("I should not be called") + msg = "I should not be called" + raise AssertionError(msg) async def message_not_processed(self): - raise AssertionError("I should not be called") + msg = "I should not be called" + raise AssertionError(msg) def stop(self): - raise AssertionError("I should not be called") + msg = "I should not be called" + raise AssertionError(msg) return Dummy() diff --git a/tests/ext/aws/conftest.py b/tests/ext/aws/conftest.py index 7c29de5..842323c 100644 --- a/tests/ext/aws/conftest.py +++ b/tests/ext/aws/conftest.py @@ -63,13 +63,11 @@ def boto_client_sqs(queue_url, sqs_message): @pytest.fixture def mock_boto_session_sqs(boto_client_sqs): - return mock.patch( - "loafer.ext.aws.bases.session.create_client", return_value=ClientContextCreator(boto_client_sqs) - ) + return mock.patch("loafer.ext.aws.bases.session.create_client", return_value=ClientContextCreator(boto_client_sqs)) @pytest.fixture -def boto_client_sns(sns_publish, sns_list_topics): +def boto_client_sns(sns_publish): mock_client = mock.Mock() mock_client.publish = mock.AsyncMock(return_value=sns_publish) mock_client.close = mock.AsyncMock() @@ -78,6 +76,4 @@ def boto_client_sns(sns_publish, sns_list_topics): @pytest.fixture def mock_boto_session_sns(boto_client_sns): - return mock.patch( - "loafer.ext.aws.bases.session.create_client", return_value=ClientContextCreator(boto_client_sns) - ) + return mock.patch("loafer.ext.aws.bases.session.create_client", return_value=ClientContextCreator(boto_client_sns)) diff --git a/tests/ext/aws/test_handlers.py b/tests/ext/aws/test_handlers.py index 90726e1..7fb533c 100644 --- a/tests/ext/aws/test_handlers.py +++ b/tests/ext/aws/test_handlers.py @@ -40,7 +40,7 @@ async def test_sqs_handler_publish_without_encoder(mock_boto_session_sqs, boto_c @pytest.mark.asyncio async def test_sqs_handler_publish_without_queue_name(): handler = SQSHandler() - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="missing queue_name attribute"): await handler.publish("wrong") @@ -92,7 +92,7 @@ async def test_sns_handler_publisher_without_encoder(mock_boto_session_sns, boto @pytest.mark.asyncio async def test_sns_handler_publish_without_topic(): handler = SNSHandler() - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="missing topic attribute"): await handler.publish("wrong") diff --git a/tests/ext/aws/test_providers.py b/tests/ext/aws/test_providers.py index 7177e13..a0811f5 100644 --- a/tests/ext/aws/test_providers.py +++ b/tests/ext/aws/test_providers.py @@ -22,9 +22,7 @@ async def test_confirm_message(mock_boto_session_sqs, boto_client_sqs): @pytest.mark.asyncio async def test_confirm_message_not_found(mock_boto_session_sqs, boto_client_sqs): - error = ClientError( - error_response={"ResponseMetadata": {"HTTPStatusCode": 404}}, operation_name="whatever" - ) + error = ClientError(error_response={"ResponseMetadata": {"HTTPStatusCode": 404}}, operation_name="whatever") boto_client_sqs.delete_message.side_effect = error with mock_boto_session_sqs: provider = SQSProvider("queue-name") @@ -39,9 +37,7 @@ async def test_confirm_message_not_found(mock_boto_session_sqs, boto_client_sqs) @pytest.mark.asyncio async def test_confirm_message_unknown_error(mock_boto_session_sqs, boto_client_sqs): - error = ClientError( - error_response={"ResponseMetadata": {"HTTPStatusCode": 400}}, operation_name="whatever" - ) + error = ClientError(error_response={"ResponseMetadata": {"HTTPStatusCode": 400}}, operation_name="whatever") boto_client_sqs.delete_message.side_effect = error with mock_boto_session_sqs: provider = SQSProvider("queue-name") @@ -130,7 +126,7 @@ async def test_backoff_factor_options(mock_boto_session_sqs, boto_client_sqs): with mock_boto_session_sqs: provider = SQSProvider("queue-name", options=options) - assert provider._backoff_factor == 1.5 + assert provider._backoff_factor == 1.5 # noqa: SLF001 await provider.fetch_messages() @@ -143,7 +139,7 @@ async def test_backoff_factor_options(mock_boto_session_sqs, boto_client_sqs): @pytest.mark.asyncio @pytest.mark.parametrize( - "options, expected", + ("options", "expected"), [ ({}, ["ApproximateReceiveCount"]), ({"AttributeNames": []}, ["ApproximateReceiveCount"]), @@ -151,9 +147,7 @@ async def test_backoff_factor_options(mock_boto_session_sqs, boto_client_sqs): ({"AttributeNames": ["All"]}, ["All"]), ], ) -async def test_backoff_factor_options_with_attributes_names( - mock_boto_session_sqs, boto_client_sqs, options, expected -): +async def test_backoff_factor_options_with_attributes_names(mock_boto_session_sqs, boto_client_sqs, options, expected): with mock_boto_session_sqs: provider = SQSProvider("queue-name", options={"BackoffFactor": 1.5, **options}) await provider.fetch_messages() @@ -165,7 +159,7 @@ async def test_backoff_factor_options_with_attributes_names( @pytest.mark.asyncio @pytest.mark.parametrize( - "visibility, backoff_multiplier, expected", + ("visibility", "backoff_multiplier", "expected"), [ (30, 1.5, 45), (30, 1.75, 52), @@ -202,12 +196,10 @@ async def test_fetch_messages_using_backoff_factor( @pytest.mark.asyncio @pytest.mark.parametrize( - "error,expectation", + ("error", "expectation"), [ ( - ClientError( - error_response={"Error": {"Code": "InvalidParameterValue"}}, operation_name="whatever" - ), + ClientError(error_response={"Error": {"Code": "InvalidParameterValue"}}, operation_name="whatever"), does_not_raise(), ), ( diff --git a/tests/ext/aws/test_routes.py b/tests/ext/aws/test_routes.py index e24d9cd..ef6d8f8 100644 --- a/tests/ext/aws/test_routes.py +++ b/tests/ext/aws/test_routes.py @@ -24,8 +24,8 @@ def test_sqs_route_keep_name(dummy_handler): def test_sqs_route_provider_options(dummy_handler): route = SQSRoute("what", {"use_ssl": False}, handler=dummy_handler, name="foobar") - assert "use_ssl" in route.provider._client_options - assert route.provider._client_options["use_ssl"] is False + assert "use_ssl" in route.provider._client_options # noqa: SLF001 + assert route.provider._client_options["use_ssl"] is False # noqa: SLF001 def test_sns_queue_route(dummy_handler): @@ -48,8 +48,6 @@ def test_sns_queue_route_keep_name(dummy_handler): def test_sns_queue_route_provider_options(dummy_handler): - route = SNSQueueRoute( - "what", provider_options={"region_name": "sa-east-1"}, handler=dummy_handler, name="foobar" - ) - assert "region_name" in route.provider._client_options - assert route.provider._client_options["region_name"] == "sa-east-1" + route = SNSQueueRoute("what", provider_options={"region_name": "sa-east-1"}, handler=dummy_handler, name="foobar") + assert "region_name" in route.provider._client_options # noqa: SLF001 + assert route.provider._client_options["region_name"] == "sa-east-1" # noqa: SLF001 diff --git a/tests/test_dispatchers.py b/tests/test_dispatchers.py index 2a58be8..b3b647d 100644 --- a/tests/test_dispatchers.py +++ b/tests/test_dispatchers.py @@ -19,16 +19,13 @@ def create_mock_route(messages): message_not_processed=mock.AsyncMock(), ) - message_translator = mock.Mock( - translate=mock.Mock(side_effect=[{"content": message} for message in messages]) - ) - route = mock.AsyncMock( + message_translator = mock.Mock(translate=mock.Mock(side_effect=[{"content": message} for message in messages])) + return mock.AsyncMock( provider=provider, handler=mock.AsyncMock(), message_translator=message_translator, spec=Route, ) - return route @pytest.fixture diff --git a/tests/test_managers.py b/tests/test_managers.py index 397ecc4..55cdfc9 100644 --- a/tests/test_managers.py +++ b/tests/test_managers.py @@ -59,8 +59,8 @@ def test_on_future_errors_cancelled(): def test_on_loop__stop(): manager = LoaferManager(routes=[]) manager.dispatcher = mock.Mock() - manager._future = mock.Mock() + manager._future = mock.Mock() # noqa: SLF001 manager.on_loop__stop() assert manager.dispatcher.stop.called - assert manager._future.cancel.called + assert manager._future.cancel.called # noqa: SLF001 diff --git a/tests/test_routes.py b/tests/test_routes.py index 6224417..5ecdbc1 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -52,10 +52,12 @@ def test_apply_message_translator_error(dummy_provider): translator = StringMessageTranslator() translator.translate = mock.Mock(return_value={"content": "", "metadata": {}}) route = Route(dummy_provider, mock.Mock(), message_translator=translator) - with pytest.raises(ValueError): + + with pytest.raises(ValueError, match="failed to translate"): route.apply_message_translator("message") - assert translator.translate.called - translator.translate.assert_called_once_with("message") + + assert translator.translate.called + translator.translate.assert_called_once_with("message") @pytest.mark.asyncio @@ -106,28 +108,28 @@ async def test_error_handler_coroutine(dummy_provider): @pytest.mark.asyncio async def test_handler_class_based(dummy_provider): - class handler: + class Handler: async def handle(self, *args, **kwargs): pass - handler = handler() + handler = Handler() route = Route(dummy_provider, handler=handler) assert route.handler == handler.handle @pytest.mark.asyncio async def test_handler_class_based_invalid(dummy_provider): - class handler: + class Handler: pass - handler = handler() - with pytest.raises(ValueError): + handler = Handler() + with pytest.raises(ValueError, match="handler must be a callable object"): Route(dummy_provider, handler=handler) @pytest.mark.asyncio async def test_handler_invalid(dummy_provider): - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="handler must be a callable object"): Route(dummy_provider, "invalid-handler") @@ -140,12 +142,12 @@ def test_route_stop(dummy_provider): def test_route_stop_with_handler_stop(dummy_provider): - class handler: + class Handler: def handle(self, *args): pass dummy_provider.stop = mock.Mock() - handler = handler() + handler = Handler() handler.stop = mock.Mock() route = Route(dummy_provider, handler) route.stop() diff --git a/tests/test_runners.py b/tests/test_runners.py index 08710e2..d52985c 100644 --- a/tests/test_runners.py +++ b/tests/test_runners.py @@ -59,8 +59,7 @@ def test_runner_prepare_stop_already_stopped(get_loop_mock): assert loop.stop.called is False -@mock.patch("loafer.runners.asyncio.get_event_loop") -def test_runner_stop_with_callback(loop_mock): +def test_runner_stop_with_callback(): callback = mock.Mock() runner = LoaferRunner(on_stop_callback=callback) @@ -71,7 +70,7 @@ def test_runner_stop_with_callback(loop_mock): def test_runner_stop_dont_raise_cancelled_error(): async def some_coro(): - raise asyncio.CancelledError() + raise asyncio.CancelledError runner = LoaferRunner() loop = runner.loop diff --git a/tests/test_utils.py b/tests/test_utils.py index 09e558e..1c89d7c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -4,7 +4,7 @@ @pytest.mark.parametrize( - "number_of_tries, backoff_factor, expected", + ("number_of_tries", "backoff_factor", "expected"), [ (0, 1.5, 1), (1, 1.5, 1.5),