funcd-bundle 0.3.2__tar.gz → 0.3.4__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.
- {funcd_bundle-0.3.2 → funcd_bundle-0.3.4}/PKG-INFO +1 -1
- {funcd_bundle-0.3.2 → funcd_bundle-0.3.4}/pyproject.toml +1 -1
- {funcd_bundle-0.3.2 → funcd_bundle-0.3.4}/src/funcd_bundle/bundle.py +33 -10
- {funcd_bundle-0.3.2 → funcd_bundle-0.3.4}/tests/test_bundle.py +70 -0
- {funcd_bundle-0.3.2 → funcd_bundle-0.3.4}/.gitignore +0 -0
- {funcd_bundle-0.3.2 → funcd_bundle-0.3.4}/README.md +0 -0
- {funcd_bundle-0.3.2 → funcd_bundle-0.3.4}/src/funcd_bundle/__init__.py +0 -0
- {funcd_bundle-0.3.2 → funcd_bundle-0.3.4}/src/funcd_bundle/cli.py +0 -0
- {funcd_bundle-0.3.2 → funcd_bundle-0.3.4}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: funcd-bundle
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
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.
|
|
4
|
+
version = "0.3.4"
|
|
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"}
|
|
@@ -86,7 +84,10 @@ def discover(project: Path) -> dict[str, Function]:
|
|
|
86
84
|
defaults = {project.resolve().name: DEFAULT_HANDLER}
|
|
87
85
|
functions = {}
|
|
88
86
|
for name, manifest in manifests.items():
|
|
89
|
-
|
|
87
|
+
try:
|
|
88
|
+
data = yaml.safe_load(manifest.read_text()) or {}
|
|
89
|
+
except yaml.YAMLError as err:
|
|
90
|
+
raise BundleError(f"{manifest}: {err}") from err
|
|
90
91
|
if not isinstance(data, dict):
|
|
91
92
|
raise BundleError(f"{manifest} is not a mapping")
|
|
92
93
|
main = data.get("main") or defaults[name]
|
|
@@ -331,16 +332,38 @@ def _copy_handler(project: Path, fn: Function, out: Path) -> list[Path]:
|
|
|
331
332
|
sources = [p for p in source.rglob("*.py") if not SKIPPED.intersection(p.relative_to(source).parts)]
|
|
332
333
|
# inside the bundle the handler sits at the root, so `main` must name it there (funcdctl dev reads it); a
|
|
333
334
|
# manifest without one gets one, else the bundle's generic manifest would default to handler.py
|
|
334
|
-
|
|
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)
|
|
335
|
+
(out / GENERIC_MANIFEST).write_text(_with_main(fn.manifest.read_text(), fn.handler.name))
|
|
341
336
|
return sources
|
|
342
337
|
|
|
343
338
|
|
|
339
|
+
def _with_main(text: str, handler: str) -> str:
|
|
340
|
+
"""The manifest with its top-level `main` set to handler. The YAML parser finds the key, however it is
|
|
341
|
+
written; the rest stays as written, since a PyYAML dump would re-resolve YAML 1.1 scalars funcd reads as
|
|
342
|
+
YAML 1.2 text."""
|
|
343
|
+
entry = f"main: {handler}"
|
|
344
|
+
root = yaml.compose(text, Loader=yaml.SafeLoader)
|
|
345
|
+
if isinstance(root, yaml.MappingNode):
|
|
346
|
+
for key, value in root.value:
|
|
347
|
+
if isinstance(key, yaml.ScalarNode) and key.value == "main":
|
|
348
|
+
start, end = key.start_mark.index, value.end_mark.index
|
|
349
|
+
# a block scalar's span ends past its line break
|
|
350
|
+
if text[start:end].endswith("\n"):
|
|
351
|
+
entry += "\n"
|
|
352
|
+
return text[:start] + entry + text[end:]
|
|
353
|
+
if root.flow_style:
|
|
354
|
+
# a block line after a flow mapping is a second root: the key goes inside, after its brace
|
|
355
|
+
brace = next(
|
|
356
|
+
t
|
|
357
|
+
for t in yaml.scan(text, Loader=yaml.SafeLoader)
|
|
358
|
+
if isinstance(t, yaml.FlowMappingStartToken)
|
|
359
|
+
)
|
|
360
|
+
at = brace.end_mark.index
|
|
361
|
+
return text[:at] + entry + (", " if root.value else "") + text[at:]
|
|
362
|
+
if text and not text.endswith("\n"):
|
|
363
|
+
text += "\n"
|
|
364
|
+
return text + entry + "\n"
|
|
365
|
+
|
|
366
|
+
|
|
344
367
|
def _top_level_modules(out: Path) -> set[str]:
|
|
345
368
|
"""The importable top-level names of the vendored distributions, from each RECORD."""
|
|
346
369
|
names: set[str] = set()
|
|
@@ -16,6 +16,7 @@ from collections.abc import Iterator
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
|
|
18
18
|
import pytest
|
|
19
|
+
import yaml
|
|
19
20
|
|
|
20
21
|
from funcd_bundle import BundleError, bundle, discover, host_platform
|
|
21
22
|
from funcd_bundle.bundle import _check
|
|
@@ -275,7 +276,76 @@ def test_issue_154_stem_manifest_without_main_names_bundled_handler(tmp_path: Pa
|
|
|
275
276
|
assert (out / "funcdctl.yaml").read_text() == manifest + "main: extract.py\n"
|
|
276
277
|
|
|
277
278
|
|
|
279
|
+
@pytest.mark.parametrize(
|
|
280
|
+
"main",
|
|
281
|
+
[
|
|
282
|
+
'"main": src/handler.py\n',
|
|
283
|
+
"'main': src/handler.py\n",
|
|
284
|
+
"main:\n src/handler.py\n",
|
|
285
|
+
],
|
|
286
|
+
ids=["double-quoted-key", "single-quoted-key", "value-on-next-line"],
|
|
287
|
+
)
|
|
288
|
+
def test_issue_r29_main_key_is_rewritten_not_duplicated(tmp_path: Path, main: str) -> None:
|
|
289
|
+
project = tmp_path / "reader"
|
|
290
|
+
_write(
|
|
291
|
+
project,
|
|
292
|
+
{
|
|
293
|
+
"pyproject.toml": '[project]\nname = "reader"\nversion = "0.1.0"\nrequires-python = ">=3.12"\n',
|
|
294
|
+
"funcdctl.yaml": "runtime: python314\n" + main + "handler: handle\n",
|
|
295
|
+
"src/handler.py": "def handle(ctx, event): ...\n",
|
|
296
|
+
},
|
|
297
|
+
)
|
|
298
|
+
_uv("lock", "--offline", "--python", "3.14", cwd=project)
|
|
299
|
+
out = bundle(project, "reader", host_platform(), tmp_path / "out", hermetic=False, check=False)
|
|
300
|
+
|
|
301
|
+
assert (out / "funcdctl.yaml").read_text() == "runtime: python314\nmain: handler.py\nhandler: handle\n", (
|
|
302
|
+
"the manifest's one main names the bundled handler"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@pytest.mark.parametrize(
|
|
307
|
+
("manifest", "want"),
|
|
308
|
+
[
|
|
309
|
+
("{runtime: python314, handler: handle}\n", {"runtime": "python314", "handler": "handle"}),
|
|
310
|
+
("!!map &m {\n runtime: python314,\n}\n", {"runtime": "python314"}),
|
|
311
|
+
("{}\n", {}),
|
|
312
|
+
],
|
|
313
|
+
ids=["one-line", "tagged-multi-line", "empty"],
|
|
314
|
+
)
|
|
315
|
+
def test_issue_r49_flow_mapping_manifest_gets_main(
|
|
316
|
+
tmp_path: Path, manifest: str, want: dict[str, str]
|
|
317
|
+
) -> None:
|
|
318
|
+
project = tmp_path / "reader"
|
|
319
|
+
_write(
|
|
320
|
+
project,
|
|
321
|
+
{
|
|
322
|
+
"pyproject.toml": '[project]\nname = "reader"\nversion = "0.1.0"\nrequires-python = ">=3.12"\n',
|
|
323
|
+
"funcdctl.yaml": manifest,
|
|
324
|
+
"handler.py": "def handle(ctx, event): ...\n",
|
|
325
|
+
},
|
|
326
|
+
)
|
|
327
|
+
_uv("lock", "--offline", "--python", "3.14", cwd=project)
|
|
328
|
+
out = bundle(project, "reader", host_platform(), tmp_path / "out", hermetic=False, check=False)
|
|
329
|
+
|
|
330
|
+
assert yaml.safe_load((out / "funcdctl.yaml").read_text()) == want | {"main": "handler.py"}, (
|
|
331
|
+
"the bundle's manifest is valid YAML whose main names the bundled handler"
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
278
335
|
def test_missing_handler_is_named(tmp_path: Path) -> None:
|
|
279
336
|
_write(tmp_path, {"funcdctl.yaml": "runtime: python314\nmain: src/nope.py\n"})
|
|
280
337
|
with pytest.raises(BundleError, match="nope.py"):
|
|
281
338
|
bundle(tmp_path, tmp_path.name, host_platform(), tmp_path / "out", hermetic=False, check=False)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def test_issue_r48_invalid_manifest_yaml_is_reported(
|
|
342
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
343
|
+
) -> None:
|
|
344
|
+
_write(tmp_path, {"funcdctl.yaml": "runtime: [\n", "handler.py": ""})
|
|
345
|
+
monkeypatch.chdir(tmp_path)
|
|
346
|
+
|
|
347
|
+
assert main(["--no-check", "--out", str(tmp_path / "out")]) == 1, "a manifest that does not parse fails"
|
|
348
|
+
err = capsys.readouterr().err
|
|
349
|
+
assert err.startswith(f"funcd-bundle: {tmp_path / 'funcdctl.yaml'}: "), err
|
|
350
|
+
assert "while parsing a flow node" in err, "the parser message names the problem"
|
|
351
|
+
assert "Traceback" not in err
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|