Skip to content

Commit

Permalink
support stdout_lxml for advanced functionality
Browse files Browse the repository at this point in the history
* lxml is in optional requirements

Signed-off-by: Aleksei Stepanov <penguinolog@gmail.com>
  • Loading branch information
penguinolog committed May 22, 2019
1 parent e072be6 commit 2f0c3c7
Show file tree
Hide file tree
Showing 8 changed files with 66 additions and 8 deletions.
4 changes: 1 addition & 3 deletions .pylintrc
Expand Up @@ -3,7 +3,7 @@
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-whitelist=
extension-pkg-whitelist=lxml.etree

# Add files or directories to the blacklist. They should be base names, not
# paths.
Expand Down Expand Up @@ -66,15 +66,13 @@ enable=all

disable=import-star-module-level,
raw-checker-failed,
bad-inline-option,
locally-disabled,
file-ignored,
suppressed-message,
use-symbolic-message-instead,
input-builtin,
round-builtin,
eq-without-hash,
exception-message-attribute,
exception-escape,
comprehension-escape,
similarities,
Expand Down
1 change: 1 addition & 0 deletions CI_REQUIREMENTS.txt
@@ -1,2 +1,3 @@
mock # no assert_called_once in py35
-r requirements.txt
lxml
2 changes: 2 additions & 0 deletions README.rst
Expand Up @@ -224,6 +224,8 @@ Execution result object has a set of useful properties:

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

* `stdout_lxml` - STDOUT decoded as XML to `ElementTree` using `lxml` library. Accessible only if lxml library installed. Can be insecure.

* `timestamp` -> `typing.Optional(datetime.datetime)`. Timestamp for received exit code.

SSHClient specific
Expand Down
10 changes: 10 additions & 0 deletions doc/source/ExecResult.rst
Expand Up @@ -154,6 +154,16 @@ API: ExecResult
:rtype: xml.etree.ElementTree.Element
:raises DeserializeValueError: STDOUT can not be deserialized as XML

.. py:attribute:: stdout_lxml
XML from stdout using lxml.

:rtype: lxml.etree.Element
:raises DeserializeValueError: STDOUT can not be deserialized as XML
:raises AttributeError: lxml is not installed

.. note:: Can be insecure.

.. py:method:: read_stdout(src=None, log=None, verbose=False)
Read stdout file-like object to stdout.
Expand Down
29 changes: 27 additions & 2 deletions exec_helpers/exec_result.py
Expand Up @@ -34,6 +34,11 @@
from exec_helpers import exceptions
from exec_helpers import proc_enums

try:
import lxml.etree # type: ignore # nosec
except ImportError:
lxml = None # pylint: disable=invalid-name

LOGGER = logging.getLogger(__name__)


Expand Down Expand Up @@ -496,6 +501,8 @@ def __deserialize(self, fmt: str) -> typing.Any:
return yaml.safe_load(self.stdout_str)
if fmt == "xml":
return defusedxml.ElementTree.fromstring(bytes(self.stdout_bin))
if fmt == "lxml":
return lxml.etree.fromstring(bytes(self.stdout_bin)) # nosec
except Exception as e:
tmpl = "{{self.cmd}} stdout is not valid {fmt}:\n" "{{stdout!r}}\n".format(fmt=fmt)
LOGGER.exception(tmpl.format(self=self, stdout=self.stdout_str))
Expand Down Expand Up @@ -530,17 +537,32 @@ def stdout_yaml(self) -> typing.Any:

@property
def stdout_xml(self) -> xml.etree.ElementTree.Element:
"""YAML from stdout.
"""XML from stdout.
:rtype: xml.etree.ElementTree.Element
:raises DeserializeValueError: STDOUT can not be deserialized as XML
"""
with self.stdout_lock:
return self.__deserialize(fmt="xml") # type: ignore

@property
def stdout_lxml(self) -> "lxml.etree.Element":
"""XML from stdout using lxml.
:rtype: lxml.etree.Element
:raises DeserializeValueError: STDOUT can not be deserialized as XML
:raises AttributeError: lxml is not installed
.. note:: Can be insecure.
"""
if lxml is None:
raise AttributeError("lxml is not installed -> attribute is not functional.")
with self.stdout_lock:
return self.__deserialize(fmt="lxml")

def __dir__(self) -> typing.List[str]:
"""Override dir for IDE and as source for getitem checks."""
return [
content = [
"cmd",
"stdout",
"stderr",
Expand All @@ -558,6 +580,9 @@ def __dir__(self) -> typing.List[str]:
"stdout_xml",
"lock",
]
if lxml is not None:
content.append("stdout_lxml")
return content

def __getitem__(self, item: str) -> typing.Any:
"""Dict like get data.
Expand Down
6 changes: 3 additions & 3 deletions requirements.txt
Expand Up @@ -4,6 +4,6 @@ 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"
psutil >= 5.0
defusedxml
typing >= 3.6 ; python_version < "3.7" # PSF
psutil >= 5.0 # BSD
defusedxml # PSF
3 changes: 3 additions & 0 deletions setup.py
Expand Up @@ -255,6 +255,9 @@ def get_simple_vars_from_src(src):
],
use_scm_version=True,
install_requires=REQUIRED,
extras_require={
"lxml": ["lxml!=3.7.0"]
},
package_data={"exec_helpers": ["py.typed"]},
)
if cythonize is not None:
Expand Down
19 changes: 19 additions & 0 deletions test/test_exec_result.py
Expand Up @@ -26,6 +26,11 @@
import exec_helpers
from exec_helpers import proc_enums

try:
import lxml.etree
except ImportError:
lxml = None

cmd = "ls -la | awk '{print $1}'"


Expand Down Expand Up @@ -305,3 +310,17 @@ def test_stdout_xml(self):
self.assertEqual(
xml.etree.ElementTree.tostring(expect), xml.etree.ElementTree.tostring(result.stdout_xml)
)

@unittest.skipIf(lxml is None, "no lxml installed")
def test_stdout_lxml(self):
result = exec_helpers.ExecResult(
"test",
stdout=[
b"<?xml version='1.0'?>\n",
b'<data>123</data>\n',
]
)
expect = lxml.etree.fromstring(b"<?xml version='1.0'?>\n<data>123</data>\n")
self.assertEqual(
lxml.etree.tostring(expect), lxml.etree.tostring(result.stdout_lxml)
)

0 comments on commit 2f0c3c7

Please sign in to comment.