pythonnative 0.2.0__py3-none-any.whl → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  from importlib import import_module
2
2
  from typing import Any, Dict
3
3
 
4
- __version__ = "0.2.0"
4
+ __version__ = "0.3.0"
5
5
 
6
6
  __all__ = [
7
7
  "ActivityIndicatorView",
pythonnative/cli/pn.py CHANGED
@@ -41,7 +41,7 @@ def init_project(args: argparse.Namespace) -> None:
41
41
 
42
42
  os.makedirs(app_dir, exist_ok=True)
43
43
 
44
- # Minimal hello world app scaffold
44
+ # Minimal hello world app scaffold (no bootstrap function; host instantiates Page directly)
45
45
  main_page_py = os.path.join(app_dir, "main_page.py")
46
46
  if not os.path.exists(main_page_py) or args.force:
47
47
  with open(main_page_py, "w", encoding="utf-8") as f:
@@ -61,13 +61,6 @@ class MainPage(pn.Page):
61
61
  button.set_on_click(lambda: print("Button clicked"))
62
62
  stack.add_view(button)
63
63
  self.set_root_view(stack)
64
-
65
-
66
- def bootstrap(native_instance):
67
- '''Entry point called by the host app (Android Activity or iOS ViewController).'''
68
- page = MainPage(native_instance)
69
- page.on_create()
70
- return page
71
64
  """
72
65
  )
73
66
 
pythonnative/page.py CHANGED
@@ -29,7 +29,9 @@ Just ensure that your PythonNative UI framework is aware of these platform
29
29
  differences and handles them appropriately.
30
30
  """
31
31
 
32
+ import json
32
33
  from abc import ABC, abstractmethod
34
+ from typing import Any, Optional, Union
33
35
 
34
36
  from .utils import IS_ANDROID, set_android_context
35
37
  from .view import ViewBase
@@ -85,7 +87,25 @@ class PageBase(ABC):
85
87
  pass
86
88
 
87
89
  @abstractmethod
90
+ def set_args(self, args: Optional[dict]) -> None:
91
+ pass
92
+
93
+ @abstractmethod
94
+ def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
95
+ pass
96
+
97
+ @abstractmethod
98
+ def pop(self) -> None:
99
+ pass
100
+
101
+ def get_args(self) -> dict:
102
+ """Return arguments provided to this Page (empty dict if none)."""
103
+ # Concrete classes should set self._args; default empty
104
+ return getattr(self, "_args", {})
105
+
106
+ # Back-compat: navigate_to delegates to push
88
107
  def navigate_to(self, page) -> None:
108
+ self.push(page)
89
109
  pass
90
110
 
91
111
 
@@ -105,9 +125,23 @@ if IS_ANDROID:
105
125
  # self.native_instance = self.native_class()
106
126
  # Stash the Activity so child views can implicitly acquire a Context
107
127
  set_android_context(native_instance)
128
+ self._args: dict = {}
108
129
 
109
130
  def set_root_view(self, view) -> None:
110
- self.native_instance.setContentView(view.native_instance)
131
+ # In fragment-based navigation, attach child view to the current fragment container.
132
+ try:
133
+ from .utils import get_android_fragment_container
134
+
135
+ container = get_android_fragment_container()
136
+ # Remove previous children if any, then add the new root
137
+ try:
138
+ container.removeAllViews()
139
+ except Exception:
140
+ pass
141
+ container.addView(view.native_instance)
142
+ except Exception:
143
+ # Fallback to setting content view directly on the Activity
144
+ self.native_instance.setContentView(view.native_instance)
111
145
 
112
146
  def on_create(self) -> None:
113
147
  print("Android on_create() called")
@@ -136,13 +170,53 @@ if IS_ANDROID:
136
170
  def on_restore_instance_state(self) -> None:
137
171
  print("Android on_restore_instance_state() called")
138
172
 
139
- def navigate_to(self, page) -> None:
140
- # intent = jclass("android.content.Intent")(self.native_instance, page.native_class)
141
- intent = jclass("android.content.Intent")(
142
- self.native_instance,
143
- jclass("com.pythonnative.pythonnative.SecondActivity"),
144
- )
145
- self.native_instance.startActivity(intent)
173
+ def set_args(self, args: Optional[dict]) -> None:
174
+ # Accept dict or JSON string for convenience when crossing language boundaries
175
+ if isinstance(args, str):
176
+ try:
177
+ self._args = json.loads(args) or {}
178
+ return
179
+ except Exception:
180
+ self._args = {}
181
+ return
182
+ self._args = args or {}
183
+
184
+ def _resolve_page_path(self, page: Union[str, Any]) -> str:
185
+ if isinstance(page, str):
186
+ return page
187
+ # If a class or instance is passed, derive dotted path
188
+ try:
189
+ module = getattr(page, "__module__", None)
190
+ name = getattr(page, "__name__", None)
191
+ if module and name:
192
+ return f"{module}.{name}"
193
+ # Instance: use its class
194
+ cls = page.__class__
195
+ return f"{cls.__module__}.{cls.__name__}"
196
+ except Exception:
197
+ raise ValueError("Unsupported page reference; expected dotted string or class/instance")
198
+
199
+ def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
200
+ # Delegate to Navigator.push to navigate to PageFragment with arguments
201
+ page_path = self._resolve_page_path(page)
202
+ try:
203
+ Navigator = jclass(f"{self.native_instance.getPackageName()}.Navigator")
204
+ args_json = json.dumps(args) if args else None
205
+ Navigator.push(self.native_instance, page_path, args_json)
206
+ except Exception:
207
+ # As a last resort, do nothing rather than crash
208
+ pass
209
+
210
+ def pop(self) -> None:
211
+ # Delegate to Navigator.pop for back-stack pop
212
+ try:
213
+ Navigator = jclass(f"{self.native_instance.getPackageName()}.Navigator")
214
+ Navigator.pop(self.native_instance)
215
+ except Exception:
216
+ try:
217
+ self.native_instance.finish()
218
+ except Exception:
219
+ pass
146
220
 
147
221
  else:
148
222
  # ========================================
@@ -150,8 +224,45 @@ else:
150
224
  # https://developer.apple.com/documentation/uikit/uiviewcontroller
151
225
  # ========================================
152
226
 
227
+ from typing import Dict
228
+
153
229
  from rubicon.objc import ObjCClass, ObjCInstance
154
230
 
231
+ # Global registry mapping native UIViewController pointer address to Page instances.
232
+ _IOS_PAGE_REGISTRY: Dict[int, Any] = {}
233
+
234
+ def _ios_register_page(vc_instance: Any, page_obj: Any) -> None:
235
+ try:
236
+ ptr = int(vc_instance.ptr) # rubicon ObjCInstance -> c_void_p convertible to int
237
+ _IOS_PAGE_REGISTRY[ptr] = page_obj
238
+ except Exception:
239
+ pass
240
+
241
+ def _ios_unregister_page(vc_instance: Any) -> None:
242
+ try:
243
+ ptr = int(vc_instance.ptr)
244
+ _IOS_PAGE_REGISTRY.pop(ptr, None)
245
+ except Exception:
246
+ pass
247
+
248
+ def forward_lifecycle(native_addr: int, event: str) -> None:
249
+ """Forward a lifecycle event from Swift ViewController to the registered Page.
250
+
251
+ :param native_addr: Integer pointer address of the UIViewController
252
+ :param event: One of 'on_start', 'on_resume', 'on_pause', 'on_stop', 'on_destroy',
253
+ 'on_save_instance_state', 'on_restore_instance_state'.
254
+ """
255
+ page = _IOS_PAGE_REGISTRY.get(int(native_addr))
256
+ if not page:
257
+ return
258
+ try:
259
+ handler = getattr(page, event, None)
260
+ if handler:
261
+ handler()
262
+ except Exception:
263
+ # Avoid surfacing exceptions across the Swift/Python boundary in lifecycle
264
+ pass
265
+
155
266
  class Page(PageBase, ViewBase):
156
267
  def __init__(self, native_instance) -> None:
157
268
  super().__init__()
@@ -164,6 +275,10 @@ else:
164
275
  native_instance = None
165
276
  self.native_instance = native_instance
166
277
  # self.native_instance = self.native_class.alloc().init()
278
+ self._args: dict = {}
279
+ # Register for lifecycle forwarding
280
+ if self.native_instance is not None:
281
+ _ios_register_page(self.native_instance, self)
167
282
 
168
283
  def set_root_view(self, view) -> None:
169
284
  # UIViewController.view is a property; access without calling.
@@ -195,6 +310,8 @@ else:
195
310
 
196
311
  def on_destroy(self) -> None:
197
312
  print("iOS on_destroy() called")
313
+ if self.native_instance is not None:
314
+ _ios_unregister_page(self.native_instance)
198
315
 
199
316
  def on_restart(self) -> None:
200
317
  print("iOS on_restart() called")
@@ -205,5 +322,75 @@ else:
205
322
  def on_restore_instance_state(self) -> None:
206
323
  print("iOS on_restore_instance_state() called")
207
324
 
208
- def navigate_to(self, page) -> None:
209
- self.native_instance.navigationController().pushViewControllerAnimated_(page.native_instance, True)
325
+ def set_args(self, args: Optional[dict]) -> None:
326
+ if isinstance(args, str):
327
+ try:
328
+ self._args = json.loads(args) or {}
329
+ return
330
+ except Exception:
331
+ self._args = {}
332
+ return
333
+ self._args = args or {}
334
+
335
+ def _resolve_page_path(self, page: Union[str, Any]) -> str:
336
+ if isinstance(page, str):
337
+ return page
338
+ try:
339
+ module = getattr(page, "__module__", None)
340
+ name = getattr(page, "__name__", None)
341
+ if module and name:
342
+ return f"{module}.{name}"
343
+ cls = page.__class__
344
+ return f"{cls.__module__}.{cls.__name__}"
345
+ except Exception:
346
+ raise ValueError("Unsupported page reference; expected dotted string or class/instance")
347
+
348
+ def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
349
+ page_path = self._resolve_page_path(page)
350
+ # Resolve the Swift ViewController class. Swift classes are namespaced by
351
+ # the module name (CFBundleName). Try plain name first, then Module.Name.
352
+ ViewController = None
353
+ try:
354
+ ViewController = ObjCClass("ViewController")
355
+ except Exception:
356
+ try:
357
+ NSBundle = ObjCClass("NSBundle")
358
+ bundle = NSBundle.mainBundle
359
+ module_name = None
360
+ try:
361
+ # Prefer CFBundleName; fallback to CFBundleExecutable
362
+ module_name = bundle.objectForInfoDictionaryKey_("CFBundleName")
363
+ if module_name is None:
364
+ module_name = bundle.objectForInfoDictionaryKey_("CFBundleExecutable")
365
+ except Exception:
366
+ module_name = None
367
+ if module_name:
368
+ ViewController = ObjCClass(f"{module_name}.ViewController")
369
+ except Exception:
370
+ ViewController = None
371
+
372
+ if ViewController is None:
373
+ raise NameError("ViewController class not found; ensure Swift class is ObjC-visible")
374
+
375
+ next_vc = ViewController.alloc().init()
376
+ try:
377
+ # Use KVC to pass metadata to Swift
378
+ next_vc.setValue_forKey_(page_path, "requestedPagePath")
379
+ if args:
380
+ next_vc.setValue_forKey_(json.dumps(args), "requestedPageArgsJSON")
381
+ except Exception:
382
+ pass
383
+ # On iOS, `navigationController` is exposed as a property; treat it as such.
384
+ nav = getattr(self.native_instance, "navigationController", None)
385
+ if nav is None:
386
+ # If no navigation controller, this push will be a no-op; rely on template to embed one.
387
+ raise RuntimeError(
388
+ "No UINavigationController available; ensure template embeds root in navigation controller"
389
+ )
390
+ # Method name maps from pushViewController:animated:
391
+ nav.pushViewController_animated_(next_vc, True)
392
+
393
+ def pop(self) -> None:
394
+ nav = getattr(self.native_instance, "navigationController", None)
395
+ if nav is not None:
396
+ nav.popViewControllerAnimated_(True)
@@ -53,6 +53,9 @@ dependencies {
53
53
  implementation 'androidx.appcompat:appcompat:1.4.1'
54
54
  implementation 'com.google.android.material:material:1.5.0'
55
55
  implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
56
+ // AndroidX Navigation for Fragment-based navigation
57
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
58
+ implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
56
59
  testImplementation 'junit:junit:4.13.2'
57
60
  androidTestImplementation 'androidx.test.ext:junit:1.1.3'
58
61
  androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
@@ -8,25 +8,28 @@ import com.chaquo.python.Python
8
8
  import com.chaquo.python.android.AndroidPlatform
9
9
 
10
10
  class MainActivity : AppCompatActivity() {
11
+ private val TAG = javaClass.simpleName
12
+
11
13
  override fun onCreate(savedInstanceState: Bundle?) {
12
14
  super.onCreate(savedInstanceState)
13
- // setContentView(R.layout.activity_main)
15
+ Log.d(TAG, "onCreate() called")
14
16
 
15
17
  // Initialize Chaquopy
16
18
  if (!Python.isStarted()) {
17
19
  Python.start(AndroidPlatform(this))
18
20
  }
19
21
  try {
22
+ // Set content view to the NavHost layout; the initial page loads via nav_graph startDestination
23
+ setContentView(R.layout.activity_main)
24
+ // Optionally, bootstrap Python so first fragment can create the initial page onCreate
20
25
  val py = Python.getInstance()
21
- val pyModule = py.getModule("app.main_page")
22
- pyModule.callAttr("bootstrap", this)
23
- // Python Page will set the content view via set_root_view
26
+ // Touch module to ensure bundled Python code is available; actual instantiation happens in PageFragment
27
+ py.getModule("app.main_page")
24
28
  } catch (e: Exception) {
25
- Log.e("PythonNative", "Python bootstrap failed", e)
26
- // Fallback: show a simple native label if Python bootstrap fails
29
+ Log.e("PythonNative", "Bootstrap failed", e)
27
30
  val tv = TextView(this)
28
31
  tv.text = "Hello from PythonNative (Android template)"
29
32
  setContentView(tv)
30
33
  }
31
34
  }
32
- }
35
+ }
@@ -0,0 +1,26 @@
1
+ package com.pythonnative.android_template
2
+
3
+ import android.os.Bundle
4
+ import androidx.core.os.bundleOf
5
+ import androidx.fragment.app.FragmentActivity
6
+ import androidx.navigation.fragment.NavHostFragment
7
+
8
+ object Navigator {
9
+ @JvmStatic
10
+ fun push(activity: FragmentActivity, pagePath: String, argsJson: String?) {
11
+ val navHost = activity.supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
12
+ val navController = navHost.navController
13
+ val args = Bundle()
14
+ args.putString("page_path", pagePath)
15
+ if (argsJson != null) {
16
+ args.putString("args_json", argsJson)
17
+ }
18
+ navController.navigate(R.id.pageFragment, args)
19
+ }
20
+
21
+ @JvmStatic
22
+ fun pop(activity: FragmentActivity) {
23
+ val navHost = activity.supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
24
+ navHost.navController.popBackStack()
25
+ }
26
+ }
@@ -0,0 +1,111 @@
1
+ package com.pythonnative.android_template
2
+
3
+ import android.os.Bundle
4
+ import android.util.Log
5
+ import android.view.LayoutInflater
6
+ import android.view.View
7
+ import android.view.ViewGroup
8
+ import android.widget.FrameLayout
9
+ import androidx.core.os.bundleOf
10
+ import androidx.fragment.app.Fragment
11
+ import com.chaquo.python.PyObject
12
+ import com.chaquo.python.Python
13
+ import com.chaquo.python.android.AndroidPlatform
14
+
15
+ class PageFragment : Fragment() {
16
+ private val TAG = javaClass.simpleName
17
+ private var page: PyObject? = null
18
+
19
+ override fun onCreate(savedInstanceState: Bundle?) {
20
+ super.onCreate(savedInstanceState)
21
+ if (!Python.isStarted()) {
22
+ context?.let { Python.start(AndroidPlatform(it)) }
23
+ }
24
+ try {
25
+ val py = Python.getInstance()
26
+ val pagePath = arguments?.getString("page_path") ?: "app.main_page.MainPage"
27
+ val argsJson = arguments?.getString("args_json")
28
+ val moduleName = pagePath.substringBeforeLast('.')
29
+ val className = pagePath.substringAfterLast('.')
30
+ val pyModule = py.getModule(moduleName)
31
+ val pageClass = pyModule.get(className)
32
+ // Pass the hosting Activity as native_instance for context
33
+ page = pageClass?.call(requireActivity())
34
+ if (!argsJson.isNullOrEmpty()) {
35
+ page?.callAttr("set_args", argsJson)
36
+ }
37
+ } catch (e: Exception) {
38
+ Log.e(TAG, "Failed to instantiate page", e)
39
+ }
40
+ }
41
+
42
+ override fun onCreateView(
43
+ inflater: LayoutInflater,
44
+ container: ViewGroup?,
45
+ savedInstanceState: Bundle?
46
+ ): View? {
47
+ // Create a simple container which Python-native views can be attached to.
48
+ val frame = FrameLayout(requireContext())
49
+ frame.layoutParams = ViewGroup.LayoutParams(
50
+ ViewGroup.LayoutParams.MATCH_PARENT,
51
+ ViewGroup.LayoutParams.MATCH_PARENT
52
+ )
53
+ return frame
54
+ }
55
+
56
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
57
+ super.onViewCreated(view, savedInstanceState)
58
+ // Python side will call set_root_view to attach a native view to Activity.
59
+ // In fragment-based architecture, the Activity will set contentView once,
60
+ // so we ensure the fragment's container is available for Python to target.
61
+ // Expose the fragment container to Python so Page.set_root_view can attach into it
62
+ try {
63
+ val py = Python.getInstance()
64
+ val utils = py.getModule("pythonnative.utils")
65
+ utils.callAttr("set_android_fragment_container", view)
66
+ // Now that container exists, invoke on_create so Python can attach its root view
67
+ page?.callAttr("on_create")
68
+ } catch (_: Exception) {
69
+ }
70
+ }
71
+
72
+ override fun onStart() {
73
+ super.onStart()
74
+ try { page?.callAttr("on_start") } catch (e: Exception) { Log.w(TAG, "on_start failed", e) }
75
+ }
76
+
77
+ override fun onResume() {
78
+ super.onResume()
79
+ try { page?.callAttr("on_resume") } catch (e: Exception) { Log.w(TAG, "on_resume failed", e) }
80
+ }
81
+
82
+ override fun onPause() {
83
+ super.onPause()
84
+ try { page?.callAttr("on_pause") } catch (e: Exception) { Log.w(TAG, "on_pause failed", e) }
85
+ }
86
+
87
+ override fun onStop() {
88
+ super.onStop()
89
+ try { page?.callAttr("on_stop") } catch (e: Exception) { Log.w(TAG, "on_stop failed", e) }
90
+ }
91
+
92
+ override fun onDestroyView() {
93
+ super.onDestroyView()
94
+ }
95
+
96
+ override fun onDestroy() {
97
+ super.onDestroy()
98
+ try { page?.callAttr("on_destroy") } catch (e: Exception) { Log.w(TAG, "on_destroy failed", e) }
99
+ }
100
+
101
+ companion object {
102
+ fun newInstance(pagePath: String, argsJson: String?): PageFragment {
103
+ val f = PageFragment()
104
+ f.arguments = bundleOf(
105
+ "page_path" to pagePath,
106
+ "args_json" to argsJson
107
+ )
108
+ return f
109
+ }
110
+ }
111
+ }
@@ -1,18 +1,10 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
- <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
2
+ <androidx.fragment.app.FragmentContainerView
3
+ xmlns:android="http://schemas.android.com/apk/res/android"
3
4
  xmlns:app="http://schemas.android.com/apk/res-auto"
