Skip to content

Commit e1c8dcb

Browse files
committed
support stdout_lxml for advanced functionality
* lxml is in optional requirements Signed-off-by: Aleksei Stepanov <penguinolog@gmail.com>
1 parent 361d3c5 commit e1c8dcb

File tree

7 files changed

+67
-8
lines changed

7 files changed

+67
-8
lines changed

CI_REQUIREMENTS.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
mock # no assert_called_once in py35
22
-r requirements.txt
3+
lxml

README.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ Execution result object has a set of useful properties:
205205

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

208+
* `stdout_lxml` - STDOUT decoded as XML to `ElementTree` using `lxml` library. Accessible only if lxml library installed. Can be insecure.
209+
208210
* `timestamp` -> `typing.Optional(datetime.datetime)`. Timestamp for received exit code.
209211

210212
SSHClient specific

doc/source/ExecResult.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,16 @@ API: ExecResult
154154
:rtype: xml.etree.ElementTree.Element
155155
:raises DeserializeValueError: STDOUT can not be deserialized as XML
156156

157+
.. py:attribute:: stdout_lxml
158+
159+
XML from stdout using lxml.
160+
161+
:rtype: lxml.etree.Element
162+
:raises DeserializeValueError: STDOUT can not be deserialized as XML
163+
:raises AttributeError: lxml is not installed
164+
165+
.. note:: Can be insecure.
166+
157167
.. py:method:: read_stdout(src=None, log=None, verbose=False)
158168
159169
Read stdout file-like object to stdout.

exec_helpers/exec_result.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2018 Alexey Stepanov aka penguinolog.
1+
# Copyright 2018 - 2019 Alexey Stepanov aka penguinolog.
22

33
# Copyright 2016 Mirantis, Inc.
44
#
@@ -37,6 +37,11 @@
3737
from exec_helpers import exceptions
3838
from exec_helpers import proc_enums
3939

40+
try:
41+
import lxml.etree # type: ignore # nosec
42+
except ImportError:
43+
lxml = None # pylint: disable=invalid-name
44+
4045
if typing.TYPE_CHECKING:
4146
import xml.etree.ElementTree # nosec # noqa # pylint: disable=unused-import
4247

@@ -496,6 +501,8 @@ def __deserialize(self, fmt): # type: (typing.Text) -> typing.Any
496501
return yaml.safe_load(self.stdout_str)
497502
if fmt == "xml":
498503
return defusedxml.ElementTree.fromstring(bytes(self.stdout_bin))
504+
if fmt == "lxml":
505+
return lxml.etree.fromstring(bytes(self.stdout_bin)) # nosec
499506
except Exception:
500507
tmpl = " stdout is not valid {fmt}:\n" "{{stdout!r}}\n".format(fmt=fmt)
501508
logger.exception(self.cmd + tmpl.format(stdout=self.stdout_str))
@@ -526,17 +533,32 @@ def stdout_yaml(self): # type: () -> typing.Any
526533

527534
@property
528535
def stdout_xml(self): # type: () -> xml.etree.ElementTree.Element
529-
"""YAML from stdout.
536+
"""XML from stdout.
530537
531538
:rtype: xml.etree.ElementTree.Element
532539
:raises DeserializeValueError: STDOUT can not be deserialized as XML
533540
"""
534541
with self.stdout_lock:
535542
return self.__deserialize(fmt="xml") # type: ignore
536543

544+
@property
545+
def stdout_lxml(self): # type: () -> "lxml.etree.Element"
546+
"""XML from stdout using lxml.
547+
548+
:rtype: lxml.etree.Element
549+
:raises DeserializeValueError: STDOUT can not be deserialized as XML
550+
:raises AttributeError: lxml is not installed
551+
552+
.. note:: Can be insecure.
553+
"""
554+
if lxml is None:
555+
raise AttributeError("lxml is not installed -> attribute is not functional.")
556+
with self.stdout_lock:
557+
return self.__deserialize(fmt="lxml")
558+
537559
def __dir__(self): # type: () -> typing.List[typing.Text]
538560
"""Override dir for IDE and as source for getitem checks."""
539-
return [
561+
content = [
540562
"cmd",
541563
"stdout",
542564
"stderr",
@@ -554,6 +576,9 @@ def __dir__(self): # type: () -> typing.List[typing.Text]
554576
"stdout_xml",
555577
"lock",
556578
]
579+
if lxml is not None:
580+
content.append("stdout_lxml")
581+
return content
557582

558583
def __getitem__(self, item): # type: (typing.Union[str, typing.Text]) -> typing.Any
559584
"""Dict like get data.

requirements.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ threaded>=1.0 # Apache-2.0
55
PyYAML>=3.12 # MIT
66
advanced-descriptors>=1.0 # Apache-2.0
77
typing >= 3.6; python_version < "3.7" # PSF
8-
futures>=3.1; python_version == "2.7"
9-
enum34>=1.1; python_version == "2.7"
10-
psutil >= 5.0
11-
defusedxml
8+
futures>=3.1; python_version == "2.7" # PSF
9+
enum34>=1.1; python_version == "2.7" # PSF
10+
psutil >= 5.0 # BSD
11+
defusedxml # PSF

setup.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2018 Alexey Stepanov aka penguinolog
1+
# Copyright 2018 - 2019 Alexey Stepanov aka penguinolog
22

33
# Copyright 2016 Mirantis, Inc.
44

@@ -178,6 +178,9 @@ def get_simple_vars_from_src(src):
178178
],
179179
use_scm_version=True,
180180
install_requires=required,
181+
extras_require={
182+
"lxml": ["lxml!=3.7.0"]
183+
},
181184
package_data={
182185
str('exec_helpers'): ['py.typed'],
183186
},

test/test_exec_result.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
import exec_helpers
3030
from exec_helpers import proc_enums
3131

32+
try:
33+
import lxml.etree
34+
except ImportError:
35+
lxml = None
3236

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

@@ -315,3 +319,17 @@ def test_stdout_xml(self):
315319
self.assertEqual(
316320
xml.etree.ElementTree.tostring(expect), xml.etree.ElementTree.tostring(result.stdout_xml)
317321
)
322+
323+
@unittest.skipIf(lxml is None, "no lxml installed")
324+
def test_stdout_lxml(self):
325+
result = exec_helpers.ExecResult(
326+
"test",
327+
stdout=[
328+
b"<?xml version='1.0'?>\n",
329+
b'<data>123</data>\n',
330+
]
331+
)
332+
expect = lxml.etree.fromstring(b"<?xml version='1.0'?>\n<data>123</data>\n")
333+
self.assertEqual(
334+
lxml.etree.tostring(expect), lxml.etree.tostring(result.stdout_lxml)
335+
)

0 commit comments

Comments
 (0)