From 26f0fb7918d7c0009cb027e437004a2ee752b28d Mon Sep 17 00:00:00 2001 From: "d.kovalenko" Date: Fri, 9 Jan 2026 18:01:25 +0300 Subject: [PATCH 1/3] ExecUtilException is updated (RO props) Changes: - message, command, exit_code, out, error are RO-props now - new: description - message returns dynamically created text - description replaces message - __str__ returns message - new: __repl__ method Tests are added, too. --- src/exceptions.py | 128 +++++++++++++++--- tests/test_os_ops_remote.py | 3 +- .../test_set001__constructor.py | 126 +++++++++++++++++ 3 files changed, 238 insertions(+), 19 deletions(-) create mode 100644 tests/units/exceptions/ExecUtilException/test_set001__constructor.py diff --git a/src/exceptions.py b/src/exceptions.py index 8b5c7c4..81606d4 100644 --- a/src/exceptions.py +++ b/src/exceptions.py @@ -3,38 +3,130 @@ from testgres.common.exceptions import TestgresException from testgres.common.exceptions import InvalidOperationException import six +import typing + + +T_CMD = typing.Union[str, list] +T_OUT_DATA = typing.Union[str, bytes] +T_ERR_DATA = typing.Union[str, bytes] class ExecUtilException(TestgresException): - def __init__(self, message=None, command=None, exit_code=0, out=None, error=None): - super(ExecUtilException, self).__init__(message) + _description: typing.Optional[str] + _command: typing.Optional[T_CMD] + _exit_code: typing.Optional[int] + _out: typing.Optional[T_OUT_DATA] + _error: typing.Optional[T_ERR_DATA] + + def __init__( + self, + message: typing.Optional[str] = None, + command: typing.Optional[T_CMD] = None, + exit_code: typing.Optional[int] = None, + out: typing.Optional[T_OUT_DATA] = None, + error: typing.Optional[T_ERR_DATA] = None, + ): + assert message is None or type(message) == str # noqa: E721 + assert command is None or type(command) in [str, list] # noqa: E721 + assert exit_code is None or type(exit_code) == int # noqa: E721 + assert out is None or type(out) in [str, bytes] # noqa: E721 + assert error is None or type(error) in [str, bytes] # noqa: E721 - self.message = message - self.command = command - self.exit_code = exit_code - self.out = out - self.error = error + super().__init__(message) - def __str__(self): + self._description = message + self._command = command + self._exit_code = exit_code + self._out = out + self._error = error + + @property + def message(self) -> str: msg = [] - if self.message: - msg.append(self.message) + if self._description: + msg.append(self._description) - if self.command: - command_s = ' '.join(self.command) if isinstance(self.command, list) else self.command + if self._command: + command_s = ' '.join(self._command) if isinstance(self._command, list) else self._command msg.append(u'Command: {}'.format(command_s)) - if self.exit_code: - msg.append(u'Exit code: {}'.format(self.exit_code)) + if self._exit_code: + msg.append(u'Exit code: {}'.format(self._exit_code)) - if self.error: - msg.append(u'---- Error:\n{}'.format(self.error)) + if self._error: + msg.append(u'---- Error:\n{}'.format(self._error)) - if self.out: + if self._out: msg.append(u'---- Out:\n{}'.format(self.out)) - return self.convert_and_join(msg) + r = self.convert_and_join(msg) + assert type(r) == str # noqa: E721 + return r + + @property + def description(self) -> typing.Optional[str]: + assert self._description is None or type(self._description) == str # noqa: E721 + return self._description + + @property + def command(self) -> typing.Optional[T_CMD]: + assert self._command is None or type(self._command) in [str, list] # noqa: E721 + return self._command + + @property + def exit_code(self) -> typing.Optional[int]: + assert self._exit_code is None or type(self._exit_code) == int # noqa: E721 + return self._exit_code + + @property + def out(self) -> typing.Optional[T_OUT_DATA]: + assert self._out is None or type(self._out) in [str, bytes] # noqa: E721 + return self._out + + @property + def error(self) -> typing.Optional[T_ERR_DATA]: + assert self._error is None or type(self._error) in [str, bytes] # noqa: E721 + return self._error + + def __str__(self) -> str: + # + # To backward compatibility. Remove this when testgres.common v1.0.0 start using. + # + r = self.message + assert type(r) == str # noqa: E721 + return r + + def __repr__(self) -> str: + args = [] + + if self._description is not None: + args.append(("message", self._description)) + + if self._command is not None: + args.append(("command", self._command)) + + if self._exit_code is not None: + args.append(("exit_code", self._exit_code)) + + if self._out is not None: + args.append(("out", self._out)) + + if self._error is not None: + args.append(("error", self._error)) + + assert type(self) == ExecUtilException # noqa: E721 + assert __class__ == ExecUtilException # noqa: E721 + + result = "{}(".format(__class__.__name__) + sep = "" + for a in args: + if a[1] is not None: + result += sep + a[0] + "=" + repr(a[1]) + sep = ", " + continue + result += ")" + return result @staticmethod def convert_and_join(msg_list): diff --git a/tests/test_os_ops_remote.py b/tests/test_os_ops_remote.py index 6d59cf2..a89c08e 100755 --- a/tests/test_os_ops_remote.py +++ b/tests/test_os_ops_remote.py @@ -33,7 +33,8 @@ def test_rmdirs__try_to_delete_file(self, os_ops: OsOperations): assert os.path.exists(path) assert type(x.value) == ExecUtilException # noqa: E721 - assert x.value.message == "Utility exited with non-zero code (20). Error: `cannot remove '" + path + "': it is not a directory`" + assert x.value.description == "Utility exited with non-zero code (20). Error: `cannot remove '" + path + "': it is not a directory`" + assert x.value.message.startswith(x.value.description) assert type(x.value.error) == str # noqa: E721 assert x.value.error.strip() == "cannot remove '" + path + "': it is not a directory" assert type(x.value.exit_code) == int # noqa: E721 diff --git a/tests/units/exceptions/ExecUtilException/test_set001__constructor.py b/tests/units/exceptions/ExecUtilException/test_set001__constructor.py new file mode 100644 index 0000000..c19f744 --- /dev/null +++ b/tests/units/exceptions/ExecUtilException/test_set001__constructor.py @@ -0,0 +1,126 @@ +from src.exceptions import ExecUtilException + + +class TestSet001_Constructor: + def test_001__default(self): + e = ExecUtilException() + assert e.message == "" + assert e.description is None + assert e.command is None + assert e.exit_code is None + assert e.out is None + assert e.error is None + assert str(e) == "" + assert repr(e) == "ExecUtilException()" + return + + def test_002__description(self): + e = ExecUtilException("operation description") + assert e.message == "operation description" + assert e.description == "operation description" + assert e.command is None + assert e.exit_code is None + assert e.out is None + assert e.error is None + assert str(e) == "operation description" + assert repr(e) == "ExecUtilException(message='operation description')" + return + + def test_003__commandList(self): + e = ExecUtilException(command=["ls", "."]) + assert e.message == "Command: ls ." + assert e.description is None + assert e.command == ["ls", "."] + assert e.exit_code is None + assert e.out is None + assert e.error is None + assert str(e) == "Command: ls ." + assert repr(e) == "ExecUtilException(command=['ls', '.'])" + return + + def test_004__commandStr(self): + e = ExecUtilException(command="ls /home") + assert e.message == "Command: ls /home" + assert e.description is None + assert e.command == "ls /home" + assert e.exit_code is None + assert e.out is None + assert e.error is None + assert str(e) == "Command: ls /home" + assert repr(e) == "ExecUtilException(command='ls /home')" + return + + def test_005__exit_code(self): + e = ExecUtilException(exit_code=123) + assert e.message == "Exit code: 123" + assert e.description is None + assert e.command is None + assert e.exit_code == 123 + assert e.out is None + assert e.error is None + assert str(e) == "Exit code: 123" + assert repr(e) == "ExecUtilException(exit_code=123)" + return + + def test_006__outBytes(self): + e = ExecUtilException(out=b'abcdefg\n123456') + assert e.message == "---- Out:\nb'abcdefg\\n123456'" + assert e.description is None + assert e.command is None + assert e.exit_code is None + assert e.out == b'abcdefg\n123456' + assert e.error is None + assert str(e) == "---- Out:\nb'abcdefg\\n123456'" + assert repr(e) == "ExecUtilException(out=b'abcdefg\\n123456')" + return + + def test_007__outStr(self): + e = ExecUtilException(out='abcdefg\n123456') + assert e.message == "---- Out:\nabcdefg\n123456" + assert e.description is None + assert e.command is None + assert e.exit_code is None + assert e.out == 'abcdefg\n123456' + assert e.error is None + assert str(e) == "---- Out:\nabcdefg\n123456" + assert repr(e) == "ExecUtilException(out='abcdefg\\n123456')" + return + + def test_008__errorBytes(self): + e = ExecUtilException(error=b'abcdefg\n123456') + assert e.message == "---- Error:\nb'abcdefg\\n123456'" + assert e.description is None + assert e.command is None + assert e.exit_code is None + assert e.out is None + assert e.error == b'abcdefg\n123456' + assert str(e) == "---- Error:\nb'abcdefg\\n123456'" + assert repr(e) == "ExecUtilException(error=b'abcdefg\\n123456')" + return + + def test_009__errorStr(self): + e = ExecUtilException(error='abcdefg\n123456') + assert e.message == "---- Error:\nabcdefg\n123456" + assert e.description is None + assert e.command is None + assert e.exit_code is None + assert e.out is None + assert e.error == 'abcdefg\n123456' + assert str(e) == "---- Error:\nabcdefg\n123456" + assert repr(e) == "ExecUtilException(error='abcdefg\\n123456')" + return + + def test_010__all(self): + e = ExecUtilException('descr', ['rm', 'me'], -1, 'out\n123456', b'error\n321') + + expected_msg = "descr\nCommand: rm me\nExit code: -1\n---- Error:\nb'error\\n321'\n---- Out:\nout\n123456" + + assert e.message == expected_msg + assert e.description == "descr" + assert e.command == ['rm', 'me'] + assert e.exit_code == -1 + assert e.out == 'out\n123456' + assert e.error == b'error\n321' + assert str(e) == expected_msg + assert repr(e) == "ExecUtilException(message='descr', command=['rm', 'me'], exit_code=-1, out='out\\n123456', error=b'error\\n321')" + return From b3519ad72f7d1444c12fe2930309b74d6ca720b1 Mon Sep 17 00:00:00 2001 From: "d.kovalenko" Date: Fri, 9 Jan 2026 18:07:48 +0300 Subject: [PATCH 2/3] flake8 --- src/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/exceptions.py b/src/exceptions.py index 81606d4..e994d19 100644 --- a/src/exceptions.py +++ b/src/exceptions.py @@ -117,7 +117,7 @@ def __repr__(self) -> str: assert type(self) == ExecUtilException # noqa: E721 assert __class__ == ExecUtilException # noqa: E721 - + result = "{}(".format(__class__.__name__) sep = "" for a in args: From 005c36ca5989ca641a35678c5046672a8212df76 Mon Sep 17 00:00:00 2001 From: "d.kovalenko" Date: Fri, 9 Jan 2026 21:08:40 +0300 Subject: [PATCH 3/3] testgres.common v1.0.0 [master] - ExecUtilException::__str__ is inherited from TestgresException - TestgresException provides RO-property 'source'. It has None value --- pyproject.toml | 2 +- src/exceptions.py | 8 -------- tests/requirements.txt | 2 +- .../ExecUtilException/test_set001__constructor.py | 10 ++++++++++ 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 06e99fe..360c67e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ classifiers = [ dependencies = [ "psutil", "six>=1.9.0", - "testgres.common>=0.0.3,<1.0.0", + "testgres.common @ git+https://github.com/postgrespro/testgres.common.git@1.0.0", ] [project.urls] diff --git a/src/exceptions.py b/src/exceptions.py index e994d19..42f08ad 100644 --- a/src/exceptions.py +++ b/src/exceptions.py @@ -89,14 +89,6 @@ def error(self) -> typing.Optional[T_ERR_DATA]: assert self._error is None or type(self._error) in [str, bytes] # noqa: E721 return self._error - def __str__(self) -> str: - # - # To backward compatibility. Remove this when testgres.common v1.0.0 start using. - # - r = self.message - assert type(r) == str # noqa: E721 - return r - def __repr__(self) -> str: args = [] diff --git a/tests/requirements.txt b/tests/requirements.txt index 5219a1f..1cc7e69 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -2,7 +2,7 @@ pytest pytest-xdist psutil six -testgres.common>=0.0.2,<1.0.0 +git+https://github.com/postgrespro/testgres.common.git@1.0.0 black flake8 flake8-pyproject diff --git a/tests/units/exceptions/ExecUtilException/test_set001__constructor.py b/tests/units/exceptions/ExecUtilException/test_set001__constructor.py index c19f744..62886ff 100644 --- a/tests/units/exceptions/ExecUtilException/test_set001__constructor.py +++ b/tests/units/exceptions/ExecUtilException/test_set001__constructor.py @@ -4,6 +4,7 @@ class TestSet001_Constructor: def test_001__default(self): e = ExecUtilException() + assert e.source is None assert e.message == "" assert e.description is None assert e.command is None @@ -16,6 +17,7 @@ def test_001__default(self): def test_002__description(self): e = ExecUtilException("operation description") + assert e.source is None assert e.message == "operation description" assert e.description == "operation description" assert e.command is None @@ -28,6 +30,7 @@ def test_002__description(self): def test_003__commandList(self): e = ExecUtilException(command=["ls", "."]) + assert e.source is None assert e.message == "Command: ls ." assert e.description is None assert e.command == ["ls", "."] @@ -40,6 +43,7 @@ def test_003__commandList(self): def test_004__commandStr(self): e = ExecUtilException(command="ls /home") + assert e.source is None assert e.message == "Command: ls /home" assert e.description is None assert e.command == "ls /home" @@ -52,6 +56,7 @@ def test_004__commandStr(self): def test_005__exit_code(self): e = ExecUtilException(exit_code=123) + assert e.source is None assert e.message == "Exit code: 123" assert e.description is None assert e.command is None @@ -64,6 +69,7 @@ def test_005__exit_code(self): def test_006__outBytes(self): e = ExecUtilException(out=b'abcdefg\n123456') + assert e.source is None assert e.message == "---- Out:\nb'abcdefg\\n123456'" assert e.description is None assert e.command is None @@ -76,6 +82,7 @@ def test_006__outBytes(self): def test_007__outStr(self): e = ExecUtilException(out='abcdefg\n123456') + assert e.source is None assert e.message == "---- Out:\nabcdefg\n123456" assert e.description is None assert e.command is None @@ -88,6 +95,7 @@ def test_007__outStr(self): def test_008__errorBytes(self): e = ExecUtilException(error=b'abcdefg\n123456') + assert e.source is None assert e.message == "---- Error:\nb'abcdefg\\n123456'" assert e.description is None assert e.command is None @@ -100,6 +108,7 @@ def test_008__errorBytes(self): def test_009__errorStr(self): e = ExecUtilException(error='abcdefg\n123456') + assert e.source is None assert e.message == "---- Error:\nabcdefg\n123456" assert e.description is None assert e.command is None @@ -115,6 +124,7 @@ def test_010__all(self): expected_msg = "descr\nCommand: rm me\nExit code: -1\n---- Error:\nb'error\\n321'\n---- Out:\nout\n123456" + assert e.source is None assert e.message == expected_msg assert e.description == "descr" assert e.command == ['rm', 'me']