how to make a calculator 10: part 1


preview

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

  1import random
  2import streamlit.testing.v1
  3import tests.test_calculator
  4import unittest
  5
  6
  7class TestStreamlitCalculator(unittest.TestCase):
  8
  9    def setUp(self):
 10        self.tester = streamlit.testing.v1.AppTest.from_file(
 11            'src/streamlit_calculator.py'
 12        )
 13        self.tester.run()
 14
 15    def test_streamlit_calculator_title(self):
 16        self.assertEqual(self.tester.title[0].value, 'Calculator')
 17
 18    def test_streamlit_calculator_display(self):
 19        self.assertEqual(
 20            self.tester.main.children[1].type,
 21            'flex_container'
 22        )
 23
 24    def test_streamlit_calculator_columns(self):
 25        self.assertEqual(len(self.tester.columns), 4)
 26        self.assertEqual(
 27            self.tester.columns[0].button('<-').label,
 28            '<-'
 29        )
 30        self.assertEqual(
 31            self.tester.columns[0].button('7').label,
 32            '7'
 33        )
 34        self.assertEqual(
 35            self.tester.columns[0].button('4').label,
 36            '4'
 37        )
 38        self.assertEqual(
 39            self.tester.columns[0].button('1').label,
 40            '1'
 41        )
 42        self.assertEqual(
 43            self.tester.columns[0].button('+/-').label,
 44            '+/-'
 45        )
 46        self.assertEqual(
 47            self.tester.columns[1].button('C').label,
 48            'C'
 49        )
 50        self.assertEqual(
 51            self.tester.columns[1].button('8').label,
 52            '8'
 53        )
 54        self.assertEqual(
 55            self.tester.columns[1].button('5').label,
 56            '5'
 57        )
 58        self.assertEqual(
 59            self.tester.columns[1].button('2').label,
 60            '2'
 61        )
 62        self.assertEqual(
 63            self.tester.columns[1].button('0').label,
 64            '0'
 65        )
 66
 67        self.assertEqual(
 68            self.tester.columns[2].button('AC').label,
 69            'AC'
 70        )
 71        self.assertEqual(
 72            self.tester.columns[2].button('9').label,
 73            '9'
 74        )
 75        self.assertEqual(
 76            self.tester.columns[2].button('6').label,
 77            '6'
 78        )
 79        self.assertEqual(
 80            self.tester.columns[2].button('3').label,
 81            '3'
 82        )
 83        self.assertEqual(
 84            self.tester.columns[2].button('.').label,
 85            '.'
 86        )
 87
 88        self.assertEqual(
 89            self.tester.columns[3].button('/').label,
 90            '/'
 91        )
 92        self.assertEqual(
 93            self.tester.columns[3].button('X').label,
 94            'X'
 95        )
 96        self.assertEqual(
 97            self.tester.columns[3].button('-').label,
 98            r'\-'
 99        )
