Skip to content

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

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 4 commits into from
Nov 1, 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
20 changes: 18 additions & 2 deletions exercises/practice/hangman/.docs/instructions.append.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
# Hints
# Instructions append

Please ignore the part regarding FRP library, a third party library is not required for this exercise.
## Python Special Instructions

A third party library **is not required** for this exercise. Please ignore the instructions regarding **FRP library**.


## 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` when the game has ended but the player tries to continue playing. 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 player tries to play, but the game is already over.
raise ValueError("The game has already ended.")
```
2 changes: 1 addition & 1 deletion exercises/practice/hangman/.meta/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def __init__(self, word):

def guess(self, char):
if self.status != STATUS_ONGOING:
raise ValueError("Game already ended, you " + self.status)
raise ValueError("The game has already ended.")

self.update_remaining_guesses(char)
self.update_masked_word()
Expand Down
16 changes: 6 additions & 10 deletions exercises/practice/hangman/hangman_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ def test_after_10_failures_the_game_is_over(self):
game.guess('x')

self.assertEqual(game.get_status(), hangman.STATUS_LOSE)
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
game.guess('x')
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(err.exception.args[0], "The game has already ended.")

def test_feeding_a_correct_letter_removes_underscores(self):
game = Hangman('foobar')
Expand Down Expand Up @@ -80,8 +82,10 @@ def test_getting_all_the_letters_right_makes_for_a_win(self):
self.assertEqual(game.get_status(), hangman.STATUS_WIN)
self.assertEqual(game.get_masked_word(), 'hello')

with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
game.guess('x')
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(err.exception.args[0], "The game has already ended.")

def test_winning_on_last_guess_still_counts_as_a_win(self):
game = Hangman('aaa')
Expand All @@ -91,11 +95,3 @@ def test_winning_on_last_guess_still_counts_as_a_win(self):
self.assertEqual(game.remaining_guesses, 0)
self.assertEqual(game.get_status(), hangman.STATUS_WIN)
self.assertEqual(game.get_masked_word(), 'aaa')

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


if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
# Instructions append

Implementation note:
In case of invalid inputs to the 'largest_product' function
your program should raise a ValueError with a meaningful error message.
Feel free to reuse your code from the 'series' exercise!
## 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` when your `largest_product()` function receives invalid input. The tests will only pass if you both `raise` the `exception` and include a message with it. Feel free to reuse your code from the `series` exercise!

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

```python
# span of numbers is longer than number series
raise ValueError("span must be smaller than string length")

# span of number is zero or negative
raise ValueError("span must be greater than zero")

# series includes non-number input
raise ValueError("digits input must only contain digits")
```
12 changes: 9 additions & 3 deletions exercises/practice/largest-series-product/.meta/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@


def slices(series, length):

if not length <= len(series):
raise ValueError("span must be smaller than string length")
elif not 0 < length:
raise ValueError("span must be greater than zero")
elif not all(item.isdigit() for item in series):
raise ValueError("digits input must only contain digits")

numbers = [int(digit) for digit in series]
if not 1 <= length <= len(numbers):
raise ValueError("Invalid slice length for this series: " +
str(length))

return [numbers[i:i + length]
for i in range(len(numbers) - length + 1)]

Expand Down
6 changes: 3 additions & 3 deletions exercises/practice/largest-series-product/.meta/template.j2
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ class {{ exercise | camel_case }}Test(unittest.TestCase):
{% for case in cases -%}
def test_{{ case["description"] | to_snake }}(self):
{%- if case is error_case %}
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
{{ test_call(case) }}
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(err.exception.args[0], "{{ case["expected"]["error"] }}")
{%- else %}
self.assertEqual({{ test_call(case) }}, {{ case["expected"] }})
{%- endif %}
Expand All @@ -26,5 +28,3 @@ class {{ exercise | camel_case }}Test(unittest.TestCase):
self.assertEqual({{ test_call(case) }}, {{ case["expected"] }})
{%- endif %}
{% endfor %}

{{ macros.footer() }}
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,12 @@ def test_reports_zero_if_all_spans_include_zero(self):
self.assertEqual(largest_product("99099", 3), 0)

def test_rejects_span_longer_than_string_length(self):
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
largest_product("123", 4)
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(
err.exception.args[0], "span must be smaller than string length"
)

def test_reports_1_for_empty_string_and_empty_product_0_span(self):
self.assertEqual(largest_product("", 0), 1)
Expand All @@ -49,16 +53,24 @@ def test_reports_1_for_nonempty_string_and_empty_product_0_span(self):
self.assertEqual(largest_product("123", 0), 1)

def test_rejects_empty_string_and_nonzero_span(self):
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
largest_product("", 1)
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(
err.exception.args[0], "span must be smaller than string length"
)

def test_rejects_invalid_character_in_digits(self):
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
largest_product("1234a5", 2)
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(err.exception.args[0], "digits input must only contain digits")

def test_rejects_negative_span(self):
with self.assertRaisesWithMessage(ValueError):
with self.assertRaises(ValueError) as err:
largest_product("12345", -1)
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(err.exception.args[0], "span must be greater than zero")

# Additional tests for this track
def test_euler_big_number(self):
Expand All @@ -69,11 +81,3 @@ def test_euler_big_number(self):
),
23514624000,
)

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


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

## Customizing and Raising Exceptions

