Skip to content

Commit 4c984db

Browse files
committed
Backport: Rework exceptions
1. Allow substitute `CalledProcessError` 2. Modify exceptions class hierarchy Signed-off-by: Alexey Stepanov <penguinolog@gmail.com>
1 parent 6b3df85 commit 4c984db

File tree

10 files changed

+107
-67
lines changed

10 files changed

+107
-67
lines changed

doc/source/exceptions.rst

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -100,16 +100,14 @@ API: exceptions
100100
``typing.Text``
101101
stdout string or brief string
102102

103-
.. py:exception:: ParallelCallExceptions(ExecCalledProcessError)
103+
.. py:exception:: ParallelCallProcessError(ExecCalledProcessError)
104104
105-
Exception raised during parallel call as result of exceptions.
105+
Exception during parallel execution.
106106

107-
.. py:method:: __init__(command, exceptions, errors, results, expected=None, )
107+
.. py:method:: __init__(command, errors, results, expected=None, )
108108
109109
:param command: command
110110
:type command: ``str``
111-
:param exceptions: Exception on connections
112-
:type exceptions: ``typing.Dict[typing.Tuple[str, int], Exception]``
113111
:param errors: results with errors
114112
:type errors: typing.Dict[typing.Tuple[str, int], ExecResult]
115113
:param results: all results
@@ -124,11 +122,6 @@ API: exceptions
124122
``str``
125123
command
126124

127-
.. py:attribute:: exceptions
128-
129-
``typing.Dict[typing.Tuple[str, int], Exception]``
130-
Exception on connections
131-
132125
.. py:attribute:: errors
133126
134127
results with errors
@@ -147,14 +140,16 @@ API: exceptions
147140

148141
:rtype: typing.List[typing.Union[int, ExitCodes]]
149142

150-
.. py:exception:: ParallelCallProcessError(ExecCalledProcessError)
143+
.. py:exception:: ParallelCallExceptions(ParallelCallProcessError)
151144
152-
Exception during parallel execution.
145+
Exception raised during parallel call as result of exceptions.
153146

154-
.. py:method:: __init__(command, errors, results, expected=None, )
147+
.. py:method:: __init__(command, exceptions, errors, results, expected=None, )
155148
156149
:param command: command
157150
:type command: ``str``
151+
:param exceptions: Exception on connections
152+
:type exceptions: ``typing.Dict[typing.Tuple[str, int], Exception]``
158153
:param errors: results with errors
159154
:type errors: typing.Dict[typing.Tuple[str, int], ExecResult]
160155
:param results: all results
@@ -169,6 +164,11 @@ API: exceptions
169164
``str``
170165
command
171166

167+
.. py:attribute:: exceptions
168+
169+
``typing.Dict[typing.Tuple[str, int], Exception]``
170+
Exception on connections
171+
172172
.. py:attribute:: errors
173173
174174
results with errors

exec_helpers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
except pkg_resources.DistributionNotFound:
5959
# package is not installed, try to get from SCM
6060
try:
61+
# noinspection PyPackageRequirements,PyUnresolvedReferences
6162
import setuptools_scm # type: ignore
6263

6364
__version__ = setuptools_scm.get_version()

