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