how to make a calculator 8

I can use the __getattribute__ method that comes with every Python object in the calculator tests


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 use pytest-watcher to run the tests

    uv run pytest-watcher . --now
    

    the terminal shows

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


test_calculator_w_getattribute


RED: make it fail


I add a new test

168    def test_calculator_functions(self):
169        for operation in self.arithmetic_tests:
170            with self.subTest(operation=operation):
171                self.assertEqual(
172                    self.arithmetic_tests[operation]['function'](
173                        self.random_first_number,
174                        self.random_second_number
175                    ),
176                    self.arithmetic_tests[operation]['expectation']
177                )
178
179    def test_calculator_w_getattribute(self):
180        self.assertEqual(
181            src.calculator.__getattribute__('BOOM!!!'),
182            src.calculator.add
183        )
184
185
186# Exceptions seen

the terminal shows AttributeError

AttributeError: module 'src.calculator' has no attribute 'BOOM!!!'
  • __getattribute__ is a method that takes a name and checks if the name is an attribute of the object

  • src.calculator.__getattribute__('BOOM!!!') checks if the calculator.py module in the src folder has anything named BOOM!!!


GREEN: make it pass


I change 'BOOM!!!' to 'add'

179      def test_calculator_w_getattribute(self):
180          self.assertEqual(
181              src.calculator.__getattribute__('add'),
182              src.calculator.add
183          )

the test passes because the add function is an attribute of src.calculator


