diff --git a/packages/cli/src/pywrangler/cli.py b/packages/cli/src/pywrangler/cli.py index 25a3616..77afe34 100644 --- a/packages/cli/src/pywrangler/cli.py +++ b/packages/cli/src/pywrangler/cli.py @@ -12,6 +12,7 @@ WRANGLER_COMMAND, WRANGLER_CREATE_COMMAND, check_wrangler_version, + get_pywrangler_version, log_startup_info, run_command, setup_logging, @@ -86,19 +87,9 @@ def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command: return command -def get_version() -> str: - """Get the version of pywrangler.""" - try: - from importlib.metadata import version - - return version("workers-py") - except Exception: - return "unknown" - - @click.group(cls=ProxyToWranglerGroup) @click.option("--debug", is_flag=True, help="Enable debug logging") -@click.version_option(version=get_version(), prog_name="pywrangler") +@click.version_option(version=get_pywrangler_version(), prog_name="pywrangler") def app(debug: bool = False) -> None: """ A CLI tool for Cloudflare Workers. diff --git a/packages/cli/src/pywrangler/sync.py b/packages/cli/src/pywrangler/sync.py index e3a9af0..0c4904f 100644 --- a/packages/cli/src/pywrangler/sync.py +++ b/packages/cli/src/pywrangler/sync.py @@ -15,6 +15,7 @@ get_project_root, get_pyodide_index, get_python_version, + get_pywrangler_version, get_uv_pyodide_interp_name, read_pyproject_toml, run_command, @@ -226,7 +227,7 @@ def _install_requirements_to_vendor(requirements: list[str]) -> str | None: # Create a pyvenv.cfg file in python_modules to mark it as a virtual environment (vendor_path / "pyvenv.cfg").touch() - get_vendor_token_path().touch() + _write_sync_token(get_vendor_token_path()) logger.info( f"Packages installed in [bold]{relative_vendor_path}[/bold].", @@ -266,7 +267,7 @@ def _install_requirements_to_venv(requirements: list[str]) -> str | None: if result.returncode != 0: return result.stdout.strip() - get_venv_workers_token_path().touch() + _write_sync_token(get_venv_workers_token_path()) logger.info( f"Packages installed in [bold]{relative_venv_workers_path}[/bold].", extra={"markup": True}, @@ -361,15 +362,42 @@ def install_requirements(requirements: list[str]) -> None: _log_installed_packages(get_venv_workers_path()) +def _write_sync_token(token: Path) -> None: + """Record the current workers-py version into the given sync token file.""" + token.parent.mkdir(parents=True, exist_ok=True) + token.write_text(get_pywrangler_version()) + + +def _read_sync_token_version(token: Path) -> str | None: + """Read the workers-py version recorded in a sync token, if any.""" + if not token.is_file(): + return None + try: + return token.read_text().strip() or None + except OSError: + return None + + def _is_out_of_date(token: Path, time: float) -> bool: if not token.exists(): return True - return time > token.stat().st_mtime + if time > token.stat().st_mtime: + return True + recorded_version = _read_sync_token_version(token) + current_version = get_pywrangler_version() + if recorded_version != current_version: + logger.debug( + f"workers-py version changed from {recorded_version!r} to {current_version!r}; " + f"{token.parent} needs to be re-synced" + ) + return True + return False def is_sync_needed() -> bool: """ - Checks if pyproject.toml has been modified since the last sync. + Checks if pyproject.toml has been modified since the last sync, or if the + workers-py version has changed since the last sync. Returns: bool: True if sync is needed, False otherwise diff --git a/packages/cli/src/pywrangler/utils.py b/packages/cli/src/pywrangler/utils.py index 0d8aa4b..be2c3ba 100644 --- a/packages/cli/src/pywrangler/utils.py +++ b/packages/cli/src/pywrangler/utils.py @@ -95,7 +95,7 @@ def setup_logging() -> int: return log_level -def _get_pywrangler_version() -> str: +def get_pywrangler_version() -> str: """Get the version of pywrangler.""" try: from importlib.metadata import version @@ -109,7 +109,7 @@ def log_startup_info() -> None: """ Log startup information for debugging. """ - logger.debug(f"pywrangler version: {_get_pywrangler_version()}") + logger.debug(f"pywrangler version: {get_pywrangler_version()}") logger.debug(f"Python: {platform.python_version()}") logger.debug(f"Platform: {sys.platform}") logger.debug(f"Working directory: {Path.cwd()}") diff --git a/packages/cli/tests/test_cli.py b/packages/cli/tests/test_cli.py index 84483a3..bf491f1 100644 --- a/packages/cli/tests/test_cli.py +++ b/packages/cli/tests/test_cli.py @@ -732,10 +732,10 @@ def test_startup_banner(test_dir, monkeypatch): importlib.reload(pywrangler.utils) - from pywrangler.utils import _get_pywrangler_version, log_startup_info + from pywrangler.utils import get_pywrangler_version, log_startup_info # Verify the functions exist and return expected content - version = _get_pywrangler_version() + version = get_pywrangler_version() assert version is not None # Verify log_startup_info can be called without error diff --git a/packages/cli/tests/test_version_sync.py b/packages/cli/tests/test_version_sync.py index fd14d0c..c05472a 100644 --- a/packages/cli/tests/test_version_sync.py +++ b/packages/cli/tests/test_version_sync.py @@ -1,5 +1,8 @@ +from pathlib import Path from unittest.mock import patch +import pytest + import pywrangler.sync as pywrangler_sync @@ -175,3 +178,75 @@ def test_known_pyodide_errors( log_messages = [record.message for record in caplog.records] assert any(message in msg for msg in log_messages) + + +class TestSyncTokenVersion: + """Tests for workers-py version tracking inside sync token files.""" + + @pytest.fixture + def project_root(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text("[project]\nname='x'\nversion='0.0.0'\n") + monkeypatch.setattr(pywrangler_sync, "find_pyproject_toml", lambda: pyproject) + monkeypatch.setattr(pywrangler_sync, "get_project_root", lambda: tmp_path) + return tmp_path + + def test_write_sync_token_records_current_version( + self, project_root: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setattr(pywrangler_sync, "get_pywrangler_version", lambda: "1.2.3") + token = project_root / ".venv-workers" / ".synced" + + pywrangler_sync._write_sync_token(token) + + assert token.is_file() + assert token.read_text().strip() == "1.2.3" + + def test_sync_not_needed_when_version_matches( + self, project_root: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setattr(pywrangler_sync, "get_pywrangler_version", lambda: "1.2.3") + pywrangler_sync._write_sync_token(pywrangler_sync.get_vendor_token_path()) + pywrangler_sync._write_sync_token(pywrangler_sync.get_venv_workers_token_path()) + + assert pywrangler_sync.is_sync_needed() is False + + def test_sync_needed_when_version_changes( + self, project_root: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setattr(pywrangler_sync, "get_pywrangler_version", lambda: "1.2.3") + pywrangler_sync._write_sync_token(pywrangler_sync.get_vendor_token_path()) + pywrangler_sync._write_sync_token(pywrangler_sync.get_venv_workers_token_path()) + + # Simulate upgrading workers-py without touching pyproject.toml. + monkeypatch.setattr(pywrangler_sync, "get_pywrangler_version", lambda: "1.2.4") + + assert pywrangler_sync.is_sync_needed() is True + + def test_sync_needed_when_only_vendor_version_changes( + self, project_root: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setattr(pywrangler_sync, "get_pywrangler_version", lambda: "1.2.3") + pywrangler_sync._write_sync_token(pywrangler_sync.get_venv_workers_token_path()) + # vendor token written with an older version + monkeypatch.setattr(pywrangler_sync, "get_pywrangler_version", lambda: "1.0.0") + pywrangler_sync._write_sync_token(pywrangler_sync.get_vendor_token_path()) + + # Current version matches venv token but not vendor token. + monkeypatch.setattr(pywrangler_sync, "get_pywrangler_version", lambda: "1.2.3") + + assert pywrangler_sync.is_sync_needed() is True + + def test_sync_needed_when_token_missing_version( + self, project_root: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setattr(pywrangler_sync, "get_pywrangler_version", lambda: "1.2.3") + # Write empty tokens to simulate pre-existing `.synced` files from older CLI versions. + vendor_token = pywrangler_sync.get_vendor_token_path() + venv_token = pywrangler_sync.get_venv_workers_token_path() + vendor_token.parent.mkdir(parents=True, exist_ok=True) + venv_token.parent.mkdir(parents=True, exist_ok=True) + vendor_token.write_text("") + venv_token.write_text("") + + assert pywrangler_sync.is_sync_needed() is True