how to make a calculator 5

I want to practice using lists with the calculator project


preview

Here are the tests I have by the end of the chapter

  1import random
  2import src.calculator
  3import unittest
  4
  5
  6def a_random_number():
  7    return random.triangular(-1000.0, 1000.0)
  8
  9
 10class TestCalculator(unittest.TestCase):
 11
 12    random_first_number = a_random_number()
 13    random_second_number = a_random_number()
 14
 15    def test_addition(self):
 16        self.assertEqual(
 17            src.calculator.add(
 18                self.random_first_number,
 19                self.random_second_number
 20            ),
 21            self.random_first_number+self.random_second_number
 22        )
 23
 24    def test_subtraction(self):
 25        self.assertEqual(
 26            src.calculator.subtract(
 27                self.random_first_number,
 28                self.random_second_number
 29            ),
 30            self.random_first_number-self.random_second_number
 31        )
 32
 33    def test_multiplication(self):
 34        self.assertEqual(
 35            src.calculator.multiply(
 36                self.random_first_number,
 37                self.random_second_number
 38            ),
 39            self.random_first_number*self.random_second_number
 40        )
 41
 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?! cannot divide by 0. Try again...'
 55            )
 56
 57    def test_calculator_sends_message_when_input_is_not_a_number(self):
 58        error_message = 'Excuse me?! 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        a_list = [0, 1, 2, 3]
 94
 95        self.assertEqual(
 96            src.calculator.add(a_list, 0),
 97            error_message
 98        )
 99        self.assertEqual(
100            src.calculator.divide(a_list, 1),
101            error_message
102        )
103        self.assertEqual(
104            src.calculator.multiply(a_list, 2),
105            error_message
106        )
107        self.assertEqual(
108            src.calculator.subtract(a_list, 3),
109            error_message
110        )
111
112    def test_calculator_w_list_items(self):
113        a_list = [self.random_first_number, self.random_second_number]
114
115        self.assertEqual(
116            src.calculator.add(a_list[0], a_list[1]),
117            self.random_first_number+self.random_second_number
118        )
119        self.assertEqual(
120            src.calculator.divide(a_list[-2], a_list[-1]),
121            self.random_first_number/self.random_second_number
122        )
123        self.assertEqual(
124            src.calculator.multiply(a_list[1], a_list[-1]),
125            self.random_second_number*self.random_second_number
126        )
127        self.assertEqual(
128            src.calculator.subtract(a_list[-2], a_list[0]),
129            self.random_first_number-self.random_first_number
130        )
131        self.assertEqual(
132            src.calculator.add(*a_list),
133            self.random_first_number+self.random_second_number
134        )
135        self.assertEqual(
136            src.calculator.divide(*a_list),
137            self.random_first_number/self.random_second_number
138        )
139        self.assertEqual(
140            src.calculator.multiply(*a_list),
141            self.random_first_number*self.random_second_number
142        )
143        self.assertEqual(
144            src.calculator.subtract(*a_list),
145            self.random_first_number-self.random_second_number
146        )
147
148    def test_calculator_raises_type_error_when_given_more_than_two_inputs(self):
149        not_two_numbers = [0, 1, 2]
150
151        with self.assertRaises(TypeError):
152            src.calculator.add(*not_two_numbers)
153        with self.assertRaises(TypeError):
154            src.calculator.divide(*not_two_numbers)
155        with self.assertRaises(TypeError):
156            src.calculator.multiply(*not_two_numbers)
157        with self.assertRaises(TypeError):
158            src.calculator.subtract(*not_two_numbers)
159
160
161# Exceptions seen
162# AssertionError
163# NameError
164# AttributeError
165# TypeError
166# ZeroDivisionError
167# SyntaxError

