Skip to content

Commit 3addb5a

Browse files
Enable Ruff checks for mutable defaults (B006, B008, B039, RUF012) (#1055)
Co-authored-by: openhands <openhands@all-hands.dev>
1 parent 0a23648 commit 3addb5a

File tree

6 files changed

+42
-21
lines changed

6 files changed

+42
-21
lines changed

openhands-agent-server/openhands/agent_server/file_router.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
@file_router.post("/upload/{path:path}")
3131
async def upload_file(
3232
path: Annotated[str, FastApiPath(alias="path", description="Absolute file path.")],
33-
file: UploadFile = File(...),
33+
file: Annotated[UploadFile, File(...)],
3434
) -> Success:
3535
"""Upload a file to the workspace."""
3636
logger.info(f"Uploading file: {path}")

openhands-sdk/openhands/sdk/agent/agent.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -309,9 +309,9 @@ def _get_action_event(
309309
tool_call: MessageToolCall,
310310
llm_response_id: str,
311311
on_event: ConversationCallbackType,
312-
thought: list[TextContent] = [],
312+
thought: list[TextContent] | None = None,
313313
reasoning_content: str | None = None,
314-
thinking_blocks: list[ThinkingBlock | RedactedThinkingBlock] = [],
314+
thinking_blocks: list[ThinkingBlock | RedactedThinkingBlock] | None = None,
315315
responses_reasoning_item: ReasoningItemModel | None = None,
316316
) -> ActionEvent | None:
317317
"""Converts a tool call into an ActionEvent, validating arguments.
@@ -328,9 +328,9 @@ def _get_action_event(
328328
# Persist assistant function_call so next turn has matching call_id
329329
tc_event = ActionEvent(
330330
source="agent",
331-
thought=thought,
331+
thought=thought or [],
332332
reasoning_content=reasoning_content,
333-
thinking_blocks=thinking_blocks,
333+
thinking_blocks=thinking_blocks or [],
334334
responses_reasoning_item=responses_reasoning_item,
335335
tool_call=tool_call,
336336
tool_name=tool_call.name,
@@ -380,9 +380,9 @@ def _get_action_event(
380380
# Persist assistant function_call so next turn has matching call_id
381381
tc_event = ActionEvent(
382382
source="agent",
383-
thought=thought,
383+
thought=thought or [],
384384
reasoning_content=reasoning_content,
385-
thinking_blocks=thinking_blocks,
385+
thinking_blocks=thinking_blocks or [],
386386
responses_reasoning_item=responses_reasoning_item,
387387
tool_call=tool_call,
388388
tool_name=tool_call.name,
@@ -401,9 +401,9 @@ def _get_action_event(
401401

402402
action_event = ActionEvent(
403403
action=action,
404-
thought=thought,
404+
thought=thought or [],
405405
reasoning_content=reasoning_content,
406-
thinking_blocks=thinking_blocks,
406+
thinking_blocks=thinking_blocks or [],
407407
responses_reasoning_item=responses_reasoning_item,
408408
tool_name=tool.name,
409409
tool_call_id=tool_call.id,

pyproject.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,25 @@ select = [
4343
"UP", # pyupgrade
4444
"ARG", # flake8-unused-arguments
4545
]
46+
# Enforce rules that catch mutable defaults and related pitfalls
47+
# - B006: mutable-argument-default
48+
# - B008: function-call-in-default-argument
49+
# - B039: mutable-contextvar-default
50+
# - RUF012: mutable-class-default
51+
extend-select = ["B006", "B008", "B039", "RUF012"]
4652

4753
[tool.ruff.lint.per-file-ignores]
4854
# Test files often have unused arguments (fixtures, mocks, interface implementations)
4955
"tests/**/*.py" = ["ARG"]
5056

57+
58+
# Allowlist safe default calls for flake8-bugbear rules (e.g., FastAPI Depends)
59+
[tool.ruff.lint.flake8-bugbear]
60+
extend-immutable-calls = [
61+
"fastapi.Depends",
62+
"fastapi.params.Depends",
63+
]
64+
5165
[tool.ruff.lint.isort]
5266
known-first-party = ["openhands"]
5367
combine-as-imports = true

tests/sdk/agent/test_agent_llms_are_discoverable.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from pydantic import Field
2+
13
from openhands.sdk import LLM, Agent, LLMSummarizingCondenser
24
from openhands.sdk.llm.router import MultimodalRouter
35

@@ -36,7 +38,7 @@ def test_automatic_llm_discovery_for_multiple_llms():
3638

3739
def test_automatic_llm_discovery_for_custom_agent_with_duplicates():
3840
class CustomAgent(Agent):
39-
model_routers: list[LLM] = []
41+
model_routers: list[LLM] = Field(default_factory=list)
4042

4143
llm_service_id = "main-agent"
4244
router_service_id = "secondary_llm"
@@ -94,8 +96,8 @@ def test_automatic_llm_discovery_with_multimodal_router():
9496

9597
def test_automatic_llm_discovery_with_llm_as_base_class():
9698
class NewLLM(LLM):
97-
list_llms: list[LLM] = []
98-
dict_llms: dict[str, LLM] = {}
99+
list_llms: list[LLM] = Field(default_factory=list)
100+
dict_llms: dict[str, LLM] = Field(default_factory=dict)
99101
raw_llm: LLM | None = None
100102

101103
list_llm = LLM(model="list-model", usage_id="list-model")

tests/sdk/conversation/test_visualizer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import json
44

5+
from pydantic import Field
56
from rich.text import Text
67

78
from openhands.sdk.conversation.visualizer import (
@@ -34,7 +35,7 @@ class VisualizerMockAction(Action):
3435
class VisualizerCustomAction(Action):
3536
"""Custom action with overridden visualize method."""
3637

37-
task_list: list[dict] = []
38+
task_list: list[dict] = Field(default_factory=list)
3839

3940
@property
4041
def visualize(self) -> Text:

tests/sdk/security/test_security_analyzer.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Tests for the SecurityAnalyzer class."""
22

3+
from pydantic import Field
4+
35
from openhands.sdk.event import ActionEvent, PauseEvent
46
from openhands.sdk.llm import MessageToolCall, TextContent
57
from openhands.sdk.security.analyzer import SecurityAnalyzerBase
@@ -19,9 +21,9 @@ class SecurityAnalyzer(SecurityAnalyzerBase):
1921
"""
2022

2123
risk_return_value: SecurityRisk = SecurityRisk.LOW
22-
security_risk_calls: list[ActionEvent] = []
23-
handle_api_request_calls: list[dict] = []
24-
close_calls: list[bool] = []
24+
security_risk_calls: list[ActionEvent] = Field(default_factory=list)
25+
handle_api_request_calls: list[dict] = Field(default_factory=list)
26+
close_calls: list[bool] = Field(default_factory=list)
2527

2628
def security_risk(self, action: ActionEvent) -> SecurityRisk:
2729
"""Return configurable risk level for testing."""
@@ -131,11 +133,13 @@ def test_analyze_pending_actions_mixed_risks() -> None:
131133

132134
class VariableRiskAnalyzer(SecurityAnalyzer):
133135
call_count: int = 0
134-
risks: list[SecurityRisk] = [
135-
SecurityRisk.LOW,
136-
SecurityRisk.HIGH,
137-
SecurityRisk.MEDIUM,
138-
]
136+
risks: list[SecurityRisk] = Field(
137+
default_factory=lambda: [
138+
SecurityRisk.LOW,
139+
SecurityRisk.HIGH,
140+
SecurityRisk.MEDIUM,
141+
]
142+
)
139143

140144
def security_risk(self, action: ActionEvent) -> SecurityRisk:
141145
risk = self.risks[self.call_count % len(self.risks)]

0 commit comments

Comments
 (0)