how to make a calculator 10: part 3
open the project
I `change directory`_ to the
calculatorfolder_cd calculatorI make a new test file_ for the Streamlit_ website
touch tests/test_streamlit_calculator.pyI add streamlit_ to the
requirements.txtfile_echo "streamlit" >> requirements.txtStreamlit_ is a Python_ library that is used for making websites, it is not part of `The Python Standard Library`_
I install the `Python packages`_ that I wrote in the requirements file_
uv add --requirement requirements.txtthe terminal shows it installed the `Python packages`_
I use
pytest-watcherto run the testsuv run pytest-watcher . --nowthe terminal_ shows
rootdir: .../pumping_python/calculator configfile: pyproject.toml collected 8 items tests/test_calculator.py ..... [ 62%] tests/test_calculator_website.py ... [100%] ======================== 5 passed in X.YZs =========================
test_streamlit_calculator_state
streamlit_ has a `session state object`_ that I can use to keep values in between button presses. They work the same as class attributes and they are dictionaries - I can add key-value pairs to them
RED: make it fail
I add a test for the `session state object`_, I want it to hold the number when I click the buttons, in test_streamlit_calculator.py
46 def test_streamlit_calculator_operations_buttons(self):
47 for key in ('/', 'X', r'\-', r'\+', '=', 'C', 'AC'):
48 with self.subTest(key=key):
49 self.assertEqual(
50 self.tester.button(key).proto.type,
51 'primary'
52 )
53
54 def test_streamlit_calculator_state(self):
55 self.assertIsNone(self.tester.session_state['number'])
56
57
58# Exceptions seen
KeyError: 'st.session_state has no key "number".
Did you forget to initialize it?
More info: https://docs.streamlit.io/develop/concepts/architecture/session-state#initialization'
GREEN: make it pass
I use the setdefault method to add a key that will hold the numbers to show in the Calculator, in
streamlit_calculator.py83def main(): 84 streamlit.title('Calculator') 85 streamlit.session_state.setdefault('number', '0') 86 add_buttons() 87 88 89if __name__ == '__main__': 90 main()the terminal_ shows AssertionError
AssertionError: 0 is not NoneI change the assertIsNone_ to assertEqual_ in
test_streamlit_calculator.py107 def test_streamlit_calculator_state(self): 108 self.assertEqual(self.tester.session_state['number'], '0')the test passes
REFACTOR: make it better
I add the `session state object`_ to the
showfunction so that when a button is clicked, it will be added to the `session state object`_ then shown in the disply4def show(display, number): 5 # display.write(number) 6 streamlit.session_state['number'] += number 7 display.write(streamlit.session_state['number'])the test passes
I remove the commented line
4def show(display, number): 5 streamlit.session_state['number'] += number 6 display.write(streamlit.session_state['number'])I add an assertion to test what happens to the `session state object`_ when I press a button for a number
54 def test_streamlit_calculator_state(self): 55 self.assertEqual(self.tester.session_state['number'], '0') 56 self.tester.button('1').click().run() 57 self.assertEqual(self.tester.session_state['number'], '0').click()presses the button.run()runs the program -streamlit_calculator.py, which is what happens when a person uses the application in the browser
the terminal_ shows AssertionError
AssertionError: '01' != '0'this is a problem
I change the expectation to match
57 self.assertEqual(self.tester.session_state['number'], '01')the test passes
I add more button clicks
57 self.assertEqual(self.tester.session_state['number'], '01') 58 self.tester.button('2').click().run() 59 self.tester.button('3').click().run() 60 self.tester.button('4').click().run() 61 self.assertEqual(self.tester.session_state['number'], '01')the terminal_ shows AssertionError
AssertionError: '01234' != '01'I change the expectation to match
61 self.assertEqual( 62 self.tester.session_state['number'], '01234' 63 )the test passes
I refresh the browser and click on all the numbers
the calculator keeps numbers when I press the buttons and the number starts with
0like the testI want the calculator to remove
0when it is the first number after I click on other numbers. I change the assertions54 def test_streamlit_calculator_state(self): 55 self.assertEqual(self.tester.session_state['number'], '0') 56 self.tester.button('1').click().run() 57 self.assertEqual(self.tester.session_state['number'], '1') 58 self.tester.button('2').click().run() 59 self.tester.button('3').click().run() 60 self.tester.button('4').click().run() 61 self.assertEqual( 62 self.tester.session_state['number'], '1234' 63 )the terminal_ shows AssertionError
AssertionError: '01' != '1'I add a condition to the
showfunction instreamlit_calculator.py4def show(display, number): 5 if streamlit.session_state['number'] == '0': 6 streamlit.session_state['number'] = number 7 else: 8 streamlit.session_state['number'] += number 9 display.write(streamlit.session_state['number'])the test passes
I refresh the browser and try all the numbers
the number no longer starts with
0I import the `random module`_ to use random numbers for the test, in
test_streamlit_calculator.py1import random 2import streamlit.testing.v1 3import unittest 4 5 6class TestStreamlitCalculator(unittest.TestCase):I use it in test_streamlit_calculator_state
55 def test_streamlit_calculator_state(self): 56 numbers = '0123456789' 57 self.assertEqual(self.tester.session_state['number'], '0') 58 # self.tester.button('1').click().run() 59 x = random.choice(numbers) 60 self.tester.button(x).click().run() 61 self.assertEqual(self.tester.session_state['number'], '1')numbers = '0123456789'makes a string_ with ten numbersx = random.choice(numbers)picks a random number from the ten numbers and pointsxto it
the terminal_ shows. AssertionError
AssertionError: 'X' != '1'where
Xis a random numberI change the expectation
55 def test_streamlit_calculator_state(self): 56 numbers = '0123456789' 57 self.assertEqual(self.tester.session_state['number'], '0') 58 # self.tester.button('1').click().run() 59 x = random.choice(numbers) 60 self.tester.button(x).click().run() 61 self.assertEqual(self.tester.session_state['number'], x) 62 self.tester.button('2').click().run() 63 self.tester.button('3').click().run() 64 self.tester.button('4').click().run() 65 self.assertEqual( 66 self.tester.session_state['number'], '1234' 67 )when
xis not1, the terminal_ shows AssertionErrorAssertionError: 'X234' != '1234'I use random numbers for the other button presses
61 self.assertEqual(self.tester.session_state['number'], x) 62 # self.tester.button('2').click().run() 63 # self.tester.button('3').click().run() 64 # self.tester.button('4').click().run() 65 y = random.choice(numbers) 66 self.tester.button(y).click().run() 67 z = random.choice(numbers) 68 self.tester.button(z).click().run() 69 a = random.choice(numbers) 70 self.tester.button(a).click().run() 71 self.assertEqual( 72 self.tester.session_state['number'], '1234' 73 )the terminal_ shows AssertionError
AssertionError: 'XYZA' != '1234'I use the random numbers in the expectation of the assertion
124 self.assertEqual( 125 self.tester.session_state['number'], f'{x}{y}{z}{a}' 126 )when
xis0, the terminal_ shows AssertionErrorAssertionError: 'YZA' != '0YZA'
what is a while loop?
I can use a `while loop`_ to make sure that
xis never0, since thesession_state['number']is always0at the beginning. I add a `while statement`_ to test_streamlit_calculator_state55 def test_streamlit_calculator_state(self): 56 numbers = '0123456789' 57 self.assertEqual(self.tester.session_state['number'], '0') 58 # self.tester.button('1').click().run() 59 x = random.choice(numbers) 60 while x == '0': 61 x = random.choice(numbers) 62 else: 63 self.tester.button(x).click().run() 64 self.tester.button(x).click().run() 65 self.assertEqual(self.tester.session_state['number'], x)x = random.choice(numbers)pointsxto a random number from thenumbersvariableif
xis not equal to'0'it goes to the next blockelse: self.tester.button(x).click().run()if
xis equal to'0'it goes towhile x == '0':which makes a loop that will continue to run as long asxis equal to'0'inside the loop
x = random.choice(numbers)pointsxto a random number from thenumbersstring_ then checks again to see ifxis equal to'0'if
xis equal to0the loop runs againif
xis not equal to0in the loop, it leaves the loop and continues to the next blockelse: self.tester.button(x).click().run()
I use ctrl+s on the keyboard a few times and there are no more random failures
I add a for loop to test with a 10 digit number
74 self.assertEqual( 75 self.tester.session_state['number'], f'{x}{y}{z}{a}' 76 ) 77 for _ in range(0, 10): 78 a_random_number = random.choice(numbers) 79 ( 80 self.tester.button(a_random_number) 81 .click().run() 82 ) 83 x += a_random_number 84 self.assertEqual( 85 self.tester.session_state['number'], 86 x 87 ) 88 89 90# Exceptions seenfor _ in range(0, 10):uses_because I do not need a name for each number from the `range object`_ in the for loop since I do not use itthe terminal_ shows AssertionError
AssertionError: 'XYZABCDEFGHIJK' != 'XBCDEFGHIJK'the first number and the last 10 numbers are the same in the two strings_
I comment out the other lines
108 def test_streamlit_calculator_state(self): 109 numbers = '0123456789' 110 # self.assertEqual(self.tester.session_state['number'], '0') 111 # self.tester.button('1').click().run() 112 x = random.choice(numbers) 113 while x == '0': 114 x = random.choice(numbers) 115 else: 116 self.tester.button(x).click().run() 117 self.assertEqual(self.tester.session_state['number'], x) 118 # self.tester.button('2').click().run() 119 # self.tester.button('3').click().run() 120 # self.tester.button('4').click().run() 121 # y = random.choice(numbers) 122 # self.tester.button(y).click().run() 123 # z = random.choice(numbers) 124 # self.tester.button(z).click().run() 125 # a = random.choice(numbers) 126 # self.tester.button(a).click().run() 127 # self.assertEqual( 128 # self.tester.session_state['number'], f'{x}{y}{z}{a}' 129 # ) 130 for _ in range(0, 10):I use ctrl+s on the keyboard to run the tests a few times and the test is still green
I remove the commented lines
55 def test_streamlit_calculator_state(self): 56 numbers = '0123456789' 57 self.assertEqual(self.tester.session_state['number'], '0') 58 x = random.choice(numbers) 59 while x == '0': 60 x = random.choice(numbers) 61 else: 62 self.tester.button(x).click().run() 63 self.assertEqual(self.tester.session_state['number'], x) 64 for _ in range(0, 10): 65 a_random_number = random.choice(numbers) 66 ( 67 self.tester.button(a_random_number) 68 .click().run() 69 ) 70 x += a_random_number 71 self.assertEqual( 72 self.tester.session_state['number'], 73 x 74 )I use the
Rename Symbolfeature of the `Integrated Development Environment (IDE)`_ to changextoexpectation55 def test_streamlit_calculator_state(self): 56 numbers = '0123456789' 57 self.assertEqual(self.tester.session_state['number'], '0') 58 expectation = random.choice(numbers) 59 while expectation == '0': 60 expectation = random.choice(numbers) 61 else: 62 self.tester.button(expectation).click().run() 63 self.assertEqual( 64 self.tester.session_state['number'], 65 expectation 66 ) 67 for _ in range(0, 10): 68 a_random_number = random.choice(numbers) 69 ( 70 self.tester.button(a_random_number) 71 .click().run() 72 ) 73 expectation += a_random_number 74 self.assertEqual( 75 self.tester.session_state['number'], 76 expectation 77 ) 78 79 80# Exceptions seen
test_streamlit_calculator_w_decimals
RED: make it fail
I add a test for decimal numbers
74 self.assertEqual( 75 self.tester.session_state['number'], 76 expectation 77 ) 78 79 def test_streamlit_calculator_w_decimals(self): 80 self.tester.button('0').click().run() 81 self.tester.button('.').click().run() 82 self.tester.button('2').click().run() 83 self.tester.button('3').click().run() 84 self.tester.button('.').click().run() 85 self.tester.button('5').click().run() 86 self.tester.button('.').click().run() 87 self.tester.button('6').click().run() 88 self.tester.button('.').click().run() 89 self.tester.button('7').click().run() 90 self.tester.button('.').click().run() 91 self.tester.button('8').click().run() 92 self.tester.button('9').click().run() 93 self.assertEqual( 94 self.tester.session_state['number'], 95 '0.2356789' 96 ) 97 98 99# Exceptions seenthe terminal_ shows AssertionError
AssertionError: '.23.5.6.7.8.9' != '0.2356789'I refresh the browser and do the same thing
the Calculator allows me add many decimal points
GREEN: make it pass
I add a function for decimals
1import streamlit 2 3 4def handle_decimals(display, number): 5 if streamlit.session_state['number'].count('.') >= 1: 6 return None 7 else: 8 streamlit.session_state['number'] += number 9 display.write(streamlit.session_state['number']) 10 11 12def show(display, number):the terminal_ shows AssertionError
AssertionError: '.23.5.6.7.89' != '0.2356789'I change the
on_clickparameter for the.button to use the new function70 column_3.button( 71 '3', key='3', width='stretch', 72 on_click=show, args=[display, '3'], 73 ) 74 column_3.button( 75 '.', key='.', width='stretch', 76 on_click=handle_decimals, args=[display, '.'], 77 ) 78 79 operations.button( 80 '/', key='/', width='stretch', type='primary', 81 )the test passes
REFACTOR: make it better
I add the logical negation of the if statement to make it simpler
4def handle_decimals(display, number): 5 if streamlit.session_state['number'].count('.') > 0: 6 return None 7 # else: 8 if streamlit.session_state['number'].count('.') == 0: 9 streamlit.session_state['number'] += number 10 display.write(streamlit.session_state['number'])the test is still green
I remove the other if statement
4def handle_decimals(display, number): 5 if streamlit.session_state['number'].count('.') == 0: 6 streamlit.session_state['number'] += number 7 display.write(streamlit.session_state['number'])still green
if streamlit.session_state['number'].count('.') == 0:checks if.is insession_state['number']streamlit.session_state['number'].count('.')counts how many times.shows up insession_state['number']if
.is NOT insession_state['number'], then.gets added tosession_state['number']and the program runs the next linedisplay.write(streamlit.session_state['number'])if
.is insession_state['number'], then the program runs the next linedisplay.write(streamlit.session_state['number'])
I refresh the browser and try many decimals again
Yes! I can only do one decimal in a number
I add a for loop to test_streamlit_calculator_w_decimals in
test_streamlit_calculator.py79 def test_streamlit_calculator_w_decimals(self): 80 for button in ('0.23.5.6.7.8.9'): 81 ( 82 self.tester.button(button) 83 .click().run() 84 ) 85 86 self.tester.button('0').click().run() 87 self.tester.button('.').click().run() 88 self.tester.button('2').click().run() 89 self.tester.button('3').click().run()the terminal_ shows AssertionError
AssertionError: '0.235678902356789' != '0.2356789'I remove the other button presses
79 def test_streamlit_calculator_w_decimals(self): 80 for button in ('0.23.5.6.7.8.9'): 81 ( 82 self.tester.button(button) 83 .click().run() 84 ) 85 86 self.assertEqual( 87 self.tester.session_state['number'], 88 '0.2356789' 89 ) 90 91 92# Exceptions seenthe test passes
test_streamlit_calculator_backspace
I want to be able to remove the last digit of a number
RED: make it fail
I add a new test
86 self.assertEqual( 87 self.tester.session_state['number'], 88 '0.2356789' 89 ) 90 91 def test_streamlit_calculator_backspace(self): 92 self.tester.button('1').click().run() 93 self.tester.button('2').click().run() 94 self.tester.button('3').click().run() 95 self.tester.button('4').click().run() 96 self.tester.button('<-').click().run() 97 self.assertEqual( 98 self.tester.session_state['number'], 99 '123' 100 ) 101 102 103 # Exceptions seenthe terminal_ shows AssertionError
AssertionError: '1234' != '123'I try the same thing in the browser and it clears the screen
GREEN: make it pass
I add a function to
streamlit_calculator.py1import streamlit 2 3 4def backspace(display): 5 streamlit.session_state['number'] = \ 6 streamlit.session_state['number'][:-1] 7 display.write(streamlit.session_state['number']) 8 9 10def handle_decimals(display, number):the terminal_ still shows AssertionError
I add the
on_clickparameter to the<-button in theadd_buttonsfunction24def add_buttons(): 25 display = streamlit.container(border=True) 26 column_1, column_2, column_3, operations = streamlit.columns(4) 27 28 column_1.button( 29 '<-', key='<-', width='stretch', 30 on_click=backspace, args=[display], 31 ) 32 column_1.button( 33 '7', key='7', width='stretch', 34 on_click=show, args=[display, '7'], 35 )the test passes
REFACTOR: make it better
I add an `import statement`_ for
tests/test_calculator.pyintest_streamlit_calculator.py1import random 2import streamlit.testing.v1 3import tests.test_calculator 4import unittest 5 6 7class TestStreamlitCalculator(unittest.TestCase):I add a variable with a for loop for a random number in test_streamlit_calculator_backspace
92 def test_streamlit_calculator_backspace(self): 93 a_random_number = tests.test_calculator.a_random_number() 94 a_random_number = str(a_random_number) 95 96 for number in a_random_number: 97 self.tester.button(number).click().run() 98 self.tester.button('<-').click().run() 99 self.assertEqual( 100 self.tester.session_state['number'], 101 a_random_number 102 ) 103 104 self.tester.button('1').click().run() 105 self.tester.button('2').click().run() 106 self.tester.button('3').click().run()a_random_number = tests.test_calculator.a_random_number()uses thea_random_numberfunctiontest_calculator.pythat I made in how to use random numbersa_random_number = str(a_random_number)changes the random number to a string_ since all the operations of the calculator have been with strings_ so far
I use ctrl+s on the keyboard to run the test a few times because I am using random numbers and the terminal_ sometimes shows AssertionError
AssertionError: 'LMN.OPQRSTUVWXYZ' != 'LMN.OPQRSTUVWXYZA'the last number was not removed. When the random number is a negative number, the terminal_ shows KeyError
KeyError: '-'I need a test for the ‘+/-’ button
I add a while loop to make sure
a_random_numberis never negative92 def test_streamlit_calculator_backspace(self): 93 a_random_number = tests.test_calculator.a_random_number() 94 while a_random_number < 0: 95 a_random_number = tests.test_calculator.a_random_number() 96 a_random_number = str(a_random_number) 97 98 for number in a_random_number:I use ctrl+s on the keyboard to run the tests a few times and the terminal_ shows AssertionError
AssertionError: 'BCD.EFGHIJKLMNOP' != 'BCD.EFGHIJKLMNOPQ'I remove the while statement when I test the ‘+/-’ button
I change the expectation to remove the last digit from the random number
101 self.assertEqual( 102 self.tester.session_state['number'], 103 a_random_number[:-1] 104 )the terminal shows AssertionError
AssertionError: 'RS.TUVWXYZABCDEF123' != '123'because I have button presses after the test. I need a way to reset the numbers back to 0
I remove the other numbers
99 for number in a_random_number: 100 self.tester.button(number).click().run() 101 self.assertEqual( 102 self.tester.session_state['number'], 103 a_random_number[:-1] 104 ) 105 self.tester.button('<-').click().run() 106 self.assertEqual( 107 self.tester.session_state['number'], 108 '123' 109 )the terminal_ shows AssertionError
AssertionError: 'GHI.JKLMNOPQRS' != '123'I change the expectation
92 def test_streamlit_calculator_backspace(self): 93 a_random_number = tests.test_calculator.a_random_number() 94 while a_random_number < 0: 95 a_random_number = tests.test_calculator.a_random_number() 96 a_random_number = str(a_random_number) 97 98 for number in a_random_number: 99 self.tester.button(number).click().run() 100 self.tester.button('<-').click().run() 101 self.assertEqual( 102 self.tester.session_state['number'], 103 a_random_number[:-1] 104 ) 105 self.tester.button('<-').click().run() 106 self.assertEqual( 107 self.tester.session_state['number'], 108 a_random_number[:-2] 109 ) 110 111 112# Exceptions seenthe test passes
I go to the browser and type a few numbers, then click on
<-and it removes the last number. Fantastic!
test_streamlit_calculator_w_plus_minus
Nothing happens when I click +/- in the calculator. I want it to
change positive numbers to negative numbers
change negative numbers to positive numbers
RED: make it fail
I add a new test for the +/-
106 self.assertEqual(
107 self.tester.session_state['number'],
108 a_random_number[:-2]
109 )
110
111 def test_streamlit_calculator_w_plus_minus(self):
112 self.tester.button('9').click().run()
113 self.assertEqual(
114 self.tester.session_state['number'], '9'
115 )
116
117 self.tester.button('+/-').click().run()
118 self.assertEqual(
119 self.tester.session_state['number'], '-9'
120 )
121
122
123# Exceptions seen
the terminal_ shows AssertionError
AssertionError: '9' != '-9'
GREEN: make it pass
I add a function for the
+/-button instreamlit_calculator.py1import streamlit 2 3 4def plus_minus(display): 5 if not streamlit.session_state['number'].startswith('-'): 6 number = '-' + streamlit.session_state['number'] 7 8 streamlit.session_state['number'] = number 9 display.write(streamlit.session_state['number']) 10 11 12def backspace(display):the terminal_ still shows AssertionError
I add the
on_clickparameter for the+/-button in theadd_buttonsfunction50 column_1.button( 51 '1', key='1', width='stretch', 52 on_click=show, args=[display, '1'], 53 ) 54 column_1.button( 55 '+/-', key='+/-', width='stretch', 56 on_click=plus_minus, args=[display], 57 ) 58 59 column_2.button( 60 'C', key='C', width='stretch', type='primary', 61 ) 62 column_2.button( 63 '8', key='8', width='stretch', 64 on_click=show, args=[display, '8'], 65 )the test passes. I can turn a positive number to a negative one with the
+/-button.
REFACTOR: make it better
I add another button press and assertion to make sure I can turn a negative number to a positive one with the
+/-button, intest_streamlit_calculator.py117 self.tester.button('+/-').click().run() 118 self.assertEqual( 119 self.tester.session_state['number'], '-9' 120 ) 121 122 self.tester.button('+/-').click().run() 123 self.assertEqual( 124 self.tester.session_state['number'], '9' 125 ) 126 127 128# Exceptions seenthe terminal_ shows AssertionError
AssertionError: '-9' != '9'I add an if statement to the
plus_minusfunction intest_streamlit_calculator.py4def plus_minus(display): 5 if streamlit.session_state['number'].startswith('-'): 6 number = streamlit.session_state['number'][1:] 7 if not streamlit.session_state['number'].startswith('-'): 8 number = '-' + streamlit.session_state['number'] 9 10 streamlit.session_state['number'] = number 11 display.write(streamlit.session_state['number'])the test passes
I use else_ for the second if statement
the test is still green
I remove the commented line
4def plus_minus(display): 5 if streamlit.session_state['number'].startswith('-'): 6 number = streamlit.session_state['number'][1:] 7 else: 8 number = '-' + streamlit.session_state['number'] 9 10 streamlit.session_state['number'] = number 11 display.write(streamlit.session_state['number'])I add a variable to remove duplication in
test_streamlit_calculator.py111 def test_streamlit_calculator_w_plus_minus(self): 112 a_number = '9' 113 self.tester.button('1').click().run()I use the number in the assertions and button presses
111 def test_streamlit_calculator_w_plus_minus(self): 112 a_number = '9' 113 # self.tester.button('1').click().run() 114 self.tester.button(a_number).click().run() 115 self.assertEqual( 116 # self.tester.session_state['number'], '9' 117 self.tester.session_state['number'], a_number 118 ) 119 120 self.tester.button('+/-').click().run() 121 self.assertEqual( 122 # self.tester.session_state['number'], '-9' 123 self.tester.session_state['number'], f'-{a_number}' 124 ) 125 126 self.tester.button('+/-').click().run() 127 self.assertEqual( 128 # self.tester.session_state['number'], '9' 129 self.tester.session_state['number'], a_number 130 )the test is still green
I remove the commented lines
111 def test_streamlit_calculator_w_plus_minus(self): 112 a_number = '9' 113 self.tester.button(a_number).click().run() 114 self.assertEqual( 115 self.tester.session_state['number'], a_number 116 ) 117 118 self.tester.button('+/-').click().run() 119 self.assertEqual( 120 self.tester.session_state['number'], f'-{a_number}' 121 ) 122 123 self.tester.button('+/-').click().run() 124 self.assertEqual( 125 self.tester.session_state['number'], a_number 126 )I try a bigger number
111 def test_streamlit_calculator_w_plus_minus(self): 112 a_number = '96' 113 self.tester.button(a_number).click().run() 114 self.assertEqual( 115 self.tester.session_state['number'], a_number 116 )KeyError: '96'I need separate button presses for the two numbers
I add a for loop
111 def test_streamlit_calculator_w_plus_minus(self): 112 a_number = '96' 113 for number in a_number: 114 self.tester.button(number).click().run() 115 self.assertEqual( 116 self.tester.session_state['number'], a_number 117 )the test passes
I try a number with 10 digits
111 def test_streamlit_calculator_w_plus_minus(self): 112 a_number = '963.0258741'the terminal_ still shows green
I refresh the browser and try to make a negative number
when I click on the
+/-button it turns a positive number negativeI try the
+/-button again
the
-is removed from the number to make it a positive number. Progress!
The last 4 functions in
streamlit_calculator.py-plus_minus,backspace,handle_decimalsandshowlook the samedef function_name(display): statements display.write(streamlit.session_state['number'])I add
numberin the parentheses for theplus_minusfunction to make it have the same signature as theshowandhandle_decimalsfunctions4def plus_minus(display, number): 5 if streamlit.session_state['number'].startswith('-'): 6 number = streamlit.session_state['number'][1:] 7 else: 8 number = '-' + streamlit.session_state['number']the terminal_ shows AssertionError
AssertionError: '1234567890' != '-1234567890'and TypeError
TypeError: plus_minus() missing 1 required positional argument: 'number'I add a second argument to the
argsparameter for the+/-button in theadd_buttonsfunction50 column_1.button( 51 '1', key='1', width='stretch', 52 on_click=show, args=[display, '1'], 53 ) 54 column_1.button( 55 '+/-', key='+/-', width='stretch', 56 on_click=plus_minus, args=[display, '+/-'], 57 ) 58 59 column_2.button( 60 'C', key='C', width='stretch', type='primary', 61 )the test is green again
I add
numberin the parentheses for thebackspacefunction to make it have the same signature as theshow,handle_decimalsandplus_minusfunctionsthe terminal_ shows AssertionError
AssertionError: 'RST.UVWXYZABCDEFGH' != 'RST.UVWXYZABCDEFG'it also shows TypeError
TypeError: backspace() missing 1 required positional argument: 'number'I add a second argument to the
argsparameter for the<-button in theadd_buttonsfunctionthe test is green again
I add a function to show the state
4import streamlit 5 6 7def show_state(display): 8 display.write(streamlit.session_state['number']) 9 10 11def plus_minus(display, number):I use the new function in the
plus_minusfunction8def plus_minus(display, number): 9 if streamlit.session_state['number'].startswith('-'): 10 number = streamlit.session_state['number'][1:] 11 else: 12 number = '-' + streamlit.session_state['number'] 13 14 streamlit.session_state['number'] = number 15 # display.write(streamlit.session_state['number']) 16 show_state(display)the test is still green
I remove the commented line
8def plus_minus(display, number): 9 if streamlit.session_state['number'].startswith('-'): 10 number = streamlit.session_state['number'][1:] 11 else: 12 number = '-' + streamlit.session_state['number'] 13 14 streamlit.session_state['number'] = number 15 show_state(display)I use the new function in the
backspacefunction18def backspace(display, number): 19 streamlit.session_state['number'] = \ 20 streamlit.session_state['number'][:-1] 21 # display.write(streamlit.session_state['number']) 22 show_state(display)the test is still green
I remove the commented line
18def backspace(display, number): 19 streamlit.session_state['number'] = \ 20 streamlit.session_state['number'][:-1] 21 show_state(display)I use the new function in the
handle_decimalsfunction24def handle_decimals(display, number): 25 if streamlit.session_state['number'].count('.') == 0: 26 streamlit.session_state['number'] += number 27 # display.write(streamlit.session_state['number']) 28 show_state(display)the test is still green
I remove the commented line
24def handle_decimals(display, number): 25 if streamlit.session_state['number'].count('.') == 0: 26 streamlit.session_state['number'] += number 27 show_state(display)I use the new function in the
showfunction30def show(display, number): 31 if streamlit.session_state['number'] == '0': 32 streamlit.session_state['number'] = number 33 else: 34 streamlit.session_state['number'] += number 35 # display.write(streamlit.session_state['number']) 36 show_state(display)the test is still green
I remove the commented line
30def show(display, number): 31 if streamlit.session_state['number'] == '0': 32 streamlit.session_state['number'] = number 33 else: 34 streamlit.session_state['number'] += number 35 show_state(display)I add a new function for adding the number to the `session state object`_
24def handle_decimals(display, number): 25 if streamlit.session_state['number'].count('.') == 0: 26 streamlit.session_state['number'] += number 27 show_state(display) 28 29 30def add_number_to_state(number): 31 if streamlit.session_state['number'] == '0': 32 streamlit.session_state['number'] = number 33 else: 34 streamlit.session_state['number'] += number 35 36 37def show(display, number):I add a function to handle all the button clicks
37def show(display, number): 38 if streamlit.session_state['number'] == '0': 39 streamlit.session_state['number'] = number 40 else: 41 streamlit.session_state['number'] += number 42 show_state(display) 43 44 45def on_click(function, display, value): 46 function(value) 47 show_state(display) 48 49 50def add_buttons():I try the
on_clickfunction with the7button in theadd_buttonsfunction49def add_buttons(): 50 display = streamlit.container(border=True) 51 column_1, column_2, column_3, operations = streamlit.columns(4) 52 53 column_1.button( 54 '<-', key='<-', width='stretch', 55 on_click=backspace, args=[display, '<-'], 56 ) 57 column_1.button( 58 '7', key='7', width='stretch', on_click=on_click, 59 # on_click=show, args=[display, '7'], 60 args=[add_number_to_state, display, '7'], 61 ) 62 column_1.button( 63 '4', key='4', width='stretch', 64 on_click=show, args=[display, '4'], 65 )the test is still green! Yes!!
I remove the commented line and use the
on_clickfunction with all the other number buttons in theadd_buttonsfunction50def add_buttons(): 51 display = streamlit.container(border=True) 52 column_1, column_2, column_3, operations = streamlit.columns(4) 53 54 column_1.button( 55 '<-', key='<-', width='stretch', 56 on_click=backspace, args=[display, '<-'], 57 ) 58 column_1.button( 59 '7', key='7', width='stretch', on_click=on_click, 60 args=[add_number_to_state, display, '7'], 61 ) 62 column_1.button( 63 '4', key='4', width='stretch', on_click=on_click, 64 args=[add_number_to_state, display, '4'], 65 ) 66 column_1.button( 67 '1', key='1', width='stretch', on_click=on_click, 68 args=[add_number_to_state, display, '1'], 69 ) 70 column_1.button( 71 '+/-', key='+/-', width='stretch', 72 on_click=plus_minus, args=[display, '+/-'], 73 ) 74 75 column_2.button( 76 'C', key='C', width='stretch', type='primary', 77 ) 78 column_2.button( 79 '8', key='8', width='stretch', on_click=on_click, 80 args=[add_number_to_state, display, '8'], 81 ) 82 column_2.button( 83 '5', key='5', width='stretch', on_click=on_click, 84 args=[add_number_to_state, display, '5'], 85 ) 86 column_2.button( 87 '2', key='2', width='stretch', on_click=on_click, 88 args=[add_number_to_state, display, '2'], 89 ) 90 column_2.button( 91 '0', key='0', width='stretch', on_click=on_click, 92 args=[add_number_to_state, display, '0'], 93 ) 94 95 column_3.button( 96 'AC', key='AC', width='stretch', type='primary', 97 ) 98 column_3.button( 99 '9', key='9', width='stretch', on_click=on_click, 100 args=[add_number_to_state, display, '9'], 101 ) 102 column_3.button( 103 '6', key='6', width='stretch', on_click=on_click, 104 args=[add_number_to_state, display, '6'], 105 ) 106 column_3.button( 107 '3', key='3', width='stretch', on_click=on_click, 108 args=[add_number_to_state, display, '3'], 109 ) 110 column_3.button( 111 '.', key='.', width='stretch', 112 on_click=handle_decimals, args=[display, '.'], 113 ) 114 115 operations.button( 116 '/', key='/', width='stretch', type='primary', 117 ) 118 operations.button( 119 'X', key='X', width='stretch', type='primary', 120 ) 121 operations.button( 122 r'\-', key=r'\-', width='stretch', type='primary', 123 ) 124 operations.button( 125 r'\+', key=r'\+', width='stretch', type='primary', 126 ) 127 operations.button( 128 '=', key='=', width='stretch', type='primary', 129 ) 130 131def main():still green
I remove the
showfunction30def add_number_to_state(number): 31 if streamlit.session_state['number'] == '0': 32 streamlit.session_state['number'] = number 33 else: 34 streamlit.session_state['number'] += number 35 36 37def on_click(function, display, number):
I add a new function for decimals
24def handle_decimals(display, number): 25 if streamlit.session_state['number'].count('.') == 0: 26 streamlit.session_state['number'] += number 27 show_state(display) 28 29 30def add_decimal(): 31 if streamlit.session_state['number'].count('.') == 0: 32 streamlit.session_state['number'] += '.' 33 34 35def add_number_to_state(number):I try the new function with the
.button in theadd_buttonsfunction103 column_3.button( 104 '3', key='3', width='stretch', on_click=on_click, 105 args=[add_number_to_state, display, '3'], 106 ) 107 column_3.button( 108 '.', key='.', width='stretch', on_click=on_click, 109 # on_click=handle_decimals, args=[display, '.'], 110 args=[add_decimal, display], 111 ) 112 113 operations.button( 114 '/', key='/', width='stretch', type='primary', 115 )FAILED test_streamlit_calculator_backspace - KeyError: 'I' FAILED test_streamlit_calculator_w_decimals - KeyError: '2'and shows TypeError
TypeError: on_click() missing 1 required positional argument: 'value'I make the
valueparameter a choice in theon_clickfunction42def on_click(function, display, *value): 43 function(*value) 44 show_state(display)the test passes
I remove the commented line
107 column_3.button( 108 '.', key='.', width='stretch', on_click=on_click, 109 args=[add_decimal, display], 110 )I remove the
handle_decimalsfunction18def backspace(display, number): 19 streamlit.session_state['number'] = \ 20 streamlit.session_state['number'][:-1] 21 show_state(display) 22 23 24def add_decimal(): 25 if streamlit.session_state['number'].count('.') == 0: 26 streamlit.session_state['number'] += '.'
I change the
on_clickandargsparameters for the<-button in theadd_buttonsfunction41def add_buttons(): 42 display = streamlit.container(border=True) 43 column_1, column_2, column_3, operations = streamlit.columns(4) 44 45 column_1.button( 46 '<-', key='<-', width='stretch', on_click=on_click, 47 # on_click=backspace, args=[display, '<-'], 48 args=[backspace, display], 49 ) 50 column_1.button( 51 '7', key='7', width='stretch', on_click=on_click, 52 args=[add_number_to_state, display, '7'], 53 )the terminal_ shows AssertionError
AssertionError: 'MN.OPQRSTUVWXYZABC' != 'MN.OPQRSTUVWXYZAB'and TypeError
TypeError: backspace() missing 2 required positional arguments: 'display' and 'number'I change the
backspacefunction18# def backspace(display, number): 19def backspace(): 20 streamlit.session_state['number'] = \ 21 streamlit.session_state['number'][:-1] 22 # show_state(display)the test passes
I remove the commented lines in the
backspacefunction18def backspace(): 19 streamlit.session_state['number'] = \ 20 streamlit.session_state['number'][:-1] 21 22 23def add_decimal():
I change the
on_clickandargsparameters for the+/-button in theadd_buttonsfunction57 column_1.button( 58 '1', key='1', width='stretch', on_click=on_click, 59 args=[add_number_to_state, display, '1'], 60 ) 61 column_1.button( 62 '+/-', key='+/-', width='stretch', on_click=on_click, 63 # on_click=plus_minus, args=[display, '+/-'], 64 args=[plus_minus, display] 65 ) 66 67 column_2.button( 68 'C', key='C', width='stretch', type='primary', 69 )the terminal_ shows AssertionError
AssertionError: '1234567890' != '-1234567890'and shows TypeError
TypeError: plus_minus() missing 2 required positional arguments: 'display' and 'number'I change the
plus_minusfunction8# def plus_minus(display, number): 9def plus_minus(): 10 if streamlit.session_state['number'].startswith('-'): 11 number = streamlit.session_state['number'][1:] 12 if not streamlit.session_state['number'].startswith('-'): 13 number = '-' + streamlit.session_state['number'] 14 15 streamlit.session_state['number'] = number 16 # show_state(display)the test passes
I remove the commented lines in the
plus_minusfunction8def plus_minus(): 9 if streamlit.session_state['number'].startswith('-'): 10 number = streamlit.session_state['number'][1:] 11 if not streamlit.session_state['number'].startswith('-'): 12 number = '-' + streamlit.session_state['number'] 13 streamlit.session_state['number'] = number 14 15 16def backspace():I remove the commented line from the
+/-button in theadd_buttonsfunction42 column_1.button( 43 '<-', key='<-', width='stretch', on_click=on_click, 44 args=[backspace, display], 45 )
That was a lot especially when all the numbers have to change. I add a function for the buttons in the first column
33def on_click(function, display, *value): 34 function(*value) 35 show_state(display) 36 37 38def add_buttons_to_column_1(column_1, display): 39 column_1.button( 40 '<-', key='<-', width='stretch', on_click=on_click, 41 args=[backspace, display], 42 ) 43 column_1.button( 44 '7', key='7', width='stretch', on_click=on_click, 45 args=[add_number_to_state, display, '7'], 46 ) 47 column_1.button( 48 '4', key='4', width='stretch', on_click=on_click, 49 args=[add_number_to_state, display, '4'], 50 ) 51 column_1.button( 52 '1', key='1', width='stretch', on_click=on_click, 53 args=[add_number_to_state, display, '1'], 54 ) 55 column_1.button( 56 '+/-', key='+/-', width='stretch', on_click=on_click, 57 args=[plus_minus, display] 58 ) 59 60 61def add_buttons():I call the new function in the
add_buttonsfunction61def add_buttons(): 62 display = streamlit.container(border=True) 63 column_1, column_2, column_3, operations = streamlit.columns(4) 64 65 add_buttons_to_column_1(column_1, display) 66 # column_1.button( 67 # '<-', key='<-', width='stretch', on_click=on_click, 68 # args=[backspace, display], 69 # ) 70 # column_1.button( 71 # '7', key='7', width='stretch', on_click=on_click, 72 # args=[add_number_to_state, display, '7'], 73 # ) 74 # column_1.button( 75 # '4', key='4', width='stretch', on_click=on_click, 76 # args=[add_number_to_state, display, '4'], 77 # ) 78 # column_1.button( 79 # '1', key='1', width='stretch', on_click=on_click, 80 # args=[add_number_to_state, display, '1'], 81 # ) 82 # column_1.button( 83 # '+/-', key='+/-', width='stretch', on_click=on_click, 84 # args=[plus_minus, display] 85 # ) 86 87 column_2.button( 88 'C', key='C', width='stretch', type='primary', 89 )the tests are still green
I remove the commented lines
61def add_buttons(): 62 display = streamlit.container(border=True) 63 column_1, column_2, column_3, operations = streamlit.columns(4) 64 65 add_buttons_to_column_1(column_1, display) 66 67 column_2.button( 68 'C', key='C', width='stretch', type='primary', 69 )I add a function for adding buttons to the second column
38def add_buttons_to_column_1(column_1, display): 39 column_1.button( 40 '<-', key='<-', width='stretch', on_click=on_click, 41 args=[backspace, display], 42 ) 43 column_1.button( 44 '7', key='7', width='stretch', on_click=on_click, 45 args=[add_number_to_state, display, '7'], 46 ) 47 column_1.button( 48 '4', key='4', width='stretch', on_click=on_click, 49 args=[add_number_to_state, display, '4'], 50 ) 51 column_1.button( 52 '1', key='1', width='stretch', on_click=on_click, 53 args=[add_number_to_state, display, '1'], 54 ) 55 column_1.button( 56 '+/-', key='+/-', width='stretch', on_click=on_click, 57 args=[plus_minus, display] 58 ) 59 60 61def add_buttons_to_column_2(column_2, display): 62 column_2.button( 63 'C', key='C', width='stretch', type='primary', 64 ) 65 column_2.button( 66 '8', key='8', width='stretch', on_click=on_click, 67 args=[add_number_to_state, display, '8'], 68 ) 69 column_2.button( 70 '5', key='5', width='stretch', on_click=on_click, 71 args=[add_number_to_state, display, '5'], 72 ) 73 column_2.button( 74 '2', key='2', width='stretch', on_click=on_click, 75 args=[add_number_to_state, display, '2'], 76 ) 77 column_2.button( 78 '0', key='0', width='stretch', on_click=on_click, 79 args=[add_number_to_state, display, '0'], 80 ) 81 82 83def add_buttons():I add a call to
add_buttons_to_column_2in theadd_buttonsfunction83def add_buttons(): 84 display = streamlit.container(border=True) 85 column_1, column_2, column_3, operations = streamlit.columns(4) 86 87 add_buttons_to_column_1(column_1, display) 88 add_buttons_to_column_2(column_2, display) 89 90 # column_2.button( 91 # 'C', key='C', width='stretch', type='primary', 92 # ) 93 # column_2.button( 94 # '8', key='8', width='stretch', on_click=on_click, 95 # args=[add_number_to_state, display, '8'], 96 # ) 97 # column_2.button( 98 # '5', key='5', width='stretch', on_click=on_click, 99 # args=[add_number_to_state, display, '5'], 100 # ) 101 # column_2.button( 102 # '2', key='2', width='stretch', on_click=on_click, 103 # args=[add_number_to_state, display, '2'], 104 # ) 105 # column_2.button( 106 # '0', key='0', width='stretch', on_click=on_click, 107 # args=[add_number_to_state, display, '0'], 108 # ) 109 110 column_3.button( 111 'AC', key='AC', width='stretch', type='primary', 112 )still green
I remove the commented lines
83def add_buttons(): 84 display = streamlit.container(border=True) 85 column_1, column_2, column_3, operations = streamlit.columns(4) 86 87 add_buttons_to_column_1(column_1, display) 88 add_buttons_to_column_2(column_2, display) 89 90 column_3.button( 91 'AC', key='AC', width='stretch', type='primary', 92 )I add a function to add buttons to the third column
83def add_buttons_to_column_3(column_3, display): 84 column_3.button( 85 'AC', key='AC', width='stretch', type='primary', 86 ) 87 column_3.button( 88 '9', key='9', width='stretch', on_click=on_click, 89 args=[add_number_to_state, display, '9'], 90 ) 91 column_3.button( 92 '6', key='6', width='stretch', on_click=on_click, 93 args=[add_number_to_state, display, '6'], 94 ) 95 column_3.button( 96 '3', key='3', width='stretch', on_click=on_click, 97 args=[add_number_to_state, display, '3'], 98 ) 99 column_3.button( 100 '.', key='.', width='stretch', on_click=on_click, 101 args=[add_decimal, display], 102 ) 103 104 105def add_buttons():I use the
add_buttons_to_column_3function in theadd_columnsfunction105def add_buttons(): 106 display = streamlit.container(border=True) 107 column_1, column_2, column_3, operations = streamlit.columns(4) 108 109 add_buttons_to_column_1(column_1, display) 110 add_buttons_to_column_2(column_2, display) 111 add_buttons_to_column_3(column_3, display) 112 113 # column_3.button( 114 # 'AC', key='AC', width='stretch', type='primary', 115 # ) 116 # column_3.button( 117 # '9', key='9', width='stretch', on_click=on_click, 118 # args=[add_number_to_state, display, '9'], 119 # ) 120 # column_3.button( 121 # '6', key='6', width='stretch', on_click=on_click, 122 # args=[add_number_to_state, display, '6'], 123 # ) 124 # column_3.button( 125 # '3', key='3', width='stretch', on_click=on_click, 126 # args=[add_number_to_state, display, '3'], 127 # ) 128 # column_3.button( 129 # '.', key='.', width='stretch', on_click=on_click, 130 # args=[add_decimal, display], 131 # ) 132 133 operations.button( 134 '/', key='/', width='stretch', type='primary', 135 )green
I remove the commented lines
105def add_buttons(): 106 display = streamlit.container(border=True) 107 column_1, column_2, column_3, operations = streamlit.columns(4) 108 109 add_buttons_to_column_1(column_1, display) 110 add_buttons_to_column_2(column_2, display) 111 add_buttons_to_column_3(column_3, display) 112 113 operations.button( 114 '/', key='/', width='stretch', type='primary', 115 )I add a function for the operations column
105def add_buttons_to_column_4(column_4): 106 column_4.button( 107 '/', key='/', width='stretch', type='primary', 108 ) 109 column_4.button( 110 'X', key='X', width='stretch', type='primary', 111 ) 112 column_4.button( 113 r'\-', key=r'\-', width='stretch', type='primary', 114 ) 115 column_4.button( 116 r'\+', key=r'\+', width='stretch', type='primary', 117 ) 118 column_4.button( 119 '=', key='=', width='stretch', type='primary', 120 ) 121 122 123def add_buttons():I use the
add_buttons_to_column_4function in theadd_buttonsfunctions123def add_buttons(): 124 display = streamlit.container(border=True) 125 column_1, column_2, column_3, operations = streamlit.columns(4) 126 127 add_buttons_to_column_1(column_1, display) 128 add_buttons_to_column_2(column_2, display) 129 add_buttons_to_column_3(column_3, display) 130 add_buttons_to_column_4(operations) 131 132 # operations.button( 133 # '/', key='/', width='stretch', type='primary', 134 # ) 135 # operations.button( 136 # 'X', key='X', width='stretch', type='primary', 137 # ) 138 # operations.button( 139 # r'\-', key=r'\-', width='stretch', type='primary', 140 # ) 141 # operations.button( 142 # r'\+', key=r'\+', width='stretch', type='primary', 143 # ) 144 # operations.button( 145 # '=', key='=', width='stretch', type='primary', 146 # ) 147 148def main():still green
I remove the commented lines
123def add_buttons(): 124 display = streamlit.container(border=True) 125 column_1, column_2, column_3, operations = streamlit.columns(4) 126 127 add_buttons_to_column_1(column_1, display) 128 add_buttons_to_column_2(column_2, display) 129 add_buttons_to_column_3(column_3, display) 130 add_buttons_to_column_4(operations) 131 132 133def main():okay! Enough playing, time to do the operations.
close the project
I close all files
I click in the terminal_, then use q to leave the tests
I `change directory`_ to the parent
cd ..
review
I now have three different versions of the same calculator:
Pure Python (chapters 1–8)
Flask website (chapter 9)
Streamlit web app (chapter 10) — the fastest and most beautiful version
The core calculator code never changed. All my tests still protect it. This is the real power of Test-Driven Development.
code from the chapter
what is next?
You have completed an amazing journey from pure functions to real web applications!
You now know how to:
Build programs with Test-Driven Development
Turn them into Flask websites
Turn them into beautiful Streamlit apps
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