universal-wasm-loader 2.0.0__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.0 → universal_wasm_loader-2.0.2}/PKG-INFO +1 -1
  2. {universal_wasm_loader-2.0.0 → 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.2/tests/test_pypi_files.py +115 -0
  5. universal_wasm_loader-2.0.2/tests/test_release_script.py +58 -0
  6. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/.gitignore +0 -0
  7. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/LICENSE +0 -0
  8. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/README.md +0 -0
  9. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/src/universal_wasm_loader/__init__.py +0 -0
  10. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/src/universal_wasm_loader/_abi.py +0 -0
  11. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/src/universal_wasm_loader/_loader.py +0 -0
  12. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/src/universal_wasm_loader/_wit_parser.py +0 -0
  13. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/tests/__init__.py +0 -0
  14. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/tests/conftest.py +0 -0
  15. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/tests/fixtures/booleans_50.wasm +0 -0
  16. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/tests/fixtures/booleans_50.wit +0 -0
  17. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/tests/fixtures/fixtures.json +0 -0
  18. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/tests/fixtures/imports_50.wasm +0 -0
  19. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/tests/fixtures/imports_50.wit +0 -0
  20. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/tests/fixtures/init_check.wasm +0 -0
  21. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/tests/fixtures/init_check.wat +0 -0
  22. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/tests/fixtures/math_50.wasm +0 -0
  23. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/tests/fixtures/math_50.wit +0 -0
  24. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/tests/fixtures/strings_50.wasm +0 -0
  25. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/tests/fixtures/strings_50.wit +0 -0
  26. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/tests/fixtures/wasi_io_50.wasm +0 -0
  27. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/tests/fixtures/wasi_io_50.wit +0 -0
  28. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/tests/py_fixtures/versioned.wat +0 -0
  29. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/tests/test_fixtures.py +0 -0
  30. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/tests/test_loader.py +0 -0
  31. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/tests/test_refusals.py +0 -0
  32. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/tests/test_wit_parser.py +0 -0
  33. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/LICENSE +0 -0
  34. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/README.md +0 -0
  35. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/booleans_50.wasm +0 -0
  36. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/booleans_50.wit +0 -0
  37. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/fixtures.json +0 -0
  38. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/imports_50.wasm +0 -0
  39. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/imports_50.wit +0 -0
  40. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/init_check.wasm +0 -0
  41. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/init_check.wat +0 -0
  42. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/loader.test.js +0 -0
  43. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/math_50.wasm +0 -0
  44. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/math_50.wit +0 -0
  45. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/strings_50.wasm +0 -0
  46. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/strings_50.wit +0 -0
  47. {universal_wasm_loader-2.0.0 → universal_wasm_loader-2.0.2}/upstream/universalwasmloader-js/tests/wasi_io_50.wasm +0 -0
  48. {universal_wasm_loader-2.0.0 → 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.0
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.0"
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 == []
@@ -0,0 +1,115 @@
1
+ """scripts/pypi_files.py: which dist files PyPI still needs (no network: urlopen is stubbed)."""
2
+ from __future__ import annotations
3
+
4
+ import importlib.util
5
+ import io
6
+ import json
7
+ import sys
8
+ import urllib.error
9
+ from pathlib import Path
10
+ from types import ModuleType
11
+ from typing import Any
12
+
13
+ import pytest
14
+
15
+ SCRIPT = Path(__file__).parent.parent / "scripts" / "pypi_files.py"
16
+ PKG, VER = "universal-wasm-loader", "2.0.0"
17
+ WHL, SDIST = "universal_wasm_loader-2.0.0-py3-none-any.whl", "universal_wasm_loader-2.0.0.tar.gz"
18
+
19
+
20
+ def _load() -> ModuleType:
21
+ spec = importlib.util.spec_from_file_location("pypi_files", SCRIPT)
22
+ assert spec is not None and spec.loader is not None
23
+ mod = importlib.util.module_from_spec(spec)
24
+ sys.modules["pypi_files"] = mod
25
+ spec.loader.exec_module(mod)
26
+ return mod
27
+
28
+
29
+ pf = _load()
30
+
31
+
32
+ def test_nothing_on_pypi_uploads_everything() -> None:
33
+ upload, _ = pf.plan({WHL: "a", SDIST: "b"}, None)
34
+ assert sorted(upload) == sorted([SDIST, WHL])
35
+
36
+
37
+ def test_complete_on_pypi_uploads_nothing() -> None:
38
+ upload, notes = pf.plan({WHL: "a", SDIST: "b"}, {WHL: "a", SDIST: "b"})
39
+ assert upload == []
40
+ assert all("same bytes" in n for n in notes)
41
+
42
+
43
+ def test_half_published_uploads_only_the_missing_file() -> None:
44
+ # The wheel went up, the sdist upload failed: a re-run sends the sdist only.
45
+ upload, _ = pf.plan({WHL: "a", SDIST: "b"}, {WHL: "a"})
46
+ assert upload == [SDIST]
47
+
48
+
49
+ def test_rebuilt_file_with_other_bytes_is_skipped_not_reuploaded() -> None:
50
+ upload, notes = pf.plan({WHL: "rebuilt", SDIST: "b"}, {WHL: "first"})
51
+ assert upload == [SDIST]
52
+ assert any("DIFFERENT bytes" in n and WHL in n for n in notes)
53
+
54
+
55
+ def test_local_files_refuses_another_version(tmp_path: Path) -> None:
56
+ (tmp_path / WHL).write_bytes(b"w")
57
+ (tmp_path / "universal_wasm_loader-1.1.1.tar.gz").write_bytes(b"s")
58
+ with pytest.raises(ValueError, match="not a dist of universal-wasm-loader 2.0.0"):
59
+ pf.local_files(tmp_path, PKG, VER)
60
+
61
+
62
+ def test_local_files_refuses_an_empty_dist(tmp_path: Path) -> None:
63
+ with pytest.raises(ValueError, match="no dist files"):
64
+ pf.local_files(tmp_path, PKG, VER)
65
+
66
+
67
+ def test_local_files_hashes(tmp_path: Path) -> None:
68
+ (tmp_path / WHL).write_bytes(b"w")
69
+ (tmp_path / SDIST).write_bytes(b"s")
70
+ got = pf.local_files(tmp_path, PKG, VER)
71
+ assert set(got) == {WHL, SDIST}
72
+ assert len(got[WHL]) == 64
73
+
74
+
75
+ def _http_error(code: int) -> urllib.error.HTTPError:
76
+ return urllib.error.HTTPError("https://pypi.org/x", code, "x", {}, None) # type: ignore[arg-type]
77
+
78
+
79
+ def test_remote_404_means_no_files(monkeypatch: pytest.MonkeyPatch) -> None:
80
+ def fake(*_: Any, **__: Any) -> Any:
81
+ raise _http_error(404)
82
+
83
+ monkeypatch.setattr(pf.urllib.request, "urlopen", fake)
84
+ assert pf.remote_files(PKG, VER) is None
85
+
86
+
87
+ @pytest.mark.parametrize("code", [403, 500, 503])
88
+ def test_remote_other_status_is_an_error_never_not_published(
89
+ monkeypatch: pytest.MonkeyPatch, code: int
90
+ ) -> None:
91
+ def fake(*_: Any, **__: Any) -> Any:
92
+ raise _http_error(code)
93
+
94
+ monkeypatch.setattr(pf.urllib.request, "urlopen", fake)
95
+ with pytest.raises(pf.PyPIError, match=f"HTTP {code}"):
96
+ pf.remote_files(PKG, VER)
97
+
98
+
99
+ def test_remote_unreachable_is_an_error(monkeypatch: pytest.MonkeyPatch) -> None:
100
+ def fake(*_: Any, **__: Any) -> Any:
101
+ raise urllib.error.URLError("no route")
102
+
103
+ monkeypatch.setattr(pf.urllib.request, "urlopen", fake)
104
+ with pytest.raises(pf.PyPIError, match="could not reach PyPI"):
105
+ pf.remote_files(PKG, VER)
106
+
107
+
108
+ def test_remote_200_lists_files(monkeypatch: pytest.MonkeyPatch) -> None:
109
+ body = {"urls": [{"filename": WHL, "digests": {"sha256": "a"}}]}
110
+
111
+ def fake(*_: Any, **__: Any) -> Any:
112
+ return io.BytesIO(json.dumps(body).encode())
113
+
114
+ monkeypatch.setattr(pf.urllib.request, "urlopen", fake)
115
+ assert pf.remote_files(PKG, VER) == {WHL: "a"}
@@ -0,0 +1,58 @@
1
+ """scripts/release.py helpers: the ls-remote read-back and the Release-page owner item (H30)."""
2
+ from __future__ import annotations
3
+
4
+ import importlib.util
5
+ import sys
6
+ import urllib.parse
7
+ from pathlib import Path
8
+ from types import ModuleType
9
+
10
+ SCRIPTS = Path(__file__).parent.parent / "scripts"
11
+
12
+
13
+ def _load() -> ModuleType:
14
+ if str(SCRIPTS) not in sys.path:
15
+ sys.path.insert(0, str(SCRIPTS)) # release.py imports _common from its own folder
16
+ spec = importlib.util.spec_from_file_location("release_script", SCRIPTS / "release.py")
17
+ assert spec is not None and spec.loader is not None
18
+ mod = importlib.util.module_from_spec(spec)
19
+ spec.loader.exec_module(mod)
20
+ return mod
21
+
22
+
23
+ rel = _load()
24
+
25
+ LS_REMOTE = (
26
+ "21e680a433345a63e653fd920ad113332c8bf1d9\tHEAD\n"
27
+ "21e680a433345a63e653fd920ad113332c8bf1d9\trefs/heads/main\n"
28
+ "c41c8b9f0b112e41840f3c6841f80cdb9657d841\trefs/tags/v2.0.0\n"
29
+ "21e680a433345a63e653fd920ad113332c8bf1d9\trefs/tags/v2.0.0^{}\n"
30
+ )
31
+
32
+
33
+ def test_remote_refs_keeps_peeled_tags_apart() -> None:
34
+ refs = rel.remote_refs(LS_REMOTE)
35
+ assert refs["refs/heads/main"].startswith("21e680a")
36
+ assert refs["refs/tags/v2.0.0"].startswith("c41c8b9") # the annotated tag object
37
+ assert refs["refs/tags/v2.0.0^{}"].startswith("21e680a") # the commit it peels to
38
+
39
+
40
+ def test_release_page_url_for_each_github_remote_form() -> None:
41
+ for remote in (
42
+ "https://github.com/synapticforge1/universalWasmLoader-py.git",
43
+ "https://github.com/synapticforge1/universalWasmLoader-py",
44
+ "git@github.com:synapticforge1/universalWasmLoader-py.git",
45
+ "ssh://git@github.com/synapticforge1/universalWasmLoader-py.git",
46
+ ):
47
+ url = rel.release_page_url(remote, "v2.0.1", "notes & more?")
48
+ assert url is not None, remote
49
+ base, _, query = url.partition("?")
50
+ assert base == "https://github.com/synapticforge1/universalWasmLoader-py/releases/new"
51
+ assert urllib.parse.parse_qs(query) == {
52
+ "tag": ["v2.0.1"], "title": ["v2.0.1"], "body": ["notes & more?"]
53
+ }
54
+
55
+
56
+ def test_release_page_url_none_for_other_hosts() -> None:
57
+ assert rel.release_page_url("/tmp/scratch-bare.git", "v1", "x") is None
58
+ assert rel.release_page_url("https://gitlab.com/a/b.git", "v1", "x") is None