4
- xmlns:tools="http://schemas.android.com/tools"
5
+ android:id="@+id/nav_host_fragment"
6
+ android:name="androidx.navigation.fragment.NavHostFragment"
5
7
  android:layout_width="match_parent"
6
8
  android:layout_height="match_parent"
7
- tools:context=".MainActivity">
8
-
9
- <TextView
10
- android:layout_width="wrap_content"
11
- android:layout_height="wrap_content"
12
- android:text="Hello World!"
13
- app:layout_constraintBottom_toBottomOf="parent"
14
- app:layout_constraintEnd_toEndOf="parent"
15
- app:layout_constraintStart_toStartOf="parent"
16
- app:layout_constraintTop_toTopOf="parent" />
17
-
18
- </androidx.constraintlayout.widget.ConstraintLayout>
9
+ app:defaultNavHost="true"
10
+ app:navGraph="@navigation/nav_graph" />
@@ -0,0 +1,22 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <navigation xmlns:android="http://schemas.android.com/apk/res/android"
3
+ xmlns:app="http://schemas.android.com/apk/res-auto"
4
+ xmlns:tools="http://schemas.android.com/tools"
5
+ android:id="@+id/nav_graph"
6
+ app:startDestination="@id/pageFragment">
7
+
8
+ <fragment
9
+ android:id="@+id/pageFragment"
10
+ android:name="com.pythonnative.android_template.PageFragment"
11
+ android:label="PageFragment">
12
+ <argument
13
+ android:name="page_path"
14
+ app:argType="string"
15
+ android:defaultValue="app.main_page.MainPage" />
16
+ <argument
17
+ android:name="args_json"
18
+ app:argType="string"
19
+ android:nullable="true" />
20
+ </fragment>
21
+
22
+ </navigation>
@@ -13,10 +13,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
13
13
 
