workers-py 1.6.2__tar.gz → 1.7.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {workers_py-1.6.2 → workers_py-1.7.1}/.github/workflows/tests.yml +1 -1
- {workers_py-1.6.2 → workers_py-1.7.1}/.gitignore +2 -0
- {workers_py-1.6.2 → workers_py-1.7.1}/.pre-commit-config.yaml +0 -1
- {workers_py-1.6.2 → workers_py-1.7.1}/CHANGELOG.md +25 -0
- {workers_py-1.6.2 → workers_py-1.7.1}/PKG-INFO +1 -1
- {workers_py-1.6.2 → workers_py-1.7.1}/pyproject.toml +1 -1
- {workers_py-1.6.2 → workers_py-1.7.1}/src/pywrangler/cli.py +10 -2
- {workers_py-1.6.2 → workers_py-1.7.1}/src/pywrangler/sync.py +70 -8
- {workers_py-1.6.2 → workers_py-1.7.1}/src/pywrangler/types.py +5 -3
- {workers_py-1.6.2 → workers_py-1.7.1}/src/pywrangler/utils.py +88 -11
- {workers_py-1.6.2 → workers_py-1.7.1}/tests/test_cli.py +122 -2
- {workers_py-1.6.2 → workers_py-1.7.1}/tests/test_types.py +5 -0
- {workers_py-1.6.2 → workers_py-1.7.1}/uv.lock +1 -1
- {workers_py-1.6.2 → workers_py-1.7.1}/.gitattributes +0 -0
- {workers_py-1.6.2 → workers_py-1.7.1}/.github/workflows/commitlint.yml +0 -0
- {workers_py-1.6.2 → workers_py-1.7.1}/.github/workflows/lint.yml +0 -0
- {workers_py-1.6.2 → workers_py-1.7.1}/.github/workflows/release.yml +0 -0
- {workers_py-1.6.2 → workers_py-1.7.1}/CLAUDE.md +0 -0
- {workers_py-1.6.2 → workers_py-1.7.1}/CONTRIBUTING.md +0 -0
- {workers_py-1.6.2 → workers_py-1.7.1}/README.md +0 -0
- {workers_py-1.6.2 → workers_py-1.7.1}/src/pywrangler/__init__.py +0 -0
- {workers_py-1.6.2 → workers_py-1.7.1}/src/pywrangler/__main__.py +0 -0
- {workers_py-1.6.2 → workers_py-1.7.1}/src/pywrangler/metadata.py +0 -0
- {workers_py-1.6.2 → workers_py-1.7.1}/src/pywrangler/py.typed +0 -0
- {workers_py-1.6.2 → workers_py-1.7.1}/tests/test_py_version_detect.py +0 -0
- {workers_py-1.6.2 → workers_py-1.7.1}/workers.py +0 -0
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v1.7.1 (2026-02-03)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Address comment ([#62](https://github.com/cloudflare/workers-py/pull/62),
|
|
10
|
+
[`53ce7be`](https://github.com/cloudflare/workers-py/commit/53ce7be9385f2aa07d8c43d5d3296d6328aafca7))
|
|
11
|
+
|
|
12
|
+
- Better windows platform support ([#62](https://github.com/cloudflare/workers-py/pull/62),
|
|
13
|
+
[`53ce7be`](https://github.com/cloudflare/workers-py/commit/53ce7be9385f2aa07d8c43d5d3296d6328aafca7))
|
|
14
|
+
|
|
15
|
+
- Ensure to cleanup the test directory ([#62](https://github.com/cloudflare/workers-py/pull/62),
|
|
16
|
+
[`53ce7be`](https://github.com/cloudflare/workers-py/commit/53ce7be9385f2aa07d8c43d5d3296d6328aafca7))
|
|
17
|
+
|
|
18
|
+
- Fix incorrect abspath ([#62](https://github.com/cloudflare/workers-py/pull/62),
|
|
19
|
+
[`53ce7be`](https://github.com/cloudflare/workers-py/commit/53ce7be9385f2aa07d8c43d5d3296d6328aafca7))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## v1.7.0 (2025-10-31)
|
|
23
|
+
|
|
24
|
+
### Features
|
|
25
|
+
|
|
26
|
+
- Better errors when unsupported packages are requested
|
|
27
|
+
([`e5000ed`](https://github.com/cloudflare/workers-py/commit/e5000eded90fb89c8f1a46dfb107f6d246f53e89))
|
|
28
|
+
|
|
29
|
+
|
|
5
30
|
## v1.6.2 (2025-10-22)
|
|
6
31
|
|
|
7
32
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: workers-py
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.1
|
|
4
4
|
Summary: A set of libraries and tools for Python Workers
|
|
5
5
|
Project-URL: Homepage, https://github.com/cloudflare/workers-py
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/cloudflare/workers-py/issues
|
|
@@ -2,6 +2,7 @@ import logging
|
|
|
2
2
|
import subprocess
|
|
3
3
|
import sys
|
|
4
4
|
import textwrap
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
from typing import Never
|
|
6
7
|
|
|
7
8
|
import click
|
|
@@ -11,6 +12,8 @@ from .utils import (
|
|
|
11
12
|
WRANGLER_COMMAND,
|
|
12
13
|
WRANGLER_CREATE_COMMAND,
|
|
13
14
|
check_wrangler_version,
|
|
15
|
+
log_startup_info,
|
|
16
|
+
run_command,
|
|
14
17
|
setup_logging,
|
|
15
18
|
write_success,
|
|
16
19
|
)
|
|
@@ -56,6 +59,8 @@ class ProxyToWranglerGroup(click.Group):
|
|
|
56
59
|
command = super().get_command(ctx, cmd_name)
|
|
57
60
|
|
|
58
61
|
if command is None:
|
|
62
|
+
log_startup_info()
|
|
63
|
+
|
|
59
64
|
try:
|
|
60
65
|
cmd_index = sys.argv.index(cmd_name)
|
|
61
66
|
remaining_args = sys.argv[cmd_index + 1 :]
|
|
@@ -106,6 +111,8 @@ def app(debug: bool = False) -> None:
|
|
|
106
111
|
if debug:
|
|
107
112
|
logger.setLevel(logging.DEBUG)
|
|
108
113
|
|
|
114
|
+
log_startup_info()
|
|
115
|
+
|
|
109
116
|
|
|
110
117
|
@app.command("types")
|
|
111
118
|
@click.option(
|
|
@@ -135,6 +142,7 @@ def sync_command(force: bool = False) -> None:
|
|
|
135
142
|
|
|
136
143
|
Also creates a virtual env for Workers that you can use for testing.
|
|
137
144
|
"""
|
|
145
|
+
|
|
138
146
|
sync(force, directly_requested=True)
|
|
139
147
|
write_success("Sync process completed successfully.")
|
|
140
148
|
|
|
@@ -143,7 +151,7 @@ def _proxy_to_wrangler(command_name: str, args_list: list[str]) -> Never:
|
|
|
143
151
|
command_to_run = WRANGLER_COMMAND + [command_name] + args_list
|
|
144
152
|
logger.info(f"Passing command to npx wrangler: {' '.join(command_to_run)}")
|
|
145
153
|
try:
|
|
146
|
-
process =
|
|
154
|
+
process = run_command(command_to_run, check=False, cwd=Path("."))
|
|
147
155
|
click.get_current_context().exit(process.returncode)
|
|
148
156
|
except FileNotFoundError as e:
|
|
149
157
|
logger.error(
|
|
@@ -156,7 +164,7 @@ def _proxy_to_create_cloudflare(args_list: list[str]) -> Never:
|
|
|
156
164
|
command_to_run = WRANGLER_CREATE_COMMAND + args_list
|
|
157
165
|
logger.info(f"Passing command to npx create-cloudflare: {' '.join(command_to_run)}")
|
|
158
166
|
try:
|
|
159
|
-
process =
|
|
167
|
+
process = run_command(command_to_run, check=False, cwd=Path("."))
|
|
160
168
|
click.get_current_context().exit(process.returncode)
|
|
161
169
|
except FileNotFoundError as e:
|
|
162
170
|
logger.error(
|
|
@@ -137,7 +137,7 @@ def create_pyodide_venv() -> None:
|
|
|
137
137
|
pyodide_venv_path.parent.mkdir(parents=True, exist_ok=True)
|
|
138
138
|
interp_name = get_uv_pyodide_interp_name()
|
|
139
139
|
run_command(["uv", "python", "install", interp_name])
|
|
140
|
-
run_command(["uv", "venv", pyodide_venv_path, "--python", interp_name])
|
|
140
|
+
run_command(["uv", "venv", str(pyodide_venv_path), "--python", interp_name])
|
|
141
141
|
|
|
142
142
|
|
|
143
143
|
def parse_requirements() -> list[str]:
|
|
@@ -147,6 +147,9 @@ def parse_requirements() -> list[str]:
|
|
|
147
147
|
dependencies = pyproject_data.get("project", {}).get("dependencies", [])
|
|
148
148
|
|
|
149
149
|
logger.info(f"Found {len(dependencies)} dependencies.")
|
|
150
|
+
if dependencies:
|
|
151
|
+
for dep in dependencies:
|
|
152
|
+
logger.debug(f" - {dep}")
|
|
150
153
|
return dependencies
|
|
151
154
|
|
|
152
155
|
|
|
@@ -177,7 +180,7 @@ def _install_requirements_to_vendor(requirements: list[str]) -> None:
|
|
|
177
180
|
extra={"markup": True},
|
|
178
181
|
)
|
|
179
182
|
with temp_requirements_file(requirements) as requirements_file:
|
|
180
|
-
run_command(
|
|
183
|
+
result = run_command(
|
|
181
184
|
[
|
|
182
185
|
"uv",
|
|
183
186
|
"pip",
|
|
@@ -190,13 +193,41 @@ def _install_requirements_to_vendor(requirements: list[str]) -> None:
|
|
|
190
193
|
"--index-strategy",
|
|
191
194
|
"unsafe-best-match",
|
|
192
195
|
],
|
|
193
|
-
|
|
196
|
+
capture_output=True,
|
|
197
|
+
check=False,
|
|
198
|
+
env=os.environ | {"VIRTUAL_ENV": str(get_pyodide_venv_path())},
|
|
194
199
|
)
|
|
200
|
+
if result.returncode != 0:
|
|
201
|
+
logger.warning(result.stdout.strip())
|
|
202
|
+
# Handle some common failures and give nicer error messages for them.
|
|
203
|
+
lowered_stdout = result.stdout.lower()
|
|
204
|
+
if "invalid peer certificate" in lowered_stdout:
|
|
205
|
+
logger.error(
|
|
206
|
+
"Installation failed because of an invalid peer certificate. Are your systems certificates correctly installed? Do you have an Enterprise VPN enabled?"
|
|
207
|
+
)
|
|
208
|
+
elif "failed to fetch" in lowered_stdout:
|
|
209
|
+
logger.error(
|
|
210
|
+
"Installation failed because of a failed fetch. Is your network connection working?"
|
|
211
|
+
)
|
|
212
|
+
elif "no solution found when resolving dependencies" in lowered_stdout:
|
|
213
|
+
logger.error(
|
|
214
|
+
"Installation failed because the packages you requested are not supported by Python Workers. See above for details."
|
|
215
|
+
)
|
|
216
|
+
else:
|
|
217
|
+
logger.error(
|
|
218
|
+
"Installation of packages into the Python Worker failed. Possibly because these packages are not currently supported. See above for details."
|
|
219
|
+
)
|
|
220
|
+
raise click.exceptions.Exit(code=result.returncode)
|
|
221
|
+
|
|
222
|
+
_log_installed_packages(get_pyodide_venv_path())
|
|
223
|
+
|
|
195
224
|
pyv = get_python_version()
|
|
196
225
|
shutil.rmtree(vendor_path)
|
|
197
|
-
|
|
198
|
-
|
|
226
|
+
|
|
227
|
+
site_packages_path = (
|
|
228
|
+
f"lib/python{pyv}/site-packages" if os.name != "nt" else "Lib/site-packages"
|
|
199
229
|
)
|
|
230
|
+
shutil.copytree(get_pyodide_venv_path() / site_packages_path, vendor_path)
|
|
200
231
|
|
|
201
232
|
# Create a pyvenv.cfg file in python_modules to mark it as a virtual environment
|
|
202
233
|
(vendor_path / "pyvenv.cfg").touch()
|
|
@@ -208,6 +239,20 @@ def _install_requirements_to_vendor(requirements: list[str]) -> None:
|
|
|
208
239
|
)
|
|
209
240
|
|
|
210
241
|
|
|
242
|
+
def _log_installed_packages(venv_path: Path) -> None:
|
|
243
|
+
result = run_command(
|
|
244
|
+
["uv", "pip", "list", "--format=freeze"],
|
|
245
|
+
env=os.environ | {"VIRTUAL_ENV": venv_path},
|
|
246
|
+
capture_output=True,
|
|
247
|
+
check=False,
|
|
248
|
+
)
|
|
249
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
250
|
+
logger.debug("Installed packages:")
|
|
251
|
+
for line in result.stdout.strip().split("\n"):
|
|
252
|
+
if line.strip():
|
|
253
|
+
logger.debug(f" {line.strip()}")
|
|
254
|
+
|
|
255
|
+
|
|
211
256
|
def _install_requirements_to_venv(requirements: list[str]) -> None:
|
|
212
257
|
# Create a requirements file for .venv-workers that includes pyodide-py
|
|
213
258
|
venv_workers_path = get_venv_workers_path()
|
|
@@ -221,7 +266,7 @@ def _install_requirements_to_venv(requirements: list[str]) -> None:
|
|
|
221
266
|
extra={"markup": True},
|
|
222
267
|
)
|
|
223
268
|
with temp_requirements_file(requirements) as requirements_file:
|
|
224
|
-
run_command(
|
|
269
|
+
result = run_command(
|
|
225
270
|
[
|
|
226
271
|
"uv",
|
|
227
272
|
"pip",
|
|
@@ -229,8 +274,16 @@ def _install_requirements_to_venv(requirements: list[str]) -> None:
|
|
|
229
274
|
"-r",
|
|
230
275
|
requirements_file,
|
|
231
276
|
],
|
|
232
|
-
|
|
277
|
+
check=False,
|
|
278
|
+
env=os.environ | {"VIRTUAL_ENV": str(venv_workers_path)},
|
|
279
|
+
capture_output=True,
|
|
233
280
|
)
|
|
281
|
+
if result.returncode != 0:
|
|
282
|
+
logger.warning(result.stdout.strip())
|
|
283
|
+
logger.error(
|
|
284
|
+
"Failed to install the requirements defined in your pyproject.toml file. See above for details."
|
|
285
|
+
)
|
|
286
|
+
raise click.exceptions.Exit(code=result.returncode)
|
|
234
287
|
|
|
235
288
|
get_venv_workers_token_path().touch()
|
|
236
289
|
logger.info(
|
|
@@ -240,8 +293,14 @@ def _install_requirements_to_venv(requirements: list[str]) -> None:
|
|
|
240
293
|
|
|
241
294
|
|
|
242
295
|
def install_requirements(requirements: list[str]) -> None:
|
|
243
|
-
|
|
296
|
+
# Note: the order these are executed is important.
|
|
297
|
+
# We need to install to .venv-workers first, so that we can determine if the packages requested
|
|
298
|
+
# by the user are valid.
|
|
244
299
|
_install_requirements_to_venv(requirements)
|
|
300
|
+
# Then we install the same requirements to the vendor directory. If this installation
|
|
301
|
+
# fails while the above succeeded, it implies that Pyodide does not support these package
|
|
302
|
+
# requirements which allows us to give a nicer error message to the user.
|
|
303
|
+
_install_requirements_to_vendor(requirements)
|
|
245
304
|
|
|
246
305
|
|
|
247
306
|
def _is_out_of_date(token: Path, time: float) -> bool:
|
|
@@ -275,12 +334,15 @@ def sync(force: bool = False, directly_requested: bool = False) -> None:
|
|
|
275
334
|
# Check if sync is needed based on file timestamps
|
|
276
335
|
sync_needed = force or is_sync_needed()
|
|
277
336
|
if not sync_needed:
|
|
337
|
+
logger.debug("Sync not needed - no changes detected")
|
|
278
338
|
if directly_requested:
|
|
279
339
|
logger.warning(
|
|
280
340
|
"pyproject.toml hasn't changed since last sync, use --force to ignore timestamp check"
|
|
281
341
|
)
|
|
282
342
|
return
|
|
283
343
|
|
|
344
|
+
logger.debug("Sync needed - proceeding with installation")
|
|
345
|
+
|
|
284
346
|
# Check to make sure a wrangler config file exists.
|
|
285
347
|
check_wrangler_config()
|
|
286
348
|
|
|
@@ -39,8 +39,10 @@ def wrangler_types(outdir_arg: str | None, config: str | None, /) -> None:
|
|
|
39
39
|
stubs_dir.mkdir(parents=True, exist_ok=True)
|
|
40
40
|
with TemporaryDirectory() as tmp_str:
|
|
41
41
|
tmp = Path(tmp_str)
|
|
42
|
-
run_command(WRANGLER_COMMAND + args + [tmp / "worker-configuration.d.ts"])
|
|
42
|
+
run_command(WRANGLER_COMMAND + args + [str(tmp / "worker-configuration.d.ts")])
|
|
43
43
|
(tmp / "tsconfig.json").write_text(TSCONFIG)
|
|
44
44
|
(tmp / "package.json").write_text(PACKAGE_JSON)
|
|
45
|
-
run_command(["npm", "-C", tmp, "install"])
|
|
46
|
-
run_command(
|
|
45
|
+
run_command(["npm", "-C", str(tmp), "install"])
|
|
46
|
+
run_command(
|
|
47
|
+
["npx", "@pyodide/ts-to-python", str(tmp), str(stubs_dir / "__init__.pyi")]
|
|
48
|
+
)
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
2
4
|
import re
|
|
5
|
+
import shutil
|
|
3
6
|
import subprocess
|
|
7
|
+
import sys
|
|
4
8
|
import tomllib
|
|
9
|
+
from collections.abc import Mapping
|
|
5
10
|
from datetime import datetime
|
|
6
11
|
from functools import cache
|
|
7
12
|
from pathlib import Path
|
|
@@ -20,12 +25,47 @@ WRANGLER_CREATE_COMMAND = ["npx", "--yes", "create-cloudflare"]
|
|
|
20
25
|
|
|
21
26
|
logger = logging.getLogger(__name__)
|
|
22
27
|
|
|
23
|
-
SUCCESS_LEVEL =
|
|
24
|
-
RUNNING_LEVEL =
|
|
25
|
-
OUTPUT_LEVEL =
|
|
28
|
+
SUCCESS_LEVEL = logging.CRITICAL + 50
|
|
29
|
+
RUNNING_LEVEL = logging.DEBUG + 5
|
|
30
|
+
OUTPUT_LEVEL = logging.DEBUG + 6
|
|
26
31
|
|
|
32
|
+
# Valid log levels for PYWRANGLER_LOG environment variable
|
|
33
|
+
_LOG_LEVEL_MAP = {
|
|
34
|
+
"debug": logging.DEBUG,
|
|
35
|
+
"info": logging.INFO,
|
|
36
|
+
"warning": logging.WARNING,
|
|
37
|
+
"warn": logging.WARNING, # alias
|
|
38
|
+
"error": logging.ERROR,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def setup_logging() -> int:
|
|
43
|
+
"""
|
|
44
|
+
Configure logging with Rich handler.
|
|
45
|
+
|
|
46
|
+
Reads PYWRANGLER_LOG environment variable to set log level.
|
|
47
|
+
Valid values: debug, info, warning, warn, error (case-insensitive).
|
|
48
|
+
Defaults to INFO if not set or invalid.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
The configured logging level (e.g., logging.DEBUG, logging.INFO).
|
|
52
|
+
"""
|
|
53
|
+
# Determine log level from environment variable
|
|
54
|
+
env_level = os.environ.get("PYWRANGLER_LOG", "").lower().strip()
|
|
55
|
+
if env_level and env_level not in _LOG_LEVEL_MAP:
|
|
56
|
+
# Print warning to stderr for invalid value (before logging is configured)
|
|
57
|
+
print(
|
|
58
|
+
f"Warning: Invalid PYWRANGLER_LOG value '{env_level}'. "
|
|
59
|
+
f"Valid values: {', '.join(sorted(set(_LOG_LEVEL_MAP.keys())))}. "
|
|
60
|
+
"Defaulting to 'info'.",
|
|
61
|
+
file=sys.stderr,
|
|
62
|
+
)
|
|
63
|
+
log_level = logging.INFO
|
|
64
|
+
elif env_level:
|
|
65
|
+
log_level = _LOG_LEVEL_MAP[env_level]
|
|
66
|
+
else:
|
|
67
|
+
log_level = logging.INFO
|
|
27
68
|
|
|
28
|
-
def setup_logging() -> None:
|
|
29
69
|
console = Console(
|
|
30
70
|
theme=Theme(
|
|
31
71
|
{
|
|
@@ -39,7 +79,7 @@ def setup_logging() -> None:
|
|
|
39
79
|
|
|
40
80
|
# Configure Rich logger
|
|
41
81
|
logging.basicConfig(
|
|
42
|
-
level=
|
|
82
|
+
level=log_level,
|
|
43
83
|
format="%(message)s",
|
|
44
84
|
force=True, # Ensure this configuration is applied
|
|
45
85
|
handlers=[
|
|
@@ -52,15 +92,37 @@ def setup_logging() -> None:
|
|
|
52
92
|
logging.addLevelName(RUNNING_LEVEL, "RUNNING")
|
|
53
93
|
logging.addLevelName(OUTPUT_LEVEL, "OUTPUT")
|
|
54
94
|
|
|
95
|
+
return log_level
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _get_pywrangler_version() -> str:
|
|
99
|
+
"""Get the version of pywrangler."""
|
|
100
|
+
try:
|
|
101
|
+
from importlib.metadata import version
|
|
102
|
+
|
|
103
|
+
return version("workers-py")
|
|
104
|
+
except Exception:
|
|
105
|
+
return "unknown"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def log_startup_info() -> None:
|
|
109
|
+
"""
|
|
110
|
+
Log startup information for debugging.
|
|
111
|
+
"""
|
|
112
|
+
logger.debug(f"pywrangler version: {_get_pywrangler_version()}")
|
|
113
|
+
logger.debug(f"Python: {platform.python_version()}")
|
|
114
|
+
logger.debug(f"Platform: {sys.platform}")
|
|
115
|
+
logger.debug(f"Working directory: {Path.cwd()}")
|
|
116
|
+
|
|
55
117
|
|
|
56
118
|
def write_success(msg: str) -> None:
|
|
57
119
|
logging.log(SUCCESS_LEVEL, msg)
|
|
58
120
|
|
|
59
121
|
|
|
60
122
|
def run_command(
|
|
61
|
-
command: list[str
|
|
123
|
+
command: list[str],
|
|
62
124
|
cwd: Path | None = None,
|
|
63
|
-
env:
|
|
125
|
+
env: Mapping[str, str | Path] | None = None,
|
|
64
126
|
check: bool = True,
|
|
65
127
|
capture_output: bool = False,
|
|
66
128
|
) -> subprocess.CompletedProcess[str]:
|
|
@@ -78,21 +140,36 @@ def run_command(
|
|
|
78
140
|
A subprocess.CompletedProcess instance.
|
|
79
141
|
"""
|
|
80
142
|
logger.log(RUNNING_LEVEL, f"{' '.join(str(arg) for arg in command)}")
|
|
143
|
+
|
|
144
|
+
# Some tools like `npm` may be a batch file on Windows (npm.cmd), and calling them only by
|
|
145
|
+
# name may fails in subprocess.run. Use shutil.which to find the real name.
|
|
146
|
+
abspath = shutil.which(command[0])
|
|
147
|
+
if not abspath:
|
|
148
|
+
logger.error(f"Command not found: {command[0]}. Is it installed and in PATH?")
|
|
149
|
+
raise click.exceptions.Exit(code=1)
|
|
150
|
+
|
|
151
|
+
realname = str(Path(command[0]).with_name(Path(abspath).name))
|
|
152
|
+
command = [realname] + command[1:]
|
|
81
153
|
try:
|
|
154
|
+
kwargs = {}
|
|
155
|
+
if capture_output:
|
|
156
|
+
kwargs = dict(stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
|
157
|
+
|
|
82
158
|
process = subprocess.run(
|
|
83
159
|
command,
|
|
84
160
|
cwd=cwd,
|
|
85
161
|
env=env,
|
|
86
162
|
check=check,
|
|
87
|
-
capture_output=capture_output,
|
|
88
163
|
text=True,
|
|
89
|
-
|
|
164
|
+
encoding="utf-8",
|
|
165
|
+
**kwargs,
|
|
166
|
+
) # type: ignore[call-overload]
|
|
90
167
|
if process.stdout and not capture_output:
|
|
91
168
|
logger.log(OUTPUT_LEVEL, f"{process.stdout.strip()}")
|
|
92
|
-
return process
|
|
169
|
+
return process # type: ignore[no-any-return]
|
|
93
170
|
except subprocess.CalledProcessError as e:
|
|
94
171
|
logger.error(
|
|
95
|
-
f"Error running command: {' '.join(str(arg) for arg in command)}\nExit code: {e.returncode}\nOutput:\n{e.stdout.strip() if e.stdout else ''}
|
|
172
|
+
f"Error running command: {' '.join(str(arg) for arg in command)}\nExit code: {e.returncode}\nOutput:\n{e.stdout.strip() if e.stdout else ''}"
|
|
96
173
|
)
|
|
97
174
|
raise click.exceptions.Exit(code=e.returncode) from None
|
|
98
175
|
except FileNotFoundError:
|
|
@@ -51,7 +51,11 @@ def test_dir(monkeypatch):
|
|
|
51
51
|
monkeypatch.setattr(
|
|
52
52
|
pywrangler_utils, "find_pyproject_toml", lambda: test_dir / "pyproject.toml"
|
|
53
53
|
)
|
|
54
|
-
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
yield test_dir.absolute()
|
|
57
|
+
finally:
|
|
58
|
+
shutil.rmtree(test_dir, ignore_errors=True)
|
|
55
59
|
|
|
56
60
|
|
|
57
61
|
def create_test_pyproject(test_dir: Path, dependencies=None):
|
|
@@ -410,7 +414,12 @@ def test_proxy_to_wrangler_handles_subprocess_error(mock_subprocess_run):
|
|
|
410
414
|
|
|
411
415
|
# Verify the error was attempted to be called
|
|
412
416
|
mock_subprocess_run.assert_called_once_with(
|
|
413
|
-
["npx", "--yes", "wrangler", "unknown_command"],
|
|
417
|
+
["npx", "--yes", "wrangler", "unknown_command"],
|
|
418
|
+
check=False,
|
|
419
|
+
cwd=Path("."),
|
|
420
|
+
env=None,
|
|
421
|
+
text=True,
|
|
422
|
+
encoding="utf-8",
|
|
414
423
|
)
|
|
415
424
|
|
|
416
425
|
|
|
@@ -542,3 +551,114 @@ def test_check_wrangler_version_insufficient(mock_run_command):
|
|
|
542
551
|
|
|
543
552
|
with pytest.raises(click.exceptions.Exit):
|
|
544
553
|
check_wrangler_version()
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
# Tests for PYWRANGLER_LOG environment variable
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def test_env_var_debug_level(test_dir, monkeypatch, caplog):
|
|
560
|
+
monkeypatch.setenv("PYWRANGLER_LOG", "debug")
|
|
561
|
+
create_test_pyproject(test_dir)
|
|
562
|
+
create_test_wrangler_jsonc(test_dir)
|
|
563
|
+
|
|
564
|
+
# Need to reimport to pick up env var change
|
|
565
|
+
import importlib
|
|
566
|
+
|
|
567
|
+
import pywrangler.utils
|
|
568
|
+
|
|
569
|
+
importlib.reload(pywrangler.utils)
|
|
570
|
+
|
|
571
|
+
from pywrangler.utils import setup_logging
|
|
572
|
+
|
|
573
|
+
level = setup_logging()
|
|
574
|
+
assert level == logging.DEBUG
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def test_env_var_error_level(test_dir, monkeypatch):
|
|
578
|
+
"""Test that PYWRANGLER_LOG=error sets ERROR level."""
|
|
579
|
+
monkeypatch.setenv("PYWRANGLER_LOG", "error")
|
|
580
|
+
|
|
581
|
+
import importlib
|
|
582
|
+
|
|
583
|
+
import pywrangler.utils
|
|
584
|
+
|
|
585
|
+
importlib.reload(pywrangler.utils)
|
|
586
|
+
|
|
587
|
+
from pywrangler.utils import setup_logging
|
|
588
|
+
|
|
589
|
+
level = setup_logging()
|
|
590
|
+
assert level == logging.ERROR
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def test_env_var_case_insensitive(test_dir, monkeypatch):
|
|
594
|
+
"""Test that PYWRANGLER_LOG is case-insensitive."""
|
|
595
|
+
monkeypatch.setenv("PYWRANGLER_LOG", "DEBUG")
|
|
596
|
+
|
|
597
|
+
import importlib
|
|
598
|
+
|
|
599
|
+
import pywrangler.utils
|
|
600
|
+
|
|
601
|
+
importlib.reload(pywrangler.utils)
|
|
602
|
+
|
|
603
|
+
from pywrangler.utils import setup_logging
|
|
604
|
+
|
|
605
|
+
level = setup_logging()
|
|
606
|
+
assert level == logging.DEBUG
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def test_debug_flag_overrides_env(test_dir, monkeypatch, caplog):
|
|
610
|
+
"""Test that --debug flag overrides PYWRANGLER_LOG=error."""
|
|
611
|
+
monkeypatch.setenv("PYWRANGLER_LOG", "error")
|
|
612
|
+
create_test_pyproject(test_dir)
|
|
613
|
+
create_test_wrangler_jsonc(test_dir)
|
|
614
|
+
|
|
615
|
+
runner = CliRunner()
|
|
616
|
+
runner.invoke(app, ["--debug", "sync"])
|
|
617
|
+
|
|
618
|
+
debug_logs = [
|
|
619
|
+
record for record in caplog.records if record.levelno == logging.DEBUG
|
|
620
|
+
]
|
|
621
|
+
assert len(debug_logs) > 0, "--debug flag should override PYWRANGLER_LOG=error"
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def test_env_var_invalid(test_dir, monkeypatch, capsys):
|
|
625
|
+
"""Test that invalid PYWRANGLER_LOG value produces warning."""
|
|
626
|
+
monkeypatch.setenv("PYWRANGLER_LOG", "invalid_value")
|
|
627
|
+
|
|
628
|
+
import importlib
|
|
629
|
+
|
|
630
|
+
import pywrangler.utils
|
|
631
|
+
|
|
632
|
+
importlib.reload(pywrangler.utils)
|
|
633
|
+
|
|
634
|
+
from pywrangler.utils import setup_logging
|
|
635
|
+
|
|
636
|
+
level = setup_logging()
|
|
637
|
+
|
|
638
|
+
captured = capsys.readouterr()
|
|
639
|
+
assert "Warning" in captured.err
|
|
640
|
+
assert "invalid_value" in captured.err
|
|
641
|
+
assert level == logging.INFO
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def test_startup_banner(test_dir, monkeypatch):
|
|
645
|
+
"""Test that debug output contains version, platform, and working directory."""
|
|
646
|
+
monkeypatch.setenv("PYWRANGLER_LOG", "debug")
|
|
647
|
+
create_test_pyproject(test_dir)
|
|
648
|
+
create_test_wrangler_jsonc(test_dir)
|
|
649
|
+
|
|
650
|
+
import importlib
|
|
651
|
+
|
|
652
|
+
import pywrangler.utils
|
|
653
|
+
|
|
654
|
+
importlib.reload(pywrangler.utils)
|
|
655
|
+
|
|
656
|
+
from pywrangler.utils import _get_pywrangler_version, log_startup_info
|
|
657
|
+
|
|
658
|
+
# Verify the functions exist and return expected content
|
|
659
|
+
version = _get_pywrangler_version()
|
|
660
|
+
assert version is not None
|
|
661
|
+
|
|
662
|
+
# Verify log_startup_info can be called without error
|
|
663
|
+
# The actual logging is tested via integration in test_debug_flag
|
|
664
|
+
log_startup_info()
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
from contextlib import chdir
|
|
2
3
|
from subprocess import run
|
|
3
4
|
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
4
7
|
# Import the full module so we can patch constants
|
|
5
8
|
from pywrangler.types import wrangler_types
|
|
6
9
|
|
|
@@ -42,7 +45,9 @@ class Default(WorkerEntrypoint):
|
|
|
42
45
|
"""
|
|
43
46
|
|
|
44
47
|
|
|
48
|
+
@pytest.mark.skipif(sys.version_info < (3, 13), reason="We create Python 3.13+ syntax")
|
|
45
49
|
def test_types(tmp_path):
|
|
50
|
+
"""Test that types are correctly revealed in a worker."""
|
|
46
51
|
config_path = tmp_path / "wrangler.toml"
|
|
47
52
|
pyproject_path = tmp_path / "pyproject.toml"
|
|
48
53
|
worker_dir = tmp_path / "src/worker"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|