how to make a calculator 10: part 2
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
calculatorfoldercd calculatorI use
pytest-watcherto run the testsuv run pytest-watcher . --nowthe terminal is my friend, and shows
rootdir: .../pumping_python/calculator configfile: pyproject.toml collected 10 items tests/test_calculator.py ..... [ 50%] tests/test_calculator_website.py ... [ 80%] tests/test_streamlit_calculator.py .. [100%] ======================== 10 passed in X.YZs =========================I open another terminal then use uv in the
calculatorfolderuv run streamlit run src/streamlit_calculator.pythe 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:8501I use ctrl/option on the keyboard and click on
http://localhost:8501with the mouse to open the browser and it shows
how to use raw strings and escape characters
I add
rand\to escape the-character because it means something to streamlit, which is why it does not show on the button in theadd_buttons_to_column_4function28def add_buttons_to_column_4(column_4): 29 column_4.button(label='/', key='/', width='stretch') 30 column_4.button(label='X', key='X', width='stretch') 31 column_4.button(label=r'\-', key=r'\-', width='stretch') 32 column_4.button(label='+', key='+', width='stretch') 33 column_4.button(label='=', key='=', width='stretch')the terminal is my friend, and shows KeyError
KeyError: '-'I change the
-tor'\-'in test_streamlit_calculator_columns_and_buttons intest_streamlit_calculator.py27 def test_streamlit_calculator_columns_and_buttons(self): 28 self.assertEqual(len(self.tester.columns), 4) 29 30 for column, keys in ( 31 (0, ('<-', '7', '4', '1', '+/-')), 32 (1, ('C', '8', '5', '2', '0')), 33 (2, ('AC', '9', '6', '3', '.')), 34 # (3, ('/', 'X', '-', '+', '=')), 35 (3, ('/', 'X', r'\-', '+', '=')), 36 ): 37 for key in keys:the test passes
I add
rand\to escape the+character because it means something to streamlit, which is why it does not show on the button in theadd_buttons_to_column_4function, instreamlit_calculator.py28def add_buttons_to_column_4(column_4): 29 column_4.button(label='/', key='/', width='stretch') 30 column_4.button(label='X', key='X', width='stretch') 31 column_4.button(label=r'\-', key=r'\-', width='stretch') 32 column_4.button(label=r'\+', key=r'\+', width='stretch') 33 column_4.button(label='=', key='=', width='stretch')the terminal is my friend, and shows KeyError
KeyError: '+'I change the
+tor'\+'in test_streamlit_calculator_columns_and_buttons intest_streamlit_calculator.py27 def test_streamlit_calculator_columns_and_buttons(self): 28 self.assertEqual(len(self.tester.columns), 4) 29 30 for column, keys in ( 31 (0, ('<-', '7', '4', '1', '+/-')), 32 (1, ('C', '8', '5', '2', '0')), 33 (2, ('AC', '9', '6', '3', '.')), 34 # (3, ('/', 'X', '-', '+', '=')), 35 # (3, ('/', 'X', r'\-', '+', '=')), 36 (3, ('/', 'X', r'\-', r'\+', '=')), 37 ): 38 for key in keys:the test passes
I remove the commented lines
27 def test_streamlit_calculator_columns_and_buttons(self): 28 self.assertEqual(len(self.tester.columns), 4) 29 30 for column, keys in ( 31 (0, ('<-', '7', '4', '1', '+/-')), 32 (1, ('C', '8', '5', '2', '0')), 33 (2, ('AC', '9', '6', '3', '.')), 34 (3, ('/', 'X', r'\-', r'\+', '=')), 35 ): 36 for key in keys: 37 with self.subTest(key=key): 38 self.assertEqual( 39 ( 40 self.tester.columns[column] 41 .button(key) 42 .label 43 ), 44 key 45 ) 46 47 48# Exceptions seenI check the browser
yes! The calculator shows all the labels.
how to show the numbers when I click on them
I want the calculator to show the number when I press a button
I add a variable to name the container so I can use it to show the numbers
50def main(): 51 streamlit.title('Calculator') 52 display = streamlit.container(border=True) 53 54 column_1, column_2, column_3, operations = streamlit.columns(4) 55 add_buttons_to_column_1(column_1) 56 add_buttons_to_column_2(column_2) 57 add_buttons_to_column_3(column_3) 58 add_buttons_to_column_4(operations)I add a function to show the text of the button when it is clicked
1import streamlit 2 3 4def show(display, number): 5 display.write(number) 6 7 8def add_buttons_to_column_1(column_1):streamlit buttons have an
on_clickparameter that lets me call a function when a button is pressed. It also takes an argument namedargswhere I can pass in the positional arguments that the function I give for theon_clickparameter takes. I pass the function and thedisplayvariable with a value as the arguments for the7button in theadd_buttons_to_column_1function8def add_buttons_to_column_1(column_1): 9 column_1.button(label='<-', key='<-', width='stretch') 10 column_1.button( 11 label='7', key='7', width='stretch', 12 on_click=show, args=[display, '7'], 13 ) 14 column_1.button(label='4', key='4', width='stretch') 15 column_1.button(label='1', key='1', width='stretch') 16 column_1.button(label='+/-', key='+/-', width='stretch')the terminal is my friend, and shows KeyError
================== 26 failed, 12 passed in A.BCs ===================the terminal for the application shows NameError
NameError: name 'display' is not definedI add a parameter for the
displayvariable in the function signature foradd_buttons_to_column_18def add_buttons_to_column_1(column_1, display):the terminal is my friend, and shows KeyError for 27 sub tests and the terminal for the streamlit application shows TypeError
TypeError: add_buttons_to_column_1() missing 1 required positional argument: 'display'I add the
displayvariable to the call toadd_buttons_to_column_1in themainfunction57def main(): 58 streamlit.title('Calculator') 59 display = streamlit.container(border=True) 60 61 column_1, column_2, column_3, operations = streamlit.columns(4) 62 add_buttons_to_column_1(column_1, display) 63 add_buttons_to_column_2(column_2) 64 add_buttons_to_column_3(column_3) 65 add_buttons_to_column_4(operations)the test passes
I go to the browser and click on the
7button
7shows on the display. Yes!I make the same change to the other numbers in the first column
8def add_buttons_to_column_1(column_1, display): 9 column_1.button(label='<-', key='<-', width='stretch') 10 column_1.button( 11 label='7', key='7', width='stretch', 12 on_click=show, args=[display, '7'], 13 ) 14 column_1.button( 15 label='4', key='4', width='stretch', 16 on_click=show, args=[display, '4'], 17 ) 18 column_1.button( 19 label='1', key='1', width='stretch', 20 on_click=show, args=[display, '1'], 21 )the test is still green
I do the same thing for the numbers in the second column
28def add_buttons_to_column_2(column_2): 29 column_2.button( 30 label='C', key='C', width='stretch', type='primary', 31 ) 32 column_2.button( 33 label='8', key='8', width='stretch', 34 on_click=show, args=[display, '8'], 35 ) 36 column_2.button( 37 label='5', key='5', width='stretch', 38 on_click=show, args=[display, '5'], 39 ) 40 column_2.button( 41 label='2', key='2', width='stretch', 42 on_click=show, args=[display, '2'], 43 ) 44 column_2.button( 45 label='0', key='0', width='stretch', 46 on_click=show, args=[display, '0'], 47 )the terminal is my friend, and shows KeyError for 20 sub tests and the terminal for the application shows NameError
I add
displayto the function signature of theadd_buttons_to_column_2function28def add_buttons_to_column_2(column_2, display):the terminal is my friend, and shows KeyError for 22 sub tests and the terminal for the application shows TypeError
TypeError: add_buttons_to_column_2() missing 1 required positional argument: 'display'I add
displayto the call to theadd_buttons_to_column_2function in themainfunction78def main(): 79 streamlit.title('Calculator') 80 display = streamlit.container(border=True) 81 82 column_1, column_2, column_3, operations = streamlit.columns(4) 83 add_buttons_to_column_1(column_1, display) 84 add_buttons_to_column_2(column_2, display) 85 add_buttons_to_column_3(column_3) 86 add_buttons_to_column_4(operations)the test passes
I make the same change for the numbers in the third column
47def add_buttons_to_column_3(column_3): 48 column_3.button( 49 label='AC', key='AC', width='stretch', type='primary', 50 ) 51 column_3.button( 52 label='9', key='9', width='stretch', 53 on_click=show, args=[display, '9'], 54 ) 55 column_3.button( 56 label='6', key='6', width='stretch', 57 on_click=show, args=[display, '6'], 58 ) 59 column_3.button( 60 label='3', key='3', width='stretch', 61 on_click=show, args=[display, '3'], 62 ) 63 column_3.button( 64 label='.', key='.', width='stretch', 65 on_click=show, args=[display, '.'], 66 )the terminal is my friend, and shows KeyError for 14 sub tests
================== 14 failed, 12 passed in X.YZs ===================and the terminal for the application shows NameError
NameError: name 'display' is not definedI add
displayto the signature of theadd_buttons_to_column_3function50def add_buttons_to_column_3(column_3, display):the terminal is my friend, and shows KeyError and the terminal for the application shows TypeError
TypeError: add_buttons_to_column_3() missing 1 required positional argument: 'display'I add
displayto the call to theadd_buttons_to_column_3in themainfunction90def main(): 91 streamlit.title('Calculator') 92 display = streamlit.container(border=True) 93 94 column_1, column_2, column_3, operations = streamlit.columns(4) 95 add_buttons_to_column_1(column_1, display) 96 add_buttons_to_column_2(column_2, display) 97 add_buttons_to_column_3(column_3, display) 98 add_buttons_to_column_4(operations)the test passes.
I go to the browser to test the numbers and they show up in the box, with one problem - every time I press a button it shows a new number.
close the project
I close
test_streamlit_calculator.py,streamlit_calculator.pyin the editorI 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
calculatorcd ..the terminal is my friend, and shows
.../pumping_pythonI am back in the
pumping_pythondirectoryI 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
calculatorcd ..the terminal is my friend, and shows
.../pumping_pythonI am back in the
pumping_pythondirectory
review
I made a website using Streamlit with a title, display and buttons
code from the chapter
what is next?
You now know how to:
build a website with streamlit
how to test the parts of the website
Would you like to continue with adding buttons to the calculator?
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