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.
Files changed (26) hide show
  1. {workers_py-1.6.2 → workers_py-1.7.1}/.github/workflows/tests.yml +1 -1
  2. {workers_py-1.6.2 → workers_py-1.7.1}/.gitignore +2 -0
  3. {workers_py-1.6.2 → workers_py-1.7.1}/.pre-commit-config.yaml +0 -1
  4. {workers_py-1.6.2 → workers_py-1.7.1}/CHANGELOG.md +25 -0
  5. {workers_py-1.6.2 → workers_py-1.7.1}/PKG-INFO +1 -1
  6. {workers_py-1.6.2 → workers_py-1.7.1}/pyproject.toml +1 -1
  7. {workers_py-1.6.2 → workers_py-1.7.1}/src/pywrangler/cli.py +10 -2
  8. {workers_py-1.6.2 → workers_py-1.7.1}/src/pywrangler/sync.py +70 -8
  9. {workers_py-1.6.2 → workers_py-1.7.1}/src/pywrangler/types.py +5 -3
  10. {workers_py-1.6.2 → workers_py-1.7.1}/src/pywrangler/utils.py +88 -11
  11. {workers_py-1.6.2 → workers_py-1.7.1}/tests/test_cli.py +122 -2
  12. {workers_py-1.6.2 → workers_py-1.7.1}/tests/test_types.py +5 -0
  13. {workers_py-1.6.2 → workers_py-1.7.1}/uv.lock +1 -1
  14. {workers_py-1.6.2 → workers_py-1.7.1}/.gitattributes +0 -0
  15. {workers_py-1.6.2 → workers_py-1.7.1}/.github/workflows/commitlint.yml +0 -0
  16. {workers_py-1.6.2 → workers_py-1.7.1}/.github/workflows/lint.yml +0 -0
  17. {workers_py-1.6.2 → workers_py-1.7.1}/.github/workflows/release.yml +0 -0
  18. {workers_py-1.6.2 → workers_py-1.7.1}/CLAUDE.md +0 -0
  19. {workers_py-1.6.2 → workers_py-1.7.1}/CONTRIBUTING.md +0 -0
  20. {workers_py-1.6.2 → workers_py-1.7.1}/README.md +0 -0
  21. {workers_py-1.6.2 → workers_py-1.7.1}/src/pywrangler/__init__.py +0 -0
  22. {workers_py-1.6.2 → workers_py-1.7.1}/src/pywrangler/__main__.py +0 -0
  23. {workers_py-1.6.2 → workers_py-1.7.1}/src/pywrangler/metadata.py +0 -0
  24. {workers_py-1.6.2 → workers_py-1.7.1}/src/pywrangler/py.typed +0 -0
  25. {workers_py-1.6.2 → workers_py-1.7.1}/tests/test_py_version_detect.py +0 -0
  26. {workers_py-1.6.2 → workers_py-1.7.1}/workers.py +0 -0
@@ -11,7 +11,7 @@ jobs:
11
11
  strategy:
12
12
  matrix:
13
13
  os: [ubuntu-latest, macos-latest]
14
- python-version: ['3.12']
14
+ python-version: ['3.12', '3.13']
15
15
  runs-on: ${{ matrix.os }}
16
16
 
17
17
  steps:
@@ -62,3 +62,5 @@ docs/_build/
62
62
  # OS specific
63
63
  .DS_Store
64
64
  Thumbs.db
65
+
66
+ tests/test_workspace
@@ -1,4 +1,3 @@
1
- exclude: (^.*patches|.*\.cgi$|^packages/micropip/src/micropip/externals|^benchmark/benchmarks$)
2
1
  default_language_version:
3
2
  python: "3.13"
4
3
  repos:
@@ -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.6.2
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "workers-py"
7
- version = "1.6.2"
7
+ version = "1.7.1"
8
8
  description = "A set of libraries and tools for Python Workers"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -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 = subprocess.run(command_to_run, check=False, cwd=".")
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 = subprocess.run(command_to_run, check=False, cwd=".")
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
- env=os.environ | {"VIRTUAL_ENV": get_pyodide_venv_path()},
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
- shutil.copytree(
198
- get_pyodide_venv_path() / f"lib/python{pyv}/site-packages", vendor_path
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
- env=os.environ | {"VIRTUAL_ENV": venv_workers_path},
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
- _install_requirements_to_vendor(requirements)
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(["npx", "@pyodide/ts-to-python", tmp, stubs_dir / "__init__.pyi"])
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 = 100
24
- RUNNING_LEVEL = 15
25
- OUTPUT_LEVEL = 16
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=logging.INFO,
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 | Path],
123
+ command: list[str],
62
124
  cwd: Path | None = None,
63
- env: dict[str, str | Path] | None = None,
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 ''}{e.stderr.strip() if e.stderr 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
- yield test_dir.absolute()
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"], check=False, cwd="."
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"
@@ -500,7 +500,7 @@ wheels = [
500
500
 
501
501
  [[package]]
502
502
  name = "workers-py"
503
- version = "1.6.2"
503
+ version = "1.7.1"
504
504
  source = { editable = "." }
505
505
  dependencies = [
506
506
  { name = "click" },
File without changes
File without changes
File without changes
File without changes
File without changes