From 6762653eb2947038dec3d17791996c0ba9aa4b04 Mon Sep 17 00:00:00 2001 From: Mark Pilgrim Date: Tue, 4 Aug 2009 19:19:43 -0700 Subject: [PATCH 1/9] various minor corrections --- unit-testing.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/unit-testing.html b/unit-testing.html index 3917e60..c9cbd84 100755 --- a/unit-testing.html +++ b/unit-testing.html @@ -128,13 +128,13 @@ if __name__ == '__main__': unittest.main()
  1. To write a test case, first subclass the TestCase class of the unittest module. This class provides many useful methods which you can use in your test case to test specific conditions. -
  2. This is a list of integer/numeral pairs that I verified manually. It includes the lowest ten numbers, the highest number, every number that translates to a single-character Roman numeral, and a random sampling of other valid numbers. The point of a unit test is not to test every possible input, but to test a representative sample. +
  3. This is a list of integer/numeral pairs that I verified manually. It includes the lowest ten numbers, the highest number, every number that translates to a single-character Roman numeral, and a random sampling of other valid numbers. You don’t need to test every possible input, but you should try to test all the obvious edge cases.
  4. Every individual test is its own method, which must take no parameters and return no value. If the method exits normally without raising an exception, the test is considered passed; if the method raises an exception, the test is considered failed.
  5. Here you call the actual to_roman() function. (Well, the function hasn’t be written yet, but once it is, this is the line that will call it.) Notice that you have now defined the API for the to_roman() function: it must take an integer (the number to convert) and return a string (the Roman numeral representation). If the API is different than that, this test is considered failed. Also notice that you are not trapping any exceptions when you call to_roman(). This is intentional. to_roman() shouldn’t raise an exception when you call it with valid input, and these input values are all valid. If to_roman() raises an exception, this test is considered failed.
  6. Assuming the to_roman() function was defined correctly, called correctly, completed successfully, and returned a value, the last step is to check whether it returned the right value. This is a common question, and the TestCase class provides a method, assertEqual, to check whether two values are equal. If the result returned from to_roman() (result) does not match the known value you were expecting (numeral), assertEqual will raise an exception and the test will fail. If the two values are equal, assertEqual will do nothing. If every value returned from to_roman() matches the known value you expect, assertEqual never raises an exception, so test_to_roman_known_values eventually exits normally, which means to_roman() has passed this test.
-

Once you have a test case, you can start coding the to_roman() function. First, you should stub it out as an empty function and make sure the tests fail. If the tests succeed before you’ve written any code, you’re doing it wrong — your tests aren’t testing your code at all! Write a test that fails, then code until it passes. +

Once you have a test case, you can start coding the to_roman() function. First, you should stub it out as an empty function and make sure the tests fail. If the tests succeed before you’ve written any code, your tests aren’t testing your code at all! Unit testing is a dance: tests lead, code follows. Write a test that fails, then code until it passes.

# roman1.py
 
 def to_roman(n):
