diff --git a/src/specify_cli/bundles/versioning.py b/src/specify_cli/bundles/versioning.py index df73b5bab6..0837b38bee 100644 --- a/src/specify_cli/bundles/versioning.py +++ b/src/specify_cli/bundles/versioning.py @@ -95,7 +95,8 @@ def same_version(actual: str, pinned: str) -> bool: r"^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)" r"(?:-(?:(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)" r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?" - r"(?:\+(?:[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" + r"(?:\+(?:[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$", + flags=re.ASCII, ) diff --git a/tests/specify_cli/bundles/test_command_validate.py b/tests/specify_cli/bundles/test_command_validate.py index f0b22f7963..28da55bdd8 100644 --- a/tests/specify_cli/bundles/test_command_validate.py +++ b/tests/specify_cli/bundles/test_command_validate.py @@ -6,6 +6,7 @@ from unittest.mock import patch # noqa: F401 import yaml # noqa: F401 +import pytest from typer.testing import CliRunner from specify_cli import app @@ -27,9 +28,12 @@ def test_validate_reports_invalid_manifest(project: Path): assert "license" in result.output -def test_validate_accepts_valid_manifest(project: Path): +@pytest.mark.parametrize("version", ["1.2.0", "1.20.30-12alpha.1+build.01", "V10.20.30"]) +def test_validate_accepts_valid_manifest(project: Path, version: str): + data = valid_manifest_dict() + data["bundle"]["version"] = version (project / "bundle.yml").write_text( - yaml.safe_dump(valid_manifest_dict()), encoding="utf-8" + yaml.safe_dump(data), encoding="utf-8" ) # Offline mode does not fail on references it cannot verify (synthetic ids # here); they surface as warnings while structure is confirmed valid. @@ -38,6 +42,23 @@ def test_validate_accepts_valid_manifest(project: Path): assert "valid" in result.output +@pytest.mark.parametrize("version", ["1٢.2.3", "1.2.3-1٢", "1.2.3-٢alpha"]) +@pytest.mark.parametrize("field", ["bundle", "extension"]) +def test_validate_rejects_non_ascii_version(project: Path, version: str, field: str): + data = valid_manifest_dict() + if field == "bundle": + data["bundle"]["version"] = version + else: + data["provides"]["extensions"][0]["version"] = version + (project / "bundle.yml").write_text(yaml.safe_dump(data), encoding="utf-8") + + result = runner.invoke(app, ["bundle", "validate", "--offline"]) + + assert result.exit_code == 1, result.output + assert "invalid" in result.output + assert version in result.output + + def test_validate_escapes_manifest_markup_in_errors(project: Path): data = valid_manifest_dict() # An invalid constraint is echoed back inside the validation error. diff --git a/tests/specify_cli/bundles/test_versioning.py b/tests/specify_cli/bundles/test_versioning.py index aa6dfc1494..36d1117b8e 100644 --- a/tests/specify_cli/bundles/test_versioning.py +++ b/tests/specify_cli/bundles/test_versioning.py @@ -14,6 +14,15 @@ ("1.2.3-alpha1", True), ("1.2.3-beta2", True), ("v1.2.3", True), + ("1.20.30-12alpha.1+build.01", True), + ("V10.20.30", True), + # SemVer identifiers use ASCII digits, not Python's broader Unicode \d. + ("1٢.2.3", False), + ("1.2٣.3", False), + ("1.2.3٤", False), + ("1.2.3-1٢", False), + ("1.2.3-٢alpha", False), + ("1.2.3-1beta", False), ("not-a-version", False), ("", False), # packaging.version.Version accepts these partial versions; SemVer must not.