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