100        self.assertEqual(
101            self.tester.columns[3].button('+').label,
102            r'\+'
103        )
104        self.assertEqual(
105            self.tester.columns[3].button('=').label,
106            '='
107        )
108
109    def test_streamlit_session_state(self):
110        expectation = '0'
111        for _ in range(0, 10):
112            number = random.choice('0123456789')
113            (
114                self.tester.button(number)
115                .click().run()
116            )
117            if expectation == '0':
118                expectation = number
119            else:
120                expectation += number
121        self.assertEqual(
122            self.tester.session_state['number'],
123            expectation
124        )
125
126    def test_streamlit_calculator_w_decimals(self):
127        for button in ('0.23.5.6.7.8.9'):
128            (
129                self.tester.button(button)
130                .click().run()
131            )
132        self.assertEqual(
133            self.tester.session_state['number'],
134            '.2356789'
135        )
136
137    def test_streamlit_calculator_w_plus_minus(self):
138        number = '963.0258741'
139        for button in number:
140            (
141                self.tester.button(button)
142                .click().run()
143            )
144        self.tester.button('+/-').click().run()
145        self.assertEqual(
146            self.tester.session_state['number'],
147            f'-{number}'
148        )
149
150        self.tester.session_state['number'] = '0'
151        number = '-963.0258741'
152        for button in number:
153            (
154                self.tester.button(button)
155                .click().run()
156            )
157
158        self.tester.button('+/-').click().run()
159        self.assertEqual(
160            self.tester.session_state['number'],
161            number[1:]
162        )
163
164    def test_streamlit_calculator_reset_state(self):
165        numbers = '123456789'
166        number = random.choice(numbers)
167        self.tester.button(number).click().run()
168        self.assertEqual(
169            self.tester.session_state['number'],
170            number
171        )
172        self.tester.button('C').click().run()
173        self.assertEqual(
174            self.tester.session_state['number'],
175            '0'
176        )
177
178        number = random.choice(numbers)
179        self.tester.button(number).click().run()
180        self.assertEqual(
181            self.tester.session_state['number'],
182            number
183        )
184        self.tester.button('AC').click().run()
185        self.assertEqual(
186            self.tester.session_state['number'],
187            '0'
188        )
189
190    @unittest.skip
191    def test_streamlit_calculator_operations(self):
192        # first_number = '1'
193        first_number = tests.test_calculator.a_random_number()
194        first_number = str(first_number)
195        second_number = '2'
196
197        for character in first_number:
198            if character == '-':
199                self.tester.button('+/-').click().run()
200            else:
201                self.tester.button(character).click().run()
202        self.tester.button('+').click().run()
203        self.assertEqual(
204            self.tester.session_state['first_number'],
205            first_number
206        )
207
208        self.tester.button(second_number).click().run()
209        self.tester.button('=').click().run()
210        self.assertEqual(
211            self.tester.session_state['second_number'],
212            second_number
213        )
214
215        self.assertEqual(
216            self.tester.session_state['number'],
217            str(float(first_number) + float(second_number))
218        )
219
220
221# Exceptions seen
222# NameError
223# AttributeError
224# AssertionError
225# SyntaxError
226# KeyError
227# streamlit.errors.StreamlitDuplicateElementKey
228# TypeError

open the project

  • I change directory to the calculator folder

    cd calculator
    
  • I make a new test file for the Streamlit website

    touch tests/test_streamlit_calculator.py
    
  • I add streamlit to the requirements.txt file

    echo "streamlit" >> requirements.txt
    

    Streamlit is a Python library that is used for making websites, it is not part of The Python Standard Library

  • I check to see what is in my requirements.txt

    cat requirements.txt
    

    the terminal is my friend, and shows

    pytest
    pytest-watcher
    flask
    streamlit
    
  • I install the Python packages that I wrote in the requirements file

    uv add --requirement requirements.txt
    

    the terminal shows that it installed the Python packages

  • I use pytest-watcher to run the tests

    uv run pytest-watcher . --now
    

    the terminal is my friend, and shows

    rootdir: .../pumping_python/calculator
    configfile: pyproject.toml
    collected 8 items
    
    tests/test_calculator.py .....                                [ 62%]
    tests/test_calculator_website.py ...                          [100%]
    
    ======================== 8 passed in X.YZs =========================
    

test_streamlit_calculator_title


RED: make it fail


  • I open test_streamlit_calculator.py from the tests folder in the editor

  • I add a new test in test_streamlit_calculator.py

    1import unittest
    2
    3
    4class TestStreamlitCalculator(unittest.TestCase):
    5
    6    def test_streamlit_calculator_title(self):
    7        tester = streamlit.testing.v1.AppTest.from_file(
    8            'src/streamlit_calculator.py'
    9        )
    

    the terminal is my friend, and shows NameError

    NameError: name 'streamlit' is not defined
    
  • I add NameError to the list of Exceptions seen

     1import unittest
     2
     3
     4class TestStreamlitCalculator(unittest.TestCase):
     5
     6    def test_streamlit_calculator_title(self):
     7        tester = streamlit.testing.v1.AppTest.from_file(
     8            'src/streamlit_calculator.py'
     9        )
    10
    11
    12# Exceptions seen
    13# NameError
    

