separate and equal telephone

The text function in the telephone project was written in tests/test_telephone.py. I want to move it to __init__.py in the telephone folder 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

telephone/tests/test_telephone.py
 1import src.telephone
 2
 3
 4text = src.telephone.text
 5
 6
 7def assert_equal(a, b):
 8    assert a == b
 9
10
11def test_passing_none():
12    assert_equal(text(None), 'I got: None')
telephone/tests/test_telephone.py
15def test_passing_booleans():
16    assert_equal(text(False), 'I got: False')
17    assert_equal(text(True), 'I got: True')
18
19
20def test_passing_an_integer():
21    an_integer = 1234
22    assert_equal(text(an_integer), f'I got: {an_integer}')
23
24
25def test_passing_a_float():
26    a_float = 5.678
27    assert_equal(text(a_float), f'I got: {a_float}')
telephone/tests/test_telephone.py
30def test_passing_a_string():
31    a_string = 'hello'
32    assert_equal(text('hello'), f'I got: {a_string}')
33
34
35def test_passing_a_tuple():
36    a_tuple = (0, 1, 2, 'n')
37    assert_equal(text(a_tuple), f"I got: {a_tuple}")
38
39
40def test_passing_a_list():
41    a_list = [0, 1, 2, 'n']
42    assert_equal(text(a_list), f'I got: {a_list}')
telephone/tests/test_telephone.py
45def test_passing_a_set():
46    a_set = {0, 1, 2, 'n'}
47    assert_equal(text(a_set), f'I got: {a_set}')
48
49
50def test_passing_a_dictionary():
51    a_dictionary = {
52        'key0': 'value0',
53        'keyN': [0, 1, 2, 'n'],
54    }
55    reality = text(a_dictionary)
56    my_expectation = f'I got: {a_dictionary}'
57    assert_equal(reality, my_expectation)
telephone/tests/test_telephone.py
60def test_passing_a_class():
61    assert_equal(
62        text(object), "I got: <class 'object'>"
63    )
64    assert_equal(text(bool), "I got: <class 'bool'>")
65    assert_equal(text(int), "I got: <class 'int'>")
66    assert_equal(text(float), "I got: <class 'float'>")
67    assert_equal(text(str), "I got: <class 'str'>")
68    assert_equal(text(tuple), "I got: <class 'tuple'>")
69    assert_equal(text(list), "I got: <class 'list'>")
70    assert_equal(text(set), "I got: <class 'set'>")
71    assert_equal(text(dict), "I got: <class 'dict'>")
72
73
74# Exceptions seen
75# AssertionError
76# NameError
77# TypeError
78# AttributeError

open the project

  • I open a terminal

  • I change directory to the project

    cd telephone
    

    the terminal shows I am in the telephone folder

    .../pumping_python/telephone
    
  • I open test_telephone.py from the tests folder

  • I use pytest-watcher to run the tests automatically

    uv run pytest-watcher . --now
    

    the terminal shows

    test_telephone.py ..........                        [100%]
    
    =================== 10 passed in A.BCs ===================
    

move text

RED: make it fail


I comment out the text function in tests/test_telephone.py

1def assert_equal(a, b):
2    assert a == b
3
4
5# def text(the_input):
6#     return f'I got: {the_input}'
7
8
9def test_passing_none():

the terminal is my friend, and shows NameError

NameError: name 'text' is not defined