open the project

  • I change directory to the calculator folder

    cd calculator
    

    the terminal shows I am in the calculator folder

    .../pumping_python/calculator
    
  • I activate the virtual environment

    source .venv/bin/activate
    

    Attention

    on Windows without Windows Subsystem for Linux use .venv/bin/activate.ps1 NOT source .venv/bin/activate

    .venv/scripts/activate.ps1
    

    the terminal shows

    (.venv) .../pumping_python/calculator
    
  • I use pytest-watch to run the tests

    pytest-watch
    

    the terminal shows

    rootdir: .../pumping_python/calculator
    collected 5 items
    
    tests/test_calculator.py ....                                        [100%]
    
    ============================ 5 passed in X.YZs =============================
    
  • I hold ctrl on the keyboard and click on tests/test_calculator.py to open it in the editor


test_calculator_sends_message_when_input_is_a_list

I want to see what happens when I send a list as input to the calculator program, will it send a message or raise TypeError?

RED: make it fail

I add a test to see what happens when I send a list as input

 88        self.assertEqual(
 89            src.calculator.subtract('1', '1'),
 90            error_message
 91        )
 92
 93    def test_calculator_sends_message_when_input_is_a_list(self):
 94        a_list = [0, 1, 2, 3]
 95
 96        self.assertEqual(
 97            src.calculator.add(a_list, 0),
 98            'BOOM!!!'
 99        )
100
101
102# Exceptions seen

the terminal shows AssertionError

AssertionError: 'Excuse me?! Numbers only! try again...' != 'BOOM!!!'

GREEN: make it pass

I change the expectation to match

96        self.assertEqual(
97            src.calculator.add(a_list, 0),
98            'Excuse me?! Numbers only! try again...'
99        )

the test passes

REFACTOR: make it better

  • I add another assertion for the next function

     96        self.assertEqual(
     97            src.calculator.add(a_list, 0),
     98            'Excuse me?! Numbers only! try again...'
     99        )
    100        self.assertEqual(
    101            src.calculator.divide(a_list, 1),
    102            'BAP!!!'
    103        )
    

    the terminal shows AssertionError

    AssertionError: 'Excuse me?! Numbers only! try again...' != 'BAP!!!'
    
  • I change the expectation

    101        self.assertEqual(
    102            src.calculator.divide(a_list, 1),
    103            'Excuse me?! Numbers only! try again...'
    104        )
    

    the test passes. Wait a minute! I just wrote the same thing twice, and I did it 8 times before in test_calculator_sends_message_when_input_is_not_a_number and 2 times in the only_takes_numbers function. Never again

  • I add a variable to remove the repetition

     93    def test_calculator_sends_message_when_input_is_a_list(self):
     94        a_list = [0, 1, 2, 3]
     95        error_message = 'Excuse me?! Numbers only! try again...'
     96
     97        self.assertEqual(
     98            src.calculator.add(a_list, 0),
     99            error_message
    100        )
    101        self.assertEqual(
    102            src.calculator.divide(a_list, 1),
    103            error_message
    104        )
    

    the test is still green


