how to make a calculator part 4

I want to practice using lists with the calculator project


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