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

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

2 

3import logging 

4from pathlib import Path 

5from typing import Optional, Type 

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.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 

21 

22logger = logging.getLogger("somesy") 

23 

24 

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

42 

43 

44def _sync_files( 

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

46): 

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

48 

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 

55 

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) 

62 

63 

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

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

66 

67 Args: 

68 somesy_input: The input configuration and metadata to sync 

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

70 

71 """ 

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

73 

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

82 

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

84 conf.resolve_paths(base_dir) 

85 

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

90 

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

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

93 

94 # First sync the current project 

95 _sync_root_project(conf, metadata) 

96 

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

102 

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 

106 

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 

114 

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 

121 

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) 

138 

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

144 

145 # Recursively call sync on the package 

146 sync(package_input, is_package=True) 

147 

148 

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 ) 

159 

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 ) 

167 

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 ) 

175 

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 ) 

183 

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 ) 

191 

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 ) 

199 

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 ) 

207 

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 ) 

217 

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 )