pythonnative 0.24.0__py3-none-any.whl → 0.26.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 (52) hide show
  1. pythonnative/__init__.py +16 -4
  2. pythonnative/animated.py +2 -2
  3. pythonnative/cli/pn.py +172 -12
  4. pythonnative/components.py +59 -0
  5. pythonnative/diagnostics.py +27 -0
  6. pythonnative/hooks.py +364 -81
  7. pythonnative/native_modules/app_state.py +2 -1
  8. pythonnative/native_modules/battery.py +3 -2
  9. pythonnative/native_modules/biometrics.py +2 -1
  10. pythonnative/native_modules/camera.py +16 -7
  11. pythonnative/native_modules/clipboard.py +3 -2
  12. pythonnative/native_modules/file_system.py +3 -2
  13. pythonnative/native_modules/haptics.py +7 -6
  14. pythonnative/native_modules/linking.py +62 -6
  15. pythonnative/native_modules/location.py +11 -8
  16. pythonnative/native_modules/net_info.py +2 -1
  17. pythonnative/native_modules/notifications.py +132 -9
  18. pythonnative/native_modules/permissions.py +27 -3
  19. pythonnative/native_modules/share.py +2 -1
  20. pythonnative/native_views/android.py +149 -124
  21. pythonnative/native_views/desktop.py +50 -49
  22. pythonnative/native_views/ios.py +240 -161
  23. pythonnative/project/android.py +15 -3
  24. pythonnative/project/builder.py +172 -70
  25. pythonnative/project/config.py +28 -6
  26. pythonnative/project/devices.py +291 -0
  27. pythonnative/project/doctor.py +5 -4
  28. pythonnative/project/ios.py +51 -77
  29. pythonnative/project/permissions.py +19 -1
  30. pythonnative/project/runtime_assets.py +78 -166
  31. pythonnative/reconciler.py +518 -37
  32. pythonnative/runtime.py +407 -215
  33. pythonnative/screen.py +53 -7
  34. pythonnative/storage.py +2 -2
  35. pythonnative/suspense.py +417 -0
  36. pythonnative/templates/android_template/app/build.gradle +5 -0
  37. pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +1 -0
  38. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +88 -12
  39. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +41 -17
  40. pythonnative/templates/ios_template/ios_template/BridgingHeader.h +14 -0
  41. pythonnative/templates/ios_template/ios_template/Info.plist +0 -2
  42. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +397 -0
  43. pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +31 -20
  44. pythonnative/templates/ios_template/ios_template/ViewController.swift +77 -198
  45. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +72 -47
  46. {pythonnative-0.24.0.dist-info → pythonnative-0.26.0.dist-info}/METADATA +2 -1
  47. {pythonnative-0.24.0.dist-info → pythonnative-0.26.0.dist-info}/RECORD +51 -48
  48. {pythonnative-0.24.0.dist-info → pythonnative-0.26.0.dist-info}/WHEEL +1 -1
  49. pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -24
  50. {pythonnative-0.24.0.dist-info → pythonnative-0.26.0.dist-info}/entry_points.txt +0 -0
  51. {pythonnative-0.24.0.dist-info → pythonnative-0.26.0.dist-info}/licenses/LICENSE +0 -0
  52. {pythonnative-0.24.0.dist-info → pythonnative-0.26.0.dist-info}/top_level.txt +0 -0
@@ -3,11 +3,16 @@
3
3
  iOS apps can't rely on a system Python, so PythonNative bundles a copy of
4
4
  CPython built for iOS by the excellent
5
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.
6
+ project. This module downloads the pinned release asset for the
7
+ project's ``app.python_version``, verifies its checksum, extracts it
8
+ once (cached under the build directory), and exposes the path to
9
+ ``Python.xcframework``.
10
+
11
+ The xcframework is linked and embedded by the bundled Xcode template at
12
+ build time; its ``build/utils.sh`` helper (shipped inside the framework
13
+ by BeeWare) installs the standard library and converts binary modules
14
+ into signed frameworks during the Xcode build. There is no post-build
15
+ copy step.
11
16
 
12
17
  Android doesn't need any of this: Chaquopy ships its own CPython via
13
18
  Gradle, so there's no Android equivalent here.
@@ -16,23 +21,34 @@ Gradle, so there's no Android equivalent here.
16
21
  from __future__ import annotations
17
22
 
18
23
  import hashlib
19
- import json
20
- import os
21
24
  import tarfile
22
25
  import urllib.request
23
26
  from dataclasses import dataclass