GREEN: make it pass



REFACTOR: make it better


  • I add more to the test to see what happens when I try to run the application even though src/streamlit_calculator.py is empty

     7    def test_streamlit_calculator_title(self):
     8        tester = streamlit.testing.v1.AppTest.from_file(
     9            'src/streamlit_calculator.py'
    10        )
    11        tester.run()
    

    the test is still green

  • I add an assertion for the title of the application

     7    def test_streamlit_calculator_title(self):
     8        tester = streamlit.testing.v1.AppTest.from_file(
     9            'src/streamlit_calculator.py'
    10        )
    11        tester.run()
    12        self.assertEqual(tester.title, 'Calculator')
    

    the terminal is my friend, and shows AssertionError

    AssertionError: ElementList() != 'Calculator'
    
  • I add AssertionError to the list of Exceptions seen

    15# Exceptions seen
    16# NameError
    17# AttributeError
    18# AssertionError
    
  • I make a new file in the src folder and call it streamlit_calculator.py

  • I open streamlit_calculator.py in the editor

  • I add code to make a streamlit application with a title, in streamlit_calculator.py

    1import streamlit
    2
    3
    4def main():
    5    streamlit.title('Calculator')
    

    the terminal is my friend, and shows AssertionError

    AssertionError: ElementList() != 'Calculator'
    
  • I add an if statement to run the main function when the module gets called as a script

    4def main():
    5    streamlit.title('Calculator')
    6
    7
    8if __name__ == '__main__':
    9    main()
    

    the terminal is my friend, and shows AssertionError

    AssertionError: ElementList(_list=[Title(tag='h1')]) != 'Calculator'
    
    • when I import a module nothing happens until I call or use the things in it

    • if __name__ == '__main__': calls main() only when src/streamlit_calculator.py gets called like a script, for example

      • in the terminal

        python3 src/streamlit_calculator.py
        
      • or in test_streamlit_calculator.py

        tester = streamlit.testing.v1.AppTest.from_file(
            'src/streamlit_calculator.py'
        )
        tester.run()
        

      it does not get called when the module is imported

    • ElementList(_list=[Title(tag='h1')]) has a list and I know how to work with lists

  • I change the assertion to get the first item from the list, in test_streamlit_calculator.py

    12        self.assertEqual(tester.title[0], 'Calculator')
    

    the terminal is my friend, and shows AssertionError

    AssertionError: Title(tag='h1') != 'Calculator'
    
  • I use the value attribute of the Title class

     7    def test_streamlit_calculator_title(self):
     8        tester = streamlit.testing.v1.AppTest.from_file(
     9            'src/streamlit_calculator.py'
    10        )
    11        tester.run()
    12        self.assertEqual(tester.title[0].value, 'Calculator')
    

    the test passes. Time to run the application


how to see the streamlit calculator website

  • I open another terminal then use uv in the calculator folder

    uv run streamlit run src/streamlit_calculator.py
    

    the terminal is my friend, and shows

    Collecting usage statistics.
    To deactivate, set browser.gatherUsageStats to false.
    
    
      You can now view your Streamlit app in your browser.
    
      Local URL: http://localhost:8501
      Network URL: http://ABC.DEF.GHI.JKL:8501
      External URL: http://MNO.PQR.STU.VWX:8501
    

    it might also show a dialog box like this, and I click on Open in Browser

    Confirm you want to view Streamlit app in browser

    or I use ctrl/option on the keyboard and click on http://localhost:8501 with the mouse to open the browser and it shows

    Calculator Streamlit App with Title

    Success!

  • I click the 3 dots by Deploy on the right hand side

    Streamlit Deploy Menu
  • I click on Settings

  • I click the check marks by Run on save and Wide mode to make sure the website changes as I make changes to the code

    Streamlit Deploy Settings

