Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions functions.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@
"## Exercise 2\n",
"\n",
"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",
"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",
"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",
"If the unit parameter is \"m\", the function should convert the length and width from meters to centimeters before calculating the area.\n",
"\n",
"Your solution function **must** handle the following input units (the output unit is **always** `cm^2`):\n",
Expand Down Expand Up @@ -1255,7 +1255,7 @@
"source": [
"\n",
"<div class=\"alert alert-block alert-warning\">\n",
" <h4><b>Question</b></h4> How many valid password are there in your range?\n",
" <h4><b>Question</b></h4> How many valid passwords are there in your range?\n",
"</div>"
]
},
Expand Down Expand Up @@ -1306,7 +1306,7 @@
"Write a new function for validating password that includes the new rule.\n",
"\n",
"<div class=\"alert alert-block alert-warning\">\n",
" <h4><b>Question</b></h4> How many valid password are there in your range <b>now</b>?\n",
" <h4><b>Question</b></h4> How many valid passwords are there in your range <b>now</b>?\n",
"</div>\n",
"\n",
"\n",
Expand Down
2 changes: 1 addition & 1 deletion tutorial/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def __init__(self, questions: list = None) -> None:
def add_question(self, question: Question):
"""Adds a question to the quiz."""
question.question.value = (
f"""<strong>Q{self.nquestions+1}:</strong> """ + question.question.value
f"""<strong>Q{self.nquestions + 1}:</strong> """ + question.question.value
)
self.questions.append(question)
self.children = self.questions + self.aux
Expand Down
145 changes: 88 additions & 57 deletions tutorial/tests/test_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import pathlib
from collections import Counter
from string import ascii_lowercase, ascii_uppercase
from typing import Any, List, Tuple
from typing import Any, List

import pytest

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


def errors_to_list(errors):
result = "<ul>"
for error in errors:
result += "<li>" + error + "</li>"
result += "</ul>"
return result


#
# Exercise 1: a `greet` function
#
Expand All @@ -22,34 +30,46 @@ def reference_greet(name: str, age: int) -> str:
return f"Hello, {name}! You are {age} years old."


def test_greet(function_to_test) -> None:
assert function_to_test.__doc__ is not None, "The function is missing a docstring"
@pytest.mark.parametrize(
"name,age",
[
("John", 30),
],
)
def test_greet(
name: str,
age: int,
function_to_test,
) -> None:
errors = []

signature = inspect.signature(function_to_test)
params = signature.parameters
return_annotation = signature.return_annotation

assert len(params) == 2, "The function should take two arguments"
assert (
"name" in params.keys() and "age" in params.keys()
), "The function's parameters should be 'name' and 'age'"
if function_to_test.__doc__ is None:
errors.append("The function is missing a docstring.")
if len(params) != 2:
errors.append("The function should take two arguments.")
if "name" not in params.keys() or "age" not in params.keys():
errors.append("The function's parameters should be 'name' and 'age'.")
if any(p.annotation == inspect.Parameter.empty for p in params.values()):
errors.append("The function's parameters should have type hints.")
if return_annotation == inspect.Signature.empty:
errors.append("The function's return value is missing the type hint.")

assert all(
p.annotation != inspect.Parameter.empty for p in params.values()
), "The function's parameters should have type hints"
assert (
return_annotation != inspect.Signature.empty
), "The function's return value is missing the type hint"
# test signature
assert not errors, errors_to_list(errors)
# test result
assert function_to_test(name, age) == reference_greet(name, age)


#
# Exercise 2: calculate area with units
#


