diff --git a/src/pyflowx/cli/bumpversion.py b/src/pyflowx/cli/bumpversion.py index 13ecc39..05118c8 100644 --- a/src/pyflowx/cli/bumpversion.py +++ b/src/pyflowx/cli/bumpversion.py @@ -14,13 +14,105 @@ import pyflowx as px BumpVersionType = Literal["patch", "minor", "major"] - -_VERSION_PATTERN = re.compile( +# 针对不同文件类型的版本号匹配模式 +# pyproject.toml: version = "X.Y.Z" 或 version = 'X.Y.Z' +_PYPROJECT_VERSION_PATTERN = re.compile( + r'(?:^|\n)\s*version\s*=\s*["\']' r"(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)" r"(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?" - r"(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?", + r"(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?" + r'["\']', + re.MULTILINE, ) +# __init__.py: __version__ = "X.Y.Z" 或 __version__ = 'X.Y.Z' +_INIT_VERSION_PATTERN = re.compile( + r'(?:^|\n)\s*__version__\s*=\s*["\']' + r"(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)" + r"(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?" + r"(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?" + r'["\']', + re.MULTILINE, +) + + +def _get_pattern_for_file(file_name: str) -> re.Pattern[str] | None: + """根据文件类型获取对应的正则表达式. + + Parameters + ---------- + file_name : str + 文件名 + + Returns + ------- + re.Pattern[str] | None + 对应的正则表达式,如果无法确定则返回 None + """ + if file_name == "pyproject.toml": + return _PYPROJECT_VERSION_PATTERN + if file_name == "__init__.py": + return _INIT_VERSION_PATTERN + return None + + +def _calculate_new_version(major: int, minor: int, patch: int, part: BumpVersionType) -> str: + """计算新版本号. + + Parameters + ---------- + major : int + 当前主版本号 + minor : int + 当前次版本号 + patch : int + 当前补丁版本号 + part : BumpVersionType + 要更新的部分 + + Returns + ------- + str + 新版本号 + """ + if part == "major": + return f"{major + 1}.0.0" + if part == "minor": + return f"{major}.{minor + 1}.0" + return f"{major}.{minor}.{patch + 1}" + + +def _build_replacement_string(original_match: str, new_version: str, file_name: str) -> str: + """构建替换字符串,保留原始格式. + + Parameters + ---------- + original_match : str + 原始匹配的字符串 + new_version : str + 新版本号 + file_name : str + 文件名 + + Returns + ------- + str + 替换字符串 + """ + quote_char = '"' if '"' in original_match else "'" + + if file_name == "pyproject.toml": + prefix_match = re.match(r'(\s*version\s*=\s*)["\']', original_match) + prefix = prefix_match.group(1) if prefix_match else "version = " + return f"{prefix}{quote_char}{new_version}{quote_char}" + + if file_name == "__init__.py": + prefix_match = re.match(r'(\s*__version__\s*=\s*)["\']', original_match) + prefix = prefix_match.group(1) if prefix_match else "__version__ = " + return f"{prefix}{quote_char}{new_version}{quote_char}" + + return new_version + def bump_file_version(file_path: Path, part: BumpVersionType = "patch") -> str | None: """更新文件中的版本号. @@ -43,27 +135,33 @@ def bump_file_version(file_path: Path, part: BumpVersionType = "patch") -> str | print(f"读取文件 {file_path} 时出错: {e}") raise - match = _VERSION_PATTERN.search(content) + # 获取文件对应的正则表达式 + pattern = _get_pattern_for_file(file_path.name) + + # 对于未知文件类型,尝试两种模式 + if pattern: + match = pattern.search(content) + else: + match = _PYPROJECT_VERSION_PATTERN.search(content) or _INIT_VERSION_PATTERN.search(content) + if not match: print(f"文件 {file_path} 中未找到版本号模式") return None + # 提取当前版本号 major = int(match.group("major")) minor = int(match.group("minor")) patch = int(match.group("patch")) # 计算新版本号 - if part == "major": - new_major = major + 1 - new_version_str = f"{new_major}.0.0" - elif part == "minor": - new_minor = minor + 1 - new_version_str = f"{major}.{new_minor}.0" - else: # patch - new_patch = patch + 1 - new_version_str = f"{major}.{minor}.{new_patch}" + new_version = _calculate_new_version(major, minor, patch, part) - content = content.replace(match.group(0), new_version_str) + # 构建替换字符串 + original_match = match.group(0) + replacement = _build_replacement_string(original_match, new_version, file_path.name) + + # 更新文件内容 + content = content.replace(original_match, replacement) try: file_path.write_text(content, encoding="utf-8") @@ -71,7 +169,7 @@ def bump_file_version(file_path: Path, part: BumpVersionType = "patch") -> str | print(f"更新文件 {file_path} 版本号时出错: {e}") raise - return new_version_str + return new_version def main() -> None: @@ -114,14 +212,16 @@ def main() -> None: # 更新所有文件的版本号(使用顺序执行避免竞争条件) # 使用相对于 cwd 的路径作为任务名,确保唯一性 - graph = px.Graph.from_specs([ - px.TaskSpec( - f"bump_{file.relative_to(Path.cwd())}".replace("\\", "_").replace("/", "_").replace(".", "_"), - fn=bump_file_version, - args=(file, part), - ) - for file in all_files - ]) + graph = px.Graph.from_specs( + [ + px.TaskSpec( + f"bump_{file.relative_to(Path.cwd())}".replace("\\", "_").replace("/", "_").replace(".", "_"), + fn=bump_file_version, + args=(file, part), + ) + for file in all_files + ] + ) report = px.run(graph, strategy="sequential") # 收集新版本号(取第一个成功的结果) @@ -139,19 +239,23 @@ def main() -> None: print(f"版本号已更新为: {new_version}") # 提交修改 - graph = px.Graph.from_specs([ - px.TaskSpec("git_add", cmd=["git", "add", "."]), - px.TaskSpec( - "git_commit", cmd=["git", "commit", "-m", f"bump version to {new_version}"], depends_on=["git_add"] - ), - ]) + graph = px.Graph.from_specs( + [ + px.TaskSpec("git_add", cmd=["git", "add", "."]), + px.TaskSpec( + "git_commit", cmd=["git", "commit", "-m", f"bump version to {new_version}"], depends_on=["git_add"] + ), + ] + ) px.run(graph, strategy="sequential") # 创建 git tag if not args.no_tag: tag_name = f"v{new_version}" - graph = px.Graph.from_specs([ - px.TaskSpec("git_tag", cmd=["git", "tag", "-a", tag_name, "-m", f"Release {tag_name}"]), - ]) + graph = px.Graph.from_specs( + [ + px.TaskSpec("git_tag", cmd=["git", "tag", "-a", tag_name, "-m", f"Release {tag_name}"]), + ] + ) px.run(graph, strategy="sequential") print(f"已创建标签: {tag_name}") diff --git a/src/pyflowx/cli/pymake.py b/src/pyflowx/cli/pymake.py index 81a738f..6f7e382 100644 --- a/src/pyflowx/cli/pymake.py +++ b/src/pyflowx/cli/pymake.py @@ -20,15 +20,13 @@ def maturin_build_cmd() -> list[str]: """ command = ["maturin", "build", "-r"].copy() if Constants.IS_WINDOWS: - command.extend( - [ - "--target", - "x86_64-win7-windows-msvc", - "-Zbuild-std", - "-i", - "python3.8", - ] - ) + command.extend([ + "--target", + "x86_64-win7-windows-msvc", + "-Zbuild-std", + "-i", + "python3.8", + ]) return command @@ -118,6 +116,7 @@ def main(): "c": px.Graph.from_specs([git_clean]), # 开发工具 "bump": px.Graph.from_specs(["c", "tc", git_add_all, bump]), + "bumpmi": px.Graph.from_specs([px.TaskSpec("bumpversion_minor", cmd=["bumpversion", "minor"])]), "cov": px.Graph.from_specs([git_clean, test_coverage]), "doc": px.Graph.from_specs([doc]), "lint": px.Graph.from_specs([ruff_lint, ruff_format]), diff --git a/tests/cli/test_bumpversion.py b/tests/cli/test_bumpversion.py index 7dab49f..714f734 100644 --- a/tests/cli/test_bumpversion.py +++ b/tests/cli/test_bumpversion.py @@ -17,38 +17,38 @@ class TestBumpFileVersion: def test_bump_patch_version(self, tmp_path: Path) -> None: """Should bump patch version correctly.""" - test_file = tmp_path / "test.txt" - test_file.write_text("version = 1.2.3", encoding="utf-8") + test_file = tmp_path / "pyproject.toml" + test_file.write_text('version = "1.2.3"', encoding="utf-8") result = bumpversion.bump_file_version(test_file, "patch") assert result == "1.2.4" - assert test_file.read_text(encoding="utf-8") == "version = 1.2.4" + assert test_file.read_text(encoding="utf-8") == 'version = "1.2.4"' def test_bump_minor_version(self, tmp_path: Path) -> None: """Should bump minor version correctly.""" - test_file = tmp_path / "test.txt" - test_file.write_text("version = 1.2.3", encoding="utf-8") + test_file = tmp_path / "pyproject.toml" + test_file.write_text('version = "1.2.3"', encoding="utf-8") result = bumpversion.bump_file_version(test_file, "minor") assert result == "1.3.0" - assert test_file.read_text(encoding="utf-8") == "version = 1.3.0" + assert test_file.read_text(encoding="utf-8") == 'version = "1.3.0"' def test_bump_major_version(self, tmp_path: Path) -> None: """Should bump major version correctly.""" - test_file = tmp_path / "test.txt" - test_file.write_text("version = 1.2.3", encoding="utf-8") + test_file = tmp_path / "pyproject.toml" + test_file.write_text('version = "1.2.3"', encoding="utf-8") result = bumpversion.bump_file_version(test_file, "major") assert result == "2.0.0" - assert test_file.read_text(encoding="utf-8") == "version = 2.0.0" + assert test_file.read_text(encoding="utf-8") == 'version = "2.0.0"' def test_version_pattern_with_prerelease(self, tmp_path: Path) -> None: """Should handle version with prerelease suffix.""" - test_file = tmp_path / "test.txt" - test_file.write_text("version = 1.2.3-alpha.1", encoding="utf-8") + test_file = tmp_path / "pyproject.toml" + test_file.write_text('version = "1.2.3-alpha.1"', encoding="utf-8") result = bumpversion.bump_file_version(test_file, "patch") @@ -59,8 +59,8 @@ class TestBumpFileVersion: def test_version_pattern_with_build_metadata(self, tmp_path: Path) -> None: """Should handle version with build metadata.""" - test_file = tmp_path / "test.txt" - test_file.write_text("version = 1.2.3+build.123", encoding="utf-8") + test_file = tmp_path / "pyproject.toml" + test_file.write_text('version = "1.2.3+build.123"', encoding="utf-8") result = bumpversion.bump_file_version(test_file, "patch") @@ -82,13 +82,13 @@ class TestBumpFileVersion: def test_utf8_encoding(self, tmp_path: Path) -> None: """Should handle UTF-8 encoded files correctly.""" - test_file = tmp_path / "test.txt" - test_file.write_text("版本 = 1.2.3", encoding="utf-8") + test_file = tmp_path / "__init__.py" + test_file.write_text('__version__ = "1.2.3"', encoding="utf-8") result = bumpversion.bump_file_version(test_file, "patch") assert result == "1.2.4" - assert test_file.read_text(encoding="utf-8") == "版本 = 1.2.4" + assert test_file.read_text(encoding="utf-8") == '__version__ = "1.2.4"' def test_pyproject_toml_format(self, tmp_path: Path) -> None: """Should handle pyproject.toml format correctly.""" @@ -124,16 +124,23 @@ __version__ = "1.0.0" assert '__version__ = "2.0.0"' in updated def test_multiple_versions_in_file(self, tmp_path: Path) -> None: - """Should only bump the first version occurrence.""" - test_file = tmp_path / "test.txt" - test_file.write_text("version1 = 1.0.0\nversion2 = 2.0.0", encoding="utf-8") + """Should only bump the project version, not dependencies.""" + test_file = tmp_path / "pyproject.toml" + content = """ +[project] +version = "1.0.0" +dependencies = ["lib >= 2.0.0", "other >= 3.0.0"] +""" + test_file.write_text(content, encoding="utf-8") result = bumpversion.bump_file_version(test_file, "patch") assert result == "1.0.1" - content = test_file.read_text(encoding="utf-8") - assert "version1 = 1.0.1" in content - assert "version2 = 2.0.0" in content + updated = test_file.read_text(encoding="utf-8") + assert 'version = "1.0.1"' in updated + # 确保 dependencies 中的版本没有被更新 + assert "lib >= 2.0.0" in updated + assert "other >= 3.0.0" in updated def test_file_read_error(self, tmp_path: Path, capsys) -> None: """Should handle file read errors.""" @@ -147,13 +154,13 @@ __version__ = "1.0.0" def test_file_write_error(self, tmp_path: Path, capsys) -> None: """Should handle file write errors.""" # 在只读目录中创建文件(这个测试在某些系统上可能不适用) - test_file = tmp_path / "readonly.txt" - test_file.write_text("version = 1.0.0", encoding="utf-8") + test_file = tmp_path / "readonly.toml" + test_file.write_text('version = "1.0.0"', encoding="utf-8") # 设置为只读 test_file.chmod(0o444) try: - with pytest.raises(Exception): + with pytest.raises(Exception): # noqa: B017 bumpversion.bump_file_version(test_file, "patch") finally: # 恢复权限以便清理 @@ -168,8 +175,8 @@ class TestVersionPattern: def test_simple_version(self, tmp_path: Path) -> None: """Should match simple version.""" - test_file = tmp_path / "test.txt" - test_file.write_text("1.0.0", encoding="utf-8") + test_file = tmp_path / "__init__.py" + test_file.write_text('__version__ = "1.0.0"', encoding="utf-8") result = bumpversion.bump_file_version(test_file, "patch") @@ -177,8 +184,8 @@ class TestVersionPattern: def test_version_with_zeros(self, tmp_path: Path) -> None: """Should handle versions with zeros correctly.""" - test_file = tmp_path / "test.txt" - test_file.write_text("0.0.0", encoding="utf-8") + test_file = tmp_path / "__init__.py" + test_file.write_text('__version__ = "0.0.0"', encoding="utf-8") result = bumpversion.bump_file_version(test_file, "patch") @@ -186,21 +193,22 @@ class TestVersionPattern: def test_large_version_numbers(self, tmp_path: Path) -> None: """Should handle large version numbers.""" - test_file = tmp_path / "test.txt" - test_file.write_text("10.20.30", encoding="utf-8") + test_file = tmp_path / "__init__.py" + test_file.write_text('__version__ = "10.20.30"', encoding="utf-8") result = bumpversion.bump_file_version(test_file, "minor") assert result == "10.21.0" def test_version_in_url(self, tmp_path: Path) -> None: - """Should only match first version in complex text.""" + """Should not match version in URL or other contexts.""" test_file = tmp_path / "test.txt" test_file.write_text("https://example.com/v1.2.3/download", encoding="utf-8") result = bumpversion.bump_file_version(test_file, "patch") - assert result == "1.2.4" + # 不应该匹配 URL 中的版本号 + assert result is None # ---------------------------------------------------------------------- # @@ -222,8 +230,8 @@ class TestEdgeCases: def test_file_with_special_chars(self, tmp_path: Path) -> None: """Should handle file with special characters.""" - test_file = tmp_path / "test.txt" - content = "# 中文注释\nversion = 1.0.0\n# 特殊字符: @#$%" + test_file = tmp_path / "__init__.py" + content = '# 中文注释\n__version__ = "1.0.0"\n# 特殊字符: @#$%' test_file.write_text(content, encoding="utf-8") result = bumpversion.bump_file_version(test_file, "patch") @@ -235,8 +243,8 @@ class TestEdgeCases: def test_consecutive_bumps(self, tmp_path: Path) -> None: """Should handle consecutive version bumps correctly.""" - test_file = tmp_path / "test.txt" - test_file.write_text("version = 1.0.0", encoding="utf-8") + test_file = tmp_path / "__init__.py" + test_file.write_text('__version__ = "1.0.0"', encoding="utf-8") # 第一次 bump result1 = bumpversion.bump_file_version(test_file, "patch") @@ -251,4 +259,4 @@ class TestEdgeCases: assert result3 == "2.0.0" # 验证最终结果 - assert test_file.read_text(encoding="utf-8") == "version = 2.0.0" + assert test_file.read_text(encoding="utf-8") == '__version__ = "2.0.0"'