Ruff 0.16.0 compatibility changes (#834)

This commit is contained in:
Glenn Jocher
2026-07-24 01:51:34 +02:00
committed by GitHub
parent b8900b5fef
commit dde1fcba37
13 changed files with 129 additions and 115 deletions
+1 -1
View File
@@ -28,4 +28,4 @@
# ├── test_summarize_pr.py
# └── ...
__version__ = "0.2.35"
__version__ = "0.2.36"
+1 -1
View File
@@ -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"]
+4 -4
View File
@@ -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()
+2 -2
View File
@@ -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"
+4 -2
View File
@@ -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)))
+2
View File
@@ -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}")
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+6 -3
View File
@@ -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)
+24 -25
View File
@@ -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)
+36 -34
View File
@@ -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")
+18 -16
View File
@@ -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():
+27 -23
View File
@@ -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)