11import argparse
2- import io
2+ import json
33import os
44import shutil
55import subprocess
6+ import sys
67import zipfile
7-
8- import requests
8+ from importlib import resources
99
1010
1111def init_project (args : argparse .Namespace ) -> None :
1212 """
1313 Initialize a new PythonNative project.
14+ Creates `app/`, `pythonnative.json`, `requirements.txt`, `.gitignore`.
1415 """
15- # TODO: Implementation
16-
17-
18- def download_template_project (template_url : str , destination : str ) -> None :
16+ project_name : str = getattr (args , "name" , None ) or os .path .basename (os .getcwd ())
17+ cwd : str = os .getcwd ()
18+
19+ app_dir = os .path .join (cwd , "app" )
20+ config_path = os .path .join (cwd , "pythonnative.json" )
21+ requirements_path = os .path .join (cwd , "requirements.txt" )
22+ gitignore_path = os .path .join (cwd , ".gitignore" )
23+
24+ # Prevent accidental overwrite unless --force is provided
25+ if not getattr (args , "force" , False ):
26+ exists = []
27+ if os .path .exists (app_dir ):
28+ exists .append ("app/" )
29+ if os .path .exists (config_path ):
30+ exists .append ("pythonnative.json" )
31+ if os .path .exists (requirements_path ):
32+ exists .append ("requirements.txt" )
33+ if os .path .exists (gitignore_path ):
34+ exists .append (".gitignore" )
35+ if exists :
36+ print (f"Refusing to overwrite existing: { ', ' .join (exists )} . Use --force to overwrite." )
37+ sys .exit (1 )
38+
39+ os .makedirs (app_dir , exist_ok = True )
40+
41+ # Minimal hello world app scaffold
42+ main_page_py = os .path .join (app_dir , "main_page.py" )
43+ if not os .path .exists (main_page_py ) or args .force :
44+ with open (main_page_py , "w" , encoding = "utf-8" ) as f :
45+ f .write (
46+ """import pythonnative as pn
47+
48+
49+ class MainPage(pn.Page):
50+ def __init__(self, native_instance):
51+ super().__init__(native_instance)
52+
53+ def on_create(self):
54+ super().on_create()
55+ stack = pn.StackView(self.native_instance)
56+ stack.add_view(pn.Label(self.native_instance, \" Hello from PythonNative!\" ))
57+ self.set_root_view(stack)
58+ """
59+ )
60+
61+ # Create config
62+ config = {
63+ "name" : project_name ,
64+ "appId" : "com.example." + project_name .replace (" " , "" ).lower (),
65+ "entryPoint" : "app/main_page.py" ,
66+ "ios" : {},
67+ "android" : {},
68+ }
69+ with open (config_path , "w" , encoding = "utf-8" ) as f :
70+ json .dump (config , f , indent = 2 )
71+
72+ # Requirements
73+ if not os .path .exists (requirements_path ) or args .force :
74+ with open (requirements_path , "w" , encoding = "utf-8" ) as f :
75+ f .write ("pythonnative\n " )
76+
77+ # .gitignore
78+ default_gitignore = "# PythonNative\n " "__pycache__/\n " "*.pyc\n " ".venv/\n " "build/\n " ".DS_Store\n "
79+ if not os .path .exists (gitignore_path ) or args .force :
80+ with open (gitignore_path , "w" , encoding = "utf-8" ) as f :
81+ f .write (default_gitignore )
82+
83+ print ("Initialized PythonNative project." )
84+
85+
86+ def _extract_zip_to_destination (zip_path : str , destination : str ) -> None :
87+ with zipfile .ZipFile (zip_path , "r" ) as zf :
88+ zf .extractall (destination )
89+
90+
91+ def _extract_bundled_template (zip_name : str , destination : str ) -> None :
1992 """
20- Download and extract a template project from a URL.
21-
22- :param template_url: The URL of the template project.
23- :param destination: The directory where the project will be created.
93+ Extract a bundled template zip into the destination directory.
94+ Tries package resources first; falls back to repo root `templates/` at dev time.
2495 """
25- response : requests .Response = requests .get (template_url , stream = True )
26-
27- if response .status_code == 200 :
28- with zipfile .ZipFile (io .BytesIO (response .content )) as z :
29- z .extractall (destination )
96+ # Try to load from installed package resources first
97+ try :
98+ pkg_templates = resources .files ("pythonnative" ).joinpath ("templates" )
99+ resource_path = str (pkg_templates .joinpath (zip_name ))
100+ if os .path .exists (resource_path ):
101+ _extract_zip_to_destination (resource_path , destination )
102+ return
103+ except Exception :
104+ # Fall back to repo layout
105+ pass
106+
107+ # Fallback: use repository-level templates directory
108+ repo_templates = os .path .join (os .path .dirname (os .path .dirname (os .path .dirname (__file__ ))), ".." , ".." , "templates" )
109+ repo_templates = os .path .abspath (repo_templates )
110+ candidate = os .path .join (repo_templates , zip_name )
111+ if os .path .exists (candidate ):
112+ _extract_zip_to_destination (candidate , destination )
113+ return
114+
115+ raise FileNotFoundError (f"Could not find bundled template { zip_name } . Ensure templates are packaged." )
30116
31117
32118def create_android_project (project_name : str , destination : str ) -> None :
@@ -36,12 +122,8 @@ def create_android_project(project_name: str, destination: str) -> None:
36122 :param project_name: The name of the project.
37123 :param destination: The directory where the project will be created.
38124 """
39- android_template_url = (
40- "https://github.com/owenthcarey/pythonnative-workspace/blob/main/libs/templates/android_template.zip?raw=true"
41- )
42-
43- # Download and extract the Android template project
44- download_template_project (android_template_url , destination )
125+ # Extract the Android template project from bundled zip
126+ _extract_bundled_template ("android_template.zip" , destination )
45127
46128
47129def create_ios_project (project_name : str , destination : str ) -> None :
@@ -51,12 +133,8 @@ def create_ios_project(project_name: str, destination: str) -> None:
51133 :param project_name: The name of the project.
52134 :param destination: The directory where the project will be created.
53135 """
54- ios_template_url = (
55- "https://github.com/owenthcarey/pythonnative-workspace/blob/main/libs/templates/ios_template.zip?raw=true"
56- )
57-
58- # Download and extract the iOS template project
59- download_template_project (ios_template_url , destination )
136+ # Extract the iOS template project from bundled zip
137+ _extract_bundled_template ("ios_template.zip" , destination )
60138
61139
62140def run_project (args : argparse .Namespace ) -> None :
@@ -85,7 +163,8 @@ def run_project(args: argparse.Namespace) -> None:
85163 if platform == "android" :
86164 dest_dir : str = os .path .join (build_dir , "android_template" , "app" , "src" , "main" , "python" , "app" )
87165 else :
88- dest_dir = os .path .join (build_dir , "app" ) # Adjust this based on your iOS project structure
166+ # For iOS, stage the Python app in a top-level folder for later integration scripts
167+ dest_dir = os .path .join (build_dir , "app" )
89168
90169 # Create the destination directory if it doesn't exist
91170 os .makedirs (dest_dir , exist_ok = True )
@@ -94,7 +173,7 @@ def run_project(args: argparse.Namespace) -> None:
94173 # Install any necessary Python packages into the project environment
95174 requirements_path = os .path .join (os .getcwd (), "requirements.txt" )
96175 if os .path .exists (requirements_path ):
97- subprocess .run (["pip" , "install" , "-r" , requirements_path ], check = False )
176+ subprocess .run ([sys . executable , "-m" , "pip" , "install" , "-r" , requirements_path ], check = False )
98177
99178 # Run the project
100179 if platform == "android" :
@@ -107,9 +186,14 @@ def run_project(args: argparse.Namespace) -> None:
107186 os .chmod (gradlew_path , 0o755 ) # this makes the file executable for the user
108187
109188 # Build the Android project and install it on the device
110- jdk_path : str = subprocess .check_output (["brew" , "--prefix" , "openjdk@17" ]).decode ().strip ()
111189 env : dict [str , str ] = os .environ .copy ()
112- env ["JAVA_HOME" ] = jdk_path
190+ # Respect JAVA_HOME if set; otherwise, attempt a best-effort on macOS via Homebrew
191+ if sys .platform == "darwin" and not env .get ("JAVA_HOME" ):
192+ try :
193+ jdk_path : str = subprocess .check_output (["brew" , "--prefix" , "openjdk@17" ]).decode ().strip ()
194+ env ["JAVA_HOME" ] = jdk_path
195+ except Exception :
196+ pass
113197 subprocess .run (["./gradlew" , "installDebug" ], check = True , env = env )
114198
115199 # Run the Android app
@@ -126,6 +210,27 @@ def run_project(args: argparse.Namespace) -> None:
126210 ],
127211 check = True ,
128212 )
213+ elif platform == "ios" :
214+ # Attempt to build the iOS project for Simulator (best-effort)
215+ ios_project_dir : str = os .path .join (build_dir , "ios_template" )
216+ if os .path .isdir (ios_project_dir ):
217+ os .chdir (ios_project_dir )
218+ try :
219+ subprocess .run (
220+ [
221+ "xcodebuild" ,
222+ "-project" ,
223+ "ios_template.xcodeproj" ,
224+ "-scheme" ,
225+ "ios_template" ,
226+ "-destination" ,
227+ "platform=iOS Simulator,name=iPhone 15" ,
228+ "build" ,
229+ ],
230+ check = False ,
231+ )
232+ except FileNotFoundError :
233+ print ("xcodebuild not found. Skipping iOS build step." )
129234
130235
131236def clean_project (args : argparse .Namespace ) -> None :
@@ -137,8 +242,10 @@ def clean_project(args: argparse.Namespace) -> None:
137242
138243 # Check if the build directory exists
139244 if os .path .exists (build_dir ):
140- # Delete the build directory
141245 shutil .rmtree (build_dir )
246+ print ("Removed build/ directory." )
247+ else :
248+ print ("No build/ directory to remove." )
142249
143250
144251def main () -> None :
@@ -147,6 +254,8 @@ def main() -> None:
147254
148255 # Create a new command 'init' that calls init_project
149256 parser_init = subparsers .add_parser ("init" )
257+ parser_init .add_argument ("name" , nargs = "?" , help = "Project name (defaults to current directory name)" )
258+ parser_init .add_argument ("--force" , action = "store_true" , help = "Overwrite existing files if present" )
150259 parser_init .set_defaults (func = init_project )
151260
152261 # Create a new command 'run' that calls run_project
@@ -160,3 +269,7 @@ def main() -> None:
160269
161270 args = parser .parse_args ()
162271 args .func (args )
272+
273+
274+ if __name__ == "__main__" :
275+ main ()
0 commit comments