24
27
  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 = {
28
+ from typing import Callable, Optional
29
+
30
+ # Pinned, checksum-verified Python-Apple-support assets. Every version in
31
+ # ``config.SUPPORTED_PYTHON_VERSIONS`` must have an entry here; iOS builds
32
+ # refuse to run against an unpinned, unverified runtime.
33
+ PINNED_ASSETS = {
34
+ "3.10": (
35
+ "3.10-b14",
36
+ "Python-3.10-iOS-support.b14.tar.gz",
37
+ "a6da479a67be74569813af77179dc8ac83a5e685324408110bfc04632166e404",
38
+ ),
29
39
  "3.11": (
30
- "Python-3.11-iOS-support.b7.tar.gz",
31
- "2b7d8589715b9890e8dd7e1bce91c210bb5287417e17b9af120fc577675ed28e",
40
+ "3.11-b9",
41
+ "Python-3.11-iOS-support.b9.tar.gz",
42
+ "56810335d2b73558f7a16b6b2f7ad855b88c3007b200cbaecae9d6a25a2d1ecc",
43
+ ),
44
+ "3.12": (
45
+ "3.12-b9",
46
+ "Python-3.12-iOS-support.b9.tar.gz",
47
+ "a3be9e278c742911db54dd3045bd7451928813508771c9acf14b4af75294edd2",
32
48
  ),
33
49
  }
34
50
 
35
- _RELEASES_API = "https://api.github.com/repos/beeware/Python-Apple-support/releases?per_page=100"
51
+ _DOWNLOAD_URL = "https://github.com/beeware/Python-Apple-support/releases/download/{tag}/{name}"
36
52
  _USER_AGENT = "pythonnative-cli"
37
53
 
38
54
  Logger = Callable[[str], None]
@@ -40,98 +56,20 @@ Logger = Callable[[str], None]
40
56
 
41
57
  @dataclass
42
58
  class IOSRuntime:
43
- """Resolved paths to an extracted iOS CPython support package.
59
+ """A resolved, extracted iOS CPython support package.
44
60
 
45
61
  Attributes:
46
62
  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.
63
+ xcframework_dir: Path to the extracted ``Python.xcframework``.
57
64
  """
58
65
 
59
66
  python_version: str
60
67
  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
68
 
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
69
+ @property
70
+ def install_script(self) -> Path:
71
+ """Path to BeeWare's ``utils.sh`` build helper inside the framework."""
72
+ return self.xcframework_dir / "build" / "utils.sh"
135
73
 
136
74
 
137
75
  def _sha256(path: Path) -> str:
@@ -159,60 +97,31 @@ def _safe_extract(tar_path: Path, dest: Path) -> None:
159
97
  tar.extractall(dest)
160
98
 
161
99
 
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
100
  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:
101
+ xcframework = extract_root / "Python.xcframework"
102
+ if not xcframework.is_dir():
176
103
  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
- )
104
+ runtime = IOSRuntime(python_version=python_version, xcframework_dir=xcframework)
105
+ if not runtime.install_script.is_file():
106
+ raise RuntimeError(
107
+ "The extracted Python.xcframework is missing build/utils.sh; the support "
108
+ "package layout is older than PythonNative expects. Delete the "
109
+ "build/ios_runtime cache and re-run to fetch the pinned asset."
110
+ )
111
+ return runtime
203
112
 
204
113
 
205
114
  def prepare_ios_runtime(
206
115
  cache_dir: Path,
207
- python_version: str = "3.11",
116
+ python_version: str = "3.12",
208
117
  *,
209
118
  log: Optional[Logger] = None,
210
119
  ) -> IOSRuntime:
211
120
  """Download (if needed), verify, and extract the iOS CPython package.
212
121
 
213
122
  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.
123
+ builds are fast. Only pinned, checksum-verified versions are
124
+ accepted; there is no unverified fallback.
216
125
 
217
126
  Args:
218
127
  cache_dir: Directory to store downloads and extractions in.
@@ -223,15 +132,20 @@ def prepare_ios_runtime(
223
132
  A resolved [`IOSRuntime`][pythonnative.project.runtime_assets.IOSRuntime].
224
133
 
225
134
  Raises:
226
- RuntimeError: If the asset URL can't be resolved, the checksum
227
- doesn't match, or the package layout is unexpected.
135
+ RuntimeError: If the version has no pinned asset, the download
136
+ fails, the checksum doesn't match, or the package layout is
137
+ unexpected.
228
138
  """
229
139
  emit: Logger = log or (lambda _message: None)
230
140
  cache_dir.mkdir(parents=True, exist_ok=True)
231
141
 
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
142
+ pinned = PINNED_ASSETS.get(python_version)
143
+ if pinned is None:
144
+ supported = ", ".join(sorted(PINNED_ASSETS))
145
+ raise RuntimeError(
146
+ f"No pinned iOS runtime for Python {python_version}. " f"Set app.python_version to one of: {supported}."
147
+ )
148
+ tag, asset_name, expected_sha = pinned
235
149
 
236
150
  extract_root = cache_dir / f"python-{python_version}"
237
151
  if extract_root.is_dir():
@@ -241,30 +155,28 @@ def prepare_ios_runtime(
241
155
  # Stale/partial extraction: re-extract below.
242
156
  pass
243
157
 
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)}")
158
+ url = _DOWNLOAD_URL.format(tag=tag, name=asset_name)
159
+ tar_path = cache_dir / asset_name
160
+ if not tar_path.exists() or _sha256(tar_path) != expected_sha:
161
+ emit(f"Downloading embedded Python runtime ({python_version} iOS): {asset_name}")
254
162
  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:
163
+ try:
164
+ with urllib.request.urlopen(req) as response, open(tar_path, "wb") as handle:
165
+ handle.write(response.read())
166
+ except OSError as exc:
261
167
  tar_path.unlink(missing_ok=True)
262
168
  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.")
169
+ f"Could not download the iOS Python runtime from {url}: {exc}. "
170
+ "Check your network connection and re-run."
171
+ ) from exc
172
+
173
+ actual = _sha256(tar_path)
174
+ if actual != expected_sha:
175
+ tar_path.unlink(missing_ok=True)
176
+ raise RuntimeError(
177
+ f"Checksum mismatch for {asset_name}: expected {expected_sha}, got {actual}. "
178
+ "The download may be corrupt; re-run to try again."
179
+ )
268
180
 
269
181
  emit("Extracting embedded Python runtime...")
270
182
  extract_root.mkdir(parents=True, exist_ok=True)