Skip to content

Commit

Permalink
Merge pull request #119 from wwkimball/feature/more-set-caps-to-lib
Browse files Browse the repository at this point in the history
Move more capabilities from yaml-set to the library
  • Loading branch information
wwkimball authored Apr 12, 2021
2 parents 6ec2b11 + 919c001 commit 97f63a8
Show file tree
Hide file tree
Showing 5 changed files with 362 additions and 78 deletions.
6 changes: 6 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ Enhancements:
* The node deletion capability of the yaml-set command is now part of the
library. See Processor::delete_nodes(...) and
Processor::delete_gathered_nodes(...) for details.
* The node aliasing capability of the yaml-set command is now part of the
library. See Processor::alias_nodes(...) and
Processor::alias_gathered_nodes(...) for details.
* The node tagging capability of the yaml-set command is now part of the
library. See Processor::tag_nodes(...) and
Processor::tag_gathered_nodes(...) for details.
* The library now supports loading YAML from String rather than only from file.
Simply pass a new `literal=True` keyword parameter to
Parsers::get_yaml_data(...) or Parsers::get_yaml_multidoc_data(...) to
Expand Down
49 changes: 31 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -690,18 +690,19 @@ https://github.com/wwkimball/yamlpath.

```text
usage: yaml-set [-h] [-V] -g YAML_PATH
[-a VALUE | -f FILE | -i | -R LENGTH | -D]
[-a VALUE | -A ANCHOR | -f FILE | -i | -R LENGTH | -N | -D]
[-F {bare,boolean,default,dquote,float,folded,int,literal,squote}]
[-c CHECK] [-s YAML_PATH] [-m] [-b]
[-t ['.', '/', 'auto', 'dot', 'fslash']] [-M CHARS] [-e]
[-x EYAML] [-r PRIVATEKEY] [-u PUBLICKEY] [-S] [-d | -v | -q]
[-t ['.', '/', 'auto', 'dot', 'fslash']] [-M CHARS] [-H ANCHOR]
[-T TAG] [-e] [-x EYAML] [-r PRIVATEKEY] [-u PUBLICKEY] [-S]
[-d | -v | -q]
[YAML_FILE]
Changes one or more Scalar values in a YAML/JSON/Compatible document at a
specified YAML Path. Matched values can be checked before they are replaced to
mitigate accidental change. When matching singular results, the value can be
archived to another key before it is replaced. Further, EYAML can be employed
to encrypt the new values and/or decrypt an old value before checking it.
archived to another key before it is replaced. Further, EYAML can be employed to
encrypt the new values and/or decrypt an old value before checking it.
positional arguments:
YAML_FILE the YAML file to update; omit or use - to read from
Expand All @@ -722,14 +723,20 @@ optional arguments:
-b, --backup save a backup YAML_FILE with an extra .bak file-
extension
-t ['.', '/', 'auto', 'dot', 'fslash'], --pathsep ['.', '/', 'auto', 'dot', 'fslash']
indicate which YAML Path seperator to use when
rendering results; default=dot
indicate which YAML Path seperator to use when rendering
results; default=dot
-M CHARS, --random-from CHARS
characters from which to build a value for --random;
default=all upper- and lower-case letters and all
digits
-S, --nostdin Do not implicitly read from STDIN, even when there is
no YAML_FILE with a non-TTY session
default=all upper- and lower-case letters and all digits
-H ANCHOR, --anchor ANCHOR
when --aliasof|-A points to a value which is not already
Anchored, a new Anchor with this name is created;
renames an existing Anchor if already set
-T TAG, --tag TAG assign a custom YAML (data-type) tag to the changed
nodes; can be used without other input options to assign
or change a tag
-S, --nostdin Do not implicitly read from STDIN, even when there is no
YAML_FILE with a non-TTY session
-d, --debug output debugging details
-v, --verbose increase output verbosity
-q, --quiet suppress all output except errors
Expand All @@ -740,20 +747,24 @@ required settings:
input options:
-a VALUE, --value VALUE
set the new value from the command-line instead of
STDIN
set the new value from the command-line instead of STDIN
-A ANCHOR, --aliasof ANCHOR
set the value as a YAML Alias of an existing Anchor, by
name (merely copies the target value for non-YAML files)
-f FILE, --file FILE read the new value from file (discarding any trailing
new-lines)
-i, --stdin accept the new value from STDIN (best for sensitive
data)
-R LENGTH, --random LENGTH
randomly generate a replacement value of a set length
-D, --delete delete rather than change target node(s)
-N, --null sets the value to null
-D, --delete delete rather than change target node(s); implies
--mustexist|-m
EYAML options:
Left unset, the EYAML keys will default to your system or user defaults.
You do not need to supply a private key unless you enable --check and the
old value is encrypted.
Left unset, the EYAML keys will default to your system or user defaults. You
do not need to supply a private key unless you enable --check and the old
value is encrypted.
-e, --eyamlcrypt encrypt the new value using EYAML
-x EYAML, --eyaml EYAML
Expand All @@ -765,7 +776,9 @@ EYAML options:
When no changes are made, no backup is created, even when -b/--backup is
specified. For more information about YAML Paths, please visit
https://github.com/wwkimball/yamlpath.
https://github.com/wwkimball/yamlpath/wiki. To report issues with this tool or
to request enhancements, please visit
https://github.com/wwkimball/yamlpath/issues.
```

