Coverage for src/somesy/cli/init.py: 61%

112 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-04 11:35 +0000

1"""Set config files for somesy.""" 

2 

3import logging 

4from pathlib import Path 

5from typing import Any 

6 

7import typer 

8 

9from somesy.commands import init_config, write_somesy_file 

10from somesy.core.core import discover_input 

11from somesy.core.log import SomesyLogLevel, set_log_level 

12from somesy.core.models import Person, SomesyConfig 

13from somesy.core.types import LicenseEnum 

14from somesy.git.harvest import harvest as harvest_git 

15from somesy.harvest import harvest_sources 

16from somesy.merge import merge_metadata 

17 

18from .util import file_arg_config, wrap_exceptions 

19 

20logger = logging.getLogger("somesy") 

21app = typer.Typer() 

22 

23 

24def _prompt_missing_metadata( 

25 sources: list[tuple[Path, dict[str, Any]]], git_metadata 

26) -> dict[str, Any]: 

27 """Prompt for required metadata that harvesting did not provide.""" 

28 harvested = [content for _, content in sources] 

29 if git_metadata is not None: 

30 harvested.append(git_metadata.model_dump(exclude_none=True)) 

31 fallback: dict[str, Any] = {} 

32 

33 for field, prompt in { 

34 "name": "Project name", 

35 "description": "Project description", 

36 }.items(): 

37 if not any(source.get(field) for source in harvested): 

38 fallback[field] = typer.prompt(prompt) 

39 

40 if not any(source.get("license") for source in harvested): 

41 while True: 

42 value = typer.prompt("SPDX license") 

43 try: 

44 fallback["license"] = LicenseEnum(value) 

45 break 

46 except ValueError: 

47 typer.echo(f"Unknown SPDX license: {value}") 

48 

49 def is_author(person: Any) -> bool: 

50 return ( 

51 person.get("author", False) 

52 if isinstance(person, dict) 

53 else getattr(person, "author", False) 

54 ) 

55 

56 has_author = any( 

57 any(is_author(person) for person in source.get(key, []) or []) 

58 for source in harvested 

59 for key in ("people", "entities", "authors") 

60 ) 

61 if not has_author: 

62 author_type = typer.prompt("Author type", type=str, default="person").lower() 

63 while author_type not in {"person", "entity"}: 

64 typer.echo("Author type must be 'person' or 'entity'.") 

65 author_type = typer.prompt("Author type", default="person").lower() 

66 if author_type == "person": 

67 person = { 

68 "given_names": typer.prompt("Author given names"), 

69 "family_names": typer.prompt("Author family names"), 

70 "author": True, 

71 } 

72 email = typer.prompt("Author email", default="") 

73 if email: 

74 person["email"] = email 

75 fallback["people"] = [Person(**person)] 

76 else: 

77 name = typer.prompt("Author organization") 

78 fallback["entities"] = [{"name": name, "author": True}] 

79 email = typer.prompt("Author email", default="") 

80 if email: 

81 fallback["entities"][0]["email"] = email 

82 

83 return fallback 

84 

85 

86@app.callback(invoke_without_command=True) 

87@wrap_exceptions 

88def initialize( 

89 ctx: typer.Context, 

90 output_file: Path = typer.Option( 

91 Path("somesy.toml"), 

92 "--output-file", 

93 "-o", 

94 help="Path for the generated somesy.toml file (default: somesy.toml).", 

95 **file_arg_config, 

96 ), 

97 overwrite: bool = typer.Option(False, "--overwrite"), 

98): 

99 """Harvest project metadata and create a somesy.toml file.""" 

100 if ctx.invoked_subcommand is not None: 

101 return 

102 root = Path.cwd() 

103 sources = harvest_sources(root) 

104 git_metadata = harvest_git(root) 

105 fallback = _prompt_missing_metadata(sources, git_metadata) 

106 source_content = [content for _, content in sources] 

107 if fallback: 

108 source_content.append(fallback) 

109 metadata = merge_metadata(source_content, git_metadata) 

110 source_names = {path.name for path, _ in sources} 

111 config = None 

112 if source_names: 

113 config = SomesyConfig.model_validate( 

114 { 

115 f"no_sync_{key}": filename not in source_names 

116 for key, filename in { 

117 "pyproject": "pyproject.toml", 

118 "package_json": "package.json", 

119 "julia": "Project.toml", 

120 "fortran": "fpm.toml", 

121 "pom_xml": "pom.xml", 

122 "mkdocs": "mkdocs.yml", 

123 "rust": "Cargo.toml", 

124 "cff": "CITATION.cff", 

125 "codemeta": "codemeta.json", 

126 }.items() 

127 } 

128 ) 

