universal-wasm-loader 2.0.1__tar.gz → 2.0.2__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 (48) hide show
  1. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/PKG-INFO +1 -1
  2. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/pyproject.toml +1 -1
  3. universal_wasm_loader-2.0.2/tests/test_github_release.py +164 -0
  4. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/.gitignore +0 -0
  5. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/LICENSE +0 -0
  6. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/README.md +0 -0
  7. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/src/universal_wasm_loader/__init__.py +0 -0
  8. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/src/universal_wasm_loader/_abi.py +0 -0
  9. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/src/universal_wasm_loader/_loader.py +0 -0
  10. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/src/universal_wasm_loader/_wit_parser.py +0 -0
  11. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/__init__.py +0 -0
  12. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/conftest.py +0 -0
  13. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/fixtures/booleans_50.wasm +0 -0
  14. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/fixtures/booleans_50.wit +0 -0
  15. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/fixtures/fixtures.json +0 -0
  16. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/fixtures/imports_50.wasm +0 -0
  17. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/fixtures/imports_50.wit +0 -0
  18. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/fixtures/init_check.wasm +0 -0
  19. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/fixtures/init_check.wat +0 -0
  20. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/fixtures/math_50.wasm +0 -0
  21. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/fixtures/math_50.wit +0 -0
  22. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/fixtures/strings_50.wasm +0 -0
  23. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/fixtures/strings_50.wit +0 -0
  24. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/fixtures/wasi_io_50.wasm +0 -0
  25. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/fixtures/wasi_io_50.wit +0 -0
  26. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/py_fixtures/versioned.wat +0 -0
  27. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/test_fixtures.py +0 -0
  28. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/test_loader.py +0 -0
  29. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/test_pypi_files.py +0 -0
  30. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/test_refusals.py +0 -0
  31. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/test_release_script.py +0 -0
  32. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/tests/test_wit_parser.py +0 -0
  33. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/LICENSE +0 -0
  34. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/README.md +0 -0
  35. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/booleans_50.wasm +0 -0
  36. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/booleans_50.wit +0 -0
  37. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/fixtures.json +0 -0
  38. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/imports_50.wasm +0 -0
  39. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/imports_50.wit +0 -0
  40. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/init_check.wasm +0 -0
  41. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/init_check.wat +0 -0
  42. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/loader.test.js +0 -0
  43. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/math_50.wasm +0 -0
  44. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/math_50.wit +0 -0
  45. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/strings_50.wasm +0 -0
  46. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/strings_50.wit +0 -0
  47. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/wasi_io_50.wasm +0 -0
  48. {universal_wasm_loader-2.0.1 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/wasi_io_50.wit +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: universal-wasm-loader
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: Universal WASM loader for Python
5
5
  Project-URL: Homepage, https://github.com/synapticforge1/universalWasmLoader-py
6
6
  Project-URL: Repository, https://github.com/synapticforge1/universalWasmLoader-py
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "universal-wasm-loader"
7
- version = "2.0.1"
7
+ version = "2.0.2"
8
8
  description = "Universal WASM loader for Python"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -0,0 +1,164 @@
1
+ """scripts/github_release.py against an in-process mock of GitHub's REST API (no network)."""
2
+ from __future__ import annotations
3
+
4
+ import importlib.util
5
+ import json
6
+ import sys
7
+ import threading
8
+ from collections.abc import Iterator
9
+ from http.server import BaseHTTPRequestHandler, HTTPServer
10
+ from pathlib import Path
11
+ from types import ModuleType
12
+ from typing import Any
13
+
14
+ import pytest
15
+
16
+ SCRIPT = Path(__file__).parent.parent / "scripts" / "github_release.py"
17
+ REPO, TAG, SHA, OTHER = "o/r", "v2.0.2", "a" * 40, "b" * 40
18
+
19
+
20
+ def _load() -> ModuleType:
21
+ spec = importlib.util.spec_from_file_location("github_release", SCRIPT)
22
+ assert spec is not None and spec.loader is not None
23
+ mod = importlib.util.module_from_spec(spec)
24
+ sys.modules["github_release"] = mod
25
+ spec.loader.exec_module(mod)
26
+ return mod
27
+
28
+
29
+ gr = _load()
30
+
31
+
32
+ class Mock:
33
+ """GitHub state + a log of (method, path, auth, body)."""
34
+
35
+ def __init__(self) -> None:
36
+ self.tags: dict[str, dict[str, str]] = {} # tag -> ref object
37
+ self.tag_objects: dict[str, dict[str, str]] = {} # annotated tag sha -> object
38
+ self.releases: dict[str, dict[str, Any]] = {}
39
+ self.log: list[tuple[str, str, str | None, Any]] = []
40
+ self.post_status = 201
41
+
42
+ def writes(self) -> list[tuple[str, str, str | None, Any]]:
43
+ return [e for e in self.log if e[0] != "GET"]
44
+
45
+
46
+ @pytest.fixture
47
+ def mock() -> Iterator[tuple[Mock, str]]:
48
+ state = Mock()
49
+
50
+ class Handler(BaseHTTPRequestHandler):
51
+ def log_message(self, *_: Any) -> None:
52
+ pass
53
+
54
+ def _send(self, code: int, doc: Any = None) -> None:
55
+ raw = b"" if doc is None else json.dumps(doc).encode()
56
+ self.send_response(code)
57
+ self.send_header("Content-Type", "application/json")
58
+ self.send_header("Content-Length", str(len(raw)))
59
+ self.end_headers()
60
+ self.wfile.write(raw)
61
+
62
+ def do_GET(self) -> None: # noqa: N802
63
+ state.log.append(("GET", self.path, self.headers.get("Authorization"), None))
64
+ p = self.path
65
+ if p.startswith(f"/repos/{REPO}/git/ref/tags/"):
66
+ obj = state.tags.get(p.rsplit("/", 1)[1])
67
+ return self._send(200, {"object": obj}) if obj else self._send(404)
68
+ if p.startswith(f"/repos/{REPO}/git/tags/"):
69
+ obj = state.tag_objects.get(p.rsplit("/", 1)[1])
70
+ return self._send(200, {"object": obj}) if obj else self._send(404)
71
+ if p.startswith(f"/repos/{REPO}/releases/tags/"):
72
+ rel = state.releases.get(p.rsplit("/", 1)[1])
73
+ return self._send(200, rel) if rel else self._send(404)
74
+ self._send(404)
75
+
76
+ def do_POST(self) -> None: # noqa: N802
77
+ body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
78
+ state.log.append(("POST", self.path, self.headers.get("Authorization"), body))
79
+ if self.path != f"/repos/{REPO}/releases" or state.post_status != 201:
80
+ return self._send(state.post_status, {"message": "nope"})
81
+ rel = {"id": 7, "tag_name": body["tag_name"], "draft": False,
82
+ "html_url": f"https://example.invalid/{body['tag_name']}"}
83
+ state.releases[body["tag_name"]] = rel
84
+ self._send(201, rel)
85
+
86
+ server = HTTPServer(("127.0.0.1", 0), Handler)
87
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
88
+ thread.start()
89
+ try:
90
+ yield state, f"http://127.0.0.1:{server.server_port}"
91
+ finally:
92
+ server.shutdown()
93
+
94
+
95
+ def _run(base: str, token: str | None = "t0k", dry: bool = False, tag: str = TAG) -> int:
96
+ return int(gr.run(REPO, tag, SHA, dry, gr.Api(base, token)))
97
+
98
+
99
+ def test_creates_the_release_then_reads_it_back(mock: tuple[Mock, str]) -> None:
100
+ state, base = mock
101
+ state.tags[TAG] = {"type": "commit", "sha": SHA}
102
+ assert _run(base) == 0
103
+ (method, path, auth, body), = state.writes()
104
+ assert (method, path, auth) == ("POST", f"/repos/{REPO}/releases", "Bearer t0k")
105
+ assert body["tag_name"] == TAG and body["generate_release_notes"] is True
106
+ assert "target_commitish" not in body # the tag already exists; nothing to target
107
+ assert "pypi.org/project/universal-wasm-loader/2.0.2/" in body["body"]
108
+ assert state.log[-2][1].endswith(f"/releases/tags/{TAG}") # read back after the POST
109
+ assert state.log[-1][1].endswith(f"/git/ref/tags/{TAG}") # tag re-checked last
110
+
111
+
112
+ def test_rerun_is_safe_existing_release_means_done(mock: tuple[Mock, str]) -> None:
113
+ state, base = mock
114
+ state.tags[TAG] = {"type": "commit", "sha": SHA}
115
+ state.releases[TAG] = {"tag_name": TAG, "draft": False, "html_url": "x"}
116
+ assert _run(base) == 0
117
+ assert state.writes() == []
118
+
119
+
120
+ def test_annotated_tag_is_peeled(mock: tuple[Mock, str]) -> None:
121
+ state, base = mock
122
+ state.tags[TAG] = {"type": "tag", "sha": "c" * 40}
123
+ state.tag_objects["c" * 40] = {"type": "commit", "sha": SHA}
124
+ assert _run(base) == 0
125
+ assert len(state.writes()) == 1
126
+
127
+
128
+ @pytest.mark.parametrize("case", ["missing tag", "moved tag", "annotated tag moved"])
129
+ def test_refuses_with_no_write(mock: tuple[Mock, str], case: str) -> None:
130
+ state, base = mock
131
+ if case == "moved tag":
132
+ state.tags[TAG] = {"type": "commit", "sha": OTHER}
133
+ elif case == "annotated tag moved":
134
+ state.tags[TAG] = {"type": "tag", "sha": "c" * 40}
135
+ state.tag_objects["c" * 40] = {"type": "commit", "sha": OTHER}
136
+ assert _run(base) == 1
137
+ assert state.writes() == []
138
+
139
+
140
+ def test_dry_run_and_no_token_send_no_write(mock: tuple[Mock, str]) -> None:
141
+ state, base = mock
142
+ state.tags[TAG] = {"type": "commit", "sha": SHA}
143
+ assert _run(base, dry=True) == 0
144
+ assert _run(base, token=None) == 1
145
+ assert state.writes() == []
146
+
147
+
148
+ def test_a_failed_post_is_a_failure(
149
+ mock: tuple[Mock, str], monkeypatch: pytest.MonkeyPatch
150
+ ) -> None:
151
+ state, base = mock
152
+ state.tags[TAG] = {"type": "commit", "sha": SHA}
153
+ state.post_status = 422
154
+ with pytest.raises(gr.ApiError, match="HTTP 422"):
155
+ _run(base)
156
+ monkeypatch.setenv("GITHUB_API_URL", base) # main() must hit the mock, never GitHub
157
+ monkeypatch.setenv("GITHUB_TOKEN", "t0k")
158
+ assert gr.main(["--repo", REPO, "--tag", TAG, "--sha", SHA]) == 1 # ApiError -> exit 1
159
+
160
+
161
+ def test_malformed_tag_sends_no_request(mock: tuple[Mock, str]) -> None:
162
+ state, base = mock
163
+ assert _run(base, tag="main") == 1
164
+ assert state.log == []