* [yaml-validate](yamlpath/commands/yaml_validate.py)
Expand Down
80 changes: 80 additions & 0 deletions tests/test_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from types import SimpleNamespace

from ruamel.yaml import YAML
from ruamel.yaml.comments import TaggedScalar

from yamlpath.func import unwrap_node_coords
from yamlpath.exceptions import YAMLPathException
Expand Down Expand Up @@ -830,3 +831,82 @@ def test_null_docs_have_nothing_to_delete(self, capsys):

console = capsys.readouterr()
assert "Refusing to delete nodes from a null document" in console.out

def test_null_docs_have_nothing_to_gather_and_alias(self, capsys):
args = SimpleNamespace(verbose=False, quiet=False, debug=True)
logger = ConsolePrinter(args)
processor = Processor(logger, None)

processor.alias_nodes("/alias*", "/anchor")

console = capsys.readouterr()
assert "Refusing to alias nodes in a null document" in console.out

def test_null_docs_have_nothing_to_alias(self, capsys):
args = SimpleNamespace(verbose=False, quiet=False, debug=True)
logger = ConsolePrinter(args)
processor = Processor(logger, None)

processor.alias_gathered_nodes([], "/anchor")

console = capsys.readouterr()
assert "Refusing to alias nodes in a null document" in console.out

def test_null_docs_have_nothing_to_tag(self, capsys):
args = SimpleNamespace(verbose=False, quiet=False, debug=True)
logger = ConsolePrinter(args)
processor = Processor(logger, None)

processor.tag_nodes("/tag_nothing", "tag_this")

console = capsys.readouterr()
assert "Refusing to tag nodes from a null document" in console.out

@pytest.mark.parametrize("alias_path,anchor_path,anchor_name,pathseperator", [
(YAMLPath("/a_hash/a_key"), YAMLPath("/some_key"), "", PathSeperators.FSLASH),
("a_hash.a_key", "some_key", "", PathSeperators.AUTO),
])
def test_anchor_nodes(self, quiet_logger, alias_path, anchor_path, anchor_name, pathseperator):
anchor_value = "This is the Anchored value!"
yamlin = """---
some_key: {}
a_hash:
a_key: A value
""".format(anchor_value)

yaml = YAML()
data = yaml.load(yamlin)
processor = Processor(quiet_logger, data)

processor.alias_nodes(
alias_path, anchor_path,
pathsep=pathseperator, anchor_name=anchor_name)

match_count = 0
for node in processor.get_nodes(
alias_path, mustexist=True
):
match_count += 1
assert unwrap_node_coords(node) == anchor_value
assert match_count == 1

@pytest.mark.parametrize("yaml_path,tag,pathseperator", [
(YAMLPath("/key"), "!taggidy", PathSeperators.FSLASH),
("key", "taggidy", PathSeperators.AUTO),
])
def test_tag_nodes(self, quiet_logger, yaml_path, tag, pathseperator):
yamlin = """---
key: value
"""

