Refactor exceptions message formatting on top of format method

This updates also some doctstrings related to exception handling

Change-Id: If9903e66f187b837e238cb5fcdd8cc8e819f22b6
This commit is contained in:
Federico Ressi 2019-04-16 12:03:20 +02:00
parent c1424ed012
commit 4631795818
9 changed files with 179 additions and 32 deletions

View File

@ -39,7 +39,7 @@ class CreateUtil(base.TobikoCMD):
class NoSuchTemplateError(tobiko.TobikoException):
message = "No such template. Existing templates:\n%(templates)s"
message = "no such template; existing templates are: {templates}"
def main():

View File

@ -19,7 +19,20 @@ import testtools
FailureException = testtools.TestCase.failureException
def fail(reason, *args, **kwargs):
def fail(msg, *args, **kwargs):
"""Fail immediately current test case execution, with the given message.
Unconditionally raises a tobiko.FailureException as in below equivalent
code:
raise FailureException(msg.format(*args, **kwargs))
:param msg: string message used to create FailureException
:param *args: positional arguments to be passed to str.format method
:param **kwargs: key-word arguments to be passed to str.format method
:returns: It never returns
:raises FailureException:
"""
if args or kwargs:
reason = reason.format(*args, **kwargs)
raise FailureException(reason)
msg = msg.format(*args, **kwargs)
raise FailureException(msg)

View File

@ -17,22 +17,40 @@ from __future__ import absolute_import
class TobikoException(Exception):
"""Base Tobiko Exception.
To use this class, inherit from it and define a 'message' property.
To use this class, inherit from it and define attribute 'message' string.
If **properties parameters is given, then it will format message string
using properties as key-word arguments.
Example:
class MyException(TobikoException):
message = "This exception occurred because of {reason}"
try:
raise MyException(reason="something went wrong")
except MyException as ex:
# It should print:
# This exception occurred because of something went wrong
print(ex)
# It should print:
# something went wrong
print(ex.reason)
:attribute message: the message to be printed out.
"""
message = None
def __init__(self, **properties):
super(TobikoException, self).__init__()
self._properties = properties
message = self.message # pylint: disable=exception-message-attribute
if message:
if properties:
message = message % properties
self._message = message or "unknown reason"
def __str__(self):
return self._message
def __init__(self, message=None, **properties):
# pylint: disable=exception-message-attribute
message = message or self.message or "unknown reason"
if properties:
message = message.format(**properties)
self.message = message
self._properties = properties or {}
super(TobikoException, self).__init__(message)
def __getattr__(self, name):
try:
@ -40,3 +58,8 @@ class TobikoException(Exception):
except KeyError:
msg = ("{!r} object has no attribute {!r}").format(self, name)
raise AttributeError(msg)
def __repr__(self):
return "{class_name}({message!r})".format(
class_name=type(self).__name__,
message=self.message)

View File

@ -301,16 +301,16 @@ def check_stack_status(stack, expected):
class InvalidHeatStackOutputKey(tobiko.TobikoException):
message = "Output key %(key)r not found in stack %(name)r."
message = "output key {key!r} not found in stack {name!r}"
class HeatStackNotFound(tobiko.TobikoException):
message = "Stack %(name)r not found"
message = "stack {name!r} not found"
class InvalidHeatStackStatus(tobiko.TobikoException):
message = ("Stack %(name)r status %(observed)r not in %(expected)r.\n"
"%(status_reason)s")
message = ("stack {name!r} status {observed!r} not in {expected!r}\n"
"{status_reason!s}")
class HeatStackCreationFailed(InvalidHeatStackStatus):

View File

