|
11 | 11 | from collections import defaultdict |
12 | 12 | from dataclasses import dataclass |
13 | 13 | import importlib |
| 14 | +import json |
14 | 15 | import logging |
15 | 16 | import os |
16 | 17 | import stat |
@@ -155,13 +156,22 @@ def _combine_into(d: dict, combined: dict) -> dict: |
155 | 156 | return combined |
156 | 157 |
|
157 | 158 |
|
158 | | -def _load_yaml_config(settings_filenames) -> dict: |
| 159 | +def _load_config_files(settings_filenames) -> dict: |
159 | 160 | global config_files |
160 | 161 | config_files += settings_filenames |
161 | 162 | config = nested_dict() |
162 | 163 | for settings_filename in settings_filenames: |
163 | 164 | with open(settings_filename, encoding="utf-8") as settings_file: |
164 | | - settings = yaml.safe_load(settings_file) |
| 165 | + try: |
| 166 | + settings = json.load(settings_file) |
| 167 | + except json.JSONDecodeError: |
| 168 | + settings_file.seek(0) |
| 169 | + try: |
| 170 | + settings = yaml.safe_load(settings_file) |
| 171 | + except yaml.YAMLError as e: |
| 172 | + message = f"Failed to parse config file {settings_filename} as JSON or YAML: {e}" |
| 173 | + logging.error(message) |
| 174 | + raise ValueError(message) from e |
165 | 175 | if settings is not None: |
166 | 176 | if _key_exists(settings, "api_key"): |
167 | 177 | if platform.system() == "Windows": |
@@ -216,7 +226,7 @@ def _load_yaml_config(settings_filenames) -> dict: |
216 | 226 |
|
217 | 227 | def _store_config(settings_files) -> None: |
218 | 228 | global system, run, plugins, reporting, version |
219 | | - settings = _load_yaml_config(settings_files) |
| 229 | + settings = _load_config_files(settings_files) |
220 | 230 | system = _set_settings(system, settings["system"]) |
221 | 231 | run = _set_settings(run, settings["run"]) |
222 | 232 | run.user_agent = run.user_agent.replace("{version}", version) |
@@ -292,27 +302,99 @@ def load_config( |
292 | 302 |
|
293 | 303 | settings_files = [str(transient.package_dir / "resources" / "garak.core.yaml")] |
294 | 304 |
|
295 | | - fq_site_config_filename = str(transient.config_dir / site_config_filename) |
296 | | - if os.path.isfile(fq_site_config_filename): |
297 | | - settings_files.append(fq_site_config_filename) |
| 305 | + if site_config_filename == "garak.site.yaml": |
| 306 | + site_config_json = str(transient.config_dir / "garak.site.json") |
| 307 | + site_config_yaml = str(transient.config_dir / "garak.site.yaml") |
| 308 | + site_config_yml = str(transient.config_dir / "garak.site.yml") |
| 309 | + |
| 310 | + has_json = os.path.isfile(site_config_json) |
| 311 | + has_yaml = os.path.isfile(site_config_yaml) |
| 312 | + has_yml = os.path.isfile(site_config_yml) |
| 313 | + |
| 314 | + if sum([has_json, has_yaml, has_yml]) > 1: |
| 315 | + message = "Multiple site config files found (garak.site.json, garak.site.yaml, garak.site.yml). Please use only one site config format." |
| 316 | + logging.error(message) |
| 317 | + raise ValueError(message) |
| 318 | + elif has_json: |
| 319 | + settings_files.append(site_config_json) |
| 320 | + elif has_yaml: |
| 321 | + settings_files.append(site_config_yaml) |
| 322 | + elif has_yml: |
| 323 | + settings_files.append(site_config_yml) |
| 324 | + else: |
| 325 | + logging.debug( |
| 326 | + "no site config found at: %s, %s, or %s", |
| 327 | + site_config_json, |
| 328 | + site_config_yaml, |
| 329 | + site_config_yml, |
| 330 | + ) |
298 | 331 | else: |
299 | | - # warning, not error, because this one has a default value |
300 | | - logging.debug("no site config found at: %s", fq_site_config_filename) |
| 332 | + fq_site_config_filename = str(transient.config_dir / site_config_filename) |
| 333 | + if os.path.isfile(fq_site_config_filename): |
| 334 | + settings_files.append(fq_site_config_filename) |
| 335 | + else: |
| 336 | + logging.debug("no site config found at: %s", fq_site_config_filename) |
301 | 337 |
|
302 | 338 | if run_config_filename is not None: |
303 | | - # take config file path as provided |
| 339 | + # If file exists as-is, use it |
304 | 340 | if os.path.isfile(run_config_filename): |
305 | 341 | settings_files.append(run_config_filename) |
306 | | - elif os.path.isfile( |
307 | | - str(transient.package_dir / "configs" / (run_config_filename + ".yaml")) |
308 | | - ): |
309 | | - settings_files.append( |
310 | | - str(transient.package_dir / "configs" / (run_config_filename + ".yaml")) |
311 | | - ) |
| 342 | + # If explicit extension, check bundled |
| 343 | + elif run_config_filename.lower().endswith((".json", ".yaml", ".yml")): |
| 344 | + bundled = str(transient.package_dir / "configs" / run_config_filename) |
| 345 | + if os.path.isfile(bundled): |
| 346 | + settings_files.append(bundled) |
| 347 | + else: |
| 348 | + message = f"run config not found: {run_config_filename}" |
| 349 | + logging.error(message) |
| 350 | + raise FileNotFoundError(message) |
| 351 | + # Extension-less: JSON-only, YAML needs explicit .yaml/.yml |
312 | 352 | else: |
313 | | - message = f"run config not found: {run_config_filename}" |
314 | | - logging.error(message) |
315 | | - raise FileNotFoundError(message) |
| 353 | + json_path = run_config_filename + ".json" |
| 354 | + yaml_path = run_config_filename + ".yaml" |
| 355 | + yml_path = run_config_filename + ".yml" |
| 356 | + json_bundled = str( |
| 357 | + transient.package_dir / "configs" / (run_config_filename + ".json") |
| 358 | + ) |
| 359 | + yaml_bundled = str( |
| 360 | + transient.package_dir / "configs" / (run_config_filename + ".yaml") |
| 361 | + ) |
| 362 | + yml_bundled = str( |
| 363 | + transient.package_dir / "configs" / (run_config_filename + ".yml") |
| 364 | + ) |
| 365 | + |
| 366 | + has_json = os.path.isfile(json_path) |
| 367 | + has_yaml = os.path.isfile(yaml_path) |
| 368 | + has_yml = os.path.isfile(yml_path) |
| 369 | + has_json_bundled = os.path.isfile(json_bundled) |
| 370 | + has_yaml_bundled = os.path.isfile(yaml_bundled) |
| 371 | + has_yml_bundled = os.path.isfile(yml_bundled) |
| 372 | + |
| 373 | + # Direct path: JSON-only, warn if both exist |
| 374 | + if has_json or has_yaml or has_yml: |
| 375 | + if has_json and (has_yaml or has_yml): |
| 376 | + yaml_ext = ".yaml" if has_yaml else ".yml" |
| 377 | + logging.warning( |
| 378 | + f"Both {run_config_filename}.json and {yaml_ext} found. Using .json" |
| 379 | + ) |
| 380 | + if has_json: |
| 381 | + settings_files.append(json_path) |
| 382 | + else: |
| 383 | + yaml_ext = ".yaml" if has_yaml else ".yml" |
| 384 | + message = f"Found {run_config_filename}{yaml_ext} but YAML needs explicit .yaml/.yml extension" |
| 385 | + logging.error(message) |
| 386 | + raise FileNotFoundError(message) |
| 387 | + elif has_json_bundled: |
| 388 | + settings_files.append(json_bundled) |
| 389 | + elif has_yaml_bundled or has_yml_bundled: |
| 390 | + yaml_ext = ".yaml" if has_yaml_bundled else ".yml" |
| 391 | + message = f"Found {run_config_filename}{yaml_ext} but YAML needs explicit .yaml/.yml extension" |
| 392 | + logging.error(message) |
| 393 | + raise FileNotFoundError(message) |
| 394 | + else: |
| 395 | + message = f"run config not found: {run_config_filename}" |
| 396 | + logging.error(message) |
| 397 | + raise FileNotFoundError(message) |
316 | 398 |
|
317 | 399 | logging.debug("Loading configs from: %s", ",".join(settings_files)) |
318 | 400 | _store_config(settings_files=settings_files) |
|
0 commit comments