14
14
 
15
15
  func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
16
- // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
17
- // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
18
- // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
19
- guard let _ = (scene as? UIWindowScene) else { return }
16
+ guard let windowScene = (scene as? UIWindowScene) else { return }
17
+ let window = UIWindow(windowScene: windowScene)
18
+ let root = ViewController()
19
+ let nav = UINavigationController(rootViewController: root)
20
+ window.rootViewController = nav
21
+ self.window = window
22
+ window.makeKeyAndVisible()
20
23
  }
21
24
 
22
25
  func sceneDidDisconnect(_ scene: UIScene) {
@@ -16,9 +16,17 @@ import Python
16
16
  #endif
17
17
 
18
18
  class ViewController: UIViewController {
19
+ // Ensure Python.framework is configured only once per process
20
+ private static var hasInitializedPython: Bool = false
21
+ // Optional keys for dynamic page navigation
22
+ @objc dynamic var requestedPagePath: String? = nil
23
+ @objc dynamic var requestedPageArgsJSON: String? = nil
24
+ private var pythonReady: Bool = false
19
25
 
20
26
  override func viewDidLoad() {
21
27
  super.viewDidLoad()
28
+ // Ensure a visible background when created programmatically (storyboards set this automatically)
29
+ view.backgroundColor = .systemBackground
22
30
  NSLog("[PN][ViewController] viewDidLoad")
23
31
  if let bundleId = Bundle.main.bundleIdentifier {
24
32
  NSLog("[PN] Bundle Identifier: \(bundleId)")
@@ -45,14 +53,19 @@ class ViewController: UIViewController {
45
53
  let frameworkLib = "\(bundlePath)/Frameworks/Python.framework/Python"
46
54
  setenv("PYTHON_LIBRARY", frameworkLib, 1)
47
55
  if FileManager.default.fileExists(atPath: frameworkLib) {
48
- NSLog("[PN] Using embedded Python lib at: \(frameworkLib)")
49
- PythonLibrary.useLibrary(at: frameworkLib)
56
+ if !ViewController.hasInitializedPython {
57
+ NSLog("[PN] Using embedded Python lib at: \(frameworkLib)")
58
+ PythonLibrary.useLibrary(at: frameworkLib)
59
+ ViewController.hasInitializedPython = true
60
+ } else {
61
+ NSLog("[PN] Python library already initialized; skipping useLibrary")
62
+ }
63
+ pythonReady = true
50
64
  } else {
51
65
  NSLog("[PN] Embedded Python library not found at: \(frameworkLib)")
52
66
  }
53
67
  }
54
- NSLog("[PN] PythonKit available; attempting Python bootstrap of app.main_page.bootstrap(self)")
55
- // Attempt Python bootstrap of app.main_page.bootstrap(self)
68
+ NSLog("[PN] PythonKit available; attempting Python bootstrap")
56
69
  let sys = Python.import("sys")
57
70
  NSLog("[PN] Python version: \(sys.version)")
58
71
  NSLog("[PN] Initial sys.path: \(sys.path)")
@@ -69,38 +82,35 @@ class ViewController: UIViewController {
69
82
  NSLog("[PN] Could not list contents of \(appDir).")
70
83
  }
71
84
  }
85
+ // Determine which Python page to load
86
+ let pagePath: String = requestedPagePath ?? "app.main_page.MainPage"
72
87
  do {
73
- let app = try Python.attemptImport("app.main_page")
74
- let pyNone = Python.None
88
+ let moduleName = String(pagePath.split(separator: ".").dropLast().joined(separator: "."))
89
+ let className = String(pagePath.split(separator: ".").last ?? "MainPage")
90
+ let pyModule = try Python.attemptImport(moduleName)
91
+ // Resolve class by name via builtins.getattr to avoid subscripting issues
75
92
  let builtins = Python.import("builtins")
76
93
  let getattrFn = builtins.getattr
77
- let bootstrap = try getattrFn.throwing.dynamicallyCall(withArguments: [app, "bootstrap", pyNone])
78
- if bootstrap != Python.None {
94
+ let pageClass = try getattrFn.throwing.dynamicallyCall(withArguments: [pyModule, className])
95
+ // Pass native pointer so Python Page can wrap via rubicon.objc
96
+ let ptr = Unmanaged.passUnretained(self).toOpaque()
97
+ let addr = UInt(bitPattern: ptr)
98
+ let page = try pageClass.throwing.dynamicallyCall(withArguments: [addr])
99
+ // If args provided, pass into Page via set_args(dict)
100
+ if let jsonStr = requestedPageArgsJSON {
101
+ let json = Python.import("json")
79
102
  do {
80
- let isCallable = try Python.callable.throwing.dynamicallyCall(withArguments: [bootstrap])
81
- if Bool(isCallable) == true {
82
- // Pass the native UIViewController pointer into Python so it can be wrapped by rubicon.objc
83
- let ptr = Unmanaged.passUnretained(self).toOpaque()
84
- let addr = UInt(bitPattern: ptr)
85
- NSLog("[PN] Passing native UIViewController pointer to Python: 0x%llx", addr)
86
- _ = try bootstrap.throwing.dynamicallyCall(withArguments: [addr])
87
- NSLog("[PN] Python bootstrap succeeded; returning early from viewDidLoad")
88
- return
89
- } else {
90
- NSLog("[PN] 'bootstrap' exists but is not callable")
91
- }
103
+ let args = try json.loads.throwing.dynamicallyCall(withArguments: [jsonStr])
104
+ _ = try page.set_args.throwing.dynamicallyCall(withArguments: [args])
92
105
  } catch {
93
- NSLog("[PN] Python callable/bootstrap raised error: \(error)")
94
- let sys = Python.import("sys")
95
- NSLog("[PN] sys.path at call error: \(sys.path)")
106
+ NSLog("[PN] Failed to decode requestedPageArgsJSON: \(error)")
96
107
  }
97
- } else {
98
- NSLog("[PN] Python bootstrap function not found on app.main_page")
99
108
  }
109
+ // Call on_create immediately so Python can insert its root view
110
+ _ = try page.on_create.throwing.dynamicallyCall(withArguments: [])
111
+ return
100
112
  } catch {
101
- NSLog("[PN] Python bootstrap failed during import/getattr: \(error)")
102
- let sys = Python.import("sys")
103
- NSLog("[PN] sys.path at failure: \(sys.path)")
113
+ NSLog("[PN] Python bootstrap failed: \(error)")
104
114
  }
105
115
  #endif
106
116
 
@@ -113,6 +123,96 @@ class ViewController: UIViewController {
113
123
  view.addSubview(label)
114
124
  }
115
125
 
126
+ override func viewWillAppear(_ animated: Bool) {
127
+ super.viewWillAppear(animated)
128
+ #if canImport(PythonKit)
129
+ if pythonReady {
130
+ let ptr = UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque())
131
+ do {
132
+ let pn = try Python.attemptImport("pythonnative.page")
133
+ _ = try pn.forward_lifecycle.throwing.dynamicallyCall(withArguments: [ptr, "on_start"])
134
+ } catch {}
135
+ }
136
+ #endif
137
+ }
138
+
139
+ override func viewDidAppear(_ animated: Bool) {
140
+ super.viewDidAppear(animated)
141
+ #if canImport(PythonKit)
142
+ if pythonReady {
143
+ let ptr = UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque())
144
+ do {
145
+ let pn = try Python.attemptImport("pythonnative.page")
146
+ _ = try pn.forward_lifecycle.throwing.dynamicallyCall(withArguments: [ptr, "on_resume"])
147
+ } catch {}
148
+ }
149
+ #endif
150
+ }
151
+
152
+ override func viewWillDisappear(_ animated: Bool) {
153
+ super.viewWillDisappear(animated)
154
+ #if canImport(PythonKit)
155
+ if pythonReady {
156
+ let ptr = UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque())
157
+ do {
158
+ let pn = try Python.attemptImport("pythonnative.page")
159
+ _ = try pn.forward_lifecycle.throwing.dynamicallyCall(withArguments: [ptr, "on_pause"])
160
+ } catch {}
161
+ }
162
+ #endif
163
+ }
164
+
165
+ override func viewDidDisappear(_ animated: Bool) {
166
+ super.viewDidDisappear(animated)
167
+ #if canImport(PythonKit)
168
+ if pythonReady {
169
+ let ptr = UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque())
170
+ do {
171
+ let pn = try Python.attemptImport("pythonnative.page")
172
+ _ = try pn.forward_lifecycle.throwing.dynamicallyCall(withArguments: [ptr, "on_stop"])
173
+ } catch {}
174
+ }
175
+ #endif
176
+ }
177
+
178
+ override func encodeRestorableState(with coder: NSCoder) {
179
+ super.encodeRestorableState(with: coder)
180
+ #if canImport(PythonKit)
181
+ if pythonReady {
182
+ let ptr = UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque())
183
+ do {
184
+ let pn = try Python.attemptImport("pythonnative.page")
185
+ _ = try pn.forward_lifecycle.throwing.dynamicallyCall(withArguments: [ptr, "on_save_instance_state"])
186
+ } catch {}
187
+ }
188
+ #endif
189
+ }
190
+
191
+ override func decodeRestorableState(with coder: NSCoder) {
192
+ super.decodeRestorableState(with: coder)
193
+ #if canImport(PythonKit)
194
+ if pythonReady {
195
+ let ptr = UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque())
196
+ do {
197
+ let pn = try Python.attemptImport("pythonnative.page")
198
+ _ = try pn.forward_lifecycle.throwing.dynamicallyCall(withArguments: [ptr, "on_restore_instance_state"])
199
+ } catch {}
200
+ }
201
+ #endif
202
+ }
203
+
204
+ deinit {
205
+ #if canImport(PythonKit)
206
+ if pythonReady {
207
+ let ptr = UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque())
208
+ do {
209
+ let pn = try Python.attemptImport("pythonnative.page")
210
+ _ = try pn.forward_lifecycle.throwing.dynamicallyCall(withArguments: [ptr, "on_destroy"])
211
+ } catch {}
212
+ }
213
+ #endif
214
+ }
215
+
116
216
 
