Skip to content

Commit d77deeb

Browse files
BethanyGkotp
andauthored
[Practice Exercises]: Add Better Error Handling Instructions & Tests for Error Raising Messages (# 3 of 8) (#2692)
* Added instruciton append for error handling, updated JinJa2 template and example and regenerated test file. * Added instruction append on error handling and edited JinJa2 template and example.py. Regenerated test file. * Added instructions append for error handling, updated JinJa2 template and example, and regenerated test files. * Added instructions append for error handling, updated JinJa2 template and example, and regenerated test files. * Regenerated go-counting test file. Again. * EOL Co-authored-by: Victor Goff <keeperotphones@gmail.com>
1 parent bac3c13 commit d77deeb

File tree

16 files changed

+237
-77
lines changed

16 files changed

+237
-77
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Instructions append
2+
3+
## Customizing and Raising Exceptions
4+
5+
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.
6+
7+
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).
8+
9+
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.
10+
11+
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 the stack is not sufficiently filled. The tests will only pass if you customize an appropriate exception, `raise` that exception, and include appropriate error messages.
12+
13+
14+
```python
15+
# subclassing the Exception to create a StackUnderflowError
16+
class StackUnderflowError(Exception):
17+
"""Exception raised when Stack is not full.
18+
message: explanation of the error.
19+
"""
20+
def __init__(self, message):
21+
self.message = message
22+
23+
24+
# raising a StackUnderflowError
25+
raise StackUnderflowError("Insufficient number of items in stack")
26+
```
27+
28+
Additionally, this exercise requires that you raise several `built-in exceptions` with error messages.
29+
To raise a `built-in exception` with a message, write the message as an argument to the `exception` type:
30+
31+
```python
32+
# an example when division by zero is attempted.
33+
raise ZeroDivisionError("divide by zero")
34+
35+
#an example when the operation is undefined.
36+
raise ValueError("undefined operation")
37+
```

exercises/practice/forth/.meta/example.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
class StackUnderflowError(Exception):
2-
pass
2+
"""Exception raised when Stack is not full.
3+
message: explanation of the error.
4+
"""
5+
def __init__(self, message):
6+
self.message = message
37

48

59
def is_integer(string):
@@ -20,11 +24,11 @@ def evaluate(input_data):
2024
values.pop(0)
2125
key = values.pop(0).lower()
2226
if is_integer(key):
23-
raise ValueError("Integers cannot be redefined")
27+
raise ValueError("illegal operation")
2428
defines[key] = [
25-
x
26-
for v in values
27-
for x in defines.get(v, [v])
29+
x
30+
for v in values
31+
for x in defines.get(v, [v])
2832
]
2933
stack = []
3034
input_data = input_data[-1].split()
@@ -44,7 +48,7 @@ def evaluate(input_data):
4448
elif word == '/':
4549
divisor = stack.pop()
4650
if divisor == 0:
47-
raise ZeroDivisionError("Attempted to divide by zero")
51+
raise ZeroDivisionError("divide by zero")
4852
stack.append(int(stack.pop() / divisor))
4953
elif word == 'dup':
5054
stack.append(stack[-1])
@@ -56,7 +60,7 @@ def evaluate(input_data):
5660
elif word == 'over':
5761
stack.append(stack[-2])
5862
else:
59-
raise ValueError("{} has not been defined".format(word))
63+
raise ValueError("undefined operation")
6064
except IndexError:
6165
raise StackUnderflowError("Insufficient number of items in stack")
6266
return stack