test_streamlit_calculator_display

I want the calculator to have a place to show results as the user clicks numbers


RED: make it fail


I add a test to see all the attributes of the application

 7    def test_streamlit_calculator_title(self):
 8        tester = streamlit.testing.v1.AppTest.from_file(
 9            'src/streamlit_calculator.py'
10        )
11        tester.run()
12        self.assertEqual(tester.title[0].value, 'Calculator')
13
14    def test_streamlit_calculator_display(self):
15        tester = streamlit.testing.v1.AppTest.from_file(
16            'src/streamlit_calculator.py'
17        )
18        tester.run()
19        self.assertIsNone(tester.main)

the terminal is my friend, and shows AssertionError

E       AssertionError: SpecialBlock(
E           type='main',
E           children={
E               0: Title(tag='h1')
E           }
E       ) is not None

I see that the children object is a dictionary. I know how to work with dictionaries.


GREEN: make it pass


  • I add an expectation with the children attribute

    19        self.assertIsNone(tester.main.children, {})
    

    the terminal is my friend, and shows AssertionError

    AssertionError: {0: Title(tag='h1')} is not None : {}
    
  • I change assertIsNone to assertEqual

    19        self.assertEqual(tester.main.children, {})
    

    the terminal is my friend, and shows AssertionError

    AssertionError: {0: Title(tag='h1')} != {}
    

    the only thing in the application is the title

  • I copy the dictionary from the terminal and use it as the expectation

    14    def test_streamlit_calculator_display(self):
    15        tester = streamlit.testing.v1.AppTest.from_file(
    16            'src/streamlit_calculator.py'
    17        )
    18        tester.run()
    19        self.assertEqual(
    20            tester.main.children,
    21            {0: Title(tag='h1')}
    22        )
    

    the terminal is my friend, and shows NameError

    NameError: name 'Title' is not defined
    
  • I change the Title object to tester.title[0]

    19        self.assertEqual(
    20            tester.main.children,
    21            {0: tester.title[0]}
    22        )
    

    the test passes


