diff --git a/packages/cli/src/pywrangler/sync.py b/packages/cli/src/pywrangler/sync.py index 2f1cb26..e3a9af0 100644 --- a/packages/cli/src/pywrangler/sync.py +++ b/packages/cli/src/pywrangler/sync.py @@ -188,6 +188,18 @@ def _install_requirements_to_vendor(requirements: list[str]) -> str | None: f"Installing packages into [bold]{relative_vendor_path}[/bold]...", extra={"markup": True}, ) + + # Clear pyodide venv site-packages so stale packages from previous syncs + # don't carry over into python_modules. + pyv = get_python_version() + site_packages_path = ( + f"lib/python{pyv}/site-packages" if os.name != "nt" else "Lib/site-packages" + ) + pyodide_site_packages = get_pyodide_venv_path() / site_packages_path + if pyodide_site_packages.is_dir(): + shutil.rmtree(pyodide_site_packages) + pyodide_site_packages.mkdir() + with temp_requirements_file(requirements) as requirements_file: result = run_command( [ @@ -209,13 +221,8 @@ def _install_requirements_to_vendor(requirements: list[str]) -> str | None: if result.returncode != 0: return result.stdout.strip() - pyv = get_python_version() shutil.rmtree(vendor_path) - - site_packages_path = ( - f"lib/python{pyv}/site-packages" if os.name != "nt" else "Lib/site-packages" - ) - shutil.copytree(get_pyodide_venv_path() / site_packages_path, vendor_path) + shutil.copytree(pyodide_site_packages, vendor_path) # Create a pyvenv.cfg file in python_modules to mark it as a virtual environment (vendor_path / "pyvenv.cfg").touch() diff --git a/packages/cli/tests/test_cli.py b/packages/cli/tests/test_cli.py index f9870f2..84483a3 100644 --- a/packages/cli/tests/test_cli.py +++ b/packages/cli/tests/test_cli.py @@ -270,6 +270,41 @@ def test_sync_command_integration(dependencies, test_dir): # noqa: C901 (test c ) +def test_sync_removes_stale_packages(test_dir): + """Test that removing a dependency from pyproject.toml cleans it up from python_modules.""" + create_test_wrangler_jsonc(test_dir, "src/worker.py") + sync_cmd = ["uv", "run", "pywrangler", "sync"] + + # First sync: install click + six + create_test_pyproject(test_dir, ["click", "six"]) + result = subprocess.run( + sync_cmd, capture_output=True, text=True, cwd=test_dir, check=False + ) + assert result.returncode == 0, ( + f"First sync failed: {result.stdout}\n{result.stderr}" + ) + + vendor_path = test_dir / "python_modules" + assert is_package_installed(vendor_path, "click") + assert is_package_installed(vendor_path, "six") + + # Second sync: remove six, keep click + create_test_pyproject(test_dir, ["click"]) + result = subprocess.run( + sync_cmd, capture_output=True, text=True, cwd=test_dir, check=False + ) + assert result.returncode == 0, ( + f"Second sync failed: {result.stdout}\n{result.stderr}" + ) + + assert is_package_installed(vendor_path, "click"), ( + "click should still be installed after second sync" + ) + assert not is_package_installed(vendor_path, "six"), ( + "six should have been removed from python_modules after being dropped from dependencies" + ) + + def test_sync_command_handles_missing_pyproject(): """Test that the sync command correctly handles a missing pyproject.toml file.""" import tempfile diff --git a/packages/cli/tests/test_types.py b/packages/cli/tests/test_types.py index 02a5612..061c6ce 100644 --- a/packages/cli/tests/test_types.py +++ b/packages/cli/tests/test_types.py @@ -69,5 +69,5 @@ def test_types(tmp_path): result = run(["uv", "run", "mypy"], capture_output=True, text=True, check=False) assert 'Revealed type is "js.Env"' in result.stdout assert 'Revealed type is "js.KVNamespace_iface"' in result.stdout - assert 'Revealed type is "builtins.str"' in result.stdout + assert 'Revealed type is "str"' in result.stdout assert "Success: no issues found" in result.stdout