-
Notifications
You must be signed in to change notification settings - Fork 5.6k
YAML documentation, test, and readability improvements #63158
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
rhansen
wants to merge
14
commits into
saltstack:3007.x
Choose a base branch
from
rhansen:yaml-cleanups
base: 3007.x
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
cb8d783
yaml: Fix documentation about `datetime` conversion
rhansen 91d8a4f
yaml: Document that `!!omap` should be avoided due to bugs
rhansen 9c917d7
yaml: Convert `salt.utils.yaml` tests to pytest
rhansen 27b8d5f
yaml: Add integration test for YAML map iteration order
rhansen 72c0036
yaml: Add TODO comments next to puzzling code
rhansen 4790a48
yaml: Delete unnecessary `IndentMixin` class to improve readability
rhansen 4c30716
yaml: Use a `for` loop to factor out duplicate code
rhansen e38272b
yaml: Register default representer with `OrderedDumper` too
rhansen e7c8792
yaml: Factor out duplicate code in `salt.utils.yaml.safe_dump()`
rhansen 1845cfb
yaml: Improve readability of `salt.utils.yaml.dump()`
rhansen 81cb55b
yaml: Delete the ineffectual timestamp representer
rhansen 6b0076a
tests: Use `salt.utils.yaml` to generate reference YAML
rhansen 9d38770
changelog: rename to .fixed.md format
dwoz 64b11cd
pre-commit: fix black formatting in yamldumper.py
dwoz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| Updated YAML idiosyncrasies documentation, improved YAML tests, and improved | ||
| readability of YAML code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,17 +35,6 @@ | |
| ] | ||
|
|
||
|
|
||
| class IndentMixin(Dumper): | ||
| """ | ||
| Mixin that improves YAML dumped list readability | ||
| by indenting them by two spaces, | ||
| instead of being flush with the key they are under. | ||
| """ | ||
|
|
||
| def increase_indent(self, flow=False, indentless=False): | ||
| return super().increase_indent(flow, False) | ||
|
|
||
|
|
||
| class OrderedDumper(Dumper): | ||
| """ | ||
| A YAML dumper that represents python OrderedDict as simple YAML map. | ||
|
|
@@ -58,11 +47,11 @@ class SafeOrderedDumper(SafeDumper): | |
| """ | ||
|
|
||
|
|
||
| class IndentedSafeOrderedDumper(IndentMixin, SafeOrderedDumper): | ||
| """ | ||
| A YAML safe dumper that represents python OrderedDict as simple YAML map, | ||
| and also indents lists by two spaces. | ||
| """ | ||
| class IndentedSafeOrderedDumper(SafeOrderedDumper): | ||
| """Like ``SafeOrderedDumper``, except it indents lists for readability.""" | ||
|
|
||
| def increase_indent(self, flow=False, indentless=False): | ||
| return super().increase_indent(flow, False) | ||
|
|
||
|
|
||
| def represent_ordereddict(dumper, data): | ||
|
|
@@ -89,56 +78,45 @@ def represent_listproxy(dumper, data): | |
| return dumper.represent_list(list(data)) | ||
|
|
||
|
|
||
| OrderedDumper.add_representer(OrderedDict, represent_ordereddict) | ||
| OrderedDumper.add_representer(HashableOrderedDict, represent_ordereddict) | ||
| SafeOrderedDumper.add_representer(OrderedDict, represent_ordereddict) | ||
| SafeOrderedDumper.add_representer(HashableOrderedDict, represent_ordereddict) | ||
| SafeOrderedDumper.add_representer(None, represent_undefined) | ||
| # OrderedDumper does not inherit from SafeOrderedDumper, so any applicable | ||
| # representers added to SafeOrderedDumper must also be explicitly added to | ||
| # OrderedDumper. | ||
| for D in (SafeOrderedDumper, OrderedDumper): | ||
| # This default registration matches types that don't match any other | ||
| # registration, overriding PyYAML's default behavior of raising an | ||
| # exception. This representer instead produces null nodes. | ||
| # | ||
| # TODO: Why does this registration exist? Isn't it better to raise an | ||
| # exception for unsupported types? | ||
| D.add_representer(None, represent_undefined) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This representer is only meant for the SafeOrderedDumper not the OrderedDumper. because it is meant to be "safe" alternative to blowing up aka throwing an exception. the OrderedDumper SHOULD blowup on unknown types but SafeOrderedDumper should just null gracefully. |
||
| D.add_representer(OrderedDict, represent_ordereddict) | ||
| D.add_representer(HashableOrderedDict, represent_ordereddict) | ||
| D.add_representer( | ||
| collections.defaultdict, yaml.representer.SafeRepresenter.represent_dict | ||
| ) | ||
| D.add_representer( | ||
| salt.utils.context.NamespacedDictWrapper, | ||
| yaml.representer.SafeRepresenter.represent_dict, | ||
| ) | ||
| D.add_representer(OptsDict, represent_optsdict) | ||
| D.add_representer(DictProxy, represent_dictproxy) | ||
| D.add_representer(ListProxy, represent_listproxy) | ||
| # Pillar containers are wrapped in MaskedDict / MaskedList for repr redaction; | ||
| # they are still plain dict / list at the data level, so dump them as such | ||
| # instead of falling through to represent_undefined (which would emit NULL). | ||
| D.add_representer(MaskedDict, yaml.representer.SafeRepresenter.represent_dict) | ||
| D.add_representer(MaskedList, yaml.representer.SafeRepresenter.represent_list) | ||
| del D | ||
|
|
||
| IndentedSafeOrderedDumper.add_representer(OrderedDict, represent_ordereddict) | ||
| IndentedSafeOrderedDumper.add_representer(HashableOrderedDict, represent_ordereddict) | ||
|
|
||
| OrderedDumper.add_representer( | ||
| collections.defaultdict, yaml.representer.SafeRepresenter.represent_dict | ||
| ) | ||
| SafeOrderedDumper.add_representer( | ||
| collections.defaultdict, yaml.representer.SafeRepresenter.represent_dict | ||
| ) | ||
| OrderedDumper.add_representer( | ||
| salt.utils.context.NamespacedDictWrapper, | ||
| yaml.representer.SafeRepresenter.represent_dict, | ||
| ) | ||
| SafeOrderedDumper.add_representer( | ||
| salt.utils.context.NamespacedDictWrapper, | ||
| yaml.representer.SafeRepresenter.represent_dict, | ||
| ) | ||
|
|
||
| OrderedDumper.add_representer(OptsDict, represent_optsdict) | ||
| SafeOrderedDumper.add_representer(OptsDict, represent_optsdict) | ||
| OrderedDumper.add_representer(DictProxy, represent_dictproxy) | ||
| SafeOrderedDumper.add_representer(DictProxy, represent_dictproxy) | ||
| OrderedDumper.add_representer(ListProxy, represent_listproxy) | ||
| SafeOrderedDumper.add_representer(ListProxy, represent_listproxy) | ||
| # Pillar containers are wrapped in MaskedDict / MaskedList for repr redaction; | ||
| # they are still plain dict / list at the data level, so dump them as such | ||
| # instead of falling through to represent_undefined (which would emit NULL). | ||
| OrderedDumper.add_representer( | ||
| MaskedDict, yaml.representer.SafeRepresenter.represent_dict | ||
| ) | ||
| SafeOrderedDumper.add_representer( | ||
| MaskedDict, yaml.representer.SafeRepresenter.represent_dict | ||
| ) | ||
| IndentedSafeOrderedDumper.add_representer( | ||
| MaskedDict, yaml.representer.SafeRepresenter.represent_dict | ||
| ) | ||
| OrderedDumper.add_representer( | ||
| MaskedList, yaml.representer.SafeRepresenter.represent_list | ||
| ) | ||
| SafeOrderedDumper.add_representer( | ||
| MaskedList, yaml.representer.SafeRepresenter.represent_list | ||
| ) | ||
| IndentedSafeOrderedDumper.add_representer( | ||
| MaskedList, yaml.representer.SafeRepresenter.represent_list | ||
| ) | ||
|
|
||
| # Also register with base YAML dumpers for salt.utils.yaml.dump() | ||
| yaml.Dumper.add_representer(OptsDict, represent_optsdict) | ||
| yaml.SafeDumper.add_representer(OptsDict, represent_optsdict) | ||
|
|
@@ -155,13 +133,6 @@ def represent_listproxy(dumper, data): | |
| MaskedList, yaml.representer.SafeRepresenter.represent_list | ||
| ) | ||
|
|
||
| OrderedDumper.add_representer( | ||
| "tag:yaml.org,2002:timestamp", OrderedDumper.represent_scalar | ||
| ) | ||
| SafeOrderedDumper.add_representer( | ||
| "tag:yaml.org,2002:timestamp", SafeOrderedDumper.represent_scalar | ||
| ) | ||
|
|
||
|
|
||
| def get_dumper(dumper_name): | ||
| return { | ||
|
|
@@ -178,8 +149,7 @@ def dump(data, stream=None, **kwargs): | |
| Helper that wraps yaml.dump and ensures that we encode unicode strings | ||
| unless explicitly told not to. | ||
| """ | ||
| if "allow_unicode" not in kwargs: | ||
|
Ch3LL marked this conversation as resolved.
|
||
| kwargs["allow_unicode"] = True | ||
| kwargs.setdefault("allow_unicode", True) | ||
| kwargs.setdefault("default_flow_style", None) | ||
| return yaml.dump(data, stream, **kwargs) | ||
|
|
||
|
|
@@ -190,7 +160,4 @@ def safe_dump(data, stream=None, **kwargs): | |
| represented properly. Ensure that unicode strings are encoded unless | ||
| explicitly told not to. | ||
| """ | ||
| if "allow_unicode" not in kwargs: | ||
|
Ch3LL marked this conversation as resolved.
|
||
| kwargs["allow_unicode"] = True | ||
| kwargs.setdefault("default_flow_style", None) | ||
|
Ch3LL marked this conversation as resolved.
|
||
| return yaml.dump(data, stream, Dumper=SafeOrderedDumper, **kwargs) | ||
| return dump(data, stream, Dumper=SafeOrderedDumper, **kwargs) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| import random | ||
| import textwrap | ||
|
|
||
| import pytest | ||
|
|
||
| pytestmark = [ | ||
| pytest.mark.slow_test, | ||
| ] | ||
|
|
||
|
|
||
| @pytest.fixture(scope="module") | ||
| def minion_run(salt_minion, salt_cli): | ||
| """Convenience fixture that runs the ``salt`` CLI targeting the minion.""" | ||
|
|
||
| def _run(*args, minion_tgt=salt_minion.id, **kwargs): | ||
| ret = salt_cli.run(*args, minion_tgt=minion_tgt, **kwargs) | ||
| assert ret.returncode == 0 | ||
| return ret.data | ||
|
|
||
| yield _run | ||
|
|
||
|
|
||
| def test_pillar_map_order(salt_master, minion_run): | ||
| """Test iteration order of YAML map entries in a Pillar ``.sls`` file. | ||
|
|
||
| This test generates a Pillar ``.sls`` file containing an ordinary YAML map | ||
| and tests whether the resulting Python object preserves iteration order. | ||
| Random keys are used to ensure that iteration order does not coincidentally | ||
| match. The generated Pillar YAML file looks like this: | ||
|
|
||
| .. code-block:: yaml | ||
|
|
||
| data: | ||
| k3334244338: 0 | ||
| k3444116829: 1 | ||
| k2072366017: 2 | ||
| # ... omitted for brevity ... | ||
| k1638299831: 19 | ||
|
|
||
| A jinja template iterates over the entries in the resulting object to ensure | ||
| that iteration order is preserved. The expected output looks like: | ||
|
|
||
| .. code-block:: text | ||
|
|
||
| k3334244338 0 | ||
| k3444116829 1 | ||
| k2072366017 2 | ||
| ... omitted for brevity ... | ||
| k1638299831 19 | ||
|
|
||
| Note: Python 3.6 switched to a new ``dict`` implementation that iterates in | ||
| insertion order. This behavior was made an official part of the ``dict`` | ||
| API in Python 3.7: | ||
|
|
||
| * https://docs.python.org/3.6/whatsnew/3.6.html#new-dict-implementation | ||
| * https://mail.python.org/pipermail/python-dev/2017-December/151283.html | ||
| * https://docs.python.org/3.7/whatsnew/3.7.html | ||
|
|
||
| Thus, this test may fail on Python 3.5 and older. However, Salt currently | ||
| requires a newer version of Python, so this should not be a problem. | ||
|
|
||
| This is a regression test for: | ||
| https://github.com/saltstack/salt/issues/12161 | ||
| """ | ||
| # Filter the random keys through a set to avoid duplicates. | ||
| keys = list({f"k{random.getrandbits(32)}" for _ in range(20)}) | ||
| # Avoid unintended correlation with set()'s iteration order. | ||
| random.shuffle(keys) | ||
| items = [(k, i) for i, k in enumerate(keys)] | ||
| top_yaml = "base: {'*': [data]}\n" | ||
| top_sls = salt_master.pillar_tree.base.temp_file("top.sls", top_yaml) | ||
| data_yaml = "data:\n" + "".join(f" {k}: {v}\n" for k, v in items) | ||
| data_sls = salt_master.pillar_tree.base.temp_file("data.sls", data_yaml) | ||
| tmpl_jinja = textwrap.dedent( | ||
| """\ | ||
| {%- for k, v in pillar['data'].items() %} | ||
| {{ k }} {{ v }} | ||
| {%- endfor %} | ||
| """ | ||
| ) | ||
| want = "\n" + "".join(f"{k} {v}\n" for k, v in items) | ||
| try: | ||
| with top_sls, data_sls: | ||
| assert minion_run("saltutil.refresh_pillar", wait=True) is True | ||
| got = minion_run( | ||
| "file.apply_template_on_contents", | ||
| tmpl_jinja, | ||
| template="jinja", | ||
| context={}, | ||
| defaults={}, | ||
| saltenv="base", | ||
| ) | ||
| assert got == want | ||
| finally: | ||
| assert minion_run("saltutil.refresh_pillar", wait=True) is True |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We cannot remove a class without properly deprecating it.