how to multiply a list

  • I add an assertion for the multiply function

    101        self.assertEqual(
    102            src.calculator.divide(a_list, 1),
    103            error_message
    104        )
    105        self.assertEqual(
    106            src.calculator.multiply(a_list, 2),
    107            'BOOM!!!'
    108        )
    

    the terminal shows AssertionError

    AssertionError: [0, 1, 2, 3, 0, 1, 2, 3] != 'BOOM!!!'
    

    I know how to multiply a list

  • I change the expectation of the test to the error message

    105        self.assertEqual(
    106            src.calculator.multiply(a_list, 2),
    107            error_message
    108        )
    

    the terminal shows AssertionError

    AssertionError: [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3] != 'Excuse me?! Numbers only! try again...'
    
  • I open calculator.py in the editor

  • I add an if statement to the only_takes_numbers function in calculator.py

     1def only_takes_numbers(function):
     2    def wrapper(first_input, second_input):
     3        error_message = 'Excuse me?! Numbers only! try again...'
     4
     5        if isinstance(first_input, str) or isinstance(second_input, str):
     6            return error_message
     7        if isinstance(first_input, list) or isinstance(second_input, list):
     8            return error_message
     9
    10        try:
    11            return function(first_input, second_input)
    12        except TypeError:
    13            return error_message
    14    return wrapper
    

    the test passes. The only_takes_numbers function looks ugly now, there has to be a better way

  • I add an assertion for the subtract function to test_calculator.py

    105        self.assertEqual(
    106            src.calculator.multiply(a_list, 2),
    107            error_message
    108        )
    109        self.assertEqual(
    110            src.calculator.subtract(a_list, 3),
    111            'BOOM!!!'
    112        )
    

    the terminal shows AssertionError

    AssertionError: 'Excuse me?! Numbers only! try again...' != 'BOOM!!!'
    
  • I change the expectation to match

    109        self.assertEqual(
    110            src.calculator.subtract(a_list, 3),
    111            error_message
    112        )
    

    the test passes

  • I remove the name of the test to move the new assertions to test_calculator_sends_message_when_input_is_not_a_number

     88        self.assertEqual(
     89            src.calculator.subtract('1', '1'),
     90            error_message
     91        )
     92
     93        two_numbers = [0, 1, 2, 3]
     94        error_message = 'Excuse me?! Numbers only! try again...'
     95
     96        self.assertEqual(
     97            src.calculator.add(a_list, 0),
     98            error_message
     99        )
    100        self.assertEqual(
    101            src.calculator.divide(a_list, 1),
    102            error_message
    103        )
    104        self.assertEqual(
    105            src.calculator.multiply(a_list, 2),
    106            error_message
    107        )
    108        self.assertEqual(
    109            src.calculator.subtract(a_list, 3),
    110            error_message
    111        )
    112
    113
    114# Exceptions seen
    

    the tests are still green

  • I remove the duplication of the error_message variable

    88        self.assertEqual(
    89            src.calculator.subtract('1', '1'),
    90            error_message
    91        )
    92
    93        a_list = [0, 1, 2, 3]
    94
    95        self.assertEqual(
    96            src.calculator.add(a_list, 0),
    97            error_message
    98        )
    

    still green. This test is long, there has to be a better way to test the calculator with inputs that are NOT numbers


how to test if something is an instance of more than one type

