Refactoring & Logic Mistakes
Thomas J. Kennedy
1 Wait… Mistakes?
We would like to continue the example from the previous lecture, i.e., Module-08/Painting-5-Adding.
On the first offering of this course… a student pointed out that 4 * length
* width
is four (4) times the area of a room’s floor instead of the surface area of the room’s walls.
Instead of rewriting the lectures and examples… I figured that this example was the perfect opportunity for a discussion in how code can
- be thoroughly tested
- follow best practices for code style
- follow best practices such as S.O.L.I.D.
and still suffer from logic and computation errors.
2 Fixing the Mistake
Let us start in compute_paint.py
.
def wall_surface_area(length: float, width: float) -> float:
"""
Compute the surface area of all four walls in a room. For the purposes of
this estimation... it is assumed that
1. each wall is a rectangle with no doorways or windows
2. there are four (4) walls in the room
Args:
length: room length
width: room width
Returns:
Wall surface area for a single room
"""
To compute the wall surface area… we need ceiling height. Let us…
- update the documentation
- add height as a parameter
def wall_surface_area(length: float, width: float, height: float) -> float:
"""
Compute the surface area of all four walls in a room. For the purposes of
this estimation... it is assumed that
1. each wall is a rectangle with no doorways or windows
2. there are four (4) walls in the room
Args:
length: room length
width: room width
height: room (ceiling) height
Returns:
Wall surface area for a single room
"""
The new computation can then replace the old one…
area_long_wall = length * height
area_wide_wall = width * height
area_four_walls = (2 * area_long_wall) + (2 * area_wide_wall)
return area_four_walls
3 Running Tests
Now… we run into an issue. Almost every test in test_compute_paint.py
now fails. Each test needs to be updated to supply the new third (height) argument to wall_surface_area
.
FAILED tests/test_compute_paint.py::test_wall_surface_area[1-1-4] - \
TypeError: wall_surface_area() missing 1 required positional argument: 'height'
FAILED tests/test_compute_paint.py::test_wall_surface_area[1-2-8] - \
TypeError: wall_surface_area() missing 1 required positional argument: 'height'
FAILED tests/test_compute_paint.py::test_wall_surface_area[2-2-16] - \
TypeError: wall_surface_area() missing 1 required positional argument: 'height'
FAILED tests/test_compute_paint.py::test_wall_surface_area[10-12-480] - \
TypeError: wall_surface_area() missing 1 required positional argument: 'height'
FAILED tests/test_compute_paint.py::test_wall_surface_area[10.5-10-420] - \
TypeError: wall_surface_area() missing 1 required positional argument: 'height'
The first step is to update the test data in tests/test_compute_paint.py
.
test_data = [
(1, 1, 1, 4),
(1, 2, 8, 48),
(2, 2, 8, 64),
(10, 12, 8, 352),
(10.5, 10, 10, 410),
]
Take note of how the third element in each tuple now corresponds to height. The fourth element is an updated expected wall surface area.
@pytest.mark.parametrize("length, width, height, surface_area", test_data)
def test_wall_surface_area(length, width, height, surface_area):
assert_that(wall_surface_area(length, width, height), close_to(surface_area, 1e-1))
The test_wall_surface_area
function needed to have height added in three places…
- the
parametrize
decorator - the functions parameters
- within the call to
wall_surface_area
Now… our entire test suite is passing. Note how we picked fixed wall surface areas for use in all other test functions. At the end of the day… the tests in test_compute_paint.py
are unit tests (which means each test focuses on a single function).
4 Running the Actual Program
Now it is time to run the actual program with…
python3.11 estimate_paint.py
After entering each value…
Enter the room length: 4
Enter the room width: 5
Enter the cost per gallon of paint: 27.25
…the program crashes…
Traceback (most recent call last):
File "Module-08/Painting-6-Logic/estimate_paint.py", line 112, in <module>
main()
File "Module-08/Painting-6-Logic/estimate_paint.py", line 98, in main
area = compute_paint.wall_surface_area(length, width)
TypeError: wall_surface_area() missing 1 required positional argument: 'height'
We need to add a prompt for height
. Our first modifications will be in estimate_paint.py
within the gather_input
function.
def gather_input(args: list[str]) -> tuple[float, float, float, float]:
The function now returns a tuple containing four (4) floats.
"""
Check the supplied `args` for four (4) user supplied arguments (for a total
length of 4). If three arguments were not supplied then prompt the used for
length (in feet), width (in feet), height (in feet), and price per gallon.
Args:
args: command line arguments to process
Returns:
a four-tuple in the form (length, width, height price_per_gallon)
"""
The pydoc documentation needs to be updated to include height.
# Command Line Arguments were supplied
if len(args) == 5:
length = float(args[1])
width = float(args[2])
height = float(args[3])
price_per_gallon = float(args[4])
We now need to check for four (4) user-supplied command line arguments
else:
length = float(input("Enter the room length: "))
width = float(input("Enter the room width: "))
height = float(input("Enter the room height: "))
price_per_gallon = float(input("Enter the cost per gallon of paint: "))
We need to add an interactive prompt for height.
return length, width, height, price_per_gallon
And… the return statement needs to be modified to include height.
5 Another Attempt
After updating gather_input
… main
needs to be updated…
length, width, price_per_gallon = gather_input(sys.argv)
needs to be updated to…
length, width, height, price_per_gallon = gather_input(sys.argv)
That leaves one last line to update… from…
area = compute_paint.wall_surface_area(length, width)
to…
area = compute_paint.wall_surface_area(length, width, height)
The program now runs to completion!
Enter the room length: 4
Enter the room width: 5
Enter the room height: 8
Enter the cost per gallon of paint: 27.28
For 2 coats...
You will need to buy 1 gallons of paint.
You will spend $ 27.28.
For 3 coats...
You will need to buy 2 gallons of paint.
You will spend $ 54.56.
For 4 coats...
You will need to buy 2 gallons of paint.
You will spend $ 54.56.
6 Final Code
The corrected code can be found in Module-08/Painting-6-Logic.