funcd-bundle 0.3.2__tar.gz → 0.3.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: funcd-bundle
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Bundle a Python funcd function and its locked dependencies for the funcd runtime
5
5
  Project-URL: Homepage, https://github.com/pyvvo/funcd-python
6
6
  Project-URL: Repository, https://github.com/pyvvo/funcd-python
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "funcd-bundle"
3
3
  # Not bumped per release: the vX.Y.Z git tag is the version, stamped into the build at publish time.
4
- version = "0.3.2"
4
+ version = "0.3.3"
5
5
  description = "Bundle a Python funcd function and its locked dependencies for the funcd runtime"
6
6
  authors = [{ name = "The funcd Authors" }]
7
7
  readme = "README.md"
@@ -16,7 +16,6 @@ import csv
16
16
  import io
17
17
  import os
18
18
  import platform as host
19
- import re
20
19
  import shlex
21
20
  import shutil
22
21
  import subprocess
@@ -38,7 +37,6 @@ DEFAULT_HANDLER = "handler.py"
38
37
  RUNTIME_PACKAGE = "funcd-shim"
39
38
  PLATFORMS = {"linux/amd64": "x86_64", "linux/arm64": "aarch64"}
40
39
  SKIPPED = {"__pycache__", "tests"}
41
- MAIN_LINE = re.compile(r"^main:.*$", re.MULTILINE)
42
40
  # never streamed into a container: caches, virtualenvs and build outputs
43
41
  NOT_COPIED = frozenset(
44
42
  {".venv", ".git", "node_modules", "__pycache__", "dist", ".mypy_cache", ".pytest_cache", ".ruff_cache"}
@@ -331,16 +329,29 @@ def _copy_handler(project: Path, fn: Function, out: Path) -> list[Path]:
331
329
  sources = [p for p in source.rglob("*.py") if not SKIPPED.intersection(p.relative_to(source).parts)]
332
330
  # inside the bundle the handler sits at the root, so `main` must name it there (funcdctl dev reads it); a
333
331
  # manifest without one gets one, else the bundle's generic manifest would default to handler.py
334
- main = f"main: {fn.handler.name}"
335
- text, found = MAIN_LINE.subn(main, fn.manifest.read_text())
336
- if not found:
337
- if text and not text.endswith("\n"):
338
- text += "\n"
339
- text += main + "\n"
340
- (out / GENERIC_MANIFEST).write_text(text)
332
+ (out / GENERIC_MANIFEST).write_text(_with_main(fn.manifest.read_text(), fn.handler.name))
341
333
  return sources
342
334
 
343
335
 
336
+ def _with_main(text: str, handler: str) -> str:
337
+ """The manifest with its top-level `main` set to handler. The YAML parser finds the key, however it is
338
+ written; the rest stays as written, since a PyYAML dump would re-resolve YAML 1.1 scalars funcd reads as
339
+ YAML 1.2 text."""
340
+ entry = f"main: {handler}"
341
+ root = yaml.compose(text, Loader=yaml.SafeLoader)
342
+ if isinstance(root, yaml.MappingNode):
343
+ for key, value in root.value:
344
+ if isinstance(key, yaml.ScalarNode) and key.value == "main":
345
+ start, end = key.start_mark.index, value.end_mark.index
346
+ # a block scalar's span ends past its line break
347
+ if text[start:end].endswith("\n"):
348
+ entry += "\n"
349
+ return text[:start] + entry + text[end:]
350
+ if text and not text.endswith("\n"):
351
+ text += "\n"
352
+ return text + entry + "\n"
353
+
354
+
344
355
  def _top_level_modules(out: Path) -> set[str]:
345
356
  """The importable top-level names of the vendored distributions, from each RECORD."""
346
357
  names: set[str] = set()
@@ -275,6 +275,33 @@ def test_issue_154_stem_manifest_without_main_names_bundled_handler(tmp_path: Pa
275
275
  assert (out / "funcdctl.yaml").read_text() == manifest + "main: extract.py\n"
276
276
 
277
277
 
278
+ @pytest.mark.parametrize(
279
+ "main",
280
+ [
281
+ '"main": src/handler.py\n',
282
+ "'main': src/handler.py\n",
283
+ "main:\n src/handler.py\n",
284
+ ],
285
+ ids=["double-quoted-key", "single-quoted-key", "value-on-next-line"],
286
+ )
287
+ def test_issue_r29_main_key_is_rewritten_not_duplicated(tmp_path: Path, main: str) -> None:
288
+ project = tmp_path / "reader"
289
+ _write(
290
+ project,
291
+ {
292
+ "pyproject.toml": '[project]\nname = "reader"\nversion = "0.1.0"\nrequires-python = ">=3.12"\n',
293
+ "funcdctl.yaml": "runtime: python314\n" + main + "handler: handle\n",
294
+ "src/handler.py": "def handle(ctx, event): ...\n",
295
+ },
296
+ )
297
+ _uv("lock", "--offline", "--python", "3.14", cwd=project)
298
+ out = bundle(project, "reader", host_platform(), tmp_path / "out", hermetic=False, check=False)
299
+
300
+ assert (out / "funcdctl.yaml").read_text() == "runtime: python314\nmain: handler.py\nhandler: handle\n", (
301
+ "the manifest's one main names the bundled handler"
302
+ )
303
+
304
+
278
305
  def test_missing_handler_is_named(tmp_path: Path) -> None:
279
306
  _write(tmp_path, {"funcdctl.yaml": "runtime: python314\nmain: src/nope.py\n"})
280
307
  with pytest.raises(BundleError, match="nope.py"):
File without changes
File without changes
File without changes