88
99from somesy .cff .writer import CFF
1010from somesy .codemeta import CodeMeta
11- from somesy .core .models import ProjectMetadata , SomesyInput
11+ from somesy .core .core import INPUT_FILES_ORDERED
12+ from somesy .core .models import ProjectMetadata , SomesyConfig , SomesyInput
1213from somesy .core .writer import ProjectMetadataWriter
1314from somesy .fortran .writer import Fortran
1415from somesy .julia .writer import Julia
@@ -40,23 +41,114 @@ def _sync_file(
4041 logger .verbose (f"Saved synced '{ file .name } '.\n " )
4142
4243
43- def _sync_files (metadata , files , writer_class , ** kwargs ):
44+ def _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+ """
4457 if isinstance (files , Path ):
4558 files = [files ]
4659 for file in files :
47- if file .is_file ():
60+ if file .is_file () or create_if_missing :
4861 _sync_file (metadata , file , writer_class , ** kwargs )
4962
5063
51- def sync (somesy_input : SomesyInput ):
52- """Sync selected metadata files with given input file."""
64+ def 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+ """
5372 conf , metadata = somesy_input .config , somesy_input .project
5473
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+
5591 pp_metadata = pretty_repr (metadata .model_dump (exclude_defaults = True ))
5692 logger .debug (f"Project metadata: { pp_metadata } " )
5793
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+
149+ def _sync_root_project (conf : SomesyConfig , metadata : ProjectMetadata ):
150+ """Sync metadata files for the root project."""
58151 # update these only if they exist:
59-
60152 if conf .pyproject_file and not conf .no_sync_pyproject :
61153 _sync_files (
62154 metadata ,
@@ -119,6 +211,7 @@ def sync(somesy_input: SomesyInput):
119211 metadata ,
120212 conf .cff_file ,
121213 CFF ,
214+ create_if_missing = True ,
122215 pass_validation = conf .pass_validation ,
123216 )
124217
@@ -127,6 +220,7 @@ def sync(somesy_input: SomesyInput):
127220 metadata ,
128221 conf .codemeta_file ,
129222 CodeMeta ,
223+ create_if_missing = True ,
130224 merge_codemeta = conf .merge_codemeta ,
131225 pass_validation = conf .pass_validation ,
132226 )
0 commit comments