Skip to content

Commit 032ef4c

Browse files
committed
Address PR 2695 feedback: Centralize prerelease policy and add boundary test
1 parent f44cbec commit 032ef4c

4 files changed

Lines changed: 45 additions & 38 deletions

File tree

src/specify_cli/_utils.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,3 +304,26 @@ def _display_project_path(project_root: Path, path: str | Path) -> str:
304304
except (OSError, ValueError):
305305
return path_obj.as_posix()
306306
return rel_path.as_posix()
307+
308+
def version_satisfies(current: str, required: str) -> bool:
309+
"""Check if current version satisfies required version specifier.
310+
311+
Evaluates the version against the specifier using the project's
312+
prerelease policy (prereleases are allowed).
313+
314+
Args:
315+
current: Current version (e.g., "0.1.5")
316+
required: Required version specifier (e.g., ">=0.1.0,<2.0.0")
317+
318+
Returns:
319+
True if version satisfies requirement
320+
"""
321+
from packaging import version as pkg_version
322+
from packaging.specifiers import InvalidSpecifier, SpecifierSet
323+
324+
try:
325+
current_ver = pkg_version.Version(current)
326+
specifier = SpecifierSet(required)
327+
return specifier.contains(current_ver, prereleases=True)
328+
except (pkg_version.InvalidVersion, InvalidSpecifier):
329+
return False

src/specify_cli/extensions/__init__.py

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
from .._init_options import is_ai_skills_enabled
3030
from .._invocation_style import is_dollar_skills_agent, is_slash_skills_agent
31-
from .._utils import dump_frontmatter, relative_extension_path_violation
31+
from .._utils import dump_frontmatter, relative_extension_path_violation, version_satisfies
3232
from ..catalogs import CatalogEntry as BaseCatalogEntry
3333
from ..catalogs import CatalogStackBase
3434
from ..shared_infra import verify_archive_sha256
@@ -1279,20 +1279,21 @@ def check_compatibility(
12791279
CompatibilityError: If extension is incompatible
12801280
"""
12811281
required = manifest.requires_speckit_version
1282-
current = pkg_version.Version(speckit_version)
12831282

12841283
# Parse version specifier (e.g., ">=0.1.0,<2.0.0")
12851284
try:
1286-
specifier = SpecifierSet(required)
1287-
if not specifier.contains(current, prereleases=True):
1288-
raise CompatibilityError(
1289-
f"Extension requires spec-kit {required}, "
1290-
f"but {speckit_version} is installed.\n"
1291-
f"Upgrade spec-kit with: {REINSTALL_COMMAND}"
1292-
)
1285+
SpecifierSet(required) # Just to validate
12931286
except InvalidSpecifier:
12941287
raise CompatibilityError(f"Invalid version specifier: {required}")
12951288

1289+
if not version_satisfies(speckit_version, required):
1290+
raise CompatibilityError(
1291+
f"Extension requires spec-kit {required}, "
1292+
f"but {speckit_version} is installed.\n"
1293+
f"Upgrade spec-kit with: {REINSTALL_COMMAND}"
1294+
)
1295+
raise CompatibilityError(f"Invalid version specifier: {required}")
1296+
12961297
return True
12971298

12981299
def install_from_directory(
@@ -1871,22 +1872,6 @@ def get_extension(self, extension_id: str) -> Optional[ExtensionManifest]:
18711872
return None
18721873

18731874

1874-
def version_satisfies(current: str, required: str) -> bool:
1875-
"""Check if current version satisfies required version specifier.
1876-
1877-
Args:
1878-
current: Current version (e.g., "0.1.5")
1879-
required: Required version specifier (e.g., ">=0.1.0,<2.0.0")
1880-
1881-
Returns:
1882-
True if version satisfies requirement
1883-
"""
1884-
try:
1885-
current_ver = pkg_version.Version(current)
1886-
specifier = SpecifierSet(required)
1887-
return specifier.contains(current_ver, prereleases=True)
1888-
except (pkg_version.InvalidVersion, InvalidSpecifier):
1889-
return False
18901875

18911876

18921877
class CommandRegistrar:

src/specify_cli/presets/__init__.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from ..extensions import REINSTALL_COMMAND, ExtensionRegistry, normalize_priority
3131
from .._init_options import is_ai_skills_enabled
3232
from ..integrations.base import IntegrationBase
33-
from .._utils import dump_frontmatter
33+
from .._utils import dump_frontmatter, version_satisfies
3434
from ..shared_infra import verify_archive_sha256
3535

3636

@@ -572,19 +572,16 @@ def check_compatibility(
572572
PresetCompatibilityError: If pack is incompatible
573573
"""
574574
required = manifest.requires_speckit_version
575-
current = pkg_version.Version(speckit_version)
576-
577575
try:
578-
specifier = SpecifierSet(required)
579-
if not specifier.contains(current, prereleases=True):
580-
raise PresetCompatibilityError(
581-
f"Preset requires spec-kit {required}, "
582-
f"but {speckit_version} is installed.\n"
583-
f"Upgrade spec-kit with: {REINSTALL_COMMAND}"
584-
)
576+
SpecifierSet(required) # Just to validate
585577
except InvalidSpecifier:
578+
raise PresetCompatibilityError(f"Invalid version specifier: {required}")
579+
580+
if not version_satisfies(speckit_version, required):
586581
raise PresetCompatibilityError(
587-
f"Invalid version specifier: {required}"
582+
f"Preset requires spec-kit {required}, "
583+
f"but {speckit_version} is installed.\n"
584+
f"Upgrade spec-kit with: {REINSTALL_COMMAND}"
588585
)
589586

590587
return True

tests/test_extensions.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@
3737
ValidationError,
3838
CompatibilityError,
3939
normalize_priority,
40-
version_satisfies,
4140
)
41+
from specify_cli._utils import version_satisfies
4242

4343

4444
def can_create_symlink(tmp_path: Path) -> bool:
@@ -2634,8 +2634,10 @@ def test_version_satisfies_complex(self):
26342634
assert not version_satisfies("1.0.3", ">=1.0.0,!=1.0.3")
26352635

26362636
def test_version_satisfies_prerelease(self):
2637-
"""Prerelease builds should satisfy compatible lower bounds."""
2637+
"""Prerelease builds should satisfy compatible lower bounds, but not higher bounds."""
26382638
assert version_satisfies("0.8.8.dev0", ">=0.2.0")
2639+
assert not version_satisfies("0.2.0.dev0", ">=0.2.0")
2640+
assert not version_satisfies("0.8.7.dev1", ">=0.8.8")
26392641

26402642
def test_version_satisfies_invalid(self):
26412643
"""Test invalid version strings."""

0 commit comments

Comments
 (0)