8000 Functions: test keyword parameter (#222) · shri-empa/python-tutorial@5442f1a · GitHub
[go: up one dir, main page]

Skip to content

Commit 5442f1a

Browse files
authored
Functions: test keyword parameter (empa-scientific-it#222)
* test keyword parameter * replace multiple asserts with if statements * update exercise 2 tests * format multi-line error * fix check errors * print multiple errors as list
1 parent e419b84 commit 5442f1a

File tree

3 files changed

+92
-61
lines changed

3 files changed

+92
-61
lines changed

functions.ipynb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@
473473
"## Exercise 2\n",
474474
"\n",
475475
"Write a Python function called `calculate_area` that takes three parameters: `length` (a float), `width` (a float), and `unit` (a string with a **default** value of `\"cm\"`).\n",
476-
"The function should calculate the area of a rectangle based on the given length and width, and return the result **as a tuple** with the correct, default unit (i.e., `cm^2`).\n",
476+
"The function should calculate the area of a rectangle based on the given length and width, and return the result **as a string** including the correct, default unit (i.e., `cm^2`).\n",
477477
"If the unit parameter is \"m\", the function should convert the length and width from meters to centimeters before calculating the area.\n",
478478
"\n",
479479
"Your solution function **must** handle the following input units (the output unit is **always** `cm^2`):\n",
@@ -1255,7 +1255,7 @@
12551255
"source": [
12561256
"\n",
12571257
"<div class=\"alert alert-block alert-warning\">\n",
1258-
" <h4><b>Question</b></h4> How many valid password are there in your range?\n",
1258+
" <h4><b>Question</b></h4> How many valid passwords are there in your range?\n",
12591259
"</div>"
12601260
]
12611261
},
@@ -1306,7 +1306,7 @@
13061306
"Write a new function for validating password that includes the new rule.\n",
13071307
"\n",
13081308
"<div class=\"alert alert-block alert-warning\">\n",
1309-
" <h4><b>Question</b></h4> How many valid password are there in your range <b>now</b>?\n",
1309+
" <h4><b>Question</b></h4> How many valid passwords are there in your range <b>now</b>?\n",
13101310
"</div>\n",
13111311
"\n",
13121312
"\n",

tutorial/tests/test_functions.py

Lines changed: 88 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import pathlib
33
from collections import Counter
44
from string import ascii_lowercase, ascii_uppercase
5-
from typing import Any, List, Tuple
5+
from typing import Any, List
66

77
import pytest
88

@@ -12,6 +12,14 @@ def read_data(name: str, data_dir: str = "data") -> pathlib.Path:
1212
return (pathlib.Path(__file__).parent / f"{data_dir}/{name}").resolve()
1313

1414

15+
def errors_to_list(errors):
16+
result = "<ul>"
17+
for error in errors:
18+
result += "<li>" + error + "</li>"
19+
result += "</ul>"
20+
return result
21+
22+
1523
#
1624
# Exercise 1: a `greet` function
1725
#
@@ -22,34 +30,46 @@ def reference_greet(name: str, age: int) -> str:
2230
return f"Hello, {name}! You are {age} years old."
2331

2432

25-
def test_greet(function_to_test) -> None:
26-
assert function_to_test.__doc__ is not None, "The function is missing a docstring"
33+
@pytest.mark.parametrize(
34+
"name,age",
35+
[
36+
("John", 30),
37+
],
38+
)
39+
def test_greet(
40+
name: str,
41+
age: int,
42+
function_to_test,
43+
) -> None:
44+
errors = []
2745

2846
signature = inspect.signature(function_to_test)
2947
params = signature.parameters
3048
return_annotation = signature.return_annotation
3149

32-
assert len(params) == 2, "The function should take two arguments"
33-
assert (
34-
"name" in params.keys() and "age" in params.keys()
35-
), "The function's parameters should be 'name' and 'age'"
50+
if function_to_test.__doc__ is None:
51+
errors.append("The function is missing a docstring.")
52+
if len(params) != 2:
53+
errors.append("The function should take two arguments.")
54+
if "name" not in params.keys() or "age" not in params.keys():
55+
errors.append("The function's parameters should be 'name' and 'age'.")
56+
if any(p.annotation == inspect.Parameter.empty for p in params.values()):
57+
errors.append("The function's parameters should have type hints.")
58+
if return_annotation == inspect.Signature.empty:
59+
errors.append("The function's return value is missing the type hint.")
3660

37-
assert all(
38-
p.annotation != inspect.Parameter.empty for p in params.values()
39-
), "The function's parameters should have type hints"
40-
assert (
41-
return_annotation != inspect.Signature.empty
42-
), "The function's return value is missing the type hint"
61+
# test signature
62+
assert not errors, errors_to_list(errors)
63+
# test result
64+
assert function_to_test(name, age) == reference_greet(name, age)
4365

4466

4567
#
4668
# Exercise 2: calculate area with units
4769
#
4870

4971

50-
def reference_calculate_area(
51-
length: float, width: float, unit: str = "cm"
52-
) -> Tuple[float, str] | str:
72+
def reference_calculate_area(length: float, width: float, unit: str = "cm") -> str:
5373
"""Reference solution for the calculate_area exercise"""
5474
# Conversion factors from supported units to centimeters
5575
units = {
@@ -65,70 +85,81 @@ def reference_calculate_area(
6585
except KeyError:
6686
return f"Invalid unit: {unit}"
6787
else:
68-
return (area, "cm^2")
88+
return f"{area} cm^2"
6989

7090

7191
def test_calculate_area_signature(function_to_test) -> None:
72-
assert function_to_test.__doc__ is not None, "The function is missing a docstring"
92+
errors = []
7393

7494
signature = inspect.signature(function_to_test)
7595
params = signature.parameters
7696
return_annotation = signature.return_annotation
7797

78-
assert len(params) == 3, "The function should take three arguments"
79-
assert (
80-
"length" in params.keys()
81-
and "width" in params.keys()
82-
and "unit" in params.keys()
83-
), "The function's parameters should be 'length', 'width' and 'unit'"
84-
85-
assert all(
86-
p.annotation != inspect.Parameter.empty for p in params.values()
87-
), "The function's parameters should have type hints"
88-
assert (
89-
return_annotation != inspect.Signature.empty
90-
), "The function's return value is missing the type hint"
98+
if function_to_test.__doc__ is None:
99+
errors.append("The function is missing a docstring.")
100+
if len(params) != 3:
101+
errors.append("The function should take three arguments.")
102+
if (
103+
"length" not in params.keys()
104+
or "width" not in params.keys()
105+
or "unit" not in params.keys()
106+
):
107+
errors.append(
108+
"The function's parameters should be 'length', 'width' and 'unit'."
109+
)
110+
if "unit" in params.keys() and not (
111+
params["unit"].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
112+
and params["unit"].default == "cm"
113+
):
114+
errors.append("Argument 'unit' should have a default value 'cm'.")
115+
if any(p.annotation == inspect.Parameter.empty for p in params.values()):
116+
errors.append("The function's parameters should have type hints.")
117+
if return_annotation == inspect.Signature.empty:
118+
errors.append("The function's return value is missing the type hint.")
119+
120+
assert not errors, errors_to_list(errors)
91121

92122

93123
@pytest.mark.parametrize(
94-
"length,width,unit,expected",
124+
"length,width,unit",
95125
[
96-
(2.0, 3.0, "cm", (6.0, "cm^2")),
97-
(4.0, 5.0, "m", (200000.0, "cm^2")),
98-
(10.0, 2.0, "mm", (2000.0, "cm^2")),
99-
(2.0, 8.0, "yd", (133780.38, "cm^2")),
100-
(5.0, 4.0, "ft", (18580.608, "cm^2")),
101-
(3.0, 5.0, "in", (96.774, "cm^2")),
126+
(2.0, 3.0, "cm"),
127+
(4.0, 5.0, "m"),
128+
(10.0, 2.0, "mm"),
129+
(2.0, 8.0, "yd"),
130+
(5.0, 4.0, "ft"),
131+
(3.0, 5.0, "in"),
102132
],
103133
)
104134
def test_calculate_area_result(
105135
length: float,
106136
width: float,
107137
unit: str,
108-
expected: Tuple[float, str],
109138
function_to_test,
110139
) -> None:
111-
result = function_to_test(length, width, unit)
112-
test_result = reference_calculate_area(length, width, unit)
140+
errors = []
113141

114142
if unit in ("cm", "m", "mm", "yd", "ft"):
115-
assert isinstance(result, Tuple), "The function should return a tuple"
116-
117-
assert "cm^2" in result, "The result should be in squared centimeters (cm^2)"
118-
119-
# Double-check the reference solution
120-
assert test_result[0] == pytest.approx(
121-
expected[0], abs=0.01
122-
), "The reference solution is incorrect"
123-
124-
assert result[0] == pytest.approx(expected[0], abs=0.01)
143+
result = function_to_test(length, width, unit)
144+
145+
if not isinstance(result, str):
146+
errors.append("The function should return a string.")
147+
if "cm^2" not in result:
148+
errors.append("The result should be in squared centimeters (cm^2).")
149+
if result != reference_calculate_area(length, width, unit):
150+
errors.append("The solution is incorrect.")
125151
else:
126-
assert isinstance(
127-
result, str
128-
), "The function should return an error string for unsupported units"
129-
assert (
130-
result == f"Invalid unit: {unit}"
131-
), "The error message is incorrectly formatted"
152+
try:
153+
result = function_to_test(length, width, unit)
154+
except KeyError:
155+
errors.append(
156+
"The function should return an error string for unsupported units."
157+
)
158+
else:
159+
if result != f"Invalid unit: {unit}":
160+
errors.append("The error message is incorrectly formatted.")
161+
162+
assert not errors
132163

133164

134165
#

tutorial/tests/testsuite/helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def format_error(exception: BaseException) -> str:
9696
)
9797

9898
# If we couldn't parse the exception message, just display it as is
99-
formatted_message = formatted_message or f"<p>{html.escape(exception_str)}</p>"
99+
formatted_message = formatted_message or f"<p>{exception_str}</p>"
100100

101101
return formatted_message
102102

0 commit comments

Comments
 (0)
0