117
217
  }
118
218
 
pythonnative/utils.py CHANGED
@@ -39,8 +39,9 @@ def _get_is_android() -> bool:
39
39
 
40
40
  IS_ANDROID: bool = _get_is_android()
41
41
 
42
- # Global hook to access the current Android Activity/Context from Python code
42
+ # Global hooks to access current Android Activity/Context and Fragment container from Python code
43
43
  _android_context: Any = None
44
+ _android_fragment_container: Any = None
44
45
 
45
46
 
46
47
  def set_android_context(context: Any) -> None:
@@ -55,6 +56,15 @@ def set_android_context(context: Any) -> None:
55
56
  _android_context = context
56
57
 
57
58
 
59
+ def set_android_fragment_container(container_view: Any) -> None:
60
+ """Record the current Fragment root container ViewGroup for rendering pages.
61
+
62
+ The current Page's `set_root_view` will attach its native view to this container.
63
+ """
64
+ global _android_fragment_container
65
+ _android_fragment_container = container_view
66
+
67
+
58
68
  def get_android_context() -> Any:
59
69
  """Return the previously set Android Activity/Context or raise if missing."""
60
70
 
@@ -65,3 +75,17 @@ def get_android_context() -> Any:
65
75
  "Android context is not set. Ensure Page is initialized from an Activity " "before constructing views."
