1+ import logging
12import sys
3+ from enum import StrEnum
24from pathlib import Path
5+ from subprocess import check_output
36from typing import Annotated , Optional
47
8+ import typer
59from copier import run_copy
610from typer import Argument , Option , Typer , colors , confirm , style
711
@@ -16,18 +20,124 @@ def version_callback(value: bool) -> None:
1620 sys .exit (0 )
1721
1822
23+ class DocumentationTool (StrEnum ):
24+ "which documentation tool to use"
25+ mkdocs = "mkdocs"
26+ sphinx = "sphinx"
27+ none = "none"
28+
29+
30+ class DocumentationTemplate (StrEnum ):
31+ "which documentation template to use"
32+ sphinx_fhg_iis = "sphinx-fhg-iis"
33+ builtin = "none"
34+
35+
36+ class RemotePlatform (StrEnum ):
37+ "which remote platform to configure"
38+ github = "github"
39+ gitlab_fhg = "gitlab-fhg"
40+ gitlab_iis = "gitlab-iis"
41+
42+
43+ def CustomOptional (_type = bool , help = "" , custom_flag : str | list = None , ** kwargs ):
44+ if issubclass (_type , StrEnum ):
45+ kwargs = {"case_sensitive" : False , ** kwargs }
46+ if not help :
47+ help = _type .__doc__
48+
49+ kwargs = {"show_default" : False , "help" : help , ** kwargs }
50+
51+ if custom_flag is None :
52+ return Annotated [Optional [_type ], Option (** kwargs )]
53+
54+ if isinstance (custom_flag , str ):
55+ custom_flag = [custom_flag ]
56+ return Annotated [Optional [_type ], Option (* custom_flag , ** kwargs )]
57+
58+
1959@app .command (name = "init-python-project" )
2060def cli (
61+ # data passed to the underlying copier template
2162 target_path : Path = Argument ("new-project" ),
63+ project_name : CustomOptional (str , "project name (title case with spaces)" ) = None ,
64+ package_name : CustomOptional (str , "Python package name (lowercase with underscores)" ) = None ,
65+ user_name : CustomOptional (str , "your user name" ) = None ,
66+ docs : CustomOptional (DocumentationTool ) = None ,
67+ docs_template : CustomOptional (DocumentationTemplate ) = None ,
68+ remote : CustomOptional (RemotePlatform ) = None ,
69+ remote_url : CustomOptional (str , "ssh url where your repository will be hosted on" ) = None ,
70+ precommit : CustomOptional (bool , "include pre-commit hooks" ) = None ,
71+ bumpversion : CustomOptional (bool , "include bumpversion configuration" ) = None ,
72+ # arguments that affect project creation
73+ defaults : Annotated [
74+ bool , Option ("--defaults" , "-d" , help = "automatically accept all default options" )
75+ ] = False ,
76+ dry_run : Annotated [bool , Option ("--dry-run" , help = "do not actually create project" )] = False ,
77+ always_confirm : Annotated [
78+ bool , Option ("--yes" , "-y" , help = "answer any confirmation request with yes" )
79+ ] = False ,
2280 version : Annotated [
23- Optional [bool ], Option ("--version" , callback = version_callback , is_eager = True )
81+ Optional [bool ],
82+ Option ("--version" , callback = version_callback , is_eager = True , help = "show version and exit" ),
83+ ] = None ,
84+ verbose : Annotated [
85+ Optional [bool ],
86+ typer .Option (
87+ "--verbose" ,
88+ "-v" ,
89+ callback = lambda x : logging .basicConfig (
90+ level = logging .INFO if x else logging .WARN , format = "%(message)s"
91+ ),
92+ is_eager = True ,
93+ help = "show more information" ,
94+ ),
95+ ] = False ,
96+ copier_args : Annotated [
97+ Optional [list [str ]],
98+ typer .Option ("--copier-arg" , help = "anything you want to pass to copier" ),
2499 ] = None ,
25100) -> None :
26- """Executes the CLI command to create a new project."""
27- target_path .mkdir (exist_ok = True )
101+ """Executes the CLI command to create a new project.
102+
103+ For a list of supported copier arguments, see
104+ https://copier.readthedocs.io/en/stable/reference/main/#copier.main.Worker.
105+
106+ Note that `src_path`, `dest_path`, `vcs_ref`, `data`, `defaults`, `user_defaults` and `unsafe`
107+ are already set by this command. Further, `--dry-run` corresponds to copier's `--pretend` and
108+ `--yes` implies copier's `--overwrite`.
109+ """
110+
111+ if docs_template not in [None , "none" ] and (
112+ docs is None or (docs is not None and not docs_template .value .startswith (docs .value ))
113+ ):
114+ typer .secho (
115+ f"Error: selected template ({ docs_template } ) not compatible "
116+ f"with documentation tool ({ docs } )" ,
117+ fg = colors .RED ,
118+ err = True ,
119+ )
120+ raise typer .Exit (1 )
121+
122+ # cast enums to their values
123+ for option in "docs remote" .split ():
124+ if locals ()[option ] is not None :
125+ locals ()[option ] = locals ()[option ].value
126+
127+ # assemble values provided by the user
128+ data = {}
129+ for (
130+ option
131+ ) in "project_name package_name user_name docs remote remote_url precommit bumpversion" .split ():
132+ value = locals ()[option ]
133+ if value is not None :
134+ logging .info ("%s: %s" , option , value )
135+ data [option ] = value
136+
28137 if (
29138 target_path .is_dir ()
30139 and any (target_path .iterdir ())
140+ and not always_confirm
31141 and not confirm (
32142 style (
33143 f"Target directory '{ target_path } ' is not empty! Continue?" ,
@@ -37,10 +147,29 @@ def cli(
37147 ):
38148 sys .exit (1 )
39149
150+ # parse copier args
151+ copier_args = {
152+ k .replace ("--" , "" ).replace ("-" , "_" ): v
153+ for k , v in (
154+ arg .split ("=" ) if "=" in arg else arg .split () if " " in arg else (arg , True )
155+ for arg in (copier_args or [])
156+ )
157+ }
158+
40159 run_copy (
41160 src_path = str (Path (__file__ ).parent .absolute ()),
42161 dst_path = target_path ,
43162 unsafe = True ,
163+ data = data ,
164+ user_defaults = dict (
165+ user_name = check_output (["whoami" ]).decode ().strip () if user_name is None else user_name ,
166+ project_name = target_path .name .replace ("-" , " " ).replace ("_" , " " ).title (),
167+ ),
168+ defaults = defaults ,
169+ overwrite = always_confirm ,
170+ pretend = dry_run or copier_args .pop ("pretend" , False ),
171+ quiet = True ,
172+ ** copier_args ,
44173 )
45174
46175
0 commit comments