Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/pythonapp.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
---
name: Python application

on:
Expand Down
4 changes: 4 additions & 0 deletions docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ See :doc:`data-process` for detail.

Do not process variable substitutions.

.. option:: --no-remove-root-underscore

Turn off removal of root level sub-object with an underscore key.

.. option:: --schema-prefix=PREFIX

Prefix for relative path schemas. See also :envvar:`YP_SCHEMA_PREFIX`.
Expand Down
7 changes: 0 additions & 7 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
# import sys
# sys.path.insert(0, os.path.abspath('.'))

import sphinx_rtd_theme

import yamlprocessor


Expand Down Expand Up @@ -64,11 +62,6 @@
#
html_theme = 'sphinx_rtd_theme'

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]

intersphinx_mapping = {
'python': ('http://python.readthedocs.org/en/latest/', None),
}
16 changes: 16 additions & 0 deletions docs/data-process.rst
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,22 @@ you can do:
:py:class:`yamlprocessor.dataprocess.DataProcessor` instance to ``False``.


Turn Off Removal of Root Level Sub-object with Underscore Key
-------------------------------------------------------------

By default, if the document is an object at root and if the root object
contains a value with an underscore ``_`` key, the application will remove the
value with the underscore key before dumping the result. To turn off this
behaviour, you can do:

- On the command line, use the
:option:`--no-remove-root-underscore <yp-data --no-remove-root-underscore>`
option.
- In Python, set the :py:attr:`.is_remove_root_underscore` attribute of the
relevant :py:class:`yamlprocessor.dataprocess.DataProcessor` instance to
``True``.


Validation with JSON Schema
---------------------------

Expand Down
2 changes: 1 addition & 1 deletion environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ channels:
- conda-forge
- defaults
dependencies:
- python>=3.7
- python>=3.9
- doc8
- flake8
- jmespath
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description = "Process values in YAML files"

readme = "README.md"

requires-python = ">=3.11"
requires-python = ">=3.9"

license = {file = "LICENSE"}

Expand All @@ -22,6 +22,8 @@ classifiers = [
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
Expand Down
19 changes: 18 additions & 1 deletion src/yamlprocessor/dataprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,10 @@ class DataProcessor:
.. py:attribute:: .is_process_variable
:type: bool

Turn on/off variable substitution.
.. py:attribute:: .is_remove_root_underscore
:type: bool

Turn on/off removal of root level sub-object with an underscore key.

.. py:attribute:: .include_dict
:type: dict
Expand Down Expand Up @@ -305,6 +308,7 @@ class DataProcessor:
def __init__(self):
self.is_process_include = True
self.is_process_variable = True
self.is_remove_root_underscore = True
self.include_paths = list(
item
for item in os.getenv('YP_INCLUDE_PATH', '').split(os.pathsep)
Expand Down Expand Up @@ -447,16 +451,21 @@ def process_data(
if isinstance(p_item, dict) or isinstance(p_item, list):
stack.append(
[p_item, parent_filenames_x, variable_map_x])
# Finally dump data
if out_filename == '-':
out_file = sys.stdout
else:
out_file = open(out_filename, 'w')
yaml = YAML(typ='safe', pure=True)
yaml.default_flow_style = False
yaml.sort_base_mapping_type_on_output = False
yaml.representer.ignore_aliases = lambda data: True
yaml.representer.add_representer(
datetime,
get_represent_datetime(self.time_formats['']))
if self.is_remove_root_underscore:
with suppress(KeyError, TypeError):
del root['_']
yaml.dump(root, out_file)
self.validate_data(root, out_filename, schema_location)

Expand Down Expand Up @@ -882,6 +891,12 @@ def main(argv=None):
action='store_false',
default=True,
help='Do not process variable substitutions')
parser.add_argument(
'--no-remove-root-underscore',
dest='is_remove_root_underscore',
action='store_false',
default=True,
help='Do not remove root level sub-object with an underscore key')
parser.add_argument(
'--quiet', '-q',
dest='is_quiet_mode',
Expand Down Expand Up @@ -930,6 +945,8 @@ def main(argv=None):

# Set up processor
processor = DataProcessor()
# Remove root level sub-object with underscore key option
processor.is_remove_root_underscore = args.is_remove_root_underscore
# Include options
processor.is_process_include = args.is_process_include
for item in args.include_paths:
Expand Down
64 changes: 48 additions & 16 deletions src/yamlprocessor/tests/test_dataprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ def test_main_0(tmp_path, yaml):
with infilename.open('w') as infile:
yaml.dump(data, infile)
outfilename = tmp_path / 'b.yaml'
main([str(infilename), str(outfilename)])
main([str(infilename), '-o', str(outfilename)])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a change in behaviour or are you just being explicit?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is introduced in a previous change and is nicer CLI usage (as multiple arguments can now be used to concat files). So being explicit here.

assert yaml.load(outfilename.open()) == data


Expand All @@ -220,7 +220,7 @@ def test_main_1(capsys, tmp_path, yaml):
with (infilename_1).open('w') as infile_1:
yaml.dump(1, infile_1)
outfilename = tmp_path / 'b.yaml'
main([str(infilename), str(outfilename)])
main([str(infilename), '-o', str(outfilename)])
assert yaml.load(outfilename.open()) == data
captured = capsys.readouterr()
assert f'[INFO] < {infilename}' in captured.err.splitlines()
Expand All @@ -241,7 +241,7 @@ def test_main_3(tmp_path, yaml):
with (tmp_path / '3x.yaml').open('w') as infile_3x:
yaml.dump([3.1, 3.14], infile_3x)
outfilename = tmp_path / 'b.yaml'
main([str(infilename), str(outfilename)])
main([str(infilename), '-o', str(outfilename)])
assert yaml.load(outfilename.open()) == data


Expand All @@ -252,7 +252,7 @@ def test_main_4(tmp_path, yaml):
with infilename.open('w') as infile:
yaml.dump(data, infile)
outfilename = tmp_path / 'b.yaml'
main(['--no-process-include', str(infilename), str(outfilename)])
main(['--no-process-include', str(infilename), '-o', str(outfilename)])
assert yaml.load(outfilename.open()) == data


Expand All @@ -269,6 +269,7 @@ def test_main_5(tmp_path, yaml):
'--define=PERSON=Jo',
'--unbound-placeholder=unknown',
str(infilename),
'-o',
str(outfilename),
])
assert yaml.load(outfilename.open()) == ['Hello Jo', 'Hello unknown']
Expand All @@ -278,6 +279,7 @@ def test_main_5(tmp_path, yaml):
'--define=PERSON=Jo',
'--unbound-placeholder=' + DataProcessor.UNBOUND_ORIGINAL,
str(infilename),
'-o',
str(outfilename),
])
assert yaml.load(outfilename.open()) == ['Hello Jo', 'Hello ${ALIEN}']
Expand All @@ -290,7 +292,7 @@ def test_main_6(tmp_path, yaml):
with infilename.open('w') as infile:
yaml.dump(data, infile)
outfilename = tmp_path / 'b.yaml'
main(['--no-process-variable', str(infilename), str(outfilename)])
main(['--no-process-variable', str(infilename), '-o', str(outfilename)])
assert yaml.load(outfilename.open()) == data


