comparison configmix/__init__.py @ 741:e069797f0e36

Implemented the new merge stragegies when merging lists: "extend" and "prepend"
author Franz Glasner <fzglas.hg@dom66.de>
date Sun, 29 Oct 2023 17:15:41 +0100
parents 324ae9a56a75
children e4fad9cdd906
comparison
equal deleted inserted replaced
740:cc9dff5fe0ca 741:e069797f0e36
55 settings from commandline arguments. 55 settings from commandline arguments.
56 :type extras: dict-alike or None 56 :type extras: dict-alike or None
57 :keyword strict: enable strict parsing mode for parsers that support it 57 :keyword strict: enable strict parsing mode for parsers that support it
58 (e.g. to prevent duplicate keys) 58 (e.g. to prevent duplicate keys)
59 :type strict: bool 59 :type strict: bool
60 :keyword merge_lists: When ``None`` then lists will be overwritten
61 by the merge process. When ``extend`` then
62 lists will be extended instead.
63 This parameter is passed to :func:`.merge`.
64 :type merge_lists: str or None
60 :returns: the configuration 65 :returns: the configuration
61 :rtype: ~configmix.config.Configuration 66 :rtype: ~configmix.config.Configuration
62 67
63 """ 68 """
64 defaults = kwargs.get("defaults") 69 defaults = kwargs.get("defaults")
65 extras = kwargs.get("extras") 70 extras = kwargs.get("extras")
66 strict = kwargs.get("strict", False) 71 strict = kwargs.get("strict", False)
72 merge_lists = kwargs.get("merge_lists", None)
67 if defaults is None: 73 if defaults is None:
68 ex = Configuration() 74 ex = Configuration()
69 else: 75 else:
70 ex = merge(None, Configuration(defaults)) 76 ex = merge(None, Configuration(defaults), merge_lists=merge_lists)
71 for f in files: 77 for f in files:
72 if f.startswith(constants.DIR_PREFIX): 78 if f.startswith(constants.DIR_PREFIX):
73 for f2 in _get_configuration_files_from_dir(f[5:]): 79 for f2 in _get_configuration_files_from_dir(f[5:]):
74 nx = _load_cfg_from_file(f2, ignore_unknown=True, strict=strict) 80 nx = _load_cfg_from_file(f2, ignore_unknown=True, strict=strict)
75 if nx is not None: 81 if nx is not None:
76 ex = merge(nx, ex) 82 ex = merge(nx, ex, merge_lists=merge_lists)
77 else: 83 else:
78 nx = _load_cfg_from_file(f, strict=strict) 84 nx = _load_cfg_from_file(f, strict=strict)
79 if nx is not None: 85 if nx is not None:
80 ex = merge(nx, ex) 86 ex = merge(nx, ex, merge_lists=merge_lists)
81 if extras: 87 if extras:
82 ex = merge(Configuration(extras), ex) 88 ex = merge(Configuration(extras), ex, merge_lists=merge_lists)
83 return Configuration(ex) 89 return Configuration(ex)
84 90
85 91
86 def safe_load(*files, **kwargs): 92 def safe_load(*files, **kwargs):
87 """Analogous to :func:`load` but do merging with :func:`safe_merge` 93 """Analogous to :func:`load` but do merging with :func:`safe_merge`
89 95
90 """ 96 """
91 defaults = kwargs.get("defaults") 97 defaults = kwargs.get("defaults")
92 extras = kwargs.get("extras") 98 extras = kwargs.get("extras")
93 strict = kwargs.get("strict", False) 99 strict = kwargs.get("strict", False)
100 merge_lists = kwargs.get("merge_lists", None)
94 if defaults is None: 101 if defaults is None:
95 ex = Configuration() 102 ex = Configuration()
96 else: 103 else:
97 ex = safe_merge(None, Configuration(defaults)) 104 ex = safe_merge(None, Configuration(defaults), merge_lists=merge_lists)
98 for f in files: 105 for f in files:
99 if f.startswith(constants.DIR_PREFIX): 106 if f.startswith(constants.DIR_PREFIX):
100 for f2 in _get_configuration_files_from_dir(f[5:]): 107 for f2 in _get_configuration_files_from_dir(f[5:]):
101 nx = _load_cfg_from_file(f2, ignore_unknown=True, strict=strict) 108 nx = _load_cfg_from_file(f2, ignore_unknown=True, strict=strict)
102 if nx is not None: 109 if nx is not None:
103 ex = safe_merge(nx, ex) 110 ex = safe_merge(nx, ex, merge_lists=merge_lists)
104 else: 111 else:
105 nx = _load_cfg_from_file(f, strict=strict) 112 nx = _load_cfg_from_file(f, strict=strict)
106 if nx is not None: 113 if nx is not None:
107 ex = safe_merge(nx, ex) 114 ex = safe_merge(nx, ex, merge_lists=merge_lists)
108 if extras: 115 if extras:
109 ex = safe_merge(Configuration(extras), ex) 116 ex = safe_merge(Configuration(extras), ex, merge_lists=merge_lists)
110 return Configuration(ex) 117 return Configuration(ex)
111 118
112 119
113 def _get_configuration_files_from_dir(root): 120 def _get_configuration_files_from_dir(root):
114 """Returns the sorted list of files within directory `root` 121 """Returns the sorted list of files within directory `root`
387 else: 394 else:
388 result[k] = deepcopy(v) # noqa 395 result[k] = deepcopy(v) # noqa
389 return result 396 return result
390 397
391 398
392 def merge(user, default, filter_comments=True, extend_lists=False): 399 def merge(user, default, filter_comments=True, merge_lists=None):
393 """Logically merge the configuration in `user` into `default`. 400 """Logically merge the configuration in `user` into `default`.
394 401
395 :param ~configmix.config.Configuration user: 402 :param ~configmix.config.Configuration user:
396 the new configuration that will be logically merged 403 the new configuration that will be logically merged
397 into `default` 404 into `default`
398 :param ~configmix.config.Configuration default: 405 :param ~configmix.config.Configuration default:
399 the base configuration where `user` is logically merged into 406 the base configuration where `user` is logically merged into
400 :param bool filter_comments: flag whether to filter comment keys that 407 :param bool filter_comments: flag whether to filter comment keys that
401 start with any of the items in :data:`.COMMENTS` 408 start with any of the items in :data:`.COMMENTS`
402 :param bool extend_lists: When ``True`` then lists will be 409 :param merge_lists: When ``None`` then lists will be overwritten
403 extended instead of overwritten by the 410 by the merge process. When ``extend`` then
404 merge process 411 lists will be extended instead.
412 :type merge_lists: str or None
405 :returns: `user` with the necessary amendments from `default`. 413 :returns: `user` with the necessary amendments from `default`.
406 If `user` is ``None`` then `default` is returned. 414 If `user` is ``None`` then `default` is returned.
407 415
408 .. note:: The configuration in `user` is augmented/changed 416 .. note:: The configuration in `user` is augmented/changed
409 **inplace**. 417 **inplace**.
436 ukv = user[k] 444 ukv = user[k]
437 if ukv == constants.DEL_VALUE: 445 if ukv == constants.DEL_VALUE:
438 # do not copy 446 # do not copy
439 del user[k] 447 del user[k]
440 else: 448 else:
441 user[k] = _merge(ukv, v, filter_comments, extend_lists) 449 user[k] = _merge(ukv, v, filter_comments, merge_lists)
442 else: 450 else:
443 user[k] = v 451 user[k] = v
452 elif merge_lists is not None:
453 if merge_lists == "extend":
454 if isinstance(user, list) and isinstance(default, list):
455 for idx, value in enumerate(default):
456 user.insert(idx, value)
457 elif merge_lists == "prepend":
458 if isinstance(user, list) and isinstance(default, list):
459 user.extend(default)
460 else:
461 raise ValueError(
462 "unknown strategy for merge_lists: %s" % (merge_lists,))
444 _filter_deletions(user) 463 _filter_deletions(user)
445 return user 464 return user
446 465
447 466
448 def _merge(user, default, filter_comments, extend_lists): 467 def _merge(user, default, filter_comments, merge_lists):
449 """Recursion helper for :func:`.merge` 468 """Recursion helper for :func:`.merge`
450 469
451 """ 470 """
452 if isinstance(user, dict) and isinstance(default, dict): 471 if isinstance(user, dict) and isinstance(default, dict):
453 for k, v in default.items(): 472 for k, v in default.items():
460 ukv = user[k] 479 ukv = user[k]
461 if ukv == constants.DEL_VALUE: 480 if ukv == constants.DEL_VALUE:
462 # do not copy 481 # do not copy
463 del user[k] 482 del user[k]
464 else: 483 else:
465 user[k] = _merge(ukv, v, filter_comments, extend_lists) 484 user[k] = _merge(ukv, v, filter_comments, merge_lists)
466 else: 485 else:
467 user[k] = v 486 user[k] = v
487 elif merge_lists is not None:
488 if merge_lists == "extend":
489 if isinstance(user, list) and isinstance(default, list):
490 for idx, value in enumerate(default):
491 user.insert(idx, value)
492 elif merge_lists == "prepend":
493 if isinstance(user, list) and isinstance(default, list):
494 user.extend(default)
495 else:
496 raise ValueError(
497 "unknown strategy for merge_lists: %s" % (merge_lists,))
468 return user 498 return user
469 499
470 500
471 def safe_merge(user, default, filter_comments=True, extend_lists=False): 501 def safe_merge(user, default, filter_comments=True, merge_lists=None):
472 """A more safe version of :func:`.merge` that makes deep copies of 502 """A more safe version of :func:`.merge` that makes deep copies of
473 the returned container objects. 503 the returned container objects.
474 504
475 Contrary to :func:`.merge` no given argument is ever changed 505 Contrary to :func:`.merge` no given argument is ever changed
476 inplace. Every object from `default` is decoupled from the result 506 inplace. Every object from `default` is decoupled from the result
497 ukv = user[k] 527 ukv = user[k]
498 if ukv == constants.DEL_VALUE: 528 if ukv == constants.DEL_VALUE:
499 # do not copy 529 # do not copy
500 del user[k] 530 del user[k]
501 else: 531 else:
502 user[k] = _safe_merge(ukv, v, filter_comments, extend_lists) 532 user[k] = _safe_merge(ukv, v, filter_comments, merge_lists)
503 else: 533 else:
504 user[k] = copy.deepcopy(v) 534 user[k] = copy.deepcopy(v)
535 elif merge_lists is not None:
536 if merge_lists == "extend":
537 if isinstance(user, list) and isinstance(default, list):
538 for idx, value in enumerate(default):
539 user.insert(idx, copy.deepcopy(value))
540 elif merge_lists == "prepend":
541 if isinstance(user, list) and isinstance(default, list):
542 user.extend(copy.deepcopy(default))
543 else:
544 raise ValueError(
545 "unknown strategy for merge_lists: %s" % (merge_lists,))
505 _filter_deletions(user) 546 _filter_deletions(user)
506 return user 547 return user
507 548
508 549
509 def _safe_merge(user, default, filter_comments, extend_lists): 550 def _safe_merge(user, default, filter_comments, merge_lists):
510 """Recursion helper for :func:`safe_merge` 551 """Recursion helper for :func:`safe_merge`
511 552
512 """ 553 """
513 if isinstance(user, dict) and isinstance(default, dict): 554 if isinstance(user, dict) and isinstance(default, dict):
514 for k, v in default.items(): 555 for k, v in default.items():
521 ukv = user[k] 562 ukv = user[k]
522 if ukv == constants.DEL_VALUE: 563 if ukv == constants.DEL_VALUE:
523 # do not copy 564 # do not copy
524 del user[k] 565 del user[k]
525 else: 566 else:
526 user[k] = _safe_merge(ukv, v, filter_comments, extend_lists) 567 user[k] = _safe_merge(ukv, v, filter_comments, merge_lists)
527 else: 568 else:
528 user[k] = copy.deepcopy(v) 569 user[k] = copy.deepcopy(v)
570 elif merge_lists is not None:
571 if merge_lists == "extend":
572 if isinstance(user, list) and isinstance(default, list):
573 for idx, value in enumerate(default):
574 user.insert(idx, copy.deepcopy(value))
575 elif merge_lists == "prepend":
576 if isinstance(user, list) and isinstance(default, list):
577 user.extend(copy.deepcopy(default))
578 else:
579 raise ValueError(
580 "unknown strategy for merge_lists: %s" % (merge_lists,))
529 return user 581 return user
530 582
531 583
532 def _filter_comments(d): 584 def _filter_comments(d):
533 """Recursively filter comments keys in the dict `d`. 585 """Recursively filter comments keys in the dict `d`.