@@ -166,7 +166,7 @@ Traceback (most recent call last):
 
  • For each test case, the unittest module will print out the docstring of the method and whether that test passed or failed. As expected, this test case fails.
  • For each failed test case, unittest displays the trace information showing exactly what happened. In this case, the call to assertEqual() raised an AssertionError because it was expecting to_roman(1) to return 'I', but it didn’t. (Since there was no explicit return statement, the function returned None, the Python null value.)
  • After the detail of each test, unittest displays a summary of how many tests were performed and how long it took. -
  • Overall, the unit test failed because at least one test case did not pass. When a test case doesn’t pass, unittest distinguishes between failures and errors. A failure is a call to an assertXYZ method, like assertEqual or assertRaises, that fails because the asserted condition is not true or the expected exception was not raised. An error is any other sort of exception raised in the code you’re testing or the unit test case itself. +
  • Overall, the test run failed because at least one test case did not pass. When a test case doesn’t pass, unittest distinguishes between failures and errors. A failure is a call to an assertXYZ method, like assertEqual or assertRaises, that fails because the asserted condition is not true or the expected exception was not raised. An error is any other sort of exception raised in the code you’re testing or the unit test case itself.

    Now, finally, you can write the to_roman() function.

    [download roman1.py] From cd545c25519d72b03aa96d78938536dbd424e7a3 Mon Sep 17 00:00:00 2001 From: Mark Pilgrim Date: Tue, 4 Aug 2009 19:19:59 -0700 Subject: [PATCH 2/9] list marker fiddling --- dip3.css | 2 +- util/lesscss.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dip3.css b/dip3.css index b80137e..cb93d52 100755 --- a/dip3.css +++ b/dip3.css @@ -76,7 +76,7 @@ pre, kbd, samp, code, var, .b, pre span { .u { font: medium/1.75 'Arial Unicode MS', FreeSerif, OpenSymbol, 'DejaVu Sans', sans-serif; } -pre .u, pre .u span, .a { +pre .u, td .u, pre .u span, .a { font: medium/1.75 'Arial Unicode MS', 'DejaVu Sans', FreeSerif, OpenSymbol, sans-serif; } .baa { diff --git a/util/lesscss.py b/util/lesscss.py index e1c8bc2..7f5896a 100755 --- a/util/lesscss.py +++ b/util/lesscss.py @@ -7,7 +7,7 @@ import sys # These selectors are kept regardless of whether this script thinks they are used. # Most of these match nodes that are dynamically inserted or manipulated by script # after the page has loaded, which is why a static analysis thinks they're unused. -SELECTOR_EXCEPTIONS = ('.w', '.b', '.str', '.kwd', '.com', '.typ', '.lit', '.pun', '.tag', '.atn', '.atv', '.dec', 'pre .u', 'pre .u span', 'li ol', 'a.hl:link', 'a.hl:visited', 'a.hl:hover', 'h2[id]:hover a.hl', 'h3[id]:hover a.hl') +SELECTOR_EXCEPTIONS = ('.w', '.b', '.str', '.kwd', '.com', '.typ', '.lit', '.pun', '.tag', '.atn', '.atv', '.dec', 'pre .u', 'pre .u span', 'td .u', 'li ol', 'a.hl:link', 'a.hl:visited', 'a.hl:hover', 'h2[id]:hover a.hl', 'h3[id]:hover a.hl') filename = sys.argv[1] pqd = pq(filename=filename) From 99d1e777230d0616f3d9f70d6e263ea4e7058178 Mon Sep 17 00:00:00 2001 From: Mark Pilgrim Date: Tue, 4 Aug 2009 19:22:05 -0700 Subject: [PATCH 3/9] 1.5 --> 1.0 for initial NotIntegerError example --- unit-testing.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unit-testing.html b/unit-testing.html index c9cbd84..3198cfc 100755 --- a/unit-testing.html +++ b/unit-testing.html @@ -461,7 +461,7 @@ OK

  • >>> import roman3 >>> roman3.to_roman(0.5) '' ->>> roman3.to_roman(1.5) +>>> roman3.to_roman(1.0) 'I'
    1. Oh, that’s bad. From 682934c0f7a45bcf55b787a2a0a17911926f6bf0 Mon Sep 17 00:00:00 2001 From: Mark Pilgrim Date: Tue, 4 Aug 2009 19:23:52 -0700 Subject: [PATCH 4/9] less mathy stuff --- unit-testing.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/unit-testing.html b/unit-testing.html index 3198cfc..73c5e70 100755 --- a/unit-testing.html +++ b/unit-testing.html @@ -570,9 +570,9 @@ OK result = roman5.from_roman(numeral) self.assertEqual(integer, result) -

      There’s a pleasing symmetry here. The to_roman() and from_roman() functions are inverses of each other. The first converts integers to specially-formatted strings, the second converts specially-formated strings to integers. In theory, we should be able to “round-trip” a number by passing to the to_roman() function to get a string, then passing that string to the from_roman() function to get an integer, and end up with the same number. In mathematical terms, +

      There’s a pleasing symmetry here. The to_roman() and from_roman() functions are inverses of each other. The first converts integers to specially-formatted strings, the second converts specially-formated strings to integers. In theory, we should be able to “round-trip” a number by passing to the to_roman() function to get a string, then passing that string to the from_roman() function to get an integer, and end up with the same number. -

      x = f(g(x)) for all values of x
      +
      n = from_roman(to_roman(n)) for all values of n

      In this case, “all values” means any number between 1..3999, since that is the valid range of inputs to the to_roman() function. We can express this symmetry in a test case that runs through all the values 1..3999, calls to_roman(), calls from_roman(), and checks that the output is the same as the original input. @@ -652,7 +652,7 @@ FAILED (failures=2) """convert Roman numeral to integer""" result = 0 index = 0 - for numeral, integer in romanNumeralMap: + for numeral, integer in roman_numeral_map: while s[index:index+len(numeral)] == numeral: result += integer index += len(numeral) @@ -667,7 +667,7 @@ FAILED (failures=2) """convert Roman numeral to integer""" result = 0 index = 0 - for numeral, integer in romanNumeralMap: + for numeral, integer in roman_numeral_map: while s[index:index+len(numeral)] == numeral: result += integer index += len(numeral) From 4d3133b94de1a88b8a1aaaea053141e21082d7f7 Mon Sep 17 00:00:00 2001 From: Mark Pilgrim Date: Tue, 4 Aug 2009 19:31:05 -0700 Subject: [PATCH 5/9] various corrections --- unit-testing.html | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/unit-testing.html b/unit-testing.html index 73c5e70..8228217 100755 --- a/unit-testing.html +++ b/unit-testing.html @@ -618,6 +618,8 @@ FAILED (errors=2) def from_roman(s): '''convert Roman numeral to integer''' +

      (Hey, did you notice that? I defined a function with nothing but a docstring. That’s legal Python. In fact, some programmers swear by it. “Don’t stub; document!”) +

      Now the test cases will actually fail.

      @@ -706,11 +708,11 @@ OK

      As you saw in Case Study: Roman Numerals, there are several simple rules for constructing a Roman numeral, using the letters M, D, C, L, X, V, and I. Let's review the rules:

        -
      1. Characters are additive. I is 1, II is 2, and III is 3. VI is 6 (literally, “5 and 1”), VII is 7, and VIII is 8. +
      2. Sometimes characters are additive. I is 1, II is 2, and III is 3. VI is 6 (literally, “5 and 1”), VII is 7, and VIII is 8.
      3. The tens characters (I, X, C, and M) can be repeated up to three times. At 4, you need to subtract from the next highest fives character. You can't represent 4 as IIII; instead, it is represented as IV (“1 less than 5”). 40 is written as XL (“10 less than 50”), 41 as XLI, 42 as XLII, 43 as XLIII, and then 44 as XLIV (“10 less than 50, then 1 less than 5”). -
      4. Similarly, at 9, you need to subtract from the next highest tens character: 8 is VIII, but 9 is IX (“1 less than 10”), not VIIII (since the I character can not be repeated four times). 90 is XC, 900 is CM. +
      5. Sometimes characters are… the opposite of additive. By putting certain characters before others, you subtract from the final value. For example, at 9, you need to subtract from the next highest tens character: 8 is VIII, but 9 is IX (“1 less than 10”), not VIIII (since the I character can not be repeated four times). 90 is XC, 900 is CM.
      6. The fives characters can not be repeated. 10 is always represented as X, never as VV. 100 is always C, never LL. -
      7. Roman numerals are always written highest to lowest, and read left to right, so order of characters matters very much. DC is 600; CD is a completely different number (400, “100 less than 500”). CI is 101; IC is not even a valid Roman numeral (because you can't subtract 1 directly from 100; you would need to write it as XCIX, “10 less than 100, then 1 less than 10”). +
      8. Roman numerals are read left to right, so the order of characters matters very much. DC is 600; CD is a completely different number (400, “100 less than 500”). CI is 101; IC is not even a valid Roman numeral (because you can't subtract 1 directly from 100; you would need to write it as XCIX, “10 less than 100, then 1 less than 10”).

      Thus, one useful test would be to ensure that the from_roman() function should fail when you pass it a string with too many repeated numerals. How many is “too many” depends on the numeral. @@ -728,7 +730,7 @@ OK for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'): self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s) -

      A third test could check that numerals appear in the correct order, from highest to lowest value. For example, CL is 150, but LC is never valid, because the numeral for 50 can never come before the numeral for 100. +

      A third test could check that numerals appear in the correct order, from highest to lowest value. For example, CL is 150, but LC is never valid, because the numeral for 50 can never come before the numeral for 100. This test includes a randomly chosen set of invalid antecedents: I before M, V before X, and so on.

          def test_malformed_antecedents(self):
               '''from_roman should fail with malformed antecedents'''
      
      From b34f7664dac3517bb8fe11c06b73d55ff4ffacf7 Mon Sep 17 00:00:00 2001
      From: Mark Pilgrim 
      Date: Tue, 4 Aug 2009 19:37:33 -0700
      Subject: [PATCH 6/9] list of characters --> tuple of characters
      
      ---
       strings.html | 2 +-
       1 file changed, 1 insertion(+), 1 deletion(-)
      
      diff --git a/strings.html b/strings.html
      index 9ea13a6..299f9a1 100755
      --- a/strings.html
      +++ b/strings.html
      @@ -93,7 +93,7 @@ My alphabet starts where your alphabet ends! 
      &m '深入 Python 3'
      1. To create a string, enclose it in quotes. Python strings can be defined with either single quotes (') or double quotes ("). -
      2. The built-in len() function returns the length of the string, i.e. the number of characters. This is the same function you use to find the length of a list. A string is like a list of characters. +
      3. The built-in len() function returns the length of the string, i.e. the number of characters. This is the same function you use to find the length of a list, tuple, set, or dictionary. A string is like a tuple of characters.
      4. Just like getting individual items out of a list, you can get individual characters out of a string using index notation.
      5. Just like lists, you can concatenate strings using the + operator.
      From 9fef46462c0e8980529cee61968a3bcae6252186 Mon Sep 17 00:00:00 2001 From: Mark Pilgrim Date: Tue, 4 Aug 2009 20:06:12 -0700 Subject: [PATCH 7/9] notes on case-sensitivity --- your-first-python-program.html | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/your-first-python-program.html b/your-first-python-program.html index 049b66b..26f49d0 100755 --- a/your-first-python-program.html +++ b/your-first-python-program.html @@ -324,6 +324,30 @@ NameError: name 'x' is not defined

      You will thank Python for this one day. +

      Everything is Case-Sensitive

      + +

      All names in Python are case-sensitive: variable names, function names, class names, module names, exception names. If you can get it, set it, call it, construct it, import it, or raise it, it’s case-sensitive. + +

      +>>> an_integer = 1
      +>>> an_integer
      +1
      +>>> AN_INTEGER
      +Traceback (most recent call last):
      +  File "<stdin>", line 1, in <module>
      +NameError: name 'AN_INTEGER' is not defined
      +>>> An_Integer
      +Traceback (most recent call last):
      +  File "<stdin>", line 1, in <module>
      +NameError: name 'An_Integer' is not defined
      +>>> an_inteGer
      +Traceback (most recent call last):
      +  File "<stdin>", line 1, in <module>
      +NameError: name 'an_inteGer' is not defined
      +
      + +

      And so on. +

      Running Scripts

      From 7a59e9d8951798eb3df0491eaeb759b28b07be0e Mon Sep 17 00:00:00 2001 From: Mark Pilgrim Date: Tue, 4 Aug 2009 20:09:27 -0700 Subject: [PATCH 8/9] header fiddling --- your-first-python-program.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/your-first-python-program.html b/your-first-python-program.html index 26f49d0..e929da7 100755 --- a/your-first-python-program.html +++ b/your-first-python-program.html @@ -306,7 +306,7 @@ except ImportError:

      By the end of this try..except block, you have imported some module and named it etree. Since both modules implement a common API, the rest of your code doesn’t need to keep checking which module got imported. And since the module that did get imported is always called etree, the rest of your code doesn’t need to be littered with if statements to call differently-named modules. -

      Unbound Variables

      +

      Unbound Variables

      Take another look at this line of code from the approximate_size() function: @@ -324,7 +324,7 @@ NameError: name 'x' is not defined

      You will thank Python for this one day. -

      Everything is Case-Sensitive

      +

      Everything is Case-Sensitive

      All names in Python are case-sensitive: variable names, function names, class names, module names, exception names. If you can get it, set it, call it, construct it, import it, or raise it, it’s case-sensitive. From dc03983c28af0625dea7b35dbb7f34967e95dc1f Mon Sep 17 00:00:00 2001 From: Mark Pilgrim Date: Tue, 4 Aug 2009 20:09:54 -0700 Subject: [PATCH 9/9] typography fiddling --- your-first-python-program.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/your-first-python-program.html b/your-first-python-program.html index e929da7..2997927 100755 --- a/your-first-python-program.html +++ b/your-first-python-program.html @@ -275,7 +275,7 @@ SyntaxError: non-keyword arg after keyword arg

      if size < 0:
           raise ValueError('number must be non-negative')
      -

      The syntax for raising an exception is simple enough. Use the raise statement, followed by the exception name, and an optional human-readable string for debugging purposes. The syntax is reminiscent of calling a function. (In reality, exceptions are implemented as classes, and this raise statement is actually creating an instance of the ValueError class and passing the string 'number must be non-negative' to its initialization method. But we’re getting ahead of ourselves!) +

      The syntax for raising an exception is simple enough. Use the raise statement, followed by the exception name, and an optional human-readable string for debugging purposes. The syntax is reminiscent of calling a function. (In reality, exceptions are implemented as classes, and this raise statement is actually creating an instance of the ValueError class and passing the string 'number must be non-negative' to its initialization method. But we’re getting ahead of ourselves!)

      You don’t need to handle an exception in the function that raises it. If one function doesn’t handle it, the exception is passed to the calling function, then that function’s calling function, and so on “up the stack.” If the exception is never handled, your program will crash, Python will print a “traceback” to standard error, and that’s the end of that. Again, maybe that’s what you want; it depends on what your program does.