The isinstance function can take a tuple as the second input, which allows me to check if the first input is an instance of any of the objects in the tuple

  • I add a variable to the only_takes_numbers function in calculator.py

     1def only_takes_numbers(function):
     2    def wrapper(first_input, second_input):
     3        bad_types = (str, list)
     4        error_message = 'Excuse me?! Numbers only! try again...'
     5
     6        # if isinstance(first_input, str) or isinstance(second_input, str):
     7        #     return error_message
     8        # if isinstance(first_input, list) or isinstance(second_input, list):
     9        #     return error_message
    10
    11        if isinstance(first_input, bad_types) or isinstance(second_input, bad_types):
    12            return error_message
    13
    14        try:
    15            return function(first_input, second_input)
    16        except TypeError:
    17            return error_message
    18    return wrapper
    

    the tests are still green

  • I remove the comments

     1def only_takes_numbers(function):
     2    def wrapper(first_input, second_input):
     3        bad_types = (str, list)
     4        error_message = 'Excuse me?! Numbers only! try again...'
     5
     6        if isinstance(first_input, bad_types) or isinstance(second_input, bad_types):
     7            return error_message
     8
     9        try:
    10            return function(first_input, second_input)
    11        except TypeError:
    12            return error_message
    13    return wrapper
    

    still green

  • I can use Logical Negation (NOT) to make the if statement allow only the types of numbers (integers and floats) that I want the calculator to work with

     1def only_takes_numbers(function):
     2    def wrapper(first_input, second_input):
     3        good_types = (int, float)
     4        bad_types = (str, list)
     5        error_message = 'Excuse me?! Numbers only! try again...'
     6
     7        # if isinstance(first_input, bad_types) or isinstance(second_input, bad_types):
     8        #     return error_message
     9
    10        if not isinstance(first_input, good_types) or not isinstance(second_input, good_types):
    11            return error_message
    12        else:
    13            try:
    14                return function(first_input, second_input)
    15            except TypeError:
    16                return error_message
    17    return wrapper
    

    the tests are still green

  • I remove the comments and the bad_types variable

     1def only_takes_numbers(function):
     2    def wrapper(first_input, second_input):
     3        good_types = (int, float)
     4        error_message = 'Excuse me?! Numbers only! try again...'
     5
     6        if not isinstance(first_input, good_types) or not isinstance(second_input, good_types):
     7            return error_message
     8        else:
     9            try:
    10                return function(first_input, second_input)
    11            except TypeError:
    12                return error_message
    13    return wrapper
    

    the tests are still passing

  • not” happens twice in the if statement, I write the line in terms of it

    6        # if not isinstance(first_input, good_types) or not isinstance(second_input, good_types):
    7        if (not isinstance(first_input, good_types)) (not and) (not isinstance(second_input, good_types)):
    8            return error_message
    

    the terminal shows SyntaxError

    SyntaxError: invalid syntax
    
  • I add SyntaxError to the list of Exceptions in test_calculator.py

    113# Exceptions seen
    114# AssertionError
    115# NameError
    116# AttributeError
    117# TypeError
    118# ZeroDivisionError
    119# SyntaxError
    
  • I fix the if statement<if statement> in calculator.py

    6        # if not isinstance(first_input, good_types) or not isinstance(second_input, good_types):
    7        # if (not isinstance(first_input, good_types)) (not and) ((not isinstance(second_input, good_types))):
    8        if not (isinstance(first_input, good_types) and isinstance(second_input, good_types)):
    9            return error_message
    

    the test is green again

  • I remove the comments

     1def only_takes_numbers(function):
     2    def wrapper(first_input, second_input):
     3        good_types = (int, float)
     4        error_message = 'Excuse me?! Numbers only! try again...'
     5
     6        if not (isinstance(first_input, good_types) and isinstance(second_input, good_types)):
     7            return error_message
     8        else:
     9            try:
    10                return function(first_input, second_input)
    11            except TypeError:
    12                return error_message
    13    return wrapper
    

    all the tests are still passing. I wonder if there is a way to write this function with only one return statement for the error message


test_calculator_w_list_items

I can use a list to test the calculator functions as long as its items are numbers

RED: make it fail

I add a new test to use the index of the items in the list to test the calculator

107          self.assertEqual(
108              src.calculator.subtract(a_list, 3),
109              error_message
110          )
111
112      def test_calculator_w_list_items(self):
113          two_numbers = [self.random_first_number, self.random_second_number]
114
115          self.assertEqual(
116              src.calculator.add(two_numbers[0], two_numbers[1]),
117              self.random_first_number-self.random_second_number
118          )
119
120
121  # Exceptions seen

the terminal shows AssertionError

AssertionError: ABC.DEFGHIJKLMNOPQ != RST.UVWXYZABCDEFG

GREEN: make it pass

I change the expectation to the right calculation

115        self.assertEqual(
116            src.calculator.add(two_numbers[0], two_numbers[1]),
117            self.random_first_number+self.random_second_number
118        )

the test passes

