-
- | Whenever possible, you should use the functions in os and os.path for file, directory, and path manipulations. These modules are wrappers for platform-specific modules, so functions like
-os.path.split work on UNIX, Windows, Mac OS, and any other platform supported by Python.
-There is one other way to get the contents of a directory. It's very powerful, and it uses the sort of wildcards that you
-may already be familiar with from working on the command line.
- Example 6.20. Listing Directories with glob
->>> os.listdir("c:\\music\\_singles\\") ①
-['a_time_long_forgotten_con.mp3', 'hellraiser.mp3',
-'kairo.mp3', 'long_way_home1.mp3', 'sidewinder.mp3',
-'spinning.mp3']
->>> import glob
->>> glob.glob('c:\\music\\_singles\\*.mp3') ②
-['c:\\music\\_singles\\a_time_long_forgotten_con.mp3',
-'c:\\music\\_singles\\hellraiser.mp3',
-'c:\\music\\_singles\\kairo.mp3',
-'c:\\music\\_singles\\long_way_home1.mp3',
-'c:\\music\\_singles\\sidewinder.mp3',
-'c:\\music\\_singles\\spinning.mp3']
->>> glob.glob('c:\\music\\_singles\\s*.mp3') ③
-['c:\\music\\_singles\\sidewinder.mp3',
-'c:\\music\\_singles\\spinning.mp3']
->>> glob.glob('c:\\music\\*\\*.mp3')④
-
-
-- As you saw earlier,
os.listdir simply takes a directory path and lists all files and directories in that directory.
- - The
glob module, on the other hand, takes a wildcard and returns the full path of all files and directories matching the wildcard.
- Here the wildcard is a directory path plus "*.mp3", which will match all .mp3 files. Note that each element of the returned list already includes the full path of the file.
- - If you want to find all the files in a specific directory that start with "s" and end with ".mp3", you can do that too.
-
- Now consider this scenario: you have a
music directory, with several subdirectories within it, with .mp3 files within each subdirectory. You can get a list of all of those with a single call to glob, by using two wildcards at once. One wildcard is the "*.mp3" (to match .mp3 files), and one wildcard is within the directory path itself, to match any subdirectory within c:\music. That's a crazy amount of power packed into one deceptively simple-looking function!
-
- Further Reading on the os Module
-
-[HTML stuff was here]
@@ -690,731 +569,6 @@ def main(argv):
-[HTTP web services stuff was here]
-
-
-
-
-
-[unit testing stuff was here]
-
-
-
-
-
- Chapter 14. Test-First Programming
- 14.1. roman.py, stage 1
- Now that the unit tests are complete, it's time to start writing the code that the test cases are attempting to test. You're
- going to do this in stages, so you can see all the unit tests fail, then watch them pass one by one as you fill in the gaps
- in roman.py.
- Example 14.1. roman1.py
- This file is available in py/roman/stage1/ in the examples directory.
- If you have not already done so, you can download this and other examples used in this book.
-
-"""Convert to and from Roman numerals"""
-
-#Define exceptions
-class RomanError(Exception): pass ①
-class OutOfRangeError(RomanError): pass ②
-class NotIntegerError(RomanError): pass
-class InvalidRomanNumeralError(RomanError): pass ③
-
-def to_roman(n):
- """convert integer to Roman numeral"""
- pass ④
-
-def from_roman(s):
- """convert Roman numeral to integer"""
- pass
-
-
-- This is how you define your own custom exceptions in Python. Exceptions are classes, and you create your own by subclassing existing exceptions. It is strongly recommended (but not
- required) that you subclass
Exception, which is the base class that all built-in exceptions inherit from. Here I am defining RomanError (inherited from Exception) to act as the base class for all my other custom exceptions to follow. This is a matter of style; I could just as easily
- have inherited each individual exception from the Exception class directly.
- - The
OutOfRangeError and NotIntegerError exceptions will eventually be used by to_roman() to flag various forms of invalid input, as specified in ToRomanBadInput.
- - The
InvalidRomanNumeralError exception will eventually be used by from_roman() to flag invalid input, as specified in FromRomanBadInput.
- - At this stage, you want to define the API of each of your functions, but you don't want to code them yet, so you stub them out using the Python reserved word
pass.
-Now for the big moment (drum roll please): you're finally going to run the unit test against this stubby little module. At
-this point, every test case should fail. In fact, if any test case passes in stage 1, you should go back to romantest.py and re-evaluate why you coded a test so useless that it passes with do-nothing functions.
- - At this stage, you want to define the API of each of your functions, but you don't want to code them yet, so you stub them out using the Python reserved word
pass.
-Run romantest1.py with the -v command-line option, which will give more verbose output so you can see exactly what's going on as each test case runs.
-With any luck, your output should look like this:
- Example 14.2. Output of romantest1.py against roman1.pyfrom_roman should only accept uppercase input ... ERROR
-to_roman should always return uppercase ... ERROR
-from_roman should fail with malformed antecedents ... FAIL
-from_roman should fail with repeated pairs of numerals ... FAIL
-from_roman should fail with too many repeated numerals ... FAIL
-from_roman should give known result with known input ... FAIL
-to_roman should give known result with known input ... FAIL
-from_roman(to_roman(n))==n for all n ... FAIL
-to_roman should fail with non-integer input ... FAIL
-to_roman should fail with negative input ... FAIL
-to_roman should fail with large input ... FAIL
-to_roman should fail with 0 input ... FAIL
-
-======================================================================
-ERROR: from_roman should only accept uppercase input
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 154, in testFromRomanCase
- roman1.from_roman(numeral.upper())
-AttributeError: 'None' object has no attribute 'upper'
-======================================================================
-ERROR: to_roman should always return uppercase
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 148, in testToRomanCase
- self.assertEqual(numeral, numeral.upper())
-AttributeError: 'None' object has no attribute 'upper'
-======================================================================
-FAIL: from_roman should fail with malformed antecedents
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 133, in testMalformedAntecedent
- self.assertRaises(roman1.InvalidRomanNumeralError, roman1.from_roman, s)
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: InvalidRomanNumeralError
-======================================================================
-FAIL: from_roman should fail with repeated pairs of numerals
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 127, in testRepeatedPairs
- self.assertRaises(roman1.InvalidRomanNumeralError, roman1.from_roman, s)
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: InvalidRomanNumeralError
-======================================================================
-FAIL: from_roman should fail with too many repeated numerals
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 122, in testTooManyRepeatedNumerals
- self.assertRaises(roman1.InvalidRomanNumeralError, roman1.from_roman, s)
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: InvalidRomanNumeralError
-======================================================================
-FAIL: from_roman should give known result with known input
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 99, in testFromRomanKnownValues
- self.assertEqual(integer, result)
- File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
- raise self.failureException, (msg or '%s != %s' % (first, second))
-AssertionError: 1 != None
-======================================================================
-FAIL: to_roman should give known result with known input
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 93, in testToRomanKnownValues
- self.assertEqual(numeral, result)
- File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
- raise self.failureException, (msg or '%s != %s' % (first, second))
-AssertionError: I != None
-======================================================================
-FAIL: from_roman(to_roman(n))==n for all n
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 141, in testSanity
- self.assertEqual(integer, result)
- File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
- raise self.failureException, (msg or '%s != %s' % (first, second))
-AssertionError: 1 != None
-======================================================================
-FAIL: to_roman should fail with non-integer input
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 116, in testNonInteger
- self.assertRaises(roman1.NotIntegerError, roman1.to_roman, 0.5)
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: NotIntegerError
-======================================================================
-FAIL: to_roman should fail with negative input
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 112, in testNegative
- self.assertRaises(roman1.OutOfRangeError, roman1.to_roman, -1)
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: OutOfRangeError
-======================================================================
-FAIL: to_roman should fail with large input
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 104, in testTooLarge
- self.assertRaises(roman1.OutOfRangeError, roman1.to_roman, 4000)
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: OutOfRangeError
-======================================================================
-FAIL: to_roman should fail with 0 input ①
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 108, in testZero
- self.assertRaises(roman1.OutOfRangeError, roman1.to_roman, 0)
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: OutOfRangeError ②
-----------------------------------------------------------------------
-Ran 12 tests in 0.040s ③
-
-FAILED (failures=10, errors=2) ④
- 14.2. roman.py, stage 2
- Now that you have the framework of the roman module laid out, it's time to start writing code and passing test cases.
- Example 14.3. roman2.py
- This file is available in py/roman/stage2/ in the examples directory.
- If you have not already done so, you can download this and other examples used in this book.
-
-"""Convert to and from Roman numerals"""
-
-#Define exceptions
-class RomanError(Exception): pass
-class OutOfRangeError(RomanError): pass
-class NotIntegerError(RomanError): pass
-class InvalidRomanNumeralError(RomanError): pass
-
-#Define digit mapping
-romanNumeralMap = (('M', 1000), ①
- ('CM', 900),
- ('D', 500),
- ('CD', 400),
- ('C', 100),
- ('XC', 90),
- ('L', 50),
- ('XL', 40),
- ('X', 10),
- ('IX', 9),
- ('V', 5),
- ('IV', 4),
- ('I', 1))
-
-def to_roman(n):
- """convert integer to Roman numeral"""
- result = ""
- for numeral, integer in romanNumeralMap:
- while n >= integer: ②
- result += numeral
- n -= integer
- return result
-
-def from_roman(s):
- """convert Roman numeral to integer"""
- pass
-
-
-- romanNumeralMap is a tuple of tuples which defines three things:
-
-
-- The character representations of the most basic Roman numerals. Note that this is not just the single-character Roman numerals;
- you're also defining two-character pairs like
CM (“one hundred less than one thousand”); this will make the to_roman() code simpler later.
-
- - The order of the Roman numerals. They are listed in descending value order, from
M all the way down to I.
-
- - The value of each Roman numeral. Each inner tuple is a pair of
(numeral, value).
-
-
- - Here's where your rich data structure pays off, because you don't need any special logic to handle the subtraction rule.
- To convert to Roman numerals, you simply iterate through romanNumeralMap looking for the largest integer value less than or equal to the input. Once found, you add the Roman numeral representation
- to the end of the output, subtract the corresponding integer value from the input, lather, rinse, repeat.
-
Example 14.4. How to_roman() works
- If you're not clear how to_roman() works, add a print statement to the end of the while loop:
- while n >= integer:
- result += numeral
- n -= integer
- print 'subtracting', integer, 'from input, adding', numeral, 'to output'
->>> import roman2
->>> roman2.to_roman(1424)
-subtracting 1000 from input, adding M to output
-subtracting 400 from input, adding CD to output
-subtracting 10 from input, adding X to output
-subtracting 10 from input, adding X to output
-subtracting 4 from input, adding IV to output
-'MCDXXIV'
- So to_roman() appears to work, at least in this manual spot check. But will it pass the unit testing? Well no, not entirely.
- Example 14.5. Output of romantest2.py against roman2.py
- Remember to run romantest2.py with the -v command-line flag to enable verbose mode.
- from_roman should only accept uppercase input ... FAIL
-to_roman should always return uppercase ... ok①
-from_roman should fail with malformed antecedents ... FAIL
-from_roman should fail with repeated pairs of numerals ... FAIL
-from_roman should fail with too many repeated numerals ... FAIL
-from_roman should give known result with known input ... FAIL
-to_roman should give known result with known input ... ok ②
-from_roman(to_roman(n))==n for all n ... FAIL
-to_roman should fail with non-integer input ... FAIL ③
-to_roman should fail with negative input ... FAIL
-to_roman should fail with large input ... FAIL
-to_roman should fail with 0 input ... FAIL
-
-to_roman() does, in fact, always return uppercase, because romanNumeralMap defines the Roman numeral representations as uppercase. So this test passes already.
-- Here's the big news: this version of the
to_roman() function passes the known values test. Remember, it's not comprehensive, but it does put the function through its paces with a variety of good inputs, including
- inputs that produce every single-character Roman numeral, the largest possible input (3999), and the input that produces the longest possible Roman numeral (3888). At this point, you can be reasonably confident that the function works for any good input value you could throw at it.
- - However, the function does not “work” for bad values; it fails every single bad input test. That makes sense, because you didn't include any checks for bad input. Those test cases look for specific exceptions to
- be raised (via
assertRaises), and you're never raising them. You'll do that in the next stage.
-Here's the rest of the output of the unit test, listing the details of all the failures. You're down to 10.
-
-======================================================================
-FAIL: from_roman should only accept uppercase input
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 156, in testFromRomanCase
- roman2.from_roman, numeral.lower())
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: InvalidRomanNumeralError
-======================================================================
-FAIL: from_roman should fail with malformed antecedents
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 133, in testMalformedAntecedent
- self.assertRaises(roman2.InvalidRomanNumeralError, roman2.from_roman, s)
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: InvalidRomanNumeralError
-======================================================================
-FAIL: from_roman should fail with repeated pairs of numerals
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 127, in testRepeatedPairs
- self.assertRaises(roman2.InvalidRomanNumeralError, roman2.from_roman, s)
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: InvalidRomanNumeralError
-======================================================================
-FAIL: from_roman should fail with too many repeated numerals
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 122, in testTooManyRepeatedNumerals
- self.assertRaises(roman2.InvalidRomanNumeralError, roman2.from_roman, s)
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: InvalidRomanNumeralError
-======================================================================
-FAIL: from_roman should give known result with known input
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 99, in testFromRomanKnownValues
- self.assertEqual(integer, result)
- File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
- raise self.failureException, (msg or '%s != %s' % (first, second))
-AssertionError: 1 != None
-======================================================================
-FAIL: from_roman(to_roman(n))==n for all n
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 141, in testSanity
- self.assertEqual(integer, result)
- File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
- raise self.failureException, (msg or '%s != %s' % (first, second))
-AssertionError: 1 != None
-======================================================================
-FAIL: to_roman should fail with non-integer input
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 116, in testNonInteger
- self.assertRaises(roman2.NotIntegerError, roman2.to_roman, 0.5)
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: NotIntegerError
-======================================================================
-FAIL: to_roman should fail with negative input
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 112, in testNegative
- self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, -1)
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: OutOfRangeError
-======================================================================
-FAIL: to_roman should fail with large input
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 104, in testTooLarge
- self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000)
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: OutOfRangeError
-======================================================================
-FAIL: to_roman should fail with 0 input
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 108, in testZero
- self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 0)
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: OutOfRangeError
-----------------------------------------------------------------------
-Ran 12 tests in 0.320s
-
-FAILED (failures=10) 14.3. roman.py, stage 3
-Now that to_roman() behaves correctly with good input (integers from 1 to 3999), it's time to make it behave correctly with bad input (everything else).
- Example 14.6. roman3.py
- This file is available in py/roman/stage3/ in the examples directory.
- If you have not already done so, you can download this and other examples used in this book.
-
-"""Convert to and from Roman numerals"""
-
-#Define exceptions
-class RomanError(Exception): pass
-class OutOfRangeError(RomanError): pass
-class NotIntegerError(RomanError): pass
-class InvalidRomanNumeralError(RomanError): pass
-
-#Define digit mapping
-romanNumeralMap = (('M', 1000),
- ('CM', 900),
- ('D', 500),
- ('CD', 400),
- ('C', 100),
- ('XC', 90),
- ('L', 50),
- ('XL', 40),
- ('X', 10),
- ('IX', 9),
- ('V', 5),
- ('IV', 4),
- ('I', 1))
-
-def to_roman(n):
- """convert integer to Roman numeral"""
- if not (0 < n < 4000): ①
- raise OutOfRangeError, "number out of range (must be 1..3999)" ②
- if int(n) <> n: ③
- raise NotIntegerError, "non-integers can not be converted"
-
- result = "" ④
- for numeral, integer in romanNumeralMap:
- while n >= integer:
- result += numeral
- n -= integer
- return result
-
-def from_roman(s):
- """convert Roman numeral to integer"""
- pass
-
-
-- This is a nice Pythonic shortcut: multiple comparisons at once. This is equivalent to
if not ((0 < n) and (n < 4000)), but it's much easier to read. This is the range check, and it should catch inputs that are too large, negative, or zero.
- - You raise exceptions yourself with the
raise statement. You can raise any of the built-in exceptions, or you can raise any of your custom exceptions that you've defined.
- The second parameter, the error message, is optional; if given, it is displayed in the traceback that is printed if the exception
- is never handled.
- - This is the non-integer check. Non-integers can not be converted to Roman numerals.
-
- The rest of the function is unchanged.
-
Example 14.7. Watching to_roman() handle bad input
->>> import roman3
->>> roman3.to_roman(4000)
-Traceback (most recent call last):
- File "<interactive input>", line 1, in ?
- File "roman3.py", line 27, in to_roman
- raise OutOfRangeError, "number out of range (must be 1..3999)"
-OutOfRangeError: number out of range (must be 1..3999)
->>> roman3.to_roman(1.5)
-Traceback (most recent call last):
- File "<interactive input>", line 1, in ?
- File "roman3.py", line 29, in to_roman
- raise NotIntegerError, "non-integers can not be converted"
-NotIntegerError: non-integers can not be converted
- Example 14.8. Output of romantest3.py against roman3.pyfrom_roman should only accept uppercase input ... FAIL
-to_roman should always return uppercase ... ok
-from_roman should fail with malformed antecedents ... FAIL
-from_roman should fail with repeated pairs of numerals ... FAIL
-from_roman should fail with too many repeated numerals ... FAIL
-from_roman should give known result with known input ... FAIL
-to_roman should give known result with known input ... ok ①
-from_roman(to_roman(n))==n for all n ... FAIL
-to_roman should fail with non-integer input ... ok ②
-to_roman should fail with negative input ... ok ③
-to_roman should fail with large input ... ok
-to_roman should fail with 0 input ... ok
-
-to_roman() still passes the known values test, which is comforting. All the tests that passed in stage 2 still pass, so the latest code hasn't broken anything.
-- More exciting is the fact that all of the bad input tests now pass. This test,
testNonInteger, passes because of the int(n) <> n check. When a non-integer is passed to to_roman(), the int(n) <> n check notices it and raises the NotIntegerError exception, which is what testNonInteger is looking for.
- - This test,
testNegative, passes because of the not (0 < n < 4000) check, which raises an OutOfRangeError exception, which is what testNegative is looking for.
-
-======================================================================
-FAIL: from_roman should only accept uppercase input
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 156, in testFromRomanCase
- roman3.from_roman, numeral.lower())
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: InvalidRomanNumeralError
-======================================================================
-FAIL: from_roman should fail with malformed antecedents
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 133, in testMalformedAntecedent
- self.assertRaises(roman3.InvalidRomanNumeralError, roman3.from_roman, s)
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: InvalidRomanNumeralError
-======================================================================
-FAIL: from_roman should fail with repeated pairs of numerals
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 127, in testRepeatedPairs
- self.assertRaises(roman3.InvalidRomanNumeralError, roman3.from_roman, s)
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: InvalidRomanNumeralError
-======================================================================
-FAIL: from_roman should fail with too many repeated numerals
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 122, in testTooManyRepeatedNumerals
- self.assertRaises(roman3.InvalidRomanNumeralError, roman3.from_roman, s)
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: InvalidRomanNumeralError
-======================================================================
-FAIL: from_roman should give known result with known input
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 99, in testFromRomanKnownValues
- self.assertEqual(integer, result)
- File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
- raise self.failureException, (msg or '%s != %s' % (first, second))
-AssertionError: 1 != None
-======================================================================
-FAIL: from_roman(to_roman(n))==n for all n
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 141, in testSanity
- self.assertEqual(integer, result)
- File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
- raise self.failureException, (msg or '%s != %s' % (first, second))
-AssertionError: 1 != None
-----------------------------------------------------------------------
-Ran 12 tests in 0.401s
-
-FAILED (failures=6) ①
-
-- You're down to 6 failures, and all of them involve
from_roman(): the known values test, the three separate bad input tests, the case check, and the sanity check. That means that to_roman() has passed all the tests it can pass by itself. (It's involved in the sanity check, but that also requires that from_roman() be written, which it isn't yet.) Which means that you must stop coding to_roman() now. No tweaking, no twiddling, no extra checks “just in case”. Stop. Now. Back away from the keyboard.
-
-
- | The most important thing that comprehensive unit testing can tell you is when to stop coding. When all the unit tests for
- a function pass, stop coding the function. When all the unit tests for an entire module pass, stop coding the module.
-14.4. roman.py, stage 4
-Now that to_roman() is done, it's time to start coding from_roman().
- the to_roman() function.
- Example 14.9. roman4.py
- This file is available in py/roman/stage4/ in the examples directory.
- If you have not already done so, you can download this and other examples used in this book.
-
-"""Convert to and from Roman numerals"""
-
-#Define exceptions
-class RomanError(Exception): pass
-class OutOfRangeError(RomanError): pass
-class NotIntegerError(RomanError): pass
-class InvalidRomanNumeralError(RomanError): pass
-
-#Define digit mapping
-romanNumeralMap = (('M', 1000),
- ('CM', 900),
- ('D', 500),
- ('CD', 400),
- ('C', 100),
- ('XC', 90),
- ('L', 50),
- ('XL', 40),
- ('X', 10),
- ('IX', 9),
- ('V', 5),
- ('IV', 4),
- ('I', 1))
-
-# to_roman function omitted for clarity (it hasn't changed)
-
-def from_roman(s):
- """convert Roman numeral to integer"""
- result = 0
- index = 0
- for numeral, integer in romanNumeralMap:
- while s[index:index+len(numeral)] == numeral: ①
- result += integer
- index += len(numeral)
- return result
-
-
-- The pattern here is the same as
to_roman(). You iterate through your Roman numeral data structure (a tuple of tuples), and instead of matching the highest integer
- values as often as possible, you match the “highest” Roman numeral character strings as often as possible.
-Example 14.10. How from_roman() works
- If you're not clear how from_roman() works, add a print statement to the end of the while loop:
- while s[index:index+len(numeral)] == numeral:
- result += integer
- index += len(numeral)
- print 'found', numeral, 'of length', len(numeral), ', adding', integer
->>> import roman4
->>> roman4.from_roman('MCMLXXII')
-found M , of length 1, adding 1000
-found CM , of length 2, adding 900
-found L , of length 1, adding 50
-found X , of length 1, adding 10
-found X , of length 1, adding 10
-found I , of length 1, adding 1
-found I , of length 1, adding 1
-1972Example 14.11. Output of romantest4.py against roman4.pyfrom_roman should only accept uppercase input ... FAIL
-to_roman should always return uppercase ... ok
-from_roman should fail with malformed antecedents ... FAIL
-from_roman should fail with repeated pairs of numerals ... FAIL
-from_roman should fail with too many repeated numerals ... FAIL
-from_roman should give known result with known input ... ok ①
-to_roman should give known result with known input ... ok
-from_roman(to_roman(n))==n for all n ... ok②
-to_roman should fail with non-integer input ... ok
-to_roman should fail with negative input ... ok
-to_roman should fail with large input ... ok
-to_roman should fail with 0 input ... ok
-
-- Two pieces of exciting news here. The first is that
from_roman() works for good input, at least for all the known values you test.
- - The second is that the sanity check also passed. Combined with the known values tests, you can be reasonably sure that both
to_roman() and from_roman() work properly for all possible good values. (This is not guaranteed; it is theoretically possible that to_roman() has a bug that produces the wrong Roman numeral for some particular set of inputs, and that from_roman() has a reciprocal bug that produces the same wrong integer values for exactly that set of Roman numerals that to_roman() generated incorrectly. Depending on your application and your requirements, this possibility may bother you; if so, write
- more comprehensive test cases until it doesn't bother you.)
-
-======================================================================
-FAIL: from_roman should only accept uppercase input
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage4\romantest4.py", line 156, in testFromRomanCase
- roman4.from_roman, numeral.lower())
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: InvalidRomanNumeralError
-======================================================================
-FAIL: from_roman should fail with malformed antecedents
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage4\romantest4.py", line 133, in testMalformedAntecedent
- self.assertRaises(roman4.InvalidRomanNumeralError, roman4.from_roman, s)
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: InvalidRomanNumeralError
-======================================================================
-FAIL: from_roman should fail with repeated pairs of numerals
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage4\romantest4.py", line 127, in testRepeatedPairs
- self.assertRaises(roman4.InvalidRomanNumeralError, roman4.from_roman, s)
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: InvalidRomanNumeralError
-======================================================================
-FAIL: from_roman should fail with too many repeated numerals
-----------------------------------------------------------------------
-Traceback (most recent call last):
- File "C:\docbook\dip\py\roman\stage4\romantest4.py", line 122, in testTooManyRepeatedNumerals
- self.assertRaises(roman4.InvalidRomanNumeralError, roman4.from_roman, s)
- File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
- raise self.failureException, excName
-AssertionError: InvalidRomanNumeralError
-----------------------------------------------------------------------
-Ran 12 tests in 1.222s
-
-FAILED (failures=4) 14.5. roman.py, stage 5
-Example 14.12. roman5.py
- This file is available in py/roman/stage5/ in the examples directory.
- If you have not already done so, you can download this and other examples used in this book.
-
-"""Convert to and from Roman numerals"""
-import re
-
-#Define exceptions
-class RomanError(Exception): pass
-class OutOfRangeError(RomanError): pass
-class NotIntegerError(RomanError): pass
-class InvalidRomanNumeralError(RomanError): pass
-
-#Define digit mapping
-romanNumeralMap = (('M', 1000),
- ('CM', 900),
- ('D', 500),
- ('CD', 400),
- ('C', 100),
- ('XC', 90),
- ('L', 50),
- ('XL', 40),
- ('X', 10),
- ('IX', 9),
- ('V', 5),
- ('IV', 4),
- ('I', 1))
-
-def to_roman(n):
- """convert integer to Roman numeral"""
- if not (0 < n < 4000):
- raise OutOfRangeError, "number out of range (must be 1..3999)"
- if int(n) <> n:
- raise NotIntegerError, "non-integers can not be converted"
-
- result = ""
- for numeral, integer in romanNumeralMap:
- while n >= integer:
- result += numeral
- n -= integer
- return result
-
-#Define pattern to detect valid Roman numerals
-romanNumeralPattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$' ①
-
-def from_roman(s):
- """convert Roman numeral to integer"""
- if not re.search(romanNumeralPattern, s):②
- raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s
-
- result = 0
- index = 0
- for numeral, integer in romanNumeralMap:
- while s[index:index+len(numeral)] == numeral:
- result += integer
- index += len(numeral)
- return result
-
-
-- This is just a continuation of the pattern you discussed in Section 7.3, “Case Study: Roman Numerals”. The tens places is either
XC (90), XL (40), or an optional L followed by 0 to 3 optional X characters. The ones place is either IX (9), IV (4), or an optional V followed by 0 to 3 optional I characters.
- - Having encoded all that logic into a regular expression, the code to check for invalid Roman numerals becomes trivial. If
-
re.search returns an object, then the regular expression matched and the input is valid; otherwise, the input is invalid.
-At this point, you are allowed to be skeptical that that big ugly regular expression could possibly catch all the types of
-invalid Roman numerals. But don't take my word for it, look at the results:
- Example 14.13. Output of romantest5.py against roman5.py
-from_roman should only accept uppercase input ... ok ①
-to_roman should always return uppercase ... ok
-from_roman should fail with malformed antecedents ... ok ②
-from_roman should fail with repeated pairs of numerals ... ok ③
-from_roman should fail with too many repeated numerals ... ok
-from_roman should give known result with known input ... ok
-to_roman should give known result with known input ... ok
-from_roman(to_roman(n))==n for all n ... ok
-to_roman should fail with non-integer input ... ok
-to_roman should fail with negative input ... ok
-to_roman should fail with large input ... ok
-to_roman should fail with 0 input ... ok
-
-----------------------------------------------------------------------
-Ran 12 tests in 2.864s
-
-OK ④
-
-- One thing I didn't mention about regular expressions is that, by default, they are case-sensitive. Since the regular expression
-romanNumeralPattern was expressed in uppercase characters, the
re.search check will reject any input that isn't completely uppercase. So the uppercase input test passes.
- - More importantly, the bad input tests pass. For instance, the malformed antecedents test checks cases like
MCMC. As you've seen, this does not match the regular expression, so from_roman() raises an InvalidRomanNumeralError exception, which is what the malformed antecedents test case is looking for, so the test passes.
- - In fact, all the bad input tests pass. This regular expression catches everything you could think of when you made your test
- cases.
-
-
- | When all of your tests pass, stop coding.
-
-
-
-
-
-[functional programming stuff was here]
-
-
-
-
-
The following is a complete Python program that acts as a cheap and simple regression testing framework. It takes unit tests that you've written for individual
modules, collects them all into one big test suite, and runs them all at once. I actually use this script as part of the
build process for this book; I have unit tests for several of the example programs (not just the roman.py module featured in Chapter 13, Unit Testing), and the first thing my automated build script does is run this program to make sure all my examples still work. If this
@@ -1762,621 +916,3 @@ if __name__ == "__main__":
[7] Technically, the second argument to filter can be any sequence, including lists, tuples, and custom classes that act like lists by defining the __getitem__ special method. If possible, filter will return the same datatype as you give it, so filtering a list returns a list, but filtering a tuple returns a tuple.
| | |