a3s-code 8.0.2__tar.gz → 8.0.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.4
2
2
  Name: a3s-code
3
- Version: 8.0.2
3
+ Version: 8.0.3
4
4
  Summary: A3S Code Python SDK — pure-Python bootstrap that fetches the native wheel from GitHub Releases
5
5
  Author: A3S Lab
6
6
  License: MIT
@@ -9,6 +9,11 @@ Project-URL: Repository, https://github.com/A3S-Lab/Code
9
9
  Project-URL: Issues, https://github.com/A3S-Lab/Code/issues
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: Implementation :: CPython
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
12
17
  Classifier: License :: OSI Approved :: MIT License
13
18
  Classifier: Operating System :: OS Independent
14
19
  Classifier: Topic :: Software Development
@@ -19,13 +24,22 @@ Description-Content-Type: text/markdown
19
24
 
20
25
  `pip install a3s-code` ships this small pure-Python package. On first
21
26
  `import a3s_code` it downloads the native extension matching your
22
- interpreter and platform from the project's
27
+ platform from the project's
23
28
  [GitHub Releases](https://github.com/A3S-Lab/Code/releases), verifies
24
29
  the wheel's sha256 against the release manifest, extracts the compiled
25
30
  extension into a per-user cache, and exposes the normal `a3s_code` API.
26
31
 
27
- Subsequent imports use the cached extension. Cache lives under
28
- `~/.cache/a3s-code/<version>/` (or `$XDG_CACHE_HOME/a3s-code/<version>/`).
32
+ The v8.0.3 release uses one CPython 3.10 stable-ABI (`cp310-abi3`) wheel per
33
+ platform. That wheel is installable by CPython 3.10, 3.11, 3.12, 3.13, and
34
+ 3.14. The published `v8.0.2` assets predate this workflow and still use exact
35
+ CPython 3.10–3.13 wheel names; upgrade the bootstrap to v8.0.3 for Python
36
+ 3.14. The loader falls back to the exact per-minor names used by older
37
+ releases, so upgrading the bootstrap does not invalidate an older release
38
+ asset.
39
+
40
+ Subsequent imports use the cached extension. Cache lives under a
41
+ platform-specific directory, `~/.cache/a3s-code/<version>/<platform-tag>/`
42
+ (or `$A3S_CODE_CACHE_DIR/<version>/<platform-tag>/` when overridden).
29
43
 
30
44
  ## Why
31
45
 
@@ -42,10 +56,16 @@ through PyPI.
42
56
  - Linux x86_64 (glibc 2.28+)
43
57
  - Windows x86_64
44
58
 
45
- CPython 3.10, 3.11, 3.12, 3.13.
59
+ CPython 3.10, 3.11, 3.12, 3.13, and 3.14.
46
60
 
47
- Python 3.14 wheels are not published yet. Use CPython 3.13 or earlier until
48
- the native binding is rebuilt for Python 3.14.
61
+ The bootstrap itself needs a working pip in the interpreter that runs it. If
62
+ `python3.14 -m pip` reports `No module named pip`, initialize pip and retry:
63
+
64
+ ```bash
65
+ python3.14 -m ensurepip --upgrade
66
+ python3.14 -m pip install --upgrade pip
67
+ python3.14 -m pip install a3s-code
68
+ ```
49
69
 
50
70
  ## Environment overrides
51
71
 
@@ -62,15 +82,19 @@ wheel directly:
62
82
 
63
83
  ```bash
64
84
  pip install \
65
- 'https://github.com/A3S-Lab/Code/releases/download/v<VERSION>/a3s_code-<VERSION>-cp312-cp312-manylinux_2_28_x86_64.whl'
85
+ 'https://github.com/A3S-Lab/Code/releases/download/v<VERSION>/a3s_code-<VERSION>-cp310-abi3-manylinux_2_28_x86_64.whl'
66
86
  ```
67
87
 
68
88
  For an Intel Mac on macOS 12 or later, use the `macosx_12_0_x86_64` asset:
69
89
 
70
90
  ```bash
71
91
  pip install \
72
- 'https://github.com/A3S-Lab/Code/releases/download/v<VERSION>/a3s_code-<VERSION>-cp312-cp312-macosx_12_0_x86_64.whl'
92
+ 'https://github.com/A3S-Lab/Code/releases/download/v<VERSION>/a3s_code-<VERSION>-cp310-abi3-macosx_12_0_x86_64.whl'
73
93
  ```
74
94
 
75
95
  Replace `<VERSION>` with the release to install. The Intel asset is available
76
96
  from the first release produced with the Intel build matrix.
97
+
98
+ The Intel macOS 12 build does not ship the optional local ONNX embedding
99
+ adapter. Keep retrieval model-free or configure an explicitly authorized
100
+ remote embedding provider when using that platform.
@@ -2,13 +2,22 @@
2
2
 
3
3
  `pip install a3s-code` ships this small pure-Python package. On first
4
4
  `import a3s_code` it downloads the native extension matching your
5
- interpreter and platform from the project's
5
+ platform from the project's
6
6
  [GitHub Releases](https://github.com/A3S-Lab/Code/releases), verifies
7
7
  the wheel's sha256 against the release manifest, extracts the compiled
8
8
  extension into a per-user cache, and exposes the normal `a3s_code` API.
9
9
 
10
- Subsequent imports use the cached extension. Cache lives under
11
- `~/.cache/a3s-code/<version>/` (or `$XDG_CACHE_HOME/a3s-code/<version>/`).
10
+ The v8.0.3 release uses one CPython 3.10 stable-ABI (`cp310-abi3`) wheel per
11
+ platform. That wheel is installable by CPython 3.10, 3.11, 3.12, 3.13, and
12
+ 3.14. The published `v8.0.2` assets predate this workflow and still use exact
13
+ CPython 3.10–3.13 wheel names; upgrade the bootstrap to v8.0.3 for Python
14
+ 3.14. The loader falls back to the exact per-minor names used by older
15
+ releases, so upgrading the bootstrap does not invalidate an older release
16
+ asset.
17
+
18
+ Subsequent imports use the cached extension. Cache lives under a
19
+ platform-specific directory, `~/.cache/a3s-code/<version>/<platform-tag>/`
20
+ (or `$A3S_CODE_CACHE_DIR/<version>/<platform-tag>/` when overridden).
12
21
 
13
22
  ## Why
14
23
 
@@ -25,10 +34,16 @@ through PyPI.
25
34
  - Linux x86_64 (glibc 2.28+)
26
35
  - Windows x86_64
27
36
 
28
- CPython 3.10, 3.11, 3.12, 3.13.
37
+ CPython 3.10, 3.11, 3.12, 3.13, and 3.14.
29
38
 
30
- Python 3.14 wheels are not published yet. Use CPython 3.13 or earlier until
31
- the native binding is rebuilt for Python 3.14.
39
+ The bootstrap itself needs a working pip in the interpreter that runs it. If
40
+ `python3.14 -m pip` reports `No module named pip`, initialize pip and retry:
41
+
42
+ ```bash
43
+ python3.14 -m ensurepip --upgrade
44
+ python3.14 -m pip install --upgrade pip
45
+ python3.14 -m pip install a3s-code
46
+ ```
32
47
 
33
48
  ## Environment overrides
34
49
 
@@ -45,15 +60,19 @@ wheel directly:
45
60
 
46
61
  ```bash
47
62
  pip install \
48
- 'https://github.com/A3S-Lab/Code/releases/download/v<VERSION>/a3s_code-<VERSION>-cp312-cp312-manylinux_2_28_x86_64.whl'
63
+ 'https://github.com/A3S-Lab/Code/releases/download/v<VERSION>/a3s_code-<VERSION>-cp310-abi3-manylinux_2_28_x86_64.whl'
49
64
  ```
50
65
 
51
66
  For an Intel Mac on macOS 12 or later, use the `macosx_12_0_x86_64` asset:
52
67
 
53
68
  ```bash
54
69
  pip install \
55
- 'https://github.com/A3S-Lab/Code/releases/download/v<VERSION>/a3s_code-<VERSION>-cp312-cp312-macosx_12_0_x86_64.whl'
70
+ 'https://github.com/A3S-Lab/Code/releases/download/v<VERSION>/a3s_code-<VERSION>-cp310-abi3-macosx_12_0_x86_64.whl'
56
71
  ```
57
72
 
58
73
  Replace `<VERSION>` with the release to install. The Intel asset is available
59
74
  from the first release produced with the Intel build matrix.
75
+
76
+ The Intel macOS 12 build does not ship the optional local ONNX embedding
77
+ adapter. Keep retrieval model-free or configure an explicitly authorized
78
+ remote embedding provider when using that platform.
@@ -7,7 +7,7 @@ name = "a3s-code"
7
7
  # Keep in sync with crates/code core release. The bootstrap loader fetches
8
8
  # the matching native wheel from `https://github.com/A3S-Lab/Code/releases/tag/v<version>`
9
9
  # at import time.
10
- version = "8.0.2"
10
+ version = "8.0.3"
11
11
  description = "A3S Code Python SDK — pure-Python bootstrap that fetches the native wheel from GitHub Releases"
12
12
  readme = "README.md"
13
13
  license = {text = "MIT"}
@@ -18,6 +18,11 @@ authors = [
18
18
  classifiers = [
19
19
  "Programming Language :: Python :: 3",
20
20
  "Programming Language :: Python :: Implementation :: CPython",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Programming Language :: Python :: 3.14",
21
26
  "License :: OSI Approved :: MIT License",
22
27
  "Operating System :: OS Independent",
23
28
  "Topic :: Software Development",
@@ -10,9 +10,8 @@ from . import _bootstrap as _bootstrap
10
10
 
11
11
  _bootstrap.ensure_native_loaded()
12
12
 
13
- # Re-import after the cache dir is on sys.path. The bootstrap extracted
14
- # `_native.<abi>.so` into a per-version cache; Python's import machinery
15
- # picks it up from there.
13
+ # The bootstrap extracted `_native.<abi>.so` into a per-version cache and
14
+ # registered it explicitly as `a3s_code._native` before returning.
16
15
  from ._native import * # noqa: E402,F401,F403
17
16
 
18
17
  __version__ = _bootstrap.__version__
@@ -1,12 +1,12 @@
1
1
  """Lazy loader for the a3s-code native extension.
2
2
 
3
3
  This module is part of the pure-Python bootstrap published to PyPI under
4
- the `a3s-code` name. On first import it resolves the matching native
5
- wheel for the current interpreter/platform, downloads it from the
6
- project's GitHub Releases, verifies the wheel's sha256 against the
4
+ the `a3s-code` name. On first import it resolves a compatible native
5
+ wheel for the current platform, downloads it from the project's GitHub
6
+ Releases, verifies the wheel's sha256 against the
7
7
  release manifest, extracts the compiled `_native` extension into a
8
- per-user cache, and prepends the cache to `sys.path` so the rest of
9
- `a3s_code/__init__.py` can `from ._native import *` normally.
8
+ per-user cache, and registers it explicitly as `a3s_code._native` because it
9
+ lives outside the installed package directory.
10
10
 
11
11
  Override the cache location via `A3S_CODE_CACHE_DIR`. Override the
12
12
  release source via `A3S_CODE_RELEASES_BASE_URL` (default points at the
@@ -31,11 +31,13 @@ from typing import Optional
31
31
 
32
32
  # Version is the bootstrap's own version, which equals the matching native
33
33
  # wheel version on GH Releases. Bumped by the release workflow.
34
- __version__ = "8.0.2"
34
+ __version__ = "8.0.3"
35
35
 
36
36
  _DEFAULT_BASE_URL = "https://github.com/A3S-Lab/Code/releases/download"
37
37
  _REQUEST_TIMEOUT_S = 120
38
38
  _USER_AGENT = f"a3s-code-bootstrap/{__version__}"
39
+ _MIN_PYTHON = (3, 10)
40
+ _ABI3_PYTHON_TAG = "cp310-abi3"
39
41
  _LOAD_LOCK = threading.Lock()
40
42
  _LOADED = False
41
43
 
@@ -44,6 +46,15 @@ class BootstrapError(RuntimeError):
44
46
  """Raised when the native extension cannot be located, downloaded, or verified."""
45
47
 
46
48
 
49
+ class BootstrapHttpError(BootstrapError):
50
+ """HTTP failure with a status code that callers can safely classify."""
51
+
52
+ def __init__(self, url: str, status_code: int):
53
+ self.url = url
54
+ self.status_code = status_code
55
+ super().__init__(f"GET {url} failed: HTTP {status_code}")
56
+
57
+
47
58
  def _base_url() -> str:
48
59
  return os.environ.get("A3S_CODE_RELEASES_BASE_URL", _DEFAULT_BASE_URL).rstrip("/")
49
60
 
@@ -51,9 +62,18 @@ def _base_url() -> str:
51
62
  def _cache_root() -> Path:
52
63
  override = os.environ.get("A3S_CODE_CACHE_DIR")
53
64
  if override:
54
- return Path(override).expanduser() / __version__
55
- base = os.environ.get("XDG_CACHE_HOME") or str(Path.home() / ".cache")
56
- return Path(base) / "a3s-code" / __version__
65
+ base = Path(override).expanduser() / __version__
66
+ else:
67
+ base = (
68
+ Path(os.environ.get("XDG_CACHE_HOME") or str(Path.home() / ".cache"))
69
+ / "a3s-code"
70
+ / __version__
71
+ )
72
+ # Keep native binaries for different architectures/OS baselines isolated.
73
+ # This matters when an Apple Silicon and an Intel interpreter share a home
74
+ # directory (for example, under Rosetta); a cached arm64 extension must
75
+ # never be handed to an x86_64 process.
76
+ return base / _platform_tag()
57
77
 
58
78
 
59
79
  def _platform_tag() -> str:
@@ -86,10 +106,50 @@ def _platform_tag() -> str:
86
106
 
87
107
 
88
108
  def _wheel_filename(version: str = __version__) -> str:
109
+ if platform.python_implementation() != "CPython":
110
+ raise BootstrapError(
111
+ "a3s-code publishes native wheels for CPython only; "
112
+ f"found {platform.python_implementation()}"
113
+ )
114
+ if (sys.version_info.major, sys.version_info.minor) < _MIN_PYTHON:
115
+ raise BootstrapError(
116
+ "a3s-code requires CPython 3.10 or newer; "
117
+ f"found {sys.version_info.major}.{sys.version_info.minor}"
118
+ )
89
119
  py_tag = f"cp{sys.version_info.major}{sys.version_info.minor}"
90
120
  return f"a3s_code-{version}-{py_tag}-{py_tag}-{_platform_tag()}.whl"
91
121
 
92
122
 
123
+ def _abi3_wheel_filename(version: str = __version__) -> str:
124
+ """Return the stable-ABI wheel name used by current releases."""
125
+ if platform.python_implementation() != "CPython":
126
+ raise BootstrapError(
127
+ "a3s-code publishes native wheels for CPython only; "
128
+ f"found {platform.python_implementation()}"
129
+ )
130
+ if (sys.version_info.major, sys.version_info.minor) < _MIN_PYTHON:
131
+ raise BootstrapError(
132
+ "a3s-code requires CPython 3.10 or newer; "
133
+ f"found {sys.version_info.major}.{sys.version_info.minor}"
134
+ )
135
+ return f"a3s_code-{version}-{_ABI3_PYTHON_TAG}-{_platform_tag()}.whl"
136
+
137
+
138
+ def _wheel_candidates(version: str = __version__) -> tuple[str, ...]:
139
+ """Return compatible wheel names in migration-safe preference order.
140
+
141
+ Existing releases contain one wheel per CPython minor, so Python 3.10–3.13
142
+ keep using those exact assets first. New releases publish a single
143
+ ``cp310-abi3`` wheel; Python 3.14+ prefers that asset and all interpreters
144
+ fall back to it when an older release has no exact match.
145
+ """
146
+ exact = _wheel_filename(version)
147
+ abi3 = _abi3_wheel_filename(version)
148
+ if sys.version_info.minor >= 14:
149
+ return (abi3, exact)
150
+ return (exact, abi3)
151
+
152
+
93
153
  def _release_url(filename: str, version: str = __version__) -> str:
94
154
  return f"{_base_url()}/v{version}/{filename}"
95
155
 
@@ -100,7 +160,7 @@ def _http_get(url: str) -> bytes:
100
160
  with urllib.request.urlopen(req, timeout=_REQUEST_TIMEOUT_S) as resp:
101
161
  return resp.read()
102
162
  except urllib.error.HTTPError as exc:
103
- raise BootstrapError(f"GET {url} failed: HTTP {exc.code}") from exc
163
+ raise BootstrapHttpError(url, exc.code) from exc
104
164
  except urllib.error.URLError as exc:
105
165
  raise BootstrapError(f"GET {url} failed: {exc.reason}") from exc
106
166
 
@@ -126,6 +186,27 @@ def _expected_sha256(wheel_name: str, version: str = __version__) -> Optional[st
126
186
  return None
127
187
 
128
188
 
189
+ def _download_wheel(version: str) -> tuple[str, bytes]:
190
+ """Download the first compatible wheel, retaining old-release fallback."""
191
+ candidates = _wheel_candidates(version)
192
+ not_found: list[str] = []
193
+ for wheel_name in candidates:
194
+ try:
195
+ return wheel_name, _http_get(_release_url(wheel_name, version))
196
+ except BootstrapHttpError as exc:
197
+ if exc.status_code == 404:
198
+ not_found.append(wheel_name)
199
+ continue
200
+ raise
201
+
202
+ tried = ", ".join(not_found)
203
+ raise BootstrapError(
204
+ "a3s-code: no compatible native wheel was published for "
205
+ f"CPython {sys.version_info.major}.{sys.version_info.minor} on "
206
+ f"{sys.platform}/{platform.machine()}. Tried: {tried}"
207
+ )
208
+
209
+
129
210
  def _extract_native(wheel_bytes: bytes, target_dir: Path) -> Path:
130
211
  """Extract the compiled `_native.*` extension from `wheel_bytes` into
131
212
  `target_dir`. Returns the path to the extracted file.
@@ -149,9 +230,23 @@ def _extract_native(wheel_bytes: bytes, target_dir: Path) -> Path:
149
230
  def _find_cached_native(cache_dir: Path) -> Optional[Path]:
150
231
  if not cache_dir.is_dir():
151
232
  return None
152
- for child in cache_dir.iterdir():
153
- if child.is_file() and child.name.startswith("_native."):
154
- return child
233
+ exact_tag = f"cp{sys.version_info.major}{sys.version_info.minor}"
234
+ cpython_tag = f"cpython-{sys.version_info.major}{sys.version_info.minor}"
235
+ exact: list[Path] = []
236
+ abi3: list[Path] = []
237
+ for child in sorted(cache_dir.iterdir()):
238
+ if not child.is_file() or not child.name.startswith("_native."):
239
+ continue
240
+ name = child.name.lower()
241
+ if f".{exact_tag}" in name or cpython_tag in name:
242
+ exact.append(child)
243
+ elif ".abi3." in name or name.endswith(".abi3"):
244
+ abi3.append(child)
245
+ # Prefer a native build tied to this interpreter, then the stable ABI.
246
+ if exact:
247
+ return exact[0]
248
+ if abi3:
249
+ return abi3[0]
155
250
  return None
156
251
 
157
252
 
@@ -193,13 +288,12 @@ def ensure_native_loaded(version: str = __version__) -> Path:
193
288
 
194
289
  native = _find_cached_native(cache)
195
290
  if native is None:
196
- wheel_name = _wheel_filename(version)
291
+ wheel_name, wheel_bytes = _download_wheel(version)
197
292
  url = _release_url(wheel_name, version)
198
293
  sys.stderr.write(
199
294
  f"a3s-code: fetching native wheel {wheel_name} "
200
295
  f"from {url} (first import only)...\n"
201
296
  )
202
- wheel_bytes = _http_get(url)
203
297
 
204
298
  if os.environ.get("A3S_CODE_SKIP_HASH_CHECK") != "1":
205
299
  expected = _expected_sha256(wheel_name, version)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: a3s-code
3
- Version: 8.0.2
3
+ Version: 8.0.3
4
4
  Summary: A3S Code Python SDK — pure-Python bootstrap that fetches the native wheel from GitHub Releases
5
5
  Author: A3S Lab
6
6
  License: MIT
@@ -9,6 +9,11 @@ Project-URL: Repository, https://github.com/A3S-Lab/Code
9
9
  Project-URL: Issues, https://github.com/A3S-Lab/Code/issues
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: Implementation :: CPython
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
12
17
  Classifier: License :: OSI Approved :: MIT License
13
18
  Classifier: Operating System :: OS Independent
14
19
  Classifier: Topic :: Software Development
@@ -19,13 +24,22 @@ Description-Content-Type: text/markdown
19
24
 
20
25
  `pip install a3s-code` ships this small pure-Python package. On first
21
26
  `import a3s_code` it downloads the native extension matching your
22
- interpreter and platform from the project's
27
+ platform from the project's
23
28
  [GitHub Releases](https://github.com/A3S-Lab/Code/releases), verifies
24
29
  the wheel's sha256 against the release manifest, extracts the compiled
25
30
  extension into a per-user cache, and exposes the normal `a3s_code` API.
26
31
 
27
- Subsequent imports use the cached extension. Cache lives under
28
- `~/.cache/a3s-code/<version>/` (or `$XDG_CACHE_HOME/a3s-code/<version>/`).
32
+ The v8.0.3 release uses one CPython 3.10 stable-ABI (`cp310-abi3`) wheel per
33
+ platform. That wheel is installable by CPython 3.10, 3.11, 3.12, 3.13, and
34
+ 3.14. The published `v8.0.2` assets predate this workflow and still use exact
35
+ CPython 3.10–3.13 wheel names; upgrade the bootstrap to v8.0.3 for Python
36
+ 3.14. The loader falls back to the exact per-minor names used by older
37
+ releases, so upgrading the bootstrap does not invalidate an older release
38
+ asset.
39
+
40
+ Subsequent imports use the cached extension. Cache lives under a
41
+ platform-specific directory, `~/.cache/a3s-code/<version>/<platform-tag>/`
42
+ (or `$A3S_CODE_CACHE_DIR/<version>/<platform-tag>/` when overridden).
29
43
 
30
44
  ## Why
31
45
 
@@ -42,10 +56,16 @@ through PyPI.
42
56
  - Linux x86_64 (glibc 2.28+)
43
57
  - Windows x86_64
44
58
 
45
- CPython 3.10, 3.11, 3.12, 3.13.
59
+ CPython 3.10, 3.11, 3.12, 3.13, and 3.14.
46
60
 
47
- Python 3.14 wheels are not published yet. Use CPython 3.13 or earlier until
48
- the native binding is rebuilt for Python 3.14.
61
+ The bootstrap itself needs a working pip in the interpreter that runs it. If
62
+ `python3.14 -m pip` reports `No module named pip`, initialize pip and retry:
63
+
64
+ ```bash
65
+ python3.14 -m ensurepip --upgrade
66
+ python3.14 -m pip install --upgrade pip
67
+ python3.14 -m pip install a3s-code
68
+ ```
49
69
 
50
70
  ## Environment overrides
51
71
 
@@ -62,15 +82,19 @@ wheel directly:
62
82
 
63
83
  ```bash
64
84
  pip install \
65
- 'https://github.com/A3S-Lab/Code/releases/download/v<VERSION>/a3s_code-<VERSION>-cp312-cp312-manylinux_2_28_x86_64.whl'
85
+ 'https://github.com/A3S-Lab/Code/releases/download/v<VERSION>/a3s_code-<VERSION>-cp310-abi3-manylinux_2_28_x86_64.whl'
66
86
  ```
67
87
 
68
88
  For an Intel Mac on macOS 12 or later, use the `macosx_12_0_x86_64` asset:
69
89
 
70
90
  ```bash
71
91
  pip install \
72
- 'https://github.com/A3S-Lab/Code/releases/download/v<VERSION>/a3s_code-<VERSION>-cp312-cp312-macosx_12_0_x86_64.whl'
92
+ 'https://github.com/A3S-Lab/Code/releases/download/v<VERSION>/a3s_code-<VERSION>-cp310-abi3-macosx_12_0_x86_64.whl'
73
93
  ```
74
94
 
75
95
  Replace `<VERSION>` with the release to install. The Intel asset is available
76
96
  from the first release produced with the Intel build matrix.
97
+
98
+ The Intel macOS 12 build does not ship the optional local ONNX embedding
99
+ adapter. Keep retrieval model-free or configure an explicitly authorized
100
+ remote embedding provider when using that platform.
@@ -81,6 +81,55 @@ class WheelFilenameTests(unittest.TestCase):
81
81
  "a3s_code-3.2.1-cp312-cp312-macosx_12_0_x86_64.whl",
82
82
  )
83
83
 
84
+ def test_macos_intel_abi3(self):
85
+ with (
86
+ mock.patch.object(sys, "platform", "darwin"),
87
+ mock.patch.object(_bootstrap.platform, "machine", return_value="x86_64"),
88
+ mock.patch.object(sys, "version_info", _FakeVersionInfo(3, 14)),
89
+ ):
90
+ self.assertEqual(
91
+ _bootstrap._abi3_wheel_filename("3.2.1"),
92
+ "a3s_code-3.2.1-cp310-abi3-macosx_12_0_x86_64.whl",
93
+ )
94
+
95
+ def test_new_python_prefers_abi3_then_exact_fallback(self):
96
+ with (
97
+ mock.patch.object(sys, "platform", "darwin"),
98
+ mock.patch.object(_bootstrap.platform, "machine", return_value="x86_64"),
99
+ mock.patch.object(sys, "version_info", _FakeVersionInfo(3, 14)),
100
+ ):
101
+ self.assertEqual(
102
+ _bootstrap._wheel_candidates("3.2.1"),
103
+ (
104
+ "a3s_code-3.2.1-cp310-abi3-macosx_12_0_x86_64.whl",
105
+ "a3s_code-3.2.1-cp314-cp314-macosx_12_0_x86_64.whl",
106
+ ),
107
+ )
108
+
109
+ def test_existing_python_prefers_exact_then_abi3(self):
110
+ with (
111
+ mock.patch.object(sys, "platform", "darwin"),
112
+ mock.patch.object(_bootstrap.platform, "machine", return_value="x86_64"),
113
+ mock.patch.object(sys, "version_info", _FakeVersionInfo(3, 12)),
114
+ ):
115
+ self.assertEqual(
116
+ _bootstrap._wheel_candidates("3.2.1"),
117
+ (
118
+ "a3s_code-3.2.1-cp312-cp312-macosx_12_0_x86_64.whl",
119
+ "a3s_code-3.2.1-cp310-abi3-macosx_12_0_x86_64.whl",
120
+ ),
121
+ )
122
+
123
+ def test_python_below_abi3_floor_raises(self):
124
+ with (
125
+ mock.patch.object(sys, "platform", "darwin"),
126
+ mock.patch.object(_bootstrap.platform, "machine", return_value="x86_64"),
127
+ mock.patch.object(sys, "version_info", _FakeVersionInfo(3, 9)),
128
+ ):
129
+ with self.assertRaises(_bootstrap.BootstrapError) as cm:
130
+ _bootstrap._wheel_candidates("3.2.1")
131
+ self.assertIn("CPython 3.10 or newer", str(cm.exception))
132
+
84
133
  def test_windows_amd64_cp313(self):
85
134
  self.assertEqual(
86
135
  self._filename_for("win32", "AMD64", 13),
@@ -114,18 +163,31 @@ class CacheDirTests(unittest.TestCase):
114
163
 
115
164
  def test_default_uses_xdg_or_home(self):
116
165
  cache = _bootstrap._cache_root()
117
- self.assertEqual(cache.parts[-2:], ("a3s-code", _bootstrap.__version__))
166
+ self.assertEqual(cache.parts[-3:-1], ("a3s-code", _bootstrap.__version__))
167
+ self.assertEqual(cache.name, _bootstrap._platform_tag())
118
168
 
119
169
  def test_xdg_cache_home_honored(self):
120
170
  os.environ["XDG_CACHE_HOME"] = "/tmp/xdg-test"
121
171
  cache = _bootstrap._cache_root()
122
- self.assertEqual(cache, Path(f"/tmp/xdg-test/a3s-code/{_bootstrap.__version__}"))
172
+ self.assertEqual(
173
+ cache,
174
+ Path(
175
+ f"/tmp/xdg-test/a3s-code/{_bootstrap.__version__}/"
176
+ f"{_bootstrap._platform_tag()}"
177
+ ),
178
+ )
123
179
 
124
180
  def test_explicit_override_wins(self):
125
181
  os.environ["XDG_CACHE_HOME"] = "/tmp/xdg-test"
126
182
  os.environ["A3S_CODE_CACHE_DIR"] = "/var/a3s-cache"
127
183
  cache = _bootstrap._cache_root()
128
- self.assertEqual(cache, Path(f"/var/a3s-cache/{_bootstrap.__version__}"))
184
+ self.assertEqual(
185
+ cache,
186
+ Path(
187
+ f"/var/a3s-cache/{_bootstrap.__version__}/"
188
+ f"{_bootstrap._platform_tag()}"
189
+ ),
190
+ )
129
191
 
130
192
 
131
193
  class ExtractNativeTests(unittest.TestCase):
@@ -147,11 +209,50 @@ class ExtractNativeTests(unittest.TestCase):
147
209
  _bootstrap._extract_native(buf.getvalue(), Path(tmp) / "pkg")
148
210
 
149
211
 
212
+ class CachedNativeSelectionTests(unittest.TestCase):
213
+ def test_selects_exact_interpreter_before_abi3(self):
214
+ with tempfile.TemporaryDirectory() as tmp:
215
+ cache = Path(tmp)
216
+ (cache / "_native.abi3.so").write_bytes(b"abi3")
217
+ (cache / "_native.cpython-312-darwin.so").write_bytes(b"exact")
218
+ with mock.patch.object(sys, "version_info", _FakeVersionInfo(3, 12)):
219
+ selected = _bootstrap._find_cached_native(cache)
220
+ self.assertIsNotNone(selected)
221
+ self.assertEqual(selected.name, "_native.cpython-312-darwin.so")
222
+
223
+ def test_does_not_reuse_another_python_minor(self):
224
+ with tempfile.TemporaryDirectory() as tmp:
225
+ cache = Path(tmp)
226
+ (cache / "_native.cpython-313-darwin.so").write_bytes(b"wrong")
227
+ with mock.patch.object(sys, "version_info", _FakeVersionInfo(3, 14)):
228
+ self.assertIsNone(_bootstrap._find_cached_native(cache))
229
+
230
+ def test_uses_abi3_when_exact_is_absent(self):
231
+ with tempfile.TemporaryDirectory() as tmp:
232
+ cache = Path(tmp)
233
+ (cache / "_native.abi3.so").write_bytes(b"abi3")
234
+ with mock.patch.object(sys, "version_info", _FakeVersionInfo(3, 14)):
235
+ selected = _bootstrap._find_cached_native(cache)
236
+ self.assertIsNotNone(selected)
237
+ self.assertEqual(selected.name, "_native.abi3.so")
238
+
239
+
150
240
  class EnsureNativeLoadedTests(unittest.TestCase):
151
241
  def setUp(self):
152
242
  self._tmp = tempfile.mkdtemp(prefix="a3s-bootstrap-test-")
153
243
  self._prev_cache = os.environ.get("A3S_CODE_CACHE_DIR")
154
244
  os.environ["A3S_CODE_CACHE_DIR"] = self._tmp
245
+ # The product intentionally rejects CPython < 3.10. macOS ships a
246
+ # system Python 3.9 on some runners, though, and these tests exercise
247
+ # download/cache behavior rather than the version-floor error path.
248
+ # Run the fixture under a supported simulated minor in that case so
249
+ # the suite remains useful when invoked with the system interpreter.
250
+ self._supported_version_patch = None
251
+ if (sys.version_info.major, sys.version_info.minor) < _bootstrap._MIN_PYTHON:
252
+ self._supported_version_patch = mock.patch.object(
253
+ sys, "version_info", _FakeVersionInfo(3, 12)
254
+ )
255
+ self._supported_version_patch.start()
155
256
  # Reset the module-level latch so each test starts clean.
156
257
  _bootstrap._LOADED = False
157
258
 
@@ -160,6 +261,8 @@ class EnsureNativeLoadedTests(unittest.TestCase):
160
261
  os.environ.pop("A3S_CODE_CACHE_DIR", None)
161
262
  else:
162
263
  os.environ["A3S_CODE_CACHE_DIR"] = self._prev_cache
264
+ if self._supported_version_patch is not None:
265
+ self._supported_version_patch.stop()
163
266
  shutil.rmtree(self._tmp, ignore_errors=True)
164
267
  _bootstrap._LOADED = False
165
268
 
@@ -172,7 +275,7 @@ class EnsureNativeLoadedTests(unittest.TestCase):
172
275
  "version": "3.2.1",
173
276
  "assets": [
174
277
  {
175
- "filename": _bootstrap._wheel_filename("3.2.1"),
278
+ "filename": _bootstrap._wheel_candidates("3.2.1")[0],
176
279
  "sha256": expected_sha,
177
280
  }
178
281
  ],
@@ -192,12 +295,15 @@ class EnsureNativeLoadedTests(unittest.TestCase):
192
295
  ):
193
296
  cache = _bootstrap.ensure_native_loaded("3.2.1")
194
297
 
195
- # `_cache_root()` keys the cache dir on the module's own __version__,
196
- # not the version arg passed to `ensure_native_loaded`. Reference it
197
- # directly so this assertion can't go stale on a version bump (the
198
- # sibling CacheDirTests follow the same pattern).
199
- self.assertEqual(cache, Path(self._tmp) / _bootstrap.__version__)
200
- # Native file extracted at cache root, not under a subdirectory.
298
+ # `_cache_root()` keys the cache dir on the module's own version and
299
+ # current platform, not the version arg passed to `ensure_native_loaded`.
300
+ self.assertEqual(
301
+ cache,
302
+ Path(self._tmp)
303
+ / _bootstrap.__version__
304
+ / _bootstrap._platform_tag(),
305
+ )
306
+ # Native file is extracted directly into the platform-scoped cache.
201
307
  extracted = list(cache.glob("_native.*"))
202
308
  self.assertEqual(len(extracted), 1)
203
309
  register_mock.assert_called_once_with(extracted[0])
@@ -209,7 +315,7 @@ class EnsureNativeLoadedTests(unittest.TestCase):
209
315
  "version": "3.2.1",
210
316
  "assets": [
211
317
  {
212
- "filename": _bootstrap._wheel_filename("3.2.1"),
318
+ "filename": _bootstrap._wheel_candidates("3.2.1")[0],
213
319
  "sha256": "0" * 64,
214
320
  }
215
321
  ],
@@ -236,7 +342,7 @@ class EnsureNativeLoadedTests(unittest.TestCase):
236
342
  "version": "3.2.1",
237
343
  "assets": [
238
344
  {
239
- "filename": _bootstrap._wheel_filename("3.2.1"),
345
+ "filename": _bootstrap._wheel_candidates("3.2.1")[0],
240
346
  "sha256": "0" * 64,
241
347
  }
242
348
  ],
@@ -265,7 +371,7 @@ class EnsureNativeLoadedTests(unittest.TestCase):
265
371
  "version": "3.2.1",
266
372
  "assets": [
267
373
  {
268
- "filename": _bootstrap._wheel_filename("3.2.1"),
374
+ "filename": _bootstrap._wheel_candidates("3.2.1")[0],
269
375
  "sha256": hashlib.sha256(wheel_bytes).hexdigest(),
270
376
  }
271
377
  ],
@@ -290,6 +396,76 @@ class EnsureNativeLoadedTests(unittest.TestCase):
290
396
  self.assertEqual(call_count["n"], calls_after_first,
291
397
  "second call must not re-download")
292
398
 
399
+ def test_python314_selects_abi3_wheel(self):
400
+ wheel_bytes = _make_wheel()
401
+ with mock.patch.object(sys, "version_info", _FakeVersionInfo(3, 14)):
402
+ abi3_name = _bootstrap._abi3_wheel_filename("3.2.1")
403
+ manifest = json.dumps(
404
+ {
405
+ "version": "3.2.1",
406
+ "assets": [
407
+ {
408
+ "filename": abi3_name,
409
+ "sha256": hashlib.sha256(wheel_bytes).hexdigest(),
410
+ }
411
+ ],
412
+ }
413
+ ).encode()
414
+
415
+ calls: list[str] = []
416
+
417
+ def fake_get(url: str) -> bytes:
418
+ calls.append(url)
419
+ if url.endswith("python-native-manifest.json"):
420
+ return manifest
421
+ return wheel_bytes
422
+
423
+ with (
424
+ mock.patch.object(_bootstrap, "_http_get", side_effect=fake_get),
425
+ mock.patch.object(_bootstrap, "_register_native") as register_mock,
426
+ ):
427
+ _bootstrap.ensure_native_loaded("3.2.1")
428
+
429
+ self.assertIn(abi3_name, calls[0])
430
+ register_mock.assert_called_once()
431
+
432
+ def test_python314_falls_back_to_exact_wheel_on_404(self):
433
+ wheel_bytes = _make_wheel()
434
+ with mock.patch.object(sys, "version_info", _FakeVersionInfo(3, 14)):
435
+ abi3_name = _bootstrap._abi3_wheel_filename("3.2.1")
436
+ exact_name = _bootstrap._wheel_filename("3.2.1")
437
+ manifest = json.dumps(
438
+ {
439
+ "version": "3.2.1",
440
+ "assets": [
441
+ {
442
+ "filename": exact_name,
443
+ "sha256": hashlib.sha256(wheel_bytes).hexdigest(),
444
+ }
445
+ ],
446
+ }
447
+ ).encode()
448
+
449
+ calls: list[str] = []
450
+
451
+ def fake_get(url: str) -> bytes:
452
+ calls.append(url)
453
+ if url.endswith("python-native-manifest.json"):
454
+ return manifest
455
+ if abi3_name in url:
456
+ raise _bootstrap.BootstrapHttpError(url, 404)
457
+ return wheel_bytes
458
+
459
+ with (
460
+ mock.patch.object(_bootstrap, "_http_get", side_effect=fake_get),
461
+ mock.patch.object(_bootstrap, "_register_native") as register_mock,
462
+ ):
463
+ _bootstrap.ensure_native_loaded("3.2.1")
464
+
465
+ self.assertIn(abi3_name, calls[0])
466
+ self.assertIn(exact_name, calls[1])
467
+ register_mock.assert_called_once()
468
+
293
469
 
294
470
  @unittest.skipUnless(
295
471
  os.environ.get("A3S_CODE_BOOTSTRAP_LIVE") == "1",
@@ -297,11 +473,12 @@ class EnsureNativeLoadedTests(unittest.TestCase):
297
473
  )
298
474
  class LiveDownloadTests(unittest.TestCase):
299
475
  def test_live_fetch_v3_2_0(self):
300
- # 3.2.0 has 12 wheels on GH Release — pick whichever matches the
301
- # current runner's platform/interpreter. Skip if the runner isn't
302
- # one of the supported triplets.
476
+ # 3.2.0 has native wheels on GH Release — pick whichever compatible
477
+ # asset is available for the current runner. Older releases predate
478
+ # the stable-ABI migration, so a newer interpreter may legitimately
479
+ # have no matching asset and should be reported as a skip.
303
480
  try:
304
- _bootstrap._wheel_filename("3.2.0")
481
+ _bootstrap._wheel_candidates("3.2.0")
305
482
  except _bootstrap.BootstrapError as exc:
306
483
  self.skipTest(str(exc))
307
484
  tmp = Path(tempfile.mkdtemp(prefix="a3s-live-"))
@@ -309,7 +486,10 @@ class LiveDownloadTests(unittest.TestCase):
309
486
  os.environ["A3S_CODE_CACHE_DIR"] = str(tmp)
310
487
  _bootstrap._LOADED = False
311
488
  with mock.patch.object(_bootstrap, "_register_native"):
312
- cache = _bootstrap.ensure_native_loaded("3.2.0")
489
+ try:
490
+ cache = _bootstrap.ensure_native_loaded("3.2.0")
491
+ except _bootstrap.BootstrapError as exc:
492
+ self.skipTest(str(exc))
313
493
  self.assertTrue(any(cache.glob("_native.*")))
314
494
  finally:
315
495
  os.environ.pop("A3S_CODE_CACHE_DIR", None)
File without changes
File without changes