REFACTOR: make it better

  • I add an assertion for the divide function

    115        self.assertEqual(
    116            src.calculator.add(two_numbers[0], two_numbers[1]),
    117            self.random_first_number+self.random_second_number
    118        )
    119        self.assertEqual(
    120            src.calculator.divide(two_numbers[-2], two_numbers[-1]),
    121            self.random_first_number*self.random_second_number
    122        )
    

    the terminal shows AssertionError

    AssertionError: D.EFGHIJKLMNOPQRST != UVWXY.ZABCDEFGHIJ
    
  • I change the calculation

    119      self.assertEqual(
    120          src.calculator.divide(two_numbers[-2], two_numbers[-1]),
    121          self.random_first_number/self.random_second_number
    122      )
    

    the test passes

  • I add another assertion

    119        self.assertEqual(
    120            src.calculator.divide(two_numbers[-2], two_numbers[-1]),
    121            self.random_first_number/self.random_second_number
    122        )
    123        self.assertEqual(
    124            src.calculator.multiply(two_numbers[1], two_numbers[-1]),
    125            self.random_first_number*self.random_second_number
    126        )
    

    the terminal shows AssertionError

    AssertionError: EFGHIJ.KLMNOPQRSTU != VWXYZ.ABCDEFGHIJKL
    
  • I change the expectation

    123        self.assertEqual(
    124            src.calculator.multiply(two_numbers[1], two_numbers[-1]),
    125            self.random_second_number*self.random_second_number
    126        )
    

    the test passes

  • I add an assertion for the subtract function

    123        self.assertEqual(
    124            src.calculator.multiply(two_numbers[1], two_numbers[-1]),
    125            self.random_second_number*self.random_second_number
    126        )
    127        self.assertEqual(
    128            src.calculator.subtract(two_numbers[-2], two_numbers[0]),
    129            self.random_first_number-self.random_second_number
    130        )
    

    the terminal shows AssertionError

    AssertionError: 0.0 != FGH.IJKLMNOPQRSTU
    
  • I change the expectation to match

    127        self.assertEqual(
    128            src.calculator.subtract(two_numbers[-2], two_numbers[0]),
    129            self.random_first_number-self.random_first_number
    130        )
    

    the test passes

  • Python allows me use a star expression like I did in test_functions_w_unknown_arguments. I add an assertion with it

    127        self.assertEqual(
    128            src.calculator.subtract(two_numbers[-2], two_numbers[0]),
    129            self.random_first_number-self.random_first_number
    130        )
    131        self.assertEqual(
    132            src.calculator.add(*two_numbers),
    133            self.random_first_number-self.random_second_number
    134        )
    

    the terminal shows AssertionError

    AssertionError: GHI.JKLMNOPQRSTUVW != XYZ.ABCDEFGHIJKLMN
    
  • I change the expectation

    131        self.assertEqual(
    132            src.calculator.add(*two_numbers),
    133            self.random_first_number+self.random_second_number
    134        )
    

    the test passes

  • I add another assertion

    131        self.assertEqual(
    132            src.calculator.add(*two_numbers),
    133            self.random_first_number+self.random_second_number
    134        )
    135        self.assertEqual(
    136            src.calculator.divide(*two_numbers),
    137            self.random_first_number*self.random_second_number
    138        )
    

    the terminal shows AssertionError

    AssertionError: H.IJKLMNOPQRSTUVWX != YZABCD.EFGHIJKLMNO
    
  • I change the calculation

    135        self.assertEqual(
    136            src.calculator.divide(*two_numbers),
    137            self.random_first_number/self.random_second_number
    138        )
    

    the test passes

  • I add an assertion for the multiply function

    135        self.assertEqual(
    136            src.calculator.divide(*two_numbers),
    137            self.random_first_number/self.random_second_number
    138        )
    139        self.assertEqual(
    140            src.calculator.multiply(*two_numbers),
    141            self.random_first_number/self.random_second_number
    142        )
    

    the terminal shows AssertionError

    AssertionError: IJKLMN.OPQRSTUVWX != Y.ZABCDEFGHIJKLMNOP
    
  • I change the calculation

    139        self.assertEqual(
    140            src.calculator.multiply(*two_numbers),
    141            self.random_first_number*self.random_second_number
    142        )
    

    the test passes

  • I add the next assertion

    139        self.assertEqual(
    140            src.calculator.multiply(*two_numbers),
    141            self.random_first_number*self.random_second_number
    142        )
    143        self.assertEqual(
    144            src.calculator.subtract(*two_numbers),
    145            self.random_first_number+self.random_second_number
    146        )
    

    the terminal shows AssertionError

    AssertionError: JKL.MNOPQRSTUVWXYZ != ABC.DEFGHIJKLMNOP
    
  • I change the expectation

    143        self.assertEqual(
    144            src.calculator.subtract(*two_numbers),
    145            self.random_first_number-self.random_second_number
    146        )
    

    the test passes