REFACTOR: make it better


  • I add a call to the add function

    179    def test_calculator_w_getattribute(self):
    180        self.assertEqual(
    181            src.calculator.__getattribute__('add'),
    182            src.calculator.add(
    183                self.random_first_number,
    184                self.random_second_number
    185            )
    186        )
    

    the terminal shows AssertionError

    AssertionError: <function numbers_only.<locals>.decorator at 0xffffa1234bc0> != DEF.GHIJKLMNOPQRS
    

    I have to call the add function after __getattribute__ returns it

  • I add parentheses at the end of the line to make it a call

    179    def test_calculator_w_getattribute(self):
    180        self.assertEqual(
    181            src.calculator.__getattribute__('add')(
    182                self.random_first_number,
    183                self.random_second_number
    184            ),
    185            src.calculator.add(
    186                self.random_first_number,
    187                self.random_second_number
    188            )
    189        )
    

    the test passes

  • I add a variables and a dictionary

    179    def test_calculator_w_getattribute(self):
    180        x = self.random_first_number
    181        y = self.random_second_number
    182        calculator_tests = {'add': x+y}
    183
    184        self.assertEqual(
    
  • I use the dictionary to change the expectation of the assertion

    184        self.assertEqual(
    185            src.calculator.__getattribute__('add')(
    186                self.random_first_number,
    187                self.random_second_number
    188            ),
    189            # src.calculator.add(
    190            #     self.random_first_number,
    191            #     self.random_second_number
    192            # )
    193            calculator_tests['add']
    194        )
    

    the test is still green

  • I use the variables for self.random_first_number and self.random_second_number in the assertion

    184        self.assertEqual(
    185            src.calculator.__getattribute__('add')(
    186                # self.random_first_number,
    187                # self.random_second_number
    188                x, y
    189            ),
    190            # src.calculator.add(
    191            #     self.random_first_number,
    192            #     self.random_second_number
    193            # )
    194            calculator_tests['add']
    195        )
    

    still green

  • I remove the commented lines

    184    def test_calculator_w_getattribute(self):
    185        x = self.random_first_number
    186        y = self.random_second_number
    187        calculator_tests = {'add': x+y}
    188
    189        self.assertEqual(
    190            src.calculator.__getattribute__('add')(x, y),
    191            calculator_tests['add']
    192        )
    

    green


  • I add subtraction to the calculator_tests dictionary

    179    def test_calculator_w_getattribute(self):
    180        x = self.random_first_number
    181        y = self.random_second_number
    182        calculator_tests = {
    183            'add': x+y,
    184            'subtract': x-y,
    185        }
    
  • then I add an assertion

    187        self.assertEqual(
    188            src.calculator.__getattribute__('add')(x, y),
    189            calculator_tests['add']
    190        )
    191        self.assertEqual(
    192            src.calculator.__getattribute__('subtract')(x, y),
    193            calculator_tests['add']
    194        )
    

    the terminal shows AssertionError

    AssertionError: OPQ.RSTUVWXYZABCDE != FGH.IJKLMNOPQRSTUV
    
  • I change the expectation

    191        self.assertEqual(
    192            src.calculator.__getattribute__('subtract')(x, y),
    193            calculator_tests['subtract']
    194        )
    

    the test passes


  • I add multiplication to the calculator_tests dictionary

    179    def test_calculator_w_getattribute(self):
    180        x = self.random_first_number
    181        y = self.random_second_number
    182        calculator_tests = {
    183            'add': x+y,
    184            'subtract': x-y,
    185            'multiply': x*y,
    186        }
    
  • then I add an assertion

    192        self.assertEqual(
    193            src.calculator.__getattribute__('subtract')(x, y),
    194            calculator_tests['subtract']
    195        )
    196        self.assertEqual(
    197            src.calculator.__getattribute__('multiply')(x, y),
    198            calculator_tests['subtract']
    199        )
    

    the terminal shows AssertionError

    AssertionError: WXYZA.BCDEFGHIJKLMNO != PQ.RSTUVWXYZABCDE
    
  • I change the expectation

    196        self.assertEqual(
    197            src.calculator.__getattribute__('multiply')(x, y),
    198            calculator_tests['multiply']
    199        )
    

    the test passes


  • I add division to the calculator_tests dictionary

    179    def test_calculator_w_getattribute(self):
    180        x = self.random_first_number
    181        y = self.random_second_number
    182        calculator_tests = {
    183            'add': x+y,
    184            'subtract': x-y,
    185            'multiply': x*y,
    186            'divide': x/y,
    187        }
    
  • then I add an assertion

    197        self.assertEqual(
    198            src.calculator.__getattribute__('multiply')(x, y),
    199            calculator_tests['multiply']
    200        )
    201        self.assertEqual(
    202            src.calculator.__getattribute__('divide')(x, y),
    203            calculator_tests['multiply']
    204        )
    

    the terminal shows AssertionError

    AssertionError: F.GHIJKLMNOPQRST != UVWXY.ZABCDEFGH
    
  • I change the expectation

    201    def test_calculator_w_getattribute(self):
    202        x = self.random_first_number
    203        y = self.random_second_number
    204        calculator_tests = {
    205            'add': x+y,
    206            'subtract': x-y,
    207            'multiply': x*y,
    208            'divide': x/y,
    209        }
    210
    211        self.assertEqual(
    212            src.calculator.__getattribute__('add')(x, y),
    213            calculator_tests['add']
    214        )
    215        self.assertEqual(
    216            src.calculator.__getattribute__('subtract')(x, y),
    217            calculator_tests['subtract']
    218        )
    219        self.assertEqual(
    220            src.calculator.__getattribute__('multiply')(x, y),
    221            calculator_tests['multiply']
    222        )
    223        self.assertEqual(
    224            src.calculator.__getattribute__('divide')(x, y),
    225            calculator_tests['divide']
    226        )
    227
    228
    229# Exceptions seen
    

    the test passes


  • I add a for loop

    179    def test_calculator_w_getattribute(self):
    180        x = self.random_first_number
    181        y = self.random_second_number
    182        calculator_tests = {
    183            'add': x+y,
    184            'subtract': x-y,
    185            'multiply': x*y,
    186            'divide': x/y,
    187        }
    188
    189        for operation in calculator_tests:
    190            with self.subTest(operation=operation):
    191                self.assertEqual(
    192                    src.calculator.__getattribute__(operation)(
    193                        x, y
    194                    ),
    195                    'BOOM!!!'
    196                )
    197
    198        self.assertEqual(
    199            src.calculator.__getattribute__('add')(x, y),
    200            calculator_tests['add']
    201        )
    

    the terminal shows AssertionError

    SUBFAILED(operation='add') ...      - AssertionError: FGH.IJKLMNOPQRSTU != 'BOOM!!!'
    SUBFAILED(operation='subtract') ... - AssertionError: VWX.YZABCDEFGHIJKL != 'BOOM!!!'
    SUBFAILED(operation='multiply') ... - AssertionError: MNOP.QRSTUVWXYZAB != 'BOOM!!!'
    SUBFAILED(operation='divide') ...   - AssertionError: CD.EFGHIJKLMNOPQR != 'BOOM!!!'
    
  • I change the expectation

    191                self.assertEqual(
    192                    src.calculator.__getattribute__(operation)(
    193                        x, y
    194                    ),
    195                    calculator_tests[operation]
    196                )
    

    the test passes

  • I remove the other assertions

    179    def test_calculator_w_getattribute(self):
    180        x = self.random_first_number
    181        y = self.random_second_number
    182        calculator_tests = {
    183            'add': x+y,
    184            'subtract': x-y,
    185            'multiply': x*y,
    186            'divide': x/y,
    187        }
    188
    189        for operation in calculator_tests:
    190            with self.subTest(operation=operation):
    191                self.assertEqual(
    192                    src.calculator.__getattribute__(operation)(
    193                        x, y
    194                    ),
    195                    calculator_tests[operation]
    196                )
    197
    198
    199# Exceptions seen
    

    is this simpler than test_calculator_functions?


  • I add calculator_tests to the setUp method to make it a class attribute

    12    def setUp(self):
    13        self.random_first_number = a_random_number()
    14        self.random_second_number = a_random_number()
    15
    16        x = self.random_first_number
    17        y = self.random_second_number
    18
    19        try:
    20            division_result = x / y
    21        except ZeroDivisionError:
    22            division_result = 'brmph?! I cannot divide by 0. Try again...'
    23
    24        self.calculator_tests = {
    25            'add': x+y,
    26            'subtract': x-y,
    27            'multiply': x*y,
    28            'divide': division_result
    29        }
    30
    31        self.arithmetic_tests = {
    
  • I use the new class attribute in test_calculator_w_getattribute

    186    def test_calculator_w_getattribute(self):
    187        x = self.random_first_number
    188        y = self.random_second_number
    189        # calculator_tests = {
    190        #     'add': x+y,
    191        #     'subtract': x-y,
    192        #     'multiply': x*y,
    193        #     'divide': x/y,
    194        # }
    195        calculator_tests = self.calculator_tests
    196
    197        for operation in calculator_tests:
    

    the test is still green

  • I use self.calculator_tests in the for loop

    197        # for operation in calculator_tests:
    198        for operation in self.calculator_tests:
    199            with self.subTest(operation=operation):
    200                self.assertEqual(
    201                    src.calculator.__getattribute__(operation)(
    202                        x, y
    203                    ),
    204                    calculator_tests[operation]
    205                )
    

    still green

  • I use it in the expectation of the assertion

    200                self.assertEqual(
    201                    src.calculator.__getattribute__(operation)(
    202                        x, y
    203                    ),
    204                    # calculator_tests[operation]
    205                    self.calculator_tests[operation]
    206                )
    

    green

  • I use self.random_first_number for x and self.random_second_number for y in the assertion

    200                self.assertEqual(
    201                    src.calculator.__getattribute__(operation)(
    202                        # x, y
    203                        self.random_first_number,
    204                        self.random_second_number
    205                    ),
    206                    # calculator_tests[operation]
    207                    self.calculator_tests[operation]
    208                )
    

    still green

  • I remove the commented lines and the variables because they are no longer used

    186    def test_calculator_w_getattribute(self):
    187        for operation in self.calculator_tests:
    188            with self.subTest(operation=operation):
    189                self.assertEqual(
    190                    src.calculator.__getattribute__(operation)(
    191                        self.random_first_number,
    192                        self.random_second_number
    193                    ),
    194                    self.calculator_tests[operation]
    195                )
    196
    197
    198# Exceptions seen
    

    the test is still green


  • I remove test_calculator_functions because test_calculator_w_getattribute has the same tests

    153    def test_calculator_sends_message_when_input_is_not_a_number(self):
    154        for bad_input in (
    155            None,
    156            True, False,
    157            str(), 'text',
    158            tuple(), (0, 1, 2, 'n'),
    159            list(), [0, 1, 2, 'n'],
    160            set(), {0, 1, 2, 'n'},
    161            dict(), {'key': 'value'},
    162        ):
    163            for operation in self.arithmetic_tests:
    164                with self.subTest(
    165                    operation=operation,
    166                    bad_input=bad_input,
    167                ):
    168                    self.assertEqual(
    169                        self.arithmetic_tests[operation]['function'](
    170                            bad_input, a_random_number()
    171                        ),
    172                        'brmph?! Numbers only. Try again...'
    173                    )
    174
    175    def test_calculator_w_getattribute(self):
    176        for operation in self.calculator_tests:
    177            with self.subTest(operation=operation):
    178                self.assertEqual(
    179                    src.calculator.__getattribute__(operation)(
    180                        self.random_first_number,
    181                        self.random_second_number
    182                    ),
    183                    self.calculator_tests[operation]
    184                )
    185
    186
    187# Exceptions seen
    

    the tests are still green

  • I change the name of test_calculator_w_getattribute to test_calculator_functions

    175    def test_calculator_functions(self):
    176        for operation in self.calculator_tests:
    177            with self.subTest(operation=operation):
    178                self.assertEqual(
    179                    src.calculator.__getattribute__(operation)(
    180                        self.random_first_number,
    181                        self.random_second_number
    182                    ),
    183                    self.calculator_tests[operation]
    184                )
    185
    186
    187# Exceptions seen
    

  • I use self.calculator_tests in the for loop of test_calculator_sends_message_when_input_is_not_a_number

    153    def test_calculator_sends_message_when_input_is_not_a_number(self):
    154        for bad_input in (
    155            None,
    156            True, False,
    157            str(), 'text',
    158            tuple(), (0, 1, 2, 'n'),
    159            list(), [0, 1, 2, 'n'],
    160            set(), {0, 1, 2, 'n'},
    161            dict(), {'key': 'value'},
    162        ):
    163            # for operation in self.arithmetic_tests:
    164            for operation in self.calculator_tests:
    165                with self.subTest(
    166                    operation=operation,
    167                    bad_input=bad_input,
    168                ):
    

    the terminal shows KeyError for all 52 sub tests

  • I use the __getattribute__ method in the assertion

    169                    self.assertEqual(
    170                        # self.arithmetic_tests[operation]['function'](
    171                        src.calculator.__getattribute__(operation)(
    172                            bad_input, a_random_number()
    173                        ),
    174                        'brmph?! Numbers only. Try again...'
    175                    )
    

    the test passes

  • I remove the commented lines

    153    def test_calculator_sends_message_when_input_is_not_a_number(self):
    154        for bad_input in (
    155            None,
    156            True, False,
    157            str(), 'text',
    158            tuple(), (0, 1, 2, 'n'),
    159            list(), [0, 1, 2, 'n'],
    160            set(), {0, 1, 2, 'n'},
    161            dict(), {'key': 'value'},
    162        ):
    163            for operation in self.calculator_tests:
    164                with self.subTest(
    165                    operation=operation,
    166                    bad_input=bad_input,
    167                ):
    168                    self.assertEqual(
    169                        src.calculator.__getattribute__(operation)(
    170                            bad_input, a_random_number()
    171                        ),
    172                        'brmph?! Numbers only. Try again...'
    173                    )
    174
    175    def test_calculator_functions(self):
    

  • I use self.calculator_tests in the for loop of test_calculator_raises_type_error_when_given_more_than_two_inputs

    143    def test_calculator_raises_type_error_when_given_more_than_two_inputs(self):
    144        # for operation in self.arithmetic_tests:
    145        for operation in self.calculator_tests:
    146            with (
    147                self.subTest(operation=operation),
    148                self.assertRaises(TypeError),
    149            ):
    150                self.arithmetic_tests[operation]['function'](
    151                    [0, 1, 2]
    152                )
    153
    154    def test_calculator_sends_message_when_input_is_not_a_number(self):
    

    the terminal shows KeyError for all 4 sub tests

  • I use the __getattribute__ method in the assertion

    150                # self.arithmetic_tests[operation]['function'](
    151                src.calculator.__getattribute__(operation)(
    152                    [0, 1, 2]
    153                )
    

    the test passes

  • I remove the commented lines

    143    def test_calculator_raises_type_error_when_given_more_than_two_inputs(self):
    144        for operation in self.calculator_tests:
    145            with (
    146                self.subTest(operation=operation),
    147                self.assertRaises(TypeError),
    148            ):
    149                src.calculator.__getattribute__(operation)(
    150                    [0, 1, 2]
    151                )
    152
    153    def test_calculator_sends_message_when_input_is_not_a_number(self):
    

    the test is still green


  • I use self.calculator_tests in the for loop of test_calculator_w_dictionary_items

     99    def test_calculator_w_dictionary_items(self):
    100        two_numbers = {
    101            'first_input': self.random_first_number,
    102            'second_input': self.random_second_number,
    103        }
    104
    105        # for operation in self.arithmetic_tests:
    106        for operation in self.calculator_tests:
    107            with self.subTest(operation=operation):
    

    the terminal shows KeyError for the 4 tests

  • I use the __getattribute__ method in the assertion

    108                self.assertEqual(
    109                    # self.arithmetic_tests[operation]['function'](
    110                    src.calculator.__getattribute__(operation)(
    111                        **two_numbers
    112                    ),
    113                    self.arithmetic_tests[operation]['expectation']
    114                )
    

    the terminal shows KeyError for the 4 sub tests

  • I use self.calculator_tests in the expectation of the assertion

    108                self.assertEqual(
    109                    # self.arithmetic_tests[operation]['function'](
    110                    src.calculator.__getattribute__(operation)(
    111                        **two_numbers
    112                    ),
    113                    # self.arithmetic_tests[operation]['expectation']
    114                    self.calculator_tests[operation]
    115                )
    

    the test passes

  • I remove the commented lines

     99    def test_calculator_w_dictionary_items(self):
    100        two_numbers = {
    101            'first_input': self.random_first_number,
    102            'second_input': self.random_second_number,
    103        }
    104
    105        for operation in self.calculator_tests:
    106            with self.subTest(operation=operation):
    107                self.assertEqual(
    108                    src.calculator.__getattribute__(operation)(
    109                        **two_numbers
    110                    ),
    111                    self.calculator_tests[operation]
    112                )
    113
    114        self.assertEqual(
    115            src.calculator.add(
    116                two_numbers['first_input'],
    117                two_numbers['second_input']
    118            ),
    119            self.random_first_number+self.random_second_number
    120        )
    121        self.assertEqual(
    122            src.calculator.divide(
    123                two_numbers['first_input'],
    124                two_numbers['second_input']
    125            ),
    126            self.random_first_number/self.random_second_number
    127        )
    128        self.assertEqual(
    129            src.calculator.multiply(
    130                two_numbers['second_input'],
    131                two_numbers['second_input']
    132            ),
    133            self.random_second_number*self.random_second_number
    134        )
    135        self.assertEqual(
    136            src.calculator.subtract(
    137                two_numbers['first_input'],
    138                two_numbers['first_input']
    139            ),
    140            self.random_first_number-self.random_first_number
    141        )
    142
    143    def test_calculator_raises_type_error_when_given_more_than_two_inputs(self):
    

  • I use self.calculator_tests in the for loop of test_calculator_w_list_items

    50  def test_calculator_w_list_items(self):
    51      # two_numbers = [
    52      #     self.random_first_number,
    53      #     self.random_second_number
    54      # ]
    55      a_dictionary = {
    56          'x': self.random_first_number,
    57          'y': self.random_second_number
    58      }
    59      two_numbers = list(a_dictionary.values())
    60
    61      # for operation in self.arithmetic_tests:
    62      for operation in self.calculator_tests:
    63          with self.subTest(operation=operation):
    

    the terminal shows KeyError for the 4 sub tests

  • I use the __getattribute__ method in the assertion

    64                self.assertEqual(
    65                    # self.arithmetic_tests[operation]['function'](
    66                    src.calculator.__getattribute__(operation)(
    67                        *two_numbers
    68                    ),
    69                    self.arithmetic_tests[operation]['expectation']
    70                )
    

    the terminal shows KeyError for the sub tests

  • I use self.calculator_tests in the expectation of the assertion

    64                self.assertEqual(
    65                    # self.arithmetic_tests[operation]['function'](
    66                    src.calculator.__getattribute__(operation)(
    67                        *two_numbers
    68                    ),
    69                    # self.arithmetic_tests[operation]['expectation']
    70                    self.calculator_tests[operation]
    71                )
    

    the test passes

  • I remove the new commented lines

    50    def test_calculator_w_list_items(self):
    51        # two_numbers = [
    52        #     self.random_first_number,
    53        #     self.random_second_number
    54        # ]
    55        a_dictionary = {
    56            'x': self.random_first_number,
    57            'y': self.random_second_number
    58        }
    59        two_numbers = list(a_dictionary.values())
    60
    61        for operation in self.calculator_tests:
    62            with self.subTest(operation=operation):
    63                self.assertEqual(
    64                    src.calculator.__getattribute__(operation)(
    65                        *two_numbers
    66                    ),
    67                    self.calculator_tests[operation]
    68                )
    69
    70        self.assertEqual(
    71            src.calculator.add(
    72                two_numbers[0],
    73                two_numbers[1]
    74            ),
    75            self.random_first_number+self.random_second_number
    76        )
    77        self.assertEqual(
    78            src.calculator.divide(
    79                two_numbers[-2],
    80                two_numbers[-1]
    81            ),
    82            self.random_first_number/self.random_second_number
    83        )
    84        self.assertEqual(
    85            src.calculator.multiply(
    86                two_numbers[1],
    87                two_numbers[-1]
    88            ),
    89            self.random_second_number*self.random_second_number
    90        )
    91        self.assertEqual(
    92            src.calculator.subtract(
    93                two_numbers[-2],
    94                two_numbers[0]
    95            ),
    96            self.random_first_number-self.random_first_number
    97        )
    98
    99    def test_calculator_w_dictionary_items(self):
    

  • I comment out self.arithmetic_tests in the setUp method to see if there are any tests that still use it

    12    def setUp(self):
    13        self.random_first_number = a_random_number()
    14        self.random_second_number = a_random_number()
    15
    16        x = self.random_first_number
    17        y = self.random_second_number
    18
    19        try:
    20            division_result = x / y
    21        except ZeroDivisionError:
    22            division_result = 'brmph?! I cannot divide by 0. Try again...'
    23
    24        self.calculator_tests = {
    25            'add': x+y,
    26            'subtract': x-y,
    27            'multiply': x*y,
    28            'divide': division_result
    29        }
    30
    31        # self.arithmetic_tests = {
    32        #     'addition': {
    33        #         'function': src.calculator.add,
    34        #         'expectation': x+y,
    35        #     },
    36        #     'subtraction': {
    37        #         'function': src.calculator.subtract,
    38        #         'expectation': x-y,
    39        #     },
    40        #     'division': {
    41        #         'function': src.calculator.divide,
    42        #         'expectation': division_result,
    43        #     },
    44        #     'multiplication': {
    45        #         'function': src.calculator.multiply,
    46        #         'expectation': x*y,
    47        #     }
    48        # }
    49
    50    def test_calculator_w_list_items(self):
    

    the tests are still green

  • I remove the commented lines

    12    def setUp(self):
    13        self.random_first_number = a_random_number()
    14        self.random_second_number = a_random_number()
    15
    16        x = self.random_first_number
    17        y = self.random_second_number
    18
    19        try:
    20            division_result = x / y
    21        except ZeroDivisionError:
    22            division_result = 'brmph?! I cannot divide by 0. Try again...'
    23
    24        self.calculator_tests = {
    25            'add': x+y,
    26            'subtract': x-y,
    27            'multiply': x*y,
    28            'divide': division_result
    29        }
    30
    31    def test_calculator_w_list_items(self):
    

  • I make a function for the division result because I can

    10class TestCalculator(unittest.TestCase):
    11
    12    def get_division_result(x, y):
    13        try:
    14            return x / y
    15        except ZeroDivisionError:
    16            return 'brmph?! I cannot divide by 0. Try again...'
    17
    18    def setUp(self):
    
  • I use the new function in self.calculator_tests

    the terminal shows TypeError

    TypeError: TestCalculator.get_division_result() takes 2 positional arguments but 3 were given
    

    I forgot to make get_division_result a staticmethod since it does not do anything with the rest of the class

  • I wrap get_division_result with the staticmethod decorator

    10class TestCalculator(unittest.TestCase):
    11
    12    @staticmethod
    13    def get_division_result(x, y):
    14        try:
    15            return x / y
    16        except ZeroDivisionError:
    17            return 'brmph?! I cannot divide by 0. Try again...'
    18
    19    def setUp(self):
    

    the test passes

  • I remove the commented line and the exception handler from the setUp method

    19def setUp(self):
    20    self.random_first_number = a_random_number()
    21    self.random_second_number = a_random_number()
    22
    23    x = self.random_first_number
    24    y = self.random_second_number
    25
    26    self.calculator_tests = {
    27        'add': x+y,
    28        'subtract': x-y,
    29        'multiply': x*y,
    30        'divide': self.get_division_result(x, y)
    31    }
    32
    33def test_calculator_w_list_items(self):
    

all the tests are passing and I feel like magic


test calculator tests again

I want to write the solution that will make all the tests in test_calculator.py pass without looking at them


RED: make it fail


  • I close test_calculator.py in the editor

  • I open calculator.py from the src folder in the editor

  • then I delete everything in calculator.py, the terminal shows 3 passing tests and AttributeError for 70 failing tests, I start with the last one

    AttributeError: module 'src.calculator' has no attribute 'add'
    
  • I add the name to calculator.py

    1add
    

    the terminal shows NameError

    NameError: name 'add' is not defined
    

GREEN: make it pass


  • I point add to None to define it

    1add = None
    

    the terminal shows TypeError

    TypeError: 'NoneType' object is not callable
    
  • I change it to a function

    1def add():
    2    return None
    

    the terminal shows TypeError

    TypeError: add() takes 0 positional arguments but 2 were given
    
  • I add a name to the parentheses

    1def add(x):
    2    return None
    

    the terminal shows TypeError

    TypeError: add() takes 1 positional argument but 2 were given
    
  • I add a second name to the parentheses

    1def add(x, y):
    2    return None
    

    the terminal shows AssertionError

    AssertionError: None != TUV.WXYZABCDEFGHIJ
    
  • I change the return statement

    1def add(x, y):
    2    return x + y
    

    the terminal shows AttributeError

    AttributeError: module 'src.calculator' has no attribute 'divide'
    
  • I add a function

    1def add(x, y):
    2    return x + y
    3
    4
    5def divide(x, y):
    6    return x / y
    

    the terminal shows AttributeError

    AttributeError: module 'src.calculator' has no attribute 'multiply'
    
  • I add a function

     5def divide(x, y):
     6    return x / y
     7
     8
     9def multiply(x, y):
    10    return x * y
    

    the terminal shows AttributeError

    AttributeError: module 'src.calculator' has no attribute 'subtract'
    
  • I add a function for it

     9def multiply(x, y):
    10    return x * y
    11
    12
    13def subtract(x, y):
    14    return x - y
    

    the terminal shows TypeError

    TypeError: divide() got an unexpected keyword argument 'first_input'
    

  • I add a name to the divide function

    5def divide(x, y, first_input):
    6    return x / y
    

    the terminal shows TypeError

    TypeError: divide() missing 1 required positional argument: 'first_input'
    
  • I add a default value to make it a choice

    5def divide(x, y, first_input=None):
    6    return x / y
    

    the terminal shows TypeError

    TypeError: divide() got an unexpected keyword argument 'second_input'
    
  • I add second_input with a default value to the function

    5def divide(x, y, first_input=None, second_input=None):
    6    return x / y
    

    the terminal shows TypeError

    TypeError: divide() missing 2 required positional arguments: 'x' and 'y'
    
  • the test must be calling divide with positional arguments in some cases and with keyword arguments in others. My solution does not work, I remove x and y from the parentheses

    5def divide(first_input=None, second_input=None):
    6    return x / y
    

    the terminal shows NameError

    NameError: name 'x' is not defined
    
  • I change the names in the return statement

    5def divide(first_input=None, second_input=None):
    6    return first_input / second_input
    

    the terminal shows TypeError

    TypeError: multiply() got an unexpected keyword argument 'first_input'
    
  • I use the Rename Symbol feature to change the names in the parentheses

     9def multiply(first_input, y):
    10    return first_input * y
    

    the terminal shows TypeError

    TypeError: divide() got an unexpected keyword argument 'second_input'
    
  • I do the same thing to y

    5def divide(first_input, second_input):
    6    return first_input / second_input
    

    the terminal shows TypeError

    TypeError: subtract() got an unexpected keyword argument 'first_input'
    

    same problem, same solution

  • I change the names

    13def subtract(first_input, second_input):
    14    return first_input - second_input
    

    the terminal shows TypeError

    TypeError: add() got an unexpected keyword argument 'first_input'
    
  • I change the names in the add function

    1def add(first_input, second_input):
    2    return first_input + second_input
    

    the terminal shows TypeError

    SUBFAILED(operation='divide', bad_input={'key': 'value'}) ... - TypeError: unsupported operand type(s) for /: 'dict' and 'float'
    

  • I add an if statement to the divide function

    5def divide(first_input=None, second_input=None):
    6    if isinstance(first_input, dict):
    7        return None
    8    return first_input / second_input
    

    the terminal shows AssertionError

    AssertionError: None != 'brmph?! Numbers only. Try again...'
    
  • I change the return statement of the if statement

    1def add(first_input, second_input):
    2    if isinstance(first_input, dict):
    3        return 'brmph?! Numbers only. Try again...'
    4    return first_input + second_input
    

    the terminal shows TypeError

    SUBFAILED(operation='multiply', bad_input={'key': 'value'}) ... - TypeError: unsupported operand type(s) for *: 'dict' and 'float'
    
  • I add an if statement to the multiply function

    11def multiply(first_input, second_input):
    12    if isinstance(first_input, dict):
    13        return None
    14    return first_input * second_input
    

    the terminal shows AssertionError

    AssertionError: None != 'brmph?! Numbers only. Try again...'
    
  • I change the return statement of the if statement

    11def multiply(first_input, second_input):
    12    if isinstance(first_input, dict):
    13        return 'brmph?! Numbers only. Try again...'
    14    return first_input * second_input
    

    the terminal shows TypeError

    SUBFAILED(operation='subtract', bad_input={'key': 'value'}) ... - TypeError: unsupported operand type(s) for -: 'dict' and 'float'
    

    same problem, same solution

  • I add an if statement with the same message in the subtract function

    17def subtract(first_input, second_input):
    18    if isinstance(first_input, dict):
    19        return 'brmph?! Numbers only. Try again...'
    20    return first_input - second_input
    

    the terminal shows TypeError

    SUBFAILED(operation='add', bad_input={'key': 'value'}) ... - TypeError: unsupported operand type(s) for +: 'dict' and 'float'
    
  • I add the same if statement to the add function

    1def add(first_input, second_input):
    2    if isinstance(first_input, dict):
    3        return 'brmph?! Numbers only. Try again...'
    4    return first_input + second_input
    

    the terminal shows TypeError

    SUBFAILED(operation='divide', bad_input={'n', 0, 2, 1}) ... - TypeError: unsupported operand type(s) for /: 'set' and 'float'
    

  • I add set to the if statement in the divide function

     7def divide(first_input=None, second_input=None):
     8    if isinstance(first_input, (dict, set)):
     9        return 'brmph?! Numbers only. Try again...'
    10    return first_input / second_input
    

    the terminal shows TypeError

    SUBFAILED(operation='multiply', bad_input={0, 1, 2, 'n'}) ... - TypeError: unsupported operand type(s) for *: 'set' and 'float'
    
  • I do the same thing to the if statement in the multiply function

    13def multiply(first_input, second_input):
    14    if isinstance(first_input, (dict, set)):
    15        return 'brmph?! Numbers only. Try again...'
    16    return first_input * second_input
    

    the terminal shows TypeError

    SUBFAILED(operation='subtract', bad_input={0, 1, 2, 'n'}) ... - TypeError: unsupported operand type(s) for -: 'set' and 'float'
    
  • I change the if statement in the subtract function

    19def subtract(first_input, second_input):
    20    if isinstance(first_input, (dict, set)):
    21        return 'brmph?! Numbers only. Try again...'
    22    return first_input - second_input
    

    the terminal shows TypeError

    SUBFAILED(operation='add', bad_input={0, 1, 2, 'n'}) ... - TypeError: unsupported operand type(s) for +: 'set' and 'float'
    
  • I add set to the if statement in the add function

    1def add(first_input, second_input):
    2    if isinstance(first_input, (dict, set)):
    3        return 'brmph?! Numbers only. Try again...'
    4    return first_input + second_input
    

    the terminal shows TypeError

    SUBFAILED(operation='divide', bad_input=[0, 1, 2, 'n']) ... - TypeError: unsupported operand type(s) for /: 'list' and 'float'
    

  • I add list to the if statement of the divide function

     7def divide(first_input=None, second_input=None):
     8    if isinstance(first_input, (dict, set, list)):
     9        return 'brmph?! Numbers only. Try again...'
    10    return first_input / second_input
    

    the terminal shows TypeError

    SUBFAILED(operation='multiply', bad_input=[0, 1, 2, 'n']) ... - TypeError: can't multiply sequence by non-int of type 'float'
    

    the error message is different but the function got [0, 1, 2, 'n'] as input, it looks I will have to do this for all the other operations

  • I add list to the if statement in the other functions

     1def add(first_input, second_input):
     2    if isinstance(first_input, (dict, set, list)):
     3        return 'brmph?! Numbers only. Try again...'
     4    return first_input + second_input
     5
     6
     7def divide(first_input=None, second_input=None):
     8    if isinstance(first_input, (dict, set, list)):
     9        return 'brmph?! Numbers only. Try again...'
    10    return first_input / second_input
    11
    12
    13def multiply(first_input, second_input):
    14    if isinstance(first_input, (dict, set, list)):
    15        return 'brmph?! Numbers only. Try again...'
    16    return first_input * second_input
    17
    18
    19def subtract(first_input, second_input):
    20    if isinstance(first_input, (dict, set, list)):
    21        return 'brmph?! Numbers only. Try again...'
    22    return first_input - second_input
    

    the terminal shows TypeError

    SUBFAILED(operation='divide', bad_input=(0, 1, 2, 'n')) ... - TypeError: unsupported operand type(s) for /: 'tuple' and 'float'
    

    another data type, there has to be a better way


  • I add tuple to the isinstance built-in function in the if statement of the divide function

     7def divide(first_input=None, second_input=None):
     8    if isinstance(first_input, (dict, set, list, tuple)):
     9        return 'brmph?! Numbers only. Try again...'
    10    return first_input / second_input
    

    the terminal shows TypeError

    SUBFAILED(operation='multiply', bad_input=(0, 1, 2, 'n')) ... - TypeError: can't multiply sequence by non-int of type 'float'
    

    same problem, same solution

  • I add tuple to the if statement of the other functions

     7def add(first_input, second_input):
     8    if isinstance(first_input, (dict, set, list, tuple)):
     9        return 'brmph?! Numbers only. Try again...'
    10    return first_input + second_input
    11
    12
    13def divide(first_input=None, second_input=None):
    14    if isinstance(first_input, (dict, set, list, tuple)):
    15        return 'brmph?! Numbers only. Try again...'
    16    return first_input / second_input
    17
    18
    19def multiply(first_input, second_input):
    20    if isinstance(first_input, (dict, set, list, tuple)):
    21        return 'brmph?! Numbers only. Try again...'
    22    return first_input * second_input
    23
    24
    25def subtract(first_input, second_input):
    26    if isinstance(first_input, (dict, set, list, tuple)):
    27        return 'brmph?! Numbers only. Try again...'
    28    return first_input - second_input
    

    the terminal shows TypeError

    TypeError: can only concatenate str (not "float") to str
    

  • I add str to the if statements of the 4 functions

     1def add(first_input, second_input):
     2    if isinstance(first_input, (dict, set, list, tuple, str)):
     3        return 'brmph?! Numbers only. Try again...'
     4    return first_input + second_input
     5
     6
     7def divide(first_input=None, second_input=None):
     8    if isinstance(first_input, (dict, set, list, tuple, str)):
     9        return 'brmph?! Numbers only. Try again...'
    10    return first_input / second_input
    11
    12
    13def multiply(first_input, second_input):
    14    if isinstance(first_input, (dict, set, list, tuple, str)):
    15        return 'brmph?! Numbers only. Try again...'
    16    return first_input * second_input
    17
    18
    19def subtract(first_input, second_input):
    20    if isinstance(first_input, (dict, set, list, tuple, str)):
    21        return 'brmph?! Numbers only. Try again...'
    22    return first_input - second_input
    

    the terminal shows AssertionError

    SUBFAILED(operation='divide', bad_input=False) ... - AssertionError: 0.0 != 'brmph?! Numbers only. Try again...'
    

    the input is a boolean in this case


  • I add bool to the if statements of the functions

     1def add(first_input, second_input):
     2    if isinstance(
     3        first_input,
     4        (dict, set, list, tuple, str, bool)
     5    ):
     6        return 'brmph?! Numbers only. Try again...'
     7    return first_input + second_input
     8
     9
    10def divide(first_input=None, second_input=None):
    11    if isinstance(
    12        first_input,
    13        (dict, set, list, tuple, str, bool)
    14    ):
    15        return 'brmph?! Numbers only. Try again...'
    16    return first_input / second_input
    17
    18
    19def multiply(first_input, second_input):
    20    if isinstance(
    21        first_input,
    22        (dict, set, list, tuple, str, bool)
    23    ):
    24        return 'brmph?! Numbers only. Try again...'
    25    return first_input * second_input
    26
    27
    28def subtract(first_input, second_input):
    29    if isinstance(
    30        first_input,
    31        (dict, set, list, tuple, str, bool)
    32    ):
    33        return 'brmph?! Numbers only. Try again...'
    34    return first_input - second_input
    

    the terminal shows TypeError

    SUBFAILED(operation='divide', bad_input=None) ... - TypeError: unsupported operand type(s) for /: 'NoneType' and 'float'
    

  • I add None to the call to the isinstance built-in function in the if statement of the divide function

    10def divide(first_input=None, second_input=None):
    11    if isinstance(
    12        first_input,
    13        (dict, set, list, tuple, str, bool, None)
    14    ):
    15        return 'brmph?! Numbers only. Try again...'
    16    return first_input / second_input
    

    the terminal shows TypeError

    TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union
    

    I cannot use None in the isinstance built-in function

  • I undo the change then add a new condition

    10def divide(first_input=None, second_input=None):
    11    if isinstance(
    12        first_input,
    13        (dict, set, list, tuple, str, bool)
    14    ) or first_input is None:
    15        return 'brmph?! Numbers only. Try again...'
    16    return first_input / second_input
    

    the terminal shows TypeError

    SUBFAILED(operation='multiply', bad_input=None) ... - TypeError: unsupported operand type(s) for *: 'NoneType' and 'float'
    

    same problem, same …

  • I add the new condition to the if statement of the other functions

     1def add(first_input, second_input):
     2    if isinstance(
     3        first_input,
     4        (dict, set, list, tuple, str, bool)
     5    ) or first_input is None:
     6        return 'brmph?! Numbers only. Try again...'
     7    return first_input + second_input
     8
     9
    10def divide(first_input=None, second_input=None):
    11    if isinstance(
    12        first_input,
    13        (dict, set, list, tuple, str, bool)
    14    ) or first_input is None:
    15        return 'brmph?! Numbers only. Try again...'
    16    return first_input / second_input
    17
    18
    19def multiply(first_input, second_input):
    20    if isinstance(
    21        first_input,
    22        (dict, set, list, tuple, str, bool)
    23    ) or first_input is None:
    24        return 'brmph?! Numbers only. Try again...'
    25    return first_input * second_input
    26
    27
    28def subtract(first_input, second_input):
    29    if isinstance(
    30        first_input,
    31        (dict, set, list, tuple, str, bool)
    32    ) or first_input is None:
    33        return 'brmph?! Numbers only. Try again...'
    34    return first_input - second_input
    

    the terminal shows AssertionError

    AssertionError: TypeError not raised
    
  • I remove the default values from the divide function because it is the only one that is different from the others

    10def divide(first_input, second_input):
    11    if isinstance(
    12        first_input,
    13        (dict, set, list, tuple, str, bool)
    14    ) or first_input is None:
    15        return 'brmph?! Numbers only. Try again...'
    16    return first_input / second_input
    

    the test passes. Time to play


REFACTOR: make it better


  • I add a decorator function

     1def check_input(function):
     2    def wrapper(first_input, second_input):
     3        if isinstance(
     4            first_input,
     5            (dict, set, list, tuple, str, bool)
     6        ) or first_input is None:
     7            return 'brmph?! Numbers only. Try again...'
     8        return function(first_input, second_input)
     9    return wrapper
    10
    11
    12def add(first_input, second_input):
    
  • I use it to wrap the add function

    12@check_input
    13def add(first_input, second_input):
    14    if isinstance(
    15        first_input,
    16        (dict, set, list, tuple, str, bool)
    17    ) or first_input is None:
    18        return 'brmph?! Numbers only. Try again...'
    19    return first_input + second_input
    

    the test is still green

  • I remove the if statement

    12@check_input
    13def add(first_input, second_input):
    14    return first_input + second_input
    15
    16
    17def divide(first_input, second_input):
    

    still green

  • I do the same thing to the divide function

    17@check_input
    18def divide(first_input, second_input):
    19    if isinstance(
    20        first_input,
    21        (dict, set, list, tuple, str, bool)
    22    ) or first_input is None:
    23        return 'brmph?! Numbers only. Try again...'
    24    return first_input / second_input
    

    green

  • I remove the if statement

    17@check_input
    18def divide(first_input, second_input):
    19    return first_input / second_input
    20
    21
    22def multiply(first_input, second_input):
    

    the test is still green

  • I make the same change to the other 2 functions

     1def check_input(function):
     2    def wrapper(first_input, second_input):
     3        if (
     4            isinstance(
     5                first_input,
     6                (dict, set, list, tuple, str, bool)
     7            ) or
     8            first_input is None
     9        ):
    10            return 'brmph?! Numbers only. Try again...'
    11        return function(first_input, second_input)
    12    return wrapper
    13
    14
    15@check_input
    16def add(first_input, second_input):
    17    return first_input + second_input
    18
    19
    20@check_input
    21def divide(first_input, second_input):
    22    return first_input / second_input
    23
    24
    25@check_input
    26def multiply(first_input=None, second_input=None):
    27    return first_input * second_input
    28
    29
    30@check_input
    31def subtract(first_input, second_input):
    32    return first_input - second_input
    

    still green


All the tests are passing, but there are problems.

I wonder what else I missed. I am still learning


close the project

  • I close test_calculator.py in the editor

  • I 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 calculator

    cd ..
    

    the terminal shows

    .../pumping_python
    

    I am back in the pumping_python directory


review

  • I used the __getattribute__ built-in function to make the calculator tests simpler.

  • I rewrote the solution after rewriting the tests and found that

    • I did not add a test for bad second inputs

    • the test for handling ZeroDivisionError has a problem


code from the chapter

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


what is next?

you have gone through a lot of things and know

Would you like to know what causes ModuleNotFoundError?


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