GREEN: make it pass


  • I use a variable to reroute the calls to the text function to the text function of the telephone module in the src folder

    1text = src.telephone.text
    2
    3
    4def assert_equal(a, b):
    

    the terminal is my friend, and shows NameError

    NameError: name 'src' is not defined
    

    because there is nothing with that name in tests/test_telephone.py.

  • I add an import statement at the top of tests/test_telephone.py

    1import src.telephone
    2
    3
    4text = src.telephone.text
    

    the terminal is my friend, and shows AttributeError

    AttributeError: module 'src.telephone'
                    has no attribute 'text'
    

    because the telephone module in the src folder does not have anything named text inside it.

  • I add AttributeError to the list of Exceptions seen

    78# Exceptions seen
    79# AssertionError
    80# NameError
    81# TypeError
    82# AttributeError
    
  • I open __init__.py from the telephone folder in the src folder

  • I delete all the text in the file then add a copy of the text function to src/telephone/__init__.py

    1def text(the_input):
    2    return f'I got: {the_input}'
    

    all the tests are green again because

    • Python brings in an object for the __init__.py file from the telephone folder in the src folder so I can use it in tests/test_telephone.py as src.telephone when import src.telephone runs.

    • Python calls the text function from the object it imported for the src/telephone/__init__.py file (src.telephone) when src.telephone.text is called.

    I think of src.telephone.text like an address

    src.telephone.text
    src/
    └── telephone/
        └── __init__.py
            └── def text(the_input):
                └── return f'I got: {the_input}'
    

REFACTOR: make it better


  • I remove the commented lines from tests/test_telephone.py

     1import src.telephone
     2
     3
     4text = src.telephone.text
     5
     6
     7def assert_equal(a, b):
     8    assert a == b
     9
    10
    11def test_passing_none():
    
  • I add a git commit message in the other terminal

    git commit -am \
    'move text function to src'
    

    the terminal shows a summary of the changes then goes back to the command line.

All the tests are passing because Python follows this path when the text function is called

  • In test_passing_none

    text = src.telephone.text
    text(None) -> 'I got: None'
    └── src/
        └── telephone/
            └── __init__.py
                └── def text(the_input):
                    ├── the_input = None
                    └── return f'I got: {the_input}'
                        return  'I got:  None      '
    
  • In test_passing_booleans

    text = src.telephone.text
    text(False) -> 'I got: False'
    └── src/
        └── telephone/
            └── __init__.py
                └── def text(the_input):
                    ├── the_input = False
                    └── return f'I got: {the_input}'
                        return  'I got:  False     '
    
  • In test_passing_an_integer

    text = src.telephone.text
    an_integer = 1234
    text(an_integer) -> 'I got: 1234'
    └── src/
        └── telephone/
            └── __init__.py
                └── def text(the_input):
                    ├── the_input = 1234
                    └── return f'I got: {the_input}'
                        return  'I got: 1234       '
    
  • In test_passing_a_float

    text = src.telephone.text
    a_float = 5.678
    text(a_float) -> 'I got: 5.678'
    └── src/
        └── telephone/
            └── __init__.py
                └── def text(the_input):
                    ├── the_input = 5.678
                    └── return f'I got: {the_input}'
                        return  'I got: 5.678      '
    
  • In test_passing_a_string

    text = src.telephone.text
    a_string = 'hello'
    text(a_string) -> 'I got: hello'
    src/
    └── telephone/
        └── __init__.py
            └── def text(the_input):
                ├── the_input = 'hello'
                └── return f'I got: {the_input}'
                    return  'I got:  hello     '
    
  • In test_passing_a_tuple

    text = src.telephone.text
    a_tuple = (0, 1, 2, 'n')
    text(a_tuple) -> "I got: (0, 1, 2, 'n')"
    └── src/
        └── telephone/
            └── __init__.py
                └── def text(the_input):
                    ├── the_input = (0, 1, 2, 'n')
                    └── return f'I got: {  the_input  }'
                        return  "I got:  (0, 1, 2, 'n')"
    
  • In test_passing_a_list

    text = src.telephone.text
    a_list = [0, 1, 2, 'n']
    text(a_list) -> "I got: [0, 1, 2, 'n']"
    └── src/
        └── telephone/
            └── __init__.py
                └── def text(the_input):
                    ├── the_input = [0, 1, 2, 'n']
                    └── return f'I got: {  the_input  }'
                        return  "I got:  [0, 1, 2, 'n']"
    
  • In test_passing_a_set

    text = src.telephone.text
    a_set = {0, 1, 2, 'n'}
    text(a_set) -> 'I got: {0, 1, 2, 'n'}'
    └── src/
        └── telephone/
            └── __init__.py
                └── def text(the_input):
                    ├── the_input = {0, 1, 2, 'n'}
                    └── return f'I got: {  the_input  }'
                        return  'I got:  {0, 1, 2, 'n'}'
    
  • In test_passing_a_dictionary

    text = src.telephone.text
    a_dictionary = {
        'key0': 'value0', 'keyN': [0, 1, 2, 'n']
    }
    text(a_dictionary) -> ("I got: {'key0': 'value0',"
    │                               'keyN': [0, 1, 2, 'n']}")
    └── src/
        └── telephone/
            └── __init__.py
                └── def 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']}")
    
  • In test_passing_a_class

    text = src.telephone.text
    text(object) -> "I got: <class 'object'>"
    └── src/
        └── telephone/
            └── __init__.py
                └── def text(the_input):
                    ├── the_input = object
                    └── return f'I got: {   the_input   }'
                        return  "I got:  <class 'object'>"
    

