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

1"""Sync selected metadata files with given input file.""" 

2 

3import logging 

4from copy import deepcopy 

5from pathlib import Path 

6 

7from rich.pretty import pretty_repr 

8 

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 

22 

23logger = logging.getLogger("somesy") 

24 

25 

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") 

47 

48 

49def _sync_files( 

50 metadata, files, writer_class, create_if_missing: bool = False, **kwargs 

51): 

52 """Sync metadata to files using the provided writer. 

53 

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 

60 

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) 

67 

68 

69def sync(somesy_input: SomesyInput, is_package: bool = False): 

70 """Sync selected metadata files with given input file. 

71 

72 Args: 

73 somesy_input: The input configuration and metadata to sync 

74 is_package: Whether this is a package (subfolder) being synced 

75 

76 """ 

77 conf, metadata = somesy_input.config, somesy_input.project 

78 

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 

87 

88 # Resolve all paths in the config relative to the base directory 

89 conf.resolve_paths(base_dir) 

90 

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]") 

95 

96 pp_metadata = pretty_repr(metadata.model_dump(exclude_defaults=True)) 

97 logger.debug(f"Project metadata: {pp_metadata}") 

98 

99 # First sync the current project 

100 _sync_root_project(conf, metadata) 

101 

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]") 

107 

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 

112 

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 

120 

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 

127 

128 if config_file is None: 

129 continue 

130 

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) 

147 

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") 

153 

154 # Recursively call sync on the package 

155 sync(package_input, is_package=True) 

156 

157 

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 ) 

168 

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 ) 

176 

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 ) 

184 

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 ) 

192 

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 ) 

200 

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 ) 

208 

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 ) 

216 

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 ) 

226 

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 )