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
4 changes: 2 additions & 2 deletions Products/zms/repositoryutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ def get_init_yaml(self, o):
d = {}
for k in keys:
v = o.get(k)
nv = yamlutil.__cleanup(v)
nv = yamlutil._cleanup(v)
if nv:
d[k] = nv
elif nv in ['0', 0, False]:
Expand All @@ -607,7 +607,7 @@ def get_init_yaml(self, o):
nl = []
l = o.get(k)
for i in l:
ni = yamlutil.__cleanup(i)
ni = yamlutil._cleanup(i)
if ni:
if type(ni) is dict:
# Remove 'ob' from attribute dict (Acquisition-Wrappers are not serializable).
Expand Down
2 changes: 1 addition & 1 deletion Products/zms/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6.1.0+5a175b9
6.1.0+120ff56
18 changes: 14 additions & 4 deletions Products/zms/yamlutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def represent_struct_time(dumper, data):
yaml.indent(mapping=2, sequence=4, offset=2)
stream = io.StringIO()
try:
yaml.dump(__cleanup(data), stream)
yaml.dump(_cleanup(data), stream)
except Exception as e:
return f"Error during YAML serialization: {str(e)}"
return stream.getvalue()
Expand Down Expand Up @@ -79,7 +79,7 @@ def construct_struct_time(loader, node):
return yaml.load(data)


def __cleanup(v):
def _cleanup(v):
"""
Recursively cleans up a dictionary by removing keys with falsy values.
@param v: Input value, typically a dictionary, list, or scalar.
Expand All @@ -93,12 +93,13 @@ def __cleanup(v):
return err

from ruamel.yaml.scalarstring import LiteralScalarString
from ruamel.yaml.scalarstring import DoubleQuotedScalarString

if v:
if isinstance(v, dict):
nd = {}
for k in list(v.keys()):
nv = __cleanup(v[k])
nv = _cleanup(v[k])
if nv:
nd[k] = nv
elif nv in ['0', 0, False]:
Expand All @@ -107,7 +108,7 @@ def __cleanup(v):
elif isinstance(v, list):
nl = []
for i in v:
nv = __cleanup(i)
nv = _cleanup(i)
if nv:
nl.append(nv)
return nl
Expand All @@ -117,4 +118,13 @@ def __cleanup(v):
# we need to convert it to a string before dumping.
elif hasattr(v, 'read') and callable(v.read):
return str(v.read())
# If v is a <MyFile> object with a getData method.
elif hasattr(v, "getData") and callable(v.getData):
data = v.getData()
if isinstance(data, (bytes, bytearray)):
v = data.decode(errors="replace")
else:
v = str(data)
v = DoubleQuotedScalarString(v)

return v
125 changes: 125 additions & 0 deletions tests/test_yamlutil.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import time
import builtins
import pytest
from unittest.mock import patch, MagicMock
from Products.zms import yamlutil


# ---------------------------------------------------------
# _load_yaml
# ---------------------------------------------------------

def test_load_yaml_success():
YAML = MagicMock()
mock_ruamel_yaml = MagicMock(YAML=YAML)

with patch.dict('sys.modules', {'ruamel.yaml': mock_ruamel_yaml}):
result, err = yamlutil._load_yaml()
assert result is YAML
assert err is None


def test_load_yaml_import_error():
with patch.dict('sys.modules', {'ruamel.yaml': None}):
result, err = yamlutil._load_yaml()
assert result is None
assert err == yamlutil.IMPORT_ERROR_MSG


# ---------------------------------------------------------
# dump()
# ---------------------------------------------------------

def test_dump_import_error():
with patch("Products.zms.yamlutil._load_yaml", return_value=(None, yamlutil.IMPORT_ERROR_MSG)):
assert yamlutil.dump({"a": 1}) == yamlutil.IMPORT_ERROR_MSG


def test_dump_struct_time_serialization():
YAML = MagicMock()
yaml_instance = MagicMock()
YAML.return_value = yaml_instance

with patch("Products.zms.yamlutil._load_yaml", return_value=(YAML, None)):
stream = MagicMock()
yaml_instance.dump = MagicMock()

data = time.strptime("2024-01-01 12:00:00", "%Y-%m-%d %H:%M:%S")
yamlutil.dump({"t": data})

# Representer should be registered
assert yaml_instance.representer.add_representer.called

# Dump should be called
assert yaml_instance.dump.called


def test_dump_error_during_serialization():
YAML = MagicMock()
yaml_instance = MagicMock()
YAML.return_value = yaml_instance

yaml_instance.dump.side_effect = Exception("boom")

with patch("Products.zms.yamlutil._load_yaml", return_value=(YAML, None)):
result = yamlutil.dump({"a": 1})
assert "Error during YAML serialization" in result


# ---------------------------------------------------------
# parse()
# ---------------------------------------------------------

def test_parse_import_error():
with patch("Products.zms.yamlutil._load_yaml", return_value=(None, yamlutil.IMPORT_ERROR_MSG)):
assert yamlutil.parse("a: 1") == yamlutil.IMPORT_ERROR_MSG


def test_parse_struct_time():
YAML = MagicMock()
yaml_instance = MagicMock()
YAML.return_value = yaml_instance

yaml_instance.load.return_value = {"t": "dummy"}

with patch("Products.zms.yamlutil._load_yaml", return_value=(YAML, None)):
result = yamlutil.parse("t: !struct_time 2024-01-01 12:00:00")
yaml_instance.constructor.add_constructor.assert_called()
assert yaml_instance.load.called


# ---------------------------------------------------------
# _cleanup()
# ---------------------------------------------------------

def test_cleanup_removes_empty_values():
assert yamlutil._cleanup({
"a": "",
"b": None,
"c": [],
"d": {},
"e": 0,
"f": False,
"g": "ok"
}) == {"e": 0, "f": False, "g": "ok"}


def test_cleanup_nested():
data = {
"a": {
"x": "",
"y": 1
},
"b": [
"",
2,
None,
[]
]
}

cleaned = yamlutil._cleanup(data)
assert cleaned == {
"a": {"y": 1},
"b": [2]
}
Loading