From dd9e6843143e5d6a595c0c771d16a9e397396b4c Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Sat, 5 Sep 2026 18:37:38 +0530 Subject: [PATCH 1/2] Migrate linting and formatting to Ruff --- .github/workflows/main.yml | 2 +- .pre-commit-config.yaml | 10 +-- README.md | 5 +- algorithms_keeper/__main__.py | 13 ++- algorithms_keeper/api.py | 3 +- algorithms_keeper/event/commands.py | 4 +- algorithms_keeper/event/pull_request.py | 10 +-- algorithms_keeper/parser/files_parser.py | 3 +- algorithms_keeper/parser/python_parser.py | 3 +- algorithms_keeper/parser/record.py | 10 +-- .../parser/rules/naming_convention.py | 7 +- .../parser/rules/require_descriptive_name.py | 4 +- .../parser/rules/require_doctest.py | 5 +- algorithms_keeper/utils.py | 18 ++-- pyproject.toml | 20 +++-- tests/test_api.py | 15 ++-- tests/test_check_runs.py | 2 +- tests/test_commands.py | 5 +- tests/test_installations.py | 2 +- tests/test_main.py | 10 +-- tests/test_parser.py | 3 +- tests/test_pull_requests.py | 10 +-- tests/test_rules.py | 15 ++-- tests/test_utils.py | 85 ++++++++++--------- tests/utils.py | 44 +++++----- 25 files changed, 158 insertions(+), 150 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6b6b1ad..08eaac6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: run: uv sync --locked - name: Run tests run: uv run --locked pytest - - name: Run pre-commit + - name: Run pre-commit (Ruff lint, format, and other checks) run: uv run --locked pre-commit run --verbose --all-files --show-diff-on-failure # FIXME: mypy is failing due to missing types-* packages # - name: Run mypy diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a1b564a..093ee67 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,12 +14,8 @@ repos: additional_dependencies: - tomli - - repo: https://github.com/psf/black - rev: 23.7.0 - hooks: - - id: black - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.285 + rev: v0.16.6 hooks: - - id: ruff + - id: ruff-check + - id: ruff-format diff --git a/README.md b/README.md index 8288b12..bac9702 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # algorithms-keeper [![CI](https://github.com/TheAlgorithms/algorithms-keeper/actions/workflows/main.yml/badge.svg)](https://github.com/TheAlgorithms/algorithms-keeper/actions/workflows/main.yml) [![codecov](https://codecov.io/gh/TheAlgorithms/algorithms-keeper/branch/master/graph/badge.svg?token=QYAZ665UJL)](https://codecov.io/gh/TheAlgorithms/algorithms-keeper) -[![code style: black](https://img.shields.io/static/v1?label=code%20style&message=black&color=black)](https://github.com/psf/black) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://docs.astral.sh/ruff/) [![Checked with mypy](https://img.shields.io/static/v1?label=mypy&message=checked&color=2a6db2&labelColor=505050)](http://mypy-lang.org/) @@ -57,8 +57,11 @@ the project and run tests from the repository root: ```shell uv sync uv run pytest +uv run pre-commit run --all-files ``` +Ruff handles Python linting and formatting through the pre-commit hooks. + ## Logging Logging is done using the standard library logging module. All the API calls made by the bot are being logged at INFO level and `aiohttp.log.access_logger` is logging the POST requests made by GitHub for delivering the payload. Other minor events relevant to the repository is also being logged along with using the using [Sentry](https://sentry.io/). The logs can be viewed best using the following command ([_requires Heroku CLI_](https://devcenter.heroku.com/articles/heroku-cli#download-and-install)): ```shell diff --git a/algorithms_keeper/__main__.py b/algorithms_keeper/__main__.py index 3c50652..98878f6 100644 --- a/algorithms_keeper/__main__.py +++ b/algorithms_keeper/__main__.py @@ -1,10 +1,10 @@ import asyncio import logging import os -import sys -from datetime import datetime, timezone +from collections.abc import MutableMapping +from datetime import UTC, datetime from pathlib import Path -from typing import Any, MutableMapping +from typing import Any from aiohttp import ClientSession, web from cachetools import LRUCache @@ -17,8 +17,7 @@ # TODO(dhruvmanila): Remove this block when it's the default. # https://github.com/Instagram/LibCST/issues/285#issuecomment-1011427731 -if sys.version_info >= (3, 10): - os.environ["LIBCST_PARSER_TYPE"] = "native" +os.environ["LIBCST_PARSER_TYPE"] = "native" cache: MutableMapping[Any, Any] = LRUCache(maxsize=500) @@ -90,7 +89,7 @@ async def main(request: web.Request) -> web.Response: logger.info( "ratelimit=%s, time_remaining=%s", f"{gh.rate_limit.remaining}/{gh.rate_limit.limit}", - gh.rate_limit.reset_datetime - datetime.now(timezone.utc), + gh.rate_limit.reset_datetime - datetime.now(UTC), ) return web.Response(status=200) except Exception as err: @@ -103,4 +102,4 @@ async def main(request: web.Request) -> web.Response: app.add_routes(routes) # Heroku dynamically assigns the app a port, so we can't set the port to a fixed # number. Heroku adds the port to the env, so we need to pull it from there. - web.run_app(app, port=int(os.environ.get("PORT", 5000))) + web.run_app(app, port=int(os.environ.get("PORT", "5000"))) diff --git a/algorithms_keeper/api.py b/algorithms_keeper/api.py index b17ab88..bf38f3e 100644 --- a/algorithms_keeper/api.py +++ b/algorithms_keeper/api.py @@ -1,6 +1,7 @@ import logging import os -from typing import Any, Mapping, MutableMapping +from collections.abc import Mapping, MutableMapping +from typing import Any from aiohttp import ClientResponse from cachetools import TTLCache diff --git a/algorithms_keeper/event/commands.py b/algorithms_keeper/event/commands.py index 3a7a94b..a3be4eb 100644 --- a/algorithms_keeper/event/commands.py +++ b/algorithms_keeper/event/commands.py @@ -5,9 +5,11 @@ including the modified files. As we cannot post review comments on lines not part of the diff, this command only modify the labels accordingly. """ + import logging import re -from typing import Any, Pattern +from re import Pattern +from typing import Any from gidgethub import routing from gidgethub.sansio import Event diff --git a/algorithms_keeper/event/pull_request.py b/algorithms_keeper/event/pull_request.py index dc1e3c8..85d3964 100644 --- a/algorithms_keeper/event/pull_request.py +++ b/algorithms_keeper/event/pull_request.py @@ -19,10 +19,11 @@ "Awaiting changes" -> "Approved" [label="Review approves", color=green] } """ + import asyncio import logging import re -from typing import Any, Optional +from typing import Any from gidgethub import routing from gidgethub.sansio import Event @@ -37,6 +38,7 @@ PR_REVIEW_COMMENT, Label, ) +from algorithms_keeper.event.check_run import check_ci_status_and_label from algorithms_keeper.parser import PythonParser # To disable this check, set the constant to 0. @@ -50,7 +52,7 @@ async def update_stage_label( - gh: GitHubAPI, *, pull_request: dict[str, Any], next_label: Optional[str] = None + gh: GitHubAPI, *, pull_request: dict[str, Any], next_label: str | None = None ) -> None: """Update the stage label of the given pull request. @@ -266,8 +268,6 @@ async def check_ci_ready_for_review_pr( removed if the checks are passing or failing. Thus, we need to manually check it with respect to the latest commit on head. """ - from algorithms_keeper.event.check_run import check_ci_status_and_label - await check_ci_status_and_label(event, gh, *args, **kwargs) @@ -343,7 +343,7 @@ async def check_merge_status( pull_request = event.data["pull_request"] for retry_interval in range(MAX_RETRIES): - mergeable: Optional[bool] = pull_request["mergeable"] + mergeable: bool | None = pull_request["mergeable"] if mergeable is None: # We will use the iter value we get as our sleep period between the polls. # In the webhook payload, the mergeable status will always be ``None``, so diff --git a/algorithms_keeper/parser/files_parser.py b/algorithms_keeper/parser/files_parser.py index 4cb3f8e..ff5912c 100644 --- a/algorithms_keeper/parser/files_parser.py +++ b/algorithms_keeper/parser/files_parser.py @@ -1,5 +1,6 @@ import logging -from typing import Any, Collection, Iterable, Mapping +from collections.abc import Collection, Iterable, Mapping +from typing import Any from algorithms_keeper.constants import Label from algorithms_keeper.utils import File diff --git a/algorithms_keeper/parser/python_parser.py b/algorithms_keeper/parser/python_parser.py index 5fcc9b9..a720bd5 100644 --- a/algorithms_keeper/parser/python_parser.py +++ b/algorithms_keeper/parser/python_parser.py @@ -1,5 +1,6 @@ import logging -from typing import Any, Iterable, Iterator, Mapping +from collections.abc import Iterable, Iterator, Mapping +from typing import Any from fixit import Config, LintRule from fixit.engine import LintRunner diff --git a/algorithms_keeper/parser/record.py b/algorithms_keeper/parser/record.py index 65771cf..6807a98 100644 --- a/algorithms_keeper/parser/record.py +++ b/algorithms_keeper/parser/record.py @@ -1,5 +1,7 @@ +import traceback +from collections.abc import Collection from dataclasses import asdict, dataclass, field -from typing import Any, Collection, Union +from typing import Any from fixit import LintViolation from libcst import ParserSyntaxError @@ -73,12 +75,8 @@ def add_comments(self, reports: Collection[LintViolation], filepath: str) -> Non ReviewComment(report.message, filepath, report.range.start.line) ) - def add_error( - self, exc: Union[SyntaxError, ParserSyntaxError], filepath: str - ) -> None: + def add_error(self, exc: SyntaxError | ParserSyntaxError, filepath: str) -> None: """Add any exception faced while parsing the source code.""" - import traceback - message = traceback.format_exc(limit=1) # It seems that ``ParserSyntaxError`` is not a subclass of ``SyntaxError``, # the same information is stored under a different attribute. There is no diff --git a/algorithms_keeper/parser/rules/naming_convention.py b/algorithms_keeper/parser/rules/naming_convention.py index adb7ade..198474b 100644 --- a/algorithms_keeper/parser/rules/naming_convention.py +++ b/algorithms_keeper/parser/rules/naming_convention.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Collection, Optional +from typing import TYPE_CHECKING import libcst as cst import libcst.matchers as m @@ -8,6 +8,9 @@ from algorithms_keeper.parser import lint_rule +if TYPE_CHECKING: + from collections.abc import Collection + INVALID_CAMEL_CASE_NAME_COMMENT: str = ( "Class names should follow the [`CamelCase`]" "(https://en.wikipedia.org/wiki/Camel_case) naming convention. " @@ -116,7 +119,7 @@ def __init__(self) -> None: self._assigntarget_counter: int = 0 def visit_Assign(self, node: cst.Assign) -> None: - metadata: Optional[Collection[QualifiedName]] = self.get_metadata( + metadata: Collection[QualifiedName] | None = self.get_metadata( QualifiedNameProvider, node.value, None ) diff --git a/algorithms_keeper/parser/rules/require_descriptive_name.py b/algorithms_keeper/parser/rules/require_descriptive_name.py index 1ead7c9..1a634f2 100644 --- a/algorithms_keeper/parser/rules/require_descriptive_name.py +++ b/algorithms_keeper/parser/rules/require_descriptive_name.py @@ -1,5 +1,3 @@ -from typing import Union - import libcst as cst from fixit import Invalid, Valid @@ -75,7 +73,7 @@ def visit_Param(self, node: cst.Param) -> None: self._validate_name_length(node, "parameter") def _validate_name_length( - self, node: Union[cst.ClassDef, cst.FunctionDef, cst.Param], nodetype: str + self, node: cst.ClassDef | cst.FunctionDef | cst.Param, nodetype: str ) -> None: nodename = node.name.value if len(nodename) == 1: diff --git a/algorithms_keeper/parser/rules/require_doctest.py b/algorithms_keeper/parser/rules/require_doctest.py index 9dbf5d8..0f1da4d 100644 --- a/algorithms_keeper/parser/rules/require_doctest.py +++ b/algorithms_keeper/parser/rules/require_doctest.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Union import libcst as cst import libcst.matchers as m @@ -238,9 +237,7 @@ def visit_FunctionDef(self, node: cst.FunctionDef) -> None: MISSING_DOCTEST.format(filepath=self._file_path, nodename=nodename), ) - def _has_doctest( - self, node: Union[cst.Module, cst.ClassDef, cst.FunctionDef] - ) -> bool: + def _has_doctest(self, node: cst.Module | cst.ClassDef | cst.FunctionDef) -> bool: """Check whether the given node contains doctests. If the ``_skip_doctest`` attribute is ``True``, the function will by default diff --git a/algorithms_keeper/utils.py b/algorithms_keeper/utils.py index 13141f8..2ace1a6 100644 --- a/algorithms_keeper/utils.py +++ b/algorithms_keeper/utils.py @@ -12,11 +12,13 @@ maintain consistency throughout the module and improve readability in files that uses all the given functions. """ + import urllib.parse from base64 import b64decode +from collections.abc import Mapping from dataclasses import dataclass from pathlib import Path -from typing import Any, Mapping, Optional, Union +from typing import Any from algorithms_keeper.api import GitHubAPI from algorithms_keeper.constants import PR_REVIEW_BODY @@ -43,9 +45,7 @@ class File: status: str -async def get_pr_for_commit( - gh: GitHubAPI, *, sha: str, repository: str -) -> Optional[Any]: +async def get_pr_for_commit(gh: GitHubAPI, *, sha: str, repository: str) -> Any | None: """Return the issue object, relative to the pull request, for the given SHA of a commit. @@ -74,7 +74,7 @@ async def get_check_runs_for_commit(gh: GitHubAPI, *, sha: str, repository: str) async def add_label_to_pr_or_issue( gh: GitHubAPI, *, - label: Union[str, list[str]], + label: str | list[str], pr_or_issue: Mapping[str, Any], ) -> None: """Add the given label(s) to the pull request or issue provided. @@ -100,7 +100,7 @@ async def add_label_to_pr_or_issue( async def remove_label_from_pr_or_issue( gh: GitHubAPI, *, - label: Union[str, list[str]], + label: str | list[str], pr_or_issue: Mapping[str, Any], ) -> None: """Remove the given label(s) from pull request or issue provided. @@ -119,8 +119,8 @@ async def remove_label_from_pr_or_issue( label_list = [label] if isinstance(label, str) else label # We can only remove labels one at a time or all (every label in the pull request # or issue) at once. - for label in label_list: - parse_label = urllib.parse.quote(label) + for label_name in label_list: + parse_label = urllib.parse.quote(label_name) await gh.delete( f"{labels_url}/{parse_label}", oauth_token=await gh.access_token, @@ -162,7 +162,7 @@ async def close_pr_or_issue( *, comment: str, pr_or_issue: Mapping[str, Any], - label: Optional[Union[str, list[str]]] = None, + label: str | list[str] | None = None, ) -> None: """Close the given pull request or issue with a comment and an optional label. If it is a pull request then dismiss all the requested reviews from it as well. diff --git a/pyproject.toml b/pyproject.toml index 71c498b..c06e0d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,10 @@ disallow_untyped_defs = false check_untyped_defs = false [tool.ruff] +line-length = 88 +target-version = "py311" + +[tool.ruff.lint] select = [ "A", # flake8-builtins "AIR", # Airflow @@ -53,7 +57,6 @@ select = [ "BLE", # flake8-blind-except "C4", # flake8-comprehensions "C90", # McCabe cyclomatic complexity - "CPY", # flake8-copyright "DJ", # flake8-django "DTZ", # flake8-datetimez "E", # pycodestyle @@ -84,7 +87,7 @@ select = [ "SLF", # flake8-self "SLOT", # flake8-slots "T10", # flake8-debugger - "TCH", # flake8-type-checking + "TC", # flake8-type-checking "TID", # flake8-tidy-imports "UP", # pyupgrade "W", # pycodestyle @@ -92,6 +95,7 @@ select = [ # "ANN", # flake8-annotations # "ARG", # flake8-unused-arguments # "COM", # flake8-commas + # "CPY", # flake8-copyright (no per-file copyright notices) # "D", # pydocstyle # "ERA", # eradicate # "FA", # flake8-future-annotations @@ -101,21 +105,19 @@ select = [ # "RET", # flake8-return # "T20", # flake8-print # "TD", # flake8-todos - # "TRY", s# tryceratops + # "TRY", # tryceratops ] -ignore = ["N802", "PGH003", "RUF012", "SLF001"] -line-length = 88 -target-version = "py38" +ignore = ["E111", "E114", "E117", "N802", "PGH003", "RUF012", "SLF001", "W191"] -[tool.ruff.mccabe] +[tool.ruff.lint.mccabe] max-complexity = 13 -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "tests/*" = ["PT006", "PT007", "S101"] "tests/test_commands.py" = ["B008"] "tests/test_pull_requests.py" = ["B008"] -[tool.ruff.pylint] +[tool.ruff.lint.pylint] allow-magic-value-types = ["bytes", "int", "str"] max-args = 6 # Recommended: 5 diff --git a/tests/test_api.py b/tests/test_api.py index f56cbdc..80abf4d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,4 +1,5 @@ -from typing import Any, AsyncGenerator, Awaitable, Callable, Dict +from collections.abc import AsyncGenerator, Awaitable, Callable +from typing import Any import aiohttp import pytest @@ -12,7 +13,7 @@ from .utils import number, token -async def mock_return(*args: Any, **kwargs: Any) -> Dict[str, str]: +async def mock_return(*args: Any, **kwargs: Any) -> dict[str, str]: return {"token": token} @@ -24,7 +25,7 @@ async def github_api() -> AsyncGenerator[GitHubAPI, None]: assert session.closed is True -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_initialization() -> None: async with aiohttp.ClientSession() as session: github_api = GitHubAPI(number, session, "algorithms-keeper") @@ -36,7 +37,7 @@ async def test_initialization() -> None: assert github_api.requester == "algorithms-keeper" -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_access_token( github_api: GitHubAPI, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -52,7 +53,7 @@ async def test_access_token( assert cached_token == token -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_request_with_local_server( github_api: GitHubAPI, aiohttp_server: Callable[..., Awaitable[TestServer]] ) -> None: @@ -73,11 +74,11 @@ async def handler(request: web.Request) -> web.Response: assert body == b"response body" -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_headers_and_log(github_api: GitHubAPI) -> None: request_headers = sansio.create_headers("algorithms-keeper") resp = await github_api._request( "GET", "https://api.github.com/rate_limit", request_headers ) - data, rate_limit, _ = sansio.decipher_response(*resp) + data, _, _ = sansio.decipher_response(*resp) assert "rate" in data diff --git a/tests/test_check_runs.py b/tests/test_check_runs.py index 70b6d35..a366673 100644 --- a/tests/test_check_runs.py +++ b/tests/test_check_runs.py @@ -20,7 +20,7 @@ # Reminder: ``Event.delivery_id`` is used as a short description for the respective # test case and as a way to id the specific test case in the parametrized group. -@pytest.mark.asyncio() +@pytest.mark.asyncio @pytest.mark.parametrize( "event, gh, expected", ( diff --git a/tests/test_commands.py b/tests/test_commands.py index 9fdde9f..016a905 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,4 +1,5 @@ -from typing import Any, Generator +from collections.abc import Generator +from typing import Any import pytest from gidgethub.sansio import Event @@ -82,7 +83,7 @@ def test_command_regex_match(text: str, group: str) -> None: # Reminder: ``Event.delivery_id`` is used as a short description for the respective # test case and as a way to id the specific test case in the parametrized group. -@pytest.mark.asyncio() +@pytest.mark.asyncio @pytest.mark.parametrize( "event, gh, expected", ( diff --git a/tests/test_installations.py b/tests/test_installations.py index 5f46216..e33286c 100644 --- a/tests/test_installations.py +++ b/tests/test_installations.py @@ -20,7 +20,7 @@ # Reminder: ``Event.delivery_id`` is used as a short description for the respective # test case and as a way to id the specific test case in the parametrized group. -@pytest.mark.asyncio() +@pytest.mark.asyncio @pytest.mark.parametrize( "event, gh, expected", ( diff --git a/tests/test_main.py b/tests/test_main.py index 4bf71ee..28fc90b 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -16,7 +16,7 @@ async def client(aiohttp_client): # type: ignore return await aiohttp_client(app) -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_ping(client): # type: ignore headers = {"X-GitHub-Event": "ping", "X-GitHub-Delivery": "1234"} data = {"zen": "testing is good"} @@ -25,7 +25,7 @@ async def test_ping(client): # type: ignore assert await response.text() == "pong" -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_failure(client): # type: ignore # Even in the face of an exception, the server should not crash. # Missing key headers. @@ -33,7 +33,7 @@ async def test_failure(client): # type: ignore assert response.status == 500 -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_success(client): # type: ignore headers = {"X-GitHub-Event": "project", "X-GitHub-Delivery": "1234"} # Sending a payload that shouldn't trigger any networking, but no errors @@ -43,7 +43,7 @@ async def test_success(client): # type: ignore assert response.status == 200 -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_index(client): # type: ignore response = await client.get("/") assert response.status == 200 @@ -51,7 +51,7 @@ async def test_index(client): # type: ignore assert "algorithms-keeper" in (await response.text()) -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_health(client): # type: ignore response = await client.get("/health") assert response.status == 200 diff --git a/tests/test_parser.py b/tests/test_parser.py index 584791c..7ae145b 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import List import pytest @@ -194,7 +193,7 @@ def test_combinations( monkeypatch: pytest.MonkeyPatch, filename: str, expected: int, - labels: List[str], + labels: list[str], add_count: int, remove_count: int, ) -> None: diff --git a/tests/test_pull_requests.py b/tests/test_pull_requests.py index 67686f4..3fe2a70 100644 --- a/tests/test_pull_requests.py +++ b/tests/test_pull_requests.py @@ -1,4 +1,5 @@ -from typing import Any, Generator +from collections.abc import Generator +from typing import Any from urllib.parse import quote import pytest @@ -6,6 +7,7 @@ from algorithms_keeper import utils from algorithms_keeper.constants import Label +from algorithms_keeper.event import pull_request from algorithms_keeper.event.pull_request import pull_request_router from .test_parser import get_source @@ -66,7 +68,7 @@ async def mock_get_file_content(*args: Any, **kwargs: Any) -> bytes: monkeypatch.undo() -@pytest.mark.asyncio() +@pytest.mark.asyncio @pytest.mark.parametrize( "event, gh, expected", # Pull request opened by the user, the bot found that the user has number of @@ -166,8 +168,6 @@ async def test_max_pr_by_user( # - The value is 0, which signals to disable the check. # We cannot rely on the actual constant which could change every now and then. So, # we will test the only two cases with monkeypatch. - from algorithms_keeper.event import pull_request - if event.delivery_id == MAX_PR_TEST_ENABLED_ID: monkeypatch.setattr(pull_request, "MAX_PR_PER_USER", MAX_PR_TEST_NUMBER) else: @@ -176,7 +176,7 @@ async def test_max_pr_by_user( assert gh == expected -@pytest.mark.asyncio() +@pytest.mark.asyncio @pytest.mark.parametrize( "event, gh, expected", ( diff --git a/tests/test_rules.py b/tests/test_rules.py index c778076..b406c31 100644 --- a/tests/test_rules.py +++ b/tests/test_rules.py @@ -1,6 +1,5 @@ import textwrap from pathlib import Path -from typing import List, Optional, Tuple, Type, Union import pytest from fixit import Config, Invalid, LintRule, Valid @@ -14,10 +13,10 @@ UseFstringRule, ) -GenTestCaseType = Tuple[Type[LintRule], Union[Valid, Invalid], str] +GenTestCaseType = tuple[type[LintRule], Valid | Invalid, str] # Test every custom rule against its embedded examples. -CUSTOM_RULES: set[Type[LintRule]] = { +CUSTOM_RULES: set[type[LintRule]] = { NamingConventionRule, RequireDoctestRule, RequireDescriptiveNameRule, @@ -47,10 +46,10 @@ def _dedent(src: str) -> str: return textwrap.dedent(src) -def _gen_all_test_cases(rules: set[Type[LintRule]]) -> List[GenTestCaseType]: +def _gen_all_test_cases(rules: set[type[LintRule]]) -> list[GenTestCaseType]: """Generate all the test cases for the provided rules.""" - cases: Optional[List[Union[Valid, Invalid]]] - all_cases: List[GenTestCaseType] = [] + cases: list[Valid | Invalid] | None + all_cases: list[GenTestCaseType] = [] for rule in rules: if not issubclass(rule, LintRule): continue @@ -67,8 +66,8 @@ def _gen_all_test_cases(rules: set[Type[LintRule]]) -> List[GenTestCaseType]: ids=_parametrized_id, ) def test_rules( - rule: Type[LintRule], - test_case: Union[Valid, Invalid], + rule: type[LintRule], + test_case: Valid | Invalid, test_case_id: str, ) -> None: """Test all the rules with the generated test cases. diff --git a/tests/test_utils.py b/tests/test_utils.py index ba9f0d7..51ea632 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,11 +1,10 @@ import urllib.parse from pathlib import Path -from typing import Dict, cast +from typing import TYPE_CHECKING, cast import pytest from algorithms_keeper import utils -from algorithms_keeper.api import GitHubAPI from algorithms_keeper.constants import Label from .utils import ( @@ -30,8 +29,11 @@ user, ) +if TYPE_CHECKING: + from algorithms_keeper.api import GitHubAPI -@pytest.mark.asyncio() + +@pytest.mark.asyncio async def test_get_issue_for_commit() -> None: getitem = { search_url: { @@ -41,7 +43,7 @@ async def test_get_issue_for_commit() -> None: } gh = MockGitHubAPI(getitem=getitem) result = await utils.get_pr_for_commit( - cast(GitHubAPI, gh), sha=sha, repository=repository + cast("GitHubAPI", gh), sha=sha, repository=repository ) assert search_url in gh.getitem_url assert result is not None @@ -49,7 +51,7 @@ async def test_get_issue_for_commit() -> None: assert result["state"] == "open" -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_get_issue_for_commit_not_found() -> None: getitem = { search_url: { @@ -59,13 +61,13 @@ async def test_get_issue_for_commit_not_found() -> None: } gh = MockGitHubAPI(getitem=getitem) result = await utils.get_pr_for_commit( - cast(GitHubAPI, gh), sha=sha, repository=repository + cast("GitHubAPI", gh), sha=sha, repository=repository ) assert search_url in gh.getitem_url assert result is None -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_get_check_runs_for_commit() -> None: getitem = { check_run_url: { @@ -78,7 +80,7 @@ async def test_get_check_runs_for_commit() -> None: } gh = MockGitHubAPI(getitem=getitem) result = await utils.get_check_runs_for_commit( - cast(GitHubAPI, gh), sha=sha, repository=repository + cast("GitHubAPI", gh), sha=sha, repository=repository ) assert check_run_url in gh.getitem_url assert result["total_count"] == 2 @@ -89,26 +91,26 @@ async def test_get_check_runs_for_commit() -> None: assert {check_run["status"] for check_run in result["check_runs"]} == {"completed"} -@pytest.mark.asyncio() +@pytest.mark.asyncio @pytest.mark.parametrize( "pr_or_issue", [{"issue_url": issue_url}, {"labels_url": labels_url}], ) -async def test_add_label_to_pr_or_issue(pr_or_issue: Dict[str, str]) -> None: +async def test_add_label_to_pr_or_issue(pr_or_issue: dict[str, str]) -> None: gh = MockGitHubAPI() await utils.add_label_to_pr_or_issue( - cast(GitHubAPI, gh), label=Label.FAILED_TEST, pr_or_issue=pr_or_issue + cast("GitHubAPI", gh), label=Label.FAILED_TEST, pr_or_issue=pr_or_issue ) assert labels_url in gh.post_url assert {"labels": [Label.FAILED_TEST]} in gh.post_data -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_add_multiple_labels() -> None: pr_or_issue = {"number": number, "issue_url": issue_url} gh = MockGitHubAPI() await utils.add_label_to_pr_or_issue( - cast(GitHubAPI, gh), + cast("GitHubAPI", gh), label=[Label.TYPE_HINT, Label.REVIEW], pr_or_issue=pr_or_issue, ) @@ -116,28 +118,28 @@ async def test_add_multiple_labels() -> None: assert {"labels": [Label.TYPE_HINT, Label.REVIEW]} in gh.post_data -@pytest.mark.asyncio() +@pytest.mark.asyncio @pytest.mark.parametrize( "pr_or_issue", [{"issue_url": issue_url}, {"labels_url": labels_url}], ) -async def test_remove_label_from_pr_or_issue(pr_or_issue: Dict[str, str]) -> None: +async def test_remove_label_from_pr_or_issue(pr_or_issue: dict[str, str]) -> None: parse_label = urllib.parse.quote(Label.FAILED_TEST) gh = MockGitHubAPI() await utils.remove_label_from_pr_or_issue( - cast(GitHubAPI, gh), label=Label.FAILED_TEST, pr_or_issue=pr_or_issue + cast("GitHubAPI", gh), label=Label.FAILED_TEST, pr_or_issue=pr_or_issue ) assert f"{labels_url}/{parse_label}" in gh.delete_url -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_remove_multiple_labels() -> None: parse_label1 = urllib.parse.quote(Label.TYPE_HINT) parse_label2 = urllib.parse.quote(Label.REVIEW) pr_or_issue = {"issue_url": issue_url} gh = MockGitHubAPI() await utils.remove_label_from_pr_or_issue( - cast(GitHubAPI, gh), + cast("GitHubAPI", gh), label=[Label.TYPE_HINT, Label.REVIEW], pr_or_issue=pr_or_issue, ) @@ -145,7 +147,7 @@ async def test_remove_multiple_labels() -> None: assert f"{labels_url}/{parse_label2}" in gh.delete_url -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_get_user_open_pr_numbers() -> None: getiter = { pr_user_search_url: { @@ -155,25 +157,25 @@ async def test_get_user_open_pr_numbers() -> None: } gh = MockGitHubAPI(getiter=getiter) result = await utils.get_user_open_pr_numbers( - cast(GitHubAPI, gh), repository=repository, user_login=user + cast("GitHubAPI", gh), repository=repository, user_login=user ) assert result == [1, 2, 3] assert gh.getiter_url[0] == pr_user_search_url -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_add_comment_to_pr_or_issue() -> None: # PR and issue both have `comments_url` key. pr_or_issue = {"number": number, "comments_url": comments_url} gh = MockGitHubAPI() await utils.add_comment_to_pr_or_issue( - cast(GitHubAPI, gh), comment=comment, pr_or_issue=pr_or_issue + cast("GitHubAPI", gh), comment=comment, pr_or_issue=pr_or_issue ) assert comments_url in gh.post_url assert {"body": comment} in gh.post_data -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_close_pr_no_reviewers() -> None: pull_request = { "url": pr_url, @@ -182,7 +184,7 @@ async def test_close_pr_no_reviewers() -> None: } gh = MockGitHubAPI() await utils.close_pr_or_issue( - cast(GitHubAPI, gh), comment=comment, pr_or_issue=pull_request + cast("GitHubAPI", gh), comment=comment, pr_or_issue=pull_request ) assert comments_url in gh.post_url assert {"body": comment} in gh.post_data @@ -192,7 +194,7 @@ async def test_close_pr_no_reviewers() -> None: assert gh.delete_data == [] -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_close_pr_with_reviewers() -> None: pull_request = { "url": pr_url, @@ -201,7 +203,7 @@ async def test_close_pr_with_reviewers() -> None: } gh = MockGitHubAPI() await utils.close_pr_or_issue( - cast(GitHubAPI, gh), comment=comment, pr_or_issue=pull_request + cast("GitHubAPI", gh), comment=comment, pr_or_issue=pull_request ) assert comments_url in gh.post_url assert {"body": comment} in gh.post_data @@ -211,13 +213,13 @@ async def test_close_pr_with_reviewers() -> None: assert {"reviewers": ["test1", "test2"]} in gh.delete_data -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_close_issue() -> None: # Issues don't have `requested_reviewers` field. issue = {"url": issue_url, "comments_url": comments_url} gh = MockGitHubAPI() await utils.close_pr_or_issue( - cast(GitHubAPI, gh), comment=comment, pr_or_issue=issue + cast("GitHubAPI", gh), comment=comment, pr_or_issue=issue ) assert comments_url in gh.post_url assert {"body": comment} in gh.post_data @@ -226,7 +228,7 @@ async def test_close_issue() -> None: assert gh.delete_url == [] -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_close_pr_or_issue_with_label() -> None: # PRs don't have `labels_url` attribute. pull_request = { @@ -237,7 +239,10 @@ async def test_close_pr_or_issue_with_label() -> None: } gh = MockGitHubAPI() await utils.close_pr_or_issue( - cast(GitHubAPI, gh), comment=comment, pr_or_issue=pull_request, label="invalid" + cast("GitHubAPI", gh), + comment=comment, + pr_or_issue=pull_request, + label="invalid", ) assert comments_url in gh.post_url assert {"body": comment} in gh.post_data @@ -248,7 +253,7 @@ async def test_close_pr_or_issue_with_label() -> None: assert gh.delete_url == [] -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_get_pr_files() -> None: getiter = { files_url: [ @@ -260,13 +265,13 @@ async def test_get_pr_files() -> None: } pull_request = {"url": pr_url} gh = MockGitHubAPI(getiter=getiter) - result = await utils.get_pr_files(cast(GitHubAPI, gh), pull_request=pull_request) + result = await utils.get_pr_files(cast("GitHubAPI", gh), pull_request=pull_request) assert len(result) == 4 assert [r.name for r in result] == ["t1.py", "t2.py", "t3.py", "t4.py"] assert files_url in gh.getiter_url -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_get_file_content() -> None: getitem = { contents_url: { @@ -282,7 +287,7 @@ async def test_get_file_content() -> None: } gh = MockGitHubAPI(getitem=getitem) result = await utils.get_file_content( - cast(GitHubAPI, gh), + cast("GitHubAPI", gh), file=utils.File("test.py", Path("test.py"), contents_url, "added"), ) assert result == ( @@ -294,31 +299,31 @@ async def test_get_file_content() -> None: assert contents_url in gh.getitem_url -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_create_pr_review() -> None: pull_request = {"url": pr_url, "head": {"sha": sha}} gh = MockGitHubAPI() await utils.create_pr_review( - cast(GitHubAPI, gh), pull_request=pull_request, comments=[{"body": "test"}] + cast("GitHubAPI", gh), pull_request=pull_request, comments=[{"body": "test"}] ) assert review_url in gh.post_url assert gh.post_data[0]["event"] == "COMMENT" -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_add_reaction() -> None: comment = {"url": comment_url} gh = MockGitHubAPI() - await utils.add_reaction(cast(GitHubAPI, gh), reaction="+1", comment=comment) + await utils.add_reaction(cast("GitHubAPI", gh), reaction="+1", comment=comment) assert reactions_url in gh.post_url assert {"content": "+1"} in gh.post_data -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_get_pr_for_issue() -> None: getitem = {pr_url: None} issue = {"pull_request": {"url": pr_url}} gh = MockGitHubAPI(getitem=getitem) - result = await utils.get_pr_for_issue(cast(GitHubAPI, gh), issue=issue) + result = await utils.get_pr_for_issue(cast("GitHubAPI", gh), issue=issue) assert result is None assert pr_url in gh.getitem_url diff --git a/tests/utils.py b/tests/utils.py index dc01984..f092cb6 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,5 +1,6 @@ +from collections.abc import AsyncGenerator from dataclasses import dataclass, field, fields -from typing import Any, AsyncGenerator, Dict, List, Optional +from typing import Any from gidgethub.sansio import Event @@ -15,17 +16,18 @@ def parametrize_id(obj: object) -> str: @dataclass(repr=False, eq=False, frozen=True) class ExpectedData: - getitem_url: List[str] = field(default_factory=list) - getiter_url: List[str] = field(default_factory=list) - post_url: List[str] = field(default_factory=list) - post_data: List[Dict[str, Any]] = field(default_factory=list) - patch_url: List[str] = field(default_factory=list) - patch_data: List[Dict[str, Any]] = field(default_factory=list) - delete_url: List[str] = field(default_factory=list) - delete_data: List[Dict[str, Any]] = field(default_factory=list) + getitem_url: list[str] = field(default_factory=list) + getiter_url: list[str] = field(default_factory=list) + post_url: list[str] = field(default_factory=list) + post_data: list[dict[str, Any]] = field(default_factory=list) + patch_url: list[str] = field(default_factory=list) + patch_data: list[dict[str, Any]] = field(default_factory=list) + delete_url: list[str] = field(default_factory=list) + delete_data: list[dict[str, Any]] = field(default_factory=list) -class MockGitHubAPI: +# Call history is mutable, so the mock is intentionally unhashable. +class MockGitHubAPI: # noqa: PLW1641 """Mocked GitHubAPI object. Arguments: @@ -58,21 +60,21 @@ class MockGitHubAPI: def __init__( self, *, - getitem: Optional[Dict[str, Any]] = None, - getiter: Optional[Dict[str, Any]] = None, - post: Optional[Dict[str, Any]] = None, + getitem: dict[str, Any] | None = None, + getiter: dict[str, Any] | None = None, + post: dict[str, Any] | None = None, ) -> None: self._getitem_return = getitem self._getiter_return = getiter self._post_return = post - self.getitem_url: List[str] = [] - self.getiter_url: List[str] = [] - self.post_url: List[str] = [] - self.post_data: List[Dict[str, Any]] = [] - self.patch_url: List[str] = [] - self.patch_data: List[Dict[str, Any]] = [] - self.delete_url: List[str] = [] - self.delete_data: List[Dict[str, Any]] = [] + self.getitem_url: list[str] = [] + self.getiter_url: list[str] = [] + self.post_url: list[str] = [] + self.post_data: list[dict[str, Any]] = [] + self.patch_url: list[str] = [] + self.patch_data: list[dict[str, Any]] = [] + self.delete_url: list[str] = [] + self.delete_data: list[dict[str, Any]] = [] @property async def access_token(self) -> str: From a4b422d4deb8edd225007bd8636da96cad1b2644 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Sat, 5 Sep 2026 18:44:31 +0530 Subject: [PATCH 2/2] Prefer future annotations in Ruff checks --- .github/workflows/main.yml | 2 +- algorithms_keeper/__main__.py | 8 ++++++-- algorithms_keeper/api.py | 11 ++++++++--- algorithms_keeper/event/check_run.py | 11 ++++++++--- algorithms_keeper/event/commands.py | 11 ++++++++--- algorithms_keeper/event/installation.py | 11 ++++++++--- algorithms_keeper/event/pull_request.py | 11 ++++++++--- algorithms_keeper/parser/files_parser.py | 11 ++++++++--- algorithms_keeper/parser/lint_rule.py | 8 +++++++- algorithms_keeper/parser/python_parser.py | 11 ++++++++--- algorithms_keeper/parser/record.py | 14 +++++++++----- .../parser/rules/require_descriptive_name.py | 8 +++++++- .../parser/rules/require_type_hint.py | 8 +++++++- algorithms_keeper/utils.py | 11 ++++++++--- pyproject.toml | 1 + tests/test_api.py | 11 ++++++++--- tests/test_commands.py | 8 ++++++-- tests/test_pull_requests.py | 8 ++++++-- tests/test_utils.py | 2 ++ tests/utils.py | 8 ++++++-- 20 files changed, 130 insertions(+), 44 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 08eaac6..ccf8f9e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: run: uv sync --locked - name: Run tests run: uv run --locked pytest - - name: Run pre-commit (Ruff lint, format, and other checks) + - name: Run pre-commit checks run: uv run --locked pre-commit run --verbose --all-files --show-diff-on-failure # FIXME: mypy is failing due to missing types-* packages # - name: Run mypy diff --git a/algorithms_keeper/__main__.py b/algorithms_keeper/__main__.py index 98878f6..fba2556 100644 --- a/algorithms_keeper/__main__.py +++ b/algorithms_keeper/__main__.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import asyncio import logging import os -from collections.abc import MutableMapping from datetime import UTC, datetime from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any from aiohttp import ClientSession, web from cachetools import LRUCache @@ -15,6 +16,9 @@ from algorithms_keeper.api import GitHubAPI from algorithms_keeper.event import main_router +if TYPE_CHECKING: + from collections.abc import MutableMapping + # TODO(dhruvmanila): Remove this block when it's the default. # https://github.com/Instagram/LibCST/issues/285#issuecomment-1011427731 os.environ["LIBCST_PARSER_TYPE"] = "native" diff --git a/algorithms_keeper/api.py b/algorithms_keeper/api.py index bf38f3e..ec6205e 100644 --- a/algorithms_keeper/api.py +++ b/algorithms_keeper/api.py @@ -1,14 +1,19 @@ +from __future__ import annotations + import logging import os -from collections.abc import Mapping, MutableMapping -from typing import Any +from typing import TYPE_CHECKING, Any -from aiohttp import ClientResponse from cachetools import TTLCache from gidgethub import apps from gidgethub.abc import UTF_8_CHARSET from gidgethub.aiohttp import GitHubAPI as BaseGitHubAPI +if TYPE_CHECKING: + from collections.abc import Mapping, MutableMapping + + from aiohttp import ClientResponse + # Timed token_cache for installation access token (1 minute less than an hour) token_cache: MutableMapping[int, str] = TTLCache(maxsize=10, ttl=1 * 59 * 60) diff --git a/algorithms_keeper/event/check_run.py b/algorithms_keeper/event/check_run.py index a3093f9..abc143b 100644 --- a/algorithms_keeper/event/check_run.py +++ b/algorithms_keeper/event/check_run.py @@ -1,13 +1,18 @@ +from __future__ import annotations + import logging -from typing import Any +from typing import TYPE_CHECKING, Any from gidgethub import routing -from gidgethub.sansio import Event from algorithms_keeper import utils -from algorithms_keeper.api import GitHubAPI from algorithms_keeper.constants import Label +if TYPE_CHECKING: + from gidgethub.sansio import Event + + from algorithms_keeper.api import GitHubAPI + check_run_router = routing.Router() logger = logging.getLogger(__package__) diff --git a/algorithms_keeper/event/commands.py b/algorithms_keeper/event/commands.py index a3be4eb..b217f9f 100644 --- a/algorithms_keeper/event/commands.py +++ b/algorithms_keeper/event/commands.py @@ -6,18 +6,23 @@ the diff, this command only modify the labels accordingly. """ +from __future__ import annotations + import logging import re from re import Pattern -from typing import Any +from typing import TYPE_CHECKING, Any from gidgethub import routing -from gidgethub.sansio import Event from algorithms_keeper import utils -from algorithms_keeper.api import GitHubAPI from algorithms_keeper.event.pull_request import check_pr_files +if TYPE_CHECKING: + from gidgethub.sansio import Event + + from algorithms_keeper.api import GitHubAPI + commands_router = routing.Router() COMMAND_RE: Pattern[str] = re.compile(r"@algorithms-keeper\s+([a-z\-]+)", re.IGNORECASE) diff --git a/algorithms_keeper/event/installation.py b/algorithms_keeper/event/installation.py index ad3046c..45522e8 100644 --- a/algorithms_keeper/event/installation.py +++ b/algorithms_keeper/event/installation.py @@ -1,12 +1,17 @@ +from __future__ import annotations + import logging -from typing import Any +from typing import TYPE_CHECKING, Any from gidgethub import routing -from gidgethub.sansio import Event -from algorithms_keeper.api import GitHubAPI from algorithms_keeper.constants import GREETING_COMMENT +if TYPE_CHECKING: + from gidgethub.sansio import Event + + from algorithms_keeper.api import GitHubAPI + installation_router = routing.Router() logger = logging.getLogger(__package__) diff --git a/algorithms_keeper/event/pull_request.py b/algorithms_keeper/event/pull_request.py index 85d3964..a81b67d 100644 --- a/algorithms_keeper/event/pull_request.py +++ b/algorithms_keeper/event/pull_request.py @@ -20,16 +20,16 @@ } """ +from __future__ import annotations + import asyncio import logging import re -from typing import Any +from typing import TYPE_CHECKING, Any from gidgethub import routing -from gidgethub.sansio import Event from algorithms_keeper import utils -from algorithms_keeper.api import GitHubAPI from algorithms_keeper.constants import ( CHECKBOX_NOT_TICKED_COMMENT, EMPTY_PR_BODY_COMMENT, @@ -41,6 +41,11 @@ from algorithms_keeper.event.check_run import check_ci_status_and_label from algorithms_keeper.parser import PythonParser +if TYPE_CHECKING: + from gidgethub.sansio import Event + + from algorithms_keeper.api import GitHubAPI + # To disable this check, set the constant to 0. MAX_PR_PER_USER = 3 STAGE_PREFIX = "awaiting" diff --git a/algorithms_keeper/parser/files_parser.py b/algorithms_keeper/parser/files_parser.py index ff5912c..80a7bcf 100644 --- a/algorithms_keeper/parser/files_parser.py +++ b/algorithms_keeper/parser/files_parser.py @@ -1,9 +1,14 @@ +from __future__ import annotations + import logging -from collections.abc import Collection, Iterable, Mapping -from typing import Any +from typing import TYPE_CHECKING, Any from algorithms_keeper.constants import Label -from algorithms_keeper.utils import File + +if TYPE_CHECKING: + from collections.abc import Collection, Iterable, Mapping + + from algorithms_keeper.utils import File # These files are updated automatically by a GitHub action in almost every pull request. IGNORE_FILES_FOR_TYPELABEL: set[str] = {"DIRECTORY.md"} diff --git a/algorithms_keeper/parser/lint_rule.py b/algorithms_keeper/parser/lint_rule.py index 58f91ae..2a3ce5b 100644 --- a/algorithms_keeper/parser/lint_rule.py +++ b/algorithms_keeper/parser/lint_rule.py @@ -1,5 +1,11 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from fixit import LintRule -from libcst import CSTNode + +if TYPE_CHECKING: + from libcst import CSTNode class ReviewLintRule(LintRule): diff --git a/algorithms_keeper/parser/python_parser.py b/algorithms_keeper/parser/python_parser.py index a720bd5..ce738f7 100644 --- a/algorithms_keeper/parser/python_parser.py +++ b/algorithms_keeper/parser/python_parser.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import logging -from collections.abc import Iterable, Iterator, Mapping -from typing import Any +from typing import TYPE_CHECKING, Any from fixit import Config, LintRule from fixit.engine import LintRunner @@ -15,7 +16,11 @@ RequireTypeHintRule, UseFstringRule, ) -from algorithms_keeper.utils import File + +if TYPE_CHECKING: + from collections.abc import Iterable, Iterator, Mapping + + from algorithms_keeper.utils import File # Select only the bot's review rules, independent of Fixit's built-in defaults. DEFAULT_RULES: frozenset[type[LintRule]] = frozenset( diff --git a/algorithms_keeper/parser/record.py b/algorithms_keeper/parser/record.py index 6807a98..d6a88c0 100644 --- a/algorithms_keeper/parser/record.py +++ b/algorithms_keeper/parser/record.py @@ -1,13 +1,17 @@ +from __future__ import annotations + import traceback -from collections.abc import Collection from dataclasses import asdict, dataclass, field -from typing import Any - -from fixit import LintViolation -from libcst import ParserSyntaxError +from typing import TYPE_CHECKING, Any from algorithms_keeper.constants import Label +if TYPE_CHECKING: + from collections.abc import Collection + + from fixit import LintViolation + from libcst import ParserSyntaxError + # Mapping of rule to the appropriate label. RULE_TO_LABEL: dict[str, str] = { "RequireDescriptiveName": Label.DESCRIPTIVE_NAME, diff --git a/algorithms_keeper/parser/rules/require_descriptive_name.py b/algorithms_keeper/parser/rules/require_descriptive_name.py index 1a634f2..ea869a1 100644 --- a/algorithms_keeper/parser/rules/require_descriptive_name.py +++ b/algorithms_keeper/parser/rules/require_descriptive_name.py @@ -1,8 +1,14 @@ -import libcst as cst +from __future__ import annotations + +from typing import TYPE_CHECKING + from fixit import Invalid, Valid from algorithms_keeper.parser import lint_rule +if TYPE_CHECKING: + import libcst as cst + MESSAGE: str = "Please provide descriptive name for the {nodetype}: `{nodename}`" diff --git a/algorithms_keeper/parser/rules/require_type_hint.py b/algorithms_keeper/parser/rules/require_type_hint.py index ed0e043..b6e07e2 100644 --- a/algorithms_keeper/parser/rules/require_type_hint.py +++ b/algorithms_keeper/parser/rules/require_type_hint.py @@ -1,8 +1,14 @@ -import libcst as cst +from __future__ import annotations + +from typing import TYPE_CHECKING + from fixit import Invalid, Valid from algorithms_keeper.parser import lint_rule +if TYPE_CHECKING: + import libcst as cst + MISSING_TYPE_HINT: str = "Please provide type hint for the parameter: `{nodename}`" MISSING_RETURN_TYPE_HINT: str = ( diff --git a/algorithms_keeper/utils.py b/algorithms_keeper/utils.py index 2ace1a6..4e389f0 100644 --- a/algorithms_keeper/utils.py +++ b/algorithms_keeper/utils.py @@ -13,16 +13,21 @@ that uses all the given functions. """ +from __future__ import annotations + import urllib.parse from base64 import b64decode -from collections.abc import Mapping from dataclasses import dataclass from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any -from algorithms_keeper.api import GitHubAPI from algorithms_keeper.constants import PR_REVIEW_BODY +if TYPE_CHECKING: + from collections.abc import Mapping + + from algorithms_keeper.api import GitHubAPI + @dataclass(frozen=True) class File: diff --git a/pyproject.toml b/pyproject.toml index c06e0d0..d9b962a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ line-length = 88 target-version = "py311" [tool.ruff.lint] +future-annotations = true select = [ "A", # flake8-builtins "AIR", # Airflow diff --git a/tests/test_api.py b/tests/test_api.py index 80abf4d..9f741c1 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,17 +1,22 @@ -from collections.abc import AsyncGenerator, Awaitable, Callable -from typing import Any +from __future__ import annotations + +from typing import TYPE_CHECKING, Any import aiohttp import pytest import pytest_asyncio from aiohttp import web -from aiohttp.test_utils import TestServer from gidgethub import apps, sansio from algorithms_keeper.api import GitHubAPI, token_cache from .utils import number, token +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Awaitable, Callable + + from aiohttp.test_utils import TestServer + async def mock_return(*args: Any, **kwargs: Any) -> dict[str, str]: return {"token": token} diff --git a/tests/test_commands.py b/tests/test_commands.py index 016a905..a4d7fa8 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,5 +1,6 @@ -from collections.abc import Generator -from typing import Any +from __future__ import annotations + +from typing import TYPE_CHECKING, Any import pytest from gidgethub.sansio import Event @@ -26,6 +27,9 @@ user, ) +if TYPE_CHECKING: + from collections.abc import Generator + @pytest.fixture(scope="module", autouse=True) def patch_module( diff --git a/tests/test_pull_requests.py b/tests/test_pull_requests.py index 3fe2a70..b96f9b5 100644 --- a/tests/test_pull_requests.py +++ b/tests/test_pull_requests.py @@ -1,5 +1,6 @@ -from collections.abc import Generator -from typing import Any +from __future__ import annotations + +from typing import TYPE_CHECKING, Any from urllib.parse import quote import pytest @@ -34,6 +35,9 @@ user, ) +if TYPE_CHECKING: + from collections.abc import Generator + # This constant can only contain one invalid filename. INVALID = "invalid" diff --git a/tests/test_utils.py b/tests/test_utils.py index 51ea632..b0f6982 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import urllib.parse from pathlib import Path from typing import TYPE_CHECKING, cast diff --git a/tests/utils.py b/tests/utils.py index f092cb6..76aeff6 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,9 +1,13 @@ -from collections.abc import AsyncGenerator +from __future__ import annotations + from dataclasses import dataclass, field, fields -from typing import Any +from typing import TYPE_CHECKING, Any from gidgethub.sansio import Event +if TYPE_CHECKING: + from collections.abc import AsyncGenerator + def parametrize_id(obj: object) -> str: """``Event.delivery_id`` is used as a short description for the respective test