def reference_calculate_area(
length: float, width: float, unit: str = "cm"
) -> Tuple[float, str] | str:
def reference_calculate_area(length: float, width: float, unit: str = "cm") -> str:
"""Reference solution for the calculate_area exercise"""
# Conversion factors from supported units to centimeters
units = {
Expand All @@ -65,70 +85,81 @@ def reference_calculate_area(
except KeyError:
return f"Invalid unit: {unit}"
else:
return (area, "cm^2")
return f"{area} cm^2"


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

signature = inspect.signature(function_to_test)
params = signature.parameters
return_annotation = signature.return_annotation

assert len(params) == 3, "The function should take three arguments"
assert (
"length" in params.keys()
and "width" in params.keys()
and "unit" in params.keys()
), "The function's parameters should be 'length', 'width' and 'unit'"

assert all(
p.annotation != inspect.Parameter.empty for p in params.values()
), "The function's parameters should have type hints"
assert (
return_annotation != inspect.Signature.empty
), "The function's return value is missing the type hint"
if function_to_test.__doc__ is None:
errors.append("The function is missing a docstring.")
if len(params) != 3:
errors.append("The function should take three arguments.")
if (
"length" not in params.keys()
or "width" not in params.keys()
or "unit" not in params.keys()
):
errors.append(
"The function's parameters should be 'length', 'width' and 'unit'."
)
if "unit" in params.keys() and not (
params["unit"].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
and params["unit"].default == "cm"
):
errors.append("Argument 'unit' should have a default value 'cm'.")
if any(p.annotation == inspect.Parameter.empty for p in params.values()):
errors.append("The function's parameters should have type hints.")
if return_annotation == inspect.Signature.empty:
errors.append("The function's return value is missing the type hint.")

assert not errors, errors_to_list(errors)


@pytest.mark.parametrize(
"length,width,unit,expected",
"length,width,unit",
[
(2.0, 3.0, "cm", (6.0, "cm^2")),
(4.0, 5.0, "m", (200000.0, "cm^2")),
(10.0, 2.0, "mm", (2000.0, "cm^2")),
(2.0, 8.0, "yd", (133780.38, "cm^2")),
(5.0, 4.0, "ft", (18580.608, "cm^2")),
(3.0, 5.0, "in", (96.774, "cm^2")),
(2.0, 3.0, "cm"),
(4.0, 5.0, "m"),
(10.0, 2.0, "mm"),
(2.0, 8.0, "yd"),
(5.0, 4.0, "ft"),
(3.0, 5.0, "in"),
],
)
def test_calculate_area_result(
length: float,
width: float,
unit: str,
expected: Tuple[float, str],
function_to_test,
) -> None:
result = function_to_test(length, width, unit)
test_result = reference_calculate_area(length, width, unit)
errors = []

if unit in ("cm", "m", "mm", "yd", "ft"):
assert isinstance(result, Tuple), "The function should return a tuple"

assert "cm^2" in result, "The result should be in squared centimeters (cm^2)"

# Double-check the reference solution
assert test_result[0] == pytest.approx(
expected[0], abs=0.01
), "The reference solution is incorrect"

assert result[0] == pytest.approx(expected[0], abs=0.01)
result = function_to_test(length, width, unit)

if not isinstance(result, str):
errors.append("The function should return a string.")
if "cm^2" not in result:
errors.append("The result should be in squared centimeters (cm^2).")
if result != reference_calculate_area(length, width, unit):
errors.append("The solution is incorrect.")
else:
assert isinstance(
result, str
), "The function should return an error string for unsupported units"
assert (
result == f"Invalid unit: {unit}"
), "The error message is incorrectly formatted"
try:
result = function_to_test(length, width, unit)
except KeyError:
errors.append(
"The function should return an error string for unsupported units."
)
else:
if result != f"Invalid unit: {unit}":
errors.append("The error message is incorrectly formatted.")

assert not errors


#
Expand Down
2 changes: 1 addition & 1 deletion tutorial/tests/test_functions_advanced.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ def create_alphabet_file(tmp_path: pathlib.Path):
d.mkdir()
p = d / "alphabet.txt"

text = "\n".join(f"{one_letter*20}" for one_letter in lowercase) + "\n"
text = "\n".join(f"{one_letter * 20}" for one_letter in lowercase) + "\n"

p.write_text(text)

Expand Down
2 changes: 1 addition & 1 deletion tutorial/tests/testsuite/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def format_error(exception: BaseException) -> str:
)

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

return formatted_message

Expand Down