pythonnative 0.20.0__py3-none-any.whl → 0.22.0__py3-none-any.whl
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.
- pythonnative/__init__.py +14 -3
- pythonnative/animated.py +420 -135
- pythonnative/cli/pn.py +450 -956
- pythonnative/components.py +519 -235
- pythonnative/events.py +210 -0
- pythonnative/gestures.py +875 -0
- pythonnative/layout.py +463 -149
- pythonnative/mutations.py +130 -0
- pythonnative/native_views/__init__.py +161 -97
- pythonnative/native_views/android.py +1050 -1124
- pythonnative/native_views/base.py +108 -18
- pythonnative/native_views/desktop.py +460 -417
- pythonnative/native_views/ios.py +1918 -1916
- pythonnative/project/__init__.py +68 -0
- pythonnative/project/android.py +504 -0
- pythonnative/project/builder.py +555 -0
- pythonnative/project/config.py +642 -0
- pythonnative/project/doctor.py +233 -0
- pythonnative/project/icons.py +247 -0
- pythonnative/project/ios.py +344 -0
- pythonnative/project/permissions.py +343 -0
- pythonnative/project/runtime_assets.py +272 -0
- pythonnative/reconciler.py +540 -470
- pythonnative/screen.py +5 -2
- pythonnative/sdk/_components.py +2 -2
- pythonnative/templates/android_template/app/build.gradle +2 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/METADATA +10 -2
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/RECORD +32 -21
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +0 -129
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Embedded CPython runtime acquisition for iOS builds.
|
|
2
|
+
|
|
3
|
+
iOS apps can't rely on a system Python, so PythonNative bundles a copy of
|
|
4
|
+
CPython built for iOS by the excellent
|
|
5
|
+
[Python-Apple-support](https://github.com/beeware/Python-Apple-support)
|
|
6
|
+
project. This module downloads the pinned release asset, verifies it,
|
|
7
|
+
extracts it once (cached under the build directory), and exposes the
|
|
8
|
+
paths the [`ios`][pythonnative.project.ios] configurator needs:
|
|
9
|
+
``Python.xcframework``, the simulator ``Python.framework``, the standard
|
|
10
|
+
library, and the simulator headers/static lib.
|
|
11
|
+
|
|
12
|
+
Android doesn't need any of this — Chaquopy ships its own CPython via
|
|
13
|
+
Gradle — so there's no Android equivalent here.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import tarfile
|
|
22
|
+
import urllib.request
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Callable, List, Optional
|
|
26
|
+
|
|
27
|
+
# Pinned, checksum-verified asset for the supported iOS Python version.
|
|
28
|
+
_PINNED_ASSETS = {
|
|
29
|
+
"3.11": (
|
|
30
|
+
"Python-3.11-iOS-support.b7.tar.gz",
|
|
31
|
+
"2b7d8589715b9890e8dd7e1bce91c210bb5287417e17b9af120fc577675ed28e",
|
|
32
|
+
),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_RELEASES_API = "https://api.github.com/repos/beeware/Python-Apple-support/releases?per_page=100"
|
|
36
|
+
_USER_AGENT = "pythonnative-cli"
|
|
37
|
+
|
|
38
|
+
Logger = Callable[[str], None]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class IOSRuntime:
|
|
43
|
+
"""Resolved paths to an extracted iOS CPython support package.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
python_version: The CPython ``major.minor`` version.
|
|
47
|
+
xcframework_dir: Path to ``Python.xcframework``.
|
|
48
|
+
simulator_framework: Path to the simulator-slice
|
|
49
|
+
``Python.framework`` (embedded into the simulator ``.app``).
|
|
50
|
+
stdlib_dir: Path to the simulator standard library directory.
|
|
51
|
+
simulator_headers: Path to the simulator-slice ``Headers``
|
|
52
|
+
directory (used when the project links the static lib).
|
|
53
|
+
simulator_static_lib: Path to the simulator ``libPythonX.Y.a``.
|
|
54
|
+
device_framework: Path to the device-slice ``Python.framework``
|
|
55
|
+
(embedded when archiving for a real device).
|
|
56
|
+
device_stdlib: Path to the device standard library directory.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
python_version: str
|
|
60
|
+
xcframework_dir: Path
|
|
61
|
+
simulator_framework: Optional[Path]
|
|
62
|
+
stdlib_dir: Optional[Path]
|
|
63
|
+
simulator_headers: Optional[Path]
|
|
64
|
+
simulator_static_lib: Optional[Path]
|
|
65
|
+
device_framework: Optional[Path] = None
|
|
66
|
+
device_stdlib: Optional[Path] = None
|
|
67
|
+
|
|
68
|
+
def framework_for(self, destination: str) -> Optional[Path]:
|
|
69
|
+
"""Return the ``Python.framework`` for a build destination.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
destination: ``"simulator"`` or ``"device"``.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
The matching framework path, or ``None`` if unavailable.
|
|
76
|
+
"""
|
|
77
|
+
return self.device_framework if destination == "device" else self.simulator_framework
|
|
78
|
+
|
|
79
|
+
def stdlib_for(self, destination: str) -> Optional[Path]:
|
|
80
|
+
"""Return the standard library directory for a build destination.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
destination: ``"simulator"`` or ``"device"``.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The matching stdlib path, or ``None`` if unavailable.
|
|
87
|
+
"""
|
|
88
|
+
return self.device_stdlib if destination == "device" else self.stdlib_dir
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _github_json(url: str) -> object:
|
|
92
|
+
headers = {"User-Agent": _USER_AGENT}
|
|
93
|
+
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
|
|
94
|
+
if token:
|
|
95
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
96
|
+
req = urllib.request.Request(url, headers=headers)
|
|
97
|
+
with urllib.request.urlopen(req) as response:
|
|
98
|
+
return json.loads(response.read().decode("utf-8"))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def resolve_asset_url(python_version: str, preferred_name: Optional[str] = None) -> Optional[str]:
|
|
102
|
+
"""Resolve a download URL for a Python-Apple-support iOS release asset.
|
|
103
|
+
|
|
104
|
+
Prefers an exact ``preferred_name`` match across all releases, then
|
|
105
|
+
falls back to the newest asset whose name contains
|
|
106
|
+
``Python-<version>-iOS-support`` and ends in ``.tar.gz``.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
python_version: CPython ``major.minor`` (e.g., ``"3.11"``).
|
|
110
|
+
preferred_name: Exact asset filename to prefer.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
A ``browser_download_url``, or ``None`` if resolution fails.
|
|
114
|
+
"""
|
|
115
|
+
try:
|
|
116
|
+
releases = _github_json(_RELEASES_API)
|
|
117
|
+
except Exception:
|
|
118
|
+
return None
|
|
119
|
+
if not isinstance(releases, list):
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
if preferred_name:
|
|
123
|
+
for release in releases:
|
|
124
|
+
for asset in release.get("assets", []) or []:
|
|
125
|
+
if asset.get("name") == preferred_name:
|
|
126
|
+
return asset.get("browser_download_url")
|
|
127
|
+
|
|
128
|
+
needle = f"Python-{python_version}-iOS-support"
|
|
129
|
+
for release in releases:
|
|
130
|
+
for asset in release.get("assets", []) or []:
|
|
131
|
+
name = asset.get("name") or ""
|
|
132
|
+
if needle in name and name.endswith(".tar.gz"):
|
|
133
|
+
return asset.get("browser_download_url")
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _sha256(path: Path) -> str:
|
|
138
|
+
digest = hashlib.sha256()
|
|
139
|
+
with open(path, "rb") as handle:
|
|
140
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
141
|
+
digest.update(chunk)
|
|
142
|
+
return digest.hexdigest()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _safe_extract(tar_path: Path, dest: Path) -> None:
|
|
146
|
+
"""Extract a tarball, refusing entries that escape ``dest``."""
|
|
147
|
+
dest = dest.resolve()
|
|
148
|
+
with tarfile.open(tar_path, "r:gz") as tar:
|
|
149
|
+
members = tar.getmembers()
|
|
150
|
+
for member in members:
|
|
151
|
+
target = (dest / member.name).resolve()
|
|
152
|
+
if not str(target).startswith(str(dest)):
|
|
153
|
+
raise RuntimeError(f"Refusing to extract unsafe path: {member.name}")
|
|
154
|
+
# ``filter='data'`` (3.12+) blocks unsafe members; older Pythons
|
|
155
|
+
# fall back to the manual check above.
|
|
156
|
+
try:
|
|
157
|
+
tar.extractall(dest, filter="data")
|
|
158
|
+
except TypeError:
|
|
159
|
+
tar.extractall(dest)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _first_existing(candidates: List[Path]) -> Optional[Path]:
|
|
163
|
+
for candidate in candidates:
|
|
164
|
+
if candidate.exists():
|
|
165
|
+
return candidate
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _locate_runtime(extract_root: Path, python_version: str) -> IOSRuntime:
|
|
170
|
+
xc_candidates = [
|
|
171
|
+
extract_root / "Python.xcframework",
|
|
172
|
+
extract_root / "support" / "Python.xcframework",
|
|
173
|
+
]
|
|
174
|
+
xcframework = _first_existing(xc_candidates)
|
|
175
|
+
if xcframework is None:
|
|
176
|
+
raise RuntimeError("Python.xcframework not found in extracted Python-Apple-support package.")
|
|
177
|
+
|
|
178
|
+
sim_slice = xcframework / "ios-arm64_x86_64-simulator"
|
|
179
|
+
simulator_framework = _first_existing([sim_slice / "Python.framework"])
|
|
180
|
+
stdlib_dir = _first_existing([sim_slice / "lib" / f"python{python_version}"])
|
|
181
|
+
simulator_headers = _first_existing([sim_slice / "Headers"])
|
|
182
|
+
simulator_static_lib = _first_existing(
|
|
183
|
+
[
|
|
184
|
+
sim_slice / f"libPython{python_version}.a",
|
|
185
|
+
sim_slice / "libpython.a",
|
|
186
|
+
]
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
device_slice = xcframework / "ios-arm64"
|
|
190
|
+
device_framework = _first_existing([device_slice / "Python.framework"])
|
|
191
|
+
device_stdlib = _first_existing([device_slice / "lib" / f"python{python_version}"])
|
|
192
|
+
|
|
193
|
+
return IOSRuntime(
|
|
194
|
+
python_version=python_version,
|
|
195
|
+
xcframework_dir=xcframework,
|
|
196
|
+
simulator_framework=simulator_framework,
|
|
197
|
+
stdlib_dir=stdlib_dir,
|
|
198
|
+
simulator_headers=simulator_headers,
|
|
199
|
+
simulator_static_lib=simulator_static_lib,
|
|
200
|
+
device_framework=device_framework,
|
|
201
|
+
device_stdlib=device_stdlib,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def prepare_ios_runtime(
|
|
206
|
+
cache_dir: Path,
|
|
207
|
+
python_version: str = "3.11",
|
|
208
|
+
*,
|
|
209
|
+
log: Optional[Logger] = None,
|
|
210
|
+
) -> IOSRuntime:
|
|
211
|
+
"""Download (if needed), verify, and extract the iOS CPython package.
|
|
212
|
+
|
|
213
|
+
The download and extraction are cached under ``cache_dir`` so repeat
|
|
214
|
+
builds are fast. For the pinned version the tarball checksum is
|
|
215
|
+
verified; for other versions the checksum is skipped with a warning.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
cache_dir: Directory to store downloads and extractions in.
|
|
219
|
+
python_version: CPython ``major.minor`` to fetch.
|
|
220
|
+
log: Optional callback for progress messages.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
A resolved [`IOSRuntime`][pythonnative.project.runtime_assets.IOSRuntime].
|
|
224
|
+
|
|
225
|
+
Raises:
|
|
226
|
+
RuntimeError: If the asset URL can't be resolved, the checksum
|
|
227
|
+
doesn't match, or the package layout is unexpected.
|
|
228
|
+
"""
|
|
229
|
+
emit: Logger = log or (lambda _message: None)
|
|
230
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
231
|
+
|
|
232
|
+
pinned = _PINNED_ASSETS.get(python_version)
|
|
233
|
+
preferred_name = pinned[0] if pinned else None
|
|
234
|
+
expected_sha = pinned[1] if pinned else None
|
|
235
|
+
|
|
236
|
+
extract_root = cache_dir / f"python-{python_version}"
|
|
237
|
+
if extract_root.is_dir():
|
|
238
|
+
try:
|
|
239
|
+
return _locate_runtime(extract_root, python_version)
|
|
240
|
+
except RuntimeError:
|
|
241
|
+
# Stale/partial extraction — re-extract below.
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
url = resolve_asset_url(python_version, preferred_name=preferred_name)
|
|
245
|
+
if not url:
|
|
246
|
+
raise RuntimeError(
|
|
247
|
+
f"Could not resolve a Python-Apple-support iOS asset for Python {python_version}. "
|
|
248
|
+
"Check your network connection or set GITHUB_TOKEN to avoid rate limits."
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
tar_path = cache_dir / os.path.basename(url)
|
|
252
|
+
if not tar_path.exists():
|
|
253
|
+
emit(f"Downloading embedded Python runtime ({python_version} iOS): {os.path.basename(url)}")
|
|
254
|
+
req = urllib.request.Request(url, headers={"User-Agent": _USER_AGENT})
|
|
255
|
+
with urllib.request.urlopen(req) as response, open(tar_path, "wb") as handle:
|
|
256
|
+
handle.write(response.read())
|
|
257
|
+
|
|
258
|
+
if expected_sha:
|
|
259
|
+
actual = _sha256(tar_path)
|
|
260
|
+
if actual != expected_sha:
|
|
261
|
+
tar_path.unlink(missing_ok=True)
|
|
262
|
+
raise RuntimeError(
|
|
263
|
+
f"Checksum mismatch for {tar_path.name}: expected {expected_sha}, got {actual}. "
|
|
264
|
+
"The download may be corrupt; re-run to try again."
|
|
265
|
+
)
|
|
266
|
+
else:
|
|
267
|
+
emit(f"Warning: no pinned checksum for Python {python_version}; skipping verification.")
|
|
268
|
+
|
|
269
|
+
emit("Extracting embedded Python runtime...")
|
|
270
|
+
extract_root.mkdir(parents=True, exist_ok=True)
|
|
271
|
+
_safe_extract(tar_path, extract_root)
|
|
272
|
+
return _locate_runtime(extract_root, python_version)
|