From 51bb487346359909c613ac576e42a0d28492f26f Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 19 Jun 2026 15:29:48 +0800 Subject: [PATCH] fix: address release workflow review feedback --- astrbot/core/utils/toml_parser.py | 117 ++++++++++++++++++++------- scripts/prepare_release.py | 22 +++--- tests/test_toml_parser.py | 126 ++++++++++++++++++++++++++++++ 3 files changed, 226 insertions(+), 39 deletions(-) diff --git a/astrbot/core/utils/toml_parser.py b/astrbot/core/utils/toml_parser.py index 0cf14957b..1b647846c 100644 --- a/astrbot/core/utils/toml_parser.py +++ b/astrbot/core/utils/toml_parser.py @@ -3,6 +3,75 @@ from pathlib import Path +def _read_quoted_value(value: str, field_name: str) -> tuple[str, str]: + """Read one quoted TOML string value and return its tail. + + Args: + value: Raw value text that starts with a quoted string. + field_name: Field name used in error messages. + + Returns: + A tuple containing the unquoted string and the remaining text. + + Raises: + ValueError: The value is not a supported quoted string. + """ + value = value.strip() + if len(value) < 2 or value[0] not in ("'", '"'): + raise ValueError(f"Unsupported {field_name} value") + + quote = value[0] + end_index = value.find(quote, 1) + if end_index == -1: + raise ValueError(f"Unterminated {field_name} string") + + result = value[1:end_index] + if not result: + raise ValueError(f"Empty {field_name} value") + return result, value[end_index + 1 :].strip() + + +def _read_dependency_array(raw_value: str) -> list[str]: + """Read a simple inline TOML string array. + + Args: + raw_value: Raw dependency array text, including the surrounding brackets. + + Returns: + Parsed dependency strings. + + Raises: + ValueError: The array is missing brackets or contains unsupported entries. + """ + value = raw_value.strip() + if not value.startswith("["): + raise ValueError("Unsupported project.dependencies value") + + dependencies = [] + value = value[1:].strip() + while value: + if value.startswith("]"): + tail = value[1:].strip() + if tail and not tail.startswith("#"): + raise ValueError("Unsupported content after project.dependencies") + return dependencies + + dependency, tail = _read_quoted_value(value, "project.dependencies entry") + dependencies.append(dependency) + + if tail.startswith(","): + value = tail[1:].strip() + continue + if tail.startswith("]"): + value = tail + continue + if tail: + raise ValueError("Unsupported content after project.dependencies entry") + raise ValueError("Unterminated project.dependencies array") + + raise ValueError("Unterminated project.dependencies array") + + def read_pyproject_project_version(pyproject_path: Path) -> str: """Read the project version from a pyproject.toml file. @@ -35,22 +104,9 @@ def read_pyproject_project_version(pyproject_path: Path) -> str: if not separator: raise ValueError("Missing value separator for project.version") - value = raw_value.strip() - if len(value) < 2 or value[0] not in ("'", '"'): - raise ValueError("Unsupported project.version value") - - quote = value[0] - end_index = value.find(quote, 1) - if end_index == -1: - raise ValueError("Unterminated project.version string") - - tail = value[end_index + 1 :].strip() + version, tail = _read_quoted_value(raw_value, "project.version") if tail and not tail.startswith("#"): raise ValueError("Unsupported content after project.version") - - version = value[1:end_index] - if not version: - raise ValueError("Empty project.version value") return version raise ValueError("Missing project.version") @@ -85,24 +141,21 @@ def read_pyproject_project_dependencies(pyproject_path: Path) -> list[str]: raise ValueError("Unsupported content after project.dependencies") return dependencies - value = line.rstrip(",").strip() - if len(value) < 2 or value[0] not in ("'", '"'): - raise ValueError("Unsupported project.dependencies entry") - - quote = value[0] - end_index = value.find(quote, 1) - if end_index == -1: - raise ValueError("Unterminated project.dependencies entry") - - tail = value[end_index + 1 :].strip() + dependency, tail = _read_quoted_value( + line, + "project.dependencies entry", + ) if tail.startswith(","): tail = tail[1:].strip() + if tail.startswith("]"): + tail = tail[1:].strip() + dependencies.append(dependency) + if tail and not tail.startswith("#"): + raise ValueError("Unsupported content after project.dependencies") + return dependencies if tail and not tail.startswith("#"): raise ValueError("Unsupported content after project.dependencies entry") - dependency = value[1:end_index] - if not dependency: - raise ValueError("Empty project.dependencies entry") dependencies.append(dependency) continue @@ -116,9 +169,15 @@ def read_pyproject_project_dependencies(pyproject_path: Path) -> list[str]: key, separator, raw_value = line.partition("=") if key.strip() != "dependencies": continue - if not separator or raw_value.strip() != "[": + if not separator: raise ValueError("Unsupported project.dependencies value") - in_dependencies_array = True + raw_value = raw_value.strip() + if raw_value == "[" or raw_value.startswith("[ #"): + in_dependencies_array = True + continue + if raw_value.startswith("["): + return _read_dependency_array(raw_value) + raise ValueError("Unsupported project.dependencies value") if in_dependencies_array: raise ValueError("Unterminated project.dependencies array") diff --git a/scripts/prepare_release.py b/scripts/prepare_release.py index 87a898fbf..5d1cff70d 100644 --- a/scripts/prepare_release.py +++ b/scripts/prepare_release.py @@ -121,15 +121,10 @@ def latest_tag() -> str: Returns: The latest tag name, or an empty string when the repository has no tags. """ - result = subprocess.run( - ["git", "describe", "--tags", "--abbrev=0"], - cwd=REPO_ROOT, - capture_output=True, - text=True, - ) - if result.returncode != 0: + try: + return git(["describe", "--tags", "--abbrev=0"], capture_output=True) + except ReleaseError: return "" - return result.stdout.strip() def release_commits(tag: str) -> list[str]: @@ -174,9 +169,15 @@ def update_pyproject_version(version: str) -> Path: if stripped.startswith("[") and stripped.endswith("]"): in_project_section = stripped == "[project]" continue - if not in_project_section or not stripped.startswith("version"): + if not in_project_section: continue + key, separator, _raw_value = stripped.partition("=") + if key.strip() != "version": + continue + if not separator: + raise ReleaseError("Unsupported pyproject.toml project.version format") + match = re.match( r"^(\s*version\s*=\s*)([\"'])(.*?)(\2)(\s*(?:#.*)?)(\n?)$", line, @@ -209,6 +210,7 @@ def write_changelog(version: str, commits: list[str]) -> Path: if changelog_path.exists(): raise ReleaseError(f"Changelog already exists: {changelog_path}") + changelog_path.parent.mkdir(parents=True, exist_ok=True) entries = [f"- {commit}" for commit in commits] or ["- "] changelog_path.write_text( "\n".join( @@ -270,7 +272,7 @@ def run_validation(args: argparse.Namespace) -> None: run_command(["pnpm", "generate:api"], cwd=REPO_ROOT / "dashboard") if not args.skip_checks: - run_command(["uv", "run", "ruff", "format", "."]) + run_command(["uv", "run", "ruff", "format", "--check", "."]) run_command(["uv", "run", "ruff", "check", "."]) if args.dashboard_build: diff --git a/tests/test_toml_parser.py b/tests/test_toml_parser.py index 525f9c844..b8ee88561 100644 --- a/tests/test_toml_parser.py +++ b/tests/test_toml_parser.py @@ -27,6 +27,64 @@ def test_read_pyproject_project_version_reads_project_section(tmp_path: Path) -> assert read_pyproject_project_version(pyproject_path) == "1.2.3-beta.4" +@pytest.mark.parametrize( + ("version_line", "expected"), + [ + ('version = "1.2.3"', "1.2.3"), + ("version='1.2.3-beta.4'", "1.2.3-beta.4"), + (' version = "1.2.3-rc.1" ', "1.2.3-rc.1"), + ], +) +def test_read_pyproject_project_version_accepts_simple_variants( + tmp_path: Path, + version_line: str, + expected: str, +) -> None: + pyproject_path = tmp_path / "pyproject.toml" + pyproject_path.write_text( + "\n".join( + [ + "[project]", + 'name = "AstrBot"', + version_line, + ] + ), + encoding="utf-8", + ) + + assert read_pyproject_project_version(pyproject_path) == expected + + +@pytest.mark.parametrize( + ("version_line", "message"), + [ + ("version", "Missing value separator for project.version"), + ('version = "1.2.3', "Unterminated project.version string"), + ('version = "1.2.3" extra', "Unsupported content after project.version"), + ('version = ""', "Empty project.version value"), + ], +) +def test_read_pyproject_project_version_rejects_invalid_values( + tmp_path: Path, + version_line: str, + message: str, +) -> None: + pyproject_path = tmp_path / "pyproject.toml" + pyproject_path.write_text( + "\n".join( + [ + "[project]", + 'name = "AstrBot"', + version_line, + ] + ), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match=message): + read_pyproject_project_version(pyproject_path) + + def test_read_pyproject_project_dependencies_reads_multiline_array( tmp_path: Path, ) -> None: @@ -50,6 +108,74 @@ def test_read_pyproject_project_dependencies_reads_multiline_array( ] +@pytest.mark.parametrize( + ("dependency_line", "expected"), + [ + ("dependencies = []", []), + ('dependencies = ["aiohttp>=3.11.18"]', ["aiohttp>=3.11.18"]), + ( + 'dependencies = ["psutil>=5.8.0,<7.2.0", "httpx[socks]>=0.28.1"]', + ["psutil>=5.8.0,<7.2.0", "httpx[socks]>=0.28.1"], + ), + ], +) +def test_read_pyproject_project_dependencies_accepts_inline_arrays( + tmp_path: Path, + dependency_line: str, + expected: list[str], +) -> None: + pyproject_path = tmp_path / "pyproject.toml" + pyproject_path.write_text( + "\n".join( + [ + "[project]", + dependency_line, + ] + ), + encoding="utf-8", + ) + + assert read_pyproject_project_dependencies(pyproject_path) == expected + + +@pytest.mark.parametrize( + ("project_lines", "message"), + [ + (["[project]", 'name = "AstrBot"'], "Missing project.dependencies"), + ( + ["[project]", "dependencies = ["], + "Unterminated project.dependencies array", + ), + ( + ["[project]", 'dependencies = "aiohttp>=3.11.18"'], + "Unsupported project.dependencies value", + ), + ( + ["[project]", "dependencies = [", " aiohttp>=3.11.18,", "]"], + "Unsupported project.dependencies entry value", + ), + ( + ["[project]", "dependencies = [", ' "aiohttp>=3.11.18" extra', "]"], + "Unsupported content after project.dependencies entry", + ), + ( + ["[project]", "dependencies = [", ' ""', "]"], + "Empty project.dependencies entry value", + ), + ], +) +def test_read_pyproject_project_dependencies_rejects_invalid_values( + tmp_path: Path, + project_lines: list[str], + message: str, +) -> None: + pyproject_path = tmp_path / "pyproject.toml" + pyproject_path.write_text("\n".join(project_lines), encoding="utf-8") + + with pytest.raises(ValueError, match=message): + read_pyproject_project_dependencies(pyproject_path) + + def test_read_pyproject_project_version_raises_when_missing(tmp_path: Path) -> None: pyproject_path = tmp_path / "pyproject.toml" pyproject_path.write_text('[project]\nname = "AstrBot"\n', encoding="utf-8")