Skip to content

Commit 2805e1d

Browse files
committed
feat(cli,templates,core): bootstrap entrypoint; pn run shows Hello UI
1 parent f3a03b0 commit 2805e1d

File tree

7 files changed

+137
-26
lines changed

7 files changed

+137
-26
lines changed

apps/pythonnative_demo/app/main_page.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,12 @@ def __init__(self, native_instance):
77

88
def on_create(self):
99
super().on_create()
10-
stack_view = pn.StackView()
11-
# list_data = ["item_{}".format(i) for i in range(100)]
12-
# list_view = pn.ListView(self.native_instance, list_data)
13-
# stack_view.add_view(list_view)
14-
button = pn.Button("Button")
15-
button.set_on_click(lambda: self.navigate_to(""))
16-
# button.set_on_click(lambda: print("Button was clicked!"))
17-
stack_view.add_view(button)
18-
self.set_root_view(stack_view)
10+
stack = pn.StackView()
11+
stack.add_view(pn.Label("Hello from PythonNative Demo!"))
12+
button = pn.Button("Tap me")
13+
button.set_on_click(lambda: print("Demo button clicked"))
14+
stack.add_view(button)
15+
self.set_root_view(stack)
1916

2017
def on_start(self):
2118
super().on_start()
@@ -40,3 +37,9 @@ def on_save_instance_state(self):
4037

4138
def on_restore_instance_state(self):
4239
super().on_restore_instance_state()
40+
41+
42+
def bootstrap(native_instance):
43+
page = MainPage(native_instance)
44+
page.on_create()
45+
return page

