373def combineConfigFiles(local, config_path, fragment_key="include"):
374 """
375 Recursively combine configuration fragments into `local`.
376
377 - Looks for `fragment_key` at any dict node.
378 - If value is a string/path: merge that fragment.
379 - If value is a list: merge all fragments in order.
380 For conflicts between fragments, the **earlier** file in the list wins.
381 Local keys still override the merged fragments.
382
383 Returns True if any merging happened below this node.
384 """
385 combined = False
386
387
388 if isinstance(local, dict):
389 to_combine = local.values()
390 elif isinstance(local, list):
391 to_combine = local
392 else:
393 return combined
394
395
396 for sub in to_combine:
397 combined = combineConfigFiles(sub, config_path, fragment_key=fragment_key) or combined
398
399
400 if fragment_key not in local:
401 return combined
402
403
404 if not isinstance(local, dict):
405 return combined
406
407
408 value = local[fragment_key]
409 if isinstance(value, (str, pathlib.Path)):
410 warnings.warn(
411 f"{fragment_key} should be followed with a list of files",
412 TextConfigWarning,
413 stacklevel=2,
414 )
415 paths = [value]
416 elif isinstance(value, list):
417 paths = value
418 else:
419 raise TypeError(f"'{fragment_key}' must be a string path or a list of paths, got {type(value).__name__}")
420
421
422 fragments_acc = {}
423 for entry in paths:
424 fragment_path = _find_fragment(pathlib.Path(entry), config_path)
425 fragment = _load_fragment(fragment_path)
426
427
428 combineConfigFiles(fragment, fragment_path.parent, fragment_key=fragment_key)
429
430
431 _merge_dicts(fragments_acc, fragment)
432
433
434 del local[fragment_key]
435
436
437 _merge_dicts(local, fragments_acc)
438
439 return True
440
441