test_telephone

Since the solution is separate from the tests, I can write the program that makes the tests pass without looking at tests/test_telephone.py.


RED: make it fail


  • I close tests/test_telephone.py

  • I delete the text in src/telephone/__init__.py and the terminal AttributeError

    AttributeError: module 'src.telephone'
                    has no attribute 'text'
    

    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 src/telephone/__init__.py

    1text
    

    the terminal is my friend, and shows NameError

    NameError: name 'text' is not defined
    
  • I point it to None (the simplest object) to define it

    1# text
    2text = None
    

    the terminal is my friend, and shows TypeError

    TypeError: 'NoneType' object is not callable
    

    because text points to None and I cannot call None like a function.

  • I make text a function to make it callable

    1# text
    2# text = None
    3def text():
    4    return None
    

    the terminal is my friend, and shows TypeError

    TypeError: text() takes 0 positional arguments
               but 1 was given
    

    because 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 None
    

    the 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 value
    

    the 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 'hello' == 'I got: hello'
    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', 'keyN': [0, 1, 2, 'n']}"
    FAILED ...::test_passing_a_class -
        assert <class 'object'> == "I got: <class 'object'>"
    

    they all expect the input (value) as part of the message

  • I add a return statement with the name of the input

    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 'I got: value'
    

    the terminal is my friend, and shows AssertionError

    FAILED ...test_passing_none -
        AssertionError: assert 'I got: value' == 'I got: None'
    FAILED ...test_passing_booleans -
        AssertionError: assert 'I got: value' == 'I got: False'
    FAILED ...test_passing_an_integer -
        AssertionError: assert 'I got: value' == 'I got: 1234'
    FAILED ...test_passing_a_float -
        AssertionError: assert 'I got: value' == 'I got: 5.678'
    FAILED ...test_passing_a_string -
        AssertionError: assert 'I got: value' == 'I got: hello'
    FAILED ...test_passing_a_tuple -
        assert 'I got: value' == "I got: (0, 1, 2, 'n')"
    FAILED ...test_passing_a_list -
        assert 'I got: value' == "I got: [0, 1, 2, 'n']"
    FAILED ...test_passing_a_set -
        assert 'I got: value' == "I got: {0, 1, 2, 'n'}"
    FAILED ...test_passing_a_dictionary -
        assert 'I got: value' == "I got: {'key..., 1, 2, 'n']}"
    FAILED ...test_passing_a_class -
        assert 'I got: value' == "I got: <class 'object'>"
    
  • I change the return statement to 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 'I got: value'
    9    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 src/telephone/__init__.py

  • I 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 telephone

    cd ..
    

    the terminal shows

    .../pumping_python
    

    I am back in the pumping_python directory.


review

I can write solutions in a different module from the tests.


code from the chapter

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


what is next?

would you like to separate the tests from the solution in the TypeError project?


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.