66
76
  )
67
77
  return _android_context
78
+
79
+
80
+ def get_android_fragment_container() -> Any:
81
+ """Return the previously set Fragment container ViewGroup or raise if missing.
82
+
83
+ This is set by the host `PageFragment` when its view is created.
84
+ """
85
+ if not IS_ANDROID:
86
+ raise RuntimeError("get_android_fragment_container() called on non-Android platform")
87
+ if _android_fragment_container is None:
88
+ raise RuntimeError(
89
+ "Android fragment container is not set. Ensure PageFragment has been created before set_root_view."
90
+ )
91
+ return _android_fragment_container
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonnative
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Cross-platform native UI toolkit for Android and iOS
5
5
  Author: Owen Carey
6
6
  License: MIT License
@@ -1,4 +1,4 @@
1
- pythonnative/__init__.py,sha256=Y4mxn_dtPj4XtsQSCIST7rlJuptMgh_utlDtgNxtvRM,2050
1
+ pythonnative/__init__.py,sha256=wU7uPSQH-YZsJs1nCdZ9XveG3QTKETbFQjUzQiDY06s,2050
2
2
  pythonnative/activity_indicator_view.py,sha256=cYRiyGf5o4dlEy7v6v9yUt212E4cK6Hpn85P7uRIin0,2314
