diff --git a/source/modules/Debugging.rst b/source/modules/Debugging.rst index b5876c8..6b0ebe2 100644 --- a/source/modules/Debugging.rst +++ b/source/modules/Debugging.rst @@ -4,53 +4,25 @@ Debugging ######### -System Development with Python - -- Maria McKinley - - -``parody@uw.edu`` - - -Topics -###### - - -- The call stack -- Exceptions -- Debugging - - The Call Stack -------------- -- A stack is a Last-In-First-Out (LIFO) data structure (stack of plates) -- The call stack is a stack data structure that stores information - about the current active function calls -- The objects in the stack are known as "stack frames". Each frame - contains the arguments passed to the function, space for local - variables, and the return address -- It is usually (unintuitively) displayed like an upside-down stack of - plates, with most recent frame on the bottom. -- When a function is called, a stack frame is created for it and pushed - onto the stack -- When a function returns, it is popped off the stack and control is - passed to the next item in the stack. If the stack is empty, the - program exits +- A stack is a Last-In-First-Out (LIFO) data structure, kind of like a stack of plates. +- The call stack is a stack data structure that stores information about the current active function calls. +- The objects in the stack are known as "stack frames". Each frame contains the arguments passed to the function, space for local variables, and the return address. +- It is usually (unintuitively) displayed like an upside-down stack of plates, with most recent frame on the bottom. +- When a function is called, a stack frame is created for it and pushed onto the stack. +- When a function returns, it is popped off the stack and control is passed to the next item in the stack. If the stack is empty, the program exits. -http://www.pythontutor.com/visualize.html#mode=edit - - -Visualize the stack! --------------------- +Visualize the Stack +------------------- .. image:: /_static/program_callstack.png :height: 580 px +How deep can that stack be? Let's find out. -.. rubric:: How deep can that stack be? - -:: +.. code-block:: python i = 0 @@ -62,11 +34,11 @@ Visualize the stack! recurse() +That maximum stack depth value can be changed with ``sys.setrecursionlimit(N)``. -That value can be changed with sys.setrecursionlimit(N) - -If we try to put more than sys.getrecursionlimit() frames on the stack, we get a RecursionError (derived from RuntimeError), which is python's version of StackOverflow +See: https://docs.python.org/3/library/sys.html#sys.setrecursionlimit. +If we try to put more than ``sys.getrecursionlimit()`` frames on the stack, we get a ``RecursionError`` (derived from ``RuntimeError``), which is Python's version of the Java's StackOverflowError. .. code-block:: ipython @@ -82,48 +54,39 @@ If we try to put more than sys.getrecursionlimit() frames on the stack, we get a recurse(3) - -module https://docs.python.org/3/library/inspect.html - -more on recursion http://www.mariakathryn.net/Blog/60 - - Exceptions ---------- -It's easier to ask for forgiveness than permission (Grace Hopper) + Ask forgiveness, not permission. -When either the interpreter or your own code detects an error condition, -an exception will be raised + -- Grace Hopper -The exception will bubble up the call stack until it is handled. If it's -not handled anywhere in the stack, the interpreter will exit the program. +When either the interpreter or your own code detects an error condition then an exception will be raised. +The exception will bubble up the call stack until it is handled. If it's not handled anywhere in the stack, the interpreter will exit the program. At each level in the stack, a handler can either: -- let it bubble through (the default if no handler) -- swallow the exception (the default for a handler) -- catch the exception and raise it again -- catch the exception and raise a new one - +- Let it bubble through. This is the default if no handler is found. +- Swallow the exception. This is the default for a handler. +- Catch the exception and raise it again. +- Catch the exception and raise a new one. -Handling exceptions +Handling Exceptions ------------------- -The most basic form uses the builtins try and except +The most basic form uses the built-ins ``try`` and ``except``. :: def temp_f_to_c(var): try: - return(float(var) - 32)/1.8000 + return(float(var) - 32)/1.8000 except ValueError as e: print("The argument does not contain numbers\n", e) - -A few more builtins for exception handling: finally, else, and raise --------------------------------------------------------------------- +``finally``, ``else``, and ``raise`` +------------------------------------ .. code-block:: python @@ -134,10 +97,11 @@ A few more builtins for exception handling: finally, else, and raise result = x / y except (ZeroDivisionError, ValueError) as e: print("caught division error or maybe a value error:\n", e) - except Exception as e: # only do this if absolutely necessary, or if planning to re-raise + except Exception as e: + # only catch "Exception" if absolutely necessary, or if planning to re-raise errors = e.args - print("Error({0})".format(errors)) - # or you can just print e + print(f"Error({errors})") + # or you can just print e print("unhandled, unexpected exception:\n", e) raise else: @@ -145,95 +109,87 @@ A few more builtins for exception handling: finally, else, and raise print("errors here will not be caught by above excepts") finally: print("this is executed no matter what") - print('this is only printed if there is no uncaught exception') - + print("this is only printed if there is no uncaught exception") It is even possible to use a try block without the exception clause: -:: +.. code-block:: python try: 5/0 finally: - print('did it work? why would you do this?') + print("did it work? why would you do this?"") -.. rubric:: Built-in exceptions - :name: built-in-exceptions +Built-in Exceptions +------------------- -:: +.. code-block:: python [name for name in dir(__builtin__) if "Error" in name] +If one of these meets your needs, by all means use it. You can add messages to them, too: -If one of these meets your needs, by all means use it. You can add messages: - -:: +.. code-block:: python raise SyntaxError("That was a mispelling") -If no builtin exceptions work, define a new exception type by subclassing Exception. +If no built-in exceptions work, define a new exception type by subclassing ``Exception``. -:: +.. code-block:: python class MyException(Exception): pass raise MyException("An exception doesn't always prove the rule!") -It is possible, but discouraged to catch all exceptions. +It is possible, but discouraged to catch all exceptions. Seriously, do not do this. -:: +.. code-block:: python try: - my_cool_code() - except: - print('no idea what the exceptions is, but I caught it') - + my_cool_code() + except: # bad! do not do this! + print("no idea what the exceptions is, but I caught it") -An exception to this exception rule is when you are running a service that should not ever crash, -like a web server. In this case, it is extremely important to have very good logging so that you -have reports of exactly what happened and what exception would have been thrown. +An exception to this exception rule is when you are running a service that should not ever crash, like a web server. In this case, it is extremely important to have very good logging so that you have reports of exactly what happened and what exception would have been thrown. But it's important to always catch at least the `Exception` exception. +.. code-block:: python -.. rubric:: Further reading - :name: further-reading - -- http://docs.python.org/3/library/exceptions.html -- http://docs.python.org/3/tutorial/errors.html + try: + my_cool_code() + except Exception: # ok! this is safe but not recommended + print("no idea what the exceptions is, but I caught it") +- http://docs.python.org/3/library/exceptions.html +- http://docs.python.org/3/tutorial/errors.html Debugging --------- -.. rubric:: Python Debugging - :name: python-debugging - - You will spend most of your time as a developer debugging. - You will spend more time than you expect on google. - Small, tested functions are easier to debug. -- Find a bug, make a test, so it doesn't come back - +- If you find a bug then make a test to prove that you fixed it and so that it doesn't come back. Tools ..... -- interpreter hints -- print() -- logging -- assert() -- tests -- debuggers - +- interpreter hints +- print() +- logging +- assert() +- tests +- debuggers The Stack Trace ............... -You already know what it looks like. Simple traceback: +You already know what it looks like. Here is a simple traceback: -:: +.. code-block:: bash - maria$ python3 define.py python + $ python3 define.py python Traceback (most recent call last): File "define.py", line 15, in definition = Definitions.article(title) @@ -245,165 +201,134 @@ You already know what it looks like. Simple traceback: But things can quickly get complicated. You may have already run into stacktraces that go on for a 50 lines or more. +Helpful Hints for Stacktraces +............................. -Some helpful hints with stacktraces: -.................................... - -- May seem obvious, but... Read it carefully! +- It may seem obvious, but read it carefully! - What is the error? Try reading it aloud. - The first place to look is the bottom. - Trace will show the line number and file of exception/calling functions. -- More than likely the error is in your code, not established packages - - look at lines in your code mentioned in the stacktrace first - - Sometimes that error was triggered by something else, and you need to look higher. (probably more than one file in the stacktrace is your code) - +- More than likely the error is in your code, not established packages. + - Look at lines in your code mentioned in the stacktrace first. + - Sometimes that error was triggered by something else, and you need to look higher. (Probably more than one file in the stacktrace is your code.) -If that fails you... +If that fails you: - Make sure the code you think is executing is really executing. -- Simplify your code (smallest code that causes bug). -- Debugger -- Save (and print) intermediate results from long expressions -- Try out bits of code at the command line +- Simplify your code to the smallest code that causes bug. +- Pull out a debugger, possibly from your IDE. +- Save and ``print`` intermediate results from long expressions. +- Try out bits of code at the command line. -If all else fails... +If all else fails then write out an email that describes the problem: -Write out an email that describes the problem: - -- include the stacktrace -- include steps you have taken to find the bug -- inlude the relative function of your code - -Often after writing out this email, you will realize what you forgot to check, and more often than not, this will happen just after you hit send. Good places to send these emails are other people on same project and mailing list for software package. For the purpose of this class, of course, copy it into slack or the class email list. +- Include the stacktrace. +- Include steps you have taken to find the bug. +- Include the relative function of your code. +Often, after writing out this email, you will realize what you forgot to check, and more often than not, this will happen just after you hit send. Good places to send these emails are other people on same project and mailing list for software package. For the purpose of this class, of course, copy it into Slack or the class email list. Print ..... -- print("my_module.py: my_variable: ", my_variable) -- can use print statements to make sure you are editing a file in the stack - +- ``print("my_module.py: my_variable: ", my_variable)`` +- You can use print statements to make sure you are editing a file in the stack. Console Debuggers ................. - pdb/ipdb -GUI debuggers (more about these below) -...................................... +GUI debuggers +............. -- Winpdb - IDEs: Eclipse, Wing IDE, PyCharm, Visual Studio Code -.. rubric:: help from the interpreter - :name: help-from-the-interpreter +Use the Interpreter +................... -1. investigate import issues with -v: +Investigate import issues with ``-v``. This will give you a very verbose output of everything being imported and more. -:: - - python -v myscript.py - - -Verbose (trace import statements) - - -2. inspect environment after running script with -i - -:: - - python -i myscript.py - - -Forces interpreter to remain active, and still in scope - -Useful tools from interpreter: -.............................. +.. code-block:: bash -- In IPython, 'who' will list all currently defined variables -- locals() -- globals() -- dir() + $ python -v myscript.py -.. rubric:: `Pdb - The Python - Debugger `__ - :name: pdb---the-python-debugger +Inspect environment after running script with ``-i``. This will dump you into a Python REPL after the program exits so you can see what the environment looked like at the time of death. -.. rubric:: Pros: +.. code-block:: bash -- You have it already, ships with the standard library -- Easy remote debugging (since it is non-graphical, see remote-pdb for true remote debugging) -- Works with any development environment + $ python -i myscript.py -.. rubric:: Cons: +Other useful tools for debugging include: -- Steep-ish learning curve -- Easy to get lost in a deep stack -- Watching variables isn't hard, but non-trivial +- If you are using IPython, ``who`` will list all currently defined variables. +- ``locals()`` +- ``globals()`` +- ``dir()`` -.. rubric:: `Pdb - The Python Debugger `_ +``pdb`` - The Python Debugger +----------------------------- -The 4-fold ways of invoking pdb -............................... +See: https://docs.python.org/3/library/pdb.html -- Postmortem mode -- Run mode -- Script mode -- Trace mode +Pros: -Note: in most cases where you see the word 'pdb' in the examples, you -can replace it with 'ipdb'. ipdb is the ipython enhanced version of pdb -which is mostly compatible, and generally easier to work with. But it -doesn't ship with Python. +- You have it already, ships with the standard library. +- Works with any development environment. -.. rubric:: Postmortem mode - :name: postmortem-mode +Cons: -For analyzing crashes due to uncaught exceptions +- Steep-ish learning curve. +- Easy to get lost in a deep stack. +- Watching variables isn't hard, but non-trivial. -:: +The Four Ways of Invoking ``pdb`` +................................. - python -i script.py - import pdb; pdb.pm() +- Postmortem mode +- Run mode +- Script mode +- Trace mode -More info on using Postmortem mode: +Note: in most cases where you see the word 'pdb' in the examples, you can replace it with 'ipdb'. ipdb is the ipython enhanced version of pdb which is mostly compatible, and generally easier to work with. But it doesn't ship with Python. -http://www.almarklein.org/pm-debugging.html +Postmortem Mode +............... -.. rubric:: Run mode - :name: run-mode +This mode is For analyzing crashes due to uncaught exceptions. -:: +.. code-block:: bash - pdb.run('some.expression()') + $ python -i script.py + >>> import pdb; pdb.pm() -.. rubric:: Script mode - :name: script-mode +Run Mode +........ -:: +.. code-block:: python - python -m pdb script.py + pdb.run("some.expression()"") +Script Mode +........... -"-m [module]" finds [module] in sys.path and executes it as a script +.. code-block:: bash + $ python -m pdb script.py -.. rubric:: Trace mode - :name: trace-mode +Trace Mode +.......... -Insert the following line into your code where you want execution to -halt: +Insert the following line into your code where you want execution to halt: -:: +.. code-block:: python import pdb; pdb.set_trace() +It's not always OK or possible to modify your code in order to debug it, but this is often the quickest way to begin inspecting state. -It's not always OK/possible to modify your code in order to debug it, -but this is often the quickest way to begin inspecting state - -.. rubric:: pdb in ipython - :name: pdb-in-ipython +``pdb`` in IPython +.................. .. code-block:: ipython @@ -414,15 +339,12 @@ but this is often the quickest way to begin inspecting state # now halts execution on uncaught exception -If you forget to turn on pdb, the magic command ``%debug`` will activate the -debugger (in 'post-mortem mode'). +If you forget to turn on pdb, the magic command ``%debug`` will activate the debugger in 'post-mortem mode'. -.. rubric:: Navigating pdb - :name: navigating-pdb +Navigating ``pdb`` +------------------ -The goal of each of the preceding techniques was to get to the pdb -prompt and get to work inspecting state. Most commands can be short-cutted -to the first letter. +The goal of each of the preceding techniques was to get to the pdb prompt and get to work inspecting state. Most commands can be shortened to just the first letter. :: @@ -432,7 +354,7 @@ to the first letter. pdb> where # print stack trace, bottom is most recent command pdb> list # list the code including and surrounding the current running code -To repeat the current command, press only the Enter key +To repeat the current command, press only the Enter key. :: @@ -453,9 +375,8 @@ To repeat the current command, press only the Enter key # advanced: create commands to be executed on a breakpoint pdb> commands - -.. rubric:: Breakpoints - :name: breakpoints +Breakpoints +........... :: @@ -472,9 +393,7 @@ To repeat the current command, press only the Enter key hasn't been loaded yet). The file is searched for on sys.path; the .py suffix may be omitted. - -Can use up, down, where and list to evalutate where you are, and use that to -set a new breakpoint in code coming up. Useful for getting out of rabbit holes. +You can use up, down, where, and list to evaluate where you are, and use that to set a new breakpoint in code coming up. This is useful for getting out of rabbit holes. :: @@ -485,9 +404,7 @@ set a new breakpoint in code coming up. Useful for getting out of rabbit holes. # print lines in range pdb> list 1,28 - -You can also delete(clear), disable and enable breakpoints - +You can also ``clear`` (i.e. delete), ``disable`` and ``enable`` breakpoints. :: @@ -497,9 +414,8 @@ You can also delete(clear), disable and enable breakpoints enable [bpnumber [bpnumber...]] - -.. rubric:: Conditional Breakpoints - :name: conditional-breakpoints +Conditional Breakpoints +....................... :: @@ -511,83 +427,43 @@ You can also delete(clear), disable and enable breakpoints 1 breakpoint keep yes at .../pdb_break.py:9 stop only if j>3 -Condition can be used to add a conditional to an existing breakpoint - - -.. rubric:: Invoking pdb with pytest +The condition can be used to add a conditional to an existing breakpoint. +Invoking ``pdb`` with ``pytest`` +-------------------------------- pytest allows one to drop into the PDB prompt via a command line option:: - pytest --pdb + pytest --pdb -This will invoke the Python debugger on every failure. -Often you might only want to do this for the first failing -test to understand a certain failure situation:: +This will invoke the Python debugger on every failure. Often you might only want to do this for the first failing test to understand a certain failure situation:: - pytest -x --pdb # drop to PDB on first failure, then end test session - pytest --pdb --maxfail=3 # drop to PDB for first three failures + pytest -x --pdb # drop to PDB on first failure, then end test session + pytest --pdb --maxfail=3 # drop to PDB for first three failures - -Try some debugging! Here is a fun tutorial intro to pdb that someone created: - -https://github.com/spiside/pdb-tutorial +Try some debugging! Here is a fun tutorial intro to pdb that someone created: https://github.com/spiside/pdb-tutorial Python IDEs ----------- -.. rubric:: PyCharm - -From JetBrains, --- integrates some of their vast array of development -tools - -Free Community Edition (CE) is available - -Good visual debugging support - - -.. rubric:: Eclipse - -A multi-language IDE - -Python support via http://pydev.org/ - -Automatic variable and expression watching - -Supports a lot of debugging features like conditional breakpoints, -provided you look in the right places! - -Further reading - -http://pydev.org/manual_adv_debugger.html - - -.. rubric:: Visual Studio Code - -Visual Studio Code has support for Python - -(not the same as the monstrosity that is Visual Studio) - -https://code.visualstudio.com/ - - -.. rubric:: winpdb - -A multi platform Python debugger with threading support +PyCharm +....... -Easier to start up and get debugging:: +From JetBrains, this integrates some of their vast array of development tools. The free Community Edition (CE) is available. It has great visual debugging support. - winpdb your_app.py +Eclipse +....... -http://winpdb.org/tutorial/WinpdbTutorial.html +A multi-language IDE with `Python support `__. +It has automatic variable and expression watching and supports a lot of debugging features like conditional breakpoints, provided you look in the right places! -Remote debugging ----------------- +See: http://pydev.org/manual_adv_debugger.html -To debug an application running a different Python, even remotely: +Visual Studio Code +.................. -remote-pdb +This is not the same as Visual Studio. Visual Studio Code is a much smaller quasi-IDE that has support for Python. -https://pypi.python.org/pypi/remote-pdb +See: https://code.visualstudio.com/ diff --git a/source/modules/TestDrivenDevelopment.rst b/source/modules/TestDrivenDevelopment.rst index c21139b..724b4fe 100644 --- a/source/modules/TestDrivenDevelopment.rst +++ b/source/modules/TestDrivenDevelopment.rst @@ -1,52 +1,35 @@ - .. _test_driven_development: -FIXME: change the path from my personal to something generic - ####################### Test Driven Development ####################### "Testing" is any strategy for making sure your code behaves as expected. "Unit testing" is a particular strategy, that: -* is easy to run in an automated fashion. -* utilizes isolated tests for each individual function. +* Is easy to run in an automated fashion. +* Utilizes isolated tests for each individual function. - -"Test Driven Development" (TDD) is a development strategy that integrates the development of unit tests with the code itself. In particular, you write the tests *before* you write the code, which seems pretty backward, but it has some real strengths. +"Test Driven Development" (TDD) is a development strategy that integrates the development of unit tests with the code itself. In particular, you write the tests *before* you write the code, and then write code until your tests pass. At first this seems pretty backward, but it has some real strengths. We'll demonstrate this technique with an example. -  -The following is adapted from Mark Pilgrim's excellent "Dive into Python": -https://diveintopython3.problemsolving.io/ +The following is adapted from Mark Pilgrim's excellent "Dive into Python": https://diveintopython3.problemsolving.io/ -The primary difference is that this version uses the simpler pytest testing framework, rather than `unittest`, which is discussed in -:ref:`unit_testing` +The primary difference is that this version uses the simpler pytest testing framework, rather than `unittest`, which is discussed in :ref:`unit_testing`. Unit Testing ============ - | "Certitude is not the test of certainty. We have been cocksure of - many things that were not so." - | — `Oliver Wendell Holmes, - Jr. `__ + "Certitude is not the test of certainty. We have been cocksure of many things that were not so." + -— `Oliver Wendell Holmes, Jr. `__ (Not) Diving In --------------- -Kids today. So spoiled by these fast computers and fancy “dynamic” -languages. Write first, ship second, debug third (if ever). In my day, -we had discipline. **Discipline, I say!** We had to write programs by -*hand*, on *paper*, and feed them to the computer on *punchcards*. And -we *liked it!* - -In this module, you’re going to write and debug a set of utility -functions to convert to and from Roman numerals. - -You’ve most likely seen Roman numerals, even if you didn’t recognize them. You may have seen them in copyrights of old movies and television shows (“Copyright MCMXLVI” instead of “Copyright 1946”), or on the dedication walls of libraries or universities (“established MDCCCLXXXVIII” instead of “established 1888”). You may also have seen them in outlines and bibliographical references. It’s a system of representing numbers that really does date back to the ancient Roman empire (hence the name). +In this module, you're going to write and debug a set of utility functions to convert to and from Roman numerals. +You've most likely seen Roman numerals, even if you didn't recognize them. You may have seen them in copyrights of old movies and television shows ("Copyright MCMXLVI" instead of "Copyright 1946"), or on the dedication walls of libraries or universities ("established MDCCCLXXXVIII" instead of "established 1888"). You may also have seen them in outlines and bibliographical references. It's a system of representing numbers that really does date back to the ancient Roman empire, hence the name. The Rules for Roman Numerals ---------------------------- @@ -63,53 +46,28 @@ In Roman numerals, there are seven characters that are repeated and combined in The following are some general rules for constructing Roman numerals: -* 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. - - -* 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”). +* 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. +* 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"). +* 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. +* The fives characters can not be repeated. 10 is always represented as X, never as VV. 100 is always C, never LL. +* 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"). +The rules for Roman numerals lead to a number of interesting observations: -* 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. +1. There is only one correct way to represent a particular number as a Roman numeral. +2. The converse is also true: if a string of characters is a valid Roman numeral, it represents only one number (that is, it can only be interpreted one way). +3. There is a limited range of numbers that can be expressed as Roman numerals, specifically ``1`` through ``3999``. The Romans did have several ways of expressing larger numbers, for instance by having a bar over a numeral to represent that its normal value should be multiplied by ``1000``. For the purposes of this exercise, let's stipulate that Roman numerals go from ``1`` to ``3999``. +4. There is no way to represent 0 in Roman numerals. +5. There is no way to represent negative numbers in Roman numerals. +6. There is no way to represent fractions or non-integer numbers in Roman numerals. -* The fives characters can not be repeated. 10 is always represented as X, never as VV. 100 is always C, never LL. +Let's start mapping out what a ``roman.py`` module should do. It will have two main functions, ``to_roman()`` and ``from_roman()``. The ``to_roman()`` function should take an integer from ``1`` to ``3999`` and return the Roman numeral representation as a string. -* 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”). +Stop right there. Now let's do something a little unexpected: write a test case that checks whether the ``to_roman()`` function does what you want it to. You read that right: you're going to write code that tests code that you haven't written yet. +This is called *test-driven development*, or TDD. The set of two conversion functions — ``to_roman()``, and later ``from_roman()`` — can be written and tested as a unit, separate from any larger program that uses them. -The rules for Roman numerals lead to a number of interesting observations: - -#. There is only one correct way to represent a particular number as a - Roman numeral. -#. The converse is also true: if a string of characters is a valid Roman - numeral, it represents only one number (that is, it can only be - interpreted one way). -#. There is a limited range of numbers that can be expressed as Roman - numerals, specifically ``1`` through ``3999``. The Romans did have - several ways of expressing larger numbers, for instance by having a - bar over a numeral to represent that its normal value should be - multiplied by ``1000``. For the purposes of this exercise, let’s - stipulate that Roman numerals go from ``1`` to ``3999``. -#. There is no way to represent 0 in Roman numerals. -#. There is no way to represent negative numbers in Roman numerals. -#. There is no way to represent fractions or non-integer numbers in - Roman numerals. - -Let’s start mapping out what a ``roman.py`` module should do. It will -have two main functions, ``to_roman()`` and ``from_roman()``. The -``to_roman()`` function should take an integer from ``1`` to ``3999`` -and return the Roman numeral representation as a string ... - -Stop right there. Now let’s do something a little unexpected: write a -test case that checks whether the ``to_roman()`` function does what you -want it to. You read that right: you’re going to write code that tests -code that you haven’t written yet. - -This is called *test-driven development*, or TDD. The set of two -conversion functions — ``to_roman()``, and later ``from_roman()`` — can -be written and tested as a unit, separate from any larger program that -uses them. - -Technically, you can write unit tests with plain Python -- recall the ``assert`` statement that you have already used to write simple tests. But it is very helpful to use a framework to make it easier to write and run your tests. In this program, we use the `pytest` package: it is both very easy to get started with, and provides a lot of powerful features to aid in testing complex systems. +Technically, you can write unit tests with plain Python. Recall the ``assert`` statement that you have already used to write simple tests. But it is very helpful to use a framework to make it easier to write and run your tests. In this program, we use the `pytest` package. It is both very easy to get started with, and provides a lot of powerful features to aid in testing complex systems. .. note:: ``pytest`` does not come with Python out of the box. But it is easily installable via `pip` (or conda, if you are using conda):: @@ -117,56 +75,32 @@ Technically, you can write unit tests with plain Python -- recall the ``assert`` Once installed, you should have the pytest command available in your terminal. -FIXME: Maybe add a small page on installing and using pytest? - -Unit testing is an important part of an overall testing-centric -development strategy. If you write unit tests, it is important to write -them early and to keep them updated as code and requirements change. -Many people advocate writing tests before they write the code they’re -testing, and that’s the style I’m going to demonstrate here. +Unit testing is an important part of an overall testing-centric development strategy. If you write unit tests, it is important to write them early and to keep them updated as code and requirements change. Many people advocate writing tests before they write the code they're testing, and that's the style I'm going to demonstrate here. But unit tests are beneficial, even critical, no matter when you write them. -- Before writing code, writing unit tests forces you to detail your - requirements in a useful fashion. -- While writing code, unit tests keep you from over-coding. When all - the test cases pass, the function is complete. -- When refactoring code, they can help prove that the new version - behaves the same way as the old version. -- When maintaining code, having tests will help you cover your ass when - someone comes screaming that your latest change broke their old code. - (“But *sir*, all the unit tests passed when I checked it in...”) -- When writing code in a team, having a comprehensive test suite - dramatically decreases the chances that your code will break someone - else’s code, because you can run their unit tests first. (I’ve seen - this sort of thing in code sprints. A team breaks up the assignment, - everybody takes the specs for their task, writes unit tests for it, - then shares their unit tests with the rest of the team. That way, - nobody goes off too far into developing code that doesn’t play well - with others.) +- Before writing code, writing unit tests forces you to detail your requirements in a useful fashion. +- While writing code, unit tests keep you from over-coding. When all the test cases pass, the function is complete. +- When refactoring code, they can help prove that the new version behaves the same way as the old version. +- When maintaining code, having tests will help you cover you when someone comes along saying that your latest change broke their old code. +- When writing code in a team, having a comprehensive test suite dramatically decreases the chances that your code will break someone else's code, because you can run their unit tests first. (I've seen this sort of thing in code sprints. A team breaks up the assignment, everybody takes the specs for their task, writes unit tests for it, then shares their unit tests with the rest of the team. That way, nobody goes off too far into developing code that doesn't play well with others.) A Single Question ----------------- -.. centered:: **Every Test is an Island** +.. centered:: **Every test is an island.** -A test case answers a single question about the code it is testing. A -test case should be able to... +A test case answers a single question about the code it is testing. A test case should be able to: -- Run completely by itself, without any human input. Unit testing is - about automation. -- Determine by itself whether the function it is testing has passed - or failed, without a human interpreting the results. -- Run in isolation, separate from any other test cases (even if they - test the same functions). Each test case is an island. +- Run completely by itself, without any human input. Unit testing is about automation. +- Determine by itself whether the function it is testing has passed or failed, without a human interpreting the results. +- Run in isolation, separate from any other test cases, even if they test the same functions. Each test case is an island. -Given that, let’s build a test case for the first requirement: +Given that, let's build a test case for the first requirement: -1. The ``to_roman()`` function should return the Roman numeral - representation for all integers ``1`` to ``3999``. +1. The ``to_roman()`` function should return the Roman numeral representation for all integers ``1`` to ``3999``. -Let's take a look at -:download:`roman.py <../examples/test_driven_development/roman.py>`. +Let's take a look at :download:`roman.py <../examples/test_driven_development/roman.py>`. .. code-block:: python :linenos: @@ -181,66 +115,65 @@ Let's take a look at tests are expected to be able to be run with the pytest system """ - ## Tests for roman numeral conversion - - KNOWN_VALUES = ( (1, 'I'), - (2, 'II'), - (3, 'III'), - (4, 'IV'), - (5, 'V'), - (6, 'VI'), - (7, 'VII'), - (8, 'VIII'), - (9, 'IX'), - (10, 'X'), - (50, 'L'), - (100, 'C'), - (500, 'D'), - (1000, 'M'), - (31, 'XXXI'), - (148, 'CXLVIII'), - (294, 'CCXCIV'), - (312, 'CCCXII'), - (421, 'CDXXI'), - (528, 'DXXVIII'), - (621, 'DCXXI'), - (782, 'DCCLXXXII'), - (870, 'DCCCLXX'), - (941, 'CMXLI'), - (1043, 'MXLIII'), - (1110, 'MCX'), - (1226, 'MCCXXVI'), - (1301, 'MCCCI'), - (1485, 'MCDLXXXV'), - (1509, 'MDIX'), - (1607, 'MDCVII'), - (1754, 'MDCCLIV'), - (1832, 'MDCCCXXXII'), - (1993, 'MCMXCIII'), - (2074, 'MMLXXIV'), - (2152, 'MMCLII'), - (2212, 'MMCCXII'), - (2343, 'MMCCCXLIII'), - (2499, 'MMCDXCIX'), - (2574, 'MMDLXXIV'), - (2646, 'MMDCXLVI'), - (2723, 'MMDCCXXIII'), - (2892, 'MMDCCCXCII'), - (2975, 'MMCMLXXV'), - (3051, 'MMMLI'), - (3185, 'MMMCLXXXV'), - (3250, 'MMMCCL'), - (3313, 'MMMCCCXIII'), - (3408, 'MMMCDVIII'), - (3501, 'MMMDI'), - (3610, 'MMMDCX'), - (3743, 'MMMDCCXLIII'), - (3844, 'MMMDCCCXLIV'), - (3888, 'MMMDCCCLXXXVIII'), - (3940, 'MMMCMXL'), - (3999, 'MMMCMXCIX'), - ) + ## Tests for roman numeral conversion + KNOWN_VALUES = ((1, 'I'), + (2, 'II'), + (3, 'III'), + (4, 'IV'), + (5, 'V'), + (6, 'VI'), + (7, 'VII'), + (8, 'VIII'), + (9, 'IX'), + (10, 'X'), + (50, 'L'), + (100, 'C'), + (500, 'D'), + (1000, 'M'), + (31, 'XXXI'), + (148, 'CXLVIII'), + (294, 'CCXCIV'), + (312, 'CCCXII'), + (421, 'CDXXI'), + (528, 'DXXVIII'), + (621, 'DCXXI'), + (782, 'DCCLXXXII'), + (870, 'DCCCLXX'), + (941, 'CMXLI'), + (1043, 'MXLIII'), + (1110, 'MCX'), + (1226, 'MCCXXVI'), + (1301, 'MCCCI'), + (1485, 'MCDLXXXV'), + (1509, 'MDIX'), + (1607, 'MDCVII'), + (1754, 'MDCCLIV'), + (1832, 'MDCCCXXXII'), + (1993, 'MCMXCIII'), + (2074, 'MMLXXIV'), + (2152, 'MMCLII'), + (2212, 'MMCCXII'), + (2343, 'MMCCCXLIII'), + (2499, 'MMCDXCIX'), + (2574, 'MMDLXXIV'), + (2646, 'MMDCXLVI'), + (2723, 'MMDCCXXIII'), + (2892, 'MMDCCCXCII'), + (2975, 'MMCMLXXV'), + (3051, 'MMMLI'), + (3185, 'MMMCLXXXV'), + (3250, 'MMMCCL'), + (3313, 'MMMCCCXIII'), + (3408, 'MMMCDVIII'), + (3501, 'MMMDI'), + (3610, 'MMMDCX'), + (3743, 'MMMDCCXLIII'), + (3844, 'MMMDCCCXLIV'), + (3888, 'MMMDCCCLXXXVIII'), + (3940, 'MMMCMXL'), + (3999, 'MMMCMXCIX'), + ) def test_to_roman_known_values(): """ @@ -250,36 +183,23 @@ Let's take a look at result = to_roman(integer) assert numeral == result - -It is not immediately obvious how this code does ... well, *anything*. -It defines a big data structure full of examples and a single function. +It is not immediately obvious how this code does, well, *anything*. It defines a big data structure full of examples and a single function. The entire script has no ``__main__`` block, so even that one function won't run. But it does do something, I promise. -`KNOWN_VALUES` is a big tuple of integer/numeral pairs that were 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. +`KNOWN_VALUES` is a big tuple of integer/numeral pairs that were 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. -.. note:: This is a major challenge of unit testing -- how to catch all the edge cases, without over testing every little thing. +.. note:: This is a major challenge of unit testing: how to catch all the edge cases, without over testing every little thing. -`pytest` makes it really simple to write a test case: simply define a function named ``test_anything``. pytest will identify any function with: "``test_``"" at the start of the name as a test function. +pytest makes it really simple to write a test case: simply define a function named ``test_anything``. pytest will identify any function with "``test_``"" at the start of its name as a test function. -* Every individual test is its own function. A test function takes no parameters, returns no value, and must have a name beginning with the five letters ``test_``. - If a test function exits normally without a failing assertion or other exception, the test is considered passed; if the function raises a failed assertion, failed. +* Every individual test is its own function. A test function takes no parameters, returns no value, and must have a name beginning with the five letters ``test_``. If a test function exits normally without a failing assertion or other exception, the test is considered passed. If the function raises a failed assertion, it is considered failed. If it raises any other type of exception it is considered errored. -In the ``test_to_roman_known_values`` function, you call the actual ``to_roman()`` function. (Well, the function hasn’t been 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. +In the ``test_to_roman_known_values`` function, you call the actual ``to_roman()`` function. (Well, the function hasn't been written yet, but once it is, this is the line that will call it). -.. 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. +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. -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 -accomplished with a simple assertion that the returned value is -equal to the known correct value: +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 accomplished with a simple assertion that the returned value is equal to the known correct value: .. code-block:: python @@ -287,22 +207,14 @@ equal to the known correct value: If the assertion fails, the test fails. -Note that in this case, we are looping through all the known values, testing each one in the loop. If any of the known values fails, the test will fail, and end the test function -- the rest of the values will not be tested. - -If every value returned from ``to_roman()`` matches the known value you expect, the assert will never fail, and ``test_to_roman_known_values`` -eventually exits normally, which means ``to_roman()`` has passed this -test. +Note that in this case, we are looping through all the known values, testing each one in the loop. If any of the known values fails, the test will fail and end the test function. The rest of the values will not be tested. +If every value returned from ``to_roman()`` matches the known value you expect, the assert will never fail, and ``test_to_roman_known_values`` eventually exits normally, which means ``to_roman()`` has passed this test. 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! TDD is a -dance: tests lead, code follows. 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! TDD is a dance: tests lead, code follows. Write a test that fails, then code until it passes. For a small system like this, we can put the code and the tests in the same file. But as you build larger systems, it is customary to put the tests in a separate file -- more on that later. @@ -336,14 +248,11 @@ To run tests with pytest, you pass in the test file on the command line: FAILED roman.py::test_to_roman_known_values - NameError: name 'to_roman'... ============================ 1 failed in 0.15s ============================ -There's a lot going on here! pytest has found your test function, set itself up, and run the tests it finds (in this case only the one). -Then it runs the test (which in this case fails), and reports the failure(s). -Along with the fact that it fails, it tells you why it failed (a ``NameError``) where it failed (line 75 of the file), and shows you the code before the test failure. -This may seem like a lot of information for such a simple case, but it can be invaluable in a more complex system. +There's a lot going on here! pytest has found your test function, set itself up, and run the tests it finds. In this case only the one. Then it runs the test, which in this case fails, and reports the failure(s). Along with the fact that it fails, it tells you why it failed (a ``NameError``), where it failed (line 75 of the file), and shows you the code before the test failure. This may seem like a lot of information for such a simple case, but it can be invaluable in a more complex system. -We got a NameError, because there is no ``to_roman`` function defined in the file. So let's add that now: +We got a ``NameError``, because there is no ``to_roman`` function defined in the file. So let's add that now: -(:download:`roman1.py <../examples/test_driven_development/roman1.py>`) +:download:`roman1.py <../examples/test_driven_development/roman1.py>` .. code-block:: python @@ -353,8 +262,7 @@ We got a NameError, because there is no ``to_roman`` function defined in the fil '''convert an integer to Roman numeral''' pass -At this stage, you want to define the API of the ``to_roman()`` function, but you don’t want to code it yet (your tests need to fail first). -To stub it out, use the Python reserved word ``pass``, which does precisely nothing. +At this stage, you want to define the API of the ``to_roman()`` function, but you don't want to code it yet. Your tests need to fail first. To stub it out, use the Python reserved word ``pass``, which does precisely nothing. Now run pytest again, with the function defined: @@ -385,22 +293,11 @@ Now run pytest again, with the function defined: FAILED roman1.py::test_to_roman_known_values - AssertionError: assert 'I... ============================ 1 failed in 0.15s ============================ -Again, pytest has found the test, run it, and again it failed. -But this time, it failed with an ``AssertionError`` -- one of the known values did not equal what was expected. -In addition to the line number where the failure occurred, pytest tells you exactly what the values being compared were. -In this case, 'I' does not equal ``None`` -- obviously not. But why did you get a ``None`` there? because Python returns None when a function does not explicitly return another value. In this case, the only content in the function is ``pass``, so ``None`` was returned implicitly. +Again, pytest has found the test, run it, and again it failed. But this time, it failed with an ``AssertionError`` -- one of the known values did not equal what was expected. In addition to the line number where the failure occurred, pytest tells you exactly what the values being compared were. In this case, ``I`` does not equal ``None`` -- obviously not. But why did you get a ``None`` there? because Python returns None when a function does not explicitly return another value. In this case, the only content in the function is ``pass``, so ``None`` was returned implicitly. -.. note:: It may seem silly, and a waste of time, to go through this process when you *know* that it will fail: you haven't written the code yet! - But this is, in fact, a useful process. - You have learned that your test is running and that it really does fail when the function does nothing. - This may seem trivial, and, of course, experienced practitioners don't *always* run tests against a do-nothing function. - But when a system gets large, with many hundreds of tests, it's easy for things to get lost -- it really is useful to know for sure that your tests are working before you start to rely on them. +.. note:: It may seem silly, and a waste of time, to go through this process when you *know* that it will fail: you haven't written the code yet! But this is, in fact, a useful process. You have learned that your test is running and that it really does fail when the function does nothing. This may seem trivial, and, of course, experienced practitioners don't *always* run tests against a do-nothing function. But when a system gets large, with many hundreds of tests, it's easy for things to get lost -- it really is useful to know for sure that your tests are working before you start to rely on them. - -Overall, the test run failed because at least one test case did not pass. -When a test case doesn’t pass, pytest distinguishes between failures and errors. -A failure is a failed assertion that fails because the asserted condition is not true. -An error is any other sort of exception raised in the code you’re testing or the test code itself. +Overall, the test run failed because at least one test case did not pass. When a test case doesn't pass, pytest distinguishes between failures and errors. A failure is a failed assertion that fails because the asserted condition is not true. An error is any other sort of exception raised in the code you're testing or the test code itself. *Now*, finally, you can write the ``to_roman()`` function. @@ -513,25 +410,11 @@ An error is any other sort of exception raised in the code you’re testing or t result = to_roman(integer) assert numeral == result -``roman_numeral_map`` is a tuple of tuples which defines three -things: the character representations of the most basic Roman -numerals; the order of the Roman numerals (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)``. It’s not -just single-character Roman numerals; it also defines two-character -pairs like ``CM`` (“one hundred less than one thousand”). This makes -the ``to_roman()`` function code simpler. - -Here’s where the rich data structure of ``roman_numeral_map`` pays -off, because you don’t need any special logic to handle the -subtraction rule. To convert to Roman numerals, simply iterate -through ``roman_numeral_map`` looking for the largest integer value -less than or equal to the input. Once found, add the Roman numeral -representation to the end of the output, subtract the corresponding -integer value from the input, lather, rinse, repeat. - -If you’re still not clear how the ``to_roman()`` function works, add a -``print()`` call to the end of the ``while`` loop: +``roman_numeral_map`` is a tuple of tuples which defines three things: the character representations of the most basic Roman numerals; the order of the Roman numerals (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)``. It's not just single-character Roman numerals; it also defines two-character pairs like ``CM`` ("one hundred less than one thousand"). This makes the ``to_roman()`` function code simpler. + +Here's where the rich data structure of ``roman_numeral_map`` pays off, because you don't need any special logic to handle the subtraction rule. To convert to Roman numerals, simply iterate through ``roman_numeral_map`` looking for the largest integer value less than or equal to the input. Once found, add the Roman numeral representation to the end of the output, subtract the corresponding integer value from the input, lather, rinse, repeat. + +If you're still not clear how the ``to_roman()`` function works, add a ``print()`` call to the end of the ``while`` loop: .. code-block:: python @@ -554,8 +437,7 @@ With the debug ``print()`` statements, the output looks like this: subtracting 4 from input, adding IV to output Out[4]: 'MCDXXIV' -So the ``to_roman()`` function appears to work, at least in this manual -spot check. But will it pass the test case you wrote? +So the ``to_roman()`` function appears to work, at least in this manual spot check. But will it pass the test case you wrote? .. code-block:: @@ -569,55 +451,36 @@ spot check. But will it pass the test case you wrote? ========================== 1 passed in 0.01s ========================== +Hooray! The ``to_roman()`` function passes the "known values" test case. It's not comprehensive, but it does put the function through its paces with a variety of 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. -Hooray! The ``to_roman()`` function passes the “known values” test case. It’s not comprehensive, but it does put the function through -its paces with a variety of 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. - -“Good” input? Hmm. What about bad input? - +"Good" input? Hmm. What about bad input? -“Halt And Catch Fire” +"Halt And Catch Fire" --------------------- The Pythonic way to halt and catch fire is to raise an exception. -It is not enough to test that functions succeed when given good input; -you must also test that they fail when given bad input. And not just any -sort of failure; they must fail in the way you expect. +It is not enough to test that functions succeed when given good input. You must also test that they fail when given bad input. And not just any sort of failure: they must fail in the way you expect. .. code-block:: ipython - In [10]: to_roman(3000) - Out[10]: 'MMM' + In [10]: to_roman(3000) + Out[10]: 'MMM' - In [11]: to_roman(4000) - Out[11]: 'MMMM' + In [11]: to_roman(4000) + Out[11]: 'MMMM' - In [12]: to_roman(5000) - Out[12]: 'MMMMM' + In [12]: to_roman(5000) + Out[12]: 'MMMMM' - In [13]: to_roman(9000) - Out[13]: 'MMMMMMMMM' + In [13]: to_roman(9000) + Out[13]: 'MMMMMMMMM' -That’s definitely *not* what you wanted — that’s not even a valid Roman -numeral! -In fact, after 3000, each of these numbers is outside the range of -acceptable input, but the function returns a bogus value anyway. -Silently returning bad values is *baaaaaaad*; if a program is going -to fail, it is far better if it fails quickly and noisily. “Halt and -catch fire,” as the saying goes. In Python, the way to halt and catch -fire is to raise an exception. +That's definitely *not* what you wanted - that's not even a valid Roman numeral! In fact, after 3000, each of these numbers is outside the range of acceptable input, but the function returns a bogus value anyway. Silently returning bad values is *bad*. If a program is going to fail, it is far better if it fails quickly and noisily. "Halt and catch fire," as the saying goes. In Python, the way to halt and catch fire is to raise an exception. -The question to ask yourself is, “How can I express this as a testable -requirement?” How’s this for starters: +The question to ask yourself is, "How can I express this as a testable requirement?" How's this for starters: - The ``to_roman()`` function should raise an ``ValueError`` when - given an integer greater than ``3999``. + The ``to_roman()`` function should raise an ``ValueError`` when given an integer greater than ``3999``. Why a ValueError? I think it's a good idea to use one of the standard built-in exceptions is there is one that fits your use case. In this case, it is the *value* of the argument that is the problem -- it is too large. So a ``ValueError`` is appropriate. @@ -637,18 +500,15 @@ So how do we test for an exception? What would that test look like? with pytest.raises(ValueError): to_roman(4000) - Like the previous test case, the test itself is a function with a name starting with ``test_``. pytest will know that it's a test due to the name. The test function has a docstring, letting us know what it is testing. -Now look at the body of that function; what the heck is that ``with`` statement? ``with`` is how we invoke a "context manager" -- the code indented after the ``with`` is run in the "context" created, in this case, by the ``pytest.raises`` function. What ``pytest.raises`` does is check to make sure that the Exception specified is raised by the following code. So in this example, if ``to_roman(4000)`` raises an ``ValueError``, the test will pass, and if it does not raise an Exception, or raises a different Exception, the test will fail. +Now look at the body of that function. What the heck is that ``with`` statement? ``with`` is how we invoke a "context manager". The code indented after the ``with`` is run in the "context" created, in this case, by the ``pytest.raises`` function. What ``pytest.raises`` does is check to make sure that the Exception specified is raised by the following code. So in this example, if ``to_roman(4000)`` raises an ``ValueError``, the test will pass, and if it does not raise an Exception, or raises a different Exception, the test will fail. -.. note:: Context managers are a powerful and sometimes complex feature - of Python. They will be covered later in detail, but for now, you only need to know that the code inside the with block runs in a special way controlled by what follows the ``with`` statement, including exception handling. - You will see ``with`` when working with files (:ref:`files`), and you can read more about it in: :ref:`context_managers` +.. note:: Context managers are a powerful and sometimes complex feature of Python. They will be covered later in detail, but for now, you only need to know that the code inside the with block runs in a special way controlled by what follows the ``with`` statement, including exception handling. You will see ``with`` when working with files (:ref:`files`), and you can read more about it in: :ref:`context_managers`. -CAUTION: you are now using a utility from the ``pytest`` package, so you need to make sure to import pytest first: +*CAUTION:* you are now using a utility from the ``pytest`` package, so you need to make sure to import pytest first: .. code-block:: ipython @@ -677,14 +537,11 @@ CAUTION: you are now using a utility from the ``pytest`` package, so you need to FAILED roman3.py::test_too_large - Failed: DID NOT RAISE `. @@ -759,20 +605,14 @@ Well *that’s* not good -- it happily accepted the input and returned an empty with pytest.raises(ValueError): to_roman(0) - def test_negative(): """to_roman should raise an ValueError with negative input""" with pytest.raises(ValueError): to_roman(-1) -The first new test is the ``test_zero()`` function. Like the -``test_too_large()`` function, it it uses the ``pytest.raises`` context manager to call our ``to_roman()`` function with a parameter of 0, and check that it raises the appropriate exception: ``ValueError``. +The first new test is the ``test_zero()`` function. Like the ``test_too_large()`` function, it it uses the ``pytest.raises`` context manager to call our ``to_roman()`` function with a parameter of 0, and check that it raises the appropriate exception: ``ValueError``. -The ``test_negative()`` function is almost identical, except it passes -``-1`` to the ``to_roman()`` function. If either of these new tests -does *not* raise an ``ValueError`` (either because the function -returns an actual value, or because it raises some other exception), -the test is considered failed. +The ``test_negative()`` function is almost identical, except it passes ``-1`` to the ``to_roman()`` function. If either of these new tests does *not* raise an ``ValueError`` (either because the function returns an actual value, or because it raises some other exception), the test is considered failed. Now check that the tests fail: @@ -810,8 +650,7 @@ Now check that the tests fail: FAILED roman5.py::test_negative - Failed: DID NOT RAISE `. @@ -829,19 +668,11 @@ code and see what we can do to make them pass. n -= integer return result -Note the ``not (0 < n < 4000)`` This is a nice Pythonic shortcut: multiple comparisons at once. -This is equivalent to ``not ((0 < n) and (n < 4000))``, but it’s much -easier to read. This one line of code should catch inputs that are -too large, negative, or zero. +Note the ``not (0 < n < 4000)`` This is a nice Pythonic shortcut: multiple comparisons at once. This is equivalent to ``not ((0 < n) and (n < 4000))``, but it's much easier to read. This one line of code should catch inputs that are too large, negative, or zero. -If you change your conditions, make sure to update your -human-readable error strings to match. pytest won’t care, -but it’ll make it difficult to do manual debugging if -your code is throwing incorrectly-described exceptions. +If you change your conditions, make sure to update your human-readable error strings to match. pytest won't care, but it'll make it difficult to do manual debugging if your code is throwing incorrectly-described exceptions. -I could show you a whole series of unrelated examples to show that the -multiple-comparisons-at-once shortcut works, but instead I’ll just run -the unit tests and prove it. +I could show you a whole series of unrelated examples to show that the multiple-comparisons-at-once shortcut works, but instead I'll just run the unit tests and prove it. .. code-block:: ipython @@ -855,11 +686,10 @@ the unit tests and prove it. ========================== 4 passed in 0.01s ========================== -Excellent! The tests all pass -- your code is working! Remember that you still have the "too large" test -- and all the tests of converting numbers. So you know you haven't inadvertently broken anything else. - +Excellent! The tests all pass. Your code is working! Remember that you still have the "too large" test, and all the tests of converting numbers. So you know you haven't inadvertently broken anything else. -And One More Thing ... ----------------------- +And One More Thing +------------------ There was one more functional requirement for converting numbers to Roman numerals: dealing with non-integers. @@ -870,7 +700,7 @@ There was one more functional requirement for converting numbers to Roman numera In [31]: to_roman(0.5) Out[31]: '' -Oh, that’s bad. +Oh, that's bad. .. code-block:: ipython @@ -905,7 +735,7 @@ And while we are at it, test a float type that happens to be an integer. """to_roman should work for floats with integer values""" assert to_roman(3.0) == "III" -Why a ``ValueError`` rather than a ``TypeError``? because it's the value that matters, not the type. It's OK to pass in a float type, as long as the value is an integer. +Why a ``ValueError`` rather than a ``TypeError``? Because it's the value that matters, not the type. It's OK to pass in a float type, as long as the value is an integer. Now check that the test fails properly. @@ -935,7 +765,7 @@ Now check that the test fails properly. Yup -- it failed. -.. hint:: when you add a new test, and see that it fails, also check that there are *more* tests than there were before. In this case, 1 failed, and 5 passed. In the previous run, 4 passed -- so you know there are, in fact, two additional tests, one of which passed. Why might there not be? because we all like to copy-and-paste, and then edit. If you forget to rename the test function, it will overwrite the previous one -- and we want all our tests to be preserved. +.. hint:: When you add a new test and see that it fails, also check that there are *more* tests than there were before. In this case, 1 failed, and 5 passed. In the previous run, 4 passed. So you know that there are, in fact, two additional tests, one of which passed. Why might there not be? Because we all like to copy-and-paste, and then edit. If you forget to rename the test function, it will overwrite the previous one. And we want all our tests to be preserved. So now write the code that makes the test pass. @@ -989,29 +819,16 @@ Finally, check that the code does indeed make the test pass. ========================== 6 passed in 0.02s ========================== - -The ``to_roman()`` function passes all of its tests, and I can’t think -of any more tests, so it’s time to move on to ``from_roman()``. - +The ``to_roman()`` function passes all of its tests, and I can't think of any more tests, so it's time to move on to ``from_roman()``. A Pleasing Symmetry ------------------- -Converting a string from a Roman numeral to an integer sounds more -difficult than converting an integer to a Roman numeral. Certainly there -is the issue of validation. It’s easy to check if an integer is greater -than 0, but a bit harder to check whether a string is a valid Roman -numeral. But we can at least make sure that correct Roman numerals convert correctly. +Converting a string from a Roman numeral to an integer sounds more difficult than converting an integer to a Roman numeral. Certainly there is the issue of validation. It's easy to check if an integer is greater than 0, but a bit harder to check whether a string is a valid Roman numeral. But we can at least make sure that correct Roman numerals convert correctly. -So we have the problem of converting the string itself. As we’ll see in -a minute, thanks to the rich data structure we defined to map individual -Roman numerals to integer values, the nitty-gritty of the -``from_roman()`` function is as straightforward as the ``to_roman()`` -function. +So we have the problem of converting the string itself. As we'll see in a minute, thanks to the rich data structure we defined to map individual Roman numerals to integer values, the nitty-gritty of the ``from_roman()`` function is as straightforward as the ``to_roman()`` function. -But first, the tests. We’ll need a “known values” test to spot-check for -accuracy. Our test suite already contains a mapping of known -values: let’s reuse that. +But first, the tests. We'll need a "known values" test to spot-check for accuracy. Our test suite already contains a mapping of known values so let's reuse that. .. code-block:: python @@ -1021,27 +838,16 @@ values: let’s reuse that. result = from_roman(numeral) assert 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. +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. .. code-block:: python - n = from_roman(to_roman(n)) for all values of n + 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. +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. .. code-block:: python - def test_roundtrip(): '''from_roman(to_roman(n))==n for all n''' for integer in range(1, 4000): @@ -1049,9 +855,7 @@ that the output is the same as the original input. result = from_roman(numeral) assert integer == result - -These new tests won’t even fail properly yet. We haven’t defined a -``from_roman()`` function at all, so they’ll just raise errors. +These new tests won't even fail properly yet. We haven't defined a ``from_roman()`` function at all, so they'll just raise errors. .. code-block:: ipython @@ -1096,7 +900,7 @@ A quick stub function will solve that problem. 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!” +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 properly fail. @@ -1137,10 +941,9 @@ Now the test cases will properly fail. FAILED roman10.py::test_roundtrip - assert 1 == None ===================== 2 failed, 6 passed in 0.11s ===================== +Now it's time to write the ``from_roman()`` function. -Now it’s time to write the ``from_roman()`` function. - -.. code-block:: +.. code-block:: python def from_roman(s): """convert Roman numeral to integer""" @@ -1152,16 +955,11 @@ Now it’s time to write the ``from_roman()`` function. index += len(numeral) return result -The pattern here is the same as the ```to_roman()`` function. -You iterate through your Roman numeral data structure (a tuple of tuples), -but instead of matching the highest integer values as often as possible, -you match the “highest” Roman numeral character -strings as often as possible. +The pattern here is the same as the ``to_roman()`` function. You iterate through your Roman numeral data structure -- a tuple of tuples -- but instead of matching the highest integer values as often as possible, you match the "highest" Roman numeral character strings as often as possible. -If you're not clear how ``from_roman()`` works, add a ``print`` -call to the end of the ``while`` loop: +If you're not clear how ``from_roman()`` works, add a ``print`` call to the end of the ``while`` loop: -.. code-block:: ipython +.. code-block:: python def from_roman(s): """convert Roman numeral to integer""" @@ -1202,65 +1000,24 @@ Time to re-run the tests. ========================== 8 passed in 0.38s ========================== +Two pieces of exciting news here. The first is that the ``from_roman()`` function works for good input, at least for all the *known values*. The second is that the "round trip" test also passed. Combined with the known values tests, you can be reasonably sure that both the ``to_roman()`` and ``from_roman()`` functions 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.) -Two pieces of exciting news here. The first is that the ``from_roman()`` -function works for good input, at least for all the *known -values*. The second is that the “round trip” test also -passed. Combined with the known values tests, you can be reasonably sure -that both the ``to_roman()`` and ``from_roman()`` functions 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.) - -.. 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 (this is known as "coverage"). 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. 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. - +.. 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. This is known as "coverage". 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. When a bug is discovered, the first thing you should do is write a test that exercises that bug. That is, 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. More Bad Input -------------- -Now that the ``from_roman()`` function works properly with good input, -it's time to fit in the last piece of the puzzle: making it work -properly with bad input. That means finding a way to look at a string -and determine if it's a valid Roman numeral. This is inherently more -difficult than validating numeric input -- but doable. Let's start by reviewing the rules. +Now that the ``from_roman()`` function works properly with good input, it's time to fit in the last piece of the puzzle: making it work properly with bad input. That means finding a way to look at a string and determine if it's a valid Roman numeral. This is inherently more difficult than validating numeric input -- but doable. Let's start by reviewing the rules. -As we saw earlier, there are several simple rules for constructing a Roman numeral, using the letters ``M``, -``D``, ``C``, ``L``, ``X``, ``V``, and ``I``. +As we saw earlier, 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: -- 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``. -- 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``\ ”). -- 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``. -- The fives characters can not be repeated. ``10`` is always - represented as ``X``, never as ``VV``. ``100`` is always ``C``, never - ``LL``. -- 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``\ ”). +- 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``. +- 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``\ "). +- 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``. +- The fives characters can not be repeated. ``10`` is always represented as ``X``, never as ``VV``. ``100`` is always ``C``, never ``LL``. +- 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``\ "). Roman numerals can only use certain characters, so we should test to make sure there aren't any other characters in the input: @@ -1279,9 +1036,7 @@ Roman numerals can only use certain characters, so we should test to make sure t print(f"trying: {s}") from_roman(s) -Another 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. +Another 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. .. code-block:: python @@ -1292,8 +1047,7 @@ numerals. How many is “too many” depends on the numeral. print(f"trying: {s}") from_roman(s) -Another useful test would be to check that certain patterns aren’t -repeated. For example, ``IX`` is ``9``, but ``IXIX`` is never valid. +Another useful test would be to check that certain patterns aren't repeated. For example, ``IX`` is ``9``, but ``IXIX`` is never valid. .. code-block:: python @@ -1304,12 +1058,7 @@ repeated. For example, ``IX`` is ``9``, but ``IXIX`` is never valid. print(f"trying: {s}") from_roman(s) - -A forth 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 arbitrarily chosen set of invalid -antecedents: ``I`` before ``M``, ``V`` before ``X``, and so on. +A fourth 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 arbitrarily chosen set of invalid antecedents: ``I`` before ``M``, ``V`` before ``X``, and so on. .. code-block:: python @@ -1320,10 +1069,7 @@ antecedents: ``I`` before ``M``, ``V`` before ``X``, and so on. with pytest.raises(ValueError): from_roman(s) - -All four of these tests should fail, since the ``from_roman()`` -function doesn’t currently have any validity checking. (If they don’t -fail now, then what the heck are they testing?) +All four of these tests should fail, since the ``from_roman()`` function doesn't currently have any validity checking. (If they don't fail now, then what the heck are they testing?) .. code-block:: @@ -1390,19 +1136,17 @@ fail now, then what the heck are they testing?) FAILED roman11.py::test_malformed_antecedents - Failed: DID NOT RAISE `_. You will find that it's using the same logic as here in pure Python. - :download:`roman15.py <../examples/test_driven_development/roman15.py>`. .. code-block:: python @@ -1532,7 +1273,7 @@ This can be done by going through it as a human would: left-to-right, looking fo else: return True -Take a little time to look through that code: it's pretty straightforward, simply going from left to right, and removing whatever is valid at that point. At the end, if there is anything left, it will return False. +Take a little time to look through that code. It's pretty straightforward, simply going from left to right, and removing whatever is valid at that point. At the end, if there is anything left, it will return False. So let's see how well that worked: @@ -1563,14 +1304,11 @@ So let's see how well that worked: ... -Darn, we got a failure! We must have done something wrong. But that's OK, frankly, most of us don't do everything right when we right some code the first time. That's actually one of the key points to TDD -- we thought we'd written the code right, but a test failed -- so we know something's wrong. +Darn, we got a failure! We must have done something wrong. But that's OK. Frankly, most of us don't do everything right when we write some code the first time. That's actually one of the key points to TDD: we thought we'd written the code correctly, but a test failed so we know something's wrong. -But what's wrong? Let's look at the error report. It says that ``from_roman()`` didn't raise a ``ValueError`` -- but on what value? That test checks for a bunch of bad values. +But what's wrong? Let's look at the error report. It says that ``from_roman()`` didn't raise a ``ValueError`` but on what value? That test checks for a bunch of bad values. -Notice what pytest did? See that line: "Captured stdout call"? -pytest has a nifty feature: when it runs tests, it redirects "stdout" -- which is all the stuff that would usually be printed to console -- the results of ``print()`` calls both in the code and the test itself. -If the test passes, then it gets thrown away, so as not to clutter up the report. -But if a test fails, like it did here, then it presents you with all the output that was produced when that test ran. +Notice what pytest did? See that line: "Captured stdout call"? pytest has a nifty feature: when it runs tests, it redirects "stdout" -- which is all the stuff that would usually be printed to console -- the results of ``print()`` calls both in the code and the test itself. If the test passes, then it gets thrown away, so as not to clutter up the report. But if a test fails, like it did here, then it presents you with all the output that was produced when that test ran. In this case, we want to look at the output starting from the bottom. See the line at the top of the output:: @@ -1620,9 +1358,9 @@ Why was that? Time to look at the code. s = s[1:] # now the tens -In this case, it is parsing MCMC -- and the first M has been removed, leaving CMC. +In this case, it is parsing MCMC and the first M has been removed, leaving CMC. -At line 66, the ``"CM"`` (meaning 900) matches, so it is removed, leaving a single C. Then we get to lines 73-75, where it is looking for up to three Cs -- it find one, so that gets removed, leaving an empty string. Ahh! that's the problem! If there was a CM, then there can't also be more Cs. We can fix that by putting that for loop in an ``else`` block: +At line 66, the ``"CM"`` (meaning 900) matches, so it is removed, leaving a single C. Then we get to lines 73-75, where it is looking for up to three Cs. It finds one, so that gets removed, leaving an empty string. Ahh! That's the problem! If there was a CM, then there can't also be more Cs. We can fix that by putting that for loop in an ``else`` block: .. code-block:: python :lineno-start: 62 @@ -1669,7 +1407,6 @@ We put the check for D inside the else as well, as the D is 500 and it can't be roman14.py:290: Failed - Still a failure in the same test. But let's look at the end of the output:: trying: XCX @@ -1687,7 +1424,7 @@ Still a failure in the same test. But let's look at the end of the output:: done s = '' -So this time it failed on XCX -- which makes sense, XC is 90, so you can't have another X (10) after that. Why didn't the code catch that? +So this time it failed on XCX. This makes sense because XC is 90, so you can't have another X (10) after that. Why didn't the code catch that? .. code-block:: python :lineno-start: 77 @@ -1709,7 +1446,6 @@ So this time it failed on XCX -- which makes sense, XC is 90, so you can't have This is actually the SAME bug as before, but for the tens -- it is checking for the Xs after XC and XL, which isn't allowed. Moving that into an else block: - .. code-block:: ipython In [13]: ! pytest roman15.py @@ -1755,8 +1491,7 @@ This is actually the SAME bug as before, but for the tens -- it is checking for FAILED roman15.py::test_malformed_antecedents - Failed: DID NOT RAISE