diff --git a/mergify_cli/ci/cli.py b/mergify_cli/ci/cli.py index a7d64c44..7850f876 100644 --- a/mergify_cli/ci/cli.py +++ b/mergify_cli/ci/cli.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import click from mergify_cli import utils @@ -99,7 +101,7 @@ def _process_tests_target_branch( type=JUnitFile(), ) @utils.run_with_asyncio -async def junit_upload( # noqa: PLR0913 +async def junit_upload( *, api_url: str, token: str, @@ -170,7 +172,7 @@ async def junit_upload( # noqa: PLR0913 type=JUnitFile(), ) @utils.run_with_asyncio -async def junit_process( # noqa: PLR0913 +async def junit_process( *, api_url: str, token: str, @@ -262,7 +264,7 @@ def scopes( type=click.Path(exists=True), ) @utils.run_with_asyncio -async def scopes_send( # noqa: PLR0913, PLR0917 +async def scopes_send( api_url: str, token: str, repository: str, diff --git a/mergify_cli/ci/detector.py b/mergify_cli/ci/detector.py index e27ca6d2..3bf52beb 100644 --- a/mergify_cli/ci/detector.py +++ b/mergify_cli/ci/detector.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import os import pathlib diff --git a/mergify_cli/ci/junit_processing/cli.py b/mergify_cli/ci/junit_processing/cli.py index 873facbe..efc50a22 100644 --- a/mergify_cli/ci/junit_processing/cli.py +++ b/mergify_cli/ci/junit_processing/cli.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys import click @@ -8,7 +10,7 @@ from mergify_cli.ci.junit_processing import upload -async def process_junit_files( # noqa: PLR0913 +async def process_junit_files( *, api_url: str, token: str, @@ -98,7 +100,7 @@ async def process_junit_files( # noqa: PLR0913 quarantine_final_failure_message = ( "Unable to determine quarantined failures due to above error" ) - except Exception as exc: # noqa: BLE001 + except Exception as exc: msg = ( f"❌ An unexpected error occurred when checking quarantined tests: {exc!s}" ) @@ -121,7 +123,7 @@ async def process_junit_files( # noqa: PLR0913 repository=repository, spans=spans, ) - except Exception as e: # noqa: BLE001 + except Exception as e: click.echo( click.style(f"❌ Error uploading JUnit XML reports: {e}", fg="red"), err=True, diff --git a/mergify_cli/ci/junit_processing/junit.py b/mergify_cli/ci/junit_processing/junit.py index 06aae3bc..4ea3f503 100644 --- a/mergify_cli/ci/junit_processing/junit.py +++ b/mergify_cli/ci/junit_processing/junit.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import dataclasses import os import pathlib @@ -45,7 +47,7 @@ async def files_to_spans( spans.extend( await junit_to_spans( run_id, - pathlib.Path(filename).read_bytes(), # noqa: ASYNC240 + pathlib.Path(filename).read_bytes(), test_language=test_language, test_framework=test_framework, ), @@ -265,9 +267,9 @@ async def junit_to_spans( spans.append(span) - testsuite_span._start_time = min_start_time # noqa: SLF001 + testsuite_span._start_time = min_start_time session_start_time = min(session_start_time, min_start_time) - session_span._start_time = session_start_time # noqa: SLF001 + session_span._start_time = session_start_time return spans diff --git a/mergify_cli/ci/junit_processing/quarantine.py b/mergify_cli/ci/junit_processing/quarantine.py index 768e33a7..368e9afa 100644 --- a/mergify_cli/ci/junit_processing/quarantine.py +++ b/mergify_cli/ci/junit_processing/quarantine.py @@ -1,15 +1,20 @@ +from __future__ import annotations + import dataclasses import typing import click import httpx -from opentelemetry.sdk.trace import ReadableSpan import opentelemetry.trace import tenacity from mergify_cli import utils +if typing.TYPE_CHECKING: + from opentelemetry.sdk.trace import ReadableSpan + + @dataclasses.dataclass class QuarantineFailedError(Exception): message: str @@ -71,7 +76,7 @@ async def check_and_update_failing_spans( span.name in quarantined_tests_tuple.quarantined_tests_names, ) - span._attributes = dict(span.attributes) | { # noqa: SLF001 + span._attributes = dict(span.attributes) | { "cicd.test.quarantined": quarantined, } if ( diff --git a/mergify_cli/ci/junit_processing/upload.py b/mergify_cli/ci/junit_processing/upload.py index 507d72a6..2ebdf881 100644 --- a/mergify_cli/ci/junit_processing/upload.py +++ b/mergify_cli/ci/junit_processing/upload.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import io import logging @@ -5,18 +7,21 @@ from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter -from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace import export from mergify_cli import console +if typing.TYPE_CHECKING: + from opentelemetry.sdk.trace import ReadableSpan + + class UploadError(Exception): pass @contextlib.contextmanager -def capture_log(logger: logging.Logger) -> typing.Generator[io.StringIO, None, None]: +def capture_log(logger: logging.Logger) -> typing.Generator[io.StringIO]: # Create a string stream to capture logs log_capture_string = io.StringIO() diff --git a/mergify_cli/ci/scopes/changed_files.py b/mergify_cli/ci/scopes/changed_files.py index 57960529..2703e25f 100644 --- a/mergify_cli/ci/scopes/changed_files.py +++ b/mergify_cli/ci/scopes/changed_files.py @@ -14,7 +14,7 @@ class ChangedFilesError(exceptions.ScopesError): def _run(cmd: list[str]) -> str: - return subprocess.check_output(cmd, text=True, encoding="utf-8").strip() + return subprocess.check_output(cmd, text=True, encoding="utf-8").strip() # noqa: S603 def has_merge_base(base: str, head: str) -> bool: diff --git a/mergify_cli/ci/scopes/config/__init__.py b/mergify_cli/ci/scopes/config/__init__.py index 9dec2f24..fdfee40e 100644 --- a/mergify_cli/ci/scopes/config/__init__.py +++ b/mergify_cli/ci/scopes/config/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from mergify_cli.ci.scopes.config.root import Config from mergify_cli.ci.scopes.config.root import ConfigInvalidError from mergify_cli.ci.scopes.config.scopes import FileFilters diff --git a/mergify_cli/ci/scopes/config/root.py b/mergify_cli/ci/scopes/config/root.py index 2e160788..2c4341ba 100644 --- a/mergify_cli/ci/scopes/config/root.py +++ b/mergify_cli/ci/scopes/config/root.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pathlib import typing @@ -20,7 +22,7 @@ class Config(pydantic.BaseModel): @classmethod def from_dict( cls, - data: dict[str, typing.Any] | typing.Any, # noqa: ANN401 + data: dict[str, typing.Any] | typing.Any, ) -> typing.Self: try: return cls.model_validate(data) diff --git a/mergify_cli/ci/scopes/exceptions.py b/mergify_cli/ci/scopes/exceptions.py index 80a4f9f7..5b601dd9 100644 --- a/mergify_cli/ci/scopes/exceptions.py +++ b/mergify_cli/ci/scopes/exceptions.py @@ -1,2 +1,5 @@ +from __future__ import annotations + + class ScopesError(Exception): pass diff --git a/mergify_cli/cli.py b/mergify_cli/cli.py index a32d8dc9..e3c55faf 100644 --- a/mergify_cli/cli.py +++ b/mergify_cli/cli.py @@ -37,6 +37,7 @@ @click.pass_context def cli( ctx: click.Context, + *, debug: bool, ) -> None: ctx.obj = {"debug": debug} @@ -53,7 +54,7 @@ def main() -> None: # Let's try our best by forcing utf-8 and if it's impossible, just returns escaped character if os.name == "nt" and not sys.flags.utf8_mode: os.environ["PYTHONUTF8"] = "1" - p = subprocess.Popen( + p = subprocess.Popen( # noqa: S603 sys.argv, env=os.environ, stdin=sys.stdin, diff --git a/mergify_cli/github_types.py b/mergify_cli/github_types.py index b073eadd..42d93d89 100644 --- a/mergify_cli/github_types.py +++ b/mergify_cli/github_types.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import typing diff --git a/mergify_cli/stack/changes.py b/mergify_cli/stack/changes.py index 6ed02ef1..9038b55d 100644 --- a/mergify_cli/stack/changes.py +++ b/mergify_cli/stack/changes.py @@ -12,7 +12,7 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - +from __future__ import annotations import asyncio import dataclasses @@ -20,13 +20,15 @@ import sys import typing -import httpx - from mergify_cli import console from mergify_cli import github_types from mergify_cli import utils +if typing.TYPE_CHECKING: + import httpx + + CHANGEID_RE = re.compile(r"Change-Id: (I[0-9a-z]{40})") ChangeId = typing.NewType("ChangeId", str) @@ -126,6 +128,7 @@ def commit_short_sha(self) -> str: def get_log_from_local_change( self, + *, dry_run: bool, create_as_draft: bool, ) -> str: @@ -184,7 +187,7 @@ def get_log_from_local_change( @dataclasses.dataclass class OrphanChange(Change): - def get_log_from_orphan_change(self, dry_run: bool) -> str: + def get_log_from_orphan_change(self, *, dry_run: bool) -> str: action = "to delete" if dry_run else "deleted" title = self.pull["title"] if self.pull else "" url = self.pull["html_url"] if self.pull else "" @@ -201,6 +204,7 @@ class Changes: def display_plan( changes: Changes, + *, create_as_draft: bool, ) -> None: for change in changes.locals: @@ -215,7 +219,8 @@ def display_plan( console.log(orphan.get_log_from_orphan_change(dry_run=True)) -async def get_changes( # noqa: PLR0913,PLR0917 +async def get_changes( + *, base_commit_sha: str, stack_prefix: str, base_branch: str, diff --git a/mergify_cli/stack/checkout.py b/mergify_cli/stack/checkout.py index 7dc5d0f5..913e5ccc 100644 --- a/mergify_cli/stack/checkout.py +++ b/mergify_cli/stack/checkout.py @@ -2,22 +2,27 @@ import dataclasses import sys +from typing import TYPE_CHECKING from mergify_cli import console -from mergify_cli import github_types from mergify_cli import utils from mergify_cli.stack import changes +if TYPE_CHECKING: + from mergify_cli import github_types + + @dataclasses.dataclass class ChangeNode: pull: github_types.PullRequest up: ChangeNode | None = None -async def stack_checkout( # noqa: PLR0913, PLR0917 +async def stack_checkout( github_server: str, token: str, + *, user: str, repo: str, branch_prefix: str | None, diff --git a/mergify_cli/stack/cli.py b/mergify_cli/stack/cli.py index 520c13a2..434f738c 100644 --- a/mergify_cli/stack/cli.py +++ b/mergify_cli/stack/cli.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import asyncio import os from urllib import parse @@ -170,8 +172,9 @@ async def edit() -> None: help="Only update existing pull requests, do not create new ones", ) @utils.run_with_asyncio -async def push( # noqa: PLR0913, PLR0917 +async def push( ctx: click.Context, + *, setup: bool, dry_run: bool, next_only: bool, @@ -239,8 +242,9 @@ async def push( # noqa: PLR0913, PLR0917 help="Change the target branch of the stack.", ) @utils.run_with_asyncio -async def checkout( # noqa: PLR0913, PLR0917 +async def checkout( ctx: click.Context, + *, author: str | None, repository: str, branch: str, @@ -252,13 +256,13 @@ async def checkout( # noqa: PLR0913, PLR0917 await stack_checkout_mod.stack_checkout( ctx.obj["github_server"], ctx.obj["token"], - user, - repo, - branch_prefix, - branch, - author, - trunk, - dry_run, + user=user, + repo=repo, + branch_prefix=branch_prefix, + branch=branch, + author=author, + trunk=trunk, + dry_run=dry_run, ) diff --git a/mergify_cli/stack/edit.py b/mergify_cli/stack/edit.py index d1e0b00e..289e97ac 100644 --- a/mergify_cli/stack/edit.py +++ b/mergify_cli/stack/edit.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from mergify_cli import utils diff --git a/mergify_cli/stack/push.py b/mergify_cli/stack/push.py index 38bb7364..a4efca5e 100644 --- a/mergify_cli/stack/push.py +++ b/mergify_cli/stack/push.py @@ -23,7 +23,6 @@ import typing from mergify_cli import console -from mergify_cli import github_types from mergify_cli import utils from mergify_cli.stack import changes @@ -31,6 +30,8 @@ if typing.TYPE_CHECKING: import httpx + from mergify_cli import github_types + DEPENDS_ON_RE = re.compile(r"Depends-On: (#[0-9]*)") TMP_STACK_BRANCH = "mergify-cli-tmp" @@ -65,7 +66,7 @@ def format_pull_description( # TODO(charly): fix code to conform to linter (number of arguments, local # variables, statements, positional arguments, branches) -async def stack_push( # noqa: PLR0913 +async def stack_push( github_server: str, token: str, *, @@ -153,18 +154,18 @@ async def stack_push( # noqa: PLR0913 with console.status("Preparing stacked branches..."): console.log("Stacked pull request plan:", style="green") planned_changes = await changes.get_changes( - base_commit_sha, - stack_prefix, - base_branch, - dest_branch, - remote_changes, - only_update_existing_pulls, - next_only, + base_commit_sha=base_commit_sha, + stack_prefix=stack_prefix, + base_branch=base_branch, + dest_branch=dest_branch, + remote_changes=remote_changes, + only_update_existing_pulls=only_update_existing_pulls, + next_only=next_only, ) changes.display_plan( planned_changes, - create_as_draft, + create_as_draft=create_as_draft, ) if dry_run: @@ -180,13 +181,13 @@ async def stack_push( # noqa: PLR0913 if change.action in {"create", "update"}: pull = await create_or_update_stack( client, - user, - repo, - remote, - change, - depends_on, - create_as_draft, - keep_pull_request_title_and_body, + user=user, + repo=repo, + remote=remote, + change=change, + depends_on=depends_on, + create_as_draft=create_as_draft, + keep_pull_request_title_and_body=keep_pull_request_title_and_body, ) change.pull = pull @@ -287,8 +288,9 @@ async def delete_stack( console.log(change.get_log_from_orphan_change(dry_run=False)) -async def create_or_update_stack( # noqa: PLR0913,PLR0917 +async def create_or_update_stack( client: httpx.AsyncClient, + *, user: str, repo: str, remote: str, diff --git a/mergify_cli/tests/ci/junit_processing/test_check_failing_spans.py b/mergify_cli/tests/ci/junit_processing/test_check_failing_spans.py index 3a738341..9a193e37 100644 --- a/mergify_cli/tests/ci/junit_processing/test_check_failing_spans.py +++ b/mergify_cli/tests/ci/junit_processing/test_check_failing_spans.py @@ -1,13 +1,20 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.trace import Status from opentelemetry.trace import StatusCode import pytest -import respx from mergify_cli.ci.junit_processing.quarantine import QuarantineFailedError from mergify_cli.ci.junit_processing.quarantine import check_and_update_failing_spans +if TYPE_CHECKING: + import respx + + API_MERGIFY_BASE_URL = "https://api.mergify.com" diff --git a/mergify_cli/tests/ci/junit_processing/test_cli.py b/mergify_cli/tests/ci/junit_processing/test_cli.py index 9819171f..89a5b7be 100644 --- a/mergify_cli/tests/ci/junit_processing/test_cli.py +++ b/mergify_cli/tests/ci/junit_processing/test_cli.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pathlib from unittest import mock @@ -31,7 +33,7 @@ async def test_process_junit_file_reporting( ): await cli.process_junit_files( api_url="https://api.mergify.com", - token="foobar", # noqa: S106 + token="foobar", repository="foo/bar", test_framework=None, test_language=None, diff --git a/mergify_cli/tests/ci/junit_processing/test_upload.py b/mergify_cli/tests/ci/junit_processing/test_upload.py index 6761a136..658a236f 100644 --- a/mergify_cli/tests/ci/junit_processing/test_upload.py +++ b/mergify_cli/tests/ci/junit_processing/test_upload.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pathlib import re diff --git a/mergify_cli/tests/ci/scopes/test_base_detector.py b/mergify_cli/tests/ci/scopes/test_base_detector.py index ddb384fa..44bbdbff 100644 --- a/mergify_cli/tests/ci/scopes/test_base_detector.py +++ b/mergify_cli/tests/ci/scopes/test_base_detector.py @@ -1,11 +1,17 @@ +from __future__ import annotations + import json -import pathlib +from typing import TYPE_CHECKING import pytest from mergify_cli.ci.scopes import base_detector +if TYPE_CHECKING: + import pathlib + + @pytest.mark.parametrize("event_name", ["pull_request", "pull_request_review", "push"]) def test_detect_base_from_repository_default_branch( event_name: str, diff --git a/mergify_cli/tests/ci/scopes/test_changed_files.py b/mergify_cli/tests/ci/scopes/test_changed_files.py index e4b3c352..966d53a3 100644 --- a/mergify_cli/tests/ci/scopes/test_changed_files.py +++ b/mergify_cli/tests/ci/scopes/test_changed_files.py @@ -1,7 +1,14 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + import pytest from mergify_cli.ci.scopes import changed_files -from mergify_cli.tests import utils as test_utils + + +if TYPE_CHECKING: + from mergify_cli.tests import utils as test_utils def test_git_changed_files(mock_subprocess: test_utils.SubprocessMocks) -> None: diff --git a/mergify_cli/tests/ci/scopes/test_cli.py b/mergify_cli/tests/ci/scopes/test_cli.py index 0e758807..90322283 100644 --- a/mergify_cli/tests/ci/scopes/test_cli.py +++ b/mergify_cli/tests/ci/scopes/test_cli.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import json -import pathlib +from typing import TYPE_CHECKING from unittest import mock import pytest @@ -11,6 +13,10 @@ from mergify_cli.ci.scopes import config +if TYPE_CHECKING: + import pathlib + + def test_from_yaml_with_extras_ignored(tmp_path: pathlib.Path) -> None: config_file = tmp_path / "config.yml" config_file.write_text( @@ -441,7 +447,7 @@ def test_detect_debug_output( async def test_upload_scopes(respx_mock: respx.MockRouter) -> None: api_url = "https://api.mergify.test" - token = "test-token" # noqa: S105 + token = "test-token" repository = "owner/repo" pull_request = 123 diff --git a/mergify_cli/tests/ci/test_cli.py b/mergify_cli/tests/ci/test_cli.py index 8f045e35..6de32822 100644 --- a/mergify_cli/tests/ci/test_cli.py +++ b/mergify_cli/tests/ci/test_cli.py @@ -1,12 +1,14 @@ +from __future__ import annotations + import json import pathlib +from typing import TYPE_CHECKING from unittest import mock import anys import click from click import testing import pytest -import respx from mergify_cli.ci import cli as ci_cli from mergify_cli.ci.junit_processing import cli as junit_processing_cli @@ -14,6 +16,10 @@ from mergify_cli.ci.junit_processing import upload +if TYPE_CHECKING: + import respx + + REPORT_XML = pathlib.Path(__file__).parent / "report.xml" diff --git a/mergify_cli/tests/ci/test_detector.py b/mergify_cli/tests/ci/test_detector.py index 5cc27c8c..21246827 100644 --- a/mergify_cli/tests/ci/test_detector.py +++ b/mergify_cli/tests/ci/test_detector.py @@ -1,12 +1,18 @@ +from __future__ import annotations + import json import pathlib +from typing import TYPE_CHECKING import pytest -import respx from mergify_cli.ci import detector +if TYPE_CHECKING: + import respx + + PULL_REQUEST_EVENT = pathlib.Path(__file__).parent / "pull_request.json" @@ -171,6 +177,7 @@ def test_get_github_pull_request_number_unsupported_ci( ) def test_is_flaky_test_detection_enabled( monkeypatch: pytest.MonkeyPatch, + *, env_value: str, expected: bool, ) -> None: diff --git a/mergify_cli/tests/ci/test_junit.py b/mergify_cli/tests/ci/test_junit.py index d3dd5e2d..18f03b6d 100644 --- a/mergify_cli/tests/ci/test_junit.py +++ b/mergify_cli/tests/ci/test_junit.py @@ -1,15 +1,21 @@ +from __future__ import annotations + import json import pathlib +from typing import TYPE_CHECKING from unittest import mock import anys import opentelemetry.trace.span -import pytest from mergify_cli.ci import detector from mergify_cli.ci.junit_processing import junit +if TYPE_CHECKING: + import pytest + + @mock.patch.object(detector, "get_ci_provider", return_value="github_actions") @mock.patch.object(detector, "get_pipeline_name", return_value="PIPELINE") @mock.patch.object(detector, "get_job_name", return_value="JOB") diff --git a/mergify_cli/tests/conftest.py b/mergify_cli/tests/conftest.py index 01b0490e..11473abb 100644 --- a/mergify_cli/tests/conftest.py +++ b/mergify_cli/tests/conftest.py @@ -12,10 +12,10 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -from collections import abc -from collections.abc import Generator -import pathlib +from __future__ import annotations + import subprocess +from typing import TYPE_CHECKING from unittest import mock import pytest @@ -23,6 +23,12 @@ from mergify_cli.tests import utils as test_utils +if TYPE_CHECKING: + from collections import abc + from collections.abc import Generator + import pathlib + + @pytest.fixture(autouse=True) def _unset_ci( monkeypatch: pytest.MonkeyPatch, @@ -60,7 +66,7 @@ def _git_repo() -> None: @pytest.fixture def git_mock( tmp_path: pathlib.Path, -) -> Generator[test_utils.GitMock, None, None]: +) -> Generator[test_utils.GitMock]: git_mock_object = test_utils.GitMock() # Top level directory is a temporary path git_mock_object.mock("rev-parse", "--show-toplevel", output=str(tmp_path)) diff --git a/mergify_cli/tests/stack/test_push.py b/mergify_cli/tests/stack/test_push.py index a06633cd..ea33fd30 100644 --- a/mergify_cli/tests/stack/test_push.py +++ b/mergify_cli/tests/stack/test_push.py @@ -12,15 +12,21 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +from __future__ import annotations + import json +from typing import TYPE_CHECKING import pytest -import respx from mergify_cli.stack import push from mergify_cli.tests import utils as test_utils +if TYPE_CHECKING: + import respx + + @pytest.mark.parametrize( "valid_branch_name", [ diff --git a/mergify_cli/tests/stack/test_setup.py b/mergify_cli/tests/stack/test_setup.py index cd715b37..6cae54c6 100644 --- a/mergify_cli/tests/stack/test_setup.py +++ b/mergify_cli/tests/stack/test_setup.py @@ -1,14 +1,17 @@ -import typing +from __future__ import annotations -import pytest +import typing from mergify_cli.stack import setup -from mergify_cli.tests import utils as test_utils if typing.TYPE_CHECKING: import pathlib + import pytest + + from mergify_cli.tests import utils as test_utils + async def test_setup( git_mock: test_utils.GitMock, diff --git a/mergify_cli/tests/test_utils.py b/mergify_cli/tests/test_utils.py index a075e6e0..98a929b1 100644 --- a/mergify_cli/tests/test_utils.py +++ b/mergify_cli/tests/test_utils.py @@ -12,11 +12,10 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +from __future__ import annotations - -import collections import json -import pathlib +from typing import TYPE_CHECKING from unittest import mock import pytest @@ -24,6 +23,11 @@ from mergify_cli import utils +if TYPE_CHECKING: + import collections + import pathlib + + @pytest.mark.usefixtures("_git_repo") async def test_get_branch_name() -> None: assert await utils.git_get_branch_name() == "main" @@ -57,6 +61,7 @@ async def test_get_trunk() -> None: ], ) async def test_defaults_config_args_set( + *, default_arg_fct: collections.abc.Callable[ [], collections.abc.Awaitable[bool | str], @@ -92,6 +97,7 @@ async def test_defaults_config_args_set( ) def test_get_boolean_env( monkeypatch: pytest.MonkeyPatch, + *, env_value: str, expected: bool, ) -> None: diff --git a/mergify_cli/tests/utils.py b/mergify_cli/tests/utils.py index 62cdca4e..707b1bdc 100644 --- a/mergify_cli/tests/utils.py +++ b/mergify_cli/tests/utils.py @@ -12,13 +12,18 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -from collections import abc +from __future__ import annotations + import dataclasses import subprocess import typing from unittest import mock +if typing.TYPE_CHECKING: + from collections import abc + + class Commit(typing.TypedDict): sha: str title: str @@ -113,7 +118,7 @@ def subprocess_mocked() -> abc.Generator[SubprocessMocks]: def check_output( cmd: list[str], - **kwargs: typing.Any, # noqa: ARG001 ANN401 + **kwargs: typing.Any, # noqa: ARG001 ) -> str: try: m = mocks.calls.pop(0) diff --git a/mergify_cli/utils.py b/mergify_cli/utils.py index 726de3fe..c1054a7d 100644 --- a/mergify_cli/utils.py +++ b/mergify_cli/utils.py @@ -40,8 +40,8 @@ _DEBUG = False -def set_debug(debug: bool) -> None: - global _DEBUG # noqa: PLW0603 +def set_debug(*, debug: bool) -> None: + global _DEBUG _DEBUG = debug @@ -212,6 +212,7 @@ async def log_httpx_response(response: httpx.Response) -> None: def get_http_client( server: str, + *, headers: dict[str, typing.Any] | None = None, event_hooks: Mapping[str, list[Callable[..., typing.Any]]] | None = None, follow_redirects: bool = False, @@ -280,7 +281,7 @@ def get_github_http_client(github_server: str, token: str) -> httpx.AsyncClient: ) -def get_boolean_env(name: str, default: bool = False) -> bool: +def get_boolean_env(name: str, *, default: bool = False) -> bool: v = os.getenv(name) if v is None: return default @@ -298,7 +299,7 @@ def get_boolean_env(name: str, default: bool = False) -> bool: R = typing.TypeVar("R") -def run_with_asyncio( +def run_with_asyncio[**P, R]( func: Callable[ P, Coroutine[typing.Any, typing.Any, R], diff --git a/pyproject.toml b/pyproject.toml index b95c7c47..22b2b967 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,103 +64,154 @@ show_error_codes = true [tool.ruff] line-length = 88 indent-width = 4 -target-version = "py311" +target-version = "py313" [tool.ruff.lint] preview = true select = [ - "F", - "E", - "W", - "I", - "N", - "UP", - "YTT", - "ANN", - "ASYNC", - "S", - "BLE", - "FBT", - "B", - "A", - "COM", - "C4", - "DTZ", - "T10", - "EM", - "FA", - "ISC", - "ICN", - "G", - "INP", - "PIE", - "T20", - "PYI", - "PT", - "Q", - "RSE", - "RET", - "SLF", - "SLOT", - "SIM", - "TID", - "TCH", - "INT", - "ARG", - "PTH", - "TD", - "ERA", - "PGH", - "PL", - "TRY", - "FLY", - "NPY", - "PERF", - "FURB", - "LOG", - "RUF", + "E", "W", "F", "I", "A", "B", "Q", + "C4", "T10", "ISC", "ICN", "BLE", + "G", "RSE", "TID", "TRY", "UP", + "RUF", "RET", "DTZ", "TC", "COM", + "PERF", "PT", "PGH", "SIM", + "PIE", "YTT", "T20", "ARG", "PTH", + "N", "PYI", "FURB", "FBT", "S", + "NPY", "FA", "LOG", "SLOT", + "PLC", "PLE", "PLR", "PLW", + "FAST", + # FIXME(sileht): doctring/rst-docstring like + # "D" ] ignore = [ - # NOTE(charly): line-length is up to the formatter - "E501", - # NOTE(charly): `subprocess` module is possibly insecure + # NOTE: Parameter {arg_name} appears in route path, but not in {function_name} signature + "FAST003", + # NOTE: subclassing of collections.UserDict/List/String instead of dict/list/string + "FURB189", + # NOTE: we don't care that much about shadowing Python modules + "A004", "A005", + # NOTE(Greesb): subprocess module is possibly insecure "S404", - # NOTE(jd): likely a false positive https://github.com/PyCQA/bandit/issues/333 - "S603", - # NOTE(charly): Starting a process with a partial executable path + # NOTE(Greesb): Starting a process with a partial executable path "S607", - # NOTE(charly): Boolean-typed positional argument in function definition. - # Interesting, but require some work. - "FBT001", - # NOTE(charly): Boolean default positional argument in function definition. - # Interesting, but require some work. - "FBT002", - # NOTE(charly): Missing issue link on the line following this TODO - "TD003", - # NOTE(charly): Magic value used in comparison + # NOTE(Greesb): itertools.starmap is less efficient than list comprehensions + "FURB140", + # NOTE(Greesb): Checks for union annotations that contain redundant numeric types (e.g., int | float). + # too confusing to only have `float` as typing when the function can also receive an `int` + "PYI041", + # NOTE(Greesb): Function name should be lowercase + # For a lot of usecases it makes sense to want to not use lowercase + "N802", + # NOTE(Greesb): variable in function should be lowercase + # For a lot of usecases it makes sense to want to not use lowercase variables + "N806", + # NOTE(Greesb): contextlib.suppress is slower than try-except-pass + "SIM105", + # NOTE(Greesb): Import should be at the top level of the file + # we already have isort for this, also when we do not have an import at top level + # it's because of circular imports + "PLC0415", + # NOTE(Greesb): Method could be a function, classmethod or static method + # rule useless since we have a lot of abstraction + "PLR6301", + # NOTE(Greesb): Magic value used in comparison + # Interesting but will require a refactor, and will need to be ignored + # in tests "PLR2004", - # List comprehensions are most efficient in most cases now + # NOTE(Greesb): too many public methods + "PLR0904", + # NOTE(Greesb): Too many return, branches, arguments, local variables, statements, positional arguments. + # Interesting but will require a big refactor + "PLR0911", "PLR0912", "PLR0913", "PLR0914", "PLR0915", "PLR0917", + # NOTE(Greesb): Too many nested blocks + # interesting but require some refactor "PLR1702", - # We use mock.patch.object, which automatically pass the mock as an - # argument to the test if no `new` is specified, without needing the mock - # itself. + # NOTE(Greesb): custom class with __eq__ but no __hash__ + # We don't need it + "PLW1641", + # NOTE(Greesb): Checks for the use of global statements to update identifiers. + "PLW0603", + # NOTE(Greesb): subprocess.run without explicit check argument + "PLW1510", + # NOTE(Greesb): continue not supported inside finally clause + # only useful for python < 3.8 + "PLE0116", + # NOTE(Greesb): we use mock.patch.object, which automatically pass + # the mock as an argument to the test if no `new` is specified, without + # needing the mock itself. "PT019", - # We don't want to enforce the number of statements - "PLR0914", "PLR0912", "PLR0915", - "B904", - # Unnecessary parenthesis on raised exceptions, this breaks `isinstance` on exceptions + # NOTE(sileht): not compatible with ruff-format + "ISC001", + # NOTE(sileht): line-length is up to black + "E501", + # NOTE(sileht): we prefer using exc_info=True + "G201", "TRY400", + # NOTE(sileht): "Unnecessary parentheses on raised exception", this breaks + # isinstance on exception "RSE102", + # NOTE(sileht): No exception with message set via __init__ + "TRY003", "TRY301", + # NOTE(sileht): This enforce usage of raise ... from ... + "B904", + # FIXME(sileht): very interesting, ruff BLE is far better that flake8-blind-except + "BLE001", + # NOTE(Greesb): mypy doesn't support the `type myvar = [...]` syntax yet + "UP040", ] +[tool.ruff.lint.flake8-pytest-style] +parametrize-values-type = "list" +parametrize-values-row-type = "tuple" + +[tool.ruff.lint.flake8-type-checking] +strict = true +quote-annotations = true +# https://docs.astral.sh/ruff/settings/#lint_flake8-type-checking_runtime-evaluated-base-classes +runtime-evaluated-base-classes = [ + "typing.Annotated", + "pydantic.BaseModel", + "pydantic.TypeAdapter", + # typing.TypedDict is generally used by pydantic for validation, + # so we need the imports of the key of a typing.TypedDict class + # to be available at runtime. + "typing.TypedDict", +] +# https://docs.astral.sh/ruff/settings/#lint_flake8-type-checking_exempt-modules +# Modules that should not be moved to a type-checking block. +exempt-modules = [ + "typing", + "annotated_types", +] +# https://docs.astral.sh/ruff/settings/#lint_flake8-type-checking_runtime-evaluated-decorators +# Exempt classes and functions decorated with any of the enumerated decorators +# from being moved into type-checking blocks. +runtime-evaluated-decorators = [ + "pydantic.dataclasses.dataclass", +] + + [tool.ruff.lint.per-file-ignores] -"mergify_cli/tests/**/*.py" = ["S101", "SLF001"] +"mergify_cli/tests/**/*.py" = [ + # Use of assert detected + "S101", + # hardcoded passwords + "S105", "S106", + # Standard pseudo-random generators are not suitable for cryptographic purposes + "S311", + # subprocess call: check for execution of untrusted input + "S603", + # Magic value used in comparison + "PLR2004", + # Function is declared async, but doesn't await or use async features. + "RUF029", +] [tool.ruff.lint.isort] force-single-line = true force-sort-within-sections = true lines-after-imports = 2 known-first-party = ["mergify_cli"] +required-imports = ["from __future__ import annotations"] [tool.ruff.lint.flake8-tidy-imports] ban-relative-imports = "all" diff --git a/test_binary_build.py b/test_binary_build.py index 37c5d193..6938949b 100644 --- a/test_binary_build.py +++ b/test_binary_build.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import subprocess