docs/getting-started.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ class MainPage(pn.Page):
3636
button.set_on_click(lambda: print("Button clicked"))
3737
stack.add_view(button)
3838
self.set_root_view(stack)
39+
40+
41+
def bootstrap(native_instance):
42+
"""Entry point called by the host app (Activity or ViewController)."""
43+
page = MainPage(native_instance)
44+
page.on_create()
45+
return page
3946
```
4047

4148
## Run on a platform

src/pythonnative/cli/pn.py

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,18 @@ def __init__(self, native_instance):
5454
def on_create(self):
5555
super().on_create()
5656
stack = pn.StackView()
57-
stack.add_view(pn.Label(\"Hello from PythonNative!\"))
57+
stack.add_view(pn.Label("Hello from PythonNative!"))
58+
button = pn.Button("Tap me")
59+
button.set_on_click(lambda: print("Button clicked"))
60+
stack.add_view(button)
5861
self.set_root_view(stack)
62+
63+
64+
def bootstrap(native_instance):
65+
'''Entry point called by the host app (Android Activity or iOS ViewController).'''
66+
page = MainPage(native_instance)
67+
page.on_create()
68+
return page
5969
"""
6070
)
6171

@@ -243,10 +253,11 @@ def run_project(args: argparse.Namespace) -> None:
243253
check=True,
244254
)
245255
elif platform == "ios":
246-
# Attempt to build the iOS project for Simulator (best-effort)
256+
# Attempt to build and run on iOS Simulator (best-effort)
247257
ios_project_dir: str = os.path.join(build_dir, "ios_template")
248258
if os.path.isdir(ios_project_dir):
249259
os.chdir(ios_project_dir)
260+
derived_data = os.path.join(ios_project_dir, "build")
250261
try:
251262
subprocess.run(
252263
[
@@ -255,14 +266,74 @@ def run_project(args: argparse.Namespace) -> None:
255266
"ios_template.xcodeproj",
256267
"-scheme",
257268
"ios_template",
269+
"-configuration",
270+
"Debug",
258271
"-destination",
259272
"platform=iOS Simulator,name=iPhone 15",
273+
"-derivedDataPath",
274+
derived_data,
260275
"build",
261276
],
262277
check=False,
263278
)
264279
except FileNotFoundError:
265280
print("xcodebuild not found. Skipping iOS build step.")
281+
return
282+
283+
# Locate built app
284+
app_path = os.path.join(derived_data, "Build", "Products", "Debug-iphonesimulator", "ios_template.app")
285+
if not os.path.isdir(app_path):
286+
print("Could not locate built .app; open the project in Xcode to run.")
287+
return
288+
289+
# Copy staged Python app into the .app bundle so PythonKit can import it
290+
try:
291+
staged_app_src = os.path.join(build_dir, "app")
292+
if os.path.isdir(staged_app_src):
293+
shutil.copytree(staged_app_src, os.path.join(app_path, "app"), dirs_exist_ok=True)
294+
except Exception:
295+
# Non-fatal; fallback UI will appear if import fails
296+
pass
297+
298+
# Find an available simulator and boot it
299+
try:
300+
import json as _json
301+
302+
result = subprocess.run(
303+
["xcrun", "simctl", "list", "devices", "available", "--json"],
304+
check=False,
305+
capture_output=True,
306+
text=True,
307+
)
308+
devices_json = _json.loads(result.stdout or "{}")
309+
all_devices = []
310+
for _runtime, devices in (devices_json.get("devices") or {}).items():
311+
all_devices.extend(devices or [])
312+
# Prefer iPhone 15/15 Pro names; else first available iPhone
313+
preferred = None
314+
for d in all_devices:
315+
name = (d.get("name") or "").lower()
316+
if "iphone 15" in name and d.get("isAvailable"):
317+
preferred = d
318+
break
319+
if not preferred:
320+
for d in all_devices:
321+
if d.get("isAvailable") and (d.get("name") or "").lower().startswith("iphone"):
322+
preferred = d
323+
break
324+
if not preferred:
325+
print("No available iOS Simulators found; open the project in Xcode to run.")
326+
return
327+
328+
udid = preferred.get("udid")
329+
# Boot (no-op if already booted)
330+
subprocess.run(["xcrun", "simctl", "boot", udid], check=False)
331+
# Install and launch
332+
subprocess.run(["xcrun", "simctl", "install", udid, app_path], check=False)
333+
subprocess.run(["xcrun", "simctl", "launch", udid, "com.pythonnative.ios-template"], check=False)
334+
print("Launched iOS app on Simulator (best-effort).")
335+
except Exception:
336+
print("Failed to auto-run on Simulator; open the project in Xcode to run.")
266337

267338

268339
def clean_project(args: argparse.Namespace) -> None:

src/pythonnative/page.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,16 @@ def __init__(self, native_instance) -> None:
160160
# self.native_instance = self.native_class.alloc().init()
161161

162162
def set_root_view(self, view) -> None:
163-
self.native_instance.view().addSubview_(view.native_instance)
163+
root_view = self.native_instance.view()
164+
# Size the root child to fill the controller's view and enable autoresizing
165+
try:
166+
bounds = root_view.bounds()
167+
view.native_instance.setFrame_(bounds)
168+
# UIViewAutoresizingFlexibleWidth (2) | UIViewAutoresizingFlexibleHeight (16)
169+
view.native_instance.setAutoresizingMask_(2 | 16)
170+
except Exception:
171+
pass
172+
root_view.addSubview_(view.native_instance)
164173

165174
def on_create(self) -> None:
166175
print("iOS on_create() called")

templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package com.pythonnative.android_template
22

33
import androidx.appcompat.app.AppCompatActivity
44
import android.os.Bundle
5-
import android.view.View
5+
import android.widget.TextView
66
import com.chaquo.python.Python
77
import com.chaquo.python.android.AndroidPlatform
88

@@ -15,9 +15,16 @@ class MainActivity : AppCompatActivity() {
1515
if (!Python.isStarted()) {
1616
Python.start(AndroidPlatform(this))
1717
}
18-
val py = Python.getInstance()
19-
val pyModule = py.getModule("app/main")
20-
val pyLayout = pyModule.callAttr("main", this).toJava(View::class.java)
21-
setContentView(pyLayout)
18+
try {
19+
val py = Python.getInstance()
20+
val pyModule = py.getModule("app.main_page")
21+
pyModule.callAttr("bootstrap", this)
22+
// Python Page will set the content view via set_root_view
23+
} catch (e: Exception) {
24+
// Fallback: show a simple native label if Python bootstrap fails
25+
val tv = TextView(this)
26+
tv.text = "Hello from PythonNative (Android template)"
27+
setContentView(tv)
28+
}
2229
}
2330
}

templates/ios_template/ios_template/ViewController.swift

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,27 @@ class ViewController: UIViewController {
1212

1313
override func viewDidLoad() {
1414
super.viewDidLoad()
15-
// Minimal PythonKit bootstrap: print Python version and attempt to import Rubicon.
15+
// Attempt Python bootstrap of app.main_page.bootstrap(self)
1616
let sys = Python.import("sys")
17-
print("Python Version: \(sys.version_info.major).\(sys.version_info.minor)")
18-
print("Python Path: \(sys.path)")
19-
20-
// Try to import Rubicon-ObjC if available. Safe no-op if not present.
17+
if let resourcePath = Bundle.main.resourcePath {
18+
sys.path.append(resourcePath)
19+
sys.path.append("\(resourcePath)/app")
20+
}
2121
do {
22-
let rubiconObjC = try Python.attemptImport("rubicon.objc")
23-
let ObjCClass = rubiconObjC.ObjCClass
24-
print("Rubicon available: \(ObjCClass)")
22+
let app = try Python.attemptImport("app.main_page")
23+
let bootstrap = app.bootstrap
24+
_ = bootstrap(self)
25+
return
2526
} catch {
26-
print("Rubicon not available; continuing without it.")
27+
print("Python bootstrap failed: \(error)")
2728
}
29+
30+
// Fallback UI if Python import/bootstrap fails
31+
let label = UILabel(frame: view.bounds)
32+
label.text = "Hello from PythonNative (iOS template)"
33+
label.textAlignment = .center
34+
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
35+
view.addSubview(label)
2836
}
2937

3038

tests/test_cli.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ def test_cli_init_and_clean():
1717
result = run_pn(["init", "MyApp"], tmpdir)
1818
assert result.returncode == 0, result.stderr
1919
assert os.path.isdir(os.path.join(tmpdir, "app"))
20+
# scaffolded entrypoint
21+
main_page_path = os.path.join(tmpdir, "app", "main_page.py")
22+
assert os.path.isfile(main_page_path)
23+
with open(main_page_path, "r", encoding="utf-8") as f:
24+
content = f.read()
25+
assert "def bootstrap(" in content
2026
assert os.path.isfile(os.path.join(tmpdir, "pythonnative.json"))
2127
assert os.path.isfile(os.path.join(tmpdir, "requirements.txt"))
2228
assert os.path.isfile(os.path.join(tmpdir, ".gitignore"))

0 commit comments

Comments
 (0)