Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 2 additions & 11 deletions packages/cli/src/pywrangler/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
WRANGLER_COMMAND,
WRANGLER_CREATE_COMMAND,
check_wrangler_version,
get_pywrangler_version,
log_startup_info,
run_command,
setup_logging,
Expand Down Expand Up @@ -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.
Expand Down
36 changes: 32 additions & 4 deletions packages/cli/src/pywrangler/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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].",
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/pywrangler/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()}")
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
75 changes: 75 additions & 0 deletions packages/cli/tests/test_version_sync.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from pathlib import Path
from unittest.mock import patch

import pytest

import pywrangler.sync as pywrangler_sync


Expand Down Expand Up @@ -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
Loading