44import shutil
55import tempfile
66from contextlib import contextmanager
7- from datetime import datetime
87from pathlib import Path
9- from typing import Literal
108
119import click
12- import pyjson5
10+ import tomllib
1311
1412from .utils import (
1513 run_command ,
1614 find_pyproject_toml ,
15+ get_python_version ,
16+ get_pyodide_index ,
17+ get_uv_pyodide_interp_name ,
18+ get_project_root ,
1719)
18- from .metadata import PYTHON_COMPAT_VERSIONS
1920
20- try :
21- import tomllib # Standard in Python 3.11+
22- except ImportError :
23- import tomli as tomllib # For Python < 3.11
2421
2522logger = logging .getLogger (__name__ )
2623
27- # Define paths
28- PYPROJECT_TOML_PATH = find_pyproject_toml ()
29- PROJECT_ROOT = PYPROJECT_TOML_PATH .parent
30- VENV_WORKERS_PATH = PROJECT_ROOT / ".venv-workers"
31- VENV_WORKERS_TOKEN = PROJECT_ROOT / ".venv-workers/.synced"
32- PYODIDE_VENV_PATH = VENV_WORKERS_PATH / "pyodide-venv"
33- VENDOR_TOKEN = PROJECT_ROOT / "python_modules/.synced"
34- VENV_REQUIREMENTS_PATH = VENV_WORKERS_PATH / "temp-venv-requirements.txt"
24+
25+ def get_venv_workers_path ():
26+ return get_project_root () / ".venv-workers"
27+
28+
29+ def get_venv_workers_token_path ():
30+ return get_venv_workers_path () / ".synced"
31+
32+
33+ def get_vendor_token_path ():
34+ return get_project_root () / "python_modules/.synced"
35+
36+
37+ def get_pyodide_venv_path ():
38+ return get_venv_workers_path () / "pyodide-venv"
3539
3640
3741def check_requirements_txt ():
38- old_requirements_txt = PROJECT_ROOT / "requirements.txt"
42+ old_requirements_txt = get_project_root () / "requirements.txt"
3943 if old_requirements_txt .is_file ():
4044 with open (old_requirements_txt , "r" ) as f :
4145 requirements = f .read ().splitlines ()
@@ -54,129 +58,18 @@ def check_requirements_txt():
5458 raise click .exceptions .Exit (code = 1 )
5559
5660
57- def check_wrangler_config ():
58- wrangler_jsonc = PROJECT_ROOT / "wrangler.jsonc"
59- wrangler_toml = PROJECT_ROOT / "wrangler.toml"
60- if not wrangler_jsonc .is_file () and not wrangler_toml .is_file ():
61- logger .error (
62- f"{ wrangler_jsonc } or { wrangler_toml } not found in { PROJECT_ROOT } ."
63- )
64- raise click .exceptions .Exit (code = 1 )
65-
66-
67- def _parse_wrangler_config () -> dict :
68- """
69- Parse wrangler configuration from either wrangler.toml or wrangler.jsonc.
70-
71- Returns:
72- dict: Parsed configuration data
73- """
74- wrangler_toml = PROJECT_ROOT / "wrangler.toml"
75- wrangler_jsonc = PROJECT_ROOT / "wrangler.jsonc"
76-
77- if wrangler_toml .is_file ():
78- try :
79- with open (wrangler_toml , "rb" ) as f :
80- return tomllib .load (f )
81- except tomllib .TOMLDecodeError as e :
82- logger .error (f"Error parsing { wrangler_toml } : { e } " )
83- raise click .exceptions .Exit (code = 1 )
84-
85- if wrangler_jsonc .is_file ():
86- try :
87- with open (wrangler_jsonc , "r" ) as f :
88- content = f .read ()
89- return pyjson5 .loads (content )
90- except (pyjson5 .Json5DecoderError , ValueError ) as e :
91- logger .error (f"Error parsing { wrangler_jsonc } : { e } " )
92- raise click .exceptions .Exit (code = 1 )
93-
94- return {}
95-
96-
97- def _get_python_version () -> Literal ["3.12" , "3.13" ]:
98- """
99- Determine Python version from wrangler configuration.
100-
101- Returns:
102- Python version string
103- """
104- config = _parse_wrangler_config ()
105-
106- if not config :
107- logger .error ("No wrangler config found" )
108- raise click .exceptions .Exit (code = 1 )
109-
110- compat_flags = config .get ("compatibility_flags" , [])
111-
112- if "compatibility_date" not in config :
113- logger .error ("No compatibility_date specified in wrangler config" )
114- raise click .exceptions .Exit (code = 1 )
115- try :
116- compat_date = datetime .strptime (config .get ("compatibility_date" ), "%Y-%m-%d" )
117- except ValueError :
118- logger .error (
119- f"Invalid compatibility_date format: { config .get ('compatibility_date' )} "
120- )
121- raise click .exceptions .Exit (code = 1 )
122-
123- # Check if python_workers base flag is present (required for Python workers)
124- if "python_workers" not in compat_flags :
125- logger .error ("`python_workers` compat flag not specified in wrangler config" )
126- raise click .exceptions .Exit (code = 1 )
127-
128- # Find the most specific Python version based on compat flags and date
129- # Sort by version descending to prioritize newer versions
130- sorted_versions = sorted (
131- PYTHON_COMPAT_VERSIONS , key = lambda x : x .version , reverse = True
132- )
133-
134- for py_version in sorted_versions :
135- # Check if the specific compat flag is present
136- if py_version .compat_flag in compat_flags :
137- return py_version .version
138-
139- # For versions with compat_date, also check the date requirement
140- if (
141- py_version .compat_date
142- and compat_date
143- and compat_date >= py_version .compat_date
144- ):
145- return py_version .version
146-
147- logger .error ("Could not determine Python version from wrangler config" )
148- raise click .exceptions .Exit (code = 1 )
149-
150-
151- def _get_uv_pyodide_interp_name ():
152- match _get_python_version ():
153- case "3.12" :
154- v = "3.12.7"
155- case "3.13" :
156- v = "3.13.2"
157- return f"cpython-{ v } -emscripten-wasm32-musl"
158-
159-
160- def _get_pyodide_index ():
161- match _get_python_version ():
162- case "3.12" :
163- v = "0.27.7"
164- case "3.13" :
165- v = "0.28.3"
166- return "https://index.pyodide.org/" + v
167-
168-
16961def _get_venv_python_version () -> str | None :
17062 """
17163 Retrieves the Python version from the virtual environment.
17264
17365 Returns:
17466 The Python version string or None if it cannot be determined.
17567 """
68+ venv_workers_path = get_venv_workers_path ()
17669 venv_python = (
177- VENV_WORKERS_PATH / "Scripts" / "python.exe"
70+ venv_workers_path / "Scripts" / "python.exe"
17871 if os .name == "nt"
179- else VENV_WORKERS_PATH / "bin" / "python"
72+ else venv_workers_path / "bin" / "python"
18073 )
18174 if not venv_python .is_file ():
18275 return None
@@ -192,37 +85,38 @@ def _get_venv_python_version() -> str | None:
19285
19386def create_workers_venv ():
19487 """
195- Creates a virtual environment at `VENV_WORKERS_PATH ` if it doesn't exist.
88+ Creates a virtual environment at `venv_workers_path ` if it doesn't exist.
19689 """
197- wanted_python_version = _get_python_version ()
90+ wanted_python_version = get_python_version ()
19891 logger .debug (f"Using python version from wrangler config: { wanted_python_version } " )
19992
200- if VENV_WORKERS_PATH .is_dir ():
93+ venv_workers_path = get_venv_workers_path ()
94+ if venv_workers_path .is_dir ():
20195 installed_version = _get_venv_python_version ()
20296 if installed_version :
20397 if wanted_python_version in installed_version :
20498 logger .debug (
205- f"Virtual environment at { VENV_WORKERS_PATH } already exists."
99+ f"Virtual environment at { venv_workers_path } already exists."
206100 )
207101 return
208102
209103 logger .warning (
210- f"Recreating virtual environment at { VENV_WORKERS_PATH } due to Python version mismatch. "
104+ f"Recreating virtual environment at { venv_workers_path } due to Python version mismatch. "
211105 f"Found { installed_version } , expected { wanted_python_version } "
212106 )
213107 else :
214108 logger .warning (
215- f"Could not determine python version for { VENV_WORKERS_PATH } , recreating."
109+ f"Could not determine python version for { venv_workers_path } , recreating."
216110 )
217111
218- shutil .rmtree (VENV_WORKERS_PATH )
112+ shutil .rmtree (venv_workers_path )
219113
220- logger .debug (f"Creating virtual environment at { VENV_WORKERS_PATH } ..." )
114+ logger .debug (f"Creating virtual environment at { venv_workers_path } ..." )
221115 run_command (
222116 [
223117 "uv" ,
224118 "venv" ,
225- str (VENV_WORKERS_PATH ),
119+ str (venv_workers_path ),
226120 "--python" ,
227121 f"python{ wanted_python_version } " ,
228122 ]
@@ -288,24 +182,26 @@ def check_wrangler_version():
288182
289183
290184def create_pyodide_venv ():
291- if PYODIDE_VENV_PATH .is_dir ():
185+ pyodide_venv_path = get_pyodide_venv_path ()
186+ if pyodide_venv_path .is_dir ():
292187 logger .debug (
293- f"Pyodide virtual environment at { PYODIDE_VENV_PATH } already exists."
188+ f"Pyodide virtual environment at { pyodide_venv_path } already exists."
294189 )
295190 return
296191
297192 check_uv_version ()
298- logger .debug (f"Creating Pyodide virtual environment at { PYODIDE_VENV_PATH } ..." )
299- PYODIDE_VENV_PATH .parent .mkdir (parents = True , exist_ok = True )
300- interp_name = _get_uv_pyodide_interp_name ()
193+ logger .debug (f"Creating Pyodide virtual environment at { pyodide_venv_path } ..." )
194+ pyodide_venv_path .parent .mkdir (parents = True , exist_ok = True )
195+ interp_name = get_uv_pyodide_interp_name ()
301196 run_command (["uv" , "python" , "install" , interp_name ])
302- run_command (["uv" , "venv" , PYODIDE_VENV_PATH , "--python" , interp_name ])
197+ run_command (["uv" , "venv" , pyodide_venv_path , "--python" , interp_name ])
303198
304199
305200def parse_requirements () -> list [str ]:
306- logger .debug (f"Reading dependencies from { PYPROJECT_TOML_PATH } ..." )
201+ pyproject_toml_path = find_pyproject_toml ()
202+ logger .debug (f"Reading dependencies from { pyproject_toml_path } ..." )
307203 try :
308- with open (PYPROJECT_TOML_PATH , "rb" ) as f :
204+ with open (pyproject_toml_path , "rb" ) as f :
309205 pyproject_data = tomllib .load (f )
310206
311207 # Extract dependencies from [project.dependencies]
@@ -314,7 +210,7 @@ def parse_requirements() -> list[str]:
314210 logger .info (f"Found { len (dependencies )} dependencies." )
315211 return dependencies
316212 except tomllib .TOMLDecodeError as e :
317- logger .error (f"Error parsing { PYPROJECT_TOML_PATH } : { str (e )} " )
213+ logger .error (f"Error parsing { pyproject_toml_path } : { str (e )} " )
318214 raise click .exceptions .Exit (code = 1 )
319215
320216
@@ -328,7 +224,7 @@ def temp_requirements_file(requirements: list[str]):
328224
329225
330226def _install_requirements_to_vendor (requirements : list [str ]):
331- vendor_path = PROJECT_ROOT / "python_modules"
227+ vendor_path = get_project_root () / "python_modules"
332228 logger .debug (f"Using vendor path: { vendor_path } " )
333229
334230 if len (requirements ) == 0 :
@@ -339,7 +235,7 @@ def _install_requirements_to_vendor(requirements: list[str]):
339235
340236 # Install packages into vendor directory
341237 vendor_path .mkdir (parents = True , exist_ok = True )
342- relative_vendor_path = vendor_path .relative_to (PROJECT_ROOT )
238+ relative_vendor_path = vendor_path .relative_to (get_project_root () )
343239 logger .info (
344240 f"Installing packages into [bold]{ relative_vendor_path } [/bold]..." ,
345241 extra = {"markup" : True },
@@ -354,21 +250,21 @@ def _install_requirements_to_vendor(requirements: list[str]):
354250 "-r" ,
355251 requirements_file ,
356252 "--extra-index-url" ,
357- _get_pyodide_index (),
253+ get_pyodide_index (),
358254 "--index-strategy" ,
359255 "unsafe-best-match" ,
360256 ],
361- env = os .environ | {"VIRTUAL_ENV" : PYODIDE_VENV_PATH },
257+ env = os .environ | {"VIRTUAL_ENV" : get_pyodide_venv_path () },
362258 )
363- pyv = _get_python_version ()
259+ pyv = get_python_version ()
364260 shutil .rmtree (vendor_path )
365261 shutil .copytree (
366- PYODIDE_VENV_PATH / f"lib/python{ pyv } /site-packages" , vendor_path
262+ get_pyodide_venv_path () / f"lib/python{ pyv } /site-packages" , vendor_path
367263 )
368264
369265 # Create a pyvenv.cfg file in python_modules to mark it as a virtual environment
370266 (vendor_path / "pyvenv.cfg" ).touch ()
371- VENDOR_TOKEN .touch ()
267+ get_vendor_token_path () .touch ()
372268
373269 logger .info (
374270 f"Packages installed in [bold]{ relative_vendor_path } [/bold]." ,
@@ -378,7 +274,9 @@ def _install_requirements_to_vendor(requirements: list[str]):
378274
379275def _install_requirements_to_venv (requirements : list [str ]):
380276 # Create a requirements file for .venv-workers that includes pyodide-py
381- relative_venv_workers_path = VENV_WORKERS_PATH .relative_to (PROJECT_ROOT )
277+ venv_workers_path = get_venv_workers_path ()
278+ project_root = get_project_root ()
279+ relative_venv_workers_path = venv_workers_path .relative_to (project_root )
382280 requirements = requirements .copy ()
383281 requirements .append ("pyodide-py" )
384282
@@ -395,9 +293,10 @@ def _install_requirements_to_venv(requirements: list[str]):
395293 "-r" ,
396294 requirements_file ,
397295 ],
398- env = os .environ | {"VIRTUAL_ENV" : VENV_WORKERS_PATH },
296+ env = os .environ | {"VIRTUAL_ENV" : venv_workers_path },
399297 )
400- VENV_WORKERS_TOKEN .touch ()
298+
299+ get_venv_workers_token_path ().touch ()
401300 logger .info (
402301 f"Packages installed in [bold]{ relative_venv_workers_path } [/bold]." ,
403302 extra = {"markup" : True },
@@ -422,12 +321,12 @@ def is_sync_needed():
422321 Returns:
423322 bool: True if sync is needed, False otherwise
424323 """
425-
426- if not PYPROJECT_TOML_PATH .is_file ():
324+ pyproject_toml_path = find_pyproject_toml ()
325+ if not pyproject_toml_path .is_file ():
427326 # If pyproject.toml doesn't exist, we need to abort anyway
428327 return True
429328
430- pyproject_mtime = PYPROJECT_TOML_PATH .stat ().st_mtime
431- return _is_out_of_date (VENDOR_TOKEN , pyproject_mtime ) or _is_out_of_date (
432- VENV_WORKERS_TOKEN , pyproject_mtime
329+ pyproject_mtime = pyproject_toml_path .stat ().st_mtime
330+ return _is_out_of_date (get_vendor_token_path () , pyproject_mtime ) or _is_out_of_date (
331+ get_venv_workers_token_path () , pyproject_mtime
433332 )
0 commit comments