exercises/practice/forth/.meta/template.j2

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,23 @@
77
{%- if case is error_case %}
88
{%- if case["expected"]["error"] == "divide by zero" %}
99
# {{ case["expected"]["error"] }}
10-
with self.assertRaisesWithMessage(ZeroDivisionError):
10+
with self.assertRaises(ZeroDivisionError) as err:
11+
{{ case["property"] }}({{ case["input"]["instructions"] }})
12+
self.assertEqual(type(err.exception), ZeroDivisionError)
13+
self.assertEqual(str(err.exception.args[0]), "{{ case["expected"]["error"] }}")
1114
{%- else %}
1215
{%- if "stack" in case["expected"]["error"] %}
13-
with self.assertRaisesWithMessage(StackUnderflowError):
16+
with self.assertRaises(StackUnderflowError) as err:
17+
{{ case["property"] }}({{ case["input"]["instructions"] }})
18+
self.assertEqual(type(err.exception), StackUnderflowError)
19+
self.assertEqual(str(err.exception.args[0]), "Insufficient number of items in stack")
1420
{%- else %}
15-
with self.assertRaisesWithMessage(ValueError):
21+
with self.assertRaises(ValueError) as err:
22+
{{ case["property"] }}({{ case["input"]["instructions"] }})
23+
self.assertEqual(type(err.exception), ValueError)
24+
self.assertEqual(str(err.exception.args[0]), "{{ case["expected"]["error"] }}")
1625
{%- endif %}
1726
{%- endif %}
18-
{{ case["property"] }}({{ case["input"]["instructions"] }})
1927
{%- else %}
2028
self.assertEqual({{ case["property"] }}({{ case["input"]["instructions"] }}), {{ case["expected"] }})
2129
{%- endif %}
@@ -27,5 +35,3 @@ class {{ exercise | camel_case }}Test(unittest.TestCase):
2735
{{ test_case(case["description"], subcase) }}
2836
{% endfor %}
2937
{% endfor %}
30-
31-
{{ macros.footer() }}

exercises/practice/forth/forth_test.py

Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,34 +16,58 @@ def test_addition_can_add_two_numbers(self):
1616
self.assertEqual(evaluate(["1 2 +"]), [3])
1717

1818
def test_addition_errors_if_there_is_nothing_on_the_stack(self):
19-
with self.assertRaisesWithMessage(StackUnderflowError):
19+
with self.assertRaises(StackUnderflowError) as err:
2020
evaluate(["+"])
21+
self.assertEqual(type(err.exception), StackUnderflowError)
22+
self.assertEqual(
23+
str(err.exception.args[0]), "Insufficient number of items in stack"
24+
)
2125

2226
def test_addition_errors_if_there_is_only_one_value_on_the_stack(self):
23-
with self.assertRaisesWithMessage(StackUnderflowError):
27+
with self.assertRaises(StackUnderflowError) as err:
2428
evaluate(["1 +"])
29+
self.assertEqual(type(err.exception), StackUnderflowError)
30+
self.assertEqual(
31+
str(err.exception.args[0]), "Insufficient number of items in stack"
32+
)
2533

2634
def test_subtraction_can_subtract_two_numbers(self):
2735
self.assertEqual(evaluate(["3 4 -"]), [-1])
2836

2937
def test_subtraction_errors_if_there_is_nothing_on_the_stack(self):
30-
with self.assertRaisesWithMessage(StackUnderflowError):
38+
with self.assertRaises(StackUnderflowError) as err:
3139
evaluate(["-"])
40+
self.assertEqual(type(err.exception), StackUnderflowError)
41+
self.assertEqual(
42+
str(err.exception.args[0]), "Insufficient number of items in stack"
43+
)
3244

3345
def test_subtraction_errors_if_there_is_only_one_value_on_the_stack(self):
34-
with self.assertRaisesWithMessage(StackUnderflowError):
46+
with self.assertRaises(StackUnderflowError) as err:
3547
evaluate(["1 -"])
48+
self.assertEqual(type(err.exception), StackUnderflowError)
49+
self.assertEqual(
50+
str(err.exception.args[0]), "Insufficient number of items in stack"
51+
)
3652

3753
def test_multiplication_can_multiply_two_numbers(self):
3854
self.assertEqual(evaluate(["2 4 *"]), [8])
3955

4056
def test_multiplication_errors_if_there_is_nothing_on_the_stack(self):
41-
with self.assertRaisesWithMessage(StackUnderflowError):
57+
with self.assertRaises(StackUnderflowError) as err:
4258
evaluate(["*"])
59+
self.assertEqual(type(err.exception), StackUnderflowError)
60+
self.assertEqual(
61+
str(err.exception.args[0]), "Insufficient number of items in stack"
62+
)
4363