REFACTOR: make it better


  • I add a streamlit container to the main function in streamlit_calculator.py

    4def main():
    5    streamlit.title('Calculator')
    6    streamlit.container()
    

    the terminal is my friend, and shows AssertionError

    AssertionError: {0: Title(tag='h1'), 1: Block(
       type='flex_container'
    )} != {0: Title(tag='h1')}
    

    there is a new key-value pair because I added something to the application

  • I change the assertion in test_streamlit_calculator.py

    19        self.assertEqual(
    20            tester.main.children,
    21            {
    22                0: tester.title[0],
    23                1: Block(type='flex_container'),
    24            }
    25        )
    

    the terminal is my friend, and shows NameError

    NameError: name 'Block' is not defined
    
  • I change the assertion to get the streamlit Block object

    19        self.assertEqual(
    20            tester.main.children[1],
    21            {
    22                0: tester.title[0],
    23                # 1: Block(type='flex_container')
    24            }
    25        )
    

    the terminal is my friend, and shows AssertionError

    AssertionError: Block(
       type='flex_container'
    )
    
  • I use the __dict__ attribute to get the streamlit Block object as a dictionary

    19        self.assertEqual(
    20            tester.main.children[1].__dict__,
    21            {
    22                0: tester.title[0],
    23                # 1: Block(type='flex_container')
    24            }
    25        )
    

    the terminal is my friend, and shows AssertionError

    AssertionError: {'children': {}, 'proto': flex_container {[503 chars] )
    
  • I set maxDiff to None

    18        tester.run()
    19        self.maxDiff = None
    20        self.assertEqual(
    21            tester.main.children[1].__dict__,
    22            {
    23                0: tester.title[0],
    24                # 1: Block(type='flex_container')
    25            }
    26        )
    

    the terminal is my friend, and shows the full difference

  • I copy the dictionary, remove the extra characters with Find and Replace (ctrl+H (Windows) or command+option+F (MacOS)) and use it as the expectation

    20        self.assertEqual(
    21            tester.main.children[1].__dict__,
    22            {
    23                'children': {},
    24                'proto': flex_container {
    25                    gap_config {
    26                        gap_size: SMALL
    27                    }
    28                    direction: VERTICAL
    29                    justify: JUSTIFY_START
    30                    align: ALIGN_START
    31                }
    32                height_config {
    33                    use_content: true
    34                }
    35                width_config {
    36                    use_stretch: true
    37                },
    38                'root': {
    39                    0: SpecialBlock(
    40                        type='main',
    41                        children={
    42                            0: Title(tag='h1'),
    43                            1: Block(
    44                                type='flex_container'
    45                            )
    46                        }
    47                    ),
    48                    1: SpecialBlock(
    49                        type='sidebar'
    50                    ),
    51                    2: SpecialBlock(
    52                        type='event'
    53                    )
    54                },
    55                'type': 'flex_container'
    56            }
    57        )
    58
    59
    60# Exceptions seen
    

    the terminal is my friend, and shows SyntaxError

    SyntaxError: invalid syntax. Perhaps you forgot a comma?
    

    this dictionary has too many things

  • I add SyntaxError to the list of Exceptions seen

    60# Exceptions seen
    61# NameError
    62# AttributeError
    63# AssertionError
    64# SyntaxError
    
  • I change the assertion to use the proto attribute since it looks like a dictionary

    20        self.assertEqual(
    21            tester.main.children[1].proto,
    22            {}
    23        )
    

    the terminal is my friend, and shows AssertionError

    E       AssertionError: flex_container {
    E         gap_config {
    E           gap_s[155 chars]ue
    E       }
    E        != {}
    
  • I use the flex_container attribute instead

    20        self.assertEqual(
    21            tester.main.children[1].proto.flex_container,
    22            {}
    23        )
    

    the terminal is my friend, and shows AssertionError

    E       AssertionError: gap_config {
    E         gap_size: SMALL
    E       }
    E       directio[49 chars]TART
    E        != {}
    
  • I use the gap_size attribute directly

    20        self.assertEqual(
    21            (
    22                tester.main.children[1].proto
    23                      .flex_container
    24                      .gap_config.gap_size
    25            ),
    26            {}
    27        )
    

    the terminal is my friend, and shows AssertionError

    AssertionError: 1 != {}
    
  • I change the expectation

    20        self.assertEqual(
    21            (
    22                tester.main.children[1].proto
    23                      .flex_container
    24                      .gap_config.gap_size
    25            ),
    26            1
    27        )
    

    the test passes

  • I remove self.maxDiff

    14    def test_streamlit_calculator_display(self):
    15        tester = streamlit.testing.v1.AppTest.from_file(
    16            'src/streamlit_calculator.py'
    17        )
    18        tester.run()
    19
    20        self.assertEqual(
    21            (
    22                tester.main.children[1].proto
    23                      .flex_container
    24                      .gap_config.gap_size
    25            ),
    26            1
    27        )
    28
    29
    30# Exceptions seen
    
  • I add a variable for the flex_container object

    14    def test_streamlit_calculator_display(self):
    15        tester = streamlit.testing.v1.AppTest.from_file(
    16            'src/streamlit_calculator.py'
    17        )
    18        tester.run()
    19
    20        display = (
    21            tester.main.children[1].proto
    22                  .flex_container
    23        )
    24        self.assertEqual(
    
  • I use the variable in the assertion

    24        self.assertEqual(
    25            # (
    26            #     tester.main.children[1].proto
    27            #           .flex_container
    28            #           .gap_config.gap_size
    29            # ),
    30            display.gap_config.gap_size, 1
    31        )
    

    the test is still green

  • I remove the commented lines

    24        self.assertEqual(display.gap_config.gap_size, 1)
    
  • I add an assertion for the next attribute of the flex_container object

    24        self.assertEqual(display.gap_config.gap_size, 1)
    25        self.assertEqual(display.direction, '')
    

    the terminal is my friend, and shows AssertionError

    AssertionError: 1 != ''
    
  • I change the expectation

    25        self.assertEqual(display.direction, 1)
    

    the test passes

  • I add an assertion for the next attribute of the flex_container object

    24        self.assertEqual(display.gap_config.gap_size, 1)
    25        self.assertEqual(display.direction, 1)
    26        self.assertEqual(display.justify, '')
    

    the terminal is my friend, and shows AssertionError

    AssertionError: 1 != ''
    
  • I change the expectation

    26        self.assertEqual(display.justify, 1)
    

    the test passes

  • I add the last attribute of the flex_container object

    24        self.assertEqual(display.gap_config.gap_size, 1)
    25        self.assertEqual(display.direction, 1)
    26        self.assertEqual(display.justify, 1)
    27        self.assertEqual(display.align, '')
    

    the terminal is my friend, and shows AssertionError

    AssertionError: 1 != ''
    
  • I change the expectation

    27        self.assertEqual(display.align, 1)
    28
    29
    30# Exceptions seen
    

    the test passes. All the attributes have 1 as their value which stands for different things in each case, they are called enums

    • gap_config.gap_size - 1 - SMALL

    • direction - 1 - VERTICAL

    • justify - 1 - JUSTIFY_START

    • align - 1 - ALIGN_START

  • I go to the browser and things look the same as before. I need to add a border

  • I add an assertion for a border in test_streamlit_calculator.py

    24        self.assertEqual(display.gap_config.gap_size, 1)
    25        self.assertEqual(display.direction, 1)
    26        self.assertEqual(display.justify, 1)
    27        self.assertEqual(display.align, 1)
    28        self.assertEqual(display.border, '')
    

    the terminal is my friend, and shows AssertionError

    AssertionError: False != ''
    
  • I change the assertEqual to assertFalse and remove the expectation

    28        self.assertFalse(display.border)
    

    the test passes

  • I change the assertFalse to assertTrue

    14    def test_streamlit_calculator_display(self):
    15        tester = streamlit.testing.v1.AppTest.from_file(
    16            'src/streamlit_calculator.py'
    17        )
    18        tester.run()
    19
    20        display = (
    21            tester.main.children[1].proto
    22                  .flex_container
    23        )
    24        self.assertEqual(display.gap_config.gap_size, 1)
    25        self.assertEqual(display.direction, 1)
    26        self.assertEqual(display.justify, 1)
    27        self.assertEqual(display.align, 1)
    28        self.assertTrue(display.border)
    29
    30
    31# Exceptions seen
    

    the terminal is my friend, and shows AssertionError

    AssertionError: False is not true
    
  • I add a border to the container in streamlit_calculator.py

    4def main():
    5    streamlit.title('Calculator')
    6    streamlit.container(border=True)
    

    the test passes

  • I go to the browser and click refresh

    Calculator Streamlit Display

    there is a box under the Calculator title


close the project

  • I close test_streamlit_calculator.py, streamlit_calculator.py in the editor

  • I click in the first 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 is my friend, and shows

    .../pumping_python
    

    I am back in the pumping_python directory

  • I click in the second terminal, then use ctrl+c on the keyboard to close the web server. The terminal goes back to the command line

  • I change directory to the parent of calculator

    cd ..
    

    the terminal is my friend, and shows

    .../pumping_python
    

    I am back in the pumping_python directory


review

I made a website using Streamlit with a title and a display


code from the chapter

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


what is next?

You now know how to:

Would you like to continue with adding buttons to the calculator?