3
3
  pythonnative/button.py,sha256=UMwgjb6EhxLQgf3OIXtnoydy5pVEg_1B3fsOp2dLISo,3766
4
4
  pythonnative/collection_view.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -15,7 +15,7 @@ pythonnative/material_search_bar.py,sha256=j8a52smIUAem48AJ693g-aeNswNHpHp3N8M0q
15
15
  pythonnative/material_switch.py,sha256=b4K6pKX_RQbmlEiL9nNEZiyOhkvJd5t_m4Ak7RoD7FE,1944
16
16
  pythonnative/material_time_picker.py,sha256=bCUcrqsIRbFnt4g-vXfiSuuJrbaJJJjLrSSV_nSdOe8,2319
17
17
  pythonnative/material_toolbar.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
- pythonnative/page.py,sha256=Mm8FXAAgYsf57t6ViVIrGzfTtEy8a9a_Y69YWrPvWMc,7071
18
+ pythonnative/page.py,sha256=pJyEmpbMWRC3-iJnZbkBfXr0DcDQK9uYSgdd2u3yzAA,14962
19
19
  pythonnative/picker_view.py,sha256=gGryk2SmBi0aWY_faJapWcWGGFCuabx_FaRidnXzIhU,1960
20
20
  pythonnative/progress_view.py,sha256=Kyq4Ed4DzDXBr64KYEbyUJJW9WLxN-01QWsHgpU7ymM,2251