129 output = output_file if output_file.is_absolute() else root / output_file 

130 write_somesy_file(metadata, output, config=config, overwrite=overwrite) 

131 typer.echo(f"Created {output}") 

132 

133 

134@app.command() 

135@wrap_exceptions 

136def config(): 

137 """Set CLI configs for somesy.""" 

138 # check if input file exists, if not, try to find it from default list 

139 input_file_default = discover_input() 

140 

141 # prompt for inputs 

142 input_file = Path(typer.prompt("Input file path", default=input_file_default)) 

143 options: dict[str, Any] = {"input_file": Path(input_file)} 

144 

145 # ---- 

146 

147 options["no_sync_cff"] = not typer.confirm( 

148 "Do you want to sync to a CFF file?", default=True 

149 ) 

150 if cff_file := typer.prompt("CFF file path", default="CITATION.cff"): 

151 options["cff_file"] = cff_file 

152 

153 options["no_sync_codemeta"] = not typer.confirm( 

154 "Do you want to sync to a codemeta.json file?", default=True 

155 ) 

156 if codemeta_file := typer.prompt( 

157 "codemeta.json file path", default="codemeta.json" 

158 ): 

159 options["codemeta_file"] = codemeta_file 

160 

161 options["no_sync_pyproject"] = not typer.confirm( 

162 "Do you want to sync to a pyproject.toml file?", default=True 

163 ) 

164 if pyproject_file := typer.prompt( 

165 "pyproject.toml file path", default="pyproject.toml" 

166 ): 

167 options["pyproject_file"] = pyproject_file 

168 

169 options["sync_package_json"] = typer.confirm( 

170 "Do you want to sync to a package.json file?", default=False 

171 ) 

172 if package_json_file := typer.prompt( 

173 "package.json file path", default="package.json" 

174 ): 

175 options["package_json_file"] = package_json_file 

176 

177 options["no_sync_julia"] = not typer.confirm( 

178 "Do you want to sync to a Project.toml(Julia) file?", default=True 

179 ) 

180 if julia_file := typer.prompt( 

181 "Project.toml (Julia) file path", default="Project.toml" 

182 ): 

183 options["julia_file"] = julia_file 

184 

185 options["no_sync_fortran"] = not typer.confirm( 

186 "Do you want to sync to a fpm.toml(fortran) file?", default=True 

187 ) 

188 fortran_file = typer.prompt("fpm.toml(fortran) file path", default="fpm.toml") 

189 if fortran_file is not None or fortran_file != "": 

190 options["fortran_file"] = fortran_file 

191 

192 options["no_sync_pom_xml"] = not typer.confirm( 

193 "Do you want to sync to a pom.xml file?", default=True 

194 ) 

195 if pom_xml_file := typer.prompt("pom.xml file path", default="pom.xml"): 

196 options["pom_xml_file"] = pom_xml_file 

197 

198 options["no_sync_mkdocs"] = not typer.confirm( 

199 "Do you want to sync to a mkdocs.yml file?", default=True 

200 ) 

201 if mkdocs_file := typer.prompt("mkdocs.yml file path", default="mkdocs.yml"): 

202 options["mkdocs_file"] = mkdocs_file 

203 

204 options["no_sync_rust"] = not typer.confirm( 

205 "Do you want to sync to a Cargo.toml file?", default=True 

206 ) 

207 if rust_file := typer.prompt("Cargo.toml file path", default="Cargo.toml"): 

208 options["rust_file"] = rust_file 

209 

210 # ---- 

211 

212 options["show_info"] = typer.confirm( 

213 "Do you want to show info about the sync process?" 

214 ) 

215 options["verbose"] = typer.confirm("Do you want to show verbose logs?") 

216 options["debug"] = typer.confirm("Do you want to show debug logs?") 

217 

218 set_log_level( 

219 SomesyLogLevel.from_flags( 

220 debug=options["debug"], 

221 verbose=options["verbose"], 

222 info=options["show_info"], 

223 ) 

224 ) 

225 

226 logger.debug(f"CLI options entered: {options}") 

227 

228 init_config(input_file, options) 

229 logger.info( 

230 f"[bold green]Input file is updated/created at {input_file}[/bold green]" 

231 )