telephone with separation
The text function in the telephone project was written in test_telephone.py. I want to move it to telephone.py in the src folder so that I can keep the tests and solution separate.
preview
I have these tests by the end of the chapter
open the project
I name this project
telephoneI open a terminal
I use uv to make a directory for the project and initialize it
uv init telephonethe terminal shows
Initialized project `telephone` at `.../pumping_python/telephone`then goes back to the command line.
I change directory to the project
cd telephonethe terminal shows I am in the
telephonefolder.../pumping_python/telephoneI make a directory for the tests
mkdir teststhe terminal goes back to the command line.
I make the
testsdirectory a Python packageDanger
use 2 underscores (__) before and after
initfor__init__.pynot_init_.pytouch tests/__init__.pyNew-Item tests/__init__.pythe terminal goes back to the command line.
I use the mv program to change the name of
main.pytotest_telephone.pyand move it to thetestsfoldermv main.py tests/test_telephone.pyMove-Item main.py tests/test_telephone.pythe terminal goes back to the command line.
I open
test_telephone.pyI delete all the text then add the first failing test to
test_telephone.py1def test_failure(): 2 assert False is TrueI go back to the terminal to make a requirements file for the Python packages I need
echo "pytest" > requirements.txtthe terminal goes back to the command line.
I add pytest-watcher to the requirements file
echo "pytest-watcher" >> requirements.txtthe terminal goes back to the command line.
I use uv to install pytest-watcher with the requirements file
uv add --requirement requirements.txtthe terminal shows that it installed pytest-watcher and its dependencies.
I add the new files and folders to git for tracking
git add .the terminal goes back to the command line.
I add a git commit message
git commit --all --message 'setup project'the terminal shows
[main (root-commit) a0b12c3] setup project 8 files changed, X insertions(+) create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 tests/__init__.py create mode 100644 tests/test_telephone.py create mode 100644 uv.lockthen goes back to the command line.
I use pytest-watcher to run the tests automatically
uv run pytest-watcher . --nowthe terminal is my friend, and shows AssertionError
======================== FAILURES ======================== ______________________ test_failure ______________________ def test_failure(): > assert False is True E assert False is True test_telephone.py:2: AssertionError ================ short test summary info ================= FAILED test_telephone.py::test_failure - assert False is True =================== 1 failed in X.YZs ====================if the terminal does not show the same error, then check
if your
tests/__init__.pyhas two underscores (__) before and afterinitfor__init__.pynot_init_.pyif you ran
echo "pytest-watcher" >> requirements.txt, to addpytest-watcherto the requirements file
and try
uv run pytest-watcher . --nowagainI add AssertionError to the list of Exceptions seen in
test_telephone.py1def test_failure(): 2 assert False is True 3 4 5# Exceptions seen 6# AssertionErrorI change True to False in the assertion
1def test_failure(): 2 # assert False is True 3 assert False is False 4 5 6# Exceptions seen 7# AssertionErrorthe test passes.
separate and equal
So far all functions I have written have been in the same file as the tests, some are even in the same function as the assertions of the tests.
In earlier tests I found it better to keep functions outside of functions so that anything could call them from outside.
I can also place them in other modules then use the import statement to bring in the function so I can test it. This helps me keep tests and solutions separate. It also means I can send tests only, solutions only or both.
RED: make it fail
I go back to the terminal where the tests are running
I change the assertion to call the
textfunction of thetelephonemodule in thesrcfolder1def text(the_input): 2 return f'I got: {the_input}' 3 4 5def test_passing_none(): 6 # assert text(None) == 'I got: None' 7 reality = src.telephone.text(None) 8 my_expectation = 'I got: None' 9 assert reality == my_expectation 10 11 12def test_passing_booleans():the terminal is my friend, and shows NameError
NameError: name 'src' is not definedbecause there is nothing with that name in
test_telephone.py.
GREEN: make it pass
I add an import statement at the top of
test_telephone.py1import src 2 3 4def text(the_input):the terminal is my friend, and shows ModuleNotFoundError
E ModuleNotFoundError: No module named 'src'because there is nothing named
srcin the project.I add ModuleNotFoundError to the list of Exceptions seen
73# Exceptions seen 74# AssertionError 75# NameError 76# TypeError 77# ModuleNotFoundErrorI go to the other terminal
I use mkdir to make a folder named
srcmkdir srcthe terminal goes back to the command line.
I go back to the terminal where the tests are running
I use ctrl/command+s (Windows & Linux/MacOS) in
test_telephone.pyto run the test again, and the terminal shows AttributeErrorAttributeError: module 'src' has no attribute 'telephone'because there is nothing named
telephonein thesrcfolder.I add AttributeError to the list of Exceptions seen
73# Exceptions seen 74# AssertionError 75# NameError 76# TypeError 77# ModuleNotFoundError 78# AttributeErrorI change the import statement to make it import
telephone.pyfrom thesrcfolder1# import src 2import src.telephone 3 4 5def text(the_input):the terminal is my friend, and shows ModuleNotFoundError
E ModuleNotFoundError: No module named 'src.telephone'because Python cannot find
telephone.pyin thesrcfolder since I have not made it yet.I go to the other terminal
I use touch to make
telephone.pyin thesrcfoldertouch src/telephone.pyNew-Item src/telephone.pythe terminal goes back to the command line.
I go back to the terminal where the tests are running and it shows AttributeError
AttributeError: module 'src.telephone' has no attribute 'text'because
telephone.pyin thesrcfolder does not have anything namedtextinside it.I open
telephone.pyfrom thesrcfolderI add a copy of the
textfunction totelephone.py1def text(the_input): 2 return f'I got: {the_input}'the test passes because
when
import src.telephoneruns, Python brings in an object for thetelephone.pyfile from thesrcfolder so I can use it intest_telephone.pyassrc.telephonewhen
src.telephone.text(None)runs, Python calls thetextfunction from the object it imported for thetelephone.pyfile from thesrcfolder (src.telephone)
I think of
src.telephone.textlike an addresssrc.telephone.text src └── telephone.py └── def text(the_input): └── return f'I got: {the input}'
REFACTOR: make it better
I remove the commented lines from
test_telephone.py1import src.telephone 2 3 4def text(the_input): 5 return f'I got: {the_input}' 6 7 8def test_passing_none(): 9 reality = src.telephone.text(None) 10 my_expectation = 'I got: None' 11 assert reality == my_expectation 12 13 14def test_passing_booleans():I change the call in the first assertion of test_passing_booleans to
src.telephone.text14def test_passing_booleans(): 15 # assert text(False) == 'I got: False' 16 reality = src.telephone.text(False) 17 my_expectation = 'I got: False' 18 assert reality == my_expectation 19 assert text(True) == 'I got: True' 20 21 22def test_passing_an_integer():the test is still green because when
src.telephone.textis called, Python follows this pathsrc.telephone.text src └── telephone.py └── def text(the_input): └── return f'I got: {the input}'then uses the string representation of the object in the curly braces
{ }text(None) text(the_input) the_input = None return f'I got: {the_input}' return 'I got: None 'I make the same change for the next assertion
14def test_passing_booleans(): 15 # assert text(False) == 'I got: False' 16 reality = src.telephone.text(False) 17 my_expectation = 'I got: False' 18 assert reality == my_expectation 19 # assert text(True) == 'I got: True' 20 reality = src.telephone.text(True) 21 my_expectation = 'I got: True' 22 assert reality == my_expectation 23 24 25def test_passing_an_integer():still green.
I remove the commented lines from test_passing_booleans
14def test_passing_booleans(): 15 reality = src.telephone.text(False) 16 my_expectation = 'I got: False' 17 assert reality == my_expectation 18 19 reality = src.telephone.text(True) 20 my_expectation = 'I got: True' 21 assert reality == my_expectation 22 23 24def test_passing_an_integer():I change the call in the assertion of test_passing_an_integer to
src.telephone.text24def test_passing_an_integer(): 25 an_integer = 1234 26 # assert text(an_integer) == f'I got: {an_integer}' 27 reality = src.telephone.text(an_integer) 28 my_expectation = f'I got: {an_integer}' 29 assert reality == my_expectation 30 31 32def test_passing_a_float():green.
I remove the commented line from test_passing_an_integer
24def test_passing_an_integer(): 25 an_integer = 1234 26 27 reality = src.telephone.text(an_integer) 28 my_expectation = f'I got: {an_integer}' 29 assert reality == my_expectation 30 31 32def test_passing_a_float():I change the call in the assertion of test_passing_a_float
32def test_passing_a_float(): 33 a_float = 5.678 34 # assert text(a_float) == f'I got: {a_float}' 35 reality = src.telephone.text(a_float) 36 my_expectation = f'I got: {a_float}' 37 assert reality == my_expectation 38 39 40def test_passing_a_string():still green because when
src.telephone.textis called, Python follows this pathsrc.telephone.text src └── telephone.py └── def text(the_input): └── return f'I got: {the input}'then uses the string representation of the object in the curly braces
{ }a_float = 5.678 text(a_float) text(the_input) the_input = 5.678 return f'I got: {the_input}' return 'I got: 5.678 'I remove the commented line
32def test_passing_a_float(): 33 a_float = 5.678 34 35 reality = src.telephone.text(a_float) 36 my_expectation = f'I got: {a_float}' 37 assert reality == my_expectation 38 39 40def test_passing_a_string():the test is still green.
I change the call in the assertion of test_passing_a_string
40def test_passing_a_string(): 41 a_string = 'hi' 42 # assert text(a_string) == f'I got: {a_string}' 43 reality = src.telephone.text(a_string) 44 my_expectation = f'I got: {a_string}' 45 assert reality == my_expectation 46 47 48def test_passing_a_tuple():I remove the commented line
40def test_passing_a_string(): 41 a_string = 'hi' 42 43 reality = src.telephone.text(a_string) 44 my_expectation = f'I got: {a_string}' 45 assert reality == my_expectation 46 47 48def test_passing_a_tuple():I change the call in the assertion of test_passing_a_tuple
48def test_passing_a_tuple(): 49 a_tuple = (0, 1, 2, 'n') 50 # assert text(a_tuple) == f'I got: {a_tuple}' 51 reality = src.telephone.text(a_tuple) 52 my_expectation = f'I got: {a_tuple}' 53 assert reality == my_expectation 54 55 56def test_passing_a_list():still green.
I remove the commented line
48def test_passing_a_tuple(): 49 a_tuple = (0, 1, 2, 'n') 50 51 reality = src.telephone.text(a_tuple) 52 my_expectation = f'I got: {a_tuple}' 53 assert reality == my_expectation 54 55 56def test_passing_a_list():I change the call in the assertion of test_passing_a_list
56def test_passing_a_list(): 57 a_list = [0, 1, 2, 'n'] 58 # assert text(a_list) == f'I got: {a_list}' 59 reality = src.telephone.text(a_list) 60 my_expectation = f'I got: {a_list}' 61 assert reality == my_expectation 62 63 64def test_passing_a_set():green.
I remove the commented line
56def test_passing_a_list(): 57 a_list = [0, 1, 2, 'n'] 58 59 reality = src.telephone.text(a_list) 60 my_expectation = f'I got: {a_list}' 61 assert reality == my_expectation 62 63 64def test_passing_a_set():I change the call in the assertion of test_passing_a_set
64def test_passing_a_set(): 65 a_set = {0, 1, 2, 'n'} 66 # assert text(a_set) == f'I got: {a_set}' 67 reality = src.telephone.text(a_set) 68 my_expectation = f'I got: {a_set}' 69 assert reality == my_expectation 70 71 72def test_passing_a_dictionary():still green.
I remove the commented line
64def test_passing_a_set(): 65 a_set = {0, 1, 2, 'n'} 66 67 reality = src.telephone.text(a_set) 68 my_expectation = f'I got: {a_set}' 69 assert reality == my_expectation 70 71 72def test_passing_a_dictionary():I change the call in
realityfor test_passing_a_dictionary72def test_passing_a_dictionary(): 73 a_dictionary = { 74 'key0': 'value0', 75 'keyN': [0, 1, 2, 'n'], 76 } 77 # reality = text(a_dictionary) 78 reality = src.telephone.text(a_dictionary) 79 my_expectation = f'I got: {a_dictionary}' 80 assert reality == my_expectation 81 82 83def test_passing_a_class():the test is still green because when
src.telephone.textis called, Python follows this pathsrc.telephone.text src └── telephone.py └── def text(the_input): └── return f'I got: {the input}'then uses the string representation of the object in the curly braces
{ }a_dictionary = { 'key0': 'value0', 'keyN': [0, 1, 2, 'n'], } text(a_dictionary) text(the_input) the_input = { 'key0': 'value0', 'keyN': [0, 1, 2, 'n'], } return f'I got: {the_input}' return "I got: {'key0': 'value0', 'keyN': [0, 1, 2, 'n']}"I remove the commented line
72def test_passing_a_dictionary(): 73 a_dictionary = { 74 'key0': 'value0', 75 'keyN': [0, 1, 2, 'n'], 76 } 77 78 reality = src.telephone.text(a_dictionary) 79 my_expectation = f'I got: {a_dictionary}' 80 assert reality == my_expectation 81 82 83def test_passing_a_class():I change the calls in the assertions of test_passing_a_class
83def test_passing_a_class(): 84 # assert text(object) == "I got: <class 'object'>" 85 reality = src.telephone.text(object) 86 my_expectation = "I got: <class 'object'>" 87 assert reality == my_expectation 88 89 # assert text(bool) == "I got: <class 'bool'>" 90 reality = src.telephone.text(bool) 91 my_expectation = "I got: <class 'bool'>" 92 assert reality == my_expectation 93 94 # assert text(int) == "I got: <class 'int'>" 95 reality = src.telephone.text(int) 96 my_expectation = "I got: <class 'int'>" 97 assert reality == my_expectation 98 99 # assert text(float) == "I got: <class 'float'>" 100 reality = src.telephone.text(float) 101 my_expectation = "I got: <class 'float'>" 102 assert reality == my_expectation 103 104 # assert text(str) == "I got: <class 'str'>" 105 reality = src.telephone.text(str) 106 my_expectation = "I got: <class 'str'>" 107 assert reality == my_expectation 108 109 # assert text(tuple) == "I got: <class 'tuple'>" 110 reality = src.telephone.text(tuple) 111 my_expectation = "I got: <class 'tuple'>" 112 assert reality == my_expectation 113 114 # assert text(list) == "I got: <class 'list'>" 115 reality = src.telephone.text(list) 116 my_expectation = "I got: <class 'list'>" 117 assert reality == my_expectation 118 119 # assert text(set) == "I got: <class 'set'>" 120 reality = src.telephone.text(set) 121 my_expectation = "I got: <class 'set'>" 122 assert reality == my_expectation 123 124 # assert text(dict) == "I got: <class 'dict'>" 125 reality = src.telephone.text(dict) 126 my_expectation = "I got: <class 'dict'>" 127 assert reality == my_expectation 128 129 130# Exceptions seenthe test is still green.
I remove the commented lines
I remove the
textfunction fromtest_telephone.py1import src.telephone 2 3 4def test_passing_none(): 5 reality = src.telephone.text(None) 6 my_expectation = 'I got: None' 7 assert reality == my_expectation 8 9 10def test_passing_booleans():all the tests are still green because all the calls to the
textfunction that was intest_telephone.pyare now to thetextfunction intelephone.pyin thesrcfolder. Whensrc.telephone.textis called Python follows this pathsrc.telephone.text src └── telephone.py └── def text(the_input): └── return f'I got: {the input}'I add a git commit message in the other terminal
git commit -am \ 'separate solution from tests'the terminal shows a summary of the changes then goes back to the command line.
test_telephone
Now that the solution is separate from the tests, I can write the program that makes the tests pass without looking at test_telephone.py.
RED: make it fail
I close
test_telephone.pyI delete the text in
telephone.pyand the terminal shows 9 failures. I start with the last AttributeErrorFAILED ...::test_passing_none - AttributeError: module 'src.telephone' has no attribute 'text' FAILED ...::test_passing_booleans - AttributeError: module 'src.telephone' has no attribute 'text' FAILED ...::test_passing_an_integer - AttributeError: module 'src.telephone' has no attribute 'text' FAILED ...::test_passing_a_float - AttributeError: module 'src.telephone' has no attribute 'text' FAILED ...::test_passing_a_string - AttributeError: module 'src.telephone' has no attribute 'text' FAILED ...::test_passing_a_tuple - AttributeError: module 'src.telephone' has no attribute 'text' FAILED ...::test_passing_a_list - AttributeError: module 'src.telephone' has no attribute 'text' FAILED ...::test_passing_a_set - AttributeError: module 'src.telephone' has no attribute 'text' FAILED ...::test_passing_a_dictionary - AttributeError: module 'src.telephone' has no attribute 'text' FAILED ...::test_passing_a_class - AttributeError: module 'src.telephone' has no attribute 'text' =================== 10 failed in A.BCs ===================Can you make the tests pass without looking at how I solve it below? You can come back to compare solutions when you are done or if you get stuck.
GREEN: make it pass
I add the name to
telephone.py1textthe terminal is my friend, and shows NameError
NameError: name 'text' is not definedI point it to None (the simplest object) to define it
1# text 2text = Nonethe terminal is my friend, and shows TypeError
TypeError: 'NoneType' object is not callablebecause
textpoints to None and I cannot call None like a function.I make
texta function to make it callable1# text 2# text = None 3def text(): 4 return Nonethe terminal is my friend, and shows TypeError
TypeError: text() takes 0 positional arguments but 1 was givenbecause this function definition does not allow any inputs, the parentheses are empty.
I make the function take input
1# text 2# text = None 3# def text(): 4def text(value): 5 return Nonethe terminal is my friend, and shows AssertionError
E assert None == "I got: <class 'object'>"I copy the string from the terminal and paste it in the return statement to match the expectation of the test
1# text 2# text = None 3# def text(): 4def text(value): 5 # return None 6 return "I got: <class 'object'>"the terminal is my friend, and shows AssertionError
E assert "I got: <class 'object'>" == "I got: <class 'bool'>"I change the return statement to see the difference between the input and the expected output (remember the identity function?)
1# text 2# text = None 3# def text(): 4def text(value): 5 # return None 6 # return "I got: <class 'object'>" 7 return valuethe test summary info shows that every test has AssertionError
FAILED ...::test_passing_none - AssertionError: assert None == 'I got: None' FAILED ...::test_passing_booleans - AssertionError: assert False == 'I got: False' FAILED ...::test_passing_an_integer - AssertionError: assert 1234 == 'I got: 1234' FAILED ...::test_passing_a_float - AssertionError: assert 5.678 == 'I got: 5.678' FAILED ...::test_passing_a_string - AssertionError: assert 'hi' == 'I got: hi' FAILED ...::test_passing_a_tuple - assert (0, 1, 2, 'n') == "I got: (0, 1, 2, 'n')" FAILED ...::test_passing_a_list - assert [0, 1, 2, 'n'] == "I got: [0, 1, 2, 'n']" FAILED ...::test_passing_a_set - assert {0, 1, 2, 'n'} == "I got: {0, 1, 2, 'n'}" FAILED ...::test_passing_a_dictionary - assert {'key0': 'value0', 'keyN': [0, 1, 2, 'n']} == "I got: {'key0': 'value0',... FAILED ...::test_passing_a_class - assert <class 'object'> == "I got: <class 'object'>"they all expect the input (
value) as part of the messageI add a return statement with an f-string
1# text 2# text = None 3# def text(): 4def text(value): 5 # return None 6 # return "I got: <class 'object'>" 7 # return value 8 return f'I got: {value}'and all the tests are passing! I am a programmer!!
I remove the commented lines
1def text(value): 2 return f'I got: {value}'I add a git commit message in the other terminal
git commit --all --message 'test telephone'the terminal shows a summary of the changes then goes back to the command line.
close the project
I close
telephone.pyI click in the terminal where the tests are running
I use q on the keyboard to leave the tests. The terminal goes back to the command line.
I change directory to the parent of
telephonecd ..the terminal shows
.../pumping_pythonI am back in the
pumping_pythondirectory.
review
code from the chapter
what is next?
You now know:
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.