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
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-04 11:35 +0000
1"""Set config files for somesy."""
3import logging
4from pathlib import Path
5from typing import Any
7import typer
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
18from .util import file_arg_config, wrap_exceptions
20logger = logging.getLogger("somesy")
21app = typer.Typer()
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] = {}
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)
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}")
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 )
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
83 return fallback
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}")
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()
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)}
145 # ----
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
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
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
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
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
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
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
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
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
210 # ----
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?")
218 set_log_level(
219 SomesyLogLevel.from_flags(
220 debug=options["debug"],
221 verbose=options["verbose"],
222 info=options["show_info"],
223 )
224 )
226 logger.debug(f"CLI options entered: {options}")
228 init_config(input_file, options)
229 logger.info(
230 f"[bold green]Input file is updated/created at {input_file}[/bold green]"
231 )