pythonnative 0.25.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.
- pythonnative/__init__.py +1 -1
- pythonnative/cli/pn.py +172 -12
- pythonnative/diagnostics.py +27 -0
- pythonnative/native_modules/app_state.py +2 -1
- pythonnative/native_modules/battery.py +3 -2
- pythonnative/native_modules/biometrics.py +2 -1
- pythonnative/native_modules/camera.py +16 -7
- pythonnative/native_modules/clipboard.py +3 -2
- pythonnative/native_modules/file_system.py +3 -2
- pythonnative/native_modules/haptics.py +7 -6
- pythonnative/native_modules/linking.py +62 -6
- pythonnative/native_modules/location.py +11 -8
- pythonnative/native_modules/net_info.py +2 -1
- pythonnative/native_modules/notifications.py +97 -6
- pythonnative/native_modules/permissions.py +3 -2
- pythonnative/native_modules/share.py +2 -1
- pythonnative/native_views/android.py +142 -122
- pythonnative/native_views/desktop.py +50 -49
- pythonnative/native_views/ios.py +228 -160
- pythonnative/project/android.py +15 -3
- pythonnative/project/builder.py +172 -70
- pythonnative/project/config.py +28 -6
- pythonnative/project/devices.py +291 -0
- pythonnative/project/doctor.py +5 -4
- pythonnative/project/ios.py +51 -77
- pythonnative/project/permissions.py +19 -1
- pythonnative/project/runtime_assets.py +78 -166
- pythonnative/templates/android_template/app/build.gradle +5 -0
- pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +1 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +88 -12
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +41 -17
- pythonnative/templates/ios_template/ios_template/BridgingHeader.h +14 -0
- pythonnative/templates/ios_template/ios_template/Info.plist +0 -2
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +397 -0
- pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +31 -20
- pythonnative/templates/ios_template/ios_template/ViewController.swift +77 -198
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +72 -47
- {pythonnative-0.25.0.dist-info → pythonnative-0.26.0.dist-info}/METADATA +1 -1
- {pythonnative-0.25.0.dist-info → pythonnative-0.26.0.dist-info}/RECORD +43 -41
- pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -24
- {pythonnative-0.25.0.dist-info → pythonnative-0.26.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.25.0.dist-info → pythonnative-0.26.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.25.0.dist-info → pythonnative-0.26.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.25.0.dist-info → pythonnative-0.26.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
//
|
|
2
|
+
// PythonRuntime.swift
|
|
3
|
+
// ios_template
|
|
4
|
+
//
|
|
5
|
+
// Embeds CPython directly through the C API (no PythonKit). The
|
|
6
|
+
// Python.xcframework is linked at build time, so a missing runtime is
|
|
7
|
+
// a build error, never a silent runtime fallback.
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
import Foundation
|
|
11
|
+
|
|
12
|
+
enum PythonRuntimeError: Error, CustomStringConvertible {
|
|
13
|
+
case startup(String)
|
|
14
|
+
case call(String)
|
|
15
|
+
|
|
16
|
+
var description: String {
|
|
17
|
+
switch self {
|
|
18
|
+
case .startup(let message): return message
|
|
19
|
+
case .call(let message): return message
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// An owned reference to a Python object. Reference counting is handled
|
|
25
|
+
/// automatically; the wrapped pointer is released on deinit.
|
|
26
|
+
final class PyRef {
|
|
27
|
+
fileprivate let pointer: UnsafeMutablePointer<PyObject>
|
|
28
|
+
|
|
29
|
+
fileprivate init(owned pointer: UnsafeMutablePointer<PyObject>) {
|
|
30
|
+
self.pointer = pointer
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
deinit {
|
|
34
|
+
let gil = PyGILState_Ensure()
|
|
35
|
+
Py_DecRef(pointer)
|
|
36
|
+
PyGILState_Release(gil)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Call a method on this object with string/number/bool arguments.
|
|
40
|
+
@discardableResult
|
|
41
|
+
func call(_ method: String, _ args: Any...) throws -> PyRef {
|
|
42
|
+
let gil = PyGILState_Ensure()
|
|
43
|
+
defer { PyGILState_Release(gil) }
|
|
44
|
+
guard let bound = PyObject_GetAttrString(pointer, method) else {
|
|
45
|
+
throw PythonRuntimeError.call("Python object has no method '\(method)': \(PythonRuntime.consumeError())")
|
|
46
|
+
}
|
|
47
|
+
defer { Py_DecRef(bound) }
|
|
48
|
+
return try PythonRuntime.invoke(bound, args: args, context: method)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Truthiness of the wrapped object (`PyObject_IsTrue`).
|
|
52
|
+
var isTruthy: Bool {
|
|
53
|
+
let gil = PyGILState_Ensure()
|
|
54
|
+
defer { PyGILState_Release(gil) }
|
|
55
|
+
return PyObject_IsTrue(pointer) == 1
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Owns the embedded CPython interpreter for the whole process.
|
|
60
|
+
final class PythonRuntime {
|
|
61
|
+
static let shared = PythonRuntime()
|
|
62
|
+
|
|
63
|
+
private(set) var started = false
|
|
64
|
+
private(set) var startupError: String? = nil
|
|
65
|
+
private var pendingURLs: [String] = []
|
|
66
|
+
|
|
67
|
+
private init() {}
|
|
68
|
+
|
|
69
|
+
// MARK: - Startup
|
|
70
|
+
|
|
71
|
+
/// Initialize the embedded interpreter once. Throws with a full
|
|
72
|
+
/// description when the bundle is not packaged correctly; there is
|
|
73
|
+
/// deliberately no fallback mode.
|
|
74
|
+
func ensureStarted() throws {
|
|
75
|
+
if started { return }
|
|
76
|
+
if let error = startupError { throw PythonRuntimeError.startup(error) }
|
|
77
|
+
do {
|
|
78
|
+
try start()
|
|
79
|
+
started = true
|
|
80
|
+
flushPendingURLs()
|
|
81
|
+
} catch let error as PythonRuntimeError {
|
|
82
|
+
startupError = error.description
|
|
83
|
+
throw error
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private func start() throws {
|
|
88
|
+
guard let resourcePath = Bundle.main.resourcePath else {
|
|
89
|
+
throw PythonRuntimeError.startup("The app bundle has no resource path; the build is corrupt.")
|
|
90
|
+
}
|
|
91
|
+
let pythonHome = "\(resourcePath)/python"
|
|
92
|
+
let appPath = "\(resourcePath)/app"
|
|
93
|
+
let packagesPath = "\(resourcePath)/app_packages"
|
|
94
|
+
for (path, hint) in [
|
|
95
|
+
(pythonHome, "the embedded Python runtime"),
|
|
96
|
+
(appPath, "the app's Python sources"),
|
|
97
|
+
(packagesPath, "the bundled Python packages"),
|
|
98
|
+
] where !FileManager.default.fileExists(atPath: path) {
|
|
99
|
+
throw PythonRuntimeError.startup(
|
|
100
|
+
"Missing \(hint) at \(path). Build the app with 'pn run ios' or 'pn build ios' "
|
|
101
|
+
+ "so the PythonNative CLI stages the Python runtime; the raw Xcode template "
|
|
102
|
+
+ "cannot run on its own."
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Signal to pythonnative that we're running on iOS. Read on the
|
|
107
|
+
// Python side (pythonnative.utils.IS_IOS) before the first import.
|
|
108
|
+
setenv("PN_PLATFORM", "ios", 1)
|
|
109
|
+
|
|
110
|
+
var preconfig = PyPreConfig()
|
|
111
|
+
PyPreConfig_InitIsolatedConfig(&preconfig)
|
|
112
|
+
preconfig.utf8_mode = 1
|
|
113
|
+
var status = Py_PreInitialize(&preconfig)
|
|
114
|
+
if PyStatus_Exception(status) != 0 {
|
|
115
|
+
throw PythonRuntimeError.startup("Unable to pre-initialize Python: \(Self.statusMessage(status))")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
var config = PyConfig()
|
|
119
|
+
PyConfig_InitIsolatedConfig(&config)
|
|
120
|
+
defer { PyConfig_Clear(&config) }
|
|
121
|
+
// Unbuffered stdio so print() reaches the device log immediately.
|
|
122
|
+
config.buffered_stdio = 0
|
|
123
|
+
// The signed bundle is immutable; never try to write bytecode.
|
|
124
|
+
config.write_bytecode = 0
|
|
125
|
+
|
|
126
|
+
guard let homeW = Py_DecodeLocale(pythonHome, nil) else {
|
|
127
|
+
throw PythonRuntimeError.startup("Could not decode the Python home path.")
|
|
128
|
+
}
|
|
129
|
+
status = withUnsafeMutablePointer(to: &config) { cfg in
|
|
130
|
+
PyConfig_SetString(cfg, &cfg.pointee.home, homeW)
|
|
131
|
+
}
|
|
132
|
+
PyMem_RawFree(homeW)
|
|
133
|
+
if PyStatus_Exception(status) != 0 {
|
|
134
|
+
throw PythonRuntimeError.startup("Unable to set the Python home: \(Self.statusMessage(status))")
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
status = PyConfig_Read(&config)
|
|
138
|
+
if PyStatus_Exception(status) != 0 {
|
|
139
|
+
throw PythonRuntimeError.startup("Unable to read the Python config: \(Self.statusMessage(status))")
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
status = Py_InitializeFromConfig(&config)
|
|
143
|
+
if PyStatus_Exception(status) != 0 {
|
|
144
|
+
throw PythonRuntimeError.startup("Unable to initialize Python: \(Self.statusMessage(status))")
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// The interpreter is live and this thread holds the GIL. Finish
|
|
148
|
+
// path setup, then release the GIL; every later entry point
|
|
149
|
+
// re-acquires it via PyGILState_Ensure.
|
|
150
|
+
do {
|
|
151
|
+
try bootstrapPaths(appPath: appPath, packagesPath: packagesPath)
|
|
152
|
+
} catch {
|
|
153
|
+
PyEval_SaveThread()
|
|
154
|
+
throw error
|
|
155
|
+
}
|
|
156
|
+
PyEval_SaveThread()
|
|
157
|
+
NSLog("[PN] Embedded Python initialized (home: \(pythonHome))")
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private func bootstrapPaths(appPath: String, packagesPath: String) throws {
|
|
161
|
+
// site.addsitedir(app_packages): adds to sys.path and honors .pth files.
|
|
162
|
+
guard let site = PyImport_ImportModule("site"),
|
|
163
|
+
let addsitedir = PyObject_GetAttrString(site, "addsitedir")
|
|
164
|
+
else {
|
|
165
|
+
throw PythonRuntimeError.startup("Could not import the site module: \(Self.consumeError())")
|
|
166
|
+
}
|
|
167
|
+
defer { Py_DecRef(site) }
|
|
168
|
+
defer { Py_DecRef(addsitedir) }
|
|
169
|
+
guard let packagesObj = PyUnicode_FromString(packagesPath),
|
|
170
|
+
let argsTuple = PyTuple_New(1)
|
|
171
|
+
else {
|
|
172
|
+
throw PythonRuntimeError.startup("Could not build site.addsitedir arguments.")
|
|
173
|
+
}
|
|
174
|
+
PyTuple_SetItem(argsTuple, 0, packagesObj) // steals packagesObj
|
|
175
|
+
defer { Py_DecRef(argsTuple) }
|
|
176
|
+
guard let addResult = PyObject_CallObject(addsitedir, argsTuple) else {
|
|
177
|
+
throw PythonRuntimeError.startup("site.addsitedir failed: \(Self.consumeError())")
|
|
178
|
+
}
|
|
179
|
+
Py_DecRef(addResult)
|
|
180
|
+
|
|
181
|
+
// sys.path.insert(0, app) so `import app.main` resolves.
|
|
182
|
+
guard let sys = PyImport_ImportModule("sys"),
|
|
183
|
+
let sysPath = PyObject_GetAttrString(sys, "path"),
|
|
184
|
+
let appObj = PyUnicode_FromString(appPath)
|
|
185
|
+
else {
|
|
186
|
+
throw PythonRuntimeError.startup("Could not access sys.path: \(Self.consumeError())")
|
|
187
|
+
}
|
|
188
|
+
defer { Py_DecRef(sys) }
|
|
189
|
+
defer { Py_DecRef(sysPath) }
|
|
190
|
+
defer { Py_DecRef(appObj) }
|
|
191
|
+
// The bundle root itself is also importable ("app" package form).
|
|
192
|
+
if let resourceObj = PyUnicode_FromString(Bundle.main.resourcePath ?? appPath) {
|
|
193
|
+
PyList_Insert(sysPath, 0, resourceObj)
|
|
194
|
+
Py_DecRef(resourceObj)
|
|
195
|
+
}
|
|
196
|
+
if PyList_Insert(sysPath, 0, appObj) != 0 {
|
|
197
|
+
throw PythonRuntimeError.startup("Could not add the app directory to sys.path.")
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#if DEBUG
|
|
201
|
+
// Dev-only: prioritize the writable hot-reload overlay. Release
|
|
202
|
+
// builds never look outside the signed bundle.
|
|
203
|
+
let documents = NSHomeDirectory() + "/Documents"
|
|
204
|
+
guard let hotReload = PyImport_ImportModule("pythonnative.hot_reload"),
|
|
205
|
+
let configure = PyObject_GetAttrString(hotReload, "configure_dev_environment"),
|
|
206
|
+
let docsObj = PyUnicode_FromString(documents),
|
|
207
|
+
let devArgs = PyTuple_New(1)
|
|
208
|
+
else {
|
|
209
|
+
throw PythonRuntimeError.startup(
|
|
210
|
+
"Could not import pythonnative: \(Self.consumeError())\n"
|
|
211
|
+
+ "The pythonnative package must be staged into app_packages by the pn CLI."
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
defer { Py_DecRef(hotReload) }
|
|
215
|
+
defer { Py_DecRef(configure) }
|
|
216
|
+
PyTuple_SetItem(devArgs, 0, docsObj) // steals docsObj
|
|
217
|
+
defer { Py_DecRef(devArgs) }
|
|
218
|
+
if let devResult = PyObject_CallObject(configure, devArgs) {
|
|
219
|
+
Py_DecRef(devResult)
|
|
220
|
+
} else {
|
|
221
|
+
throw PythonRuntimeError.startup("Hot-reload setup failed: \(Self.consumeError())")
|
|
222
|
+
}
|
|
223
|
+
#endif
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// MARK: - Calling into Python
|
|
227
|
+
|
|
228
|
+
/// Import `module` and call its `function` with the given arguments.
|
|
229
|
+
/// Supported argument types: String, Int, UInt, Bool, Double.
|
|
230
|
+
@discardableResult
|
|
231
|
+
func call(module: String, function: String, _ args: Any...) throws -> PyRef {
|
|
232
|
+
try callList(module: module, function: function, args)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
@discardableResult
|
|
236
|
+
func callList(module: String, function: String, _ args: [Any]) throws -> PyRef {
|
|
237
|
+
guard started else {
|
|
238
|
+
throw PythonRuntimeError.call("Python is not running (startup failed or never happened).")
|
|
239
|
+
}
|
|
240
|
+
let gil = PyGILState_Ensure()
|
|
241
|
+
defer { PyGILState_Release(gil) }
|
|
242
|
+
guard let moduleObj = PyImport_ImportModule(module) else {
|
|
243
|
+
throw PythonRuntimeError.call("Could not import \(module): \(Self.consumeError())")
|
|
244
|
+
}
|
|
245
|
+
defer { Py_DecRef(moduleObj) }
|
|
246
|
+
guard let fnObj = PyObject_GetAttrString(moduleObj, function) else {
|
|
247
|
+
throw PythonRuntimeError.call("\(module) has no attribute '\(function)': \(Self.consumeError())")
|
|
248
|
+
}
|
|
249
|
+
defer { Py_DecRef(fnObj) }
|
|
250
|
+
return try Self.invoke(fnObj, args: args, context: "\(module).\(function)")
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/// Like `call`, but logs failures instead of throwing. For host
|
|
254
|
+
/// notifications where the app must keep running.
|
|
255
|
+
func notify(module: String, function: String, _ args: Any...) {
|
|
256
|
+
do {
|
|
257
|
+
_ = try callList(module: module, function: function, args)
|
|
258
|
+
} catch {
|
|
259
|
+
NSLog("[PN] \(module).\(function) failed: \(error)")
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/// Shared invocation helper. The caller must hold the GIL.
|
|
264
|
+
fileprivate static func invoke(
|
|
265
|
+
_ callable: UnsafeMutablePointer<PyObject>,
|
|
266
|
+
args: [Any],
|
|
267
|
+
context: String
|
|
268
|
+
) throws -> PyRef {
|
|
269
|
+
guard let tuple = PyTuple_New(args.count) else {
|
|
270
|
+
throw PythonRuntimeError.call("Could not allocate arguments for \(context).")
|
|
271
|
+
}
|
|
272
|
+
for (index, arg) in args.enumerated() {
|
|
273
|
+
guard let converted = convert(arg) else {
|
|
274
|
+
Py_DecRef(tuple)
|
|
275
|
+
throw PythonRuntimeError.call("Unsupported argument type \(type(of: arg)) for \(context).")
|
|
276
|
+
}
|
|
277
|
+
PyTuple_SetItem(tuple, index, converted) // steals converted
|
|
278
|
+
}
|
|
279
|
+
defer { Py_DecRef(tuple) }
|
|
280
|
+
guard let result = PyObject_CallObject(callable, tuple) else {
|
|
281
|
+
throw PythonRuntimeError.call("\(context) raised: \(consumeError())")
|
|
282
|
+
}
|
|
283
|
+
return PyRef(owned: result)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private static func convert(_ value: Any) -> UnsafeMutablePointer<PyObject>? {
|
|
287
|
+
switch value {
|
|
288
|
+
case let string as String:
|
|
289
|
+
return PyUnicode_FromString(string)
|
|
290
|
+
case let flag as Bool:
|
|
291
|
+
return PyBool_FromLong(flag ? 1 : 0)
|
|
292
|
+
case let unsigned as UInt:
|
|
293
|
+
return PyLong_FromUnsignedLongLong(UInt64(unsigned))
|
|
294
|
+
case let integer as Int:
|
|
295
|
+
return PyLong_FromLongLong(Int64(integer))
|
|
296
|
+
case let double as Double:
|
|
297
|
+
return PyFloat_FromDouble(double)
|
|
298
|
+
default:
|
|
299
|
+
return nil
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// MARK: - Deep links
|
|
304
|
+
|
|
305
|
+
/// Deliver a deep-link URL to the Python side, buffering it until
|
|
306
|
+
/// the interpreter is running.
|
|
307
|
+
func deliverURL(_ url: String) {
|
|
308
|
+
guard started else {
|
|
309
|
+
pendingURLs.append(url)
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
notify(module: "pythonnative.native_modules.linking", function: "dispatch_url", url)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private func flushPendingURLs() {
|
|
316
|
+
let urls = pendingURLs
|
|
317
|
+
pendingURLs = []
|
|
318
|
+
for url in urls {
|
|
319
|
+
notify(module: "pythonnative.native_modules.linking", function: "dispatch_url", url)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// MARK: - Error formatting
|
|
324
|
+
|
|
325
|
+
/// Format and clear the pending Python exception (caller holds the GIL).
|
|
326
|
+
fileprivate static func consumeError() -> String {
|
|
327
|
+
var ptype: UnsafeMutablePointer<PyObject>? = nil
|
|
328
|
+
var pvalue: UnsafeMutablePointer<PyObject>? = nil
|
|
329
|
+
var ptrace: UnsafeMutablePointer<PyObject>? = nil
|
|
330
|
+
PyErr_Fetch(&ptype, &pvalue, &ptrace)
|
|
331
|
+
PyErr_NormalizeException(&ptype, &pvalue, &ptrace)
|
|
332
|
+
defer {
|
|
333
|
+
Py_DecRef(ptype)
|
|
334
|
+
Py_DecRef(pvalue)
|
|
335
|
+
Py_DecRef(ptrace)
|
|
336
|
+
}
|
|
337
|
+
guard ptype != nil else { return "unknown Python error" }
|
|
338
|
+
|
|
339
|
+
if let traceback = PyImport_ImportModule("traceback") {
|
|
340
|
+
defer { Py_DecRef(traceback) }
|
|
341
|
+
let fnName = ptrace != nil ? "format_exception" : "format_exception_only"
|
|
342
|
+
if let formatFn = PyObject_GetAttrString(traceback, fnName) {
|
|
343
|
+
defer { Py_DecRef(formatFn) }
|
|
344
|
+
let count = ptrace != nil ? 3 : 2
|
|
345
|
+
if let tuple = PyTuple_New(count) {
|
|
346
|
+
Py_IncRef(ptype)
|
|
347
|
+
PyTuple_SetItem(tuple, 0, ptype)
|
|
348
|
+
Py_IncRef(pvalue)
|
|
349
|
+
PyTuple_SetItem(tuple, 1, pvalue)
|
|
350
|
+
if count == 3 {
|
|
351
|
+
Py_IncRef(ptrace)
|
|
352
|
+
PyTuple_SetItem(tuple, 2, ptrace)
|
|
353
|
+
}
|
|
354
|
+
defer { Py_DecRef(tuple) }
|
|
355
|
+
if let lines = PyObject_CallObject(formatFn, tuple) {
|
|
356
|
+
defer { Py_DecRef(lines) }
|
|
357
|
+
if let empty = PyUnicode_FromString(""),
|
|
358
|
+
let joined = PyUnicode_Join(empty, lines)
|
|
359
|
+
{
|
|
360
|
+
Py_DecRef(empty)
|
|
361
|
+
defer { Py_DecRef(joined) }
|
|
362
|
+
if let utf8 = PyUnicode_AsUTF8(joined) {
|
|
363
|
+
return String(cString: utf8)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Fallback: str(value).
|
|
371
|
+
if let value = pvalue, let str = PyObject_Str(value) {
|
|
372
|
+
defer { Py_DecRef(str) }
|
|
373
|
+
if let utf8 = PyUnicode_AsUTF8(str) {
|
|
374
|
+
return String(cString: utf8)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return "unknown Python error"
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private static func statusMessage(_ status: PyStatus) -> String {
|
|
381
|
+
if let errMsg = status.err_msg {
|
|
382
|
+
return String(cString: errMsg)
|
|
383
|
+
}
|
|
384
|
+
return "unknown error"
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/// Exported for the Python side: `pythonnative.runtime` wakes the guest
|
|
389
|
+
/// asyncio loop by resolving this symbol via `ctypes.CDLL(None)` and
|
|
390
|
+
/// calling it whenever loop work is scheduled.
|
|
391
|
+
@_cdecl("pn_schedule_render_drain")
|
|
392
|
+
public func pn_schedule_render_drain() {
|
|
393
|
+
DispatchQueue.main.async {
|
|
394
|
+
guard PythonRuntime.shared.started else { return }
|
|
395
|
+
PythonRuntime.shared.notify(module: "pythonnative.screen", function: "drain_ios_scheduled_renders")
|
|
396
|
+
}
|
|
397
|
+
}
|
|
@@ -2,18 +2,27 @@
|
|
|
2
2
|
// SceneDelegate.swift
|
|
3
3
|
// ios_template
|
|
4
4
|
//
|
|
5
|
-
//
|
|
5
|
+
// Creates the window programmatically and forwards scene-level events
|
|
6
|
+
// (deep links, foreground/background transitions) to the Python side.
|
|
6
7
|
//
|
|
7
8
|
|
|
8
9
|
import UIKit
|
|
9
10
|
|
|
10
11
|
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|
11
|
-
|
|
12
12
|
var window: UIWindow?
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
func scene(
|
|
15
|
+
_ scene: UIScene,
|
|
16
|
+
willConnectTo session: UISceneSession,
|
|
17
|
+
options connectionOptions: UIScene.ConnectionOptions
|
|
18
|
+
) {
|
|
16
19
|
guard let windowScene = (scene as? UIWindowScene) else { return }
|
|
20
|
+
// A cold start from a deep link delivers the URL here, before
|
|
21
|
+
// Python is running; PythonRuntime buffers it and flushes after
|
|
22
|
+
// startup so Linking.get_initial_url() sees it.
|
|
23
|
+
for context in connectionOptions.urlContexts {
|
|
24
|
+
PythonRuntime.shared.deliverURL(context.url.absoluteString)
|
|
25
|
+
}
|
|
17
26
|
let window = UIWindow(windowScene: windowScene)
|
|
18
27
|
let root = ViewController()
|
|
19
28
|
let nav = UINavigationController(rootViewController: root)
|
|
@@ -22,34 +31,36 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|
|
22
31
|
window.makeKeyAndVisible()
|
|
23
32
|
}
|
|
24
33
|
|
|
25
|
-
func
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
|
|
35
|
+
for context in URLContexts {
|
|
36
|
+
PythonRuntime.shared.deliverURL(context.url.absoluteString)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// MARK: - AppState forwarding
|
|
41
|
+
|
|
42
|
+
private func dispatchAppState(_ state: String) {
|
|
43
|
+
guard PythonRuntime.shared.started else { return }
|
|
44
|
+
PythonRuntime.shared.notify(
|
|
45
|
+
module: "pythonnative.native_modules.app_state", function: "dispatch_app_state", state
|
|
46
|
+
)
|
|
30
47
|
}
|
|
31
48
|
|
|
32
49
|
func sceneDidBecomeActive(_ scene: UIScene) {
|
|
33
|
-
|
|
34
|
-
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
|
|
50
|
+
dispatchAppState("active")
|
|
35
51
|
}
|
|
36
52
|
|
|
37
53
|
func sceneWillResignActive(_ scene: UIScene) {
|
|
38
|
-
|
|
39
|
-
// This may occur due to temporary interruptions (ex. an incoming phone call).
|
|
54
|
+
dispatchAppState("inactive")
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
func sceneWillEnterForeground(_ scene: UIScene) {
|
|
43
|
-
|
|
44
|
-
// Use this method to undo the changes made on entering the background.
|
|
58
|
+
dispatchAppState("inactive")
|
|
45
59
|
}
|
|
46
60
|
|
|
47
61
|
func sceneDidEnterBackground(_ scene: UIScene) {
|
|
48
|
-
|
|
49
|
-
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
|
50
|
-
// to restore the scene back to its current state.
|
|
62
|
+
dispatchAppState("background")
|
|
51
63
|
}
|
|
52
64
|
|
|
53
|
-
|
|
65
|
+
func sceneDidDisconnect(_ scene: UIScene) {}
|
|
54
66
|
}
|
|
55
|
-
|