21
21
  pythonnative/scroll_view.py,sha256=elvqWPihQBREqUPQtAwdPtHVp2prIkg5arKhI-u8His,2024
@@ -25,24 +25,26 @@ pythonnative/switch.py,sha256=Ca-XJnHMQdqJrLzDMRZGmm2d7J7GIZ_B0F1xxXF85Zs,1896
25
25
  pythonnative/text_field.py,sha256=CaLH9erxY0hfKWYkkrF9rTkzgmt7Nax8qjqy-qW_Lmc,1962
26
26
  pythonnative/text_view.py,sha256=ZO3Ff_vl4CX1evYYTHZqzgYhpxyvk0bSKI2oIMHW9bs,2140
27
27
  pythonnative/time_picker.py,sha256=StGElEDNKnuw1j6iVPyIMwBAyZ5yxEwx2XTzfam2a0M,2258
28
- pythonnative/utils.py,sha256=XbYDUv710VgMXRK__WUX51aW9rzmmX2xqgnaiQ3NTMU,2089
28
+ pythonnative/utils.py,sha256=0Ub6DDg4uX4ToqoyioQzUJ-_ERzXlJ2r70pBSmeiaYY,3038
29
29
  pythonnative/view.py,sha256=PJ0vNdRkyKKtiIxTYYDU0iDvR2r4WG8ZVUmMSeeJhmE,586
30
30
  pythonnative/web_view.py,sha256=knBh-R1jP_JChADIo1SqwI5mCbonJuI81Q6vdWot7UU,1775
31
31
  pythonnative/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
- pythonnative/cli/pn.py,sha256=7CBzpqCUGx3zBwbAixnkq9sSEajyhd2xhCBCxVlzDdY,27274
32
+ pythonnative/cli/pn.py,sha256=UbkZtkYMNuuC9HylEhF44vQ39JRG4UTdA8Hb1lOLDrI,27136
33
33
  pythonnative/templates/android_template/build.gradle,sha256=nM8OKenPZzdTJjzZlpl3EpLAoj1YL9A1arX0UPSvaL4,352
34
34
  pythonnative/templates/android_template/gradle.properties,sha256=REPaKLRfQiiVfIV8wYmgwzPWvF1f3bhh_kAMV9p4HME,1358
35
35
  pythonnative/templates/android_template/gradlew,sha256=YxNShxF6Hm0SyEWA8fScYdG6AiGOzShmBgXpf5dufWU,5766
36
36
  pythonnative/templates/android_template/gradlew.bat,sha256=xGonx5AHdG3lkisXq7YjDWStixujrRWF7lxlQ8KpsSk,2674
37
37
  pythonnative/templates/android_template/settings.gradle,sha256=GKZiYUYWsaXxaiKOB65xnOs4jLmf0rhvI_3f8x0ic-o,333
38
- pythonnative/templates/android_template/app/build.gradle,sha256=Yoy8-RCSysYJ5N-xiZdtJOOcRset-ttFIwaHT4l2bto,1670
38
+ pythonnative/templates/android_template/app/build.gradle,sha256=y0-v6bUcVV1nlm0N90h_jbnYplAhIJTEhSoGiTC3k3Q,1863
39
39
  pythonnative/templates/android_template/app/proguard-rules.pro,sha256=Vv2WDPIl9spA-YKxOl27DYvD394T_3ZCKCXGBw0KGJA,750
40
40
  pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt,sha256=Am8Yla3i1eR_ac5FVgPU_RsuMrCbyT79h1BcajGE-zI,693
41
41
  pythonnative/templates/android_template/app/src/main/AndroidManifest.xml,sha256=MdWrXxOrwUjnqtDbV952NI4nVF2dTUX9xwSS8chhd9I,940
42
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt,sha256=4Y0U1hH-vYtC8KDw-3dxjE3oHce4sQMY5KcAi6kVZmE,1130
42
+ pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt,sha256=sqOQ4k--WVyfnrhfNkzqAEQ211uYNPxhmIUXDrIb0zY,1321
43
+ pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt,sha256=dWOpdJFuGO2CWZZQjYPmSNxljjDyGUuys7-ehHhAqyM,931
44
+ pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt,sha256=lnIgCfAifZWp9mlJvSqO5mUOfMhH-RWaK11WfkE7uZ8,4001
43
45
  pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml,sha256=7UI8c6b0Ck0pCfCQHmBSezqAfNWeG1WTvKrhgIscYyE,5606
44
46
  pythonnative/templates/android_template/app/src/main/res/drawable-v24/ic_launcher_foreground.xml,sha256=AdGmpsEjTrf-Jw0JfrKD1yucla5RGIhvG2VzqtKA8fc,1702
