Fix mypy errors in runtime/utils directory (#6902)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Graham Neubig 2025-03-25 08:00:03 -07:00 committed by GitHub
parent 0efe4feb2a
commit 86c6feafcc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 87 additions and 73 deletions

View File

@ -87,13 +87,14 @@ class RunloopRuntime(ActionExecutionClient):
# Add some additional commands based on our image
# NB: start off as root, action_execution_server will ultimately choose user but expects all context
# (ie browser) to be installed as root
start_command = (
# Convert start_command list to a single command string with additional setup
start_command_str = (
'export MAMBA_ROOT_PREFIX=/openhands/micromamba && '
'cd /openhands/code && '
+ '/openhands/micromamba/bin/micromamba run -n openhands poetry config virtualenvs.path /openhands/poetry && '
'/openhands/micromamba/bin/micromamba run -n openhands poetry config virtualenvs.path /openhands/poetry && '
+ ' '.join(start_command)
)
entrypoint = f"sudo bash -c '{start_command}'"
entrypoint = f"sudo bash -c '{start_command_str}'"
devbox = self.runloop_api_client.devboxes.create(
entrypoint=entrypoint,

View File

@ -4,8 +4,9 @@ import time
import traceback
import uuid
from enum import Enum
from typing import Any
import bashlex
import bashlex # type: ignore
import libtmux
from openhands.core.logger import openhands_logger as logger
@ -19,7 +20,7 @@ from openhands.events.observation.commands import (
from openhands.utils.shutdown_listener import should_continue
def split_bash_commands(commands):
def split_bash_commands(commands: str) -> list[str]:
if not commands.strip():
return ['']
try:
@ -82,7 +83,7 @@ def escape_bash_special_chars(command: str) -> str:
parts = []
last_pos = 0
def visit_node(node):
def visit_node(node: Any) -> None:
nonlocal last_pos
if (
node.kind == 'redirect'
@ -183,7 +184,7 @@ class BashSession:
self._initialized = False
self.max_memory_mb = max_memory_mb
def initialize(self):
def initialize(self) -> None:
self.server = libtmux.Server()
_shell_command = '/bin/bash'
if self.username in ['root', 'openhands']:
@ -203,7 +204,7 @@ class BashSession:
session_name = f'openhands-{self.username}-{uuid.uuid4()}'
self.session = self.server.new_session(
session_name=session_name,
start_directory=self.work_dir,
start_directory=self.work_dir, # This parameter is supported by libtmux
kill_session=True,
x=1000,
y=1000,
@ -218,7 +219,7 @@ class BashSession:
self.window = self.session.new_window(
window_name='bash',
window_shell=window_command,
start_directory=self.work_dir,
start_directory=self.work_dir, # This parameter is supported by libtmux
)
self.pane = self.window.attached_pane
logger.debug(f'pane: {self.pane}; history_limit: {self.session.history_limit}')
@ -241,7 +242,7 @@ class BashSession:
self._cwd = os.path.abspath(self.work_dir)
self._initialized = True
def __del__(self):
def __del__(self) -> None:
"""Ensure the session is closed when the object is destroyed."""
self.close()
@ -256,7 +257,7 @@ class BashSession:
)
return content
def close(self):
def close(self) -> None:
"""Clean up the session."""
if self._closed:
return
@ -264,7 +265,7 @@ class BashSession:
self._closed = True
@property
def cwd(self):
def cwd(self) -> str:
return self._cwd
def _is_special_key(self, command: str) -> bool:
@ -273,7 +274,7 @@ class BashSession:
_command = command.strip()
return _command.startswith('C-') and len(_command) == 3
def _clear_screen(self):
def _clear_screen(self) -> None:
"""Clear the tmux pane screen and history."""
self.pane.send_keys('C-l', enter=False)
time.sleep(0.1)
@ -424,7 +425,7 @@ class BashSession:
metadata=metadata,
)
def _ready_for_next_command(self):
def _ready_for_next_command(self) -> None:
"""Reset the content buffer for a new command."""
# Clear the current content
self._clear_screen()
@ -498,9 +499,9 @@ class BashSession:
if len(splited_commands) > 1:
return ErrorObservation(
content=(
f'ERROR: Cannot execute multiple commands at once.\n'
f'Please run each command separately OR chain them into a single command via && or ;\n'
f'Provided commands:\n{"\n".join(f"({i+1}) {cmd}" for i, cmd in enumerate(splited_commands))}'
f"ERROR: Cannot execute multiple commands at once.\n"
f"Please run each command separately OR chain them into a single command via && or ;\n"
f"Provided commands:\n{'\n'.join(f'({i + 1}) {cmd}' for i, cmd in enumerate(splited_commands))}"
)
)
@ -573,8 +574,8 @@ class BashSession:
logger.debug(
f'PANE CONTENT GOT after {time.time() - _start_time:.2f} seconds'
)
logger.debug(f'BEGIN OF PANE CONTENT: {cur_pane_output.split("\n")[:10]}')
logger.debug(f'END OF PANE CONTENT: {cur_pane_output.split("\n")[-10:]}')
logger.debug(f"BEGIN OF PANE CONTENT: {cur_pane_output.split('\n')[:10]}")
logger.debug(f"END OF PANE CONTENT: {cur_pane_output.split('\n')[-10:]}")
ps1_matches = CmdOutputMetadata.matches_ps1_metadata(cur_pane_output)
if cur_pane_output != last_pane_output:
last_pane_output = cur_pane_output

View File

@ -18,7 +18,7 @@ def get_action_execution_server_startup_command(
python_prefix: list[str] = DEFAULT_PYTHON_PREFIX,
override_user_id: int | None = None,
override_username: str | None = None,
):
) -> list[str]:
sandbox_config = app_config.sandbox
# Plugin args

View File

@ -2,8 +2,9 @@ import os
import re
import tempfile
from abc import ABC, abstractmethod
from typing import Any
from openhands_aci.utils.diff import get_diff
from openhands_aci.utils.diff import get_diff # type: ignore
from openhands.core.config import AppConfig
from openhands.core.logger import openhands_logger as logger
@ -52,12 +53,12 @@ IMPORTANT:
""".strip()
def _extract_code(string):
def _extract_code(string: str) -> str | None:
pattern = r'```(?:\w*\n)?(.*?)```'
matches = re.findall(pattern, string, re.DOTALL)
if not matches:
return None
return matches[0]
return str(matches[0])
def get_new_file_contents(
@ -102,7 +103,7 @@ class FileEditRuntimeMixin(FileEditRuntimeInterface):
# This restricts the number of lines we can edit to avoid exceeding the token limit.
MAX_LINES_TO_EDIT = 300
def __init__(self, enable_llm_editor: bool, *args, **kwargs):
def __init__(self, enable_llm_editor: bool, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.enable_llm_editor = enable_llm_editor
@ -301,10 +302,10 @@ class FileEditRuntimeMixin(FileEditRuntimeInterface):
'Here are some snippets that maybe relevant to the provided edit.\n'
)
for i, chunk in enumerate(topk_chunks):
error_msg += f'[begin relevant snippet {i+1}. Line range: L{chunk.line_range[0]}-L{chunk.line_range[1]}. Similarity: {chunk.normalized_lcs}]\n'
error_msg += f'[begin relevant snippet {i + 1}. Line range: L{chunk.line_range[0]}-L{chunk.line_range[1]}. Similarity: {chunk.normalized_lcs}]\n'
error_msg += f'[Browse around it via `open_file("{action.path}", {(chunk.line_range[0] + chunk.line_range[1]) // 2})`]\n'
error_msg += chunk.visualize() + '\n'
error_msg += f'[end relevant snippet {i+1}]\n'
error_msg += f'[end relevant snippet {i + 1}]\n'
error_msg += '-' * 40 + '\n'
error_msg += 'Consider using `open_file` to explore around the relevant snippets if needed.\n'

View File

@ -14,7 +14,7 @@ def resolve_path(
working_directory: str,
workspace_base: str,
workspace_mount_path_in_sandbox: str,
):
) -> Path:
"""Resolve a file path to a path on the host filesystem.
Args:
@ -51,7 +51,7 @@ def resolve_path(
return path_in_host_workspace
def read_lines(all_lines: list[str], start=0, end=-1):
def read_lines(all_lines: list[str], start: int = 0, end: int = -1) -> list[str]:
start = max(start, 0)
start = min(start, len(all_lines))
end = -1 if end == -1 else max(end, 0)
@ -69,7 +69,12 @@ def read_lines(all_lines: list[str], start=0, end=-1):
async def read_file(
path, workdir, workspace_base, workspace_mount_path_in_sandbox, start=0, end=-1
path: str,
workdir: str,
workspace_base: str,
workspace_mount_path_in_sandbox: str,
start: int = 0,
end: int = -1,
) -> Observation:
try:
whole_path = resolve_path(
@ -95,7 +100,7 @@ async def read_file(
def insert_lines(
to_insert: list[str], original: list[str], start: int = 0, end: int = -1
):
) -> list[str]:
"""Insert the new content to the original content based on start and end"""
new_lines = [''] if start == 0 else original[:start]
new_lines += [i + '\n' for i in to_insert]
@ -104,13 +109,13 @@ def insert_lines(
async def write_file(
path,
workdir,
workspace_base,
workspace_mount_path_in_sandbox,
content,
start=0,
end=-1,
path: str,
workdir: str,
workspace_base: str,
workspace_mount_path_in_sandbox: str,
content: str,
start: int = 0,
end: int = -1,
) -> Observation:
insert = content.split('\n')

View File

@ -25,7 +25,7 @@ class LogStreamer:
self.stdout_thread.daemon = True
self.stdout_thread.start()
def _stream_logs(self):
def _stream_logs(self) -> None:
"""Stream logs from the Docker container to stdout."""
try:
for log_line in self.log_generator:
@ -37,11 +37,11 @@ class LogStreamer:
except Exception as e:
self.log('error', f'Error streaming docker logs to stdout: {e}')
def __del__(self):
def __del__(self) -> None:
if self.stdout_thread and self.stdout_thread.is_alive():
self.close(timeout=5)
def close(self, timeout: float = 5.0):
def close(self, timeout: float = 5.0) -> None:
"""Clean shutdown of the log streaming."""
self._stop_event.set()
if self.stdout_thread and self.stdout_thread.is_alive():

View File

@ -2,7 +2,7 @@
import threading
from memory_profiler import memory_usage
from memory_profiler import memory_usage # type: ignore
from openhands.core.logger import openhands_logger as logger
@ -10,11 +10,11 @@ from openhands.core.logger import openhands_logger as logger
class LogStream:
"""Stream-like object that redirects writes to a logger."""
def write(self, message):
def write(self, message: str) -> None:
if message and not message.isspace():
logger.info(f'[Memory usage] {message.strip()}')
def flush(self):
def flush(self) -> None:
pass
@ -26,7 +26,7 @@ class MemoryMonitor:
self.log_stream = LogStream()
self.enable = enable
def start_monitoring(self):
def start_monitoring(self) -> None:
"""Start monitoring memory usage."""
if not self.enable:
return
@ -34,7 +34,7 @@ class MemoryMonitor:
if self._monitoring_thread is not None:
return
def monitor_process():
def monitor_process() -> None:
try:
# Use memory_usage's built-in monitoring loop
mem_usage = memory_usage(
@ -55,7 +55,7 @@ class MemoryMonitor:
self._monitoring_thread.start()
logger.info('Memory monitoring started')
def stop_monitoring(self):
def stop_monitoring(self) -> None:
"""Stop monitoring memory usage."""
if not self.enable:
return

View File

@ -11,7 +11,7 @@ from openhands.utils.tenacity_stop import stop_if_should_exit
class RequestHTTPError(requests.HTTPError):
"""Exception raised when an error occurs in a request with details."""
def __init__(self, *args, detail=None, **kwargs):
def __init__(self, *args: Any, detail: Any = None, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.detail = detail
@ -22,7 +22,7 @@ class RequestHTTPError(requests.HTTPError):
return s
def is_retryable_error(exception):
def is_retryable_error(exception: Any) -> bool:
return (
isinstance(exception, requests.HTTPError)
and exception.response.status_code == 429
@ -56,4 +56,4 @@ def send_request(
response=e.response,
detail=_json.get('detail') if _json is not None else None,
) from e
return response
return response # type: ignore

View File

@ -9,7 +9,7 @@ from pathlib import Path
from typing import List
import docker
from dirhash import dirhash
from dirhash import dirhash # type: ignore
from jinja2 import Environment, FileSystemLoader
import openhands
@ -25,7 +25,7 @@ class BuildFromImageType(Enum):
LOCK = 'lock' # Fastest: Reuse the most recent image with the exact SAME dependencies (lock files)
def get_runtime_image_repo():
def get_runtime_image_repo() -> str:
return os.getenv('OH_RUNTIME_RUNTIME_IMAGE_REPO', 'ghcr.io/all-hands-ai/runtime')
@ -252,7 +252,7 @@ def prep_build_folder(
base_image: str,
build_from: BuildFromImageType,
extra_deps: str | None,
):
) -> None:
# Copy the source code to directory. It will end up in build_folder/code
# If package is not found, build from source code
openhands_source_dir = Path(openhands.__file__).parent
@ -301,7 +301,7 @@ def truncate_hash(hash: str) -> str:
return ''.join(result)
def get_hash_for_lock_files(base_image: str):
def get_hash_for_lock_files(base_image: str) -> str:
openhands_source_dir = Path(openhands.__file__).parent
md5 = hashlib.md5()
md5.update(base_image.encode())
@ -318,11 +318,11 @@ def get_hash_for_lock_files(base_image: str):
return result
def get_tag_for_versioned_image(base_image: str):
def get_tag_for_versioned_image(base_image: str) -> str:
return base_image.replace('/', '_s_').replace(':', '_t_').lower()[-96:]
def get_hash_for_source_files():
def get_hash_for_source_files() -> str:
openhands_source_dir = Path(openhands.__file__).parent
dir_hash = dirhash(
openhands_source_dir,
@ -348,7 +348,7 @@ def _build_sandbox_image(
versioned_tag: str | None,
platform: str | None = None,
extra_build_args: List[str] | None = None,
):
) -> str:
"""Build and tag the sandbox image. The image will be tagged with all tags that do not yet exist."""
names = [
f'{runtime_image_repo}:{source_tag}',

View File

@ -80,25 +80,29 @@ RUN mkdir -p ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-memory-monitor && \
{% macro install_dependencies() %}
# Install all dependencies
WORKDIR /openhands/code
RUN \
/openhands/micromamba/bin/micromamba config set changeps1 False && \
# Configure Poetry and create virtual environment
# Configure micromamba and poetry
RUN /openhands/micromamba/bin/micromamba config set changeps1 False && \
/openhands/micromamba/bin/micromamba run -n openhands poetry config virtualenvs.path /openhands/poetry && \
/openhands/micromamba/bin/micromamba run -n openhands poetry env use python3.12 && \
# Install project dependencies
/openhands/micromamba/bin/micromamba run -n openhands poetry install --only main,runtime --no-interaction --no-root && \
# Update and install additional tools
apt-get update && \
/openhands/micromamba/bin/micromamba run -n openhands poetry env use python3.12
# Install project dependencies in smaller chunks
RUN /openhands/micromamba/bin/micromamba run -n openhands poetry install --only main --no-interaction --no-root
RUN /openhands/micromamba/bin/micromamba run -n openhands poetry install --only runtime --no-interaction --no-root
# Install playwright and its dependencies
RUN apt-get update && \
/openhands/micromamba/bin/micromamba run -n openhands poetry run pip install playwright && \
/openhands/micromamba/bin/micromamba run -n openhands poetry run playwright install --with-deps chromium && \
# Set environment variables
echo "OH_INTERPRETER_PATH=$(/openhands/micromamba/bin/micromamba run -n openhands poetry run python -c "import sys; print(sys.executable)")" >> /etc/environment && \
# Clear caches
/openhands/micromamba/bin/micromamba run -n openhands poetry cache clear --all . -n && \
# Set permissions
/openhands/micromamba/bin/micromamba run -n openhands poetry run playwright install --with-deps chromium
# Set environment variables and permissions
RUN /openhands/micromamba/bin/micromamba run -n openhands poetry run python -c "import sys; print('OH_INTERPRETER_PATH=' + sys.executable)" >> /etc/environment && \
chmod -R g+rws /openhands/poetry && \
mkdir -p /openhands/workspace && chmod -R g+rws,o+rw /openhands/workspace && \
# Clean up
mkdir -p /openhands/workspace && chmod -R g+rws,o+rw /openhands/workspace
# Clear caches
RUN /openhands/micromamba/bin/micromamba run -n openhands poetry cache clear --all . -n && \
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
/openhands/micromamba/bin/micromamba clean --all

View File

@ -15,7 +15,9 @@ def check_port_available(port: int) -> bool:
sock.close()
def find_available_tcp_port(min_port=30000, max_port=39999, max_attempts=10) -> int:
def find_available_tcp_port(
min_port: int = 30000, max_port: int = 39999, max_attempts: int = 10
) -> int:
"""Find an available TCP port in a specified range.
Args: