From bd5f148e7598349f039d4e45c58071053fa5b602 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Mon, 7 Oct 2024 14:49:57 -0700 Subject: [PATCH] Add YamlProvider.order_mode natrual is default, adds simple --- CHANGELOG.md | 5 +++++ octodns/provider/yaml.py | 14 ++++++++++++-- octodns/yaml.py | 34 +++++++++++++++++++++++++++++----- tests/test_octodns_yaml.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e644bf..3afa75e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## v1.?.? - 2024-??-?? - ??? + +* Add YamlProvider.order_mode to allow picking between natrual (human) + the default when enforce_order=True and simple `sort`. + ## v1.10.0 - 2024-10-06 - Lots of little stuff * Zone name validation checking for double dots, and throwing InvalidNameError diff --git a/octodns/provider/yaml.py b/octodns/provider/yaml.py index 3fc6102..57f2745 100644 --- a/octodns/provider/yaml.py +++ b/octodns/provider/yaml.py @@ -34,6 +34,11 @@ class YamlProvider(BaseProvider): # Whether or not to enforce sorting order when loading yaml # (optional, default True) enforce_order: true + # What sort mode to employ when enforcing order + # - simple: `sort` + # - natual: https://pypi.org/project/natsort/ + # (optional, default natual) + order_mode: natrual # Whether duplicate records should replace rather than error # (optional, default False) @@ -174,6 +179,7 @@ class YamlProvider(BaseProvider): directory, default_ttl=3600, enforce_order=True, + order_mode='natrual', populate_should_replace=False, supports_root_ns=True, split_extension=False, @@ -186,11 +192,12 @@ class YamlProvider(BaseProvider): klass = self.__class__.__name__ self.log = logging.getLogger(f'{klass}[{id}]') self.log.debug( - '__init__: id=%s, directory=%s, default_ttl=%d, enforce_order=%d, populate_should_replace=%s, supports_root_ns=%s, split_extension=%s, split_catchall=%s, shared_filename=%s, disable_zonefile=%s', + '__init__: id=%s, directory=%s, default_ttl=%d, enforce_order=%d, order_mode=%s, populate_should_replace=%s, supports_root_ns=%s, split_extension=%s, split_catchall=%s, shared_filename=%s, disable_zonefile=%s', id, directory, default_ttl, enforce_order, + order_mode, populate_should_replace, supports_root_ns, split_extension, @@ -202,6 +209,7 @@ class YamlProvider(BaseProvider): self.directory = directory self.default_ttl = default_ttl self.enforce_order = enforce_order + self.order_mode = order_mode self.populate_should_replace = populate_should_replace self.supports_root_ns = supports_root_ns self.split_extension = split_extension @@ -304,7 +312,9 @@ class YamlProvider(BaseProvider): def _populate_from_file(self, filename, zone, lenient): with open(filename, 'r') as fh: - yaml_data = safe_load(fh, enforce_order=self.enforce_order) + yaml_data = safe_load( + fh, enforce_order=self.enforce_order, order_mode=self.order_mode + ) if yaml_data: for name, data in yaml_data.items(): if not isinstance(data, list): diff --git a/octodns/yaml.py b/octodns/yaml.py index 433486a..8040849 100644 --- a/octodns/yaml.py +++ b/octodns/yaml.py @@ -44,11 +44,12 @@ ContextLoader.add_constructor( # Found http://stackoverflow.com/a/21912744 which guided me on how to hook in # here class SortEnforcingLoader(ContextLoader): + def _construct(self, node): ret, pairs, context = self._pairs(node) keys = [d[0] for d in pairs] - keys_sorted = sorted(keys, key=_natsort_key) + keys_sorted = sorted(keys, key=self.KEYGEN) for key in keys: expected = keys_sorted.pop(0) if key != expected: @@ -62,13 +63,36 @@ class SortEnforcingLoader(ContextLoader): return ret -SortEnforcingLoader.add_constructor( - SortEnforcingLoader.DEFAULT_MAPPING_TAG, SortEnforcingLoader._construct +class NaturalSortEnforcingLoader(SortEnforcingLoader): + KEYGEN = _natsort_key + + +NaturalSortEnforcingLoader.add_constructor( + SortEnforcingLoader.DEFAULT_MAPPING_TAG, + NaturalSortEnforcingLoader._construct, ) -def safe_load(stream, enforce_order=True): - return load(stream, SortEnforcingLoader if enforce_order else ContextLoader) +class SimpleSortEnforcingLoader(SortEnforcingLoader): + KEYGEN = lambda _, s: s + + +SimpleSortEnforcingLoader.add_constructor( + SortEnforcingLoader.DEFAULT_MAPPING_TAG, + SimpleSortEnforcingLoader._construct, +) + + +def safe_load(stream, enforce_order=True, order_mode='natrual'): + if enforce_order: + loader = { + 'natrual': NaturalSortEnforcingLoader, + 'simple': SimpleSortEnforcingLoader, + }[order_mode] + else: + loader = ContextLoader + + return load(stream, loader) class SortingDumper(SafeDumper): diff --git a/tests/test_octodns_yaml.py b/tests/test_octodns_yaml.py index 396d59c..7015716 100644 --- a/tests/test_octodns_yaml.py +++ b/tests/test_octodns_yaml.py @@ -86,3 +86,32 @@ class TestYaml(TestCase): "[Errno 2] No such file or directory: 'tests/config/include/does-not-exist.yaml'", str(ctx.exception), ) + + def test_order_mode(self): + self.assertEqual( + {'*.1.2': 'a', '*.10.1': 'c', '*.11.2': 'd', '*.2.2': 'b'}, + safe_load( + ''' +'*.1.2': 'a' +'*.10.1': 'c' +'*.11.2': 'd' +'*.2.2': 'b' +''', + order_mode='simple', + ), + ) + # natrual sort throws error + with self.assertRaises(ConstructorError) as ctx: + safe_load( + ''' +'*.1.2': 'a' +'*.2.2': 'b' +'*.10.1': 'c' +'*.11.2': 'd' +''', + order_mode='simple', + ) + self.assertTrue( + 'keys out of order: expected *.10.1 got *.2.2 at' + in ctx.exception.problem + )