mirror of
https://github.com/RustPython/RustPython.git
synced 2026-06-02 19:39:49 +09:00
Compare commits
5 Commits
2025-06-16
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| f1863cc40f | |||
| 5f4ab448cb | |||
| 8a646957eb | |||
| 9aa703e07d | |||
| 287cfea18a |
304
.github/scripts/code_review.py
vendored
Normal file
304
.github/scripts/code_review.py
vendored
Normal file
@@ -0,0 +1,304 @@
|
||||
"""Code Reviewer for Gitea."""
|
||||
|
||||
import asyncio
|
||||
import fnmatch
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
import aiohttp
|
||||
from model import Model
|
||||
|
||||
ACCESS_TOKEN = os.getenv("ACCESS_TOKEN", "")
|
||||
HEADERS = {"Authorization": f"token {ACCESS_TOKEN}"}
|
||||
|
||||
GITHUB_EVENT_PATH = os.getenv("GITHUB_EVENT_PATH")
|
||||
try:
|
||||
with open(GITHUB_EVENT_PATH, "r") as f:
|
||||
EVENT_DATA = json.load(f)
|
||||
except FileNotFoundError:
|
||||
print("Failed to load event data.")
|
||||
exit(1)
|
||||
|
||||
FULL_CONTEXT_MODEL_NAME = os.getenv("FULL_CONTEXT_MODEL", "")
|
||||
SINGLE_CHUNK_MODEL_NAME = os.getenv("SINGLE_CHUNK_MODEL", "")
|
||||
FULL_CONTEXT_API_KEY = os.getenv("FULL_CONTEXT_API_KEY", "")
|
||||
SINGLE_CHUNK_API_KEY = os.getenv("SINGLE_CHUNK_API_KEY", "")
|
||||
|
||||
EXCLUDE_PATTERNS = os.getenv("EXCLUDE", "").split(",")
|
||||
|
||||
|
||||
def get_diff() -> str | None:
|
||||
"""Get code difference between base and head from Gitea.
|
||||
|
||||
Returns:
|
||||
str | None: code difference between base and head, or None if failed to get diff
|
||||
"""
|
||||
url = EVENT_DATA["pull_request"]["diff_url"]
|
||||
try:
|
||||
response = requests.get(url, headers=HEADERS)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
except requests.RequestException as e:
|
||||
print(f"Failed to get diff: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def parse_diff(diff: str) -> list[dict[str, Any]]:
|
||||
"""Parse diff into list of dicts.
|
||||
|
||||
Args:
|
||||
diff: str, code difference between base and head
|
||||
|
||||
Returns:
|
||||
list[dict[str, Any]]: list of dicts, each dict represents a code chunks
|
||||
"""
|
||||
file_pattern = re.compile(
|
||||
r"(?s)diff --git a/(.+?) b/(.*?)\r?\n(.*?)(?=diff --git a/|$)", re.S
|
||||
)
|
||||
old_new_pattern = re.compile(r"(?m)^(---|\+\+\+)\s+(.*)$")
|
||||
chunk_range_pattern = re.compile(
|
||||
r"@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*?)?(?=@@|\Z)",
|
||||
re.MULTILINE | re.DOTALL,
|
||||
)
|
||||
list_diff = []
|
||||
for match in file_pattern.finditer(diff):
|
||||
diff_text = match.group(3)
|
||||
|
||||
old_new_match = list(old_new_pattern.finditer(diff_text))
|
||||
if len(old_new_match) != 2:
|
||||
continue
|
||||
|
||||
old_file = old_new_match[0].group(2)
|
||||
old_file = old_file.lstrip("a/") if old_file.startswith("a/") else old_file
|
||||
|
||||
new_file = old_new_match[1].group(2)
|
||||
if new_file == "/dev/null":
|
||||
print("Neglict deleted file")
|
||||
continue
|
||||
new_file = new_file.lstrip("b/")
|
||||
if any(fnmatch.fnmatch(new_file, pattern) for pattern in EXCLUDE_PATTERNS):
|
||||
print(f"Exclude file {new_file}")
|
||||
continue
|
||||
|
||||
output_diff_text = []
|
||||
for chunk_range_match in chunk_range_pattern.finditer(diff_text):
|
||||
old_idx = int(chunk_range_match.group(1))
|
||||
new_idx = int(chunk_range_match.group(3))
|
||||
for line in chunk_range_match.group(5).splitlines():
|
||||
if line.startswith("-"):
|
||||
output_diff_text.append(f"{old_idx} None {line}")
|
||||
old_idx += 1
|
||||
elif line.startswith("+"):
|
||||
output_diff_text.append(f"None {new_idx} {line}")
|
||||
new_idx += 1
|
||||
else:
|
||||
output_diff_text.append(f"{old_idx} {new_idx} {line}")
|
||||
old_idx += 1
|
||||
new_idx += 1
|
||||
|
||||
output_diff_text = "\n".join(output_diff_text)
|
||||
list_diff.append(
|
||||
{
|
||||
"file": new_file,
|
||||
"chunk": output_diff_text,
|
||||
}
|
||||
)
|
||||
return list_diff
|
||||
|
||||
|
||||
def create_comment(
|
||||
file: str, ai_response: list[dict[str, Any]]
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Create comments for single chunk review.
|
||||
|
||||
Args:
|
||||
file: str, file name
|
||||
ai_response: list[dict[str, Any]], AI response for single chunk review
|
||||
|
||||
Returns:
|
||||
list[dict[str, Any]]: comments for single chunk review
|
||||
"""
|
||||
comments = []
|
||||
for ai_response in ai_response:
|
||||
comments.append(
|
||||
{
|
||||
"body": f"[REVIEW] {ai_response['reviewComment']}",
|
||||
"path": file,
|
||||
"new_position": int(ai_response["lineNumber"]),
|
||||
}
|
||||
)
|
||||
return comments
|
||||
|
||||
|
||||
async def analyze_single_chunks(
|
||||
single_chunk_model: Model, parsed_diff: list[dict[str, Any]]
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Analyze single chunks and create comments.
|
||||
|
||||
Args:
|
||||
single_chunk_model: AI Session for single chunk analysis
|
||||
parsed_diff: list[dict[str, Any]], parsed diff
|
||||
|
||||
Returns:
|
||||
list[dict[str, Any]]: comments for single chunk review
|
||||
"""
|
||||
|
||||
async def process_single_chunk(diff: dict[str, Any]):
|
||||
file = diff["file"]
|
||||
chunk = diff["chunk"]
|
||||
response = await single_chunk_model.get_response_single_chunk(
|
||||
file, title, description, chunk
|
||||
)
|
||||
response = response.strip("`").lstrip("json").strip() or "[]"
|
||||
|
||||
try:
|
||||
response_json = json.loads(response)
|
||||
return create_comment(file, response_json)
|
||||
except json.JSONDecodeError:
|
||||
print(f"Failed to parse response: {response}")
|
||||
return []
|
||||
|
||||
title = EVENT_DATA["pull_request"]["title"]
|
||||
description = EVENT_DATA["pull_request"]["body"]
|
||||
tasks = [process_single_chunk(diff) for diff in parsed_diff]
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
# Flatten the list of comments
|
||||
comments = [comment for result in results for comment in result]
|
||||
return comments
|
||||
|
||||
|
||||
async def get_file_content(file: str) -> str | None:
|
||||
"""Get file content from Gitea.
|
||||
|
||||
Args:
|
||||
file: str, file name
|
||||
|
||||
Returns:
|
||||
str | None: file content, or None if failed to get file content
|
||||
"""
|
||||
repo_url = EVENT_DATA["pull_request"]["head"]["repo"]["url"]
|
||||
branch = EVENT_DATA["pull_request"]["head"]["ref"]
|
||||
|
||||
replaced_file = file.replace("/", "%2F")
|
||||
url = f"{repo_url}/raw/{branch}%2F{replaced_file}?ref={branch}"
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(headers=HEADERS) as session:
|
||||
async with session.get(url) as response:
|
||||
response.raise_for_status()
|
||||
return await response.text()
|
||||
except aiohttp.ClientError as e: # More specific exception handling
|
||||
print(f"Network error fetching {file}: {e}")
|
||||
except asyncio.TimeoutError:
|
||||
print(f"Timeout fetching {file}")
|
||||
return None
|
||||
|
||||
|
||||
async def analyze_full_context(
|
||||
full_context_model: Model, parsed_diff: list[dict[str, Any]]
|
||||
) -> str:
|
||||
"""Analyze full context and create review.
|
||||
|
||||
Args:
|
||||
full_context_model: AI Session for full context analysis
|
||||
parsed_diff: list[dict[str, Any]], parsed diff
|
||||
|
||||
Returns:
|
||||
str: review for full context
|
||||
"""
|
||||
|
||||
async def get_file_data(diff: dict[str, Any]):
|
||||
file = diff["file"]
|
||||
chunk = diff["chunk"]
|
||||
content = get_file_content(file)
|
||||
if content is None:
|
||||
return None
|
||||
return f"File: {file}\n{content}\nDiff: {chunk}"
|
||||
|
||||
tasks = [get_file_data(diff) for diff in parsed_diff]
|
||||
file_contents_list = await asyncio.gather(*tasks)
|
||||
|
||||
file_contents = [item for item in file_contents_list if item is not None]
|
||||
|
||||
if not file_contents:
|
||||
return ""
|
||||
|
||||
title = EVENT_DATA["pull_request"]["title"]
|
||||
description = EVENT_DATA["pull_request"]["body"]
|
||||
response = await full_context_model.get_response_full_context(
|
||||
title, description, file_contents
|
||||
)
|
||||
response = response.strip("`").lstrip("markdown").strip()
|
||||
return response
|
||||
|
||||
|
||||
def post_review(
|
||||
full_context_review: str, single_chunk_comments: list[dict[str, Any]]
|
||||
) -> None:
|
||||
"""Post review to Gitea.
|
||||
|
||||
Args:
|
||||
full_context_review: str, review for full context
|
||||
single_chunk_comments: list[dict[str, Any]], comments for single chunk review
|
||||
"""
|
||||
repo_url = EVENT_DATA["pull_request"]["head"]["repo"]["url"]
|
||||
pull_number = EVENT_DATA["number"]
|
||||
commit_id = EVENT_DATA["pull_request"]["head"]["sha"]
|
||||
url = f"{repo_url}/pulls/{pull_number}/reviews"
|
||||
data = {
|
||||
"body": full_context_review,
|
||||
"event": "COMMENT",
|
||||
"comments": single_chunk_comments,
|
||||
"commit_id": commit_id,
|
||||
}
|
||||
response = requests.post(url, headers=HEADERS, json=data)
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Code Reviewer for Gitea: Asynchronous version."""
|
||||
if EVENT_DATA["action"] not in ["opened", "synchronized"]:
|
||||
print("Unsupported event.")
|
||||
return
|
||||
|
||||
diff = get_diff()
|
||||
if diff is None:
|
||||
return
|
||||
elif not diff:
|
||||
print("No diff found.")
|
||||
return
|
||||
|
||||
full_context_model = Model(
|
||||
model=FULL_CONTEXT_MODEL_NAME,
|
||||
api_key=FULL_CONTEXT_API_KEY,
|
||||
is_full_context=True,
|
||||
)
|
||||
single_chunk_model = Model(
|
||||
model=SINGLE_CHUNK_MODEL_NAME,
|
||||
api_key=SINGLE_CHUNK_API_KEY,
|
||||
is_full_context=False,
|
||||
)
|
||||
|
||||
parsed_diff = parse_diff(diff)
|
||||
comments_task = asyncio.create_task(
|
||||
analyze_single_chunks(single_chunk_model, parsed_diff)
|
||||
)
|
||||
|
||||
if EVENT_DATA["action"] == "opened":
|
||||
full_context_response_task = asyncio.create_task(
|
||||
analyze_full_context(full_context_model, parsed_diff)
|
||||
)
|
||||
full_context_response = await full_context_response_task
|
||||
else:
|
||||
full_context_response = ""
|
||||
|
||||
comments = await comments_task
|
||||
post_review(full_context_response, comments)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
261
.github/scripts/model.py
vendored
Normal file
261
.github/scripts/model.py
vendored
Normal file
@@ -0,0 +1,261 @@
|
||||
"""Model for code review."""
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
import google.generativeai as genai
|
||||
import typing_extensions as typing
|
||||
from anthropic import AsyncAnthropic
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
|
||||
class GoogleResponse(typing.TypedDict):
|
||||
"""The response from Google model."""
|
||||
|
||||
lineNumber: int
|
||||
reviewComment: str
|
||||
|
||||
|
||||
class ModelProvider(Enum):
|
||||
"""The model provider."""
|
||||
|
||||
OPENAI = "openai"
|
||||
ANTHROPIC = "anthropic"
|
||||
GOOGLE = "google"
|
||||
DEEPSEEK = "deepseek"
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model: str) -> "ModelProvider":
|
||||
"""Get the model provider from the model name.
|
||||
|
||||
Args:
|
||||
model (str): The model name.
|
||||
|
||||
Returns:
|
||||
ModelProvider: The model provider.
|
||||
"""
|
||||
for prefix, provider in PREFIX_TO_MODEL.items():
|
||||
if model.startswith(prefix):
|
||||
return provider
|
||||
raise ValueError(f"Unknown model: {model}")
|
||||
|
||||
|
||||
PREFIX_TO_MODEL = {
|
||||
"gpt": ModelProvider.OPENAI,
|
||||
"o1": ModelProvider.OPENAI,
|
||||
"o3": ModelProvider.OPENAI,
|
||||
"claude": ModelProvider.ANTHROPIC,
|
||||
"gemini": ModelProvider.GOOGLE,
|
||||
"deepseek": ModelProvider.DEEPSEEK,
|
||||
}
|
||||
|
||||
|
||||
class Model:
|
||||
"""The model class.
|
||||
|
||||
Attributes:
|
||||
model (str): The model name.
|
||||
api_key (str): The API key.
|
||||
system_prompt (str): The system prompt.
|
||||
max_tokens (int): The maximum tokens.
|
||||
"""
|
||||
|
||||
def __init__( # noqa: D107
|
||||
self,
|
||||
model: str,
|
||||
api_key: str,
|
||||
is_full_context: bool,
|
||||
max_tokens: int = 4196,
|
||||
):
|
||||
self.model = model
|
||||
self.system_prompt = (
|
||||
FULL_CONTEXT_SYSTEM_PROMPT
|
||||
if is_full_context
|
||||
else SINGLE_CHUNK_SYSTEM_PROMPT
|
||||
)
|
||||
self.max_tokens = max_tokens
|
||||
self.provider = ModelProvider.from_model(model)
|
||||
self.session = self.create_session(api_key)
|
||||
|
||||
def create_session(self, api_key: str) -> Any:
|
||||
"""Create a session for the model.
|
||||
|
||||
Args:
|
||||
api_key (str): The API key.
|
||||
|
||||
Returns:
|
||||
Any: The session.
|
||||
"""
|
||||
match self.provider:
|
||||
case ModelProvider.OPENAI:
|
||||
return AsyncOpenAI(api_key=api_key)
|
||||
case ModelProvider.ANTHROPIC:
|
||||
return AsyncAnthropic(api_key=api_key)
|
||||
case ModelProvider.GOOGLE:
|
||||
genai.configure(api_key=api_key)
|
||||
return genai.GenerativeModel(
|
||||
model_name=self.model, system_instruction=self.system_prompt
|
||||
)
|
||||
case ModelProvider.DEEPSEEK:
|
||||
return AsyncOpenAI(api_key=api_key, base_url="https://api.deepseek.com")
|
||||
|
||||
async def request(self, prompt: str) -> str:
|
||||
"""Request the model to generate a response.
|
||||
|
||||
Args:
|
||||
prompt (str): The prompt to generate a response for.
|
||||
|
||||
Returns:
|
||||
str: The generated response.
|
||||
"""
|
||||
match self.provider:
|
||||
case ModelProvider.OPENAI | ModelProvider.DEEPSEEK:
|
||||
response = await self.session.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=[
|
||||
{"role": "system", "content": self.system_prompt},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
temperature=0.2,
|
||||
max_tokens=self.max_tokens,
|
||||
top_p=1,
|
||||
frequency_penalty=0,
|
||||
presence_penalty=0,
|
||||
)
|
||||
return response.choices[0].message.content.strip()
|
||||
case ModelProvider.ANTHROPIC:
|
||||
response = await self.session.messages.create(
|
||||
model=self.model,
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
system=[
|
||||
{
|
||||
"type": "text",
|
||||
"text": self.system_prompt,
|
||||
"cache_control": {"type": "ephemeral"},
|
||||
}
|
||||
],
|
||||
temperature=0.2,
|
||||
max_tokens=self.max_tokens,
|
||||
)
|
||||
return response.content[0].text.strip()
|
||||
case ModelProvider.GOOGLE:
|
||||
response = await self.session.generate_content_async(
|
||||
prompt,
|
||||
generation_config=genai.GenerationConfig(
|
||||
response_mime_type="application/json",
|
||||
response_schema=list[GoogleResponse],
|
||||
),
|
||||
)
|
||||
return response.text.strip()
|
||||
|
||||
async def get_response_single_chunk(
|
||||
self, file: str, title: str, description: str, chunk: str
|
||||
) -> str:
|
||||
"""Get the response for a single chunk.
|
||||
|
||||
Args:
|
||||
file (str): The file name.
|
||||
title (str): The pull request title.
|
||||
description (str): The pull request description.
|
||||
chunk (str): The diff chunk.
|
||||
|
||||
Returns:
|
||||
str: The response.
|
||||
"""
|
||||
prompt = SINGLE_CHUNK_USER_PROMPT.format(file, title, description, chunk)
|
||||
return await self.request(prompt)
|
||||
|
||||
async def get_response_full_context(
|
||||
self, title: str, description: str, file_contents: list[str]
|
||||
) -> str:
|
||||
"""Get the response for full context.
|
||||
|
||||
Args:
|
||||
title (str): The pull request title.
|
||||
description (str): The pull request description.
|
||||
file_contents (list[str]): The file contents, diffs.
|
||||
|
||||
Returns:
|
||||
str: The response.
|
||||
"""
|
||||
try:
|
||||
prompt = FULL_CONTEXT_USER_PROMPT.format(
|
||||
title, description, "\n".join(file_contents)
|
||||
)
|
||||
return await self.request(prompt)
|
||||
except Exception as e:
|
||||
print(f"Error during full context response: {e}")
|
||||
print(prompt)
|
||||
return None
|
||||
|
||||
|
||||
SINGLE_CHUNK_SYSTEM_PROMPT = (
|
||||
"Your task is to review pull requests. Instructions:\n"
|
||||
"- Provide the response in the following JSON format: "
|
||||
"""[{{"lineNumber": int, "reviewComment": str}}] \n"""
|
||||
"- lineNumber is about the line number of the code that in new file. \n"
|
||||
"- lineNumber can be found at the front of each line. \n"
|
||||
"- At the first number is old line number, the second number is new line number. \n"
|
||||
"- If the line starts with `+`, it means the line is added. \n"
|
||||
"- If the line starts with `-`, it means the line is deleted. \n"
|
||||
"- Evaluate whether the code changes and additions are appropriate "
|
||||
"and if the new code structure is suitable. \n"
|
||||
"- Do not give positive comments or compliments. \n"
|
||||
"- Provide comments and suggestions ONLY if there is something to improve"
|
||||
"otherwise return an empty array. \n"
|
||||
"- Write the comment in GitHub Markdown format. \n"
|
||||
"- Use the given description only for the overall context "
|
||||
"and only comment the code. \n"
|
||||
"- Do not suggest type hint or naming convention. \n"
|
||||
"- IMPORTANT: NEVER suggest adding comments to the code. \n"
|
||||
)
|
||||
SINGLE_CHUNK_USER_PROMPT = (
|
||||
"Review the following code diff in the file "
|
||||
"{} and take the pull request title and description into account "
|
||||
"when writing the response. \n"
|
||||
"Pull request title: {} \n"
|
||||
"Pull request description: \n"
|
||||
"--- \n"
|
||||
"{} \n"
|
||||
"--- \n"
|
||||
"Git diff to review: \n"
|
||||
"```diff \n"
|
||||
"{} \n"
|
||||
"```"
|
||||
)
|
||||
|
||||
FULL_CONTEXT_SYSTEM_PROMPT = (
|
||||
"You are an experienced software engineer specializing in reviewing pull "
|
||||
"requests. Your task is to provide an overall code review summary for a PR. "
|
||||
"Focus on assessing the following aspects:\n"
|
||||
"1. **Code Structure & Architecture:** "
|
||||
"Evaluate whether the code is well-organized, modular, "
|
||||
"and adheres to clean code principles. Suggest improvements if needed.\n"
|
||||
"2. **Refactoring Opportunities:** "
|
||||
"Identify areas where the code can be optimized or simplified without changing "
|
||||
"its behavior.\n"
|
||||
"3. **Potential Future Problems:** "
|
||||
"Highlight possible scalability, maintainability, or dependency issues that might "
|
||||
"arise in the future based on the current implementation.\n"
|
||||
"Be constructive and clear in your feedback. Avoid commenting on trivial issues "
|
||||
"or syntax errors—focus on high-level feedback.\n"
|
||||
"Precise instructions:\n"
|
||||
"- Do not give positive comments or compliments.\n"
|
||||
"- Provide comments and suggestions ONLY if there is something to improve, "
|
||||
"otherwise return an empty string.\n"
|
||||
"- Write the comment in GitHub Markdown format.\n"
|
||||
"- Do not start with 'markdown' or '```markdown'.\n"
|
||||
"- IMPORTANT: Give example code block or pseudo code if you can.\n"
|
||||
)
|
||||
|
||||
FULL_CONTEXT_USER_PROMPT = (
|
||||
"Review the following code and take the pull request title "
|
||||
"and description into account when writing the response. \n"
|
||||
"Pull request title: {} \n"
|
||||
"Pull request description: \n"
|
||||
"--- \n"
|
||||
"{} \n"
|
||||
"--- \n"
|
||||
"Code to review: \n"
|
||||
"{}"
|
||||
)
|
||||
196
.github/workflows/ci.yaml
vendored
196
.github/workflows/ci.yaml
vendored
@@ -1,42 +1,14 @@
|
||||
on:
|
||||
push:
|
||||
branches: [main, release]
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
types: [unlabeled, opened, synchronize, reopened]
|
||||
merge_group:
|
||||
workflow_dispatch:
|
||||
|
||||
name: CI
|
||||
|
||||
# Cancel previous workflows if they are the same workflow on same ref (branch/tags)
|
||||
# with the same event (push/pull_request) even they are in progress.
|
||||
# This setting will help reduce the number of duplicated workflows.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
||||
cancel-in-progress: true
|
||||
name: Dev-CI
|
||||
|
||||
env:
|
||||
CARGO_ARGS: --no-default-features --features stdlib,importlib,encodings,sqlite,ssl
|
||||
# Skip additional tests on Windows. They are checked on Linux and MacOS.
|
||||
# test_glob: many failing tests
|
||||
# test_io: many failing tests
|
||||
# test_os: many failing tests
|
||||
# test_pathlib: support.rmtree() failing
|
||||
# test_posixpath: OSError: (22, 'The filename, directory name, or volume label syntax is incorrect. (os error 123)')
|
||||
# test_venv: couple of failing tests
|
||||
WINDOWS_SKIPS: >-
|
||||
test_glob
|
||||
test_io
|
||||
test_os
|
||||
test_rlcompleter
|
||||
test_pathlib
|
||||
test_posixpath
|
||||
test_venv
|
||||
# configparser: https://github.com/RustPython/RustPython/issues/4995#issuecomment-1582397417
|
||||
# socketserver: seems related to configparser crash.
|
||||
MACOS_SKIPS: >-
|
||||
test_configparser
|
||||
test_socketserver
|
||||
CARGO_ARGS: --no-default-features --features stdlib,zlib,importlib,encodings,sqlite,ssl
|
||||
# PLATFORM_INDEPENDENT_TESTS are tests that do not depend on the underlying OS. They are currently
|
||||
# only run on Linux to speed up the CI.
|
||||
PLATFORM_INDEPENDENT_TESTS: >-
|
||||
@@ -117,11 +89,7 @@ jobs:
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
name: Run rust tests
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
fail-fast: false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
@@ -129,26 +97,12 @@ jobs:
|
||||
components: clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Set up the Windows environment
|
||||
shell: bash
|
||||
run: |
|
||||
cargo install --target-dir=target -v cargo-vcpkg
|
||||
cargo vcpkg -v build
|
||||
if: runner.os == 'Windows'
|
||||
- name: Set up the Mac environment
|
||||
run: brew install autoconf automake libtool
|
||||
if: runner.os == 'macOS'
|
||||
|
||||
- name: run clippy
|
||||
run: cargo clippy ${{ env.CARGO_ARGS }} --workspace --exclude rustpython_wasm -- -Dwarnings
|
||||
|
||||
- name: run rust tests
|
||||
run: cargo test --workspace --exclude rustpython_wasm --verbose --features threading ${{ env.CARGO_ARGS }}
|
||||
if: runner.os != 'macOS'
|
||||
- name: run rust tests
|
||||
run: cargo test --workspace --exclude rustpython_wasm --exclude rustpython-jit --verbose --features threading ${{ env.CARGO_ARGS }}
|
||||
if: runner.os == 'macOS'
|
||||
|
||||
|
||||
- name: check compilation without threading
|
||||
run: cargo check ${{ env.CARGO_ARGS }}
|
||||
|
||||
@@ -156,24 +110,6 @@ jobs:
|
||||
run:
|
||||
cargo run --manifest-path example_projects/barebone/Cargo.toml
|
||||
cargo run --manifest-path example_projects/frozen_stdlib/Cargo.toml
|
||||
if: runner.os == 'Linux'
|
||||
|
||||
- name: prepare AppleSilicon build
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: aarch64-apple-darwin
|
||||
if: runner.os == 'macOS'
|
||||
- name: Check compilation for Apple Silicon
|
||||
run: cargo check --target aarch64-apple-darwin
|
||||
if: runner.os == 'macOS'
|
||||
- name: prepare iOS build
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: aarch64-apple-ios
|
||||
if: runner.os == 'macOS'
|
||||
- name: Check compilation for iOS
|
||||
run: cargo check --target aarch64-apple-ios
|
||||
if: runner.os == 'macOS'
|
||||
|
||||
exotic_targets:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }}
|
||||
@@ -240,11 +176,7 @@ jobs:
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
name: Run snippets and cpython tests
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
fail-fast: false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
@@ -252,54 +184,29 @@ jobs:
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: Set up the Windows environment
|
||||
shell: bash
|
||||
run: |
|
||||
cargo install cargo-vcpkg
|
||||
cargo vcpkg build
|
||||
if: runner.os == 'Windows'
|
||||
- name: Set up the Mac environment
|
||||
run: brew install autoconf automake libtool openssl@3
|
||||
if: runner.os == 'macOS'
|
||||
- name: build rustpython
|
||||
run: cargo build --release --verbose --features=threading ${{ env.CARGO_ARGS }}
|
||||
if: runner.os == 'macOS'
|
||||
- name: build rustpython
|
||||
run: cargo build --release --verbose --features=threading ${{ env.CARGO_ARGS }},jit
|
||||
if: runner.os != 'macOS'
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: run snippets
|
||||
run: python -m pip install -r requirements.txt && pytest -v
|
||||
working-directory: ./extra_tests
|
||||
- if: runner.os == 'Linux'
|
||||
name: run cpython platform-independent tests
|
||||
- name: run cpython platform-independent tests
|
||||
run:
|
||||
target/release/rustpython -m test -j 1 -u all --slowest --fail-env-changed -v ${{ env.PLATFORM_INDEPENDENT_TESTS }}
|
||||
- if: runner.os == 'Linux'
|
||||
name: run cpython platform-dependent tests (Linux)
|
||||
- name: run cpython platform-dependent tests (Linux)
|
||||
run: target/release/rustpython -m test -j 1 -u all --slowest --fail-env-changed -v -x ${{ env.PLATFORM_INDEPENDENT_TESTS }}
|
||||
- if: runner.os == 'macOS'
|
||||
name: run cpython platform-dependent tests (MacOS)
|
||||
run: target/release/rustpython -m test -j 1 --slowest --fail-env-changed -v -x ${{ env.PLATFORM_INDEPENDENT_TESTS }} ${{ env.MACOS_SKIPS }}
|
||||
- if: runner.os == 'Windows'
|
||||
name: run cpython platform-dependent tests (windows partial - fixme)
|
||||
run:
|
||||
target/release/rustpython -m test -j 1 --slowest --fail-env-changed -v -x ${{ env.PLATFORM_INDEPENDENT_TESTS }} ${{ env.WINDOWS_SKIPS }}
|
||||
- if: runner.os != 'Windows'
|
||||
name: check that --install-pip succeeds
|
||||
- name: check that --install-pip succeeds
|
||||
run: |
|
||||
mkdir site-packages
|
||||
target/release/rustpython --install-pip ensurepip --user
|
||||
target/release/rustpython -m pip install six
|
||||
- if: runner.os != 'Windows'
|
||||
name: Check that ensurepip succeeds.
|
||||
- name: Check that ensurepip succeeds.
|
||||
run: |
|
||||
target/release/rustpython -m ensurepip
|
||||
target/release/rustpython -c "import pip"
|
||||
- if: runner.os != 'Windows'
|
||||
name: Check if pip inside venv is functional
|
||||
- name: Check if pip inside venv is functional
|
||||
run: |
|
||||
target/release/rustpython -m venv testvenv
|
||||
testvenv/bin/rustpython -m pip install wheel
|
||||
@@ -348,84 +255,3 @@ jobs:
|
||||
# a memory leak, at least until we have proper cyclic gc
|
||||
run: MIRIFLAGS='-Zmiri-ignore-leaks' cargo +nightly miri test -p rustpython-vm -- miri_test
|
||||
|
||||
wasm:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }}
|
||||
name: Check the WASM package and demo
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
- name: install geckodriver
|
||||
run: |
|
||||
wget https://github.com/mozilla/geckodriver/releases/download/v0.36.0/geckodriver-v0.36.0-linux64.tar.gz
|
||||
mkdir geckodriver
|
||||
tar -xzf geckodriver-v0.36.0-linux64.tar.gz -C geckodriver
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- run: python -m pip install -r requirements.txt
|
||||
working-directory: ./wasm/tests
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "wasm/demo/package-lock.json"
|
||||
- name: run test
|
||||
run: |
|
||||
export PATH=$PATH:`pwd`/../../geckodriver
|
||||
npm install
|
||||
npm run test
|
||||
env:
|
||||
NODE_OPTIONS: "--openssl-legacy-provider"
|
||||
working-directory: ./wasm/demo
|
||||
- uses: mwilliamson/setup-wabt-action@v3
|
||||
with: { wabt-version: "1.0.36" }
|
||||
- name: check wasm32-unknown without js
|
||||
run: |
|
||||
cd wasm/wasm-unknown-test
|
||||
cargo build --release --verbose
|
||||
if wasm-objdump -xj Import target/wasm32-unknown-unknown/release/wasm_unknown_test.wasm; then
|
||||
echo "ERROR: wasm32-unknown module expects imports from the host environment" >2
|
||||
fi
|
||||
- name: build notebook demo
|
||||
if: github.ref == 'refs/heads/release'
|
||||
run: |
|
||||
npm install
|
||||
npm run dist
|
||||
mv dist ../demo/dist/notebook
|
||||
env:
|
||||
NODE_OPTIONS: "--openssl-legacy-provider"
|
||||
working-directory: ./wasm/notebook
|
||||
- name: Deploy demo to Github Pages
|
||||
if: success() && github.ref == 'refs/heads/release'
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
env:
|
||||
ACTIONS_DEPLOY_KEY: ${{ secrets.ACTIONS_DEMO_DEPLOY_KEY }}
|
||||
PUBLISH_DIR: ./wasm/demo/dist
|
||||
EXTERNAL_REPOSITORY: RustPython/demo
|
||||
PUBLISH_BRANCH: master
|
||||
|
||||
wasm-wasi:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }}
|
||||
name: Run snippets and cpython tests on wasm-wasi
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: wasm32-wasip1
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Setup Wasmer
|
||||
uses: wasmerio/setup-wasmer@v3
|
||||
- name: Install clang
|
||||
run: sudo apt-get update && sudo apt-get install clang -y
|
||||
- name: build rustpython
|
||||
run: cargo build --release --target wasm32-wasip1 --features freeze-stdlib,stdlib --verbose
|
||||
- name: run snippets
|
||||
run: wasmer run --dir `pwd` target/wasm32-wasip1/release/rustpython.wasm -- `pwd`/extra_tests/snippets/stdlib_random.py
|
||||
- name: run cpython unittest
|
||||
run: wasmer run --dir `pwd` target/wasm32-wasip1/release/rustpython.wasm -- `pwd`/Lib/test/test_int.py
|
||||
|
||||
36
.github/workflows/code-review.yml
vendored
Normal file
36
.github/workflows/code-review.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install aiohttp requests py-gitea openai anthropic google-generativeai
|
||||
|
||||
- name: Run Code Review
|
||||
env:
|
||||
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
FULL_CONTEXT_MODEL: o3-mini
|
||||
FULL_CONTEXT_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
SINGLE_CHUNK_MODEL: gemini-2.0-flash-exp
|
||||
SINGLE_CHUNK_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
EXCLUDE: "*.yml,*.yaml"
|
||||
run: python .gitea/scripts/code_review.py
|
||||
|
||||
Reference in New Issue
Block a user