how to make a calculator 4
I want to use TypeError with exception handlers to make sure that the calculator program only works with numbers, like a Calculator in the real world.
preview
These are the tests I have by the end of the chapter
open the project
I change directory to the
calculatorfoldercd calculatorthe terminal shows I am in the
calculatorfolder.../pumping_python/calculatorI use
pytest-watcherto run the testsuv run pytest-watcher . --nowthe terminal shows
rootdir: .../pumping_python/calculator configfile: pyproject.toml collected 4 items tests/test_calculator.py .... [100%] ======================== 4 passed in X.YZs =========================I hold ctrl on the keyboard, then click on
tests/test_calculator.pyto open it in the editor
test_calculator_raises_type_error_w_none
RED: make it fail
I add a new failing test to show that the calculator raises TypeError when one of the inputs is None, just like in test_type_error_w_objects_that_do_not_mix
42 def test_division(self):
43 try:
44 self.assertEqual(
45 src.calculator.divide(
46 self.random_first_number,
47 self.random_second_number
48 ),
49 self.random_first_number/self.random_second_number
50 )
51 except ZeroDivisionError:
52 self.assertEqual(
53 src.calculator.divide(self.random_first_number, 0),
54 'brmph?! I cannot divide by 0. Try again...'
55 )
56
57 def test_calculator_raises_type_error_w_none(self):
58 src.calculator.add(None, None)
59
60
61# Exceptions seen
TypeError: unsupported operand type(s) for +: 'NoneType' and 'NoneType'
GREEN: make it pass
I add assertRaises
57 def test_calculator_raises_type_error_w_none(self):
58 with self.assertRaises(TypeError):
59 src.calculator.add(None, None)
the test passes
REFACTOR: make it better
I add a failing line for division
57 def test_calculator_raises_type_error_w_none(self): 58 with self.assertRaises(TypeError): 59 src.calculator.add(None, None) 60 src.calculator.divide(None, None)TypeError: unsupported operand type(s) for /: 'NoneType' and 'NoneType'I add assertRaises
58 with self.assertRaises(TypeError): 59 src.calculator.add(self.random_first_number, None) 60 with self.assertRaises(TypeError): 61 src.calculator.divide(self.random_first_number, None)the test passes
I add another failing line, this time for multiplication
60 with self.assertRaises(TypeError): 61 src.calculator.divide(None, None) 62 src.calculator.multiply(None, None)TypeError: unsupported operand type(s) for *: 'NoneType' and 'NoneType'I add assertRaises
60 with self.assertRaises(TypeError): 61 src.calculator.divide(None, None) 62 with self.assertRaises(TypeError): 63 src.calculator.multiply(None, None)the test passes
I add another one for subtraction
62 with self.assertRaises(TypeError): 63 src.calculator.multiply(None, None) 64 src.calculator.subtract(None, None)TypeError: unsupported operand type(s) for -: 'NoneType' and 'NoneType'I add the assertRaises method
57 def test_calculator_raises_type_error_w_none(self): 58 with self.assertRaises(TypeError): 59 src.calculator.add(None, None) 60 with self.assertRaises(TypeError): 61 src.calculator.divide(None, None) 62 with self.assertRaises(TypeError): 63 src.calculator.multiply(None, None) 64 with self.assertRaises(TypeError): 65 src.calculator.subtract(None, None) 66 67 68# Exceptions seenthe test passes
The calculator raises TypeError when None is given as input.
What does it do if the input is a boolean, string, tuple, list, set or a dictionary?
test_calculator_raises_type_error_w_strings
RED: make it fail
I add a new test with an assertion from test_what_is_an_assertion, to test the add function with strings
64 with self.assertRaises(TypeError):
65 src.calculator.subtract(None, None)
66
67 def test_calculator_with_strings(self):
68 self.assertEqual(src.calculator.add('1', '1'), '2')
69
70
71# Exceptions seen
the terminal shows AssertionError
AssertionError: '11' != '2'
GREEN: make it pass
I change the expectation to match reality
68 self.assertEqual(src.calculator.add('1', '1'), '11')
the test passes
REFACTOR: make it better
I add an assertion for the divide function
67 def test_calculator_with_strings(self): 68 self.assertEqual(src.calculator.add('1', '1'), '11') 69 self.assertEqual(src.calculator.divide('1', '1'), '11')TypeError: unsupported operand type(s) for /: 'str' and 'str'I change assertEqual to assertRaises
68 def test_calculator_with_strings(self): 69 self.assertEqual(src.calculator.add('1', '1'), '11') 70 with self.assertRaises(TypeError): 71 src.calculator.divide('1', '1')the test passes
I try it with the multiply function
69 with self.assertRaises(TypeError): 70 src.calculator.divide('1', '1') 71 src.calculator.multiply('1', '1')TypeError: can't multiply sequence by non-int of type 'str'I add assertRaises
69 with self.assertRaises(TypeError): 70 src.calculator.divide('1', '1') 71 with self.assertRaises(TypeError): 72 src.calculator.multiply('1', '1')the test passes
I add an assertion for the subtract function
71 with self.assertRaises(TypeError): 72 src.calculator.multiply('1', '1') 73 src.calculator.subtract('1', '1')TypeError: unsupported operand type(s) for -: 'str' and 'str'I add assertRaises
67 def test_calculator_with_strings(self): 68 self.assertEqual(src.calculator.add('1', '1'), '11') 69 with self.assertRaises(TypeError): 70 src.calculator.divide('1', '1') 71 with self.assertRaises(TypeError): 72 src.calculator.multiply('1', '1') 73 with self.assertRaises(TypeError): 74 src.calculator.subtract('1', '1')the test passes
how to test if something is an instance of an object in a program
I want the add function to raise TypeError when it gets a string, the same way the other functions raise TypeError when one of the inputs is a string. I can use the isinstance function which is like the assertIsInstance method from when I tested None, it checks if one thing is an instance or child of a class
I change the assertEqual to assertRaises in test_calculator_with_strings
67 def test_calculator_with_strings(self): 68 with self.assertRaises(TypeError): 69 src.calculator.add('1', '1') 70 with self.assertRaises(TypeError): 71 src.calculator.divide('1', '1')the terminal shows AssertionError
AssertionError: TypeError not raisedthen I add an if statement to the add function in
calculator.py16def add(first_input, second_input): 17 if ( 18 isinstance(first_input, str) 19 or 20 isinstance(second_input, str) 21 ): 22 raise TypeError 23 else: 24 return first_input + second_inputthe test passes
Note
the isinstance function like the assertIsInstance method checks if the first input it is given is an instance (child) of the class it is given as the second input. It is part of Python’s Built-in Functions
the if statement
if isinstance(first_input, str) or isinstance(second_input, str):is True ifthe statement is only False if
first_inputis NOT a string andsecond_inputis NOT a string.
This is Logical Disjunction from the Truth Table, which only returns False, if the two inputs are False
I change the name of the test to say what it does
67 def test_calculator_raises_type_error_w_strings(self): 68 with self.assertRaises(TypeError): 69 src.calculator.add('1', '1') 70 with self.assertRaises(TypeError): 71 src.calculator.divide('1', '1') 72 with self.assertRaises(TypeError): 73 src.calculator.multiply('1', '1') 74 with self.assertRaises(TypeError): 75 src.calculator.subtract('1', '1') 76 77 78# Exceptions seen
test_calculator_sends_message_when_input_is_not_a_number
I want the calculator functions to send a message when it gets something that is not a number, not raise TypeError which causes the program to stop. I want the user to be able to try again with different input.
RED: make it fail
I change the assertRaises to assertEqual for the add function in test_calculator_raises_type_error_w_none
57 def test_calculator_raises_type_error_w_none(self):
58 self.assertEqual(
59 src.calculator.add(None, None),
60 'brmph?! Numbers only. Try again...'
61 )
62 with self.assertRaises(TypeError):
63 src.calculator.divide(None, None)
64 with self.assertRaises(TypeError):
65 src.calculator.multiply(None, None)
66 with self.assertRaises(TypeError):
67 src.calculator.subtract(None, None)
TypeError: unsupported operand type(s) for +: 'NoneType' and 'NoneType'
GREEN: make it pass
I add an exception handler to the else clause of the add function in calculator.py
16def add(first_input, second_input):
17 if (
18 isinstance(first_input, str)
19 or
20 isinstance(second_input, str)
21 ):
22 raise TypeError
23 else:
24 try:
25 return first_input + second_input
26 except TypeError:
27 return 'brmph?! Numbers only. Try again...'
the test passes
REFACTOR: make it better
I change the assertRaises to assertEqual for the divide function
57 def test_calculator_raises_type_error_w_none(self): 58 self.assertEqual( 59 src.calculator.add(None, None), 60 'brmph?! Numbers only. Try again...' 61 ) 62 self.assertEqual( 63 src.calculator.divide(None, None), 64 'brmph?! Numbers only. Try again...' 65 ) 66 with self.assertRaises(TypeError):TypeError: unsupported operand type(s) for /: 'NoneType' and 'NoneType'I add another except clause to the exception handler in the divide function in
calculator.py9def divide(first_input, second_input): 10 try: 11 return first_input / second_input 12 except ZeroDivisionError: 13 return 'brmph?! I cannot divide by 0. Try again...' 14 except TypeError: 15 return 'brmph?! Numbers only. Try again...'the terminal shows AssertionError
AssertionError: TypeError not raisedmy change made the assertion in test_calculator_raises_type_error_w_strings fail
I undo the change, then add an if statement
9def divide(first_input, second_input): 10 if first_input is None or second_input is None: 11 return 'brmph?! Numbers only. Try again...' 12 try: 13 return first_input / second_input 14 except ZeroDivisionError: 15 return 'brmph?! I cannot divide by 0. Try again...' 16 17 18def add(first_input, second_input):the test passes
I change the assertRaises to assertEqual for the multiply function in test_calculator_raises_type_error_w_none in
test_calculator.py62 self.assertEqual( 63 src.calculator.divide(None, None), 64 'brmph?! Numbers only. Try again...' 65 ) 66 self.assertEqual( 67 src.calculator.multiply(None, None), 68 'brmph?! Numbers only. Try again...' 69 ) 70 with self.assertRaises(TypeError): 71 src.calculator.subtract(None, None) 72 73 def test_calculator_raises_type_error_w_strings(self):TypeError: unsupported operand type(s) for *: 'NoneType' and 'NoneType'I add an if statement to the multiply function in
calculator.py5def multiply(first_input, second_input): 6 if first_input is None or second_input is None: 7 return 'brmph?! Numbers only. Try again...' 8 return first_input * second_input 9 10 11def divide(first_input, second_input):the test passes
I change the assertRaises for the subtract function to assertEqual in test_calculator_raises_type_error_w_none in
test_calculator.py66 self.assertEqual( 67 src.calculator.multiply(None, None), 68 'brmph?! Numbers only. Try again...' 69 ) 70 self.assertEqual( 71 src.calculator.subtract(None, None), 72 'brmph?! Numbers only. Try again...' 73 ) 74 75 def test_calculator_raises_type_error_w_strings(self):TypeError: unsupported operand type(s) for -: 'NoneType' and 'NoneType'I add an if statement to the subtract function in
calculator.py1def subtract(first_input, second_input): 2 if first_input is None or second_input is None: 3 return 'brmph?! Numbers only. Try again...' 4 return first_input - second_input 5 6 7def multiply(first_input, second_input):the test passes
I add a variable
57 def test_calculator_raises_type_error_w_none(self): 58 error_message = 'brmph?! Numbers only. Try again...' 59 60 self.assertEqual( 61 src.calculator.add(None, None), 62 'brmph?! Numbers only. Try again...' 63 )I use it to remove the repetition of the error message
57 def test_calculator_raises_type_error_w_none(self): 58 error_message = 'brmph?! Numbers only. Try again...' 59 60 self.assertEqual( 61 src.calculator.add(None, None), 62 error_message 63 ) 64 self.assertEqual( 65 src.calculator.divide(None, None), 66 error_message 67 ) 68 self.assertEqual( 69 src.calculator.multiply(None, None), 70 error_message 71 ) 72 self.assertEqual( 73 src.calculator.subtract(None, None), 74 error_message 75 ) 76 77 def test_calculator_raises_type_error_w_strings(self):the test is still green
what is a decorator function?
3 of the functions in the calculator program have the same if statement
if first_input is None or second_input is None:
return 'brmph?! Numbers only. Try again...'
How can I remove this repetition? The only difference between the 3 functions is in what they do with the inputs
return first_input - second_input
return first_input * second_input
return first_input / second_input
I can use a decorator/wrapper function to remove the repetition from those 3 functions. It is a function that takes another function as input.
I add a decorator function to
calculator.py1def input_is_not_none(function): 2 def wrapper(first_input, second_input): 3 if first_input is None or second_input is None: 4 return 'brmph?! Numbers only. Try again...' 5 return function(first_input, second_input) 6 return wrapper 7 8 9def subtract(first_input, second_input):The
input_is_not_nonefunctiontakes a given function as input
uses logical disjunction to check the inputs
if the first input is None or the second input is None it returns an error message
if the two inputs are both NOT None it returns the result of calling the given function with the inputs
I use it with the subtract function
9@input_is_not_none 10def subtract(first_input, second_input): 11 if first_input is None or second_input is None: 12 return 'brmph?! Numbers only. Try again...' 13 return first_input - second_inputthe tests are still green
I remove the if statement from the subtract function
9@input_is_not_none 10def subtract(first_input, second_input): 11 return first_input - second_input 12 13 14def multiply(first_input, second_input):still green
I use
input_is_not_nonewith the multiply function14@input_is_not_none 15def multiply(first_input, second_input): 16 if first_input is None or second_input is None: 17 return 'brmph?! Numbers only. Try again...' 18 return first_input * second_inputgreen
I remove the if statement from the multiply function
14@input_is_not_none 15def multiply(first_input, second_input): 16 return first_input * second_input 17 18 19def divide(first_input, second_input):still green
I wrap the divide function with
input_is_not_none19@input_is_not_none 20def divide(first_input, second_input): 21 if first_input is None or second_input is None: 22 return 'brmph?! Numbers only. Try again...' 23 try: 24 return first_input / second_input 25 except ZeroDivisionError: 26 return 'brmph?! I cannot divide by 0. Try again...'the tests are still green
I remove the if statement
19@input_is_not_none 20def divide(first_input, second_input): 21 try: 22 return first_input / second_input 23 except ZeroDivisionError: 24 return 'brmph?! I cannot divide by 0. Try again...' 25 26 27def add(first_input, second_input):still green
I try it with the add function
27@input_is_not_none 28def add(first_input, second_input): 29 if ( 30 isinstance(first_input, str) 31 or 32 isinstance(second_input, str) 33 ): 34 raise TypeError 35 else: 36 try: 37 return first_input + second_input 38 except TypeError: 39 return 'brmph?! Numbers only. Try again...'the tests are still green
I remove the exception handler from the add function
1def input_is_not_none(function): 2 def wrapper(first_input, second_input): 3 if first_input is None or second_input is None: 4 return 'brmph?! Numbers only. Try again...' 5 return function(first_input, second_input) 6 return wrapper 7 8 9@input_is_not_none 10def subtract(first_input, second_input): 11 return first_input - second_input 12 13 14@input_is_not_none 15def multiply(first_input, second_input): 16 return first_input * second_input 17 18 19@input_is_not_none 20def divide(first_input, second_input): 21 try: 22 return first_input / second_input 23 except ZeroDivisionError: 24 return 'brmph?! I cannot divide by 0. Try again...' 25 26 27@input_is_not_none 28def add(first_input, second_input): 29 if ( 30 isinstance(first_input, str) 31 or 32 isinstance(second_input, str) 33 ): 34 raise TypeError 35 else: 36 return first_input + second_inputstill green
to continue with the goal that the calculator functions send a message when they gets something that is not a number, I change assertRaises to assertEqual in test_calculator_raises_type_error_w_strings for the add function in
test_calculator.py77 def test_calculator_raises_type_error_w_strings(self): 78 self.assertEqual( 79 src.calculator.add('1', '1'), 80 'brmph?! Numbers only! Try again...' 81 ) 82 with self.assertRaises(TypeError):TypeErrorI could have added a message to the raise statement for that Exception
I change the raise statement to a return statement with a message in
calculator.py27@input_is_not_none 28def add(first_input, second_input): 29 if ( 30 isinstance(first_input, str) 31 or 32 isinstance(second_input, str) 33 ): 34 return 'brmph?! Numbers only. Try again...' 35 else: 36 return first_input + second_inputthe test passes
I change assertRaises to assertEqual for the divide function in
test_calculator.py77 def test_calculator_raises_type_error_w_strings(self): 78 self.assertEqual( 79 src.calculator.add('1', '1'), 80 'brmph?! Numbers only. Try again...' 81 ) 82 self.assertEqual( 83 src.calculator.divide('1', '1'), 84 'brmph?! Numbers only. Try again...' 85 ) 86 with self.assertRaises(TypeError):TypeError: unsupported operand type(s) for /: 'str' and 'str'I add another except clause to the divide function in
calculator.py19@input_is_not_none 20def divide(first_input, second_input): 21 try: 22 return first_input / second_input 23 except ZeroDivisionError: 24 return 'brmph?! I cannot divide by 0. Try again...' 25 except TypeError: 26 return 'brmph?! Numbers only. Try again...'the test passes
I change assertRaises to assertEqual for the multiply function in test_calculator_raises_type_error_w_strings in
test_calculator.py77 def test_calculator_raises_type_error_w_strings(self): 78 self.assertEqual( 79 src.calculator.add('1', '1'), 80 'brmph?! Numbers only. Try again...' 81 ) 82 self.assertEqual( 83 src.calculator.divide('1', '1'), 84 'brmph?! Numbers only. Try again...' 85 ) 86 self.assertEqual( 87 src.calculator.multiply('1', '1'), 88 'brmph?! Numbers only. Try again...' 89 ) 90 with self.assertRaises(TypeError): 91 src.calculator.subtract('1', '1')TypeError: can't multiply sequence by non-int of type 'str'I add an exception handler to the multiply function in
calculator.py14@input_is_not_none 15def multiply(first_input, second_input): 16 try: 17 return first_input * second_input 18 except TypeError: 19 return 'brmph?! Numbers only. Try again...'the test passes
I change the assertRaises to assertEqual for the subtract function in
test_calculator.py77 def test_calculator_raises_type_error_w_strings(self): 78 self.assertEqual( 79 src.calculator.add('1', '1'), 80 'brmph?! Numbers only. Try again...' 81 ) 82 self.assertEqual( 83 src.calculator.divide('1', '1'), 84 'brmph?! Numbers only. Try again...' 85 ) 86 self.assertEqual( 87 src.calculator.multiply('1', '1'), 88 'brmph?! Numbers only. Try again...' 89 ) 90 self.assertEqual( 91 src.calculator.subtract('1', '1'), 92 'brmph?! Numbers only. Try again...' 93 ) 94 95 96# Exceptions seenTypeError: unsupported operand type(s) for -: 'str' and 'str'I add an exception handler to the subtract function in
calculator.py9@input_is_not_none 10def subtract(first_input, second_input): 11 try: 12 return first_input - second_input 13 except TypeError: 14 return 'brmph?! Numbers only. Try again...'the test passes
3 of the functions in the calculator program have the same exception handler
try:
something
except TypeError:
return 'brmph?! Numbers only. Try again...'
the divide function is different because it has another except clause
except ZeroDivisionError:
return 'brmph?! I cannot divide by 0. Try again...'
I can use a decorator/wrapper function to remove the repetition of the exception handler from the functions
I add a new decorator function to
calculator.py1def type_error_handler(function): 2 def handler(first_input, second_input): 3 try: 4 return function(first_input, second_input) 5 except TypeError: 6 return 'brmph?! Numbers only. Try again...' 7 return handler 8 9 10def input_is_not_none(function):The
type_error_handlerfunctiontakes a given function as input
tries to return the result of calling the given function with the inputs
if the call to the given function with the inputs raises TypeError it returns a message
I use it to wrap the subtract function
18@type_error_handler 19@input_is_not_none 20def subtract(first_input, second_input): 21 try: 22 return first_input - second_input 23 except TypeError: 24 return 'brmph?! Numbers only. Try again...'the tests are still green
I remove the exception handler from the subtract function
18@type_error_handler 19@input_is_not_none 20def subtract(first_input, second_input): 21 return first_input - second_input 22 23 24@input_is_not_none 25def multiply(first_input, second_input):still green
I wrap the multiply function
24@type_error_handler 25@input_is_not_none 26def multiply(first_input, second_input): 27 try: 28 return first_input * second_input 29 except TypeError: 30 return 'brmph?! Numbers only. Try again...'green
I remove the exception handler from the multiply function
24@type_error_handler 25@input_is_not_none 26def multiply(first_input, second_input): 27 return first_input * second_input 28 29 30@input_is_not_none 31def divide(first_input, second_input):still green
I wrap the divide function
30@type_error_handler 31@input_is_not_none 32def divide(first_input, second_input): 33 try: 34 return first_input / second_input 35 except ZeroDivisionError: 36 return 'brmph?! I cannot divide by 0. Try again...' 37 except TypeError: 38 return 'brmph?! Numbers only. Try again...'the test is still green
I remove the second except clause
30@type_error_handler 31@input_is_not_none 32def divide(first_input, second_input): 33 try: 34 return first_input / second_input 35 except ZeroDivisionError: 36 return 'brmph?! I cannot divide by 0. Try again...' 37 38 39@input_is_not_none 40def add(first_input, second_input):still green
The two decorator functions both return the result of calling the given functions or an error message
return function(first_input, second_input) return 'brmph?! Numbers only. Try again...'I make a new decorator to do the work of
type_error_handlerandinput_is_not_none1def numbers_only(function): 2 def decorator(first_input, second_input): 3 error_message = 'brmph?! Numbers only. Try again...' 4 if first_input is None or second_input is None: 5 return error_message 6 else: 7 try: 8 return function(first_input, second_input) 9 except TypeError: 10 return error_message 11 return decorator 12 13 14def type_error_handler(function):The
numbers_onlyfunctiontakes a given function as input
makes a variable named
error_messagefor the error messageuses logical disjunction to check the inputs
if the first input is None or the second input is None it returns the error message
if the two inputs are both NOT None it tries to return the result of calling the given function with the inputs
if the call to the given function with the inputs raises TypeError it returns a message
I use
numbers_onlyto wrap the subtract function31@numbers_only 32@type_error_handler 33@input_is_not_none 34def subtract(first_input, second_input): 35 return first_input - second_inputthe tests are still green
I remove the other wrappers from the the subtract function
76@numbers_only 77def subtract(first_input, second_input): 78 return first_input - second_input 79 80 81@type_error_handler 82@input_is_not_none 83def multiply(first_input, second_input):still green
I wrap the multiply function
36@numbers_only 37@type_error_handler 38@input_is_not_none 39def multiply(first_input, second_input): 40 return first_input * second_inputgreen
I remove the other wrappers from the multiply function
44@numbers_only 45def multiply(first_input, second_input): 46 return first_input * second_input 47 48 49@type_error_handler 50@input_is_not_none 51def divide(first_input, second_input):still green
I wrap the divide function
41@numbers_only 42@type_error_handler 43@input_is_not_none 44def divide(first_input, second_input): 45 try: 46 return first_input / second_input 47 except ZeroDivisionError: 48 return 'brmph?! I cannot divide by 0. Try again...'the tests are still green
I remove the other wrappers from the divide function
41@numbers_only 42def divide(first_input, second_input): 43 try: 44 return first_input / second_input 45 except ZeroDivisionError: 46 return 'brmph?! I cannot divide by 0. Try again...' 47 48 49@type_error_handler 50@input_is_not_none 51def add(first_input, second_input):still green
I wrap the add function
49@numbers_only 50@type_error_handler 51@input_is_not_none 52def add(first_input, second_input): 53 if ( 54 isinstance(first_input, str) 55 or 56 isinstance(second_input, str) 57 ): 58 return 'brmph?! Numbers only. Try again...' 59 else: 60 return first_input + second_inputgreen
I remove the other wrappers from the add function
41@numbers_only 42def add(first_input, second_input): 43 if ( 44 isinstance(first_input, str) 45 or 46 isinstance(second_input, str) 47 ): 48 return 'brmph?! Numbers only. Try again...' 49 else: 50 return first_input + second_inputstill green
I remove
type_error_handlerandinput_is_not_nonebecause they are no longer used1def numbers_only(function): 2 def decorator(first_input, second_input): 3 error_message = 'brmph?! Numbers only. Try again...' 4 if first_input is None or second_input is None: 5 return error_message 6 else: 7 try: 8 return function(first_input, second_input) 9 except TypeError: 10 return error_message 11 return decorator 12 13 14@numbers_only 15def subtract(first_input, second_input):all tests are still green
I add a variable to test_calculator_raises_type_error_w_strings
77 def test_calculator_raises_type_error_w_strings(self): 78 error_message = 'brmph?! Numbers only. Try again...' 79 80 self.assertEqual( 81 src.calculator.add('1', '1'), 82 'brmph?! Numbers only. Try again...' 83 )I use the variable to remove the repetition of the error message
77 def test_calculator_raises_type_error_w_strings(self): 78 error_message = 'brmph?! Numbers only. Try again...' 79 80 self.assertEqual( 81 src.calculator.add('1', '1'), 82 error_message 83 ) 84 self.assertEqual( 85 src.calculator.divide('1', '1'), 86 error_message 87 ) 88 self.assertEqual( 89 src.calculator.multiply('1', '1'), 90 error_message 91 ) 92 self.assertEqual( 93 src.calculator.subtract('1', '1'), 94 error_message 95 ) 96 97 98# Exceptions seenstill green
I remove the name of test_calculator_raises_type_error_w_strings to make its assertions part of test_calculator_raises_type_error_w_none
68 self.assertEqual( 69 src.calculator.multiply(None, None), 70 error_message 71 ) 72 self.assertEqual( 73 src.calculator.subtract(None, None), 74 error_message 75 ) 76 77 error_message = 'brmph?! Numbers only. Try again...' 78 79 self.assertEqual( 80 src.calculator.add('1', '1'), 81 error_message 82 ) 83 self.assertEqual( 84 src.calculator.divide('1', '1'), 85 error_message 86 )I remove the repetition of the
error_messagevariable68 self.assertEqual( 69 src.calculator.multiply(None, None), 70 error_message 71 ) 72 self.assertEqual( 73 src.calculator.subtract(None, None), 74 error_message 75 ) 76 self.assertEqual( 77 src.calculator.add('1', '1'), 78 error_message 79 ) 80 self.assertEqual( 81 src.calculator.divide('1', '1'), 82 error_message 83 ) 84 self.assertEqual(I change the name from test_calculator_raises_type_error_w_none to
test_calculator_sends_message_when_input_is_not_a_numberto be clearer57 def test_calculator_sends_message_when_input_is_not_a_number(self): 58 error_message = 'brmph?! Numbers only. Try again...' 59 60 self.assertEqual( 61 src.calculator.add(None, None), 62 error_message 63 ) 64 self.assertEqual( 65 src.calculator.divide(None, None), 66 error_message 67 ) 68 self.assertEqual( 69 src.calculator.multiply(None, None), 70 error_message 71 ) 72 self.assertEqual( 73 src.calculator.subtract(None, None), 74 error_message 75 ) 76 self.assertEqual( 77 src.calculator.add('1', '1'), 78 error_message 79 ) 80 self.assertEqual( 81 src.calculator.divide('1', '1'), 82 error_message 83 ) 84 self.assertEqual( 85 src.calculator.multiply('1', '1'), 86 error_message 87 ) 88 self.assertEqual( 89 src.calculator.subtract('1', '1'), 90 error_message 91 ) 92 93 94# Exceptions seen 95# AssertionError 96# NameError 97# AttributeError 98# TypeErrorthe tests are still green. All these assertions look the same, they check that the calculator functions return an error message if they get input that is NOT a number
self.assertEqual( src.calculator.function(NOT_A_NUMBER, ALSO_NOT_A_NUMBER), error_message )there has to be a better way to test the calculator with inputs that are NOT numbers, especially since I want to test the other data types - booleans, tuples, lists and dictionaries. I do not think I am ready to write 4 assertions for each one.
I can make a function for the condition in the if statement in the add function in
calculator.py24@numbers_only 25def divide(first_input, second_input): 26 try: 27 return first_input / second_input 28 except ZeroDivisionError: 29 return 'brmph?! I cannot divide by 0. Try again...' 30 31 32def is_string(something): 33 return isinstance(something, str) 34 35 36@numbers_only 37def add(first_input, second_input):then use
is_stringin the add function36@numbers_only 37def add(first_input, second_input): 38 # if ( 39 # isinstance(first_input, str) 40 # or 41 # isinstance(second_input, str) 42 # ): 43 if is_string(first_input) or is_string(second_input): 44 return 'brmph?! Numbers only. Try again...' 45 else: 46 return first_input + second_inputthe test is still green. This removes the duplication of
strin the call to the isinstance functionisinstance(first_input, str) isinstance(second_input, str)it also adds 2 lines of code to remove 6 characters. WOW!
I undo the change
I can make a function for the if statement in the add function
24@numbers_only 25def divide(first_input, second_input): 26 try: 27 return first_input / second_input 28 except ZeroDivisionError: 29 return 'brmph?! I cannot divide by 0. Try again...' 30 31 32def one_input_is_a_string(first_input, second_input): 33 return ( 34 isinstance(first_input, str) 35 or 36 isinstance(second_input, str) 37 ) 38 39 40@numbers_only 41def add(first_input, second_input):then use
one_input_is_a_stringin the add function40@numbers_only 41def add(first_input, second_input): 42 # if ( 43 # isinstance(first_input, str) 44 # or 45 # isinstance(second_input, str) 46 # ): 47 if one_input_is_a_string(first_input, second_input): 48 return 'brmph?! Numbers only. Try again...' 49 else: 50 return first_input + second_inputthe test is still green. the
one_input_is_a_stringfunctionalso uses logical disjunction like the if statement in
numbers_onlymakes it easier to change the condition later without touching the add function
still adds 2 lines of code
enough experiments for now. I undo the change because I do not need it
1def numbers_only(function): 2 def decorator(first_input, second_input): 3 error_message = 'brmph?! Numbers only. Try again...' 4 if first_input is None or second_input is None: 5 return error_message 6 else: 7 try: 8 return function(first_input, second_input) 9 except TypeError: 10 return error_message 11 return decorator 12 13 14@numbers_only 15def subtract(first_input, second_input): 16 return first_input - second_input 17 18 19@numbers_only 20def multiply(first_input, second_input): 21 return first_input * second_input 22 23 24@numbers_only 25def divide(first_input, second_input): 26 try: 27 return first_input / second_input 28 except ZeroDivisionError: 29 return 'brmph?! I cannot divide by 0. Try again...' 30 31 32@numbers_only 33def add(first_input, second_input): 34 if ( 35 isinstance(first_input, str) 36 or 37 isinstance(second_input, str) 38 ): 39 return 'brmph?! Numbers only. Try again...' 40 else: 41 return first_input + second_inputand all the tests are still passing. The world is my oyster!
close the project
I close
test_calculator.pyandcalculator.pyin the editorI click in the terminal, then use q on the keyboard to leave the tests. The terminal goes back to the command line
I change directory to the parent of
calculatorcd ..the terminal shows
.../pumping_pythonI am back in the
pumping_pythondirectory
review
The calculator program can take 2 inputs and check if they are both numbers, then add, subtract, multiply or divide them
Even though the program says it only works with numbers, I did not add tests for tuples, lists, sets, and dictionaries, though they are touched in test_type_error_w_objects_that_do_not_mix, Do you want to add them or do we already have enough tests to know what would happen?
code from the chapter
what is next?
you know
rate pumping python
If this has been a 7 star experience for you, please CLICK HERE to leave a 5 star review of pumping python. It helps other people get into the book too