Expand All @@ -313,7 +315,7 @@ def test_main_7(capsys, tmp_path, yaml):
with (include_3x).open('w') as infile_3x:
yaml.dump([3.1, 3.14], infile_3x)
outfilename = tmp_path / 'b.yaml'
main(['-I', str(include_d), str(infilename), str(outfilename)])
main(['-I', str(include_d), str(infilename), '-o', str(outfilename)])
assert yaml.load(outfilename.open()) == data
captured = capsys.readouterr()
assert f'[INFO] YP_INCLUDE_PATH={include_d}' in captured.err.splitlines()
Expand All @@ -330,7 +332,7 @@ def test_main_8(tmp_path, yaml):
with infilename.open('w') as infile:
infile.write(incontent)
outfilename = tmp_path / 'b.yaml'
main([str(infilename), str(outfilename)])
main([str(infilename), '-o', str(outfilename)])
with outfilename.open() as outfile:
outcontent = outfile.read()
assert incontent == outcontent
Expand All @@ -357,7 +359,7 @@ def test_main_9(tmp_path, yaml):
with (tmp_path / '1.yaml').open('w') as infile_1:
yaml.dump(data_1, infile_1)
outfilename = tmp_path / 'b.yaml'
main([str(infilename), str(outfilename)])
main([str(infilename), '-o', str(outfilename)])
assert yaml.load(outfilename.open()) == data


