Repository: python-useful-helpers/exec-helpers

Issue:
Timeout error object should contain execution result and timeout for the future processing
Currently `ExecHelperTimeoutError` contains only message and inherited from `ExecHelperError` while used in single call scenarios only.
Need to re-inherit `ExecHelperError` and provide result + timeout + properties access to cmd, stdout_str and stderr_str.

Discussion:
coveralls: ## Pull Request Test Coverage Report for [Build 128](https://coveralls.io/builds/17142361)

* **17** of **17**   **(100.0%)**  changed or added relevant lines in **3** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **100.0%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/17142361/badge)](https://coveralls.io/builds/17142361) |
| :-- | --: |
| Change from base [Build 127](https://coveralls.io/builds/17054024): |  0.0% |
| Covered Lines: | 945 |
| Relevant Lines: | 945 |

---
##### 💛  - [Coveralls](https://coveralls.io)

coveralls: ## Pull Request Test Coverage Report for [Build 128](https://coveralls.io/builds/17142361)

* **17** of **17**   **(100.0%)**  changed or added relevant lines in **3** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **100.0%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/17142361/badge)](https://coveralls.io/builds/17142361) |
| :-- | --: |
| Change from base [Build 127](https://coveralls.io/builds/17054024): |  0.0% |
| Covered Lines: | 945 |
| Relevant Lines: | 945 |

---
##### 💛  - [Coveralls](https://coveralls.io)

coveralls: ## Pull Request Test Coverage Report for [Build 128](https://coveralls.io/builds/17142361)

* **17** of **17**   **(100.0%)**  changed or added relevant lines in **3** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **100.0%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/17142361/badge)](https://coveralls.io/builds/17142361) |
| :-- | --: |
| Change from base [Build 127](https://coveralls.io/builds/17054024): |  0.0% |
| Covered Lines: | 945 |
| Relevant Lines: | 945 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/doc/source/exceptions.rst b/doc/source/exceptions.rst
index d558cae..5d50d24 100644
--- a/doc/source/exceptions.rst
+++ b/doc/source/exceptions.rst
@@ -14,13 +14,45 @@ API: exceptions
 
     Deserialize impossible.
 
-.. py:exception:: ExecHelperTimeoutError(ExecHelperError)
+.. py:exception:: ExecCalledProcessError(ExecHelperError)
+
+    Base class for process call errors.
+
+.. py:exception:: ExecHelperTimeoutError(ExecCalledProcessError)
 
     Execution timeout.
 
-.. py:exception:: ExecCalledProcessError(ExecHelperError)
+    .. versionchanged:: 1.3.0 provide full result and timeout inside.
+    .. versionchanged:: 1.3.0 subclass ExecCalledProcessError
 
-    Base class for process call errors.
+    .. py:method:: __init__(self, result, timeout)
+
+        Exception for error on process calls.
+
+        :param result: execution result
+        :type result: exec_result.ExecResult
+        :param timeout: timeout for command
+        :type timeout: typing.Union[int, float]
+
+    .. py:attribute:: timeout
+
+        ``int``
+
+    .. py:attribute:: result
+
+        Execution result
+
+        :rtype: ExecResult
+
+    .. py:attribute:: stdout
+
+        ``typing.Text``
+        stdout string or brief string
+
+    .. py:attribute:: stderr
+
+        ``typing.Text``
+        stdout string or brief string
 
 .. py:exception:: CalledProcessError(ExecCalledProcessError)
 
@@ -60,12 +92,12 @@ API: exceptions
 
     .. py:attribute:: stdout
 
-        ``typing.Any``
+        ``typing.Text``
         stdout string or brief string
 
     .. py:attribute:: stderr
 
-        ``typing.Any``
+        ``typing.Text``
         stdout string or brief string
 
 .. py:exception:: ParallelCallExceptions(ExecCalledProcessError)
diff --git a/exec_helpers/_ssh_client_base.py b/exec_helpers/_ssh_client_base.py
index 65395bc..f0ce5b5 100644
--- a/exec_helpers/_ssh_client_base.py
+++ b/exec_helpers/_ssh_client_base.py
@@ -735,7 +735,7 @@ class SSHClientBase(six.with_metaclass(_MemorizedSSH, _api.ExecHelper)):
             timeout=timeout
         )
         self.logger.debug(wait_err_msg)
-        raise exceptions.ExecHelperTimeoutError(wait_err_msg)
+        raise exceptions.ExecHelperTimeoutError(result=result, timeout=timeout)
 
     def execute_through_host(
         self,
diff --git a/exec_helpers/exceptions.py b/exec_helpers/exceptions.py
index 07d2cb4..fc01aa7 100644
--- a/exec_helpers/exceptions.py
+++ b/exec_helpers/exceptions.py
@@ -21,6 +21,7 @@ from __future__ import unicode_literals
 import typing  # noqa  # pylint: disable=unused-import
 
 from exec_helpers import proc_enums
+from exec_helpers import _log_templates
 
 __all__ = (
     'ExecHelperError',
@@ -44,16 +45,58 @@ class DeserializeValueError(ExecHelperError, ValueError):
     __slots__ = ()
 
 
-class ExecHelperTimeoutError(ExecHelperError):
-    """Execution timeout."""
+class ExecCalledProcessError(ExecHelperError):
+    """Base class for process call errors."""
 
     __slots__ = ()
 
 
-class ExecCalledProcessError(ExecHelperError):
-    """Base class for process call errors."""
+class ExecHelperTimeoutError(ExecCalledProcessError):
+    """Execution timeout.
 
-    __slots__ = ()
+    .. versionchanged:: 1.3.0 provide full result and timeout inside.
+    .. versionchanged:: 1.3.0 subclass ExecCalledProcessError
+    """
+
+    __slots__ = (
+        'result',
+        'timeout',
+    )
+
+    def __init__(
+        self,
+        result,  # type: exec_result.ExecResult
+        timeout,  # type: typing.Union[int, float]
+    ):  # type: (...) -> None
+        """Exception for error on process calls.
+
+        :param result: execution result
+        :type result: exec_result.ExecResult
+        :param timeout: timeout for command
+        :type timeout: typing.Union[int, float]
+        """
+        self.result = result
+        self.timeout = timeout
+        message = _log_templates.CMD_WAIT_ERROR.format(
+            result=result,
+            timeout=timeout
+        )
+        super(ExecHelperTimeoutError, self).__init__(message)
+
+    @property
+    def cmd(self):  # type: () -> str
+        """Failed command."""
+        return self.result.cmd
+
+    @property
+    def stdout(self):  # type: () -> typing.Text
+        """Command stdout."""
+        return self.result.stdout_str
+
+    @property
+    def stderr(self):  # type: () -> typing.Text
+        """Command stderr."""
+        return self.result.stderr_str
 
 
 class CalledProcessError(ExecCalledProcessError):
@@ -74,9 +117,7 @@ class CalledProcessError(ExecCalledProcessError):
         :param result: execution result
         :type result: exec_result.ExecResult
         :param expected: expected return codes
-        :type expected: typing.Optional[
-            typing.List[typing.Union[int, proc_enums.ExitCodes]]
-        ]
+        :type expected: typing.Optional[typing.List[typing.Union[int, proc_enums.ExitCodes]]]
 
         .. versionchanged:: 1.1.1 - provide full result
         """
@@ -89,7 +130,7 @@ class CalledProcessError(ExecCalledProcessError):
             "\tSTDOUT:\n"
             "{result.stdout_brief}\n"
             "\tSTDERR:\n{result.stderr_brief}".format(
-                result=result,
+                result=self.result,
                 expected=self.expected
             )
         )
diff --git a/exec_helpers/exceptions.pyi b/exec_helpers/exceptions.pyi
index 4a22763..1f4558b 100644
--- a/exec_helpers/exceptions.pyi
+++ b/exec_helpers/exceptions.pyi
@@ -7,13 +7,32 @@ class ExecHelperError(Exception):
 class DeserializeValueError(ExecHelperError, ValueError):
     ...
 
-class ExecHelperTimeoutError(ExecHelperError):
-    ...
 
 class ExecCalledProcessError(ExecHelperError):
     ...
 
 
+class ExecHelperTimeoutError(ExecCalledProcessError):
+
+    result: exec_result.ExecResult = ...
+    timeout: int = ...
+
+    def __init__(
+        self,
+        result: exec_result.ExecResult,
+        timeout: typing.Union[int, float],
+    ) -> None: ...
+
+    @property
+    def cmd(self) -> str: ...
+
+    @property
+    def stdout(self) -> typing.Text: ...
+
+    @property
+    def stderr(self) -> typing.Text: ...
+
+
 class CalledProcessError(ExecCalledProcessError):
 
     result: exec_result.ExecResult = ...
diff --git a/exec_helpers/subprocess_runner.py b/exec_helpers/subprocess_runner.py
index 8b79cc2..b36a05c 100644
--- a/exec_helpers/subprocess_runner.py
+++ b/exec_helpers/subprocess_runner.py
@@ -269,7 +269,7 @@ class Subprocess(six.with_metaclass(SingletonMeta, _api.ExecHelper)):
             timeout=timeout
         )
         logger.debug(wait_err_msg)
-        raise exceptions.ExecHelperTimeoutError(wait_err_msg)
+        raise exceptions.ExecHelperTimeoutError(result=result, timeout=timeout)
 
     def execute_async(
         self,


Test patch:
diff --git a/test/test_ssh_client.py b/test/test_ssh_client.py
index 9119773..f02a68f 100644
--- a/test/test_ssh_client.py
+++ b/test/test_ssh_client.py
@@ -1028,10 +1028,15 @@ class TestExecute(unittest.TestCase):
 
         logger.reset_mock()
 
-        with self.assertRaises(exec_helpers.ExecHelperTimeoutError):
+        with self.assertRaises(exec_helpers.ExecHelperTimeoutError) as cm:
             # noinspection PyTypeChecker
             ssh.execute(command=command, verbose=False, timeout=0.2)
 
+        self.assertEqual(cm.exception.timeout, 0.2)
+        self.assertEqual(cm.exception.cmd, command)
+        self.assertEqual(cm.exception.stdout, stdout_str)
+        self.assertEqual(cm.exception.stderr, stderr_str)
+
         execute_async.assert_called_once_with(command, verbose=False)
         chan.assert_has_calls((mock.call.status_event.is_set(), ))
 
diff --git a/test/test_subprocess_runner.py b/test/test_subprocess_runner.py
index 04555c7..18d508d 100644
--- a/test/test_subprocess_runner.py
+++ b/test/test_subprocess_runner.py
@@ -233,10 +233,15 @@ class TestSubprocessRunner(unittest.TestCase):
 
         # noinspection PyTypeChecker
 
-        with self.assertRaises(exec_helpers.ExecHelperTimeoutError):
+        with self.assertRaises(exec_helpers.ExecHelperTimeoutError) as cm:
             # noinspection PyTypeChecker
             runner.execute(command, timeout=0.2)
 
+        self.assertEqual(cm.exception.timeout, 0.2)
+        self.assertEqual(cm.exception.cmd, command)
+        self.assertEqual(cm.exception.stdout, exp_result.stdout_str)
+        self.assertEqual(cm.exception.stderr, exp_result.stderr_str)
+
         popen.assert_has_calls((
             mock.call(
                 args=[command],