From d31b2d266aa044f3cd011fb28ed2ffaa017cd9bd Mon Sep 17 00:00:00 2001 From: ChaDongWun <66347959+lovewave02@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:28:41 +0900 Subject: [PATCH] Improve exception string formatting --- doc/exceptions.rst | 8 ++++++++ pssh/exceptions.py | 40 ++++++++++++++++++++++++++-------------- tests/test_exceptions.py | 23 +++++++++++++++++++++++ 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/doc/exceptions.rst b/doc/exceptions.rst index 21ffe5d3..b81ba60b 100644 --- a/doc/exceptions.rst +++ b/doc/exceptions.rst @@ -1,6 +1,14 @@ Exceptions ========== +Exceptions initialized with a printf-style message and its values have a +human-readable string representation while retaining the original values in +``args``. For example:: + + error = AuthenticationError("Authentication failed for %s", "host.example.com") + print(error) + # Authentication failed for host.example.com + .. automodule:: pssh.exceptions :members: :undoc-members: diff --git a/pssh/exceptions.py b/pssh/exceptions.py index b0a3fa5e..394c27dd 100644 --- a/pssh/exceptions.py +++ b/pssh/exceptions.py @@ -19,7 +19,19 @@ """Exceptions raised by parallel-ssh classes.""" -class NoIPv6AddressFoundError(Exception): +class _PSSHError(Exception): + """Base class for exceptions with printf-style message arguments.""" + + def __str__(self): + if len(self.args) < 2 or not isinstance(self.args[0], str): + return super().__str__() + try: + return self.args[0] % self.args[1:] + except (KeyError, TypeError, ValueError): + return super().__str__() + + +class NoIPv6AddressFoundError(_PSSHError): """Raised when an IPV6 only address was requested but none are available for a host. @@ -29,7 +41,7 @@ class NoIPv6AddressFoundError(Exception): """ -class UnknownHostError(Exception): +class UnknownHostError(_PSSHError): """Raised when a host is unknown (dns failure)""" pass @@ -39,7 +51,7 @@ class UnknownHostError(Exception): ConnectionErrorException = ConnectionError -class AuthenticationError(Exception): +class AuthenticationError(_PSSHError): """Raised on authentication error (user/password/ssh key error)""" pass @@ -47,7 +59,7 @@ class AuthenticationError(Exception): AuthenticationException = AuthenticationError -class SSHError(Exception): +class SSHError(_PSSHError): """Raised on error authenticating with SSH server""" pass @@ -55,7 +67,7 @@ class SSHError(Exception): SSHException = SSHError -class HostArgumentError(Exception): +class HostArgumentError(_PSSHError): """Raised on errors with per-host arguments to parallel functions""" pass @@ -63,12 +75,12 @@ class HostArgumentError(Exception): HostArgumentException = HostArgumentError -class SessionError(Exception): +class SessionError(_PSSHError): """Raised on errors establishing SSH session""" pass -class SFTPError(Exception): +class SFTPError(_PSSHError): """Raised on SFTP errors""" pass @@ -78,29 +90,29 @@ class SFTPIOError(SFTPError): pass -class ProxyError(Exception): +class ProxyError(_PSSHError): """Raised on proxy errors""" -class Timeout(Exception): +class Timeout(_PSSHError): """Raised on timeout requested and reached""" -class SCPError(Exception): +class SCPError(_PSSHError): """Raised on errors copying file via SCP""" -class PKeyFileError(Exception): +class PKeyFileError(_PSSHError): """Raised on errors finding private key file""" -class ShellError(Exception): +class ShellError(_PSSHError): """Raised on errors running command on interactive shell""" -class HostConfigError(Exception): +class HostConfigError(_PSSHError): """Raised on invalid host configuration""" -class InvalidAPIUseError(Exception): +class InvalidAPIUseError(_PSSHError): """Raised on invalid use of library API""" diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index c0cedce9..7d577bdf 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -20,6 +20,7 @@ from pssh.exceptions import AuthenticationError, AuthenticationException, UnknownHostError, \ UnknownHostException, ConnectionError, ConnectionErrorException, SSHError, SSHException, \ HostArgumentError, HostArgumentException +from pssh.exceptions import SCPError, Timeout class ParallelSSHUtilsTest(unittest.TestCase): @@ -67,3 +68,25 @@ def test_errors(self): raise HostArgumentError except HostArgumentException: pass + + def test_formatted_error_string(self): + message = "Authentication error while connecting to %s:%s - %s - retries %s/%s" + cause = AuthenticationError("No authentication methods succeeded") + error = AuthenticationError( + message, "host.example.com", 22, cause, 3, 3) + + self.assertEqual( + str(error), + "Authentication error while connecting to host.example.com:22 - " + "No authentication methods succeeded - retries 3/3") + self.assertEqual( + error.args, + (message, "host.example.com", 22, cause, 3, 3)) + + def test_invalid_formatted_error_falls_back_to_default(self): + error = Timeout("Timeout after %s seconds: %s", 10) + + self.assertEqual(str(error), str(Exception(*error.args))) + + def test_single_argument_error_string_is_unchanged(self): + self.assertEqual(str(SCPError("copy failed")), "copy failed")