Skip to content

Commit

Permalink
Added support for appending items to lists
Browse files Browse the repository at this point in the history
Also supports incrementing integers and appending to strings
  • Loading branch information
campos-ddc committed Oct 10, 2022
1 parent 6e19c87 commit 0ae83b8
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 75 deletions.
36 changes: 27 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,56 @@ pip install yaml-patch

You can pass any number of patches to be applied, they use the following syntax options:

### Patch a single value:
### Override a single value:
`<field>.<subfield>=<value>`

Example:
```bash
yaml-patch -f test.yml "spec.replicas=2"
yaml-patch -f test.yml "spec.replicas=2"
```

### Patch a value inside a single list item:
### Override a value inside a single list item:
`<field>.[<position]>.<subfield>=<value>`

Example:
```bash
yaml-patch -f test.yml "spec.template.containers.[0].image='mycontainer:latest'"
yaml-patch -f test.yml "spec.template.containers.[0].image='mycontainer:latest'"
```

### Patch a value inside all list items:
### Override a value inside all list items:
`<field>.[].<subfield>=<value>`

Example:
```bash
yaml-patch -f test.yml "spec.template.containers.[].image='mycontainer:latest'"
```

### Append a single value:
`<field>.<subfield>+=<value>`

Example (increment int):
```bash
yaml-patch -f test.yml "spec.replicas+=2"
```

Example (append string):
```bash
yaml-patch -f test.yml "spec.template.containers.[0].image+=':latest'"
```

Example (append item to list):
```bash
yaml-patch -f test.yml "spec.template.containers.[0].args+=['--verbose']"
```

## As a Python library

To use `yaml-patch` as a library just import the function and pass patches as dictionary entries.
To use `yaml-patch` as a library just import the function and pass patches as you would in the CLI examples above.

Example:

```python
from yaml_patch import patch
from yaml_patch import patch_yaml
from textwrap import dedent

def override_list_all_values():
Expand All @@ -59,13 +77,13 @@ def override_list_all_values():
- bob
"""
)
patches = {"some_list.[]": "charlie"}
patches = ["some_list.[]='charlie'"]
expected_yaml = dedent(
"""\
some_list:
- charlie
- charlie
"""
)
assert patch(source_yaml, patches) == expected_yaml
assert patch_yaml(source_yaml, patches) == expected_yaml
```
44 changes: 18 additions & 26 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,46 +13,38 @@ def test_no_patches():

runner = CliRunner()

with mock.patch("yaml_patch.patch", return_value="mock_output") as mock_patch:
with mock.patch("yaml_patch.patch_yaml", return_value="mock_output") as mock_patch:
from yaml_patch.cli import cli

result = runner.invoke(cli, input="key: value")

__assert_result_success(result)
assert result.stdout == "mock_output"
mock_patch.assert_called_once_with(yaml_contents="key: value", patches=dict())
mock_patch.assert_called_once_with(yaml_contents="key: value", patches=tuple())


def test_patches():
"""
Test that patch arguments from CLI are parsed correctly
Test that patch arguments from CLI are forwarded correctly
"""
runner = CliRunner()

cli_patches = [
cli_patches = (
"key='new value'",
"key.subkey='new subvalue'",
"key.list.[0]='value in list'",
"key.subint=5",
"key.subbool=true",
]

expected_patches = {
"key": "new value",
"key.subkey": "new subvalue",
"key.list.[0]": "value in list",
"key.subint": 5,
"key.subbool": True,
}
)

with mock.patch("yaml_patch.patch", return_value="mock_output") as mock_patch:
with mock.patch("yaml_patch.patch_yaml", return_value="mock_output") as mock_patch:
from yaml_patch.cli import cli

result = runner.invoke(cli, cli_patches, input="key: value")

__assert_result_success(result)
assert result.stdout == "mock_output"
mock_patch.assert_called_once_with(yaml_contents="key: value", patches=expected_patches)
mock_patch.assert_called_once_with(yaml_contents="key: value", patches=cli_patches)


def test_patch_file_input(tmp_path):
Expand All @@ -64,14 +56,14 @@ def test_patch_file_input(tmp_path):

runner = CliRunner()

with mock.patch("yaml_patch.patch", return_value="mock_output") as mock_patch:
with mock.patch("yaml_patch.patch_yaml", return_value="mock_output") as mock_patch:
from yaml_patch.cli import cli

result = runner.invoke(cli, [f"--file={tmp_yaml}"])

__assert_result_success(result)
assert result.stdout == "mock_output"
mock_patch.assert_called_once_with(yaml_contents="key: 'value in file'", patches=dict())
mock_patch.assert_called_once_with(yaml_contents="key: 'value in file'", patches=tuple())


def test_patch_file_output(tmp_path):
Expand All @@ -83,15 +75,15 @@ def test_patch_file_output(tmp_path):

runner = CliRunner()

with mock.patch("yaml_patch.patch", return_value="mock_output") as mock_patch:
with mock.patch("yaml_patch.patch_yaml", return_value="mock_output") as mock_patch:
from yaml_patch.cli import cli

result = runner.invoke(cli, [f"--output={tmp_yaml}"], input="key: value")

__assert_result_success(result)
assert result.stdout == "" # stdout is empty because we output to a file
assert tmp_yaml.read_text() == "mock_output"
mock_patch.assert_called_once_with(yaml_contents="key: value", patches=dict())
mock_patch.assert_called_once_with(yaml_contents="key: value", patches=tuple())


def test_patch_file_input_and_output(tmp_path):
Expand All @@ -104,15 +96,15 @@ def test_patch_file_input_and_output(tmp_path):

runner = CliRunner()

