Skip to content

Commit 9373a75

Browse files
a little clean up.
1 parent 0e69edb commit 9373a75

2 files changed

Lines changed: 199 additions & 60 deletions

File tree

‎source/modules/DictionaryAsSwitch.rst‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ Many languages have a "switch-case" construct::
2929
return "nothing";
3030
};
3131

32+
.. Fixme -- use a Ruby example?
33+
3234
How do you say this in Python?
3335

3436
``if-elif`` chains

‎source/modules/TestDrivenDevelopment.rst‎

Lines changed: 197 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,6 +1218,8 @@ integer values for exactly that set of Roman numerals that
12181218
your requirements, this possibility may bother you; if so, write more
12191219
comprehensive test cases until it doesn't bother you.)
12201220

1221+
.. note:: Comprehensive test coverage is a bit of a fantasy. YOu can make sure that every line of code you write is run at least once during the testing. But you can't make sure that every function is called with *every* possible type and value! So what we can do is anticipate what we think might break our code, and test for that. Some things *will* slip through the cracks -- but here's a hint: when a bug is discovered, the first thing you should do is write a test that exercises that bug -- a test that will fail due to the bug. Then fix it. Since all your other test still pass (they do, don't they?) -- you know the fix hasn't broken anything else. ANd since you have a test for it -- you know you won't accidentally reintroduce that bug.
1222+
12211223

12221224
More Bad Input
12231225
--------------
@@ -1232,10 +1234,10 @@ regular expressions. (If you’re not familiar with regular expressions,
12321234
now would be a good time to read `the regular expressions
12331235
chapter <regular-expressions.html>`__.)
12341236

1235-
As you saw in `Case Study: Roman
1236-
Numerals <regular-expressions.html#romannumerals>`__, there are several
1237-
simple rules for constructing a Roman numeral, using the letters ``M``,
1238-
``D``, ``C``, ``L``, ``X``, ``V``, and ``I``. Let's review the rules:
1237+
As we saw earlier, there are several simple rules for constructing a Roman numeral, using the letters ``M``,
1238+
``D``, ``C``, ``L``, ``X``, ``V``, and ``I``.
1239+
1240+
Let's review the rules:
12391241

12401242
- Sometimes characters are additive. ``I`` is ``1``, ``II`` is ``2``,
12411243
and ``III`` is ``3``. ``VI`` is ``6`` (literally, “\ ``5`` and
@@ -1265,91 +1267,226 @@ simple rules for constructing a Roman numeral, using the letters ``M``,
12651267
it as ``XCIX``, “\ ``10`` less than ``100``, then ``1`` less than
12661268
``10``\ ”).
12671269

1268-
Thus, one useful test would be to ensure that the ``from_roman()``
1270+
Roman numerals can only use certain characters, so we should test to make sure there aren't any other characters in the input:
1271+
1272+
.. code-block:: python
1273+
1274+
def test_invalid_character():
1275+
"""
1276+
Roman numerals can only use these characters:
1277+
1278+
M, D, C, L, X, V, I
1279+
1280+
This tests that other characters will cause a failure
1281+
"""
1282+
for s in ['Z', 'XXIIIQ', 'QXXIII', 'XXYIII']:
1283+
with pytest.raises(ValueError):
1284+
from_roman(s)
1285+
1286+
Another useful test would be to ensure that the ``from_roman()``
12691287
function should fail when you pass it a string with too many repeated
12701288
numerals. How many is “too many” depends on the numeral.
12711289

12721290
.. code-block:: python
12731291
1274-
class FromRomanBadInput(unittest.TestCase):
1275-
def test_too_many_repeated_numerals(self):
1276-
'''from_roman should fail with too many repeated numerals'''
1277-
for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):
1278-
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
1292+
def test_too_many_repeated_numerals():
1293+
'''from_roman should fail with too many repeated numerals'''
1294+
for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):
1295+
with pytest.raises(ValueError):
1296+
from_roman(s)
12791297
12801298
Another useful test would be to check that certain patterns aren’t
12811299
repeated. For example, ``IX`` is ``9``, but ``IXIX`` is never valid.
12821300

12831301
.. code-block:: python
12841302
1285-
def test_repeated_pairs(self):
1286-
'''from_roman should fail with repeated pairs of numerals'''
1287-
for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):
1288-
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
1303+
def test_repeated_pairs():
1304+
'''from_roman should fail with repeated pairs of numerals'''
1305+
for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):
1306+
with pytest.raises(ValueError):
1307+
from_roman(s)
1308+
12891309
1290-
A third test could check that numerals appear in the correct order, from
1310+
A forth test could check that numerals appear in the correct order, from
12911311
highest to lowest value. For example, ``CL`` is ``150``, but ``LC`` is
12921312
never valid, because the numeral for ``50`` can never come before the
12931313
numeral for ``100``. This test includes a randomly chosen set of invalid
12941314
antecedents: ``I`` before ``M``, ``V`` before ``X``, and so on.
12951315

