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
@@ -147,6 +244,8 @@ def scaffold(
147244 console .print ("[red]--template is required in non-interactive mode[/red]" )
148245 raise typer .Exit (2 )
149246
247+ set_map = _parse_set_options (set_opt )
248+
150249 from create_awesome_python_app .catalog import (
151250 CatalogResolutionError ,
152251 resolve_catalog_spec ,
@@ -161,6 +260,10 @@ def scaffold(
161260 console .print (f"[red]{ err } [/red]" )
162261 raise typer .Exit (2 ) from err
163262
263+ if pin and "://" in template and "ref=" not in template :
264+ sep = "&" if "?" in template else "?"
265+ template = f"{ template } { sep } ref={ pin } "
266+
164267 if want_interactive and not addons :
165268 try :
166269 import questionary
@@ -213,9 +316,38 @@ def scaffold(
213316 console .print ("[red]questionary not available[/red]" )
214317 raise typer .Exit (1 ) from None
215318
216- if pin and "://" in template and "ref=" not in template :
217- sep = "&" if "?" in template else "?"
218- template = f"{ template } { sep } ref={ pin } "
319+ if want_interactive :
320+ try :
321+ from create_awesome_python_app .catalog import (
322+ find_template_by_url ,
323+ get_catalog_data ,
324+ )
325+
326+ interactive_catalog = interactive_catalog or get_catalog_data ()
327+ registry_options : list [dict [str , Any ]] = []
328+ template_entry = find_template_by_url (interactive_catalog , template )
329+ if template_entry :
330+ raw_registry_options = (
331+ template_entry .get ("customOptions" )
332+ or template_entry .get ("custom_options" )
333+ or []
334+ )
335+ if isinstance (raw_registry_options , list ):
336+ registry_options = [
337+ item for item in raw_registry_options if isinstance (item , dict )
338+ ]
339+ custom_answers = _prompt_custom_options (
340+ template ,
341+ set_map = set_map ,
342+ cache_dir = cache_dir ,
343+ offline = offline ,
344+ registry_options = registry_options ,
345+ )
346+ except ImportError :
347+ console .print ("[red]questionary not available[/red]" )
348+ raise typer .Exit (1 ) from None
349+ custom_answers .update (set_map )
350+ set_map = custom_answers
219351
220352 # version check
221353 latest = asyncio .run (check_for_latest_version ("create-awesome-python-app" ))
@@ -230,14 +362,6 @@ def scaffold(
230362 raise typer .Exit (1 )
231363 console .print (f"[yellow]{ msg } [/yellow]" )
232364
233- set_map : dict [str , str ] = {}
234- for item in set_opt or []:
235- if "=" not in item :
236- console .print (f"[red]Invalid --set { item } (expected key=value)[/red]" )
237- raise typer .Exit (2 )
238- k , v = item .split ("=" , 1 )
239- set_map [k ] = v
240-
241365 asyncio .run (
242366 create_python_app (
243367 project_directory or "my-project" ,
0 commit comments