4464
def test_multiplication_errors_if_there_is_only_one_value_on_the_stack(self):
45-
with self.assertRaisesWithMessage(StackUnderflowError):
65+
with self.assertRaises(StackUnderflowError) as err:
4666
evaluate(["1 *"])
67+
self.assertEqual(type(err.exception), StackUnderflowError)
68+
self.assertEqual(
69+
str(err.exception.args[0]), "Insufficient number of items in stack"
70+
)
4771

4872
def test_division_can_divide_two_numbers(self):
4973
self.assertEqual(evaluate(["12 3 /"]), [4])
@@ -53,16 +77,26 @@ def test_division_performs_integer_division(self):
5377

5478
def test_division_errors_if_dividing_by_zero(self):
5579
# divide by zero
56-
with self.assertRaisesWithMessage(ZeroDivisionError):
80+
with self.assertRaises(ZeroDivisionError) as err:
5781
evaluate(["4 0 /"])
82+
self.assertEqual(type(err.exception), ZeroDivisionError)
83+
self.assertEqual(str(err.exception.args[0]), "divide by zero")
5884

5985
def test_division_errors_if_there_is_nothing_on_the_stack(self):
60-
with self.assertRaisesWithMessage(StackUnderflowError):
86+
with self.assertRaises(StackUnderflowError) as err:
6187
evaluate(["/"])
88+
self.assertEqual(type(err.exception), StackUnderflowError)
89+
self.assertEqual(
90+
str(err.exception.args[0]), "Insufficient number of items in stack"
91+
)
6292

6393
def test_division_errors_if_there_is_only_one_value_on_the_stack(self):
64-
with self.assertRaisesWithMessage(StackUnderflowError):
94+
with self.assertRaises(StackUnderflowError) as err:
6595
evaluate(["1 /"])
96+
self.assertEqual(type(err.exception), StackUnderflowError)
97+
self.assertEqual(
98+
str(err.exception.args[0]), "Insufficient number of items in stack"
99+
)
66100

67101
def test_combined_arithmetic_addition_and_subtraction(self):
68102
self.assertEqual(evaluate(["1 2 + 4 -"]), [-1])
@@ -77,8 +111,12 @@ def test_dup_copies_the_top_value_on_the_stack(self):
77111
self.assertEqual(evaluate(["1 2 dup"]), [1, 2, 2])
78112

79113
def test_dup_errors_if_there_is_nothing_on_the_stack(self):
80-
with self.assertRaisesWithMessage(StackUnderflowError):
114+
with self.assertRaises(StackUnderflowError) as err:
81115
evaluate(["dup"])
116+
self.assertEqual(type(err.exception), StackUnderflowError)
117+
self.assertEqual(
118+
str(err.exception.args[0]), "Insufficient number of items in stack"
119+
)
82120

83121
def test_drop_removes_the_top_value_on_the_stack_if_it_is_the_only_one(self):
84122
self.assertEqual(evaluate(["1 drop"]), [])
@@ -87,8 +125,12 @@ def test_drop_removes_the_top_value_on_the_stack_if_it_is_not_the_only_one(self)
87125
self.assertEqual(evaluate(["1 2 drop"]), [1])
88126

89127
def test_drop_errors_if_there_is_nothing_on_the_stack(self):
90-
with self.assertRaisesWithMessage(StackUnderflowError):
128+
with self.assertRaises(StackUnderflowError) as err:
91129
evaluate(["drop"])
130+
self.assertEqual(type(err.exception), StackUnderflowError)
131+
self.assertEqual(
132+
str(err.exception.args[0]), "Insufficient number of items in stack"
133+
)
92134

93135
def test_swap_swaps_the_top_two_values_on_the_stack_if_they_are_the_only_ones(self):
94136
self.assertEqual(evaluate(["1 2 swap"]), [2, 1])
@@ -99,12 +141,20 @@ def test_swap_swaps_the_top_two_values_on_the_stack_if_they_are_not_the_only_one
99141
self.assertEqual(evaluate(["1 2 3 swap"]), [1, 3, 2])
100142

