Coverage for src/somesy/commands/sync.py: 97%
88 statements
« prev ^ index » next coverage.py v7.6.0, created at 2025-03-10 14:56 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2025-03-10 14:56 +0000
1"""Sync selected metadata files with given input file."""
3import logging
4from pathlib import Path
5from typing import Optional, Type
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.models import ProjectMetadata, SomesyConfig, SomesyInput
13from somesy.core.writer import ProjectMetadataWriter
14from somesy.fortran.writer import Fortran
15from somesy.julia.writer import Julia
16from somesy.mkdocs import MkDocs
17from somesy.package_json.writer import PackageJSON
18from somesy.pom_xml.writer import POM
19from somesy.pyproject.writer import Pyproject
20from somesy.rust import Rust
22logger = logging.getLogger("somesy")
25def _sync_file(
26 metadata: ProjectMetadata,
27 file: Path,
28 writer_cls: Type[ProjectMetadataWriter],
29 merge_codemeta: Optional[bool] = False,
30 pass_validation: Optional[bool] = False,
31):
32 """Sync metadata to a file using the provided writer."""
33 logger.verbose(f"Loading '{file.name}' ...")
34 if writer_cls == CodeMeta:
35 writer = writer_cls(file, merge=merge_codemeta, pass_validation=pass_validation)
36 else:
37 writer = writer_cls(file, pass_validation=pass_validation)
38 logger.verbose(f"Syncing '{file.name}' ...")
39 writer.sync(metadata)
40 writer.save(file)
41 logger.verbose(f"Saved synced '{file.name}'.\n")
44def _sync_files(
45 metadata, files, writer_class, create_if_missing: bool = False, **kwargs
46):
47 """Sync metadata to files using the provided writer.
49 Args:
50 metadata: Project metadata to sync
51 files: Path or list of paths to sync
52 writer_class: Writer class to use
53 create_if_missing: Whether to create the file if it doesn't exist
54 **kwargs: Additional arguments passed to the writer
56 """
57 if isinstance(files, Path):
58 files = [files]
59 for file in files:
60 if file.is_file() or create_if_missing:
61 _sync_file(metadata, file, writer_class, **kwargs)
64def sync(somesy_input: SomesyInput, is_package: bool = False):
65 """Sync selected metadata files with given input file.
67 Args:
68 somesy_input: The input configuration and metadata to sync
69 is_package: Whether this is a package (subfolder) being synced
71 """
72 conf, metadata = somesy_input.config, somesy_input.project
74 # Get the base directory from the input file's location
75 try:
76 base_dir = somesy_input._origin.parent
77 except AttributeError:
78 logger.warning(
79 "No origin found for somesy input, using current working directory."
80 )
81 base_dir = Path.cwd()
83 # Resolve all paths in the config relative to the base directory
84 conf.resolve_paths(base_dir)
86 if is_package:
87 logger.info("\n[bold green]Synchronizing package metadata...[/bold green]")
88 else:
89 logger.info("\n[bold green]Synchronizing root project metadata...[/bold green]")
91 pp_metadata = pretty_repr(metadata.model_dump(exclude_defaults=True))
92 logger.debug(f"Project metadata: {pp_metadata}")
94 # First sync the current project
95 _sync_root_project(conf, metadata)
97 # Then sync each package if defined
98 if conf.packages:
99 packages = [conf.packages] if isinstance(conf.packages, Path) else conf.packages
100 for package in packages:
101 logger.info(f"\n[bold blue]Processing package {package}...[/bold blue]")
103 # Try all possible input files in order of priority
104 config_files = [package / file for file in INPUT_FILES_ORDERED]
105 package_input = None
107 for config_file in config_files:
108 try:
109 package_input = SomesyInput.from_input_file(config_file)
110 logger.debug(f"Found config file: {config_file}")
111 break
112 except (FileNotFoundError, RuntimeError):
113 continue
115 if package_input is None:
116 logger.warning(
117 f"No valid somesy config found in package {package} "
118 f"(tried: {', '.join(str(f) for f in config_files)})"
119 )
120 continue
122 # Create new config with CLI options and package's input file
123 cli_options = {
124 "no_sync_pyproject": conf.no_sync_pyproject,
125 "no_sync_package_json": conf.no_sync_package_json,
126 "no_sync_julia": conf.no_sync_julia,
127 "no_sync_fortran": conf.no_sync_fortran,
128 "no_sync_pom_xml": conf.no_sync_pom_xml,
129 "no_sync_mkdocs": conf.no_sync_mkdocs,
130 "no_sync_rust": conf.no_sync_rust,
131 "no_sync_cff": conf.no_sync_cff,
132 "no_sync_codemeta": conf.no_sync_codemeta,
133 "merge_codemeta": conf.merge_codemeta,
134 "pass_validation": conf.pass_validation,
135 "packages": None, # Don't pass packages to avoid recursive package handling
136 }
137 package_input.config = SomesyConfig(input_file=config_file, **cli_options)
139 # Set default CFF and CodeMeta paths in package directory if not specified
140 if not package_input.config.no_sync_cff:
141 package_input.config.cff_file = Path("CITATION.cff")
142 if not package_input.config.no_sync_codemeta:
143 package_input.config.codemeta_file = Path("codemeta.json")
145 # Recursively call sync on the package
146 sync(package_input, is_package=True)
149def _sync_root_project(conf: SomesyConfig, metadata: ProjectMetadata):
150 """Sync metadata files for the root project."""
151 # update these only if they exist:
152 if conf.pyproject_file and not conf.no_sync_pyproject:
153 _sync_files(
154 metadata,
155 conf.pyproject_file,
156 Pyproject,
157 pass_validation=conf.pass_validation,
158 )
160 if conf.package_json_file and not conf.no_sync_package_json:
161 _sync_files(
162 metadata,
163 conf.package_json_file,
164 PackageJSON,
165 pass_validation=conf.pass_validation,
166 )
168 if conf.julia_file and not conf.no_sync_julia:
169 _sync_files(
170 metadata,
171 conf.julia_file,
172 Julia,
173 pass_validation=conf.pass_validation,
174 )
176 if conf.fortran_file and not conf.no_sync_fortran:
177 _sync_files(
178 metadata,
179 conf.fortran_file,
180 Fortran,
181 pass_validation=conf.pass_validation,
182 )
184 if conf.pom_xml_file and not conf.no_sync_pom_xml:
185 _sync_files(
186 metadata,
187 conf.pom_xml_file,
188 POM,
189 pass_validation=conf.pass_validation,
190 )
192 if conf.mkdocs_file and not conf.no_sync_mkdocs:
193 _sync_files(
194 metadata,
195 conf.mkdocs_file,
196 MkDocs,
197 pass_validation=conf.pass_validation,
198 )
200 if conf.rust_file and not conf.no_sync_rust:
201 _sync_files(
202 metadata,
203 conf.rust_file,
204 Rust,
205 pass_validation=conf.pass_validation,
206 )
208 # create these by default if they are missing:
209 if not conf.no_sync_cff:
210 _sync_files(
211 metadata,
212 conf.cff_file,
213 CFF,
214 create_if_missing=True,
215 pass_validation=conf.pass_validation,
216 )
218 if not conf.no_sync_codemeta:
219 _sync_files(
220 metadata,
221 conf.codemeta_file,
222 CodeMeta,
223 create_if_missing=True,
224 merge_codemeta=conf.merge_codemeta,
225 pass_validation=conf.pass_validation,
226 )