with mock.patch("yaml_patch.patch", return_value="mock_output") as mock_patch:
with mock.patch("yaml_patch.patch_yaml", return_value="mock_output") as mock_patch:
from yaml_patch.cli import cli

result = runner.invoke(cli, [f"--file={input_yaml}", f"--output={output_yaml}"], input="key: value")

__assert_result_success(result)
assert result.stdout == "" # stdout is empty because we output to a file
assert output_yaml.read_text() == "mock_output"
mock_patch.assert_called_once_with(yaml_contents="key: 'value in file'", patches=dict())
mock_patch.assert_called_once_with(yaml_contents="key: 'value in file'", patches=tuple())


def test_patch_file_same_input_and_output(tmp_path):
Expand All @@ -124,15 +116,15 @@ def test_patch_file_same_input_and_output(tmp_path):

runner = CliRunner()

with mock.patch("yaml_patch.patch", return_value="mock_output") as mock_patch:
with mock.patch("yaml_patch.patch_yaml", return_value="mock_output") as mock_patch:
from yaml_patch.cli import cli

result = runner.invoke(cli, [f"--file={tmp_yaml}", f"--output={tmp_yaml}"], input="key: value")

__assert_result_success(result)
assert result.stdout == "" # stdout is empty because we output to a file
assert tmp_yaml.read_text() == "mock_output"
mock_patch.assert_called_once_with(yaml_contents="key: 'value in file'", patches=dict())
mock_patch.assert_called_once_with(yaml_contents="key: 'value in file'", patches=tuple())


def test_inplace(tmp_path):
Expand All @@ -144,15 +136,15 @@ def test_inplace(tmp_path):

runner = CliRunner()

with mock.patch("yaml_patch.patch", return_value="mock_output") as mock_patch:
with mock.patch("yaml_patch.patch_yaml", return_value="mock_output") as mock_patch:
from yaml_patch.cli import cli

result = runner.invoke(cli, [f"--file={tmp_yaml}", "--in-place"], input="key: value")

__assert_result_success(result)
assert result.stdout == "" # stdout is empty because we output to a file
assert tmp_yaml.read_text() == "mock_output"
mock_patch.assert_called_once_with(yaml_contents="key: 'value in file'", patches=dict())
mock_patch.assert_called_once_with(yaml_contents="key: 'value in file'", patches=tuple())


def test_cant_use_inplace_with_stdin(tmp_path):
Expand All @@ -164,7 +156,7 @@ def test_cant_use_inplace_with_stdin(tmp_path):

runner = CliRunner()

with mock.patch("yaml_patch.patch", return_value="mock_output") as mock_patch:
with mock.patch("yaml_patch.patch_yaml", return_value="mock_output") as mock_patch:
from yaml_patch.cli import cli

result = runner.invoke(cli, ["--in-place"], input="key: value")
Expand Down
53 changes: 52 additions & 1 deletion tests/test_patch.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from textwrap import dedent

from yaml_patch import patch
import pytest

from yaml_patch import patch_yaml, patch


def check(obtained, expected):
Expand Down Expand Up @@ -132,3 +134,52 @@ def test_override_object_within_a_list():
"""
)
check(patch(source_yaml, patches), expected_yaml)


def test_append_to_int():
source_yaml = dedent(
"""\
some_key: 1
"""
)
patches = ["some_key+=2"]
expected_yaml = dedent(
"""\
some_key: 3
"""
)
check(patch_yaml(source_yaml, patches), expected_yaml)


def test_append_to_list():
source_yaml = dedent(
"""\
some_list:
- alice
- bob
"""
)
patches = ["some_list+=['charlie']"]
expected_yaml = dedent(
"""\
some_list:
- alice
- bob
- charlie
"""
)
check(patch_yaml(source_yaml, patches), expected_yaml)


def test_append_str_to_list():
source_yaml = dedent(
"""\
some_list:
- alice
- bob
"""
)
patches = ["some_list+='charlie'"]
with pytest.raises(TypeError) as e:
patch_yaml(source_yaml, patches)
assert str(e.value) == 'can only concatenate list (not "str") to list'
4 changes: 2 additions & 2 deletions yaml_patch/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .patch import patch
from .patch import patch_yaml, patch

__all__ = ["patch"]
__all__ = ["patch_yaml", "patch"]
32 changes: 18 additions & 14 deletions yaml_patch/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,24 @@ def cli(file, output, in_place, patches):
if in_place and file == sys.stdin:
raise ValueError("Cannot use --in-place with stdin as the source")

# Split each patch into key+value separated by `=`. Use YAML to load the values coming from command line to ensure
# they are parsed into yaml syntax equivalents (automatically detect strings, ints, bools, etc).
from ruamel.yaml import YAML

yaml = YAML()
dict_patches = dict()
for p in patches:
k, v = p.split("=")
dict_patches[k] = yaml.load(v)

# Apply patches
from yaml_patch import patch

patched = patch(yaml_contents=file.read(), patches=dict_patches)
from yaml_patch import patch_yaml

patched = patch_yaml(yaml_contents=file.read(), patches=patches)

# # Split each patch into key+value separated by `=`. Use YAML to load the values coming from command line to ensure
# # they are parsed into yaml syntax equivalents (automatically detect strings, ints, bools, etc).
# from ruamel.yaml import YAML
#
# yaml = YAML()
# dict_patches = dict()
# for p in patches:
# k, v = p.split("=")
# dict_patches[k] = yaml.load(v)
#
# # Apply patches
# from yaml_patch import patch
#
# patched = patch(yaml_contents=file.read(), patches=dict_patches)

# Output results
if in_place:
Expand Down
Loading

0 comments on commit 0ae83b8

Please sign in to comment.