Coverage for src/somesy/commands/sync.py: 97%
93 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"""Sync selected metadata files with given input file."""
3import logging
4from copy import deepcopy
5from pathlib import Path
7from rich.pretty import pretty_repr
9from somesy.cff.writer import CFF
10from somesy.codemeta import CodeMeta
11from somesy.core.core import INPUT_FILES_ORDERED
12from somesy.core.log import VERBOSE
13from somesy.core.models import ProjectMetadata, SomesyConfig, SomesyInput
14from somesy.core.writer import ProjectMetadataWriter
15from somesy.fortran.writer import Fortran
16from somesy.julia.writer import Julia
17from somesy.mkdocs import MkDocs
18from somesy.package_json.writer import PackageJSON
19from somesy.pom_xml.writer import POM
20from somesy.pyproject.writer import Pyproject
21from somesy.rust import Rust
23logger = logging.getLogger("somesy")
26def _sync_file(
27 metadata: ProjectMetadata,
28 file: Path,
29 writer_cls: type[ProjectMetadataWriter],
30 merge_codemeta: bool | None = False,
31 pass_validation: bool | None = False,
32):
33 """Sync metadata to a file using the provided writer."""
34 logger.log(VERBOSE, f"Loading '{file.name}' ...")
35 if writer_cls == CodeMeta:
36 writer: ProjectMetadataWriter = writer_cls(
37 file, merge=merge_codemeta, pass_validation=pass_validation
38 )
39 else:
40 writer = writer_cls(file, pass_validation=pass_validation)
41 logger.log(VERBOSE, f"Syncing '{file.name}' ...")
42 original_data = deepcopy(writer._data)
43 writer.sync(metadata)
44 if writer._data != original_data:
45 writer.save(file)
46 logger.log(VERBOSE, f"Saved synced '{file.name}'.\n")
49def _sync_files(
50 metadata, files, writer_class, create_if_missing: bool = False, **kwargs
51):
52 """Sync metadata to files using the provided writer.
54 Args:
55 metadata: Project metadata to sync
56 files: Path or list of paths to sync
57 writer_class: Writer class to use
58 create_if_missing: Whether to create the file if it doesn't exist
59 **kwargs: Additional arguments passed to the writer
61 """
62 if isinstance(files, Path):
63 files = [files]
64 for file in files:
65 if file.is_file() or create_if_missing:
66 _sync_file(metadata, file, writer_class, **kwargs)
69def sync(somesy_input: SomesyInput, is_package: bool = False):
70 """Sync selected metadata files with given input file.
72 Args:
73 somesy_input: The input configuration and metadata to sync
74 is_package: Whether this is a package (subfolder) being synced
76 """
77 conf, metadata = somesy_input.config, somesy_input.project
79 # Get the base directory from the input file's location
80 if somesy_input._origin is None:
81 logger.warning(
82 "No origin found for somesy input, using current working directory."
83 )
84 base_dir = Path.cwd()
85 else:
86 base_dir = somesy_input._origin.parent
88 # Resolve all paths in the config relative to the base directory
89 conf.resolve_paths(base_dir)
91 if is_package:
92 logger.info("\n[bold green]Synchronizing package metadata...[/bold green]")
93 else:
94 logger.info("\n[bold green]Synchronizing root project metadata...[/bold green]")
96 pp_metadata = pretty_repr(metadata.model_dump(exclude_defaults=True))
97 logger.debug(f"Project metadata: {pp_metadata}")
99 # First sync the current project
100 _sync_root_project(conf, metadata)
102 # Then sync each package if defined
103 if conf.packages:
104 packages = [conf.packages] if isinstance(conf.packages, Path) else conf.packages
105 for package in packages:
106 logger.info(f"\n[bold blue]Processing package {package}...[/bold blue]")
108 # Try all possible input files in order of priority
109 config_files = [package / file for file in INPUT_FILES_ORDERED]
110 package_input = None
111 config_file: Path | None = None
113 for config_file in config_files:
114 try:
115 package_input = SomesyInput.from_input_file(config_file)
116 logger.debug(f"Found config file: {config_file}")
117 break
118 except (FileNotFoundError, RuntimeError):
119 continue
121 if package_input is None:
122 logger.warning(
123 f"No valid somesy config found in package {package} "
124 f"(tried: {', '.join(str(f) for f in config_files)})"
125 )
126 continue
128 if config_file is None:
129 continue
131 # Create new config with CLI options and package's input file
132 cli_options = {
133 "no_sync_pyproject": conf.no_sync_pyproject,
134 "no_sync_package_json": conf.no_sync_package_json,
135 "no_sync_julia": conf.no_sync_julia,
136 "no_sync_fortran": conf.no_sync_fortran,
137 "no_sync_pom_xml": conf.no_sync_pom_xml,
138 "no_sync_mkdocs": conf.no_sync_mkdocs,
139 "no_sync_rust": conf.no_sync_rust,
140 "no_sync_cff": conf.no_sync_cff,
141 "no_sync_codemeta": conf.no_sync_codemeta,
142 "merge_codemeta": conf.merge_codemeta,
143 "pass_validation": conf.pass_validation,
144 "packages": None, # Don't pass packages to avoid recursive package handling
145 }
146 package_input.config = SomesyConfig(input_file=config_file, **cli_options)
148 # Set default CFF and CodeMeta paths in package directory if not specified
149 if not package_input.config.no_sync_cff:
150 package_input.config.cff_file = Path("CITATION.cff")
151 if not package_input.config.no_sync_codemeta:
152 package_input.config.codemeta_file = Path("codemeta.json")
154 # Recursively call sync on the package
155 sync(package_input, is_package=True)
158def _sync_root_project(conf: SomesyConfig, metadata: ProjectMetadata):
159 """Sync metadata files for the root project."""
160 # update these only if they exist:
161 if conf.pyproject_file and not conf.no_sync_pyproject:
162 _sync_files(
163 metadata,
164 conf.pyproject_file,
165 Pyproject,
166 pass_validation=conf.pass_validation,
167 )
169 if conf.package_json_file and not conf.no_sync_package_json:
170 _sync_files(
171 metadata,
172 conf.package_json_file,
173 PackageJSON,
174 pass_validation=conf.pass_validation,
175 )
177 if conf.julia_file and not conf.no_sync_julia:
178 _sync_files(
179 metadata,
180 conf.julia_file,
181 Julia,
182 pass_validation=conf.pass_validation,
183 )
185 if conf.fortran_file and not conf.no_sync_fortran:
186 _sync_files(
187 metadata,
188 conf.fortran_file,
189 Fortran,
190 pass_validation=conf.pass_validation,
191 )
193 if conf.pom_xml_file and not conf.no_sync_pom_xml:
194 _sync_files(
195 metadata,
196 conf.pom_xml_file,
197 POM,
198 pass_validation=conf.pass_validation,
199 )
201 if conf.mkdocs_file and not conf.no_sync_mkdocs:
202 _sync_files(
203 metadata,
204 conf.mkdocs_file,
205 MkDocs,
206 pass_validation=conf.pass_validation,
207 )
209 if conf.rust_file and not conf.no_sync_rust:
210 _sync_files(
211 metadata,
212 conf.rust_file,
213 Rust,
214 pass_validation=conf.pass_validation,
215 )
217 # create these by default if they are missing:
218 if not conf.no_sync_cff:
219 _sync_files(
220 metadata,
221 conf.cff_file,
222 CFF,
223 create_if_missing=True,
224 pass_validation=conf.pass_validation,
225 )
227 if not conf.no_sync_codemeta:
228 _sync_files(
229 metadata,
230 conf.codemeta_file,
231 CodeMeta,
232 create_if_missing=True,
233 merge_codemeta=conf.merge_codemeta,
234 pass_validation=conf.pass_validation,
235 )