101143
def test_swap_errors_if_there_is_nothing_on_the_stack(self):
102-
with self.assertRaisesWithMessage(StackUnderflowError):
144+
with self.assertRaises(StackUnderflowError) as err:
103145
evaluate(["swap"])
146+
self.assertEqual(type(err.exception), StackUnderflowError)
147+
self.assertEqual(
148+
str(err.exception.args[0]), "Insufficient number of items in stack"
149+
)
104150

105151
def test_swap_errors_if_there_is_only_one_value_on_the_stack(self):
106-
with self.assertRaisesWithMessage(StackUnderflowError):
152+
with self.assertRaises(StackUnderflowError) as err:
107153
evaluate(["1 swap"])
154+
self.assertEqual(type(err.exception), StackUnderflowError)
155+
self.assertEqual(
156+
str(err.exception.args[0]), "Insufficient number of items in stack"
157+
)
108158

109159
def test_over_copies_the_second_element_if_there_are_only_two(self):
110160
self.assertEqual(evaluate(["1 2 over"]), [1, 2, 1])
@@ -113,12 +163,20 @@ def test_over_copies_the_second_element_if_there_are_more_than_two(self):
113163
self.assertEqual(evaluate(["1 2 3 over"]), [1, 2, 3, 2])
114164

115165
def test_over_errors_if_there_is_nothing_on_the_stack(self):
116-
with self.assertRaisesWithMessage(StackUnderflowError):
166+
with self.assertRaises(StackUnderflowError) as err:
117167
evaluate(["over"])
168+
self.assertEqual(type(err.exception), StackUnderflowError)
169+
self.assertEqual(
170+
str(err.exception.args[0]), "Insufficient number of items in stack"
171+
)
118172

119173
def test_over_errors_if_there_is_only_one_value_on_the_stack(self):
120-
with self.assertRaisesWithMessage(StackUnderflowError):
174+
with self.assertRaises(StackUnderflowError) as err:
121175
evaluate(["1 over"])
176+
self.assertEqual(type(err.exception), StackUnderflowError)
177+
self.assertEqual(
178+
str(err.exception.args[0]), "Insufficient number of items in stack"
179+
)
122180

