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.
Files changed (33) hide show
  1. pythonnative/__init__.py +14 -3
  2. pythonnative/animated.py +420 -135
  3. pythonnative/cli/pn.py +450 -956
  4. pythonnative/components.py +519 -235
  5. pythonnative/events.py +210 -0
  6. pythonnative/gestures.py +875 -0
  7. pythonnative/layout.py +463 -149
  8. pythonnative/mutations.py +130 -0
  9. pythonnative/native_views/__init__.py +161 -97
  10. pythonnative/native_views/android.py +1050 -1124
  11. pythonnative/native_views/base.py +108 -18
  12. pythonnative/native_views/desktop.py +460 -417
  13. pythonnative/native_views/ios.py +1918 -1916
  14. pythonnative/project/__init__.py +68 -0
  15. pythonnative/project/android.py +504 -0
  16. pythonnative/project/builder.py +555 -0
  17. pythonnative/project/config.py +642 -0
  18. pythonnative/project/doctor.py +233 -0
  19. pythonnative/project/icons.py +247 -0
  20. pythonnative/project/ios.py +344 -0
  21. pythonnative/project/permissions.py +343 -0
  22. pythonnative/project/runtime_assets.py +272 -0
  23. pythonnative/reconciler.py +540 -470
  24. pythonnative/screen.py +5 -2
  25. pythonnative/sdk/_components.py +2 -2
  26. pythonnative/templates/android_template/app/build.gradle +2 -0
  27. {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/METADATA +10 -2
  28. {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/RECORD +32 -21
  29. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +0 -129
  30. {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/WHEEL +0 -0
  31. {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/entry_points.txt +0 -0
  32. {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/licenses/LICENSE +0 -0
  33. {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)