@ -74,7 +74,7 @@ def keystone_credentials(api_version=None, auth_url=None,
class InvalidKeystoneCredentials(tobiko.TobikoException):
message = "Invalid Keystone credentials (%(credentials)r): %(reason)s."
message = "invalid Keystone credentials; {reason!s}; {credentials!r}"
class EnvironKeystoneCredentialsFixture(tobiko.SharedFixture):

View File

@ -24,7 +24,7 @@ class PingException(tobiko.TobikoException):
class PingError(PingException):
"""Base ping error"""
message = ("%(details)s")
message = "{details!s}"
class LocalPingError(PingError):
@ -41,13 +41,13 @@ class UnknowHostError(PingError):
class BadAddressPingError(PingError):
"""Raised when passing wrong address to ping command"""
message = ("Bad address: %(address)r")
message = "bad address ({address!r})"
class PingFailed(PingError, tobiko.FailureException):
"""Raised when ping timeout expires before reaching expected message count
"""
message = ("Timeout of %(timeout)d seconds expired after counting only "
"%(count)d out of expected %(expected_count)d ICMP messages of "
"type %(message_type)r.")
message = ("timeout of {timeout} seconds expired after counting only "
"{count} out of expected {expected_count} ICMP messages of "
"type {message_type!r}")

View File

@ -25,14 +25,14 @@ class ShellError(tobiko.TobikoException):
class ShellCommandFailed(ShellError):
"""Raised when shell command exited with non-zero status
"""
message = ("Command %(command)r failed, exit status: %(exit_status)d, "
"stderr:\n%(stderr)s\n"
"stdout:\n%(stdout)s")
message = ("command {command!r} failed (exit status is {exit_status}); "
"stderr:\n{stderr!s}\n"
"stdout:\n{stdout!s}")
class ShellTimeoutExpired(ShellError):
"""Raised when shell command timeouts and has been killed before exiting
"""
message = ("Command '%(command)s' timed out: %(timeout)d, "
"stderr:\n%(stderr)s\n"
"stdout:\n%(stdout)s")
message = ("command {command!r} timed out after {timeout!s} seconds; "
"stderr:\n{stderr!s}\n"
"stdout:\n{stdout!s}")

View File

@ -0,0 +1,38 @@
# Copyright 2019 Red Hat
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import absolute_import
import tobiko
from tobiko.tests import unit
class TestFail(unit.TobikoUnitTest):
def test_fail(self):
self._test_fail('some_reason')
def test_fail_with_args(self):
self._test_fail('some {1!r} {0!s}', 'reason', 'strange')
def test_fail_with_kwargs(self):
self._test_fail('some {b!r} {a!s}', a='reason', b='strange')
def _test_fail(self, reason, *args, **kwargs):
ex = self.assertRaises(
tobiko.FailureException, tobiko.fail, reason, *args, **kwargs)
if args or kwargs:
expected_reason = reason.format(*args, **kwargs)
else:
expected_reason = reason
self.assertEqual(expected_reason, str(ex))

View File

@ -0,0 +1,73 @@
# Copyright 2019 Red Hat
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import absolute_import
import tobiko
from tobiko.tests import unit
class SomeException(tobiko.TobikoException):
message = "message formatted with {b} and {a}"
class TestException(unit.TobikoUnitTest):
def test_init(self, message=None, **properties):
ex = SomeException(message, **properties)
expected_str = message or SomeException.message
if properties:
expected_str = expected_str.format(**properties)
self.assertEqual(expected_str, str(ex))
for k, v in properties.items():
self.assertEqual(v, getattr(ex, k))
def test_init_with_properties(self):
self.test_init(a='a', b='b')
def test_init_with_message(self):
self.test_init('{other} message', other='another')
def test_raise(self):
def _raise_my_exception(**kwargs):
raise SomeException(**kwargs)
ex = self.assertRaises(SomeException, _raise_my_exception, b=1, a=2)
self.assertEqual('message formatted with 1 and 2', str(ex))
def test_repr(self):
ex = SomeException('some reason')
self.assertEqual("SomeException('some reason')", repr(ex))
def test_get_invalid_property(self):
ex = SomeException(a='1', b='2')
def _get_invalid_property():
return ex.invalid_attribute_name
ex = self.assertRaises(AttributeError, _get_invalid_property)
self.assertEqual(
"SomeException('message formatted with 2 and 1') object has no "
"attribute 'invalid_attribute_name'", str(ex))
def test_docstring_example(self):
class MyException(tobiko.TobikoException):
message = "This exception occurred because of {reason}"
try:
raise MyException(reason="something went wrong")
except MyException as ex:
self.assertEqual(
'This exception occurred because of something went wrong',
str(ex))
self.assertEqual('something went wrong', ex.reason)