From dde1fcba37d18b18a4976bf53b3cea0448b6e65c Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Fri, 24 Jul 2026 01:51:34 +0200 Subject: [PATCH] Ruff 0.16.0 compatibility changes (#834) --- actions/__init__.py | 2 +- actions/cla.py | 2 +- actions/dependabot.py | 8 ++-- actions/dispatch_actions.py | 4 +- actions/format_python_docstrings.py | 6 ++- actions/github_report.py | 2 + actions/summarize_release.py | 4 +- actions/update_file_headers.py | 4 +- actions/utils/github_utils.py | 9 ++-- tests/test_dispatch_actions.py | 49 ++++++++++---------- tests/test_file_headers.py | 70 +++++++++++++++-------------- tests/test_github_utils.py | 34 +++++++------- tests/test_openai_utils.py | 50 +++++++++++---------- 13 files changed, 129 insertions(+), 115 deletions(-) diff --git a/actions/__init__.py b/actions/__init__.py index 494fc9d..7e07189 100644 --- a/actions/__init__.py +++ b/actions/__init__.py @@ -28,4 +28,4 @@ # ├── test_summarize_pr.py # └── ... -__version__ = "0.2.35" +__version__ = "0.2.36" diff --git a/actions/cla.py b/actions/cla.py index e6b462a..d67be66 100644 --- a/actions/cla.py +++ b/actions/cla.py @@ -89,7 +89,7 @@ def _ledger(action: Action) -> tuple[dict, str]: payload = response.json() content = json.loads(base64.b64decode(payload["content"])) if not isinstance(content.get("signedContributors"), list): - raise RuntimeError("CLA signature ledger has an invalid schema") + raise TypeError("CLA signature ledger has an invalid schema") return content, payload["sha"] diff --git a/actions/dependabot.py b/actions/dependabot.py index 8fa9881..8947fef 100644 --- a/actions/dependabot.py +++ b/actions/dependabot.py @@ -121,10 +121,9 @@ def compute_update(current_ref, comment, latest): # Major-only tag like @v6 -> update to @v8 only if that tag actually exists. if int(latest_major.group(1)) > int(current_major.group(1)) and latest.get("major_tag"): return latest["major_tag"], comment - elif current_ref != latest_tag: - # Specific tag like @v2.8.0 -> update to the latest tag only when it is semantically newer. - if is_newer_version(current_ref, latest_tag): - return latest_tag, comment + # Specific tag like @v2.8.0 -> update to the latest tag only when it is semantically newer. + elif current_ref != latest_tag and is_newer_version(current_ref, latest_tag): + return latest_tag, comment return None @@ -180,6 +179,7 @@ def get_open_pr_titles(org, repo): ["gh", "pr", "list", "--repo", f"{org}/{repo}", "--state", "open", "--json", "title", "--limit", "100"], capture_output=True, text=True, + check=False, ) if result.returncode != 0: return set() diff --git a/actions/dispatch_actions.py b/actions/dispatch_actions.py index 431329a..1b28f8f 100644 --- a/actions/dispatch_actions.py +++ b/actions/dispatch_actions.py @@ -4,7 +4,7 @@ from __future__ import annotations import os import time -from datetime import datetime +from datetime import datetime, timezone from .utils import ACTIONS_CREDIT, GITHUB_API_URL, Action @@ -139,7 +139,7 @@ def update_comment(event, comment_body: str, command: str, triggered_actions: li if not triggered_actions: return - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC") + timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") failed = all(action.get("error") for action in triggered_actions) status = ( "No GitHub Actions workflows were started" if failed else "GitHub Actions below triggered via workflow dispatch" diff --git a/actions/format_python_docstrings.py b/actions/format_python_docstrings.py index 627a088..3595fc2 100644 --- a/actions/format_python_docstrings.py +++ b/actions/format_python_docstrings.py @@ -473,7 +473,8 @@ def format_python_file(text: str, width: int = 120, start_newline: bool = False) nl[0] = src[sl][:sc] + nl[0] nl[-1] += src[el][ec:] src[sl : el + 1] = nl - except Exception: + except Exception as e: + print(f"Failed to apply docstring replacement: {e}") continue return "\n".join(src) @@ -505,7 +506,8 @@ def iter_py_files(paths: list[Path]) -> list[Path]: stack.append(child) elif child.is_file() and child.suffix == ".py": out.append(child) - except Exception: + except Exception as e: + print(f"Failed to inspect {d}: {e}") continue return list(dict.fromkeys(sorted(out))) diff --git a/actions/github_report.py b/actions/github_report.py index 7627f4a..3850d02 100644 --- a/actions/github_report.py +++ b/actions/github_report.py @@ -160,6 +160,7 @@ def auto_merge_actions_prs(org, repos): ], capture_output=True, text=True, + check=False, ) if result.returncode == 0: prs.extend(json.loads(result.stdout or "[]")) @@ -232,6 +233,7 @@ def auto_merge_actions_prs(org, repos): ], capture_output=True, text=True, + check=False, ) if result.returncode == 0: print(f" ✅ Merged {pr_ref}") diff --git a/actions/summarize_release.py b/actions/summarize_release.py index 278154d..e2637ef 100644 --- a/actions/summarize_release.py +++ b/actions/summarize_release.py @@ -6,7 +6,7 @@ import os import re import subprocess import time -from datetime import datetime +from datetime import datetime, timezone from .utils import GITHUB_API_URL, Action, get_response, remove_html_comments @@ -60,7 +60,7 @@ def get_prs_between_tags(event, previous_tag: str, latest_tag: str) -> list: ) # Sort PRs by merge date - prs.sort(key=lambda x: datetime.strptime(x["merged_at"], "%Y-%m-%dT%H:%M:%SZ")) + prs.sort(key=lambda x: datetime.strptime(x["merged_at"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)) return prs diff --git a/actions/update_file_headers.py b/actions/update_file_headers.py index 6adfa3f..52f7689 100644 --- a/actions/update_file_headers.py +++ b/actions/update_file_headers.py @@ -4,7 +4,7 @@ from __future__ import annotations import os import re -from datetime import datetime +from datetime import datetime, timezone from pathlib import Path from actions.utils import Action @@ -178,7 +178,7 @@ def update_file(file_path, prefix, block_start, block_end, base_header): def main(*args, **kwargs): """Automates file header updates for supported file types under the current working directory.""" event = Action(*args, **kwargs) - current_year = datetime.now().year + current_year = datetime.now(timezone.utc).year repository = (event.repository or "").lower() # Only process repos owned by the Ultralytics organization diff --git a/actions/utils/github_utils.py b/actions/utils/github_utils.py index c72b261..05b4f8b 100644 --- a/actions/utils/github_utils.py +++ b/actions/utils/github_utils.py @@ -444,9 +444,12 @@ Thank you 🙏 for commit in data["commits"]["nodes"]: commit_data = commit["commit"] for user_type in ["author", "committer"]: - if user := commit_data[user_type].get("user"): - if user["__typename"] != "Bot" and (login := user.get("login")): - contributors.add(login) + if ( + (user := commit_data[user_type].get("user")) + and user["__typename"] != "Bot" + and (login := user.get("login")) + ): + contributors.add(login) contributors.discard(author) contributors.discard(username) diff --git a/tests/test_dispatch_actions.py b/tests/test_dispatch_actions.py index 911bb10..7a2c335 100644 --- a/tests/test_dispatch_actions.py +++ b/tests/test_dispatch_actions.py @@ -1,6 +1,6 @@ # Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license -from datetime import datetime +from datetime import datetime, timezone from unittest.mock import MagicMock, patch from actions.dispatch_actions import ( @@ -40,9 +40,10 @@ def test_get_pr_branch_fork(): "base": {"repo": {"id": 1}}, } - with patch("time.time", return_value=1234567.890), patch("subprocess.run") as mock_run: - with patch("os.environ.get", return_value="test-token"): - branch, temp_branch = get_pr_branch(mock_event) + with patch("time.time", return_value=1234567.890), patch("subprocess.run") as mock_run, patch( + "os.environ.get", return_value="test-token" + ): + branch, temp_branch = get_pr_branch(mock_event) assert branch == "temp-ci-456-1234567890" assert temp_branch == "temp-ci-456-1234567890" @@ -204,7 +205,7 @@ def test_update_comment_function(): # Mock datetime to have a consistent timestamp with patch("actions.dispatch_actions.datetime") as mock_datetime: - mock_datetime.now.return_value = datetime(2023, 1, 1, 12, 0, 0) + mock_datetime.now.return_value = datetime(2023, 1, 1, 12, 0, 0, tzinfo=timezone.utc) # Call without capturing return value update_comment(mock_event, comment_body, RUN_CI_KEYWORD, triggered_actions) @@ -271,12 +272,12 @@ def test_main_triggers_ci_only(): } mock_event.is_org_member.return_value = True - with patch("actions.dispatch_actions.get_pr_branch") as mock_get_branch: - with patch("actions.dispatch_actions.trigger_and_get_workflow_info") as mock_trigger: - with patch("actions.dispatch_actions.update_comment"): - mock_get_branch.return_value = ("feature-branch", None) - mock_trigger.return_value = [{"name": "CI", "file": "ci.yml", "url": "url", "run_number": 1}] - main() + with patch("actions.dispatch_actions.get_pr_branch") as mock_get_branch, patch( + "actions.dispatch_actions.trigger_and_get_workflow_info" + ) as mock_trigger, patch("actions.dispatch_actions.update_comment"): + mock_get_branch.return_value = ("feature-branch", None) + mock_trigger.return_value = [{"name": "CI", "file": "ci.yml", "url": "url", "run_number": 1}] + main() mock_trigger.assert_called_once_with(mock_event, "feature-branch", ["ci.yml"], None) @@ -294,12 +295,12 @@ def test_main_triggers_all_workflows(): } mock_event.is_org_member.return_value = True - with patch("actions.dispatch_actions.get_pr_branch") as mock_get_branch: - with patch("actions.dispatch_actions.trigger_and_get_workflow_info") as mock_trigger: - with patch("actions.dispatch_actions.update_comment"): - mock_get_branch.return_value = ("feature-branch", None) - mock_trigger.return_value = [{"name": "CI", "file": "ci.yml", "url": "url", "run_number": 1}] - main() + with patch("actions.dispatch_actions.get_pr_branch") as mock_get_branch, patch( + "actions.dispatch_actions.trigger_and_get_workflow_info" + ) as mock_trigger, patch("actions.dispatch_actions.update_comment"): + mock_get_branch.return_value = ("feature-branch", None) + mock_trigger.return_value = [{"name": "CI", "file": "ci.yml", "url": "url", "run_number": 1}] + main() mock_trigger.assert_called_once_with(mock_event, "feature-branch", ["ci.yml", "docker.yml"], None) @@ -317,14 +318,12 @@ def test_main_triggers_docker_only(): } mock_event.is_org_member.return_value = True - with patch("actions.dispatch_actions.get_pr_branch") as mock_get_branch: - with patch("actions.dispatch_actions.trigger_and_get_workflow_info") as mock_trigger: - with patch("actions.dispatch_actions.update_comment"): - mock_get_branch.return_value = ("feature-branch", None) - mock_trigger.return_value = [ - {"name": "Docker", "file": "docker.yml", "url": "url", "run_number": 1} - ] - main() + with patch("actions.dispatch_actions.get_pr_branch") as mock_get_branch, patch( + "actions.dispatch_actions.trigger_and_get_workflow_info" + ) as mock_trigger, patch("actions.dispatch_actions.update_comment"): + mock_get_branch.return_value = ("feature-branch", None) + mock_trigger.return_value = [{"name": "Docker", "file": "docker.yml", "url": "url", "run_number": 1}] + main() mock_trigger.assert_called_once_with(mock_event, "feature-branch", ["docker.yml"], None) diff --git a/tests/test_file_headers.py b/tests/test_file_headers.py index 6d8eec3..a73a28d 100644 --- a/tests/test_file_headers.py +++ b/tests/test_file_headers.py @@ -130,23 +130,24 @@ def test_ignore_paths(): def test_main_real_files(): """Test main function on actual repository files.""" - with patch("actions.update_file_headers.update_file", return_value=False) as mock_update: - with patch("actions.update_file_headers.Action") as mock_action: - mock_action.return_value.repository = "ultralytics/actions" - mock_action.return_value.is_repo_private.return_value = False - main() - assert mock_update.call_count > 0 + with patch("actions.update_file_headers.update_file", return_value=False) as mock_update, patch( + "actions.update_file_headers.Action" + ) as mock_action: + mock_action.return_value.repository = "ultralytics/actions" + mock_action.return_value.is_repo_private.return_value = False + main() + assert mock_update.call_count > 0 def test_main_with_custom_header(): """Test main function with custom header environment variable.""" - with patch("actions.update_file_headers.update_file", return_value=False): - with patch("actions.update_file_headers.HEADER", "Custom Test Header"): - with patch("actions.update_file_headers.Action") as mock_action: - mock_event = mock_action.return_value - mock_event.repository = "test/repo" - main() - mock_action.assert_called_once() + with patch("actions.update_file_headers.update_file", return_value=False), patch( + "actions.update_file_headers.HEADER", "Custom Test Header" + ), patch("actions.update_file_headers.Action") as mock_action: + mock_event = mock_action.return_value + mock_event.repository = "test/repo" + main() + mock_action.assert_called_once() def test_main_private_and_skipped_repos(): @@ -155,22 +156,22 @@ def test_main_private_and_skipped_repos(): tmp_path = Path(tmp_dir) (tmp_path / "app.py").write_text("print('hello')\n") - with patch("actions.update_file_headers.Path.cwd", return_value=tmp_path): - with patch("actions.update_file_headers.update_file", return_value=False) as mock_update: - with patch("actions.update_file_headers.Action") as mock_action: - mock_event = mock_action.return_value - mock_event.repository = "ultralytics/private" - mock_event.is_repo_private.return_value = True - main() - assert any("CONFIDENTIAL" in call.args[4] for call in mock_update.call_args_list) + with patch("actions.update_file_headers.Path.cwd", return_value=tmp_path), patch( + "actions.update_file_headers.update_file", return_value=False + ) as mock_update, patch("actions.update_file_headers.Action") as mock_action: + mock_event = mock_action.return_value + mock_event.repository = "ultralytics/private" + mock_event.is_repo_private.return_value = True + main() + assert any("CONFIDENTIAL" in call.args[4] for call in mock_update.call_args_list) - with patch("actions.update_file_headers.update_file", return_value=False) as mock_update: - with patch("actions.update_file_headers.HEADER", None): - with patch("actions.update_file_headers.Action") as mock_action: - mock_action.return_value.repository = "other/repo" - main() - mock_action.return_value.is_repo_private.assert_not_called() - mock_update.assert_not_called() + with patch("actions.update_file_headers.update_file", return_value=False) as mock_update, patch( + "actions.update_file_headers.HEADER", None + ), patch("actions.update_file_headers.Action") as mock_action: + mock_action.return_value.repository = "other/repo" + main() + mock_action.return_value.is_repo_private.assert_not_called() + mock_update.assert_not_called() def test_main_updates_files_in_current_directory(): @@ -180,11 +181,12 @@ def test_main_updates_files_in_current_directory(): test_file = tmp_path / "app.py" test_file.write_text("print('hello')\n") - with patch("actions.update_file_headers.Path.cwd", return_value=tmp_path): - with patch("actions.update_file_headers.Action") as mock_action: - mock_event = mock_action.return_value - mock_event.repository = "ultralytics/actions" - mock_event.is_repo_private.return_value = False - main() + with patch("actions.update_file_headers.Path.cwd", return_value=tmp_path), patch( + "actions.update_file_headers.Action" + ) as mock_action: + mock_event = mock_action.return_value + mock_event.repository = "ultralytics/actions" + mock_event.is_repo_private.return_value = False + main() assert test_file.read_text().startswith("# Ultralytics 🚀 AGPL-3.0 License") diff --git a/tests/test_github_utils.py b/tests/test_github_utils.py index 503e09e..81b8d38 100644 --- a/tests/test_github_utils.py +++ b/tests/test_github_utils.py @@ -11,19 +11,20 @@ from actions.utils import Action, check_pypi_version, ultralytics_actions_info @pytest.mark.skipif(sys.version_info < (3, 11), reason="tomllib requires Python 3.11+") def test_check_pypi_version(): """Test check_pypi_version function.""" - with patch("tomllib.load", return_value={"project": {"name": "test-package", "version": "1.0.0"}}): - with patch("requests.get") as mock_get: - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.elapsed.total_seconds.return_value = 0.5 - mock_response.json.return_value = {"info": {"version": "0.9.0"}} - mock_get.return_value = mock_response + with patch("tomllib.load", return_value={"project": {"name": "test-package", "version": "1.0.0"}}), patch( + "requests.get" + ) as mock_get: + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.elapsed.total_seconds.return_value = 0.5 + mock_response.json.return_value = {"info": {"version": "0.9.0"}} + mock_get.return_value = mock_response - local_version, online_version, publish = check_pypi_version() + local_version, online_version, publish = check_pypi_version() - assert local_version == "1.0.0" - assert online_version == "0.9.0" - assert publish is True + assert local_version == "1.0.0" + assert online_version == "0.9.0" + assert publish is True def test_action_init(): @@ -89,11 +90,12 @@ def test_get_pr_contributors_excludes_bots(): def test_load_event_data(): """Test loading event data from file.""" - with patch("pathlib.Path.exists", return_value=True): - with patch("pathlib.Path.read_text", return_value='{"test": "data"}'): - action = Action() - data = action._load_event_data("fake_path") - assert data == {"test": "data"} + with patch("pathlib.Path.exists", return_value=True), patch( + "pathlib.Path.read_text", return_value='{"test": "data"}' + ): + action = Action() + data = action._load_event_data("fake_path") + assert data == {"test": "data"} def test_ultralytics_actions_info(): diff --git a/tests/test_openai_utils.py b/tests/test_openai_utils.py index 4d954fd..060bf94 100644 --- a/tests/test_openai_utils.py +++ b/tests/test_openai_utils.py @@ -88,9 +88,10 @@ def test_remove_outer_codeblocks(): def test_get_review_model_override(): """Test review model override logic.""" - with patch("actions.utils.openai_utils.REVIEW_MODEL", "claude-opus-4-7"): - with patch("actions.utils.openai_utils.MODEL", "gpt-5.6-terra"): - assert get_review_model() == "claude-opus-4-7" + with patch("actions.utils.openai_utils.REVIEW_MODEL", "claude-opus-4-7"), patch( + "actions.utils.openai_utils.MODEL", "gpt-5.6-terra" + ): + assert get_review_model() == "claude-opus-4-7" def test_get_review_model_fallback(): @@ -120,9 +121,10 @@ def test_get_response(mock_post): messages = [{"role": "system", "content": "You are a helpful assistant"}, {"role": "user", "content": "Hello"}] # Use a context manager for the environment variable - with patch.dict("os.environ", {"OPENAI_API_KEY": "test-key"}, clear=False): - with patch("actions.utils.openai_utils.OPENAI_API_KEY", "test-key"): - result = get_response(messages, check_links=False) + with patch.dict("os.environ", {"OPENAI_API_KEY": "test-key"}, clear=False), patch( + "actions.utils.openai_utils.OPENAI_API_KEY", "test-key" + ): + result = get_response(messages, check_links=False) assert result == "Test response from OpenAI" mock_post.assert_called_once() @@ -186,9 +188,10 @@ def test_get_response_with_link_check(mock_check_links, mock_post): messages = [{"role": "user", "content": "Hello"}] # Use a context manager for the environment variable - with patch.dict("os.environ", {"OPENAI_API_KEY": "test-key"}, clear=False): - with patch("actions.utils.openai_utils.OPENAI_API_KEY", "test-key"): - result = get_response(messages) + with patch.dict("os.environ", {"OPENAI_API_KEY": "test-key"}, clear=False), patch( + "actions.utils.openai_utils.OPENAI_API_KEY", "test-key" + ): + result = get_response(messages) assert result == "Response with https://example.com link" mock_check_links.assert_called_once() @@ -350,16 +353,17 @@ def test_get_agent_response_summarizes_after_max_turns(mock_post): } ] - with patch("actions.utils.openai_utils.OPENAI_API_KEY", "test-key"): - with patch("actions.utils.openai_utils.time.sleep") as mock_sleep: - result = get_agent_response( - [{"role": "user", "content": "review"}], - tools=tools, - tool_handlers={"lookup_value": lambda value: f"raw tool output for {value}"}, - text_format={"format": {"type": "json_schema", "name": "review", "strict": True, "schema": schema}}, - max_turns=1, - retries=0, - ) + with patch("actions.utils.openai_utils.OPENAI_API_KEY", "test-key"), patch( + "actions.utils.openai_utils.time.sleep" + ) as mock_sleep: + result = get_agent_response( + [{"role": "user", "content": "review"}], + tools=tools, + tool_handlers={"lookup_value": lambda value: f"raw tool output for {value}"}, + text_format={"format": {"type": "json_schema", "name": "review", "strict": True, "schema": schema}}, + max_turns=1, + retries=0, + ) assert result == {"comments": [], "summary": "synthesized"} assert mock_post.call_count == 3 @@ -396,10 +400,10 @@ def test_get_response_anthropic(mock_post): messages = [{"role": "system", "content": "You are a helpful assistant"}, {"role": "user", "content": "Hello"}] - with patch.dict("os.environ", {"ANTHROPIC_API_KEY": "test-key"}, clear=False): - with patch("actions.utils.openai_utils.ANTHROPIC_API_KEY", "test-key"): - with patch("builtins.print") as mock_print: - result = get_response(messages, check_links=False, model="claude-sonnet-4-6", background=True) + with patch.dict("os.environ", {"ANTHROPIC_API_KEY": "test-key"}, clear=False), patch( + "actions.utils.openai_utils.ANTHROPIC_API_KEY", "test-key" + ), patch("builtins.print") as mock_print: + result = get_response(messages, check_links=False, model="claude-sonnet-4-6", background=True) assert result == "Test response from Claude" printed = "\n".join(str(c.args[0]) for c in mock_print.call_args_list if c.args)