test_calculator_raises_type_error_when_given_more_than_two_inputs

It is important to note that the star expression always gives the items from the list in order, and I cannot use a list that has more than 2 numbers with these calculator functions since they only take 2 inputs

RED: make it fail

I add a new test to show the problem when I have more than 2 inputs and use a star expression

143        self.assertEqual(
144            src.calculator.subtract(*a_list),
145            self.random_first_number-self.random_second_number
146        )
147
148    def test_calculator_raises_type_error_when_given_more_than_two_inputs(self):
149        not_two_numbers = [0, 1, 2]
150
151        src.calculator.add(*not_two_numbers)
152
153
154# Exceptions seen

the terminal shows TypeError

TypeError: only_takes_numbers.<locals>.wrapper() takes 2 positional arguments but 3 were given

GREEN: make it pass

I add the assertRaises method to handle the Exception

148    def test_calculator_raises_type_error_when_given_more_than_two_inputs(self):
149        not_two_numbers = [0, 1, 2]
150
151        with self.assertRaises(TypeError):
152            src.calculator.add(*not_two_numbers)

the test passes

REFACTOR: make it better

  • I add a failing line for division with the new list

    151        with self.assertRaises(TypeError):
    152            src.calculator.add(*not_two_numbers)
    153        src.calculator.divide(*not_two_numbers)
    

    the terminal shows TypeError

    TypeError: only_takes_numbers.<locals>.wrapper() takes 2 positional arguments but 3 were given
    
  • I add assertRaises

    151        with self.assertRaises(TypeError):
    152            src.calculator.add(*not_two_numbers)
    153        with self.assertRaises(TypeError):
    154            src.calculator.divide(*not_two_numbers)
    155
    156the test passes
    
  • I add a line for multiplication

    153        with self.assertRaises(TypeError):
    154            src.calculator.divide(*not_two_numbers)
    155        src.calculator.multiply(*not_two_numbers)
    

    the terminal shows TypeError

    TypeError: only_takes_numbers.<locals>.wrapper() takes 2 positional arguments but 3 were given
    
  • I add assertRaises

    153        with self.assertRaises(TypeError):
    154            src.calculator.divide(*not_two_numbers)
    155        with self.assertRaises(TypeError):
    156            src.calculator.multiply(*not_two_numbers)
    

    the test passes

  • I add the last line

    155        with self.assertRaises(TypeError):
    156            src.calculator.multiply(*not_two_numbers)
    157        src.calculator.subtract(*not_two_numbers)
    

    the terminal shows TypeError

    TypeError: only_takes_numbers.<locals>.wrapper() takes 2 positional arguments but 3 were given
    
  • I handle the Exception

    148    def test_calculator_raises_type_error_when_given_more_than_two_inputs(self):
    149        not_two_numbers = [0, 1, 2]
    150
    151        with self.assertRaises(TypeError):
    152            src.calculator.add(*not_two_numbers)
    153        with self.assertRaises(TypeError):
    154            src.calculator.divide(*not_two_numbers)
    155        with self.assertRaises(TypeError):
    156            src.calculator.multiply(*not_two_numbers)
    157        with self.assertRaises(TypeError):
    158            src.calculator.subtract(*not_two_numbers)
    

    the test passes


close the project

  • I close test_calculator.py and calculator.py in the editors

  • I click in the terminal and exit the tests with ctrl+c on the keyboard, the terminal shows

    (.venv) .../pumping_python/calculator
    
  • I deactivate the virtual environment

    deactivate
    

    the terminal goes back to the command line, (.venv) is no longer on the left side

    .../pumping_python/calculator
    
  • I change directory to the parent of calculator

    cd ..
    

    the terminal shows

    .../pumping_python
    

    I am back in the pumping_python directory


review

I added the following tests to the calculator program after testing lists


code from the chapter

Do you want to see all the CODE I typed in this chapter?


what is next?

you know

would you like to test list comprehensions? They are a quick way to make lists


rate pumping python

If this has been a 7 star experience for you, please leave a 5 star review. It helps other people get into the book too