12961316
.. code-block:: python
12971317
1298-
def test_malformed_antecedents(self):
1299-
'''from_roman should fail with malformed antecedents'''
1300-
for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
1301-
'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):
1302-
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
1303-
1304-
Each of these tests relies the ``from_roman()`` function raising a new
1305-
exception, ``InvalidRomanNumeralError``, which we haven’t defined yet.
1306-
1307-
.. code-block:: python
1318+
def test_malformed_antecedents():
1319+
'''from_roman should fail with malformed antecedents'''
1320+
for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
1321+
'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):
1322+
with pytest.raises(ValueError):
1323+
from_roman(s)
13081324
1309-
# roman6.py
1310-
class InvalidRomanNumeralError(ValueError): pass
13111325
1312-
All three of these tests should fail, since the ``from_roman()``
1326+
All four of these tests should fail, since the ``from_roman()``
13131327
function doesn’t currently have any validity checking. (If they don’t
13141328
fail now, then what the heck are they testing?)
13151329

13161330
.. code-block::
13171331
1318-
you@localhost:~/diveintopython3/examples$ python3 romantest6.py
1319-
FFF.......
1320-
======================================================================
1321-
FAIL: test_malformed_antecedents (__main__.FromRomanBadInput)
1322-
from_roman should fail with malformed antecedents
1323-
----------------------------------------------------------------------
1324-
Traceback (most recent call last):
1325-
File "romantest6.py", line 113, in test_malformed_antecedents
1326-
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
1327-
AssertionError: InvalidRomanNumeralError not raised by from_roman
1328-
1329-
======================================================================
1330-
FAIL: test_repeated_pairs (__main__.FromRomanBadInput)
1331-
from_roman should fail with repeated pairs of numerals
1332-
----------------------------------------------------------------------
1333-
Traceback (most recent call last):
1334-
File "romantest6.py", line 107, in test_repeated_pairs
1335-
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
1336-
AssertionError: InvalidRomanNumeralError not raised by from_roman
1337-
1338-
======================================================================
1339-
FAIL: test_too_many_repeated_numerals (__main__.FromRomanBadInput)
1340-
from_roman should fail with too many repeated numerals
1341-
----------------------------------------------------------------------
1342-
Traceback (most recent call last):
1343-
File "romantest6.py", line 102, in test_too_many_repeated_numerals
1344-
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
1345-
AssertionError: InvalidRomanNumeralError not raised by from_roman
1332+
In [61]: ! pytest roman11.py
1333+
============================ test session starts ============================
1334+
platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
1335+
rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/examples/test_driven_development
1336+
collected 12 items
1337+
1338+
roman11.py ........FFFF [100%]
1339+
1340+
================================= FAILURES ==================================
1341+
__________________________ test_invalid_character ___________________________
1342+
1343+
def test_invalid_character():
1344+
"""
1345+
Roman numerals can only use these characters:
1346+
1347+
M, D, C, L, X, V, I
1348+
1349+
This tests that other characters will cause a failure
1350+
"""
1351+
for s in ['Z', 'XXIIIQ', 'QXXIII', 'XXYIII']:
1352+
with pytest.raises(ValueError):
1353+
> from_roman(s)
1354+
E Failed: DID NOT RAISE <class 'ValueError'>
1355+
1356+
roman11.py:191: Failed
1357+
______________________ test_too_many_repeated_numerals ______________________
1358+
1359+
def test_too_many_repeated_numerals():
1360+
'''from_roman should fail with too many repeated numerals'''
1361+
for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):
1362+
with pytest.raises(ValueError):
1363+
> from_roman(s)
1364+
E Failed: DID NOT RAISE <class 'ValueError'>
1365+
1366+
roman11.py:198: Failed
1367+
____________________________ test_repeated_pairs ____________________________
1368+
1369+
def test_repeated_pairs():
1370+
'''from_roman should fail with repeated pairs of numerals'''
1371+
for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):
1372+
with pytest.raises(ValueError):
1373+
> from_roman(s)
1374+
E Failed: DID NOT RAISE <class 'ValueError'>
1375+
1376+
roman11.py:205: Failed
1377+
________________________ test_malformed_antecedents _________________________
1378+
1379+
def test_malformed_antecedents():
1380+
'''from_roman should fail with malformed antecedents'''
1381+
for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
1382+
'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):
1383+
with pytest.raises(ValueError):
1384+
> from_roman(s)
1385+
E Failed: DID NOT RAISE <class 'ValueError'>
1386+
1387+
roman11.py:213: Failed
1388+
========================== short test summary info ==========================
1389+
FAILED roman11.py::test_invalid_character - Failed: DID NOT RAISE <class '...
1390+
FAILED roman11.py::test_too_many_repeated_numerals - Failed: DID NOT RAISE...
1391+
FAILED roman11.py::test_repeated_pairs - Failed: DID NOT RAISE <class 'Val...
1392+
FAILED roman11.py::test_malformed_antecedents - Failed: DID NOT RAISE <cla...
1393+
======================== 4 failed, 8 passed in 0.13s ========================
1394+
1395+
1396+
Good deal -- yes, we *wanted* four tests to fail.
1397+
1398+
Now, "all" we need to do is write the code to check if the Roman numeral satisfies all the requirements.
1399+
1400+
So let's do that one requirement at a time:
1401+
1402+
.. note: This is actually a great use for "regular expressions". That is a topic all to itself, so we won't do that here. But if you are curious, you can read up on how to use regular expressions in Python to parse Roman Numerals in `Dive into Python 3 <https://diveintopython3.problemsolving.io/regular-expressions.html#romannumerals>`_
1403+
1404+
**Requirement:** you can only use the letters ``M``,
1405+
``D``, ``C``, ``L``, ``X``, ``V``, and ``I``.
1406+
1407+
So let's try that:
1408+
1409+
.. code-block:: Python
1410+
1411+
def is_valid_roman_numeral(s):
1412+
"""
1413+
check if the input is a valid roman numeral
1414+
1415+
returns True if it is, False other wise
1416+
"""
1417+
1418+
# does it use only valid characters?
1419+
for c in s:
1420+
if c not in "MDCLXVI":
1421+
return False
1422+
1423+
return True
1424+
1425+
1426+
This is the start of a function to test if a string is a valid Roman numeral. So far, it loops through all the characters in the string, and makes sure they are in the VALID_CHARS string. If not, then it returns False.
1427+
1428+
It is called in the ``from_roman`` function:
1429+
1430+
.. code-block:: Python
1431+
1432+
def from_roman(s):
1433+
"""convert Roman numeral to integer"""
1434+
if not is_valid_roman_numeral(s):
1435+
raise ValueError(f"{s} is not a valid Roman numeral")
1436+
...
1437+
1438+
1439+
Now that we have that, let's run the tests again:
1440+
1441+
.. code-block:: ipython
1442+
1443+
In [63]: ! pytest roman12.py
1444+
============================ test session starts ============================
1445+
1446+
...
1447+
1448+
========================== short test summary info ==========================
1449+
FAILED roman12.py::test_too_many_repeated_numerals - Failed: DID NOT RAISE...
1450+
FAILED roman12.py::test_repeated_pairs - Failed: DID NOT RAISE <class 'Val...
1451+
FAILED roman12.py::test_malformed_antecedents - Failed: DID NOT RAISE <cla...
1452+
======================== 3 failed, 9 passed in 0.14s ========================
1453+
1454+
Only three failures -- progess!
1455+
1456+
On to the next requirement: each character can only be repeated a certain number of times, so lets check to see if any appear to many times, but adding this to the ``is_valid_roman_numeral()`` function:
1457+
1458+
.. code-block:: python
1459+
1460+
# are any chars repeated too much?
1461+
for c in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):
1462+
if c in s:
1463+
return False
1464+
1465+
And run the tests again:
1466+
1467+
.. code-block:: ipython
1468+
1469+
In [64]: ! pytest roman13.py
1470+
============================ test session starts ============================
1471+
1472+
...
1473+
1474+
========================== short test summary info ==========================
1475+
FAILED roman13.py::test_repeated_pairs - Failed: DID NOT RAISE <class 'Val...
1476+
FAILED roman13.py::test_malformed_antecedents - Failed: DID NOT RAISE <cla...
1477+
======================= 2 failed, 10 passed in 0.14s ========================
1478+
1479+
Two down, two to go!
1480+
1481+
Requirement: Sometimes characters are the opposite of additive.
1482+
By putting certain characters before others, you subtract from the final value. But that pattern can't be repeated, so let's check for that:
1483+
1484+
.. code-block:: python
1485+
13461486
1347-
----------------------------------------------------------------------
1348-
Ran 10 tests in 0.058s
13491487
1350-
FAILED (failures=3)
13511488
1352-
Good deal. Now, all we need to do is add the `regular expression to test
1489+
Now, all we need to do is add the `regular expression to test
13531490
for valid Roman numerals <regular-expressions.html#romannumerals>`__
13541491
into the ``from_roman()`` function.
13551492

0 commit comments

Comments
 (0)