I have been working with with exceptions where im trying to still improve on how to raise correctly. I have done something like this
main.py
from lib.utils import ExceptionCounter
class BarryException():
pass
candy = "hello"
color = "world"
try:
test = type(int("string"))
except Exception as err:
# My own counter for exception that sends to discord if allowed_count is 0 by calling discord.notify(msg)
ExceptionCounter().check(
exception=BarryException,
allowed_count=0,
msg={
'Title': 'Unexpected Error',
'Reason': str(err),
'Payload': str({"candy": candy, "color": color})
}
)
lib.utils
class ExceptionCounter:
"""
Counter to check if we get exceptions x times in a row.
"""
def __init__(self):
self.exception_count = {}
def check(self, exception, msg, allowed_count):
exception_name = exception.__name__
# the dict.get() method will return the value from a dict if exists, else it will return the value provided
self.exception_count[exception_name] = self.exception_count.get(exception_name, 0) + 1
if self.exception_count[exception_name] >= allowed_count:
Discord_notify(msg)
raise exception(msg)
def reset(self):
self.exception_count.clear()
However Someone from Code Review recommended me to do this instead:
Raise with contextual data in a custom exception type:
try:
...
except Exception as e:
raise BarryException('Unexpected error testing', candy=candy, color=color) from e
Don't call Discord from there; don't do exception counting there; don't form a discord message payload dictionary there - just raise. In an upper method, you can except and do the Discord notification.
ExceptionCounter should not know about Discord at all, and should be strictly constrained to exception count limits and re-raising.
My problem is that I dont quite understand the part just raise. In an upper method, you can except and do the Discord notification. - I would appreciate if someone could explain or even show me a example of what it actually means :(
Related
Note: This question is based on a previous question I asked but modified to be different according to this answer.
Using side_effect, I am trying to raise a 'URLError' exception when a mock is called but I get a DID NOT RAISE error that I do not understand.
I have a Query class with a class method make_request_and_get_response which can raise several exceptions. I am not catching the 'URLError' exception within the get_response_from_external_api method in main.py, so I should be able to expect to be raised and, subsequently, its exception mocked.
query.py
from urllib.request import urlopen
import contextlib
import urllib
class Query:
def __init__(self, a, b):
self.a = a
self.b = b
self.query = self.make_query()
def make_query(self):
# create query request using self.a and self.b
return query
def make_request_and_get_response(self): # <--- the 'dangerous' method that can raise exceptions
with contextlib.closing(urlopen(self.query)) as response:
return response.read().decode('utf-8')
main.py
from foo.query import *
def get_response_from_external_api(query):
try:
response = query.make_request_and_get_response()
except urllib.error.HTTPError as e:
print('Got a HTTPError: ', e)
except Exception:
print('Got a generic Exception!')
# handle this exception
if __name__ == "__main__":
query = Query('input A', 'input B')
result = get_response_from_external_api(query)
return result
Using pytest, I am trying to mock that 'dangerous' method (make_request_and_get_response) with a side effect for a specific exception. Then, I proceed with creating a mocked Query object to use when calling the make_request_and_get_response with the expectation that this last call give a 'URLError' exception.
test_main.py
import pytest
from unittest.mock import patch
from foo.query import Query
from foo.main import get_response_from_external_api
class TestExternalApiCall:
#patch('foo.query.Query')
def test_url_error(self, mockedQuery):
with patch('foo.query.Query.make_request_and_get_response', side_effect=Exception('URLError')):
with pytest.raises(Exception) as excinfo:
q= mockedQuery()
foo.main.get_response_from_external_api(q)
assert excinfo.value = 'URLError'
# assert excinfo.value.message == 'URLError' # this gives object has no attribute 'message'
The test above gives the following error:
> foo.main.get_response_from_external_api(q)
E Failed: DID NOT RAISE <class 'Exception'> id='72517784'>") == 'URLError'
The same error occurs even if I catch the 'URLError' exception and then re-raise it in get_response_from_external_api.
Can someone help me understand what I am missing in order to be able to raise the exception in a pytest?
Update according to #SimeonVisser's response:
If I modify main.py to remove the except Excpetion case:
def get_response_from_external_api(query):
try:
response = query.make_request_and_get_response()
except urllib.error.URLError as e:
print('Got a URLError: ', e)
except urllib.error.HTTPError as e:
print('Got a HTTPError: ', e)
then the test in test_main.py:
def test_url_error2(self):
mock_query = Mock()
mock_query.make_request_and_get_response.side_effect = Exception('URLError')
with pytest.raises(Exception) as excinfo:
get_response_from_external_api(mock_query)
assert str(excinfo.value) == 'URLError'
The test passes OK.
The issue is that get_response_from_external_api() already catches an Exception and doesn't raise it to anywhere outside that function. So when you mock the query and make make_request_and_get_response raise an exception, it won't be seen by pytest because get_response_from_external_api() already catches it.
If you change it to be the following then pytest has a chance of seeing it:
except Exception:
print('Got a generic Exception!')
# handle this exception
raise
The test case also doesn't appear to work as intended. It can be simplified to the following (it's a bit easier to create a mock query directly and pass that around):
import pytest
from unittest import mock
from foo.main import get_response_from_external_api
class TestExternalApiCall:
def test_url_error(self):
mock_query = mock.Mock()
mock_query.make_request_and_get_response.side_effect = Exception('URLError')
with pytest.raises(Exception) as excinfo:
get_response_from_external_api(mock_query)
assert str(excinfo.value) == 'URLError'
and then the test case passes (in addition to also making the above change with raise).
Response to question 1:
The difference is that you're mocking Query using the decorator but then you're mocking the class foo.query.Query inside the test case to raise that exception. But at no point does mockedQuery actually change to do anything differently for that method. So q is a regular instance of mock.Mock() without anything special.
You could change it to the following (similar to the above approach):
import pytest
from unittest.mock import patch
from foo.query import Query
from foo.main import get_response_from_external_api
class TestExternalApiCall:
#patch('foo.query.Query')
def test_url_error(self, mockedQuery):
with patch.object(mockedQuery, 'make_request_and_get_response', side_effect=Exception('URLError')):
with pytest.raises(Exception) as excinfo:
get_response_from_external_api(mockedQuery)
assert str(excinfo.value) == 'URLError'
Your with patch('foo.query.Query........ statement would work if anywhere in the code it then instantiated a Query instance from that location.
Response to question 2:
Hmm, I can't reproduce that - is the test case that you have locally the same as in the question? I had to modify it as foo.main.get_response_from_external_api(q) doesn't exist (foo isn't being imported) so I changed it to call get_response_from_external_api(q) and I keep getting:
> get_response_from_external_api(q)
E Failed: DID NOT RAISE <class 'Exception'>
Say I have a function that can produce a vast variety of errors.
I have a ValueError that I need to catch, a specific AttributeError, and then I also need to handle any other type of error.
try:
func()
except AttributeError as e:
if "specific error text" in str(e):
print("The specific AttributeError occurred")
else:
raise
except ValueError:
print("A value error occurred")
except Exception as e:
print("Another error occurred: {}".format(str(e)))
Problem: If func() bubbles an AttributeError that's not the specific one I'm looking for, in this case, it'll be re-raised and not handled how I want it to be handled (via the general Exception handler).
How do I force non-specific errors to be handled further down in the chain, without duplicating code from the Exception section into the AttributeError section?
As an option you can process AttributeError and ValueError in one try-except block and all other Exceptions on the top level like
try:
try:
func()
except AttributeError as e:
if "specific error text" in str(e):
print("The specific AttributeError occurred")
else:
raise
except ValueError:
print("A value error occurred")
except Exception as e:
print("Another error occurred: {}".format(str(e)))
this may look a bit ugly though, so we can extract inner try-except block in a separate function like
def func_with_expected_exceptions_handling():
try:
func()
except AttributeError as e:
if "specific error text" in str(e):
print("The specific AttributeError occurred")
else:
raise
except ValueError:
print("A value error occurred")
and after that
try:
func_with_expected_exceptions_handling()
except Exception as e:
print("Another error occurred: {}".format(str(e)))
this doesn't save us from an actual nested structure, but it may come in handy if this kind of func processing arises in other places.
BTW, I don't think checking for a specific error message in exception is a good idea, we need a little bit more context to see if it can be done easier.
EDIT
If I understood correctly your func looks like
def func(...):
getattr(COMMANDS, cmd.command).command(irc_c, msg, cmd)
and you want to handle error from getattr call.
I can see next options here:
Wrap getattr call in try-except and process AttributeError in-place
def func(...):
try:
commander = getattr(COMMANDS, cmd.command)
except AttributeError:
print('Command {} not found'.format(cmd.command))
else:
commander.command(irc_c, msg, cmd)
Wrap getattr call in try-except, re-raise a custom exception (or ValueError) and process it afterwards in OP try-except
class CommandNotFound(Exception): pass
def func(...):
try:
commander = getattr(COMMANDS, cmd.command)
except AttributeError:
raise CommandNotFound() # or we can use `ValueError` instead
else:
commander.command(irc_c, msg, cmd)
Use default parameter of getattr function and make some kind of logging there like
class DefaultCommand:
def command(self, irc_c, msg, cmd):
print("Command {} is not found".format(cmd.command))
and after that used like
getattr(COMMANDS, cmd.command, DefaultCommand()).command(irc_c, msg, cmd)
Basically you need to handle the specific error first. From more general to more specific, i.e Exception => AttributeError => YourError
>>> try:
... raise MyCustomAttrErr("Hey, this failed!")
... except MyCustomAttrErr as e:
... print(e)
... except AttributteError as e:
... print("Attribute error raised")
... except Exception as e:
... print("Base exception raised")
...
Hey, this failed!
Python handled the except blocks in order from top to bottom and stops in the first block that captures it.
I'm trying to create a class that does nothing but handle error. Use the classes methods when calling functions so that the methods from the error handling class stop the execution of the function if user input is bad.
class Error(Exception):
pass
class UserError(Error):
def handle_even(number):
try: number % 2 == 0
except: raise UserError.handle_even('error: odd number\n')
def handle_odd(number):
try: number % 2 != 0
except: raise UserError.handle_odd('error: even number\n')
def take_even(number):
print(number) #if user input is bad, this shouldn't execute
def take_odd(number):
print(number) #if user input is bad, this shouldn't execute
take_even(UserError.handle_even(5)) # I expect this to print 'error: odd number'
take_odd(UserError.handle_odd(4)) # I expect this to print 'error: even number'
So, the output I want is:
error: odd number
error: even number
However, the output I get instead is:
None
None
The try block won't generate an exception because an expression evaluation was true/false, also your odd/even checking logic was wrong.
class Error(Exception):
pass
class UserError(Error):
#classmethod
def handle_even(cls, number):
if number % 2 != 0:
raise Error('error: odd number\n')
return number
#classmethod
def handle_odd(cls, number):
if number % 2 == 0:
raise Error('error: even number\n')
return number
def take_even(number):
print(number) # if user input is bad, this won't execute
def take_odd(number):
print(number) # if user input is bad, this won't execute
# This will raise an 'Error' exception (error: odd number)
take_even(UserError.handle_even(5))
# This will raise an 'Error' exception (error: even number)
take_odd(UserError.handle_odd(4))
You could handle these specific Error exceptions like so:
try:
take_even(UserError.handle_even(5))
except Error as e:
print(e) # print 'error: odd number'
try:
take_odd(UserError.handle_odd(4))
except Error as e:
print(e) # print 'error: even number'
Edit:
Changing the methods as class methods per #NathanVÄ“rzemnieks recommendation.
class A:
def __init__(self, data):
self.data = data
def first():
# Some functionality
# raise Exception on some condition
return self
def second():
#Some functionality
a = A('test string').first().second()
In this case I want that if first() raises an error then the chaining breaks and that error is raised. Currently it just fails silently.
How can I fix this?
Probably the best you can do is localize the type of errors that are possible in the chain and then ladder the exception handlers:
class A:
def __init__(self, data):
self.data = data
def first(self):
# Some functionality
# raise Exception on some condition
if self.data=='A':
1/0
return self
def second(self):
next(0)
pass
#Some functionality
You would know that first is numerical, and second is an iterable function that might have a TypeError.
So try this:
try:
A('A').first().second()
except ZeroDivisionError as e:
print('Likely first:', e)
except TypeError as e:
print('Likely second:', e)
Would print Likely first: division by zero with A('A') and print Likely second: 'int' object is not an iterator if you use A('something_else'). You would use this construct if you wanted to stop the chain as executed.
You can also add raise to re-raise the error and get Python's diagnosis of when are where:
try:
A('B').first().second()
except ZeroDivisionError as e:
print('Likely first:', e)
raise
except TypeError as e:
print('Likely second:', e)
raise
Prints:
Likely second: 'int' object is not an iterator
Traceback (most recent call last):
File "Untitled 79.py", line 19, in <module>
A('B').first().second()
File "Untitled 79.py", line 14, in second
next(0)
TypeError: 'int' object is not an iterator
Or, better still, use the try and except in each method:
class A:
def __init__(self, data):
self.data = data
def first(self):
try:
1/0
except ZeroDivisionError as e:
print('DEFINITELY first:', e)
return self
def second(self):
try:
next(0)
except TypeError as e:
print('DEFINITELY second:', e)
return self
>>> A('').first().second()
DEFINITELY first: division by zero
DEFINITELY second: 'int' object is not an iterator
I don't think it could fail silently if its actually failing, as this would error out python. However, you might want something like this, maybe it will jog your thinking.
class A:
def __init__(self, data):
self.data = data
def first():
try:
# Some functionality
raise ERROR_YOU WANT_RAISE:
# raise Exception on some condition
except ERROR_TO_CATCH_FROM_RAISE:
# This is where you would handle the exception after its caught
# and break your chaining possibly. Set a flag for second()
# for example that handles the error.
return self
def second():
#Some functionality
I am looking at Face Detection, using Kairos API while working on this program with the following code
def Test():
image = cap.read()[1]
cv2.imwrite("opencv_frame.png",image)
recognized_faces = kairos_face.verify_face(file="filepath/opencv_frame.png", subject_id='David',gallery_name='Test')
print(recognized_faces)
if recognized_faces.get('images')[0].get('transaction').get('status') !='success':
print('No')
else:
print('Hello', recognized_faces.get('images')[0].get('transaction').get('subject_id'))
this works fine if i look straight at the camera, but if i turn my head it breaks with the following response.
kairos_face.exceptions.ServiceRequestError: {'Errors': [{'Message': 'no faces found in the image', 'ErrCode': 5002}]}
How can i handle the exception Error, and force the test function to keep running until a face is detected.
Can't you just catch the exception and try again?
def Test():
captured = False
while not captured:
try:
image = cap.read()[1]
cv2.imwrite("opencv_frame.png",image)
recognized_faces = kairos_face.verify_face(file="filepath/opencv_frame.png", subject_id='David',gallery_name='Test')
captured = True
except kairos_face.exceptions.ServiceRequestError:
pass # optionally wait
print(recognized_faces)
if recognized_faces.get('images')[0].get('transaction').get('status') !='success':
print('No')
else:
print('Hello', recognized_faces.get('images')[0].get('transaction').get('subject_id'))