Sometimes it is necessary to both [customize](https://docs.python.org/3/tutorial/errors.html#user-defined-exceptions) and [`raise`](https://docs.python.org/3/tutorial/errors.html#raising-exceptions) exceptions in your code. 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.

Custom exceptions can be created through new exception classes (see [`classes`](https://docs.python.org/3/tutorial/classes.html#tut-classes) for more detail.) that are typically subclasses of [`Exception`](https://docs.python.org/3/library/exceptions.html#Exception).

For situations where you know the error source will be a derivative of a certain exception type, you can choose to inherit from one of the [`built in error types`](https://docs.python.org/3/library/exceptions.html#base-classes) under the _Exception_ class. When raising the error, you should still include a meaningful message.

This particular exercise requires that you create a _custom exception_ to be [raised](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement)/"thrown" when your `meetup()` function is given a weekday name and number combination that is invalid. The tests will only pass if you customize appropriate exceptions, `raise` those exceptions, and include appropriate error messages.

To customize a `built-in exception`, create a `class` that inherits from that exception. When raising the custom exception with a message, write the message as an argument to the `exception` type:

```python
# subclassing the built-in ValueError to create MeetupDayException
class MeetupDayException(ValueError):
"""Exception raised when the Meetup weekday and count do not result in a valid date.

message: explanation of the error.

"""
def __init__(self, message):
self.message = message


# raising a MeetupDayException
raise MeetupDayException("That day does not exist.")
```
72 changes: 69 additions & 3 deletions exercises/practice/meetup/.meta/additional_tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,81 @@
"expected": "2015-03-30"
},
{
"description": "nonexistent fifth Monday of February 2015",
"description": "fifth Thursday of February 2024",
"property": "meetup",
"input": {
"year": 2015,
"year": 2024,
"month": 2,
"week": "5th",
"dayofweek": "Thursday"
},
"expected": "2024-02-29"
},
{
"description": "fifth Saturday of February 2020",
"property": "meetup",
"input": {
"year": 2020,
"month": 2,
"week": "5th",
"dayofweek": "Saturday"
},
"expected": "2020-02-29"
},
{
"description": "last Sunday of June 2024",
"property": "meetup",
"input": {
"year": 2024,
"month": 6,
"week": "last",
"dayofweek": "Sunday"
},
"expected": "2024-06-30"
},
{
"description": "teenth Friday of May 2022",
"property": "meetup",
"input": {
"year": 2022,
"month": 5,
"week": "teenth",
"dayofweek": "Friday"
},
"expected": "2022-05-13"
},
{
"description": "nonexistent fifth Monday of February 2022",
"property": "meetup",
"input": {
"year": 2022,
"month": 2,
"week": "5th",
"dayofweek": "Monday"
},
"expected": {"error": "day does not exist"}
"expected": {"error": "That day does not exist."}
},
{
"description": "nonexistent fifth Friday of August 2022",
"property": "meetup",
"input": {
"year": 2022,
"month": 8,
"week": "5th",
"dayofweek": "Friday"
},
"expected": {"error": "That day does not exist."}
},
{
"description": "nonexistent fifth Thursday of May 2023",
"property": "meetup",
"input": {
"year": 2023,
"month": 5,
"week": "5th",
"dayofweek": "Thursday"
},
"expected": {"error": "That day does not exist."}
}
]
}
12 changes: 9 additions & 3 deletions exercises/practice/meetup/.meta/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@ def _choice(week):
def _func(dates):
if day < len(dates):
return dates[day]
raise MeetupDayException('day does not exist')
raise MeetupDayException('That day does not exist.')
return _func


class MeetupDayException(Exception):
pass
class MeetupDayException(ValueError):
"""Exception raised when the Meetup weekday and count do not result in a valid date.

message: explanation of the error.

"""
def __init__(self, message):
self.message = message
9 changes: 4 additions & 5 deletions exercises/practice/meetup/.meta/template.j2
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
{%- set input.month = case["input"]["month"] %}
{%- set input.week = case["input"]["week"] %}
{%- set input.dayofweek = case["input"]["dayofweek"] %}
{%- for (k, v) in {"first":"1st", "second":"2nd", "third":"3rd", "fourth":"4th", "fifth":"5th"}.items() %}
{%- for (k, v) in {"first":"1st", "second":"2nd", "third":"3rd", "fourth":"4th", "fifth":"5th", "sixth":"6th"}.items() %}
{%- set input.week = input.week.replace(k, v) %}
{%- endfor %}
{%- if case is error_case %}
with self.assertRaisesWithMessage(MeetupDayException):
with self.assertRaises(MeetupDayException) as err:
{{ case["property"] }}({{ input.year }}, {{ input.month }}, "{{ input.week }}", "{{ input.dayofweek }}")
self.assertEqual(type(err.exception), MeetupDayException)
self.assertEqual(err.exception.args[0], "{{ case["expected"]["error"] }}")
{%- else %}
{%- set (year, month, day) = case["expected"].replace("-0", "-").split("-") %}
self.assertEqual({{ case["property"] }}(
Expand All @@ -34,6 +36,3 @@ class {{ exercise | camel_case }}Test(unittest.TestCase):
{{ test_case(case) }}
{% endfor %}
{%- endif %}


{{ macros.footer(True) }}
11 changes: 11 additions & 0 deletions exercises/practice/meetup/meetup.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,13 @@
# subclassing the built-in ValueError to create MeetupDayException
class MeetupDayException(ValueError):
"""Exception raised when the Meetup weekday and count do not result in a valid date.

message: explanation of the error.

"""
def __init__(self):
pass


def meetup(year, month, week, day_of_week):
pass
Loading