yaml = YAML()
data = yaml.load(yamlin)
processor = Processor(quiet_logger, data)

processor.tag_nodes(yaml_path, tag, pathsep=pathseperator)

if tag and not tag[0] == "!":
tag = "!{}".format(tag)

assert isinstance(data['key'], TaggedScalar)
assert data['key'].tag.value == tag
65 changes: 7 additions & 58 deletions yamlpath/commands/yaml_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@
from os.path import isfile, exists
from shutil import copy2, copyfileobj
from pathlib import Path
from typing import Any, Dict

from yamlpath import __version__ as YAMLPATH_VERSION
from yamlpath.common import Anchors, Nodes, Parsers
from yamlpath.common import Nodes, Parsers
from yamlpath import YAMLPath
from yamlpath.exceptions import YAMLPathException
from yamlpath.enums import YAMLValueFormats, PathSeperators
Expand Down Expand Up @@ -415,61 +414,11 @@ def _alias_nodes(
log, processor, assign_to_nodes, anchor_path, anchor_name
):
"""Assign YAML Aliases to the target nodes."""
anchor_node_coordinates = _get_nodes(
log, processor, anchor_path, must_exist=True)
if len(anchor_node_coordinates) > 1:
log.critical(
"It is impossible to Alias more than one Anchor at a time from {}!"
.format(anchor_path), 1)

anchor_coord = anchor_node_coordinates[0]
anchor_node = anchor_coord.node
if not hasattr(anchor_node, "anchor"):
anchor_coord.parent[anchor_coord.parentref] = Nodes.wrap_type(
anchor_node)
anchor_node = anchor_coord.parent[anchor_coord.parentref]

known_anchors: Dict[str, Any] = {}
Anchors.scan_for_anchors(processor.data, known_anchors)

if anchor_name:
# Rename any pre-existing anchor or set an original anchor name; the
# assigned name must be unique!
if anchor_name in known_anchors:
log.critical(
"Anchor names must be unique within YAML documents. Anchor"
" name, {}, is already used.".format(anchor_name))
anchor_node.yaml_set_anchor(anchor_name, always_dump=True)
elif anchor_node.anchor.value:
# The source node already has an anchor name
anchor_name = anchor_node.anchor.value
else:
# An orignial, unique-to-the-document anchor name must be generated
new_anchor = Anchors.generate_unique_anchor_name(
processor.data, anchor_coord, known_anchors)
anchor_node.yaml_set_anchor(new_anchor, always_dump=True)

for node_coord in assign_to_nodes:
log.debug(
"Attempting to set the anchor name for node to {}:"
.format(anchor_name),
data=node_coord,
prefix="yaml_set::_alias_nodes: ")
node_coord.parent[node_coord.parentref] = anchor_node

def _tag_nodes(document, tag, nodes):
"""Assign a data-type tag to a set of nodes."""
for node_coord in nodes:
old_node = node_coord.node
if node_coord.parent is None:
node_coord.node.yaml_set_tag(tag)
else:
node_coord.parent[node_coord.parentref] = Nodes.apply_yaml_tag(
node_coord.node, tag)
if Anchors.get_node_anchor(old_node) is not None:
Anchors.replace_anchor(
document, old_node,
node_coord.parent[node_coord.parentref])
try:
processor.alias_gathered_nodes(
assign_to_nodes, anchor_path, anchor_name=anchor_name)
except YAMLPathException as ex:
log.critical(ex, 1)

# pylint: disable=locally-disabled,too-many-locals,too-many-branches,too-many-statements
def main():
Expand Down Expand Up @@ -642,7 +591,7 @@ def main():
except YAMLPathException as ex:
log.critical(ex, 1)
elif args.tag:
_tag_nodes(processor.data, args.tag, change_node_coordinates)
processor.tag_gathered_nodes(change_node_coordinates, args.tag)

# Write out the result
write_output_document(args, log, yaml, yaml_data)
Expand Down
Loading

0 comments on commit 97f63a8

Please sign in to comment.