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
calculatorfoldercd calculatorI make a new test file for the Streamlit website
touch tests/test_streamlit_calculator.pyI add streamlit to the
requirements.txtfileecho "streamlit" >> requirements.txtStreamlit 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.txtcat requirements.txtthe terminal is my friend, and shows
pytest pytest-watcher flask streamlitI install the Python packages that I wrote in the requirements file
uv add --requirement requirements.txtthe terminal shows that it installed the Python packages
I use
pytest-watcherto run the testsuv run pytest-watcher . --nowthe 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.pyfrom thetestsfolder in the editorI add a new test in
test_streamlit_calculator.py1import 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 definedI 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
I add an import statement at the top of the file
1import streamlit 2import unittest 3 4 5class TestStreamlitCalculator(unittest.TestCase):the terminal is my friend, and shows AttributeError
AttributeError: module 'streamlit' has no attribute 'testing'I add AttributeError to the list of Exceptions seen
13# Exceptions seen 14# NameError 15# AttributeErrorI add more to the import statement
1import streamlit.testing 2import unittestthe terminal is my friend, and shows AttributeError
AttributeError: module 'streamlit.testing' has no attribute 'v1'I add
v1to the import statement1import streamlit.testing.v1 2import unittestthe test passes
streamlit.testing.v1.AppTest.from_filetests the website I am making with streamlitAppTest is a class from
v1intestingin the streamlit library.from_fileuses the from_file method to run the Python module that I use to make the application
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.pyis empty7 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# AssertionErrorI make a new file in the
srcfolder and call itstreamlit_calculator.pyI open
streamlit_calculator.pyin the editorI add code to make a streamlit application with a title, in
streamlit_calculator.py1import 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
mainfunction when the module gets called as a script4def 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__':callsmain()only whensrc/streamlit_calculator.pygets called like a script, for examplein the terminal
python3 src/streamlit_calculator.pyor in
test_streamlit_calculator.pytester = 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.py12 self.assertEqual(tester.title[0], 'Calculator')the terminal is my friend, and shows AssertionError
AssertionError: Title(tag='h1') != 'Calculator'I use the
valueattribute of theTitleclass7 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
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:8501it might also show a dialog box like this, and I click on
Open in Browser
or I use ctrl/option on the keyboard and click on
http://localhost:8501with the mouse to open the browser and it shows
Success!
I click the 3 dots by
Deployon the right hand side
I click on
SettingsI click the check marks by
Run on saveandWide modeto make sure the website changes as I make changes to the code
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
childrenattribute19 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 definedI change the
Titleobject totester.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
mainfunction instreamlit_calculator.py4def 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.py19 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 definedI 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 dictionary19 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] )-
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 expectation20 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 seenthe 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# SyntaxErrorI change the assertion to use the
protoattribute since it looks like a dictionary20 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_containerattribute instead20 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_sizeattribute directly20 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.maxDiff14 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 seenI add a variable for the
flex_containerobject14 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_containerobject24 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_containerobject24 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_containerobject24 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 seenthe 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 - SMALLdirection- 1 - VERTICALjustify- 1 - JUSTIFY_STARTalign- 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.py24 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 seenthe terminal is my friend, and shows AssertionError
AssertionError: False is not trueI add a border to the container in
streamlit_calculator.py4def main(): 5 streamlit.title('Calculator') 6 streamlit.container(border=True)the test passes
I go to the browser and click refresh
there is a box under the
Calculatortitle
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
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?