exec_helpers/_ssh_client_base.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,7 @@ def _exec_command( # type: ignore
637637
638638
.. versionchanged:: 1.2.0 log_mask_re regex rule for masking cmd
639639
"""
640+
640641
def poll_streams(): # type: () -> None
641642
"""Poll FIFO buffers if data available."""
642643
if async_result.stdout and async_result.interface.recv_ready():
@@ -811,6 +812,7 @@ def execute_together(
811812
.. versionchanged:: 1.2.0 default timeout 1 hour
812813
.. versionchanged:: 1.2.0 log_mask_re regex rule for masking cmd
813814
"""
815+
814816
@threaded.threadpooled
815817
def get_result(remote): # type: (SSHClientBase) -> exec_result.ExecResult
816818
"""Get result from remote call."""
@@ -858,7 +860,9 @@ def get_result(remote): # type: (SSHClientBase) -> exec_result.ExecResult
858860
if raised_exceptions: # always raise
859861
raise exceptions.ParallelCallExceptions(command, raised_exceptions, errors, results, expected=expected)
860862
if errors and raise_on_err:
861-
raise exceptions.ParallelCallProcessError(command, errors, results, expected=expected)
863+
raise kwargs.get("exception_class", exceptions.ParallelCallProcessError)(
864+
command, errors, results, expected=expected
865+
)
862866
return results
863867

864868
def open(self, path, mode="r"): # type: (str, str) -> paramiko.SFTPFile

exec_helpers/_subprocess_helpers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def kill_proc_tree(pid, including_parent=True): # type:(int, bool) -> None # p
4040
:param including_parent: kill also parent process
4141
:type including_parent: bool
4242
"""
43+
4344
def safe_stop(proc, kill=False): # type: (psutil.Process, bool) -> None
4445
"""Do not crash on already stopped process."""
4546
try:

exec_helpers/api.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def _mask_command(self, cmd, log_mask_re=None): # type: (str, typing.Optional[s
107107
108108
.. versionadded:: 1.2.0
109109
"""
110+
110111
def mask(text, rules): # type: (str, str) -> str
111112
"""Mask part of text using rules."""
112113
indexes = [0] # Start of the line
@@ -125,7 +126,7 @@ def mask(text, rules): # type: (str, str) -> str
125126
end = indexes[idx + 1]
126127
masked += text[start:end] + "<*masked*>"
127128

128-
masked += text[indexes[-2]: indexes[-1]] # final part
129+
masked += text[indexes[-2] : indexes[-1]] # final part
129130
return masked
130131

131132
cmd = cmd.rstrip()
@@ -292,7 +293,7 @@ def check_call(
292293
)
293294
self.logger.error(msg=message)
294295
if raise_on_err:
295-
raise exceptions.CalledProcessError(result=ret, expected=expected_codes)
296+
raise kwargs.get("exception_class", exceptions.CalledProcessError)(result=ret, expected=expected_codes)
296297
return ret
297298

298299
def check_stderr(
@@ -335,7 +336,9 @@ def check_stderr(
335336
)
336337
self.logger.error(msg=message)
337338
if raise_on_err:
338-
raise exceptions.CalledProcessError(result=ret, expected=kwargs.get("expected"))
339+
raise kwargs.get("exception_class", exceptions.CalledProcessError)(
340+
result=ret, expected=kwargs.get("expected")
341+
)
339342
return ret
340343

341344
@staticmethod

exec_helpers/exceptions.py

Lines changed: 48 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,10 @@ def __init__(self, result, timeout): # type: (exec_result.ExecResult, typing.Un
7171
:param timeout: timeout for command
7272
:type timeout: typing.Union[int, float]
7373
"""
74-
self.result = result
75-
self.timeout = timeout
7674
message = _log_templates.CMD_WAIT_ERROR.format(result=result, timeout=timeout)
7775
super(ExecHelperTimeoutError, self).__init__(message)
76+
self.result = result
77+
self.timeout = timeout
7878

7979
@property
8080
def cmd(self): # type: () -> str
@@ -100,14 +100,14 @@ class CalledProcessError(ExecCalledProcessError):
100100
def __init__(
101101
self,
102102
result, # type: exec_result.ExecResult
103-
expected=None, # type: typing.Optional[typing.List[typing.Union[int, proc_enums.ExitCodes]]]
103+
expected=None, # type: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]]
104104
): # type: (...) -> None
105105
"""Exception for error on process calls.
106106
107107
:param result: execution result
108108
:type result: exec_result.ExecResult
109109
:param expected: expected return codes
110-
:type expected: typing.Optional[typing.List[typing.Union[int, proc_enums.ExitCodes]]]
110+
:type expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]]
111111
112112
.. versionchanged:: 1.1.1 - provide full result
113113
"""
@@ -144,91 +144,97 @@ def stderr(self): # type: () -> typing.Text
144144
return self.result.stderr_str
145145

146146

147-
class ParallelCallExceptions(ExecCalledProcessError):
148-
"""Exception raised during parallel call as result of exceptions."""
147+
class ParallelCallProcessError(ExecCalledProcessError):
148+
"""Exception during parallel execution."""
149149

150-
__slots__ = ("cmd", "exceptions", "errors", "results", "expected")
150+
__slots__ = ("cmd", "errors", "results", "expected")
151151

152152
def __init__(
153153
self,
154154
command, # type: str
155-
exceptions, # type: typing.Dict[typing.Tuple[str, int], Exception]
156155
errors, # type: typing.Dict[typing.Tuple[str, int], exec_result.ExecResult]
157156
results, # type: typing.Dict[typing.Tuple[str, int], exec_result.ExecResult]
158157
expected=None, # type: typing.Optional[typing.List[typing.Union[int, proc_enums.ExitCodes]]]
158+
**kwargs # type: typing.Any
159159
): # type: (...) -> None
160160
"""Exception raised during parallel call as result of exceptions.
161161
162162
:param command: command
163163
:type command: str
164-
:param exceptions: Exceptions on connections
165-
:type exceptions: typing.Dict[typing.Tuple[str, int], Exception]
166164
:param errors: results with errors
167165
:type errors: typing.Dict[typing.Tuple[str, int], ExecResult]
168166
:param results: all results
169167
:type results: typing.Dict[typing.Tuple[str, int], ExecResult]
170168
:param expected: expected return codes
171169
:type expected: typing.Optional[typing.List[typing.Union[int, proc_enums.ExitCodes]]]
170+
:keyword _message: error message override
172171
"""
173172
expected = expected or [proc_enums.ExitCodes.EX_OK]
174-
self.expected = proc_enums.exit_codes_to_enums(expected)
175-
self.cmd = command
176-
self.exceptions = exceptions
177-
self.errors = errors
178-
self.results = results
179-
message = (
180-
"Command {self.cmd!r} "
181-
"during execution raised exceptions: \n"
182-
"\t{exceptions}".format(
183-
self=self,
184-
exceptions="\n\t".join(
185-
"{host}:{port} - {exc} ".format(host=host, port=port, exc=exc)
186-
for (host, port), exc in exceptions.items()
173+
prep_expected = proc_enums.exit_codes_to_enums(expected)
174+
message = kwargs.get("_message", None) or (
175+
"Command {cmd!r} "
176+
"returned unexpected exit codes on several hosts\n"
177+
"Expected: {expected}\n"
178+
"Got:\n"
179+
"\t{errors}".format(
180+
cmd=command,
181+
expected=prep_expected,
182+
errors="\n\t".join(
183+
"{host}:{port} - {code} ".format(host=host, port=port, code=result.exit_code)
184+
for (host, port), result in errors.items()
187185
),
188186
)
189187
)
190-
super(ParallelCallExceptions, self).__init__(message)
188+
super(ParallelCallProcessError, self).__init__(message)
189+
self.cmd = command
190+
self.errors = errors
191+
self.results = results
192+
self.expected = prep_expected
191193

192194

193-
class ParallelCallProcessError(ExecCalledProcessError):
194-
"""Exception during parallel execution."""
195+
class ParallelCallExceptions(ParallelCallProcessError):
196+
"""Exception raised during parallel call as result of exceptions."""
195197

196-
__slots__ = ("cmd", "errors", "results", "expected")
198+
__slots__ = ("cmd", "exceptions")
197199

198200
def __init__(
199201
self,
200202
command, # type: str
203+
exceptions, # type: typing.Dict[typing.Tuple[str, int], Exception]
201204
errors, # type: typing.Dict[typing.Tuple[str, int], exec_result.ExecResult]
202205
results, # type: typing.Dict[typing.Tuple[str, int], exec_result.ExecResult]
203206
expected=None, # type: typing.Optional[typing.List[typing.Union[int, proc_enums.ExitCodes]]]
207+
**kwargs # type: typing.Any
204208
): # type: (...) -> None
205209
"""Exception during parallel execution.
206210
207211
:param command: command
208212
:type command: str
213+
:param exceptions: Exceptions on connections
214+
:type exceptions: typing.Dict[typing.Tuple[str, int], Exception]
209215
:param errors: results with errors
210216
:type errors: typing.Dict[typing.Tuple[str, int], ExecResult]
211217
:param results: all results
212218
:type results: typing.Dict[typing.Tuple[str, int], ExecResult]
213219
:param expected: expected return codes
214220
:type expected: typing.Optional[typing.List[typing.Union[int, proc_enums.ExitCodes]]]
221+
:keyword _message: error message override
215222
"""
216223
expected = expected or [proc_enums.ExitCodes.EX_OK]
217-
self.expected = proc_enums.exit_codes_to_enums(expected)
218-
self.cmd = command
219-
self.errors = errors
220-
self.results = results
221-
message = (
222-
"Command {self.cmd!r} "
223-
"returned unexpected exit codes on several hosts\n"
224-
"Expected: {self.expected}\n"
225-
"Got:\n"
226-
"\t{errors}".format(
227-
self=self,
228-
errors="\n\t".join(
229-
"{host}:{port} - {code} ".format(host=host, port=port, code=result.exit_code)
230-
for (host, port), result in errors.items()
224+
prep_expected = proc_enums.exit_codes_to_enums(expected)
225+
message = kwargs.get("_message", None) or (
226+
"Command {cmd!r} "
227+
"during execution raised exceptions: \n"
228+
"\t{exceptions}".format(
229+
cmd=command,
230+
exceptions="\n\t".join(
231+
"{host}:{port} - {exc} ".format(host=host, port=port, exc=exc)
232+
for (host, port), exc in exceptions.items()
231233
),
232234
)
233235
)
234-
super(ParallelCallProcessError, self).__init__(message)
236+
super(ParallelCallExceptions, self).__init__(
237+
command=command, errors=errors, results=results, expected=prep_expected, _message=message
238+
)
239+
self.cmd = command
240+
self.exceptions = exceptions

exec_helpers/metaclasses.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020

2121
import abc
2222
import threading
23+
2324
# pylint: disable=unused-import
2425
import typing # noqa: F401
26+
2527
# pylint: enable=unused-import
2628

2729

exec_helpers/subprocess_runner.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ def _exec_command( # type: ignore
101101
102102
.. versionadded:: 1.2.0
103103
"""
104+
104105
@threaded.threadpooled
105106
def poll_stdout(): # type: () -> None
106107
"""Sync stdout poll."""
@@ -149,21 +150,21 @@ def close_streams(): # type: () -> None
149150
exit_code = async_result.interface.poll()
150151
if exit_code is not None: # Nothing to kill
151152
self.logger.warning(
152-
"{!s} has been completed just after timeout: please validate timeout.".format(command))
153+
"{!s} has been completed just after timeout: please validate timeout.".format(command)
154+
)
153155
concurrent.futures.wait([stdout_future, stderr_future], timeout=0.1)
154156
result.exit_code = exit_code
155157
return result
156158
raise # Some other error
157159
finally:
158160
stdout_future.cancel()
159161
stderr_future.cancel()
160-
_, not_done = concurrent.futures.wait([stdout_future, stderr_future], timeout=5)
162+
_, not_done = concurrent.futures.wait([stdout_future, stderr_future], timeout=1)
161163
if not_done:
162-
exit_code = async_result.interface.poll()
163-
if exit_code:
164+
if async_result.interface.returncode:
164165
self.logger.critical(
165166
"Process {!s} was closed with exit code {!s}, but FIFO buffers are still open".format(
166-
command, exit_code
167+
command, async_result.interface.returncode
167168
)
168169
)
169170
close_streams()

setup.cfg

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,25 @@ exclude =
3939
__init__.py,
4040
docs
4141
ignore =
42-
# line break before binary operator
42+
E203,
4343
W503
44+
# whitespace before ':'
45+
# line break before binary operator
4446
show-pep8 = True
4547
show-source = True
4648
count = True
4749
max-line-length = 120
4850

4951
[pydocstyle]
50-
ignore = D401, D203, D213
52+
ignore =
53+
D401,
54+
D202,
55+
D203,
56+
D213
57+
# First line should be in imperative mood; try rephrasing
58+
# No blank lines allowed after function docstring
59+
# 1 blank line required before class docstring
60+
# Multi-line docstring summary should start at the second line
5161

5262
[aliases]
5363
test=pytest

0 commit comments

Comments
 (0)