Expand All @@ -372,7 +374,7 @@ def test_main_10(tmp_path, yaml):
with infilename.open('w') as infile:
yaml.dump(data, infile)
outfilename = tmp_path / 'b.yaml'
main([str(infilename), str(outfilename)])
main([str(infilename), '-o', str(outfilename)])
assert yaml.load(outfilename.open()) == {
'you-time': '2030-04-05T06:07:08Z',
'me-time': '2030-04-05T06:07:08+09:00',
Expand Down Expand Up @@ -413,6 +415,7 @@ def test_main_11(tmp_path, yaml):
'--define=NAME=earth',
'--define=PEOPLE=human',
str(infilename),
'-o',
str(outfilename)])
assert yaml.load(outfilename.open()) == {
'hello': [
Expand Down Expand Up @@ -449,7 +452,7 @@ def test_main_12(tmp_path, yaml):
with include_infilename.open('w') as infile:
yaml.dump(more_data_2, infile)
outfilename = tmp_path / 'b.yaml'
main([str(infilename), str(outfilename)])
main([str(infilename), '-o', str(outfilename)])
assert yaml.load(outfilename.open()) == [
{'name': 'cat', 'speak': ['meow', 'miaow']},
{'name': 'dog', 'speak': ['woof', 'bark']},
Expand Down Expand Up @@ -478,7 +481,7 @@ def test_main_12_empty_list_1(tmp_path, yaml):
with include_infilename.open('w') as infile:
yaml.dump(void_data, infile)
outfilename = tmp_path / 'b.yaml'
main(['--define=MATTER=stuff', str(infilename), str(outfilename)])
main(['--define=MATTER=stuff', str(infilename), '-o', str(outfilename)])
assert yaml.load(outfilename.open()) == [
{'name': 'stuff'},
{'name': 'stuff'},
Expand All @@ -504,7 +507,7 @@ def test_main_12_empty_list_2(tmp_path, yaml):
with infilename.open('w') as infile:
yaml.dump(data, infile)
outfilename = tmp_path / 'b.yaml'
main(['--define=MATTER=stuff', str(infilename), str(outfilename)])
main(['--define=MATTER=stuff', str(infilename), '-o', str(outfilename)])
assert yaml.load(outfilename.open()) == [
{'name': 'stuff'},
{'name': 'stuff'},
Expand All @@ -529,7 +532,7 @@ def test_main_12_empty_dict_1(tmp_path, yaml):
with include_infilename.open('w') as infile:
yaml.dump(void_data, infile)
outfilename = tmp_path / 'b.yaml'
main(['--define=MATTER=stuff', str(infilename), str(outfilename)])
main(['--define=MATTER=stuff', str(infilename), '-o', str(outfilename)])
assert yaml.load(outfilename.open()) == {
'name1': 'stuff',
'name2': 'stuff',
Expand All @@ -555,7 +558,7 @@ def test_main_12_empty_dict_2(tmp_path, yaml):
with infilename.open('w') as infile:
yaml.dump(data, infile)
outfilename = tmp_path / 'b.yaml'
main(['--define=MATTER=stuff', str(infilename), str(outfilename)])
main(['--define=MATTER=stuff', str(infilename), '-o', str(outfilename)])
assert yaml.load(outfilename.open()) == {
'name1': 'stuff',
'name2': 'stuff',
Expand Down Expand Up @@ -586,6 +589,7 @@ def test_main_13(tmp_path, yaml):
outfilename = tmp_path / 'b.yaml'
main([
str(infilename),
'-o',
str(outfilename),
'-D', 'CAT_THINK=humans are cats',
])
Expand Down Expand Up @@ -625,6 +629,7 @@ def test_main_14(tmp_path, yaml):
'--define=WORLD=Mars',
'--define=PEOPLE=Martians',
str(infilename),
'-o',
str(outfilename),
])
assert yaml.load(outfilename.open()) == {
Expand Down Expand Up @@ -656,7 +661,7 @@ def test_main_15(tmp_path, yaml):
with (tmp_path / 'in_1.yaml').open('w') as infile:
infile.write(yaml_1)
outfilename = tmp_path / 'out.yaml'
main([str(infilename), str(outfilename)])
main([str(infilename), '-o', str(outfilename)])
assert yaml.load(outfilename.open()) == {
'hello': {
'earth': 'sapiens',
Expand Down Expand Up @@ -733,6 +738,7 @@ def test_main_19(tmp_path, yaml):
'-DCOLOUR_A=red',
'-DCOLOUR_B=yellow',
str(infilename),
'-o',
str(outfilename),
])
assert yaml.load(outfilename.open()) == {
Expand All @@ -747,6 +753,32 @@ def test_main_19(tmp_path, yaml):
'flowers': {'tulip': 'U+1F337'}}


def test_main_20(tmp_path, yaml):
"""Test main, remove underscore sub-object at root level."""
infilename = tmp_path / 'a.yaml'
with infilename.open('w') as infile:
infile.write('_:\n')
infile.write('- &breakfast\n')
infile.write(' - egg\n')
infile.write(' - bread\n')
infile.write('food: *breakfast\n')
outfilename = tmp_path / 'b.yaml'
# Default
main([str(infilename), '-o', str(outfilename)])
assert yaml.load(outfilename.open()) == {'food': ['egg', 'bread']}
# Don't remove root underscore
main([
'--no-remove-root-underscore',
str(infilename),
'-o',
str(outfilename),
])
assert yaml.load(outfilename.open()) == {
'_': [['egg', 'bread']],
'food': ['egg', 'bread'],
}


def test_main_validate_1(tmp_path, capsys, yaml):
"""Test main, YAML with JSON schema validation."""
schema = {
Expand All @@ -766,7 +798,7 @@ def test_main_validate_1(tmp_path, capsys, yaml):
with infilename.open('w') as infile:
infile.write(f'{prefix}{schemafilename}\n')
yaml.dump({'hello': 'earth'}, infile)
main([str(infilename), str(outfilename)])
main([str(infilename), '-o', str(outfilename)])
captured = capsys.readouterr()
assert f'[INFO] ok {outfilename}' in captured.err.splitlines()
# Schema specified as a file:// URL
Expand Down