123181
def test_user_defined_words_can_consist_of_built_in_words(self):
124182
self.assertEqual(evaluate([": dup-twice dup dup ;", "1 dup-twice"]), [1, 1, 1])
@@ -146,12 +204,16 @@ def test_user_defined_words_can_define_word_that_uses_word_with_the_same_name(se
146204
self.assertEqual(evaluate([": foo 10 ;", ": foo foo 1 + ;", "foo"]), [11])
147205

148206
def test_user_defined_words_cannot_redefine_non_negative_numbers(self):
149-
with self.assertRaisesWithMessage(ValueError):
207+
with self.assertRaises(ValueError) as err:
150208
evaluate([": 1 2 ;"])
209+
self.assertEqual(type(err.exception), ValueError)
210+
self.assertEqual(str(err.exception.args[0]), "illegal operation")
151211

152212
def test_user_defined_words_errors_if_executing_a_non_existent_word(self):
153-
with self.assertRaisesWithMessage(ValueError):
213+
with self.assertRaises(ValueError) as err:
154214
evaluate(["foo"])
215+
self.assertEqual(type(err.exception), ValueError)
216+
self.assertEqual(str(err.exception.args[0]), "undefined operation")
155217

156218
def test_case_insensitivity_dup_is_case_insensitive(self):
157219
self.assertEqual(evaluate(["1 DUP Dup dup"]), [1, 1, 1, 1])
@@ -170,11 +232,3 @@ def test_case_insensitivity_user_defined_words_are_case_insensitive(self):
170232

171233
def test_case_insensitivity_definitions_are_case_insensitive(self):
172234
self.assertEqual(evaluate([": SWAP DUP Dup dup ;", "1 swap"]), [1, 1, 1, 1])
173-
174-
# Utility functions
175-
def assertRaisesWithMessage(self, exception):
176-
return self.assertRaisesRegex(exception, r".+")
177-
178-
179-
if __name__ == "__main__":
180-
unittest.main()
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Instructions append
2+
3+
## Exception messages
4+
5+
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.
6+
7+
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 given invalid coordinates. The tests will only pass if you both `raise` the `exception` and include a message with it.
8+
9+
To raise a `ValueError` with a message, write the message as an argument to the `exception` type:
10+
11+
```python
12+
# when the coordinates for the piece are invalid
13+
raise ValueError('Invalid coordinate')
14+
```

exercises/practice/go-counting/.meta/example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def walk(self, x, y,
3737

3838
def territory(self, x, y):
3939
if not self.valid(x, y):
40-
raise ValueError('invalid coordinate')
40+
raise ValueError('Invalid coordinate')
4141
if self.board[y][x] in STONES:
4242
return (NONE, set())
4343

exercises/practice/go-counting/.meta/template.j2

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,15 @@
1515

1616

1717
{% macro test_case(case) -%}
18+
{%- set expected = case["expected"] %}
19+
{%- set exp_error = expected["error"] %}
1820
def test_{{ case["description"] | to_snake }}(self):
1921
board = Board({{ case["input"]["board"] }})
2022
{%- if case is error_case %}
21-
with self.assertRaisesWithMessage(ValueError):
23+
with self.assertRaises(ValueError) as err:
2224
board.territory(x={{ case["input"]["x"] }}, y={{ case["input"]["y"] }})
25+
self.assertEqual(type(err.exception), ValueError)
26+
self.assertEqual(err.exception.args[0], "{{ exp_error }}")
2327
{%- else %}
2428
{%- if "owner" in case["expected"] %}
2529
stone, territory = board.territory(x={{ case["input"]["x"] }}, y={{ case["input"]["y"] }})

exercises/practice/go-counting/go_counting_test.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,23 +37,31 @@ def test_a_stone_and_not_a_territory_on_5x5_board(self):
3737

3838
def test_invalid_because_x_is_too_low_for_5x5_board(self):
3939
board = Board([" B ", " B B ", "B W B", " W W ", " W "])
40-
with self.assertRaisesWithMessage(ValueError):
40+
with self.assertRaises(ValueError) as err:
4141
board.territory(x=-1, y=1)
42+
self.assertEqual(type(err.exception), ValueError)
43+
self.assertEqual(err.exception.args[0], "Invalid coordinate")
4244

4345
def test_invalid_because_x_is_too_high_for_5x5_board(self):
4446
board = Board([" B ", " B B ", "B W B", " W W ", " W "])
45-
with self.assertRaisesWithMessage(ValueError):
47+
with self.assertRaises(ValueError) as err:
4648
board.territory(x=5, y=1)
49+
self.assertEqual(type(err.exception), ValueError)
50+
self.assertEqual(err.exception.args[0], "Invalid coordinate")
4751

4852
def test_invalid_because_y_is_too_low_for_5x5_board(self):
4953
board = Board([" B ", " B B ", "B W B", " W W ", " W "])
50-
with self.assertRaisesWithMessage(ValueError):
54+
with self.assertRaises(ValueError) as err:
5155
board.territory(x=1, y=-1)
56+
self.assertEqual(type(err.exception), ValueError)
57+
self.assertEqual(err.exception.args[0], "Invalid coordinate")
5258

5359
def test_invalid_because_y_is_too_high_for_5x5_board(self):
5460
board = Board([" B ", " B B ", "B W B", " W W ", " W "])
55-
with self.assertRaisesWithMessage(ValueError):
61+
with self.assertRaises(ValueError) as err:
5662
board.territory(x=1, y=5)
63+
self.assertEqual(type(err.exception), ValueError)
64+
self.assertEqual(err.exception.args[0], "Invalid coordinate")
5765

5866
def test_one_territory_is_the_whole_board(self):
5967
board = Board([" "])

0 commit comments

Comments
 (0)