Skip to content

[Practice Exercises]: Add Better Error Handling Instructions & Tests for Error Raising Messages (# 6 of 8) #2725

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Nov 5, 2021
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
14 changes: 14 additions & 0 deletions exercises/practice/perfect-numbers/.docs/instructions.append.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Instructions append

## Exception messages

Sometimes it is necessary to [raise an exception](https://docs.python.org/3/tutorial/errors.html#raising-exceptions). When you do this, you should always include a **meaningful error message** to indicate what the source of the error is. This makes your code more readable and helps significantly with debugging. For situations where you know that the error source will be a certain type, you can choose to raise one of the [built in error types](https://docs.python.org/3/library/exceptions.html#base-classes), but should still include a meaningful message.

This particular exercise requires that you use the [raise statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement) to "throw" a `ValueError` if the `classify()` function is passed a number that is not a _positive integer_. The tests will only pass if you both `raise` the `exception` and include a message with it.

To raise a `ValueError` with a message, write the message as an argument to the `exception` type:

```python
# if a number to be classified is less than 1.
raise ValueError("Classification is only possible for positive integers.")
```
40 changes: 25 additions & 15 deletions exercises/practice/perfect-numbers/.meta/example.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
def divisor_generator(n):
''' Returns an unordered list of divisors for n (1 < n). '''
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
yield i
if i * i != n:
yield n // i

def divisor_generator(number):
"""Returns an unordered list of divisors for n (1 < number).

def classify(n):
''' A perfect number equals the sum of its positive divisors. '''
if n <= 0:
raise ValueError("Classification is only possible" +
" for positive whole numbers.")
:param number: int a positive integer
:return: list of int divisors
"""

aliquot_sum = sum(divisor_generator(n)) + (1 if n > 1 else 0)
for index in range(2, int(number ** 0.5) + 1):
if number % index == 0:
yield index
if index * index != number:
yield number // index

if aliquot_sum < n:

def classify(number):
""" A perfect number equals the sum of its positive divisors.

:param number: int a positive integer
:return: str the classification of the input integer
"""

if number <= 0:
raise ValueError("Classification is only possible for positive integers.")

aliquot_sum = sum(divisor_generator(number)) + (1 if number > 1 else 0)

if aliquot_sum < number:
return "deficient"
elif aliquot_sum == n:
elif aliquot_sum == number:
return "perfect"
else:
return "abundant"
6 changes: 3 additions & 3 deletions exercises/practice/perfect-numbers/.meta/template.j2
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
{%- set input = case["input"] -%}
def test_{{ case["description"] | to_snake }}(self):
{%- if case["expected"]["error"] is string %}
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
{{ case["property"] | to_snake }}({{input["number"]}})
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(err.exception.args[0], "{{ case["expected"]["error"] }}")
{% else %}
self.assertIs(
{{ case["property"] | to_snake }}({{input["number"]}}),
Expand All @@ -20,5 +22,3 @@ class {{ case["description"] | camel_case }}Test(unittest.TestCase):
{{ test_case(subcase) }}
{% endfor %}
{% endfor %}

{{ macros.footer() }}
5 changes: 5 additions & 0 deletions exercises/practice/perfect-numbers/perfect_numbers.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
def classify(number):
""" A perfect number equals the sum of its positive divisors.

:param number: int a positive integer
:return: str the classification of the input integer
"""
pass
22 changes: 12 additions & 10 deletions exercises/practice/perfect-numbers/perfect_numbers_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,19 @@ def test_edge_case_no_factors_other_than_itself_is_classified_correctly(self):

class InvalidInputsTest(unittest.TestCase):
def test_zero_is_rejected_as_it_is_not_a_positive_integer(self):
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
classify(0)
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(
err.exception.args[0],
"Classification is only possible for positive integers.",
)

def test_negative_integer_is_rejected_as_it_is_not_a_positive_integer(self):
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
classify(-1)

# Utility functions
def assertRaisesWithMessage(self, exception):
return self.assertRaisesRegex(exception, r".+")


if __name__ == "__main__":
unittest.main()
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(
err.exception.args[0],
"Classification is only possible for positive integers.",
)
38 changes: 38 additions & 0 deletions exercises/practice/phone-number/.docs/instructions.append.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Instructions append

## Exception messages

Sometimes it is necessary to [raise an exception](https://docs.python.org/3/tutorial/errors.html#raising-exceptions). When you do this, you should always include a **meaningful error message** to indicate what the source of the error is. This makes your code more readable and helps significantly with debugging. For situations where you know that the error source will be a certain type, you can choose to raise one of the [built in error types](https://docs.python.org/3/library/exceptions.html#base-classes), but should still include a meaningful message.

This particular exercise requires that you use the [raise statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement) to "throw" multiple `ValueErrors` if the `PhoneNumber()` class constructor is passed a number that is not a _valid phone number_. This includes errors for when area code or exchange codes are invalid, when the number has too many (or too few) digits, and for when punctuation or letters are given as input. The tests will only pass if you both `raise` the `exception` and include a message with it.

To raise a `ValueError` with a message, write the message as an argument to the `exception` type:

```python
# if a phone number has less than 10 digits.
raise ValueError("incorrect number of digits")

# if a phone number has more than 11 digits.
raise ValueError("more than 11 digits")

# if a phone number has 11 digits, but starts with a number other than 1.
raise ValueError("11 digits must start with 1")

# if a phone number has an exchange code that starts with 0.
raise ValueError("exchange code cannot start with zero")

# if a phone number has an exchange code that starts with 1.
raise ValueError("exchange code cannot start with one")

# if a phone number has an area code that starts with 0.
raise ValueError("area code cannot start with zero")

# if a phone number has an area code that starts with 1.
raise ValueError("area code cannot start with one")

# if a phone number has punctuation in place of some digits.
raise ValueError("punctuations not permitted")

# if a phone number has letters in place of some digits.
raise ValueError("letters not permitted")
```
37 changes: 30 additions & 7 deletions exercises/practice/phone-number/.meta/example.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
from string import punctuation


class PhoneNumber:
Expand All @@ -9,20 +10,42 @@ def __init__(self, number):
self.subscriber_number = self.number[-4:]

def pretty(self):
return "({})-{}-{}".format(
self.area_code, self.exchange_code, self.subscriber_number
)
return f"({self.area_code})-{self.exchange_code}-{self.subscriber_number}"

def _clean(self, number):
return self._normalize(re.sub(r"[^\d]", "", number))
preprocess = re.sub(r"[() +-.]", "", number)

if any(item for item in preprocess if item.isalpha()):
raise ValueError("letters not permitted")

if any(item for item in preprocess if item in punctuation):
raise ValueError("punctuations not permitted")

return self._normalize(preprocess)

def _normalize(self, number):
if len(number) < 10:
raise ValueError("incorrect number of digits")

if len(number) > 11:
raise ValueError("more than 11 digits")

if len(number) == 10 or len(number) == 11 and number.startswith("1"):
valid = number[-10] in "23456789" and number[-7] in "23456789"
if number[-10] == "0":
raise ValueError("area code cannot start with zero")
elif number[-10] == "1":
raise ValueError("area code cannot start with one")
elif number[-7] == "0":
raise ValueError("exchange code cannot start with zero")
elif number[-7] == "1":
raise ValueError("exchange code cannot start with one")
else:
valid = number[-10] in "23456789" and number[-7] in "23456789"

else:
valid = False
if number[0] in "023456789":
raise ValueError("11 digits must start with 1")

if valid:
return number[-10:]
else:
raise ValueError("{} is not a valid phone number".format(number))
6 changes: 3 additions & 3 deletions exercises/practice/phone-number/.meta/template.j2
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ class {{ class }}Test(unittest.TestCase):
{% for case in cases -%}
def test_{{ case["description"] | to_snake }}(self):
{% if "error" in case["expected"] -%}
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
{{ class }}("{{ case["input"]["phrase"] }}")
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(err.exception.args[0], "{{ case["expected"]["error"] }}")
{% else -%}
number = {{ class }}("{{ case["input"]["phrase"] }}").number
self.assertEqual(number, "{{ case["expected"] }}")
Expand All @@ -26,5 +28,3 @@ class {{ class }}Test(unittest.TestCase):
number = {{ class }}("{{ case["input"]["phrase"] }}")
self.assertEqual(number.{{ case["property"] }}{{ method }}, "{{ case["expected"] }}")
{% endfor %}

{{ macros.footer() }}
60 changes: 39 additions & 21 deletions exercises/practice/phone-number/phone_number_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@ def test_cleans_numbers_with_multiple_spaces(self):
self.assertEqual(number, "2234567890")

def test_invalid_when_9_digits(self):
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
PhoneNumber("123456789")
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(err.exception.args[0], "incorrect number of digits")

def test_invalid_when_11_digits_does_not_start_with_a_1(self):
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
PhoneNumber("22234567890")
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(err.exception.args[0], "11 digits must start with 1")

def test_valid_when_11_digits_and_starting_with_1(self):
number = PhoneNumber("12234567890").number
Expand All @@ -37,48 +41,70 @@ def test_valid_when_11_digits_and_starting_with_1_even_with_punctuation(self):
self.assertEqual(number, "2234567890")

def test_invalid_when_more_than_11_digits(self):
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
PhoneNumber("321234567890")
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(err.exception.args[0], "more than 11 digits")

def test_invalid_with_letters(self):
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
PhoneNumber("123-abc-7890")
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(err.exception.args[0], "letters not permitted")

def test_invalid_with_punctuations(self):
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
PhoneNumber("123-@:!-7890")
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(err.exception.args[0], "punctuations not permitted")

def test_invalid_if_area_code_starts_with_0(self):
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
PhoneNumber("(023) 456-7890")
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(err.exception.args[0], "area code cannot start with zero")

def test_invalid_if_area_code_starts_with_1(self):
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
PhoneNumber("(123) 456-7890")
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(err.exception.args[0], "area code cannot start with one")

def test_invalid_if_exchange_code_starts_with_0(self):
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
PhoneNumber("(223) 056-7890")
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(err.exception.args[0], "exchange code cannot start with zero")

def test_invalid_if_exchange_code_starts_with_1(self):
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
PhoneNumber("(223) 156-7890")
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(err.exception.args[0], "exchange code cannot start with one")

def test_invalid_if_area_code_starts_with_0_on_valid_11_digit_number(self):
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
PhoneNumber("1 (023) 456-7890")
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(err.exception.args[0], "area code cannot start with zero")

def test_invalid_if_area_code_starts_with_1_on_valid_11_digit_number(self):
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
PhoneNumber("1 (123) 456-7890")
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(err.exception.args[0], "area code cannot start with one")

def test_invalid_if_exchange_code_starts_with_0_on_valid_11_digit_number(self):
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
PhoneNumber("1 (223) 056-7890")
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(err.exception.args[0], "exchange code cannot start with zero")

def test_invalid_if_exchange_code_starts_with_1_on_valid_11_digit_number(self):
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
PhoneNumber("1 (223) 156-7890")
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(err.exception.args[0], "exchange code cannot start with one")

# Additional tests for this track
def test_area_code(self):
Expand All @@ -92,11 +118,3 @@ def test_pretty_print(self):
def test_pretty_print_with_full_us_phone_number(self):
number = PhoneNumber("12234567890")
self.assertEqual(number.pretty(), "(223)-456-7890")

# Utility functions
def assertRaisesWithMessage(self, exception):
return self.assertRaisesRegex(exception, r".+")


if __name__ == "__main__":
unittest.main()
17 changes: 17 additions & 0 deletions exercises/practice/pov/.docs/instructions.append.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Instructions append

## Exception messages

Sometimes it is necessary to [raise an exception](https://docs.python.org/3/tutorial/errors.html#raising-exceptions). When you do this, you should always include a **meaningful error message** to indicate what the source of the error is. This makes your code more readable and helps significantly with debugging. For situations where you know that the error source will be a certain type, you can choose to raise one of the [built in error types](https://docs.python.org/3/library/exceptions.html#base-classes), but should still include a meaningful message.

This particular exercise requires that you use the [raise statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement) to "throw" multiple `ValueErrors` if the `Tree()` class is passed a tree that cannot be reoriented, or a path cannot be found between a `start node` and an `end node`. The tests will only pass if you both `raise` the `exception` and include a message with it.

To raise a `ValueError` with a message, write the message as an argument to the `exception` type:

```python
# when a tree cannot be oriented to a new node POV
raise ValueError("Tree could not be reoriented")

#when a path cannot be found between a start and end node on the tree.
raise ValueError("No path found")
```
Loading