Skip to content

Commit

Permalink
Make yaml optional. Fix #25
Browse files Browse the repository at this point in the history
* use PyYaml/Ruamel.YAML if present
* [yaml] requrement rule requests PyYAML

Signed-off-by: Aleksei Stepanov <penguinolog@gmail.com>
  • Loading branch information
penguinolog committed May 23, 2019
1 parent 18df7ba commit 4b25718
Show file tree
Hide file tree
Showing 9 changed files with 69 additions and 12 deletions.
4 changes: 3 additions & 1 deletion CI_REQUIREMENTS.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
-r requirements.txt
lxml
lxml!=3.7.0
PyYAML>=3.12
ruamel.yaml
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ Execution result object has a set of useful properties:

* `stdout_json` - STDOUT decoded as JSON.

* `stdout_yaml` - STDOUT decoded as YAML.
* `stdout_yaml` - STDOUT decoded as YAML. Accessible only if PyYAML or ruamel.YAML library installed.

* `stdout_xml` - STDOUT decoded as XML to `ElementTree` using `defusedxml` library.

Expand Down
1 change: 1 addition & 0 deletions doc/source/ExecResult.rst
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ API: ExecResult

:rtype: ``typing.Any``
:raises DeserializeValueError: STDOUT can not be deserialized as YAML
:raises AttributeError: no any yaml parser installed

.. py:attribute:: stdout_xml
Expand Down
26 changes: 21 additions & 5 deletions exec_helpers/exec_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,23 @@

# External Dependencies
import defusedxml.ElementTree # type: ignore
import yaml

# Exec-Helpers Implementation
from exec_helpers import exceptions
from exec_helpers import proc_enums

try:
# noinspection PyPackageRequirements
import yaml
except ImportError:
yaml = None # type:ignore # pylint: disable=invalid-name
try:
import ruamel.yaml as ruamel_yaml # type: ignore
except ImportError:
ruamel_yaml = None # pylint: disable=invalid-name

try:
# noinspection PyPackageRequirements
import lxml.etree # type: ignore # nosec
except ImportError:
lxml = None # pylint: disable=invalid-name
Expand Down Expand Up @@ -496,9 +506,11 @@ def __deserialize(self, fmt: str) -> typing.Any:
if fmt == "json":
return json.loads(self.stdout_str, encoding="utf-8")
if fmt == "yaml":
if yaml.__with_libyaml__:
return yaml.load(self.stdout_str, Loader=yaml.CSafeLoader) # nosec # Safe
return yaml.safe_load(self.stdout_str)
if yaml is not None:
if yaml.__with_libyaml__:
return yaml.load(self.stdout_str, Loader=yaml.CSafeLoader) # nosec # Safe
return yaml.safe_load(self.stdout_str)
return ruamel_yaml.YAML(typ="safe").load(self.stdout_str) # nosec # Safe
if fmt == "xml":
return defusedxml.ElementTree.fromstring(bytes(self.stdout_bin))
if fmt == "lxml":
Expand Down Expand Up @@ -531,7 +543,10 @@ def stdout_yaml(self) -> typing.Any:
:rtype: typing.Any
:raises DeserializeValueError: STDOUT can not be deserialized as YAML
:raises AttributeError: no any yaml parser installed
"""
if yaml is None and ruamel_yaml is None:
raise AttributeError("no any yaml parser installed -> attribute is not functional.")
with self.stdout_lock:
return self.__deserialize(fmt="yaml")

Expand Down Expand Up @@ -576,10 +591,11 @@ def __dir__(self) -> typing.List[str]:
"stdout_lines",
"stderr_lines",
"stdout_json",
"stdout_yaml",
"stdout_xml",
"lock",
]
if yaml is not None or ruamel_yaml is not None:
content.append("stdout_yaml")
if lxml is not None:
content.append("stdout_lxml")
return content
Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ paramiko>=2.4 # LGPLv2.1+
tenacity>=4.4.0 # Apache-2.0
six>=1.10.0 # MIT
threaded>=2.0 # Apache-2.0
PyYAML>=3.12 # MIT
advanced-descriptors>=1.0 # Apache-2.0
typing >= 3.6 ; python_version < "3.7" # PSF
psutil >= 5.0 # BSD
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ ignore =
# 1 blank line required before class docstring
D213
# Multi-line docstring summary should start at the second line
match = (?!_version|test_)*.py

[doc8]
max-line-length = 150
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,8 @@ def get_simple_vars_from_src(src):
use_scm_version={'write_to': 'exec_helpers/_version.py'},
install_requires=REQUIRED,
extras_require={
"lxml": ["lxml!=3.7.0"]
"lxml": ["lxml!=3.7.0"],
"yaml": ["PyYAML>=3.12"],
},
package_data={"exec_helpers": ["py.typed"]},
)
Expand Down
42 changes: 40 additions & 2 deletions test/test_exec_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@
import exec_helpers
from exec_helpers import proc_enums

try:
import yaml
except ImportError:
yaml = None
try:
import ruamel.yaml as ruamel_yaml
except ImportError:
ruamel_yaml = None
try:
import lxml.etree
except ImportError:
Expand Down Expand Up @@ -79,7 +87,6 @@ def test_create_minimal(self, logger):
# noinspection PyStatementEffect
result["stdout_json"] # pylint: disable=pointless-statement
logger.assert_has_calls((mock.call.exception(f"{cmd} stdout is not valid json:\n{result.stdout_str!r}\n"),))
self.assertIsNone(result["stdout_yaml"])

self.assertEqual(hash(result), hash((exec_helpers.ExecResult, cmd, None, (), (), proc_enums.INVALID)))

Expand Down Expand Up @@ -149,7 +156,6 @@ def test_wrong_result(self, logger):
# noinspection PyStatementEffect
result.stdout_json # pylint: disable=pointless-statement
logger.assert_has_calls((mock.call.exception(f"{cmd} stdout is not valid json:\n{result.stdout_str!r}\n"),))
self.assertIsNone(result["stdout_yaml"])

def test_not_equal(self):
"""Exec result equality is validated by all fields."""
Expand Down Expand Up @@ -294,3 +300,35 @@ def test_stdout_lxml(self):
self.assertEqual(
lxml.etree.tostring(expect), lxml.etree.tostring(result.stdout_lxml)
)

@unittest.skipUnless(yaml is not None, "PyYAML parser should be installed")
def test_stdout_yaml_pyyaml(self):
result = exec_helpers.ExecResult(
"test",
stdout=[
b"{test: data}\n"
]
)
expect = {"test": "data"}
self.assertEqual(expect, result.stdout_yaml)


# noinspection PyTypeChecker
class TestExecResultRuamelYaml(unittest.TestCase):
def setUp(self) -> None:
self._orig_yaml, exec_helpers.exec_result.yaml = exec_helpers.exec_result.yaml, None

def tearDown(self) -> None:
exec_helpers.exec_result.yaml = self._orig_yaml

@unittest.skipUnless(ruamel_yaml is not None, "Ruamel.YAML parser should be installed")
def test_stdout_yaml_ruamel(self):
result = exec_helpers.ExecResult(
"test",
stdout=[
b"{test: data}\n"
]
)
expect = {"test": "data"}
result = result.stdout_yaml
self.assertEqual(expect, result)
1 change: 0 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ commands = pipdeptree
[testenv:mypy]
deps =
mypy>=0.700
lxml
-r{toxinidir}/CI_REQUIREMENTS.txt
commands =
python setup.py --version
Expand Down

0 comments on commit 4b25718

Please sign in to comment.