45
- pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml,sha256=ENxRHKEqpOvKdy-ayDEXHxoxLO3kwiX7EqitXVmi8p8,778
47
+ pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml,sha256=HIgdCNktb3YoJC8QOTIv-0qZRtMRoPdARK59nyYFO6g,461
46
48
  pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml,sha256=iPdlNJnvUkEm6lAYqZuvnMMmnn5YTWIF39122znznMA,343
47
49
  pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml,sha256=iPdlNJnvUkEm6lAYqZuvnMMmnn5YTWIF39122znznMA,343
48
50
  pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp,sha256=3QCZYZhkDtKPvAnNzXo4B8-HB_PrJVtlljTaPKam_wE,1404
@@ -55,6 +57,7 @@ pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launch
55
57
  pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp,sha256=MAn60Hn1dy8w7Ndn-YkkNn--D4HDAEjWctT9otXKfRI,5914
56
58
  pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp,sha256=-Y_vW8O_5bZWksQK0cuuK-xPr58bJJw5diZ0DbcbYtg,3844
57
59
  pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp,sha256=X68DN0XIyILEO7GzctTkpYkMrsY9mJKi51bJVBCLhdw,7778
60
+ pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml,sha256=QGvhfqtiWKIu9U_MQboPV3leLByyRgDvyWMoyGCFXKc,763
58
61
  pythonnative/templates/android_template/app/src/main/res/values/colors.xml,sha256=77cNdJlUmlfOoysA55DvBjLLDJXNru_RaQPIzRLIieQ,147
59
62
  pythonnative/templates/android_template/app/src/main/res/values/strings.xml,sha256=y212ihQZPuuegFVU54kyOMwU50J759yi6h1CNAo-RDc,78
60
63
  pythonnative/templates/android_template/app/src/main/res/values/themes.xml,sha256=8Amp6l23WClwtnLrfXYI2UFvC1eS6JzMRivN57rQBzw,420
@@ -66,8 +69,8 @@ pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar,sha256
66
69
  pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties,sha256=EWegb_Gq2up5HPSrDpYbDQ5NwrRIWmTFTdkBSDNia_4,230
67
70
  pythonnative/templates/ios_template/ios_template/AppDelegate.swift,sha256=_6G8GNcw4idXd75qKgQKTDCr45Ez73QB8WTvhBqqcMw,1349
68
71
  pythonnative/templates/ios_template/ios_template/Info.plist,sha256=ZQIJGpo8Y2qP0j29xqOsIEGvPpEVICLTAw2NehC5CSo,704
69
- pythonnative/templates/ios_template/ios_template/SceneDelegate.swift,sha256=Lnm2xnnXPZ6AaNaPwuSFybyqGmGQwXoUBsfiHHNgT_E,2292
70
- pythonnative/templates/ios_template/ios_template/ViewController.swift,sha256=yo5a5p1ivAO7eVBGeM0gFPb_YKM4ZXEslPG422kYgfc,5299
72
+ pythonnative/templates/ios_template/ios_template/SceneDelegate.swift,sha256=lqtre92dc6d6s-f4ieh_M_4xmc_zMGW79j46tDu9cOY,2177
73
+ pythonnative/templates/ios_template/ios_template/ViewController.swift,sha256=CvIpVOLRY3WSmm-_25N441b_8kaYKgmfkkQChNcBU7M,9241
71
74
  pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json,sha256=D9Sbo8NYXHCWeOAEaoIcPGBoXscGNyDTDTo0SL46IIs,63
72
75
  pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json,sha256=mvZQhvowtJJS-uGhIlcxaR3nlPd3WvdNcb7-tQfRK3w,123
73
76
  pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json,sha256=VUwGr7K_geOvQjFh5VKB6iVXV1mi0tjGMinUmB2JvQs,177
@@ -78,9 +81,9 @@ pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/x
78
81
  pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift,sha256=YnwzZx7yXB13xKAXEGNgz17VuhWeqkHTRTtBJ2Vu3_E,1238
79
82
  pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift,sha256=l2Pwa50F_rv-qPu2go6e4bQernM6PTQJeNPFl_c4ivY,1387
80
83
  pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift,sha256=f5JrG0uVtLMeJQy26Yyz7Om-JUkT220osqcbeIVkj2g,815
81
- pythonnative-0.2.0.dist-info/licenses/LICENSE,sha256=O7jIzERBe5XxsbuZaWnPBneOk6JoSYNj6g2M0noRTws,1069
82
- pythonnative-0.2.0.dist-info/METADATA,sha256=Xq4sJABcatAnd4GXEJ135lziDLO02zQzC54Wl9mx5kc,5083
83
- pythonnative-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
84
- pythonnative-0.2.0.dist-info/entry_points.txt,sha256=iUtDawWSAJAEyWTycpZxDuYz73ol31butpzDIEAgPO0,48
85
- pythonnative-0.2.0.dist-info/top_level.txt,sha256=kT4SEATY2ywzrZ2Pgea6_zxyym44Q_PbOsUoOYjJLFE,13
86
- pythonnative-0.2.0.dist-info/RECORD,,
84
+ pythonnative-0.3.0.dist-info/licenses/LICENSE,sha256=O7jIzERBe5XxsbuZaWnPBneOk6JoSYNj6g2M0noRTws,1069
85
+ pythonnative-0.3.0.dist-info/METADATA,sha256=jaDrFxaYR4ZQvhmSMSP7FXIDPKisfMXZh1eAUL9y5ts,5083
86
+ pythonnative-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
87
+ pythonnative-0.3.0.dist-info/entry_points.txt,sha256=iUtDawWSAJAEyWTycpZxDuYz73ol31butpzDIEAgPO0,48
88
+ pythonnative-0.3.0.dist-info/top_level.txt,sha256=kT4SEATY2ywzrZ2Pgea6_zxyym44Q_PbOsUoOYjJLFE,13
89
+ pythonnative-0.3.0.dist-info/RECORD,,