55import asyncio
66import os
77from pathlib import Path
8+ from typing import Any
89
910import typer
1011from create_python_app_core import (
12+ ConfigParseError ,
13+ CpaCustomOption ,
1114 check_for_latest_version ,
1215 check_python_version ,
1316 create_python_app ,
1417 default_cache_dir ,
18+ download_repository ,
19+ load_cpa_config ,
1520 print_env_info ,
21+ resolve_source ,
1622)
1723from rich .console import Console
1824
@@ -33,6 +39,97 @@ def _in_ci() -> bool:
3339 return os .environ .get ("CI" , "" ).lower () in {"1" , "true" , "yes" }
3440
3541
42+ def _template_config_path (source_subdir : str | None , root : Path ) -> Path :
43+ cfg_path = root / "cpa.config.json"
44+ if not cfg_path .is_file () and source_subdir :
45+ cfg_path = root / source_subdir / "cpa.config.json"
46+ return cfg_path
47+
48+
49+ def _parse_set_options (set_opt : list [str ] | None ) -> dict [str , str ]:
50+ set_map : dict [str , str ] = {}
51+ for item in set_opt or []:
52+ if "=" not in item :
53+ console .print (f"[red]Invalid --set { item } (expected key=value)[/red]" )
54+ raise typer .Exit (2 )
55+ key , value = item .split ("=" , 1 )
56+ set_map [key ] = value
57+ return set_map
58+
59+
60+ def _stringify_option_value (value : Any ) -> str :
61+ if value is None :
62+ return ""
63+ if isinstance (value , bool ):
64+ return "true" if value else "false"
65+ return str (value )
66+
67+
68+ def _registry_custom_options (items : list [dict [str , Any ]]) -> list [CpaCustomOption ]:
69+ options : list [CpaCustomOption ] = []
70+ for item in items :
71+ key = item .get ("key" ) or item .get ("name" )
72+ if not key :
73+ continue
74+ options .append (
75+ CpaCustomOption (
76+ key = str (key ),
77+ type = str (item .get ("type" , "string" )),
78+ message = str (item .get ("message" , "" )),
79+ default = item .get ("default" , item .get ("initial" )),
80+ )
81+ )
82+ return options
83+
84+
85+ def _prompt_custom_options (
86+ template : str ,
87+ * ,
88+ set_map : dict [str , str ],
89+ cache_dir : Path | None ,
90+ offline : bool ,
91+ registry_options : list [dict [str , Any ]] | None = None ,
92+ ) -> dict [str , str ]:
93+ import questionary
94+
95+ source = resolve_source (template , cache_dir = cache_dir )
96+ root = download_repository (source , offline = offline , cache_root = cache_dir )
97+ try :
98+ config = load_cpa_config (_template_config_path (source .subdir , root ))
99+ except ConfigParseError as err :
100+ console .print (f"[yellow]Warning: { err } [/yellow]" )
101+ return {}
102+
103+ custom_options = config .custom_options
104+ if not custom_options and registry_options :
105+ custom_options = _registry_custom_options (registry_options )
106+
107+ answers : dict [str , str ] = {}
108+ blocked_types = {"password" , "invisible" }
109+ for option in custom_options :
110+ if option .type in blocked_types :
111+ console .print (
112+ f"[yellow]Warning: skipped blocked custom option { option .key } [/yellow]"
113+ )
114+ continue
115+ if option .key in set_map :
116+ answers [option .key ] = set_map [option .key ]
117+ continue
118+ initial = set_map .get (option .key , _stringify_option_value (option .default ))
119+ message = option .message or option .key
120+ if option .type in {"bool" , "boolean" , "confirm" }:
121+ answer = questionary .confirm (
122+ message ,
123+ default = initial .lower () in {"1" , "true" , "yes" , "on" },
124+ ).ask ()
125+ else :
126+ answer = questionary .text (message , default = initial ).ask ()
127+ if answer is None :
128+ raise typer .Exit (1 )
129+ answers [option .key ] = _stringify_option_value (answer )
130+ return answers
131+
132+
36133def main () -> None :
37134 """Console script entrypoint.
38135
@@ -145,6 +242,8 @@ def scaffold(
145242 console .print ("[red]--template is required in non-interactive mode[/red]" )
146243 raise typer .Exit (2 )
147244
245+ set_map = _parse_set_options (set_opt )
246+
148247 from create_awesome_python_app .catalog import (
149248 CatalogResolutionError ,
150249 resolve_catalog_spec ,
@@ -159,6 +258,10 @@ def scaffold(
159258 console .print (f"[red]{ err } [/red]" )
160259 raise typer .Exit (2 ) from err
161260
261+ if pin and "://" in template and "ref=" not in template :
262+ sep = "&" if "?" in template else "?"
263+ template = f"{ template } { sep } ref={ pin } "
264+
162265 if want_interactive and not addons :
163266 try :
164267 import questionary
@@ -211,9 +314,38 @@ def scaffold(
211314 console .print ("[red]questionary not available[/red]" )
212315 raise typer .Exit (1 ) from None
213316
214- if pin and "://" in template and "ref=" not in template :
215- sep = "&" if "?" in template else "?"
216- template = f"{ template } { sep } ref={ pin } "
317+ if want_interactive :
318+ try :
319+ from create_awesome_python_app .catalog import (
320+ find_template_by_url ,
321+ get_catalog_data ,
322+ )
323+
324+ interactive_catalog = interactive_catalog or get_catalog_data ()
325+ registry_options : list [dict [str , Any ]] = []
326+ template_entry = find_template_by_url (interactive_catalog , template )
327+ if template_entry :
328+ raw_registry_options = (
329+ template_entry .get ("customOptions" )
330+ or template_entry .get ("custom_options" )
331+ or []
332+ )
333+ if isinstance (raw_registry_options , list ):
334+ registry_options = [
335+ item for item in raw_registry_options if isinstance (item , dict )
336+ ]
337+ custom_answers = _prompt_custom_options (
338+ template ,
339+ set_map = set_map ,
340+ cache_dir = cache_dir ,
341+ offline = offline ,
342+ registry_options = registry_options ,
343+ )
344+ except ImportError :
345+ console .print ("[red]questionary not available[/red]" )
346+ raise typer .Exit (1 ) from None
347+ custom_answers .update (set_map )
348+ set_map = custom_answers
217349
218350 # version check
219351 latest = asyncio .run (check_for_latest_version ("create-awesome-python-app" ))
@@ -228,14 +360,6 @@ def scaffold(
228360 raise typer .Exit (1 )
229361 console .print (f"[yellow]{ msg } [/yellow]" )
230362
231- set_map : dict [str , str ] = {}
232- for item in set_opt or []:
233- if "=" not in item :
234- console .print (f"[red]Invalid --set { item } (expected key=value)[/red]" )
235- raise typer .Exit (2 )
236- k , v = item .split ("=" , 1 )
237- set_map [k ] = v
238-
239363 asyncio .run (
240364 create_python_app (
241365 project_directory or "my-project" ,
0 commit comments