diff --git a/.gitignore b/.gitignore index b6e4761..c821c84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.DS_Store + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/Makefile b/Makefile deleted file mode 100644 index d0c3cbf..0000000 --- a/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/README.md b/README.md deleted file mode 100644 index faaf969..0000000 --- a/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Python210CourseMaterials - -Course Materials for Python 210 - -**LICENSE:** Creative Commons Attribution-ShareAlike 4.0 International Public License diff --git a/_downloads/002778a2a65b40093409693b6043a07d/roman1.py b/_downloads/002778a2a65b40093409693b6043a07d/roman1.py new file mode 100644 index 0000000..aa01e5c --- /dev/null +++ b/_downloads/002778a2a65b40093409693b6043a07d/roman1.py @@ -0,0 +1,86 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + + +def to_roman(n): + '''convert an integer to Roman numeral''' + pass + + +## 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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result + + diff --git a/_downloads/02d1f83ac629de4767feac2c7890462a/roman6.py b/_downloads/02d1f83ac629de4767feac2c7890462a/roman6.py new file mode 100644 index 0000000..c14cfdf --- /dev/null +++ b/_downloads/02d1f83ac629de4767feac2c7890462a/roman6.py @@ -0,0 +1,132 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + +import pytest + +roman_numeral_map = (('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1)) + + +def to_roman(n): + """convert integer to Roman numeral""" + if not (0 < n < 4000): + raise ValueError("number out of range (must be 1..3999)") + + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + return result + + +## 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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result + + +def test_too_large(): + """ + to_roman should raise an ValueError when passed + values over 3999 + """ + with pytest.raises(ValueError): + to_roman(4000) + + +def test_zero(): + """to_roman should raise an ValueError with 0 input""" + 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) + + + diff --git a/_downloads/051b47da30d80fc908192323355db0cf/test_html_render.py b/_downloads/051b47da30d80fc908192323355db0cf/test_html_render.py new file mode 100644 index 0000000..5833d50 --- /dev/null +++ b/_downloads/051b47da30d80fc908192323355db0cf/test_html_render.py @@ -0,0 +1,264 @@ +""" +test code for html_render.py + +This is just a start -- you will need more tests! +""" + +import io +import pytest + +# import * is often bad form, but makes it easier to test everything in a module. +from html_render import * + + +# utility function for testing render methods +# needs to be used in multiple tests, so we write it once here. +def render_result(element, ind=""): + """ + calls the element's render method, and returns what got rendered as a + string + """ + # the StringIO object is a "file-like" object -- something that + # provides the methods of a file, but keeps everything in memory + # so it can be used to test code that writes to a file, without + # having to actually write to disk. + outfile = io.StringIO() + # this so the tests will work before we tackle indentation + if ind: + element.render(outfile, ind) + else: + element.render(outfile) + return outfile.getvalue() + +######## +# Step 1 +######## + +def test_init(): + """ + This only tests that it can be initialized with and without + some content -- but it's a start + """ + e = Element() + + e = Element("this is some text") + + +def test_append(): + """ + This tests that you can append text + + It doesn't test if it works -- + that will be covered by the render test later + """ + e = Element("this is some text") + e.append("some more text") + + +def test_render_element(): + """ + Tests whether the Element can render two pieces of text + So it is also testing that the append method works correctly. + + It is not testing whether indentation or line feeds are correct. + """ + e = Element("this is some text") + e.append("and this is some more text") + + # This uses the render_results utility above + file_contents = render_result(e).strip() + + # making sure the content got in there. + assert("this is some text") in file_contents + assert("and this is some more text") in file_contents + + # make sure it's in the right order + assert file_contents.index("this is") < file_contents.index("and this") + + # making sure the opening and closing tags are right. + assert file_contents.startswith("") + assert file_contents.endswith("") + +# # Uncomment this one after you get the one above to pass +# # Does it pass right away? +# def test_render_element2(): +# """ +# Tests whether the Element can render two pieces of text +# So it is also testing that the append method works correctly. + +# It is not testing whether indentation or line feeds are correct. +# """ +# e = Element() +# e.append("this is some text") +# e.append("and this is some more text") + +# # This uses the render_results utility above +# file_contents = render_result(e).strip() + +# # making sure the content got in there. +# assert("this is some text") in file_contents +# assert("and this is some more text") in file_contents + +# # make sure it's in the right order +# assert file_contents.index("this is") < file_contents.index("and this") + +# # making sure the opening and closing tags are right. +# assert file_contents.startswith("") +# assert file_contents.endswith("") + + + +# # ######## +# # # Step 2 +# # ######## + +# # tests for the new tags +# def test_html(): +# e = Html("this is some text") +# e.append("and this is some more text") + +# file_contents = render_result(e).strip() + +# assert("this is some text") in file_contents +# assert("and this is some more text") in file_contents +# print(file_contents) +# assert file_contents.endswith("") + + +# def test_body(): +# e = Body("this is some text") +# e.append("and this is some more text") + +# file_contents = render_result(e).strip() + +# assert("this is some text") in file_contents +# assert("and this is some more text") in file_contents + +# assert file_contents.startswith("") +# assert file_contents.endswith("") + + +# def test_p(): +# e = P("this is some text") +# e.append("and this is some more text") + +# file_contents = render_result(e).strip() + +# assert("this is some text") in file_contents +# assert("and this is some more text") in file_contents + +# assert file_contents.startswith("

") +# assert file_contents.endswith("

") + + +# def test_sub_element(): +# """ +# tests that you can add another element and still render properly +# """ +# page = Html() +# page.append("some plain text.") +# page.append(P("A simple paragraph of text")) +# page.append("Some more plain text.") + +# file_contents = render_result(page) +# print(file_contents) # so we can see it if the test fails + +# # note: The previous tests should make sure that the tags are getting +# # properly rendered, so we don't need to test that here. +# assert "some plain text" in file_contents +# assert "A simple paragraph of text" in file_contents +# assert "Some more plain text." in file_contents +# assert "some plain text" in file_contents +# # but make sure the embedded element's tags get rendered! +# assert "

" in file_contents +# assert "

" in file_contents + + + + +######## +# Step 3 +######## + +# Add your tests here! + +# ##################### +# # indentation testing +# # Uncomment for Step 9 -- adding indentation +# ##################### + + +# def test_indent(): +# """ +# Tests that the indentation gets passed through to the renderer +# """ +# html = Html("some content") +# file_contents = render_result(html, ind=" ").rstrip() #remove the end newline + +# print(file_contents) +# lines = file_contents.split("\n") +# assert lines[0].startswith(" <") +# print(repr(lines[-1])) +# assert lines[-1].startswith(" <") + + +# def test_indent_contents(): +# """ +# The contents in a element should be indented more than the tag +# by the amount in the indent class attribute +# """ +# html = Element("some content") +# file_contents = render_result(html, ind="") + +# print(file_contents) +# lines = file_contents.split("\n") +# assert lines[1].startswith(Element.indent) + + +# def test_multiple_indent(): +# """ +# make sure multiple levels get indented fully +# """ +# body = Body() +# body.append(P("some text")) +# html = Html(body) + +# file_contents = render_result(html) + +# print(file_contents) +# lines = file_contents.split("\n") +# for i in range(3): # this needed to be adapted to the tag +# assert lines[i + 1].startswith(i * Element.indent + "<") + +# assert lines[4].startswith(3 * Element.indent + "some") + + +# def test_element_indent1(): +# """ +# Tests whether the Element indents at least simple content + +# we are expecting to to look like this: + +# +# this is some text +# <\html> + +# More complex indentation should be tested later. +# """ +# e = Element("this is some text") + +# # This uses the render_results utility above +# file_contents = render_result(e).strip() + +# # making sure the content got in there. +# assert("this is some text") in file_contents + +# # break into lines to check indentation +# lines = file_contents.split('\n') +# # making sure the opening and closing tags are right. +# assert lines[0] == "" +# # this line should be indented by the amount specified +# # by the class attribute: "indent" +# assert lines[1].startswith(Element.indent + "thi") +# assert lines[2] == "" +# assert file_contents.endswith("") diff --git a/_downloads/07e47474b668fd57c1d833720d29a989/capitalize.zip b/_downloads/07e47474b668fd57c1d833720d29a989/capitalize.zip new file mode 100644 index 0000000..2c7d361 Binary files /dev/null and b/_downloads/07e47474b668fd57c1d833720d29a989/capitalize.zip differ diff --git a/_downloads/09156b87264df3eed45e647a1081ff51/quadratic.py b/_downloads/09156b87264df3eed45e647a1081ff51/quadratic.py new file mode 100644 index 0000000..4b78223 --- /dev/null +++ b/_downloads/09156b87264df3eed45e647a1081ff51/quadratic.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +""" +A quaratic function evaluator + +used to demonstrate callable classes +""" + +class Quadratic: + """ + Class to evaluate quadratic equations + + Each instance wil have a certain set of coefficients + """ + + def __init__(self, A, B, C): + self.A = A + self.B = B + self.C = C + + def __call__(self, x): + return self.A * x**2 + self.B * x + self.C + \ No newline at end of file diff --git a/_downloads/0a8e2144d3df54cba18b122dfca38574/unicodify.py b/_downloads/0a8e2144d3df54cba18b122dfca38574/unicodify.py new file mode 100644 index 0000000..15683ee --- /dev/null +++ b/_downloads/0a8e2144d3df54cba18b122dfca38574/unicodify.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +''' +Decorators to convert all arguments passed to a function or method to +unicode or str, including default arguments + +From: http://axialcorps.com/2014/03/20/unicode-str/ + +''' + + +import sys +import functools +import inspect + +def _convert_arg(arg, from_, conv, enc): + '''Safely convert unicode to string or string to unicode''' + return getattr(arg, conv)(encoding=enc) if isinstance(arg, from_) else arg + +def _wrap_convert(from_type, fn, encoding=None): + '''Decorate a function converting all str arguments to unicode or + vice-versa''' + conv = 'decode' if from_type is str else 'encode' + encoding = encoding or sys.getdefaultencoding() + + # override string defaults using partial + aspec, dflts = inspect.getargspec(fn), {} + if aspec.defaults: + for k,v in zip(aspec.args[-len(aspec.defaults):],aspec.defaults): + dflts[k] = _convert_arg(v, from_type, conv, encoding) + fn = functools.partial(fn, **dflts) + + @functools.wraps(fn.func if isinstance(fn, functools.partial) else fn) + def converted(*args, **kwargs): + args = [_convert_arg(a, from_type, conv, encoding) for a in args] + for k,v in kwargs.iteritems(): + kwargs[k] = _convert_arg(v, from_type, conv, encoding) + return fn(*args, **kwargs) + + return converted + +def unicodify(fn=None, encoding=None): + '''Convert all str arguments to unicode''' + if fn is None: + return functools.partial(unicodify, encoding=encoding) + return _wrap_convert(str, fn, encoding=encoding) + +def stringify(fn=None, encoding=None): + '''Convert all unicode arguments to str''' + if fn is None: + return functools.partial(stringify, encoding=encoding) + return _wrap_convert(unicode, fn, encoding=encoding) + +__all__ = ['unicodify', 'stringify'] \ No newline at end of file diff --git a/_downloads/0ceb7320d480c2a40958bbe7130460f1/test_html_output4.html b/_downloads/0ceb7320d480c2a40958bbe7130460f1/test_html_output4.html new file mode 100644 index 0000000..5846b22 --- /dev/null +++ b/_downloads/0ceb7320d480c2a40958bbe7130460f1/test_html_output4.html @@ -0,0 +1,10 @@ + + +PythonClass = Revision 1087: + + +

+Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+ + \ No newline at end of file diff --git a/_downloads/0ff1b282ffc1bc599348d4646fc91699/test_html_output4.html b/_downloads/0ff1b282ffc1bc599348d4646fc91699/test_html_output4.html new file mode 100644 index 0000000..5846b22 --- /dev/null +++ b/_downloads/0ff1b282ffc1bc599348d4646fc91699/test_html_output4.html @@ -0,0 +1,10 @@ + + +PythonClass = Revision 1087: + + +

+Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+ + \ No newline at end of file diff --git a/_downloads/1299d43327253c58b8a484d1185dc914/unicodify.py b/_downloads/1299d43327253c58b8a484d1185dc914/unicodify.py new file mode 100644 index 0000000..15683ee --- /dev/null +++ b/_downloads/1299d43327253c58b8a484d1185dc914/unicodify.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +''' +Decorators to convert all arguments passed to a function or method to +unicode or str, including default arguments + +From: http://axialcorps.com/2014/03/20/unicode-str/ + +''' + + +import sys +import functools +import inspect + +def _convert_arg(arg, from_, conv, enc): + '''Safely convert unicode to string or string to unicode''' + return getattr(arg, conv)(encoding=enc) if isinstance(arg, from_) else arg + +def _wrap_convert(from_type, fn, encoding=None): + '''Decorate a function converting all str arguments to unicode or + vice-versa''' + conv = 'decode' if from_type is str else 'encode' + encoding = encoding or sys.getdefaultencoding() + + # override string defaults using partial + aspec, dflts = inspect.getargspec(fn), {} + if aspec.defaults: + for k,v in zip(aspec.args[-len(aspec.defaults):],aspec.defaults): + dflts[k] = _convert_arg(v, from_type, conv, encoding) + fn = functools.partial(fn, **dflts) + + @functools.wraps(fn.func if isinstance(fn, functools.partial) else fn) + def converted(*args, **kwargs): + args = [_convert_arg(a, from_type, conv, encoding) for a in args] + for k,v in kwargs.iteritems(): + kwargs[k] = _convert_arg(v, from_type, conv, encoding) + return fn(*args, **kwargs) + + return converted + +def unicodify(fn=None, encoding=None): + '''Convert all str arguments to unicode''' + if fn is None: + return functools.partial(unicodify, encoding=encoding) + return _wrap_convert(str, fn, encoding=encoding) + +def stringify(fn=None, encoding=None): + '''Convert all unicode arguments to str''' + if fn is None: + return functools.partial(stringify, encoding=encoding) + return _wrap_convert(unicode, fn, encoding=encoding) + +__all__ = ['unicodify', 'stringify'] \ No newline at end of file diff --git a/_downloads/13b14cf6f7129c695b65425495ed5916/calculator_functions.py b/_downloads/13b14cf6f7129c695b65425495ed5916/calculator_functions.py new file mode 100644 index 0000000..532d776 --- /dev/null +++ b/_downloads/13b14cf6f7129c695b65425495ed5916/calculator_functions.py @@ -0,0 +1,22 @@ +"""calculator functions""" + + +def add(x, y): + """ Add two numbers + + >>> add(1, 2) + 3 + >>> add(-7, 2) + -5 + """ + return int(x) + int(y) + + +def subtract(x, y): + return int(x) - int(y) + +def multiply(x, y): + return int(x) * int(y) + +def divide(x, y): + return int(x) / int(y) diff --git a/_downloads/14f7079015a09629095a1e335c5c216b/test_html_output2.html b/_downloads/14f7079015a09629095a1e335c5c216b/test_html_output2.html new file mode 100644 index 0000000..4908a12 --- /dev/null +++ b/_downloads/14f7079015a09629095a1e335c5c216b/test_html_output2.html @@ -0,0 +1,10 @@ + + +

+Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+

+And here is another piece of text -- you should be able to add any number +

+ + \ No newline at end of file diff --git a/_downloads/151bc0d618fe0bd8cac43e0b183fd66f/test_calculator.py b/_downloads/151bc0d618fe0bd8cac43e0b183fd66f/test_calculator.py new file mode 100644 index 0000000..bbb129c --- /dev/null +++ b/_downloads/151bc0d618fe0bd8cac43e0b183fd66f/test_calculator.py @@ -0,0 +1,39 @@ +import unittest + +import calculator_functions as calc + + +def setUpModule(): + print("running setup module") + + +class TestCalculatorFunctions(unittest.TestCase): + + def setUp(self): + print("running setup") + self.x = 2 + self.y = 3 + + def tearDown(self): + print("running teardown") + + def test_add(self): + print("running test_add") + self.assertEqual(calc.add(self.x, self.y), 5) + + def test_add2(self): + print("running test_add2") + self.assertEqual(calc.add(7, 8), 15) + + +# class TestCalculatorFunctions2(unittest.TestCase): + +# def setUp(self): +# self.x = 2 +# self.y = 3 + +# def test_add(self): +# self.assertEqual(calc.subtract(self.y, self.x), 1) + +if __name__ == "__main__": + unittest.main() diff --git a/_downloads/17291a22f70b7a2b8620b90132af747e/test_object_canvas.py b/_downloads/17291a22f70b7a2b8620b90132af747e/test_object_canvas.py new file mode 100644 index 0000000..f88a58c --- /dev/null +++ b/_downloads/17291a22f70b7a2b8620b90132af747e/test_object_canvas.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 + +""" +test code for the object_canvas + +Note: Testing image generation is hard. So for now, this mostly just + tests that the rendering function runs. + And during development, you can look at the resulting files. + + One could store "properly" rendered results for future tests to + check against. +""" + +# import os +import pathlib +import object_canvas as oc + +SAVE_ALL=True # save all the temp files? + + +def render_to_file(canvas, filename="test_image.png", save=False): + """ + utility to render a canvas to a file + + :param filename: name of file to render to it will be put in a test_images dir. + + :param remove=True: whether to remove the file after rendering. + """ + path = pathlib.Path("test_images") + path.mkdir(exist_ok=True) + path /= filename + canvas.render(str(path)) + assert path.is_file() + if not (SAVE_ALL or save): + path.unlink() + + +def test_init(): + canvas = oc.ObjectCanvas() + + assert canvas + +def test_backgound(): + canvas = oc.ObjectCanvas(background='blue') + render_to_file(canvas, "blue_background.png") + +def test_polyline(): + """ + can we draw a polyline? + """ + canvas = oc.ObjectCanvas() + points = ((10, 10), # this should be a triangle + (10, 400), + (400, 10), + (10, 10), + ) + + pl = oc.PolyLine(points) + canvas.add_object(pl) + render_to_file(canvas, "polyline.png") + + +def test_circle(): + canvas = oc.ObjectCanvas() + center = (100, 100) + diameter = 75 + for line_width in range(1, 5): + c = oc.Circle(center, + diameter, + line_color="red", + fill_color="blue", + line_width=line_width, + ) + canvas.add_object(c) + center = (center[0] + 50, center[0] + 50) + render_to_file(canvas, "circle.png") + diff --git a/_downloads/18502ed76746be8055f5f7fb718243aa/sherlock.txt b/_downloads/18502ed76746be8055f5f7fb718243aa/sherlock.txt new file mode 100644 index 0000000..4dec201 --- /dev/null +++ b/_downloads/18502ed76746be8055f5f7fb718243aa/sherlock.txt @@ -0,0 +1,13052 @@ +Project Gutenberg's The Adventures of Sherlock Holmes, by Arthur Conan Doyle + +This eBook is for the use of anyone anywhere at no cost and with +almost no restrictions whatsoever. You may copy it, give it away or +re-use it under the terms of the Project Gutenberg License included +with this eBook or online at www.gutenberg.net + + +Title: The Adventures of Sherlock Holmes + +Author: Arthur Conan Doyle + +Posting Date: April 18, 2011 [EBook #1661] +First Posted: November 29, 2002 + +Language: English + + +*** START OF THIS PROJECT GUTENBERG EBOOK THE ADVENTURES OF SHERLOCK HOLMES *** + + + + +Produced by an anonymous Project Gutenberg volunteer and Jose Menendez + + + + + + + + + +THE ADVENTURES OF SHERLOCK HOLMES + +by + +SIR ARTHUR CONAN DOYLE + + + + I. A Scandal in Bohemia + II. The Red-headed League + III. A Case of Identity + IV. The Boscombe Valley Mystery + V. The Five Orange Pips + VI. The Man with the Twisted Lip + VII. The Adventure of the Blue Carbuncle +VIII. The Adventure of the Speckled Band + IX. The Adventure of the Engineer's Thumb + X. The Adventure of the Noble Bachelor + XI. The Adventure of the Beryl Coronet + XII. The Adventure of the Copper Beeches + + + + +ADVENTURE I. A SCANDAL IN BOHEMIA + +I. + +To Sherlock Holmes she is always THE woman. I have seldom heard +him mention her under any other name. In his eyes she eclipses +and predominates the whole of her sex. It was not that he felt +any emotion akin to love for Irene Adler. All emotions, and that +one particularly, were abhorrent to his cold, precise but +admirably balanced mind. He was, I take it, the most perfect +reasoning and observing machine that the world has seen, but as a +lover he would have placed himself in a false position. He never +spoke of the softer passions, save with a gibe and a sneer. They +were admirable things for the observer--excellent for drawing the +veil from men's motives and actions. But for the trained reasoner +to admit such intrusions into his own delicate and finely +adjusted temperament was to introduce a distracting factor which +might throw a doubt upon all his mental results. Grit in a +sensitive instrument, or a crack in one of his own high-power +lenses, would not be more disturbing than a strong emotion in a +nature such as his. And yet there was but one woman to him, and +that woman was the late Irene Adler, of dubious and questionable +memory. + +I had seen little of Holmes lately. My marriage had drifted us +away from each other. My own complete happiness, and the +home-centred interests which rise up around the man who first +finds himself master of his own establishment, were sufficient to +absorb all my attention, while Holmes, who loathed every form of +society with his whole Bohemian soul, remained in our lodgings in +Baker Street, buried among his old books, and alternating from +week to week between cocaine and ambition, the drowsiness of the +drug, and the fierce energy of his own keen nature. He was still, +as ever, deeply attracted by the study of crime, and occupied his +immense faculties and extraordinary powers of observation in +following out those clues, and clearing up those mysteries which +had been abandoned as hopeless by the official police. From time +to time I heard some vague account of his doings: of his summons +to Odessa in the case of the Trepoff murder, of his clearing up +of the singular tragedy of the Atkinson brothers at Trincomalee, +and finally of the mission which he had accomplished so +delicately and successfully for the reigning family of Holland. +Beyond these signs of his activity, however, which I merely +shared with all the readers of the daily press, I knew little of +my former friend and companion. + +One night--it was on the twentieth of March, 1888--I was +returning from a journey to a patient (for I had now returned to +civil practice), when my way led me through Baker Street. As I +passed the well-remembered door, which must always be associated +in my mind with my wooing, and with the dark incidents of the +Study in Scarlet, I was seized with a keen desire to see Holmes +again, and to know how he was employing his extraordinary powers. +His rooms were brilliantly lit, and, even as I looked up, I saw +his tall, spare figure pass twice in a dark silhouette against +the blind. He was pacing the room swiftly, eagerly, with his head +sunk upon his chest and his hands clasped behind him. To me, who +knew his every mood and habit, his attitude and manner told their +own story. He was at work again. He had risen out of his +drug-created dreams and was hot upon the scent of some new +problem. I rang the bell and was shown up to the chamber which +had formerly been in part my own. + +His manner was not effusive. It seldom was; but he was glad, I +think, to see me. With hardly a word spoken, but with a kindly +eye, he waved me to an armchair, threw across his case of cigars, +and indicated a spirit case and a gasogene in the corner. Then he +stood before the fire and looked me over in his singular +introspective fashion. + +"Wedlock suits you," he remarked. "I think, Watson, that you have +put on seven and a half pounds since I saw you." + +"Seven!" I answered. + +"Indeed, I should have thought a little more. Just a trifle more, +I fancy, Watson. And in practice again, I observe. You did not +tell me that you intended to go into harness." + +"Then, how do you know?" + +"I see it, I deduce it. How do I know that you have been getting +yourself very wet lately, and that you have a most clumsy and +careless servant girl?" + +"My dear Holmes," said I, "this is too much. You would certainly +have been burned, had you lived a few centuries ago. It is true +that I had a country walk on Thursday and came home in a dreadful +mess, but as I have changed my clothes I can't imagine how you +deduce it. As to Mary Jane, she is incorrigible, and my wife has +given her notice, but there, again, I fail to see how you work it +out." + +He chuckled to himself and rubbed his long, nervous hands +together. + +"It is simplicity itself," said he; "my eyes tell me that on the +inside of your left shoe, just where the firelight strikes it, +the leather is scored by six almost parallel cuts. Obviously they +have been caused by someone who has very carelessly scraped round +the edges of the sole in order to remove crusted mud from it. +Hence, you see, my double deduction that you had been out in vile +weather, and that you had a particularly malignant boot-slitting +specimen of the London slavey. As to your practice, if a +gentleman walks into my rooms smelling of iodoform, with a black +mark of nitrate of silver upon his right forefinger, and a bulge +on the right side of his top-hat to show where he has secreted +his stethoscope, I must be dull, indeed, if I do not pronounce +him to be an active member of the medical profession." + +I could not help laughing at the ease with which he explained his +process of deduction. "When I hear you give your reasons," I +remarked, "the thing always appears to me to be so ridiculously +simple that I could easily do it myself, though at each +successive instance of your reasoning I am baffled until you +explain your process. And yet I believe that my eyes are as good +as yours." + +"Quite so," he answered, lighting a cigarette, and throwing +himself down into an armchair. "You see, but you do not observe. +The distinction is clear. For example, you have frequently seen +the steps which lead up from the hall to this room." + +"Frequently." + +"How often?" + +"Well, some hundreds of times." + +"Then how many are there?" + +"How many? I don't know." + +"Quite so! You have not observed. And yet you have seen. That is +just my point. Now, I know that there are seventeen steps, +because I have both seen and observed. By-the-way, since you are +interested in these little problems, and since you are good +enough to chronicle one or two of my trifling experiences, you +may be interested in this." He threw over a sheet of thick, +pink-tinted note-paper which had been lying open upon the table. +"It came by the last post," said he. "Read it aloud." + +The note was undated, and without either signature or address. + +"There will call upon you to-night, at a quarter to eight +o'clock," it said, "a gentleman who desires to consult you upon a +matter of the very deepest moment. Your recent services to one of +the royal houses of Europe have shown that you are one who may +safely be trusted with matters which are of an importance which +can hardly be exaggerated. This account of you we have from all +quarters received. Be in your chamber then at that hour, and do +not take it amiss if your visitor wear a mask." + +"This is indeed a mystery," I remarked. "What do you imagine that +it means?" + +"I have no data yet. It is a capital mistake to theorize before +one has data. Insensibly one begins to twist facts to suit +theories, instead of theories to suit facts. But the note itself. +What do you deduce from it?" + +I carefully examined the writing, and the paper upon which it was +written. + +"The man who wrote it was presumably well to do," I remarked, +endeavouring to imitate my companion's processes. "Such paper +could not be bought under half a crown a packet. It is peculiarly +strong and stiff." + +"Peculiar--that is the very word," said Holmes. "It is not an +English paper at all. Hold it up to the light." + +I did so, and saw a large "E" with a small "g," a "P," and a +large "G" with a small "t" woven into the texture of the paper. + +"What do you make of that?" asked Holmes. + +"The name of the maker, no doubt; or his monogram, rather." + +"Not at all. The 'G' with the small 't' stands for +'Gesellschaft,' which is the German for 'Company.' It is a +customary contraction like our 'Co.' 'P,' of course, stands for +'Papier.' Now for the 'Eg.' Let us glance at our Continental +Gazetteer." He took down a heavy brown volume from his shelves. +"Eglow, Eglonitz--here we are, Egria. It is in a German-speaking +country--in Bohemia, not far from Carlsbad. 'Remarkable as being +the scene of the death of Wallenstein, and for its numerous +glass-factories and paper-mills.' Ha, ha, my boy, what do you +make of that?" His eyes sparkled, and he sent up a great blue +triumphant cloud from his cigarette. + +"The paper was made in Bohemia," I said. + +"Precisely. And the man who wrote the note is a German. Do you +note the peculiar construction of the sentence--'This account of +you we have from all quarters received.' A Frenchman or Russian +could not have written that. It is the German who is so +uncourteous to his verbs. It only remains, therefore, to discover +what is wanted by this German who writes upon Bohemian paper and +prefers wearing a mask to showing his face. And here he comes, if +I am not mistaken, to resolve all our doubts." + +As he spoke there was the sharp sound of horses' hoofs and +grating wheels against the curb, followed by a sharp pull at the +bell. Holmes whistled. + +"A pair, by the sound," said he. "Yes," he continued, glancing +out of the window. "A nice little brougham and a pair of +beauties. A hundred and fifty guineas apiece. There's money in +this case, Watson, if there is nothing else." + +"I think that I had better go, Holmes." + +"Not a bit, Doctor. Stay where you are. I am lost without my +Boswell. And this promises to be interesting. It would be a pity +to miss it." + +"But your client--" + +"Never mind him. I may want your help, and so may he. Here he +comes. Sit down in that armchair, Doctor, and give us your best +attention." + +A slow and heavy step, which had been heard upon the stairs and +in the passage, paused immediately outside the door. Then there +was a loud and authoritative tap. + +"Come in!" said Holmes. + +A man entered who could hardly have been less than six feet six +inches in height, with the chest and limbs of a Hercules. His +dress was rich with a richness which would, in England, be looked +upon as akin to bad taste. Heavy bands of astrakhan were slashed +across the sleeves and fronts of his double-breasted coat, while +the deep blue cloak which was thrown over his shoulders was lined +with flame-coloured silk and secured at the neck with a brooch +which consisted of a single flaming beryl. Boots which extended +halfway up his calves, and which were trimmed at the tops with +rich brown fur, completed the impression of barbaric opulence +which was suggested by his whole appearance. He carried a +broad-brimmed hat in his hand, while he wore across the upper +part of his face, extending down past the cheekbones, a black +vizard mask, which he had apparently adjusted that very moment, +for his hand was still raised to it as he entered. From the lower +part of the face he appeared to be a man of strong character, +with a thick, hanging lip, and a long, straight chin suggestive +of resolution pushed to the length of obstinacy. + +"You had my note?" he asked with a deep harsh voice and a +strongly marked German accent. "I told you that I would call." He +looked from one to the other of us, as if uncertain which to +address. + +"Pray take a seat," said Holmes. "This is my friend and +colleague, Dr. Watson, who is occasionally good enough to help me +in my cases. Whom have I the honour to address?" + +"You may address me as the Count Von Kramm, a Bohemian nobleman. +I understand that this gentleman, your friend, is a man of honour +and discretion, whom I may trust with a matter of the most +extreme importance. If not, I should much prefer to communicate +with you alone." + +I rose to go, but Holmes caught me by the wrist and pushed me +back into my chair. "It is both, or none," said he. "You may say +before this gentleman anything which you may say to me." + +The Count shrugged his broad shoulders. "Then I must begin," said +he, "by binding you both to absolute secrecy for two years; at +the end of that time the matter will be of no importance. At +present it is not too much to say that it is of such weight it +may have an influence upon European history." + +"I promise," said Holmes. + +"And I." + +"You will excuse this mask," continued our strange visitor. "The +august person who employs me wishes his agent to be unknown to +you, and I may confess at once that the title by which I have +just called myself is not exactly my own." + +"I was aware of it," said Holmes dryly. + +"The circumstances are of great delicacy, and every precaution +has to be taken to quench what might grow to be an immense +scandal and seriously compromise one of the reigning families of +Europe. To speak plainly, the matter implicates the great House +of Ormstein, hereditary kings of Bohemia." + +"I was also aware of that," murmured Holmes, settling himself +down in his armchair and closing his eyes. + +Our visitor glanced with some apparent surprise at the languid, +lounging figure of the man who had been no doubt depicted to him +as the most incisive reasoner and most energetic agent in Europe. +Holmes slowly reopened his eyes and looked impatiently at his +gigantic client. + +"If your Majesty would condescend to state your case," he +remarked, "I should be better able to advise you." + +The man sprang from his chair and paced up and down the room in +uncontrollable agitation. Then, with a gesture of desperation, he +tore the mask from his face and hurled it upon the ground. "You +are right," he cried; "I am the King. Why should I attempt to +conceal it?" + +"Why, indeed?" murmured Holmes. "Your Majesty had not spoken +before I was aware that I was addressing Wilhelm Gottsreich +Sigismond von Ormstein, Grand Duke of Cassel-Felstein, and +hereditary King of Bohemia." + +"But you can understand," said our strange visitor, sitting down +once more and passing his hand over his high white forehead, "you +can understand that I am not accustomed to doing such business in +my own person. Yet the matter was so delicate that I could not +confide it to an agent without putting myself in his power. I +have come incognito from Prague for the purpose of consulting +you." + +"Then, pray consult," said Holmes, shutting his eyes once more. + +"The facts are briefly these: Some five years ago, during a +lengthy visit to Warsaw, I made the acquaintance of the well-known +adventuress, Irene Adler. The name is no doubt familiar to you." + +"Kindly look her up in my index, Doctor," murmured Holmes without +opening his eyes. For many years he had adopted a system of +docketing all paragraphs concerning men and things, so that it +was difficult to name a subject or a person on which he could not +at once furnish information. In this case I found her biography +sandwiched in between that of a Hebrew rabbi and that of a +staff-commander who had written a monograph upon the deep-sea +fishes. + +"Let me see!" said Holmes. "Hum! Born in New Jersey in the year +1858. Contralto--hum! La Scala, hum! Prima donna Imperial Opera +of Warsaw--yes! Retired from operatic stage--ha! Living in +London--quite so! Your Majesty, as I understand, became entangled +with this young person, wrote her some compromising letters, and +is now desirous of getting those letters back." + +"Precisely so. But how--" + +"Was there a secret marriage?" + +"None." + +"No legal papers or certificates?" + +"None." + +"Then I fail to follow your Majesty. If this young person should +produce her letters for blackmailing or other purposes, how is +she to prove their authenticity?" + +"There is the writing." + +"Pooh, pooh! Forgery." + +"My private note-paper." + +"Stolen." + +"My own seal." + +"Imitated." + +"My photograph." + +"Bought." + +"We were both in the photograph." + +"Oh, dear! That is very bad! Your Majesty has indeed committed an +indiscretion." + +"I was mad--insane." + +"You have compromised yourself seriously." + +"I was only Crown Prince then. I was young. I am but thirty now." + +"It must be recovered." + +"We have tried and failed." + +"Your Majesty must pay. It must be bought." + +"She will not sell." + +"Stolen, then." + +"Five attempts have been made. Twice burglars in my pay ransacked +her house. Once we diverted her luggage when she travelled. Twice +she has been waylaid. There has been no result." + +"No sign of it?" + +"Absolutely none." + +Holmes laughed. "It is quite a pretty little problem," said he. + +"But a very serious one to me," returned the King reproachfully. + +"Very, indeed. And what does she propose to do with the +photograph?" + +"To ruin me." + +"But how?" + +"I am about to be married." + +"So I have heard." + +"To Clotilde Lothman von Saxe-Meningen, second daughter of the +King of Scandinavia. You may know the strict principles of her +family. She is herself the very soul of delicacy. A shadow of a +doubt as to my conduct would bring the matter to an end." + +"And Irene Adler?" + +"Threatens to send them the photograph. And she will do it. I +know that she will do it. You do not know her, but she has a soul +of steel. She has the face of the most beautiful of women, and +the mind of the most resolute of men. Rather than I should marry +another woman, there are no lengths to which she would not +go--none." + +"You are sure that she has not sent it yet?" + +"I am sure." + +"And why?" + +"Because she has said that she would send it on the day when the +betrothal was publicly proclaimed. That will be next Monday." + +"Oh, then we have three days yet," said Holmes with a yawn. "That +is very fortunate, as I have one or two matters of importance to +look into just at present. Your Majesty will, of course, stay in +London for the present?" + +"Certainly. You will find me at the Langham under the name of the +Count Von Kramm." + +"Then I shall drop you a line to let you know how we progress." + +"Pray do so. I shall be all anxiety." + +"Then, as to money?" + +"You have carte blanche." + +"Absolutely?" + +"I tell you that I would give one of the provinces of my kingdom +to have that photograph." + +"And for present expenses?" + +The King took a heavy chamois leather bag from under his cloak +and laid it on the table. + +"There are three hundred pounds in gold and seven hundred in +notes," he said. + +Holmes scribbled a receipt upon a sheet of his note-book and +handed it to him. + +"And Mademoiselle's address?" he asked. + +"Is Briony Lodge, Serpentine Avenue, St. John's Wood." + +Holmes took a note of it. "One other question," said he. "Was the +photograph a cabinet?" + +"It was." + +"Then, good-night, your Majesty, and I trust that we shall soon +have some good news for you. And good-night, Watson," he added, +as the wheels of the royal brougham rolled down the street. "If +you will be good enough to call to-morrow afternoon at three +o'clock I should like to chat this little matter over with you." + + +II. + +At three o'clock precisely I was at Baker Street, but Holmes had +not yet returned. The landlady informed me that he had left the +house shortly after eight o'clock in the morning. I sat down +beside the fire, however, with the intention of awaiting him, +however long he might be. I was already deeply interested in his +inquiry, for, though it was surrounded by none of the grim and +strange features which were associated with the two crimes which +I have already recorded, still, the nature of the case and the +exalted station of his client gave it a character of its own. +Indeed, apart from the nature of the investigation which my +friend had on hand, there was something in his masterly grasp of +a situation, and his keen, incisive reasoning, which made it a +pleasure to me to study his system of work, and to follow the +quick, subtle methods by which he disentangled the most +inextricable mysteries. So accustomed was I to his invariable +success that the very possibility of his failing had ceased to +enter into my head. + +It was close upon four before the door opened, and a +drunken-looking groom, ill-kempt and side-whiskered, with an +inflamed face and disreputable clothes, walked into the room. +Accustomed as I was to my friend's amazing powers in the use of +disguises, I had to look three times before I was certain that it +was indeed he. With a nod he vanished into the bedroom, whence he +emerged in five minutes tweed-suited and respectable, as of old. +Putting his hands into his pockets, he stretched out his legs in +front of the fire and laughed heartily for some minutes. + +"Well, really!" he cried, and then he choked and laughed again +until he was obliged to lie back, limp and helpless, in the +chair. + +"What is it?" + +"It's quite too funny. I am sure you could never guess how I +employed my morning, or what I ended by doing." + +"I can't imagine. I suppose that you have been watching the +habits, and perhaps the house, of Miss Irene Adler." + +"Quite so; but the sequel was rather unusual. I will tell you, +however. I left the house a little after eight o'clock this +morning in the character of a groom out of work. There is a +wonderful sympathy and freemasonry among horsey men. Be one of +them, and you will know all that there is to know. I soon found +Briony Lodge. It is a bijou villa, with a garden at the back, but +built out in front right up to the road, two stories. Chubb lock +to the door. Large sitting-room on the right side, well +furnished, with long windows almost to the floor, and those +preposterous English window fasteners which a child could open. +Behind there was nothing remarkable, save that the passage window +could be reached from the top of the coach-house. I walked round +it and examined it closely from every point of view, but without +noting anything else of interest. + +"I then lounged down the street and found, as I expected, that +there was a mews in a lane which runs down by one wall of the +garden. I lent the ostlers a hand in rubbing down their horses, +and received in exchange twopence, a glass of half and half, two +fills of shag tobacco, and as much information as I could desire +about Miss Adler, to say nothing of half a dozen other people in +the neighbourhood in whom I was not in the least interested, but +whose biographies I was compelled to listen to." + +"And what of Irene Adler?" I asked. + +"Oh, she has turned all the men's heads down in that part. She is +the daintiest thing under a bonnet on this planet. So say the +Serpentine-mews, to a man. She lives quietly, sings at concerts, +drives out at five every day, and returns at seven sharp for +dinner. Seldom goes out at other times, except when she sings. +Has only one male visitor, but a good deal of him. He is dark, +handsome, and dashing, never calls less than once a day, and +often twice. He is a Mr. Godfrey Norton, of the Inner Temple. See +the advantages of a cabman as a confidant. They had driven him +home a dozen times from Serpentine-mews, and knew all about him. +When I had listened to all they had to tell, I began to walk up +and down near Briony Lodge once more, and to think over my plan +of campaign. + +"This Godfrey Norton was evidently an important factor in the +matter. He was a lawyer. That sounded ominous. What was the +relation between them, and what the object of his repeated +visits? Was she his client, his friend, or his mistress? If the +former, she had probably transferred the photograph to his +keeping. If the latter, it was less likely. On the issue of this +question depended whether I should continue my work at Briony +Lodge, or turn my attention to the gentleman's chambers in the +Temple. It was a delicate point, and it widened the field of my +inquiry. I fear that I bore you with these details, but I have to +let you see my little difficulties, if you are to understand the +situation." + +"I am following you closely," I answered. + +"I was still balancing the matter in my mind when a hansom cab +drove up to Briony Lodge, and a gentleman sprang out. He was a +remarkably handsome man, dark, aquiline, and moustached--evidently +the man of whom I had heard. He appeared to be in a +great hurry, shouted to the cabman to wait, and brushed past the +maid who opened the door with the air of a man who was thoroughly +at home. + +"He was in the house about half an hour, and I could catch +glimpses of him in the windows of the sitting-room, pacing up and +down, talking excitedly, and waving his arms. Of her I could see +nothing. Presently he emerged, looking even more flurried than +before. As he stepped up to the cab, he pulled a gold watch from +his pocket and looked at it earnestly, 'Drive like the devil,' he +shouted, 'first to Gross & Hankey's in Regent Street, and then to +the Church of St. Monica in the Edgeware Road. Half a guinea if +you do it in twenty minutes!' + +"Away they went, and I was just wondering whether I should not do +well to follow them when up the lane came a neat little landau, +the coachman with his coat only half-buttoned, and his tie under +his ear, while all the tags of his harness were sticking out of +the buckles. It hadn't pulled up before she shot out of the hall +door and into it. I only caught a glimpse of her at the moment, +but she was a lovely woman, with a face that a man might die for. + +"'The Church of St. Monica, John,' she cried, 'and half a +sovereign if you reach it in twenty minutes.' + +"This was quite too good to lose, Watson. I was just balancing +whether I should run for it, or whether I should perch behind her +landau when a cab came through the street. The driver looked +twice at such a shabby fare, but I jumped in before he could +object. 'The Church of St. Monica,' said I, 'and half a sovereign +if you reach it in twenty minutes.' It was twenty-five minutes to +twelve, and of course it was clear enough what was in the wind. + +"My cabby drove fast. I don't think I ever drove faster, but the +others were there before us. The cab and the landau with their +steaming horses were in front of the door when I arrived. I paid +the man and hurried into the church. There was not a soul there +save the two whom I had followed and a surpliced clergyman, who +seemed to be expostulating with them. They were all three +standing in a knot in front of the altar. I lounged up the side +aisle like any other idler who has dropped into a church. +Suddenly, to my surprise, the three at the altar faced round to +me, and Godfrey Norton came running as hard as he could towards +me. + +"'Thank God,' he cried. 'You'll do. Come! Come!' + +"'What then?' I asked. + +"'Come, man, come, only three minutes, or it won't be legal.' + +"I was half-dragged up to the altar, and before I knew where I was +I found myself mumbling responses which were whispered in my ear, +and vouching for things of which I knew nothing, and generally +assisting in the secure tying up of Irene Adler, spinster, to +Godfrey Norton, bachelor. It was all done in an instant, and +there was the gentleman thanking me on the one side and the lady +on the other, while the clergyman beamed on me in front. It was +the most preposterous position in which I ever found myself in my +life, and it was the thought of it that started me laughing just +now. It seems that there had been some informality about their +license, that the clergyman absolutely refused to marry them +without a witness of some sort, and that my lucky appearance +saved the bridegroom from having to sally out into the streets in +search of a best man. The bride gave me a sovereign, and I mean +to wear it on my watch-chain in memory of the occasion." + +"This is a very unexpected turn of affairs," said I; "and what +then?" + +"Well, I found my plans very seriously menaced. It looked as if +the pair might take an immediate departure, and so necessitate +very prompt and energetic measures on my part. At the church +door, however, they separated, he driving back to the Temple, and +she to her own house. 'I shall drive out in the park at five as +usual,' she said as she left him. I heard no more. They drove +away in different directions, and I went off to make my own +arrangements." + +"Which are?" + +"Some cold beef and a glass of beer," he answered, ringing the +bell. "I have been too busy to think of food, and I am likely to +be busier still this evening. By the way, Doctor, I shall want +your co-operation." + +"I shall be delighted." + +"You don't mind breaking the law?" + +"Not in the least." + +"Nor running a chance of arrest?" + +"Not in a good cause." + +"Oh, the cause is excellent!" + +"Then I am your man." + +"I was sure that I might rely on you." + +"But what is it you wish?" + +"When Mrs. Turner has brought in the tray I will make it clear to +you. Now," he said as he turned hungrily on the simple fare that +our landlady had provided, "I must discuss it while I eat, for I +have not much time. It is nearly five now. In two hours we must +be on the scene of action. Miss Irene, or Madame, rather, returns +from her drive at seven. We must be at Briony Lodge to meet her." + +"And what then?" + +"You must leave that to me. I have already arranged what is to +occur. There is only one point on which I must insist. You must +not interfere, come what may. You understand?" + +"I am to be neutral?" + +"To do nothing whatever. There will probably be some small +unpleasantness. Do not join in it. It will end in my being +conveyed into the house. Four or five minutes afterwards the +sitting-room window will open. You are to station yourself close +to that open window." + +"Yes." + +"You are to watch me, for I will be visible to you." + +"Yes." + +"And when I raise my hand--so--you will throw into the room what +I give you to throw, and will, at the same time, raise the cry of +fire. You quite follow me?" + +"Entirely." + +"It is nothing very formidable," he said, taking a long cigar-shaped +roll from his pocket. "It is an ordinary plumber's smoke-rocket, +fitted with a cap at either end to make it self-lighting. +Your task is confined to that. When you raise your cry of fire, +it will be taken up by quite a number of people. You may then +walk to the end of the street, and I will rejoin you in ten +minutes. I hope that I have made myself clear?" + +"I am to remain neutral, to get near the window, to watch you, +and at the signal to throw in this object, then to raise the cry +of fire, and to wait you at the corner of the street." + +"Precisely." + +"Then you may entirely rely on me." + +"That is excellent. I think, perhaps, it is almost time that I +prepare for the new role I have to play." + +He disappeared into his bedroom and returned in a few minutes in +the character of an amiable and simple-minded Nonconformist +clergyman. His broad black hat, his baggy trousers, his white +tie, his sympathetic smile, and general look of peering and +benevolent curiosity were such as Mr. John Hare alone could have +equalled. It was not merely that Holmes changed his costume. His +expression, his manner, his very soul seemed to vary with every +fresh part that he assumed. The stage lost a fine actor, even as +science lost an acute reasoner, when he became a specialist in +crime. + +It was a quarter past six when we left Baker Street, and it still +wanted ten minutes to the hour when we found ourselves in +Serpentine Avenue. It was already dusk, and the lamps were just +being lighted as we paced up and down in front of Briony Lodge, +waiting for the coming of its occupant. The house was just such +as I had pictured it from Sherlock Holmes' succinct description, +but the locality appeared to be less private than I expected. On +the contrary, for a small street in a quiet neighbourhood, it was +remarkably animated. There was a group of shabbily dressed men +smoking and laughing in a corner, a scissors-grinder with his +wheel, two guardsmen who were flirting with a nurse-girl, and +several well-dressed young men who were lounging up and down with +cigars in their mouths. + +"You see," remarked Holmes, as we paced to and fro in front of +the house, "this marriage rather simplifies matters. The +photograph becomes a double-edged weapon now. The chances are +that she would be as averse to its being seen by Mr. Godfrey +Norton, as our client is to its coming to the eyes of his +princess. Now the question is, Where are we to find the +photograph?" + +"Where, indeed?" + +"It is most unlikely that she carries it about with her. It is +cabinet size. Too large for easy concealment about a woman's +dress. She knows that the King is capable of having her waylaid +and searched. Two attempts of the sort have already been made. We +may take it, then, that she does not carry it about with her." + +"Where, then?" + +"Her banker or her lawyer. There is that double possibility. But +I am inclined to think neither. Women are naturally secretive, +and they like to do their own secreting. Why should she hand it +over to anyone else? She could trust her own guardianship, but +she could not tell what indirect or political influence might be +brought to bear upon a business man. Besides, remember that she +had resolved to use it within a few days. It must be where she +can lay her hands upon it. It must be in her own house." + +"But it has twice been burgled." + +"Pshaw! They did not know how to look." + +"But how will you look?" + +"I will not look." + +"What then?" + +"I will get her to show me." + +"But she will refuse." + +"She will not be able to. But I hear the rumble of wheels. It is +her carriage. Now carry out my orders to the letter." + +As he spoke the gleam of the side-lights of a carriage came round +the curve of the avenue. It was a smart little landau which +rattled up to the door of Briony Lodge. As it pulled up, one of +the loafing men at the corner dashed forward to open the door in +the hope of earning a copper, but was elbowed away by another +loafer, who had rushed up with the same intention. A fierce +quarrel broke out, which was increased by the two guardsmen, who +took sides with one of the loungers, and by the scissors-grinder, +who was equally hot upon the other side. A blow was struck, and +in an instant the lady, who had stepped from her carriage, was +the centre of a little knot of flushed and struggling men, who +struck savagely at each other with their fists and sticks. Holmes +dashed into the crowd to protect the lady; but just as he reached +her he gave a cry and dropped to the ground, with the blood +running freely down his face. At his fall the guardsmen took to +their heels in one direction and the loungers in the other, while +a number of better-dressed people, who had watched the scuffle +without taking part in it, crowded in to help the lady and to +attend to the injured man. Irene Adler, as I will still call her, +had hurried up the steps; but she stood at the top with her +superb figure outlined against the lights of the hall, looking +back into the street. + +"Is the poor gentleman much hurt?" she asked. + +"He is dead," cried several voices. + +"No, no, there's life in him!" shouted another. "But he'll be +gone before you can get him to hospital." + +"He's a brave fellow," said a woman. "They would have had the +lady's purse and watch if it hadn't been for him. They were a +gang, and a rough one, too. Ah, he's breathing now." + +"He can't lie in the street. May we bring him in, marm?" + +"Surely. Bring him into the sitting-room. There is a comfortable +sofa. This way, please!" + +Slowly and solemnly he was borne into Briony Lodge and laid out +in the principal room, while I still observed the proceedings +from my post by the window. The lamps had been lit, but the +blinds had not been drawn, so that I could see Holmes as he lay +upon the couch. I do not know whether he was seized with +compunction at that moment for the part he was playing, but I +know that I never felt more heartily ashamed of myself in my life +than when I saw the beautiful creature against whom I was +conspiring, or the grace and kindliness with which she waited +upon the injured man. And yet it would be the blackest treachery +to Holmes to draw back now from the part which he had intrusted +to me. I hardened my heart, and took the smoke-rocket from under +my ulster. After all, I thought, we are not injuring her. We are +but preventing her from injuring another. + +Holmes had sat up upon the couch, and I saw him motion like a man +who is in need of air. A maid rushed across and threw open the +window. At the same instant I saw him raise his hand and at the +signal I tossed my rocket into the room with a cry of "Fire!" The +word was no sooner out of my mouth than the whole crowd of +spectators, well dressed and ill--gentlemen, ostlers, and +servant-maids--joined in a general shriek of "Fire!" Thick clouds +of smoke curled through the room and out at the open window. I +caught a glimpse of rushing figures, and a moment later the voice +of Holmes from within assuring them that it was a false alarm. +Slipping through the shouting crowd I made my way to the corner +of the street, and in ten minutes was rejoiced to find my +friend's arm in mine, and to get away from the scene of uproar. +He walked swiftly and in silence for some few minutes until we +had turned down one of the quiet streets which lead towards the +Edgeware Road. + +"You did it very nicely, Doctor," he remarked. "Nothing could +have been better. It is all right." + +"You have the photograph?" + +"I know where it is." + +"And how did you find out?" + +"She showed me, as I told you she would." + +"I am still in the dark." + +"I do not wish to make a mystery," said he, laughing. "The matter +was perfectly simple. You, of course, saw that everyone in the +street was an accomplice. They were all engaged for the evening." + +"I guessed as much." + +"Then, when the row broke out, I had a little moist red paint in +the palm of my hand. I rushed forward, fell down, clapped my hand +to my face, and became a piteous spectacle. It is an old trick." + +"That also I could fathom." + +"Then they carried me in. She was bound to have me in. What else +could she do? And into her sitting-room, which was the very room +which I suspected. It lay between that and her bedroom, and I was +determined to see which. They laid me on a couch, I motioned for +air, they were compelled to open the window, and you had your +chance." + +"How did that help you?" + +"It was all-important. When a woman thinks that her house is on +fire, her instinct is at once to rush to the thing which she +values most. It is a perfectly overpowering impulse, and I have +more than once taken advantage of it. In the case of the +Darlington substitution scandal it was of use to me, and also in +the Arnsworth Castle business. A married woman grabs at her baby; +an unmarried one reaches for her jewel-box. Now it was clear to +me that our lady of to-day had nothing in the house more precious +to her than what we are in quest of. She would rush to secure it. +The alarm of fire was admirably done. The smoke and shouting were +enough to shake nerves of steel. She responded beautifully. The +photograph is in a recess behind a sliding panel just above the +right bell-pull. She was there in an instant, and I caught a +glimpse of it as she half-drew it out. When I cried out that it +was a false alarm, she replaced it, glanced at the rocket, rushed +from the room, and I have not seen her since. I rose, and, making +my excuses, escaped from the house. I hesitated whether to +attempt to secure the photograph at once; but the coachman had +come in, and as he was watching me narrowly it seemed safer to +wait. A little over-precipitance may ruin all." + +"And now?" I asked. + +"Our quest is practically finished. I shall call with the King +to-morrow, and with you, if you care to come with us. We will be +shown into the sitting-room to wait for the lady, but it is +probable that when she comes she may find neither us nor the +photograph. It might be a satisfaction to his Majesty to regain +it with his own hands." + +"And when will you call?" + +"At eight in the morning. She will not be up, so that we shall +have a clear field. Besides, we must be prompt, for this marriage +may mean a complete change in her life and habits. I must wire to +the King without delay." + +We had reached Baker Street and had stopped at the door. He was +searching his pockets for the key when someone passing said: + +"Good-night, Mister Sherlock Holmes." + +There were several people on the pavement at the time, but the +greeting appeared to come from a slim youth in an ulster who had +hurried by. + +"I've heard that voice before," said Holmes, staring down the +dimly lit street. "Now, I wonder who the deuce that could have +been." + + +III. + +I slept at Baker Street that night, and we were engaged upon our +toast and coffee in the morning when the King of Bohemia rushed +into the room. + +"You have really got it!" he cried, grasping Sherlock Holmes by +either shoulder and looking eagerly into his face. + +"Not yet." + +"But you have hopes?" + +"I have hopes." + +"Then, come. I am all impatience to be gone." + +"We must have a cab." + +"No, my brougham is waiting." + +"Then that will simplify matters." We descended and started off +once more for Briony Lodge. + +"Irene Adler is married," remarked Holmes. + +"Married! When?" + +"Yesterday." + +"But to whom?" + +"To an English lawyer named Norton." + +"But she could not love him." + +"I am in hopes that she does." + +"And why in hopes?" + +"Because it would spare your Majesty all fear of future +annoyance. If the lady loves her husband, she does not love your +Majesty. If she does not love your Majesty, there is no reason +why she should interfere with your Majesty's plan." + +"It is true. And yet--Well! I wish she had been of my own +station! What a queen she would have made!" He relapsed into a +moody silence, which was not broken until we drew up in +Serpentine Avenue. + +The door of Briony Lodge was open, and an elderly woman stood +upon the steps. She watched us with a sardonic eye as we stepped +from the brougham. + +"Mr. Sherlock Holmes, I believe?" said she. + +"I am Mr. Holmes," answered my companion, looking at her with a +questioning and rather startled gaze. + +"Indeed! My mistress told me that you were likely to call. She +left this morning with her husband by the 5:15 train from Charing +Cross for the Continent." + +"What!" Sherlock Holmes staggered back, white with chagrin and +surprise. "Do you mean that she has left England?" + +"Never to return." + +"And the papers?" asked the King hoarsely. "All is lost." + +"We shall see." He pushed past the servant and rushed into the +drawing-room, followed by the King and myself. The furniture was +scattered about in every direction, with dismantled shelves and +open drawers, as if the lady had hurriedly ransacked them before +her flight. Holmes rushed at the bell-pull, tore back a small +sliding shutter, and, plunging in his hand, pulled out a +photograph and a letter. The photograph was of Irene Adler +herself in evening dress, the letter was superscribed to +"Sherlock Holmes, Esq. To be left till called for." My friend +tore it open and we all three read it together. It was dated at +midnight of the preceding night and ran in this way: + +"MY DEAR MR. SHERLOCK HOLMES,--You really did it very well. You +took me in completely. Until after the alarm of fire, I had not a +suspicion. But then, when I found how I had betrayed myself, I +began to think. I had been warned against you months ago. I had +been told that if the King employed an agent it would certainly +be you. And your address had been given me. Yet, with all this, +you made me reveal what you wanted to know. Even after I became +suspicious, I found it hard to think evil of such a dear, kind +old clergyman. But, you know, I have been trained as an actress +myself. Male costume is nothing new to me. I often take advantage +of the freedom which it gives. I sent John, the coachman, to +watch you, ran up stairs, got into my walking-clothes, as I call +them, and came down just as you departed. + +"Well, I followed you to your door, and so made sure that I was +really an object of interest to the celebrated Mr. Sherlock +Holmes. Then I, rather imprudently, wished you good-night, and +started for the Temple to see my husband. + +"We both thought the best resource was flight, when pursued by +so formidable an antagonist; so you will find the nest empty when +you call to-morrow. As to the photograph, your client may rest in +peace. I love and am loved by a better man than he. The King may +do what he will without hindrance from one whom he has cruelly +wronged. I keep it only to safeguard myself, and to preserve a +weapon which will always secure me from any steps which he might +take in the future. I leave a photograph which he might care to +possess; and I remain, dear Mr. Sherlock Holmes, + + "Very truly yours, + "IRENE NORTON, ne ADLER." + +"What a woman--oh, what a woman!" cried the King of Bohemia, when +we had all three read this epistle. "Did I not tell you how quick +and resolute she was? Would she not have made an admirable queen? +Is it not a pity that she was not on my level?" + +"From what I have seen of the lady she seems indeed to be on a +very different level to your Majesty," said Holmes coldly. "I am +sorry that I have not been able to bring your Majesty's business +to a more successful conclusion." + +"On the contrary, my dear sir," cried the King; "nothing could be +more successful. I know that her word is inviolate. The +photograph is now as safe as if it were in the fire." + +"I am glad to hear your Majesty say so." + +"I am immensely indebted to you. Pray tell me in what way I can +reward you. This ring--" He slipped an emerald snake ring from +his finger and held it out upon the palm of his hand. + +"Your Majesty has something which I should value even more +highly," said Holmes. + +"You have but to name it." + +"This photograph!" + +The King stared at him in amazement. + +"Irene's photograph!" he cried. "Certainly, if you wish it." + +"I thank your Majesty. Then there is no more to be done in the +matter. I have the honour to wish you a very good-morning." He +bowed, and, turning away without observing the hand which the +King had stretched out to him, he set off in my company for his +chambers. + +And that was how a great scandal threatened to affect the kingdom +of Bohemia, and how the best plans of Mr. Sherlock Holmes were +beaten by a woman's wit. He used to make merry over the +cleverness of women, but I have not heard him do it of late. And +when he speaks of Irene Adler, or when he refers to her +photograph, it is always under the honourable title of the woman. + + + +ADVENTURE II. THE RED-HEADED LEAGUE + +I had called upon my friend, Mr. Sherlock Holmes, one day in the +autumn of last year and found him in deep conversation with a +very stout, florid-faced, elderly gentleman with fiery red hair. +With an apology for my intrusion, I was about to withdraw when +Holmes pulled me abruptly into the room and closed the door +behind me. + +"You could not possibly have come at a better time, my dear +Watson," he said cordially. + +"I was afraid that you were engaged." + +"So I am. Very much so." + +"Then I can wait in the next room." + +"Not at all. This gentleman, Mr. Wilson, has been my partner and +helper in many of my most successful cases, and I have no +doubt that he will be of the utmost use to me in yours also." + +The stout gentleman half rose from his chair and gave a bob of +greeting, with a quick little questioning glance from his small +fat-encircled eyes. + +"Try the settee," said Holmes, relapsing into his armchair and +putting his fingertips together, as was his custom when in +judicial moods. "I know, my dear Watson, that you share my love +of all that is bizarre and outside the conventions and humdrum +routine of everyday life. You have shown your relish for it by +the enthusiasm which has prompted you to chronicle, and, if you +will excuse my saying so, somewhat to embellish so many of my own +little adventures." + +"Your cases have indeed been of the greatest interest to me," I +observed. + +"You will remember that I remarked the other day, just before we +went into the very simple problem presented by Miss Mary +Sutherland, that for strange effects and extraordinary +combinations we must go to life itself, which is always far more +daring than any effort of the imagination." + +"A proposition which I took the liberty of doubting." + +"You did, Doctor, but none the less you must come round to my +view, for otherwise I shall keep on piling fact upon fact on you +until your reason breaks down under them and acknowledges me to +be right. Now, Mr. Jabez Wilson here has been good enough to call +upon me this morning, and to begin a narrative which promises to +be one of the most singular which I have listened to for some +time. You have heard me remark that the strangest and most unique +things are very often connected not with the larger but with the +smaller crimes, and occasionally, indeed, where there is room for +doubt whether any positive crime has been committed. As far as I +have heard it is impossible for me to say whether the present +case is an instance of crime or not, but the course of events is +certainly among the most singular that I have ever listened to. +Perhaps, Mr. Wilson, you would have the great kindness to +recommence your narrative. I ask you not merely because my friend +Dr. Watson has not heard the opening part but also because the +peculiar nature of the story makes me anxious to have every +possible detail from your lips. As a rule, when I have heard some +slight indication of the course of events, I am able to guide +myself by the thousands of other similar cases which occur to my +memory. In the present instance I am forced to admit that the +facts are, to the best of my belief, unique." + +The portly client puffed out his chest with an appearance of some +little pride and pulled a dirty and wrinkled newspaper from the +inside pocket of his greatcoat. As he glanced down the +advertisement column, with his head thrust forward and the paper +flattened out upon his knee, I took a good look at the man and +endeavoured, after the fashion of my companion, to read the +indications which might be presented by his dress or appearance. + +I did not gain very much, however, by my inspection. Our visitor +bore every mark of being an average commonplace British +tradesman, obese, pompous, and slow. He wore rather baggy grey +shepherd's check trousers, a not over-clean black frock-coat, +unbuttoned in the front, and a drab waistcoat with a heavy brassy +Albert chain, and a square pierced bit of metal dangling down as +an ornament. A frayed top-hat and a faded brown overcoat with a +wrinkled velvet collar lay upon a chair beside him. Altogether, +look as I would, there was nothing remarkable about the man save +his blazing red head, and the expression of extreme chagrin and +discontent upon his features. + +Sherlock Holmes' quick eye took in my occupation, and he shook +his head with a smile as he noticed my questioning glances. +"Beyond the obvious facts that he has at some time done manual +labour, that he takes snuff, that he is a Freemason, that he has +been in China, and that he has done a considerable amount of +writing lately, I can deduce nothing else." + +Mr. Jabez Wilson started up in his chair, with his forefinger +upon the paper, but his eyes upon my companion. + +"How, in the name of good-fortune, did you know all that, Mr. +Holmes?" he asked. "How did you know, for example, that I did +manual labour. It's as true as gospel, for I began as a ship's +carpenter." + +"Your hands, my dear sir. Your right hand is quite a size larger +than your left. You have worked with it, and the muscles are more +developed." + +"Well, the snuff, then, and the Freemasonry?" + +"I won't insult your intelligence by telling you how I read that, +especially as, rather against the strict rules of your order, you +use an arc-and-compass breastpin." + +"Ah, of course, I forgot that. But the writing?" + +"What else can be indicated by that right cuff so very shiny for +five inches, and the left one with the smooth patch near the +elbow where you rest it upon the desk?" + +"Well, but China?" + +"The fish that you have tattooed immediately above your right +wrist could only have been done in China. I have made a small +study of tattoo marks and have even contributed to the literature +of the subject. That trick of staining the fishes' scales of a +delicate pink is quite peculiar to China. When, in addition, I +see a Chinese coin hanging from your watch-chain, the matter +becomes even more simple." + +Mr. Jabez Wilson laughed heavily. "Well, I never!" said he. "I +thought at first that you had done something clever, but I see +that there was nothing in it, after all." + +"I begin to think, Watson," said Holmes, "that I make a mistake +in explaining. 'Omne ignotum pro magnifico,' you know, and my +poor little reputation, such as it is, will suffer shipwreck if I +am so candid. Can you not find the advertisement, Mr. Wilson?" + +"Yes, I have got it now," he answered with his thick red finger +planted halfway down the column. "Here it is. This is what began +it all. You just read it for yourself, sir." + +I took the paper from him and read as follows: + +"TO THE RED-HEADED LEAGUE: On account of the bequest of the late +Ezekiah Hopkins, of Lebanon, Pennsylvania, U. S. A., there is now +another vacancy open which entitles a member of the League to a +salary of 4 pounds a week for purely nominal services. All +red-headed men who are sound in body and mind and above the age +of twenty-one years, are eligible. Apply in person on Monday, at +eleven o'clock, to Duncan Ross, at the offices of the League, 7 +Pope's Court, Fleet Street." + +"What on earth does this mean?" I ejaculated after I had twice +read over the extraordinary announcement. + +Holmes chuckled and wriggled in his chair, as was his habit when +in high spirits. "It is a little off the beaten track, isn't it?" +said he. "And now, Mr. Wilson, off you go at scratch and tell us +all about yourself, your household, and the effect which this +advertisement had upon your fortunes. You will first make a note, +Doctor, of the paper and the date." + +"It is The Morning Chronicle of April 27, 1890. Just two months +ago." + +"Very good. Now, Mr. Wilson?" + +"Well, it is just as I have been telling you, Mr. Sherlock +Holmes," said Jabez Wilson, mopping his forehead; "I have a small +pawnbroker's business at Coburg Square, near the City. It's not a +very large affair, and of late years it has not done more than +just give me a living. I used to be able to keep two assistants, +but now I only keep one; and I would have a job to pay him but +that he is willing to come for half wages so as to learn the +business." + +"What is the name of this obliging youth?" asked Sherlock Holmes. + +"His name is Vincent Spaulding, and he's not such a youth, +either. It's hard to say his age. I should not wish a smarter +assistant, Mr. Holmes; and I know very well that he could better +himself and earn twice what I am able to give him. But, after +all, if he is satisfied, why should I put ideas in his head?" + +"Why, indeed? You seem most fortunate in having an employ who +comes under the full market price. It is not a common experience +among employers in this age. I don't know that your assistant is +not as remarkable as your advertisement." + +"Oh, he has his faults, too," said Mr. Wilson. "Never was such a +fellow for photography. Snapping away with a camera when he ought +to be improving his mind, and then diving down into the cellar +like a rabbit into its hole to develop his pictures. That is his +main fault, but on the whole he's a good worker. There's no vice +in him." + +"He is still with you, I presume?" + +"Yes, sir. He and a girl of fourteen, who does a bit of simple +cooking and keeps the place clean--that's all I have in the +house, for I am a widower and never had any family. We live very +quietly, sir, the three of us; and we keep a roof over our heads +and pay our debts, if we do nothing more. + +"The first thing that put us out was that advertisement. +Spaulding, he came down into the office just this day eight +weeks, with this very paper in his hand, and he says: + +"'I wish to the Lord, Mr. Wilson, that I was a red-headed man.' + +"'Why that?' I asks. + +"'Why,' says he, 'here's another vacancy on the League of the +Red-headed Men. It's worth quite a little fortune to any man who +gets it, and I understand that there are more vacancies than +there are men, so that the trustees are at their wits' end what +to do with the money. If my hair would only change colour, here's +a nice little crib all ready for me to step into.' + +"'Why, what is it, then?' I asked. You see, Mr. Holmes, I am a +very stay-at-home man, and as my business came to me instead of +my having to go to it, I was often weeks on end without putting +my foot over the door-mat. In that way I didn't know much of what +was going on outside, and I was always glad of a bit of news. + +"'Have you never heard of the League of the Red-headed Men?' he +asked with his eyes open. + +"'Never.' + +"'Why, I wonder at that, for you are eligible yourself for one +of the vacancies.' + +"'And what are they worth?' I asked. + +"'Oh, merely a couple of hundred a year, but the work is slight, +and it need not interfere very much with one's other +occupations.' + +"Well, you can easily think that that made me prick up my ears, +for the business has not been over-good for some years, and an +extra couple of hundred would have been very handy. + +"'Tell me all about it,' said I. + +"'Well,' said he, showing me the advertisement, 'you can see for +yourself that the League has a vacancy, and there is the address +where you should apply for particulars. As far as I can make out, +the League was founded by an American millionaire, Ezekiah +Hopkins, who was very peculiar in his ways. He was himself +red-headed, and he had a great sympathy for all red-headed men; +so when he died it was found that he had left his enormous +fortune in the hands of trustees, with instructions to apply the +interest to the providing of easy berths to men whose hair is of +that colour. From all I hear it is splendid pay and very little to +do.' + +"'But,' said I, 'there would be millions of red-headed men who +would apply.' + +"'Not so many as you might think,' he answered. 'You see it is +really confined to Londoners, and to grown men. This American had +started from London when he was young, and he wanted to do the +old town a good turn. Then, again, I have heard it is no use your +applying if your hair is light red, or dark red, or anything but +real bright, blazing, fiery red. Now, if you cared to apply, Mr. +Wilson, you would just walk in; but perhaps it would hardly be +worth your while to put yourself out of the way for the sake of a +few hundred pounds.' + +"Now, it is a fact, gentlemen, as you may see for yourselves, +that my hair is of a very full and rich tint, so that it seemed +to me that if there was to be any competition in the matter I +stood as good a chance as any man that I had ever met. Vincent +Spaulding seemed to know so much about it that I thought he might +prove useful, so I just ordered him to put up the shutters for +the day and to come right away with me. He was very willing to +have a holiday, so we shut the business up and started off for +the address that was given us in the advertisement. + +"I never hope to see such a sight as that again, Mr. Holmes. From +north, south, east, and west every man who had a shade of red in +his hair had tramped into the city to answer the advertisement. +Fleet Street was choked with red-headed folk, and Pope's Court +looked like a coster's orange barrow. I should not have thought +there were so many in the whole country as were brought together +by that single advertisement. Every shade of colour they +were--straw, lemon, orange, brick, Irish-setter, liver, clay; +but, as Spaulding said, there were not many who had the real +vivid flame-coloured tint. When I saw how many were waiting, I +would have given it up in despair; but Spaulding would not hear +of it. How he did it I could not imagine, but he pushed and +pulled and butted until he got me through the crowd, and right up +to the steps which led to the office. There was a double stream +upon the stair, some going up in hope, and some coming back +dejected; but we wedged in as well as we could and soon found +ourselves in the office." + +"Your experience has been a most entertaining one," remarked +Holmes as his client paused and refreshed his memory with a huge +pinch of snuff. "Pray continue your very interesting statement." + +"There was nothing in the office but a couple of wooden chairs +and a deal table, behind which sat a small man with a head that +was even redder than mine. He said a few words to each candidate +as he came up, and then he always managed to find some fault in +them which would disqualify them. Getting a vacancy did not seem +to be such a very easy matter, after all. However, when our turn +came the little man was much more favourable to me than to any of +the others, and he closed the door as we entered, so that he +might have a private word with us. + +"'This is Mr. Jabez Wilson,' said my assistant, 'and he is +willing to fill a vacancy in the League.' + +"'And he is admirably suited for it,' the other answered. 'He has +every requirement. I cannot recall when I have seen anything so +fine.' He took a step backward, cocked his head on one side, and +gazed at my hair until I felt quite bashful. Then suddenly he +plunged forward, wrung my hand, and congratulated me warmly on my +success. + +"'It would be injustice to hesitate,' said he. 'You will, +however, I am sure, excuse me for taking an obvious precaution.' +With that he seized my hair in both his hands, and tugged until I +yelled with the pain. 'There is water in your eyes,' said he as +he released me. 'I perceive that all is as it should be. But we +have to be careful, for we have twice been deceived by wigs and +once by paint. I could tell you tales of cobbler's wax which +would disgust you with human nature.' He stepped over to the +window and shouted through it at the top of his voice that the +vacancy was filled. A groan of disappointment came up from below, +and the folk all trooped away in different directions until there +was not a red-head to be seen except my own and that of the +manager. + +"'My name,' said he, 'is Mr. Duncan Ross, and I am myself one of +the pensioners upon the fund left by our noble benefactor. Are +you a married man, Mr. Wilson? Have you a family?' + +"I answered that I had not. + +"His face fell immediately. + +"'Dear me!' he said gravely, 'that is very serious indeed! I am +sorry to hear you say that. The fund was, of course, for the +propagation and spread of the red-heads as well as for their +maintenance. It is exceedingly unfortunate that you should be a +bachelor.' + +"My face lengthened at this, Mr. Holmes, for I thought that I was +not to have the vacancy after all; but after thinking it over for +a few minutes he said that it would be all right. + +"'In the case of another,' said he, 'the objection might be +fatal, but we must stretch a point in favour of a man with such a +head of hair as yours. When shall you be able to enter upon your +new duties?' + +"'Well, it is a little awkward, for I have a business already,' +said I. + +"'Oh, never mind about that, Mr. Wilson!' said Vincent Spaulding. +'I should be able to look after that for you.' + +"'What would be the hours?' I asked. + +"'Ten to two.' + +"Now a pawnbroker's business is mostly done of an evening, Mr. +Holmes, especially Thursday and Friday evening, which is just +before pay-day; so it would suit me very well to earn a little in +the mornings. Besides, I knew that my assistant was a good man, +and that he would see to anything that turned up. + +"'That would suit me very well,' said I. 'And the pay?' + +"'Is 4 pounds a week.' + +"'And the work?' + +"'Is purely nominal.' + +"'What do you call purely nominal?' + +"'Well, you have to be in the office, or at least in the +building, the whole time. If you leave, you forfeit your whole +position forever. The will is very clear upon that point. You +don't comply with the conditions if you budge from the office +during that time.' + +"'It's only four hours a day, and I should not think of leaving,' +said I. + +"'No excuse will avail,' said Mr. Duncan Ross; 'neither sickness +nor business nor anything else. There you must stay, or you lose +your billet.' + +"'And the work?' + +"'Is to copy out the "Encyclopaedia Britannica." There is the first +volume of it in that press. You must find your own ink, pens, and +blotting-paper, but we provide this table and chair. Will you be +ready to-morrow?' + +"'Certainly,' I answered. + +"'Then, good-bye, Mr. Jabez Wilson, and let me congratulate you +once more on the important position which you have been fortunate +enough to gain.' He bowed me out of the room and I went home with +my assistant, hardly knowing what to say or do, I was so pleased +at my own good fortune. + +"Well, I thought over the matter all day, and by evening I was in +low spirits again; for I had quite persuaded myself that the +whole affair must be some great hoax or fraud, though what its +object might be I could not imagine. It seemed altogether past +belief that anyone could make such a will, or that they would pay +such a sum for doing anything so simple as copying out the +'Encyclopaedia Britannica.' Vincent Spaulding did what he could to +cheer me up, but by bedtime I had reasoned myself out of the +whole thing. However, in the morning I determined to have a look +at it anyhow, so I bought a penny bottle of ink, and with a +quill-pen, and seven sheets of foolscap paper, I started off for +Pope's Court. + +"Well, to my surprise and delight, everything was as right as +possible. The table was set out ready for me, and Mr. Duncan Ross +was there to see that I got fairly to work. He started me off +upon the letter A, and then he left me; but he would drop in from +time to time to see that all was right with me. At two o'clock he +bade me good-day, complimented me upon the amount that I had +written, and locked the door of the office after me. + +"This went on day after day, Mr. Holmes, and on Saturday the +manager came in and planked down four golden sovereigns for my +week's work. It was the same next week, and the same the week +after. Every morning I was there at ten, and every afternoon I +left at two. By degrees Mr. Duncan Ross took to coming in only +once of a morning, and then, after a time, he did not come in at +all. Still, of course, I never dared to leave the room for an +instant, for I was not sure when he might come, and the billet +was such a good one, and suited me so well, that I would not risk +the loss of it. + +"Eight weeks passed away like this, and I had written about +Abbots and Archery and Armour and Architecture and Attica, and +hoped with diligence that I might get on to the B's before very +long. It cost me something in foolscap, and I had pretty nearly +filled a shelf with my writings. And then suddenly the whole +business came to an end." + +"To an end?" + +"Yes, sir. And no later than this morning. I went to my work as +usual at ten o'clock, but the door was shut and locked, with a +little square of cardboard hammered on to the middle of the +panel with a tack. Here it is, and you can read for yourself." + +He held up a piece of white cardboard about the size of a sheet +of note-paper. It read in this fashion: + + THE RED-HEADED LEAGUE + + IS + + DISSOLVED. + + October 9, 1890. + +Sherlock Holmes and I surveyed this curt announcement and the +rueful face behind it, until the comical side of the affair so +completely overtopped every other consideration that we both +burst out into a roar of laughter. + +"I cannot see that there is anything very funny," cried our +client, flushing up to the roots of his flaming head. "If you can +do nothing better than laugh at me, I can go elsewhere." + +"No, no," cried Holmes, shoving him back into the chair from +which he had half risen. "I really wouldn't miss your case for +the world. It is most refreshingly unusual. But there is, if you +will excuse my saying so, something just a little funny about it. +Pray what steps did you take when you found the card upon the +door?" + +"I was staggered, sir. I did not know what to do. Then I called +at the offices round, but none of them seemed to know anything +about it. Finally, I went to the landlord, who is an accountant +living on the ground-floor, and I asked him if he could tell me +what had become of the Red-headed League. He said that he had +never heard of any such body. Then I asked him who Mr. Duncan +Ross was. He answered that the name was new to him. + +"'Well,' said I, 'the gentleman at No. 4.' + +"'What, the red-headed man?' + +"'Yes.' + +"'Oh,' said he, 'his name was William Morris. He was a solicitor +and was using my room as a temporary convenience until his new +premises were ready. He moved out yesterday.' + +"'Where could I find him?' + +"'Oh, at his new offices. He did tell me the address. Yes, 17 +King Edward Street, near St. Paul's.' + +"I started off, Mr. Holmes, but when I got to that address it was +a manufactory of artificial knee-caps, and no one in it had ever +heard of either Mr. William Morris or Mr. Duncan Ross." + +"And what did you do then?" asked Holmes. + +"I went home to Saxe-Coburg Square, and I took the advice of my +assistant. But he could not help me in any way. He could only say +that if I waited I should hear by post. But that was not quite +good enough, Mr. Holmes. I did not wish to lose such a place +without a struggle, so, as I had heard that you were good enough +to give advice to poor folk who were in need of it, I came right +away to you." + +"And you did very wisely," said Holmes. "Your case is an +exceedingly remarkable one, and I shall be happy to look into it. +From what you have told me I think that it is possible that +graver issues hang from it than might at first sight appear." + +"Grave enough!" said Mr. Jabez Wilson. "Why, I have lost four +pound a week." + +"As far as you are personally concerned," remarked Holmes, "I do +not see that you have any grievance against this extraordinary +league. On the contrary, you are, as I understand, richer by some +30 pounds, to say nothing of the minute knowledge which you have +gained on every subject which comes under the letter A. You have +lost nothing by them." + +"No, sir. But I want to find out about them, and who they are, +and what their object was in playing this prank--if it was a +prank--upon me. It was a pretty expensive joke for them, for it +cost them two and thirty pounds." + +"We shall endeavour to clear up these points for you. And, first, +one or two questions, Mr. Wilson. This assistant of yours who +first called your attention to the advertisement--how long had he +been with you?" + +"About a month then." + +"How did he come?" + +"In answer to an advertisement." + +"Was he the only applicant?" + +"No, I had a dozen." + +"Why did you pick him?" + +"Because he was handy and would come cheap." + +"At half-wages, in fact." + +"Yes." + +"What is he like, this Vincent Spaulding?" + +"Small, stout-built, very quick in his ways, no hair on his face, +though he's not short of thirty. Has a white splash of acid upon +his forehead." + +Holmes sat up in his chair in considerable excitement. "I thought +as much," said he. "Have you ever observed that his ears are +pierced for earrings?" + +"Yes, sir. He told me that a gipsy had done it for him when he +was a lad." + +"Hum!" said Holmes, sinking back in deep thought. "He is still +with you?" + +"Oh, yes, sir; I have only just left him." + +"And has your business been attended to in your absence?" + +"Nothing to complain of, sir. There's never very much to do of a +morning." + +"That will do, Mr. Wilson. I shall be happy to give you an +opinion upon the subject in the course of a day or two. To-day is +Saturday, and I hope that by Monday we may come to a conclusion." + +"Well, Watson," said Holmes when our visitor had left us, "what +do you make of it all?" + +"I make nothing of it," I answered frankly. "It is a most +mysterious business." + +"As a rule," said Holmes, "the more bizarre a thing is the less +mysterious it proves to be. It is your commonplace, featureless +crimes which are really puzzling, just as a commonplace face is +the most difficult to identify. But I must be prompt over this +matter." + +"What are you going to do, then?" I asked. + +"To smoke," he answered. "It is quite a three pipe problem, and I +beg that you won't speak to me for fifty minutes." He curled +himself up in his chair, with his thin knees drawn up to his +hawk-like nose, and there he sat with his eyes closed and his +black clay pipe thrusting out like the bill of some strange bird. +I had come to the conclusion that he had dropped asleep, and +indeed was nodding myself, when he suddenly sprang out of his +chair with the gesture of a man who has made up his mind and put +his pipe down upon the mantelpiece. + +"Sarasate plays at the St. James's Hall this afternoon," he +remarked. "What do you think, Watson? Could your patients spare +you for a few hours?" + +"I have nothing to do to-day. My practice is never very +absorbing." + +"Then put on your hat and come. I am going through the City +first, and we can have some lunch on the way. I observe that +there is a good deal of German music on the programme, which is +rather more to my taste than Italian or French. It is +introspective, and I want to introspect. Come along!" + +We travelled by the Underground as far as Aldersgate; and a short +walk took us to Saxe-Coburg Square, the scene of the singular +story which we had listened to in the morning. It was a poky, +little, shabby-genteel place, where four lines of dingy +two-storied brick houses looked out into a small railed-in +enclosure, where a lawn of weedy grass and a few clumps of faded +laurel-bushes made a hard fight against a smoke-laden and +uncongenial atmosphere. Three gilt balls and a brown board with +"JABEZ WILSON" in white letters, upon a corner house, announced +the place where our red-headed client carried on his business. +Sherlock Holmes stopped in front of it with his head on one side +and looked it all over, with his eyes shining brightly between +puckered lids. Then he walked slowly up the street, and then down +again to the corner, still looking keenly at the houses. Finally +he returned to the pawnbroker's, and, having thumped vigorously +upon the pavement with his stick two or three times, he went up +to the door and knocked. It was instantly opened by a +bright-looking, clean-shaven young fellow, who asked him to step +in. + +"Thank you," said Holmes, "I only wished to ask you how you would +go from here to the Strand." + +"Third right, fourth left," answered the assistant promptly, +closing the door. + +"Smart fellow, that," observed Holmes as we walked away. "He is, +in my judgment, the fourth smartest man in London, and for daring +I am not sure that he has not a claim to be third. I have known +something of him before." + +"Evidently," said I, "Mr. Wilson's assistant counts for a good +deal in this mystery of the Red-headed League. I am sure that you +inquired your way merely in order that you might see him." + +"Not him." + +"What then?" + +"The knees of his trousers." + +"And what did you see?" + +"What I expected to see." + +"Why did you beat the pavement?" + +"My dear doctor, this is a time for observation, not for talk. We +are spies in an enemy's country. We know something of Saxe-Coburg +Square. Let us now explore the parts which lie behind it." + +The road in which we found ourselves as we turned round the +corner from the retired Saxe-Coburg Square presented as great a +contrast to it as the front of a picture does to the back. It was +one of the main arteries which conveyed the traffic of the City +to the north and west. The roadway was blocked with the immense +stream of commerce flowing in a double tide inward and outward, +while the footpaths were black with the hurrying swarm of +pedestrians. It was difficult to realise as we looked at the line +of fine shops and stately business premises that they really +abutted on the other side upon the faded and stagnant square +which we had just quitted. + +"Let me see," said Holmes, standing at the corner and glancing +along the line, "I should like just to remember the order of the +houses here. It is a hobby of mine to have an exact knowledge of +London. There is Mortimer's, the tobacconist, the little +newspaper shop, the Coburg branch of the City and Suburban Bank, +the Vegetarian Restaurant, and McFarlane's carriage-building +depot. That carries us right on to the other block. And now, +Doctor, we've done our work, so it's time we had some play. A +sandwich and a cup of coffee, and then off to violin-land, where +all is sweetness and delicacy and harmony, and there are no +red-headed clients to vex us with their conundrums." + +My friend was an enthusiastic musician, being himself not only a +very capable performer but a composer of no ordinary merit. All +the afternoon he sat in the stalls wrapped in the most perfect +happiness, gently waving his long, thin fingers in time to the +music, while his gently smiling face and his languid, dreamy eyes +were as unlike those of Holmes the sleuth-hound, Holmes the +relentless, keen-witted, ready-handed criminal agent, as it was +possible to conceive. In his singular character the dual nature +alternately asserted itself, and his extreme exactness and +astuteness represented, as I have often thought, the reaction +against the poetic and contemplative mood which occasionally +predominated in him. The swing of his nature took him from +extreme languor to devouring energy; and, as I knew well, he was +never so truly formidable as when, for days on end, he had been +lounging in his armchair amid his improvisations and his +black-letter editions. Then it was that the lust of the chase +would suddenly come upon him, and that his brilliant reasoning +power would rise to the level of intuition, until those who were +unacquainted with his methods would look askance at him as on a +man whose knowledge was not that of other mortals. When I saw him +that afternoon so enwrapped in the music at St. James's Hall I +felt that an evil time might be coming upon those whom he had set +himself to hunt down. + +"You want to go home, no doubt, Doctor," he remarked as we +emerged. + +"Yes, it would be as well." + +"And I have some business to do which will take some hours. This +business at Coburg Square is serious." + +"Why serious?" + +"A considerable crime is in contemplation. I have every reason to +believe that we shall be in time to stop it. But to-day being +Saturday rather complicates matters. I shall want your help +to-night." + +"At what time?" + +"Ten will be early enough." + +"I shall be at Baker Street at ten." + +"Very well. And, I say, Doctor, there may be some little danger, +so kindly put your army revolver in your pocket." He waved his +hand, turned on his heel, and disappeared in an instant among the +crowd. + +I trust that I am not more dense than my neighbours, but I was +always oppressed with a sense of my own stupidity in my dealings +with Sherlock Holmes. Here I had heard what he had heard, I had +seen what he had seen, and yet from his words it was evident that +he saw clearly not only what had happened but what was about to +happen, while to me the whole business was still confused and +grotesque. As I drove home to my house in Kensington I thought +over it all, from the extraordinary story of the red-headed +copier of the "Encyclopaedia" down to the visit to Saxe-Coburg +Square, and the ominous words with which he had parted from me. +What was this nocturnal expedition, and why should I go armed? +Where were we going, and what were we to do? I had the hint from +Holmes that this smooth-faced pawnbroker's assistant was a +formidable man--a man who might play a deep game. I tried to +puzzle it out, but gave it up in despair and set the matter aside +until night should bring an explanation. + +It was a quarter-past nine when I started from home and made my +way across the Park, and so through Oxford Street to Baker +Street. Two hansoms were standing at the door, and as I entered +the passage I heard the sound of voices from above. On entering +his room I found Holmes in animated conversation with two men, +one of whom I recognised as Peter Jones, the official police +agent, while the other was a long, thin, sad-faced man, with a +very shiny hat and oppressively respectable frock-coat. + +"Ha! Our party is complete," said Holmes, buttoning up his +pea-jacket and taking his heavy hunting crop from the rack. +"Watson, I think you know Mr. Jones, of Scotland Yard? Let me +introduce you to Mr. Merryweather, who is to be our companion in +to-night's adventure." + +"We're hunting in couples again, Doctor, you see," said Jones in +his consequential way. "Our friend here is a wonderful man for +starting a chase. All he wants is an old dog to help him to do +the running down." + +"I hope a wild goose may not prove to be the end of our chase," +observed Mr. Merryweather gloomily. + +"You may place considerable confidence in Mr. Holmes, sir," said +the police agent loftily. "He has his own little methods, which +are, if he won't mind my saying so, just a little too theoretical +and fantastic, but he has the makings of a detective in him. It +is not too much to say that once or twice, as in that business of +the Sholto murder and the Agra treasure, he has been more nearly +correct than the official force." + +"Oh, if you say so, Mr. Jones, it is all right," said the +stranger with deference. "Still, I confess that I miss my rubber. +It is the first Saturday night for seven-and-twenty years that I +have not had my rubber." + +"I think you will find," said Sherlock Holmes, "that you will +play for a higher stake to-night than you have ever done yet, and +that the play will be more exciting. For you, Mr. Merryweather, +the stake will be some 30,000 pounds; and for you, Jones, it will +be the man upon whom you wish to lay your hands." + +"John Clay, the murderer, thief, smasher, and forger. He's a +young man, Mr. Merryweather, but he is at the head of his +profession, and I would rather have my bracelets on him than on +any criminal in London. He's a remarkable man, is young John +Clay. His grandfather was a royal duke, and he himself has been +to Eton and Oxford. His brain is as cunning as his fingers, and +though we meet signs of him at every turn, we never know where to +find the man himself. He'll crack a crib in Scotland one week, +and be raising money to build an orphanage in Cornwall the next. +I've been on his track for years and have never set eyes on him +yet." + +"I hope that I may have the pleasure of introducing you to-night. +I've had one or two little turns also with Mr. John Clay, and I +agree with you that he is at the head of his profession. It is +past ten, however, and quite time that we started. If you two +will take the first hansom, Watson and I will follow in the +second." + +Sherlock Holmes was not very communicative during the long drive +and lay back in the cab humming the tunes which he had heard in +the afternoon. We rattled through an endless labyrinth of gas-lit +streets until we emerged into Farrington Street. + +"We are close there now," my friend remarked. "This fellow +Merryweather is a bank director, and personally interested in the +matter. I thought it as well to have Jones with us also. He is +not a bad fellow, though an absolute imbecile in his profession. +He has one positive virtue. He is as brave as a bulldog and as +tenacious as a lobster if he gets his claws upon anyone. Here we +are, and they are waiting for us." + +We had reached the same crowded thoroughfare in which we had +found ourselves in the morning. Our cabs were dismissed, and, +following the guidance of Mr. Merryweather, we passed down a +narrow passage and through a side door, which he opened for us. +Within there was a small corridor, which ended in a very massive +iron gate. This also was opened, and led down a flight of winding +stone steps, which terminated at another formidable gate. Mr. +Merryweather stopped to light a lantern, and then conducted us +down a dark, earth-smelling passage, and so, after opening a +third door, into a huge vault or cellar, which was piled all +round with crates and massive boxes. + +"You are not very vulnerable from above," Holmes remarked as he +held up the lantern and gazed about him. + +"Nor from below," said Mr. Merryweather, striking his stick upon +the flags which lined the floor. "Why, dear me, it sounds quite +hollow!" he remarked, looking up in surprise. + +"I must really ask you to be a little more quiet!" said Holmes +severely. "You have already imperilled the whole success of our +expedition. Might I beg that you would have the goodness to sit +down upon one of those boxes, and not to interfere?" + +The solemn Mr. Merryweather perched himself upon a crate, with a +very injured expression upon his face, while Holmes fell upon his +knees upon the floor and, with the lantern and a magnifying lens, +began to examine minutely the cracks between the stones. A few +seconds sufficed to satisfy him, for he sprang to his feet again +and put his glass in his pocket. + +"We have at least an hour before us," he remarked, "for they can +hardly take any steps until the good pawnbroker is safely in bed. +Then they will not lose a minute, for the sooner they do their +work the longer time they will have for their escape. We are at +present, Doctor--as no doubt you have divined--in the cellar of +the City branch of one of the principal London banks. Mr. +Merryweather is the chairman of directors, and he will explain to +you that there are reasons why the more daring criminals of +London should take a considerable interest in this cellar at +present." + +"It is our French gold," whispered the director. "We have had +several warnings that an attempt might be made upon it." + +"Your French gold?" + +"Yes. We had occasion some months ago to strengthen our resources +and borrowed for that purpose 30,000 napoleons from the Bank of +France. It has become known that we have never had occasion to +unpack the money, and that it is still lying in our cellar. The +crate upon which I sit contains 2,000 napoleons packed between +layers of lead foil. Our reserve of bullion is much larger at +present than is usually kept in a single branch office, and the +directors have had misgivings upon the subject." + +"Which were very well justified," observed Holmes. "And now it is +time that we arranged our little plans. I expect that within an +hour matters will come to a head. In the meantime Mr. +Merryweather, we must put the screen over that dark lantern." + +"And sit in the dark?" + +"I am afraid so. I had brought a pack of cards in my pocket, and +I thought that, as we were a partie carre, you might have your +rubber after all. But I see that the enemy's preparations have +gone so far that we cannot risk the presence of a light. And, +first of all, we must choose our positions. These are daring men, +and though we shall take them at a disadvantage, they may do us +some harm unless we are careful. I shall stand behind this crate, +and do you conceal yourselves behind those. Then, when I flash a +light upon them, close in swiftly. If they fire, Watson, have no +compunction about shooting them down." + +I placed my revolver, cocked, upon the top of the wooden case +behind which I crouched. Holmes shot the slide across the front +of his lantern and left us in pitch darkness--such an absolute +darkness as I have never before experienced. The smell of hot +metal remained to assure us that the light was still there, ready +to flash out at a moment's notice. To me, with my nerves worked +up to a pitch of expectancy, there was something depressing and +subduing in the sudden gloom, and in the cold dank air of the +vault. + +"They have but one retreat," whispered Holmes. "That is back +through the house into Saxe-Coburg Square. I hope that you have +done what I asked you, Jones?" + +"I have an inspector and two officers waiting at the front door." + +"Then we have stopped all the holes. And now we must be silent +and wait." + +What a time it seemed! From comparing notes afterwards it was but +an hour and a quarter, yet it appeared to me that the night must +have almost gone and the dawn be breaking above us. My limbs +were weary and stiff, for I feared to change my position; yet my +nerves were worked up to the highest pitch of tension, and my +hearing was so acute that I could not only hear the gentle +breathing of my companions, but I could distinguish the deeper, +heavier in-breath of the bulky Jones from the thin, sighing note +of the bank director. From my position I could look over the case +in the direction of the floor. Suddenly my eyes caught the glint +of a light. + +At first it was but a lurid spark upon the stone pavement. Then +it lengthened out until it became a yellow line, and then, +without any warning or sound, a gash seemed to open and a hand +appeared, a white, almost womanly hand, which felt about in the +centre of the little area of light. For a minute or more the +hand, with its writhing fingers, protruded out of the floor. Then +it was withdrawn as suddenly as it appeared, and all was dark +again save the single lurid spark which marked a chink between +the stones. + +Its disappearance, however, was but momentary. With a rending, +tearing sound, one of the broad, white stones turned over upon +its side and left a square, gaping hole, through which streamed +the light of a lantern. Over the edge there peeped a clean-cut, +boyish face, which looked keenly about it, and then, with a hand +on either side of the aperture, drew itself shoulder-high and +waist-high, until one knee rested upon the edge. In another +instant he stood at the side of the hole and was hauling after +him a companion, lithe and small like himself, with a pale face +and a shock of very red hair. + +"It's all clear," he whispered. "Have you the chisel and the +bags? Great Scott! Jump, Archie, jump, and I'll swing for it!" + +Sherlock Holmes had sprung out and seized the intruder by the +collar. The other dived down the hole, and I heard the sound of +rending cloth as Jones clutched at his skirts. The light flashed +upon the barrel of a revolver, but Holmes' hunting crop came +down on the man's wrist, and the pistol clinked upon the stone +floor. + +"It's no use, John Clay," said Holmes blandly. "You have no +chance at all." + +"So I see," the other answered with the utmost coolness. "I fancy +that my pal is all right, though I see you have got his +coat-tails." + +"There are three men waiting for him at the door," said Holmes. + +"Oh, indeed! You seem to have done the thing very completely. I +must compliment you." + +"And I you," Holmes answered. "Your red-headed idea was very new +and effective." + +"You'll see your pal again presently," said Jones. "He's quicker +at climbing down holes than I am. Just hold out while I fix the +derbies." + +"I beg that you will not touch me with your filthy hands," +remarked our prisoner as the handcuffs clattered upon his wrists. +"You may not be aware that I have royal blood in my veins. Have +the goodness, also, when you address me always to say 'sir' and +'please.'" + +"All right," said Jones with a stare and a snigger. "Well, would +you please, sir, march upstairs, where we can get a cab to carry +your Highness to the police-station?" + +"That is better," said John Clay serenely. He made a sweeping bow +to the three of us and walked quietly off in the custody of the +detective. + +"Really, Mr. Holmes," said Mr. Merryweather as we followed them +from the cellar, "I do not know how the bank can thank you or +repay you. There is no doubt that you have detected and defeated +in the most complete manner one of the most determined attempts +at bank robbery that have ever come within my experience." + +"I have had one or two little scores of my own to settle with Mr. +John Clay," said Holmes. "I have been at some small expense over +this matter, which I shall expect the bank to refund, but beyond +that I am amply repaid by having had an experience which is in +many ways unique, and by hearing the very remarkable narrative of +the Red-headed League." + + +"You see, Watson," he explained in the early hours of the morning +as we sat over a glass of whisky and soda in Baker Street, "it +was perfectly obvious from the first that the only possible +object of this rather fantastic business of the advertisement of +the League, and the copying of the 'Encyclopaedia,' must be to get +this not over-bright pawnbroker out of the way for a number of +hours every day. It was a curious way of managing it, but, +really, it would be difficult to suggest a better. The method was +no doubt suggested to Clay's ingenious mind by the colour of his +accomplice's hair. The 4 pounds a week was a lure which must draw +him, and what was it to them, who were playing for thousands? +They put in the advertisement, one rogue has the temporary +office, the other rogue incites the man to apply for it, and +together they manage to secure his absence every morning in the +week. From the time that I heard of the assistant having come for +half wages, it was obvious to me that he had some strong motive +for securing the situation." + +"But how could you guess what the motive was?" + +"Had there been women in the house, I should have suspected a +mere vulgar intrigue. That, however, was out of the question. The +man's business was a small one, and there was nothing in his +house which could account for such elaborate preparations, and +such an expenditure as they were at. It must, then, be something +out of the house. What could it be? I thought of the assistant's +fondness for photography, and his trick of vanishing into the +cellar. The cellar! There was the end of this tangled clue. Then +I made inquiries as to this mysterious assistant and found that I +had to deal with one of the coolest and most daring criminals in +London. He was doing something in the cellar--something which +took many hours a day for months on end. What could it be, once +more? I could think of nothing save that he was running a tunnel +to some other building. + +"So far I had got when we went to visit the scene of action. I +surprised you by beating upon the pavement with my stick. I was +ascertaining whether the cellar stretched out in front or behind. +It was not in front. Then I rang the bell, and, as I hoped, the +assistant answered it. We have had some skirmishes, but we had +never set eyes upon each other before. I hardly looked at his +face. His knees were what I wished to see. You must yourself have +remarked how worn, wrinkled, and stained they were. They spoke of +those hours of burrowing. The only remaining point was what they +were burrowing for. I walked round the corner, saw the City and +Suburban Bank abutted on our friend's premises, and felt that I +had solved my problem. When you drove home after the concert I +called upon Scotland Yard and upon the chairman of the bank +directors, with the result that you have seen." + +"And how could you tell that they would make their attempt +to-night?" I asked. + +"Well, when they closed their League offices that was a sign that +they cared no longer about Mr. Jabez Wilson's presence--in other +words, that they had completed their tunnel. But it was essential +that they should use it soon, as it might be discovered, or the +bullion might be removed. Saturday would suit them better than +any other day, as it would give them two days for their escape. +For all these reasons I expected them to come to-night." + +"You reasoned it out beautifully," I exclaimed in unfeigned +admiration. "It is so long a chain, and yet every link rings +true." + +"It saved me from ennui," he answered, yawning. "Alas! I already +feel it closing in upon me. My life is spent in one long effort +to escape from the commonplaces of existence. These little +problems help me to do so." + +"And you are a benefactor of the race," said I. + +He shrugged his shoulders. "Well, perhaps, after all, it is of +some little use," he remarked. "'L'homme c'est rien--l'oeuvre +c'est tout,' as Gustave Flaubert wrote to George Sand." + + + +ADVENTURE III. A CASE OF IDENTITY + +"My dear fellow," said Sherlock Holmes as we sat on either side +of the fire in his lodgings at Baker Street, "life is infinitely +stranger than anything which the mind of man could invent. We +would not dare to conceive the things which are really mere +commonplaces of existence. If we could fly out of that window +hand in hand, hover over this great city, gently remove the +roofs, and peep in at the queer things which are going on, the +strange coincidences, the plannings, the cross-purposes, the +wonderful chains of events, working through generations, and +leading to the most outr results, it would make all fiction with +its conventionalities and foreseen conclusions most stale and +unprofitable." + +"And yet I am not convinced of it," I answered. "The cases which +come to light in the papers are, as a rule, bald enough, and +vulgar enough. We have in our police reports realism pushed to +its extreme limits, and yet the result is, it must be confessed, +neither fascinating nor artistic." + +"A certain selection and discretion must be used in producing a +realistic effect," remarked Holmes. "This is wanting in the +police report, where more stress is laid, perhaps, upon the +platitudes of the magistrate than upon the details, which to an +observer contain the vital essence of the whole matter. Depend +upon it, there is nothing so unnatural as the commonplace." + +I smiled and shook my head. "I can quite understand your thinking +so," I said. "Of course, in your position of unofficial adviser +and helper to everybody who is absolutely puzzled, throughout +three continents, you are brought in contact with all that is +strange and bizarre. But here"--I picked up the morning paper +from the ground--"let us put it to a practical test. Here is the +first heading upon which I come. 'A husband's cruelty to his +wife.' There is half a column of print, but I know without +reading it that it is all perfectly familiar to me. There is, of +course, the other woman, the drink, the push, the blow, the +bruise, the sympathetic sister or landlady. The crudest of +writers could invent nothing more crude." + +"Indeed, your example is an unfortunate one for your argument," +said Holmes, taking the paper and glancing his eye down it. "This +is the Dundas separation case, and, as it happens, I was engaged +in clearing up some small points in connection with it. The +husband was a teetotaler, there was no other woman, and the +conduct complained of was that he had drifted into the habit of +winding up every meal by taking out his false teeth and hurling +them at his wife, which, you will allow, is not an action likely +to occur to the imagination of the average story-teller. Take a +pinch of snuff, Doctor, and acknowledge that I have scored over +you in your example." + +He held out his snuffbox of old gold, with a great amethyst in +the centre of the lid. Its splendour was in such contrast to his +homely ways and simple life that I could not help commenting upon +it. + +"Ah," said he, "I forgot that I had not seen you for some weeks. +It is a little souvenir from the King of Bohemia in return for my +assistance in the case of the Irene Adler papers." + +"And the ring?" I asked, glancing at a remarkable brilliant which +sparkled upon his finger. + +"It was from the reigning family of Holland, though the matter in +which I served them was of such delicacy that I cannot confide it +even to you, who have been good enough to chronicle one or two of +my little problems." + +"And have you any on hand just now?" I asked with interest. + +"Some ten or twelve, but none which present any feature of +interest. They are important, you understand, without being +interesting. Indeed, I have found that it is usually in +unimportant matters that there is a field for the observation, +and for the quick analysis of cause and effect which gives the +charm to an investigation. The larger crimes are apt to be the +simpler, for the bigger the crime the more obvious, as a rule, is +the motive. In these cases, save for one rather intricate matter +which has been referred to me from Marseilles, there is nothing +which presents any features of interest. It is possible, however, +that I may have something better before very many minutes are +over, for this is one of my clients, or I am much mistaken." + +He had risen from his chair and was standing between the parted +blinds gazing down into the dull neutral-tinted London street. +Looking over his shoulder, I saw that on the pavement opposite +there stood a large woman with a heavy fur boa round her neck, +and a large curling red feather in a broad-brimmed hat which was +tilted in a coquettish Duchess of Devonshire fashion over her +ear. From under this great panoply she peeped up in a nervous, +hesitating fashion at our windows, while her body oscillated +backward and forward, and her fingers fidgeted with her glove +buttons. Suddenly, with a plunge, as of the swimmer who leaves +the bank, she hurried across the road, and we heard the sharp +clang of the bell. + +"I have seen those symptoms before," said Holmes, throwing his +cigarette into the fire. "Oscillation upon the pavement always +means an affaire de coeur. She would like advice, but is not sure +that the matter is not too delicate for communication. And yet +even here we may discriminate. When a woman has been seriously +wronged by a man she no longer oscillates, and the usual symptom +is a broken bell wire. Here we may take it that there is a love +matter, but that the maiden is not so much angry as perplexed, or +grieved. But here she comes in person to resolve our doubts." + +As he spoke there was a tap at the door, and the boy in buttons +entered to announce Miss Mary Sutherland, while the lady herself +loomed behind his small black figure like a full-sailed +merchant-man behind a tiny pilot boat. Sherlock Holmes welcomed +her with the easy courtesy for which he was remarkable, and, +having closed the door and bowed her into an armchair, he looked +her over in the minute and yet abstracted fashion which was +peculiar to him. + +"Do you not find," he said, "that with your short sight it is a +little trying to do so much typewriting?" + +"I did at first," she answered, "but now I know where the letters +are without looking." Then, suddenly realising the full purport +of his words, she gave a violent start and looked up, with fear +and astonishment upon her broad, good-humoured face. "You've +heard about me, Mr. Holmes," she cried, "else how could you know +all that?" + +"Never mind," said Holmes, laughing; "it is my business to know +things. Perhaps I have trained myself to see what others +overlook. If not, why should you come to consult me?" + +"I came to you, sir, because I heard of you from Mrs. Etherege, +whose husband you found so easy when the police and everyone had +given him up for dead. Oh, Mr. Holmes, I wish you would do as +much for me. I'm not rich, but still I have a hundred a year in +my own right, besides the little that I make by the machine, and +I would give it all to know what has become of Mr. Hosmer Angel." + +"Why did you come away to consult me in such a hurry?" asked +Sherlock Holmes, with his finger-tips together and his eyes to +the ceiling. + +Again a startled look came over the somewhat vacuous face of Miss +Mary Sutherland. "Yes, I did bang out of the house," she said, +"for it made me angry to see the easy way in which Mr. +Windibank--that is, my father--took it all. He would not go to +the police, and he would not go to you, and so at last, as he +would do nothing and kept on saying that there was no harm done, +it made me mad, and I just on with my things and came right away +to you." + +"Your father," said Holmes, "your stepfather, surely, since the +name is different." + +"Yes, my stepfather. I call him father, though it sounds funny, +too, for he is only five years and two months older than myself." + +"And your mother is alive?" + +"Oh, yes, mother is alive and well. I wasn't best pleased, Mr. +Holmes, when she married again so soon after father's death, and +a man who was nearly fifteen years younger than herself. Father +was a plumber in the Tottenham Court Road, and he left a tidy +business behind him, which mother carried on with Mr. Hardy, the +foreman; but when Mr. Windibank came he made her sell the +business, for he was very superior, being a traveller in wines. +They got 4700 pounds for the goodwill and interest, which wasn't +near as much as father could have got if he had been alive." + +I had expected to see Sherlock Holmes impatient under this +rambling and inconsequential narrative, but, on the contrary, he +had listened with the greatest concentration of attention. + +"Your own little income," he asked, "does it come out of the +business?" + +"Oh, no, sir. It is quite separate and was left me by my uncle +Ned in Auckland. It is in New Zealand stock, paying 4 1/2 per +cent. Two thousand five hundred pounds was the amount, but I can +only touch the interest." + +"You interest me extremely," said Holmes. "And since you draw so +large a sum as a hundred a year, with what you earn into the +bargain, you no doubt travel a little and indulge yourself in +every way. I believe that a single lady can get on very nicely +upon an income of about 60 pounds." + +"I could do with much less than that, Mr. Holmes, but you +understand that as long as I live at home I don't wish to be a +burden to them, and so they have the use of the money just while +I am staying with them. Of course, that is only just for the +time. Mr. Windibank draws my interest every quarter and pays it +over to mother, and I find that I can do pretty well with what I +earn at typewriting. It brings me twopence a sheet, and I can +often do from fifteen to twenty sheets in a day." + +"You have made your position very clear to me," said Holmes. +"This is my friend, Dr. Watson, before whom you can speak as +freely as before myself. Kindly tell us now all about your +connection with Mr. Hosmer Angel." + +A flush stole over Miss Sutherland's face, and she picked +nervously at the fringe of her jacket. "I met him first at the +gasfitters' ball," she said. "They used to send father tickets +when he was alive, and then afterwards they remembered us, and +sent them to mother. Mr. Windibank did not wish us to go. He +never did wish us to go anywhere. He would get quite mad if I +wanted so much as to join a Sunday-school treat. But this time I +was set on going, and I would go; for what right had he to +prevent? He said the folk were not fit for us to know, when all +father's friends were to be there. And he said that I had nothing +fit to wear, when I had my purple plush that I had never so much +as taken out of the drawer. At last, when nothing else would do, +he went off to France upon the business of the firm, but we went, +mother and I, with Mr. Hardy, who used to be our foreman, and it +was there I met Mr. Hosmer Angel." + +"I suppose," said Holmes, "that when Mr. Windibank came back from +France he was very annoyed at your having gone to the ball." + +"Oh, well, he was very good about it. He laughed, I remember, and +shrugged his shoulders, and said there was no use denying +anything to a woman, for she would have her way." + +"I see. Then at the gasfitters' ball you met, as I understand, a +gentleman called Mr. Hosmer Angel." + +"Yes, sir. I met him that night, and he called next day to ask if +we had got home all safe, and after that we met him--that is to +say, Mr. Holmes, I met him twice for walks, but after that father +came back again, and Mr. Hosmer Angel could not come to the house +any more." + +"No?" + +"Well, you know father didn't like anything of the sort. He +wouldn't have any visitors if he could help it, and he used to +say that a woman should be happy in her own family circle. But +then, as I used to say to mother, a woman wants her own circle to +begin with, and I had not got mine yet." + +"But how about Mr. Hosmer Angel? Did he make no attempt to see +you?" + +"Well, father was going off to France again in a week, and Hosmer +wrote and said that it would be safer and better not to see each +other until he had gone. We could write in the meantime, and he +used to write every day. I took the letters in in the morning, so +there was no need for father to know." + +"Were you engaged to the gentleman at this time?" + +"Oh, yes, Mr. Holmes. We were engaged after the first walk that +we took. Hosmer--Mr. Angel--was a cashier in an office in +Leadenhall Street--and--" + +"What office?" + +"That's the worst of it, Mr. Holmes, I don't know." + +"Where did he live, then?" + +"He slept on the premises." + +"And you don't know his address?" + +"No--except that it was Leadenhall Street." + +"Where did you address your letters, then?" + +"To the Leadenhall Street Post Office, to be left till called +for. He said that if they were sent to the office he would be +chaffed by all the other clerks about having letters from a lady, +so I offered to typewrite them, like he did his, but he wouldn't +have that, for he said that when I wrote them they seemed to come +from me, but when they were typewritten he always felt that the +machine had come between us. That will just show you how fond he +was of me, Mr. Holmes, and the little things that he would think +of." + +"It was most suggestive," said Holmes. "It has long been an axiom +of mine that the little things are infinitely the most important. +Can you remember any other little things about Mr. Hosmer Angel?" + +"He was a very shy man, Mr. Holmes. He would rather walk with me +in the evening than in the daylight, for he said that he hated to +be conspicuous. Very retiring and gentlemanly he was. Even his +voice was gentle. He'd had the quinsy and swollen glands when he +was young, he told me, and it had left him with a weak throat, +and a hesitating, whispering fashion of speech. He was always +well dressed, very neat and plain, but his eyes were weak, just +as mine are, and he wore tinted glasses against the glare." + +"Well, and what happened when Mr. Windibank, your stepfather, +returned to France?" + +"Mr. Hosmer Angel came to the house again and proposed that we +should marry before father came back. He was in dreadful earnest +and made me swear, with my hands on the Testament, that whatever +happened I would always be true to him. Mother said he was quite +right to make me swear, and that it was a sign of his passion. +Mother was all in his favour from the first and was even fonder +of him than I was. Then, when they talked of marrying within the +week, I began to ask about father; but they both said never to +mind about father, but just to tell him afterwards, and mother +said she would make it all right with him. I didn't quite like +that, Mr. Holmes. It seemed funny that I should ask his leave, as +he was only a few years older than me; but I didn't want to do +anything on the sly, so I wrote to father at Bordeaux, where the +company has its French offices, but the letter came back to me on +the very morning of the wedding." + +"It missed him, then?" + +"Yes, sir; for he had started to England just before it arrived." + +"Ha! that was unfortunate. Your wedding was arranged, then, for +the Friday. Was it to be in church?" + +"Yes, sir, but very quietly. It was to be at St. Saviour's, near +King's Cross, and we were to have breakfast afterwards at the St. +Pancras Hotel. Hosmer came for us in a hansom, but as there were +two of us he put us both into it and stepped himself into a +four-wheeler, which happened to be the only other cab in the +street. We got to the church first, and when the four-wheeler +drove up we waited for him to step out, but he never did, and +when the cabman got down from the box and looked there was no one +there! The cabman said that he could not imagine what had become +of him, for he had seen him get in with his own eyes. That was +last Friday, Mr. Holmes, and I have never seen or heard anything +since then to throw any light upon what became of him." + +"It seems to me that you have been very shamefully treated," said +Holmes. + +"Oh, no, sir! He was too good and kind to leave me so. Why, all +the morning he was saying to me that, whatever happened, I was to +be true; and that even if something quite unforeseen occurred to +separate us, I was always to remember that I was pledged to him, +and that he would claim his pledge sooner or later. It seemed +strange talk for a wedding-morning, but what has happened since +gives a meaning to it." + +"Most certainly it does. Your own opinion is, then, that some +unforeseen catastrophe has occurred to him?" + +"Yes, sir. I believe that he foresaw some danger, or else he +would not have talked so. And then I think that what he foresaw +happened." + +"But you have no notion as to what it could have been?" + +"None." + +"One more question. How did your mother take the matter?" + +"She was angry, and said that I was never to speak of the matter +again." + +"And your father? Did you tell him?" + +"Yes; and he seemed to think, with me, that something had +happened, and that I should hear of Hosmer again. As he said, +what interest could anyone have in bringing me to the doors of +the church, and then leaving me? Now, if he had borrowed my +money, or if he had married me and got my money settled on him, +there might be some reason, but Hosmer was very independent about +money and never would look at a shilling of mine. And yet, what +could have happened? And why could he not write? Oh, it drives me +half-mad to think of it, and I can't sleep a wink at night." She +pulled a little handkerchief out of her muff and began to sob +heavily into it. + +"I shall glance into the case for you," said Holmes, rising, "and +I have no doubt that we shall reach some definite result. Let the +weight of the matter rest upon me now, and do not let your mind +dwell upon it further. Above all, try to let Mr. Hosmer Angel +vanish from your memory, as he has done from your life." + +"Then you don't think I'll see him again?" + +"I fear not." + +"Then what has happened to him?" + +"You will leave that question in my hands. I should like an +accurate description of him and any letters of his which you can +spare." + +"I advertised for him in last Saturday's Chronicle," said she. +"Here is the slip and here are four letters from him." + +"Thank you. And your address?" + +"No. 31 Lyon Place, Camberwell." + +"Mr. Angel's address you never had, I understand. Where is your +father's place of business?" + +"He travels for Westhouse & Marbank, the great claret importers +of Fenchurch Street." + +"Thank you. You have made your statement very clearly. You will +leave the papers here, and remember the advice which I have given +you. Let the whole incident be a sealed book, and do not allow it +to affect your life." + +"You are very kind, Mr. Holmes, but I cannot do that. I shall be +true to Hosmer. He shall find me ready when he comes back." + +For all the preposterous hat and the vacuous face, there was +something noble in the simple faith of our visitor which +compelled our respect. She laid her little bundle of papers upon +the table and went her way, with a promise to come again whenever +she might be summoned. + +Sherlock Holmes sat silent for a few minutes with his fingertips +still pressed together, his legs stretched out in front of him, +and his gaze directed upward to the ceiling. Then he took down +from the rack the old and oily clay pipe, which was to him as a +counsellor, and, having lit it, he leaned back in his chair, with +the thick blue cloud-wreaths spinning up from him, and a look of +infinite languor in his face. + +"Quite an interesting study, that maiden," he observed. "I found +her more interesting than her little problem, which, by the way, +is rather a trite one. You will find parallel cases, if you +consult my index, in Andover in '77, and there was something of +the sort at The Hague last year. Old as is the idea, however, +there were one or two details which were new to me. But the +maiden herself was most instructive." + +"You appeared to read a good deal upon her which was quite +invisible to me," I remarked. + +"Not invisible but unnoticed, Watson. You did not know where to +look, and so you missed all that was important. I can never bring +you to realise the importance of sleeves, the suggestiveness of +thumb-nails, or the great issues that may hang from a boot-lace. +Now, what did you gather from that woman's appearance? Describe +it." + +"Well, she had a slate-coloured, broad-brimmed straw hat, with a +feather of a brickish red. Her jacket was black, with black beads +sewn upon it, and a fringe of little black jet ornaments. Her +dress was brown, rather darker than coffee colour, with a little +purple plush at the neck and sleeves. Her gloves were greyish and +were worn through at the right forefinger. Her boots I didn't +observe. She had small round, hanging gold earrings, and a +general air of being fairly well-to-do in a vulgar, comfortable, +easy-going way." + +Sherlock Holmes clapped his hands softly together and chuckled. + +"'Pon my word, Watson, you are coming along wonderfully. You have +really done very well indeed. It is true that you have missed +everything of importance, but you have hit upon the method, and +you have a quick eye for colour. Never trust to general +impressions, my boy, but concentrate yourself upon details. My +first glance is always at a woman's sleeve. In a man it is +perhaps better first to take the knee of the trouser. As you +observe, this woman had plush upon her sleeves, which is a most +useful material for showing traces. The double line a little +above the wrist, where the typewritist presses against the table, +was beautifully defined. The sewing-machine, of the hand type, +leaves a similar mark, but only on the left arm, and on the side +of it farthest from the thumb, instead of being right across the +broadest part, as this was. I then glanced at her face, and, +observing the dint of a pince-nez at either side of her nose, I +ventured a remark upon short sight and typewriting, which seemed +to surprise her." + +"It surprised me." + +"But, surely, it was obvious. I was then much surprised and +interested on glancing down to observe that, though the boots +which she was wearing were not unlike each other, they were +really odd ones; the one having a slightly decorated toe-cap, and +the other a plain one. One was buttoned only in the two lower +buttons out of five, and the other at the first, third, and +fifth. Now, when you see that a young lady, otherwise neatly +dressed, has come away from home with odd boots, half-buttoned, +it is no great deduction to say that she came away in a hurry." + +"And what else?" I asked, keenly interested, as I always was, by +my friend's incisive reasoning. + +"I noted, in passing, that she had written a note before leaving +home but after being fully dressed. You observed that her right +glove was torn at the forefinger, but you did not apparently see +that both glove and finger were stained with violet ink. She had +written in a hurry and dipped her pen too deep. It must have been +this morning, or the mark would not remain clear upon the finger. +All this is amusing, though rather elementary, but I must go back +to business, Watson. Would you mind reading me the advertised +description of Mr. Hosmer Angel?" + +I held the little printed slip to the light. + +"Missing," it said, "on the morning of the fourteenth, a gentleman +named Hosmer Angel. About five ft. seven in. in height; +strongly built, sallow complexion, black hair, a little bald in +the centre, bushy, black side-whiskers and moustache; tinted +glasses, slight infirmity of speech. Was dressed, when last seen, +in black frock-coat faced with silk, black waistcoat, gold Albert +chain, and grey Harris tweed trousers, with brown gaiters over +elastic-sided boots. Known to have been employed in an office in +Leadenhall Street. Anybody bringing--" + +"That will do," said Holmes. "As to the letters," he continued, +glancing over them, "they are very commonplace. Absolutely no +clue in them to Mr. Angel, save that he quotes Balzac once. There +is one remarkable point, however, which will no doubt strike +you." + +"They are typewritten," I remarked. + +"Not only that, but the signature is typewritten. Look at the +neat little 'Hosmer Angel' at the bottom. There is a date, you +see, but no superscription except Leadenhall Street, which is +rather vague. The point about the signature is very suggestive--in +fact, we may call it conclusive." + +"Of what?" + +"My dear fellow, is it possible you do not see how strongly it +bears upon the case?" + +"I cannot say that I do unless it were that he wished to be able +to deny his signature if an action for breach of promise were +instituted." + +"No, that was not the point. However, I shall write two letters, +which should settle the matter. One is to a firm in the City, the +other is to the young lady's stepfather, Mr. Windibank, asking +him whether he could meet us here at six o'clock tomorrow +evening. It is just as well that we should do business with the +male relatives. And now, Doctor, we can do nothing until the +answers to those letters come, so we may put our little problem +upon the shelf for the interim." + +I had had so many reasons to believe in my friend's subtle powers +of reasoning and extraordinary energy in action that I felt that +he must have some solid grounds for the assured and easy +demeanour with which he treated the singular mystery which he had +been called upon to fathom. Once only had I known him to fail, in +the case of the King of Bohemia and of the Irene Adler +photograph; but when I looked back to the weird business of the +Sign of Four, and the extraordinary circumstances connected with +the Study in Scarlet, I felt that it would be a strange tangle +indeed which he could not unravel. + +I left him then, still puffing at his black clay pipe, with the +conviction that when I came again on the next evening I would +find that he held in his hands all the clues which would lead up +to the identity of the disappearing bridegroom of Miss Mary +Sutherland. + +A professional case of great gravity was engaging my own +attention at the time, and the whole of next day I was busy at +the bedside of the sufferer. It was not until close upon six +o'clock that I found myself free and was able to spring into a +hansom and drive to Baker Street, half afraid that I might be too +late to assist at the dnouement of the little mystery. I found +Sherlock Holmes alone, however, half asleep, with his long, thin +form curled up in the recesses of his armchair. A formidable +array of bottles and test-tubes, with the pungent cleanly smell +of hydrochloric acid, told me that he had spent his day in the +chemical work which was so dear to him. + +"Well, have you solved it?" I asked as I entered. + +"Yes. It was the bisulphate of baryta." + +"No, no, the mystery!" I cried. + +"Oh, that! I thought of the salt that I have been working upon. +There was never any mystery in the matter, though, as I said +yesterday, some of the details are of interest. The only drawback +is that there is no law, I fear, that can touch the scoundrel." + +"Who was he, then, and what was his object in deserting Miss +Sutherland?" + +The question was hardly out of my mouth, and Holmes had not yet +opened his lips to reply, when we heard a heavy footfall in the +passage and a tap at the door. + +"This is the girl's stepfather, Mr. James Windibank," said +Holmes. "He has written to me to say that he would be here at +six. Come in!" + +The man who entered was a sturdy, middle-sized fellow, some +thirty years of age, clean-shaven, and sallow-skinned, with a +bland, insinuating manner, and a pair of wonderfully sharp and +penetrating grey eyes. He shot a questioning glance at each of +us, placed his shiny top-hat upon the sideboard, and with a +slight bow sidled down into the nearest chair. + +"Good-evening, Mr. James Windibank," said Holmes. "I think that +this typewritten letter is from you, in which you made an +appointment with me for six o'clock?" + +"Yes, sir. I am afraid that I am a little late, but I am not +quite my own master, you know. I am sorry that Miss Sutherland +has troubled you about this little matter, for I think it is far +better not to wash linen of the sort in public. It was quite +against my wishes that she came, but she is a very excitable, +impulsive girl, as you may have noticed, and she is not easily +controlled when she has made up her mind on a point. Of course, I +did not mind you so much, as you are not connected with the +official police, but it is not pleasant to have a family +misfortune like this noised abroad. Besides, it is a useless +expense, for how could you possibly find this Hosmer Angel?" + +"On the contrary," said Holmes quietly; "I have every reason to +believe that I will succeed in discovering Mr. Hosmer Angel." + +Mr. Windibank gave a violent start and dropped his gloves. "I am +delighted to hear it," he said. + +"It is a curious thing," remarked Holmes, "that a typewriter has +really quite as much individuality as a man's handwriting. Unless +they are quite new, no two of them write exactly alike. Some +letters get more worn than others, and some wear only on one +side. Now, you remark in this note of yours, Mr. Windibank, that +in every case there is some little slurring over of the 'e,' and +a slight defect in the tail of the 'r.' There are fourteen other +characteristics, but those are the more obvious." + +"We do all our correspondence with this machine at the office, +and no doubt it is a little worn," our visitor answered, glancing +keenly at Holmes with his bright little eyes. + +"And now I will show you what is really a very interesting study, +Mr. Windibank," Holmes continued. "I think of writing another +little monograph some of these days on the typewriter and its +relation to crime. It is a subject to which I have devoted some +little attention. I have here four letters which purport to come +from the missing man. They are all typewritten. In each case, not +only are the 'e's' slurred and the 'r's' tailless, but you will +observe, if you care to use my magnifying lens, that the fourteen +other characteristics to which I have alluded are there as well." + +Mr. Windibank sprang out of his chair and picked up his hat. "I +cannot waste time over this sort of fantastic talk, Mr. Holmes," +he said. "If you can catch the man, catch him, and let me know +when you have done it." + +"Certainly," said Holmes, stepping over and turning the key in +the door. "I let you know, then, that I have caught him!" + +"What! where?" shouted Mr. Windibank, turning white to his lips +and glancing about him like a rat in a trap. + +"Oh, it won't do--really it won't," said Holmes suavely. "There +is no possible getting out of it, Mr. Windibank. It is quite too +transparent, and it was a very bad compliment when you said that +it was impossible for me to solve so simple a question. That's +right! Sit down and let us talk it over." + +Our visitor collapsed into a chair, with a ghastly face and a +glitter of moisture on his brow. "It--it's not actionable," he +stammered. + +"I am very much afraid that it is not. But between ourselves, +Windibank, it was as cruel and selfish and heartless a trick in a +petty way as ever came before me. Now, let me just run over the +course of events, and you will contradict me if I go wrong." + +The man sat huddled up in his chair, with his head sunk upon his +breast, like one who is utterly crushed. Holmes stuck his feet up +on the corner of the mantelpiece and, leaning back with his hands +in his pockets, began talking, rather to himself, as it seemed, +than to us. + +"The man married a woman very much older than himself for her +money," said he, "and he enjoyed the use of the money of the +daughter as long as she lived with them. It was a considerable +sum, for people in their position, and the loss of it would have +made a serious difference. It was worth an effort to preserve it. +The daughter was of a good, amiable disposition, but affectionate +and warm-hearted in her ways, so that it was evident that with +her fair personal advantages, and her little income, she would +not be allowed to remain single long. Now her marriage would +mean, of course, the loss of a hundred a year, so what does her +stepfather do to prevent it? He takes the obvious course of +keeping her at home and forbidding her to seek the company of +people of her own age. But soon he found that that would not +answer forever. She became restive, insisted upon her rights, and +finally announced her positive intention of going to a certain +ball. What does her clever stepfather do then? He conceives an +idea more creditable to his head than to his heart. With the +connivance and assistance of his wife he disguised himself, +covered those keen eyes with tinted glasses, masked the face with +a moustache and a pair of bushy whiskers, sunk that clear voice +into an insinuating whisper, and doubly secure on account of the +girl's short sight, he appears as Mr. Hosmer Angel, and keeps off +other lovers by making love himself." + +"It was only a joke at first," groaned our visitor. "We never +thought that she would have been so carried away." + +"Very likely not. However that may be, the young lady was very +decidedly carried away, and, having quite made up her mind that +her stepfather was in France, the suspicion of treachery never +for an instant entered her mind. She was flattered by the +gentleman's attentions, and the effect was increased by the +loudly expressed admiration of her mother. Then Mr. Angel began +to call, for it was obvious that the matter should be pushed as +far as it would go if a real effect were to be produced. There +were meetings, and an engagement, which would finally secure the +girl's affections from turning towards anyone else. But the +deception could not be kept up forever. These pretended journeys +to France were rather cumbrous. The thing to do was clearly to +bring the business to an end in such a dramatic manner that it +would leave a permanent impression upon the young lady's mind and +prevent her from looking upon any other suitor for some time to +come. Hence those vows of fidelity exacted upon a Testament, and +hence also the allusions to a possibility of something happening +on the very morning of the wedding. James Windibank wished Miss +Sutherland to be so bound to Hosmer Angel, and so uncertain as to +his fate, that for ten years to come, at any rate, she would not +listen to another man. As far as the church door he brought her, +and then, as he could go no farther, he conveniently vanished +away by the old trick of stepping in at one door of a +four-wheeler and out at the other. I think that was the chain of +events, Mr. Windibank!" + +Our visitor had recovered something of his assurance while Holmes +had been talking, and he rose from his chair now with a cold +sneer upon his pale face. + +"It may be so, or it may not, Mr. Holmes," said he, "but if you +are so very sharp you ought to be sharp enough to know that it is +you who are breaking the law now, and not me. I have done nothing +actionable from the first, but as long as you keep that door +locked you lay yourself open to an action for assault and illegal +constraint." + +"The law cannot, as you say, touch you," said Holmes, unlocking +and throwing open the door, "yet there never was a man who +deserved punishment more. If the young lady has a brother or a +friend, he ought to lay a whip across your shoulders. By Jove!" +he continued, flushing up at the sight of the bitter sneer upon +the man's face, "it is not part of my duties to my client, but +here's a hunting crop handy, and I think I shall just treat +myself to--" He took two swift steps to the whip, but before he +could grasp it there was a wild clatter of steps upon the stairs, +the heavy hall door banged, and from the window we could see Mr. +James Windibank running at the top of his speed down the road. + +"There's a cold-blooded scoundrel!" said Holmes, laughing, as he +threw himself down into his chair once more. "That fellow will +rise from crime to crime until he does something very bad, and +ends on a gallows. The case has, in some respects, been not +entirely devoid of interest." + +"I cannot now entirely see all the steps of your reasoning," I +remarked. + +"Well, of course it was obvious from the first that this Mr. +Hosmer Angel must have some strong object for his curious +conduct, and it was equally clear that the only man who really +profited by the incident, as far as we could see, was the +stepfather. Then the fact that the two men were never together, +but that the one always appeared when the other was away, was +suggestive. So were the tinted spectacles and the curious voice, +which both hinted at a disguise, as did the bushy whiskers. My +suspicions were all confirmed by his peculiar action in +typewriting his signature, which, of course, inferred that his +handwriting was so familiar to her that she would recognise even +the smallest sample of it. You see all these isolated facts, +together with many minor ones, all pointed in the same +direction." + +"And how did you verify them?" + +"Having once spotted my man, it was easy to get corroboration. I +knew the firm for which this man worked. Having taken the printed +description. I eliminated everything from it which could be the +result of a disguise--the whiskers, the glasses, the voice, and I +sent it to the firm, with a request that they would inform me +whether it answered to the description of any of their +travellers. I had already noticed the peculiarities of the +typewriter, and I wrote to the man himself at his business +address asking him if he would come here. As I expected, his +reply was typewritten and revealed the same trivial but +characteristic defects. The same post brought me a letter from +Westhouse & Marbank, of Fenchurch Street, to say that the +description tallied in every respect with that of their employ, +James Windibank. Voil tout!" + +"And Miss Sutherland?" + +"If I tell her she will not believe me. You may remember the old +Persian saying, 'There is danger for him who taketh the tiger +cub, and danger also for whoso snatches a delusion from a woman.' +There is as much sense in Hafiz as in Horace, and as much +knowledge of the world." + + + +ADVENTURE IV. THE BOSCOMBE VALLEY MYSTERY + +We were seated at breakfast one morning, my wife and I, when the +maid brought in a telegram. It was from Sherlock Holmes and ran +in this way: + +"Have you a couple of days to spare? Have just been wired for from +the west of England in connection with Boscombe Valley tragedy. +Shall be glad if you will come with me. Air and scenery perfect. +Leave Paddington by the 11:15." + +"What do you say, dear?" said my wife, looking across at me. +"Will you go?" + +"I really don't know what to say. I have a fairly long list at +present." + +"Oh, Anstruther would do your work for you. You have been looking +a little pale lately. I think that the change would do you good, +and you are always so interested in Mr. Sherlock Holmes' cases." + +"I should be ungrateful if I were not, seeing what I gained +through one of them," I answered. "But if I am to go, I must pack +at once, for I have only half an hour." + +My experience of camp life in Afghanistan had at least had the +effect of making me a prompt and ready traveller. My wants were +few and simple, so that in less than the time stated I was in a +cab with my valise, rattling away to Paddington Station. Sherlock +Holmes was pacing up and down the platform, his tall, gaunt +figure made even gaunter and taller by his long grey +travelling-cloak and close-fitting cloth cap. + +"It is really very good of you to come, Watson," said he. "It +makes a considerable difference to me, having someone with me on +whom I can thoroughly rely. Local aid is always either worthless +or else biassed. If you will keep the two corner seats I shall +get the tickets." + +We had the carriage to ourselves save for an immense litter of +papers which Holmes had brought with him. Among these he rummaged +and read, with intervals of note-taking and of meditation, until +we were past Reading. Then he suddenly rolled them all into a +gigantic ball and tossed them up onto the rack. + +"Have you heard anything of the case?" he asked. + +"Not a word. I have not seen a paper for some days." + +"The London press has not had very full accounts. I have just +been looking through all the recent papers in order to master the +particulars. It seems, from what I gather, to be one of those +simple cases which are so extremely difficult." + +"That sounds a little paradoxical." + +"But it is profoundly true. Singularity is almost invariably a +clue. The more featureless and commonplace a crime is, the more +difficult it is to bring it home. In this case, however, they +have established a very serious case against the son of the +murdered man." + +"It is a murder, then?" + +"Well, it is conjectured to be so. I shall take nothing for +granted until I have the opportunity of looking personally into +it. I will explain the state of things to you, as far as I have +been able to understand it, in a very few words. + +"Boscombe Valley is a country district not very far from Ross, in +Herefordshire. The largest landed proprietor in that part is a +Mr. John Turner, who made his money in Australia and returned +some years ago to the old country. One of the farms which he +held, that of Hatherley, was let to Mr. Charles McCarthy, who was +also an ex-Australian. The men had known each other in the +colonies, so that it was not unnatural that when they came to +settle down they should do so as near each other as possible. +Turner was apparently the richer man, so McCarthy became his +tenant but still remained, it seems, upon terms of perfect +equality, as they were frequently together. McCarthy had one son, +a lad of eighteen, and Turner had an only daughter of the same +age, but neither of them had wives living. They appear to have +avoided the society of the neighbouring English families and to +have led retired lives, though both the McCarthys were fond of +sport and were frequently seen at the race-meetings of the +neighbourhood. McCarthy kept two servants--a man and a girl. +Turner had a considerable household, some half-dozen at the +least. That is as much as I have been able to gather about the +families. Now for the facts. + +"On June 3rd, that is, on Monday last, McCarthy left his house at +Hatherley about three in the afternoon and walked down to the +Boscombe Pool, which is a small lake formed by the spreading out +of the stream which runs down the Boscombe Valley. He had been +out with his serving-man in the morning at Ross, and he had told +the man that he must hurry, as he had an appointment of +importance to keep at three. From that appointment he never came +back alive. + +"From Hatherley Farm-house to the Boscombe Pool is a quarter of a +mile, and two people saw him as he passed over this ground. One +was an old woman, whose name is not mentioned, and the other was +William Crowder, a game-keeper in the employ of Mr. Turner. Both +these witnesses depose that Mr. McCarthy was walking alone. The +game-keeper adds that within a few minutes of his seeing Mr. +McCarthy pass he had seen his son, Mr. James McCarthy, going the +same way with a gun under his arm. To the best of his belief, the +father was actually in sight at the time, and the son was +following him. He thought no more of the matter until he heard in +the evening of the tragedy that had occurred. + +"The two McCarthys were seen after the time when William Crowder, +the game-keeper, lost sight of them. The Boscombe Pool is thickly +wooded round, with just a fringe of grass and of reeds round the +edge. A girl of fourteen, Patience Moran, who is the daughter of +the lodge-keeper of the Boscombe Valley estate, was in one of the +woods picking flowers. She states that while she was there she +saw, at the border of the wood and close by the lake, Mr. +McCarthy and his son, and that they appeared to be having a +violent quarrel. She heard Mr. McCarthy the elder using very +strong language to his son, and she saw the latter raise up his +hand as if to strike his father. She was so frightened by their +violence that she ran away and told her mother when she reached +home that she had left the two McCarthys quarrelling near +Boscombe Pool, and that she was afraid that they were going to +fight. She had hardly said the words when young Mr. McCarthy came +running up to the lodge to say that he had found his father dead +in the wood, and to ask for the help of the lodge-keeper. He was +much excited, without either his gun or his hat, and his right +hand and sleeve were observed to be stained with fresh blood. On +following him they found the dead body stretched out upon the +grass beside the pool. The head had been beaten in by repeated +blows of some heavy and blunt weapon. The injuries were such as +might very well have been inflicted by the butt-end of his son's +gun, which was found lying on the grass within a few paces of the +body. Under these circumstances the young man was instantly +arrested, and a verdict of 'wilful murder' having been returned +at the inquest on Tuesday, he was on Wednesday brought before the +magistrates at Ross, who have referred the case to the next +Assizes. Those are the main facts of the case as they came out +before the coroner and the police-court." + +"I could hardly imagine a more damning case," I remarked. "If +ever circumstantial evidence pointed to a criminal it does so +here." + +"Circumstantial evidence is a very tricky thing," answered Holmes +thoughtfully. "It may seem to point very straight to one thing, +but if you shift your own point of view a little, you may find it +pointing in an equally uncompromising manner to something +entirely different. It must be confessed, however, that the case +looks exceedingly grave against the young man, and it is very +possible that he is indeed the culprit. There are several people +in the neighbourhood, however, and among them Miss Turner, the +daughter of the neighbouring landowner, who believe in his +innocence, and who have retained Lestrade, whom you may recollect +in connection with the Study in Scarlet, to work out the case in +his interest. Lestrade, being rather puzzled, has referred the +case to me, and hence it is that two middle-aged gentlemen are +flying westward at fifty miles an hour instead of quietly +digesting their breakfasts at home." + +"I am afraid," said I, "that the facts are so obvious that you +will find little credit to be gained out of this case." + +"There is nothing more deceptive than an obvious fact," he +answered, laughing. "Besides, we may chance to hit upon some +other obvious facts which may have been by no means obvious to +Mr. Lestrade. You know me too well to think that I am boasting +when I say that I shall either confirm or destroy his theory by +means which he is quite incapable of employing, or even of +understanding. To take the first example to hand, I very clearly +perceive that in your bedroom the window is upon the right-hand +side, and yet I question whether Mr. Lestrade would have noted +even so self-evident a thing as that." + +"How on earth--" + +"My dear fellow, I know you well. I know the military neatness +which characterises you. You shave every morning, and in this +season you shave by the sunlight; but since your shaving is less +and less complete as we get farther back on the left side, until +it becomes positively slovenly as we get round the angle of the +jaw, it is surely very clear that that side is less illuminated +than the other. I could not imagine a man of your habits looking +at himself in an equal light and being satisfied with such a +result. I only quote this as a trivial example of observation and +inference. Therein lies my mtier, and it is just possible that +it may be of some service in the investigation which lies before +us. There are one or two minor points which were brought out in +the inquest, and which are worth considering." + +"What are they?" + +"It appears that his arrest did not take place at once, but after +the return to Hatherley Farm. On the inspector of constabulary +informing him that he was a prisoner, he remarked that he was not +surprised to hear it, and that it was no more than his deserts. +This observation of his had the natural effect of removing any +traces of doubt which might have remained in the minds of the +coroner's jury." + +"It was a confession," I ejaculated. + +"No, for it was followed by a protestation of innocence." + +"Coming on the top of such a damning series of events, it was at +least a most suspicious remark." + +"On the contrary," said Holmes, "it is the brightest rift which I +can at present see in the clouds. However innocent he might be, +he could not be such an absolute imbecile as not to see that the +circumstances were very black against him. Had he appeared +surprised at his own arrest, or feigned indignation at it, I +should have looked upon it as highly suspicious, because such +surprise or anger would not be natural under the circumstances, +and yet might appear to be the best policy to a scheming man. His +frank acceptance of the situation marks him as either an innocent +man, or else as a man of considerable self-restraint and +firmness. As to his remark about his deserts, it was also not +unnatural if you consider that he stood beside the dead body of +his father, and that there is no doubt that he had that very day +so far forgotten his filial duty as to bandy words with him, and +even, according to the little girl whose evidence is so +important, to raise his hand as if to strike him. The +self-reproach and contrition which are displayed in his remark +appear to me to be the signs of a healthy mind rather than of a +guilty one." + +I shook my head. "Many men have been hanged on far slighter +evidence," I remarked. + +"So they have. And many men have been wrongfully hanged." + +"What is the young man's own account of the matter?" + +"It is, I am afraid, not very encouraging to his supporters, +though there are one or two points in it which are suggestive. +You will find it here, and may read it for yourself." + +He picked out from his bundle a copy of the local Herefordshire +paper, and having turned down the sheet he pointed out the +paragraph in which the unfortunate young man had given his own +statement of what had occurred. I settled myself down in the +corner of the carriage and read it very carefully. It ran in this +way: + +"Mr. James McCarthy, the only son of the deceased, was then called +and gave evidence as follows: 'I had been away from home for +three days at Bristol, and had only just returned upon the +morning of last Monday, the 3rd. My father was absent from home at +the time of my arrival, and I was informed by the maid that he +had driven over to Ross with John Cobb, the groom. Shortly after +my return I heard the wheels of his trap in the yard, and, +looking out of my window, I saw him get out and walk rapidly out +of the yard, though I was not aware in which direction he was +going. I then took my gun and strolled out in the direction of +the Boscombe Pool, with the intention of visiting the rabbit +warren which is upon the other side. On my way I saw William +Crowder, the game-keeper, as he had stated in his evidence; but +he is mistaken in thinking that I was following my father. I had +no idea that he was in front of me. When about a hundred yards +from the pool I heard a cry of "Cooee!" which was a usual signal +between my father and myself. I then hurried forward, and found +him standing by the pool. He appeared to be much surprised at +seeing me and asked me rather roughly what I was doing there. A +conversation ensued which led to high words and almost to blows, +for my father was a man of a very violent temper. Seeing that his +passion was becoming ungovernable, I left him and returned +towards Hatherley Farm. I had not gone more than 150 yards, +however, when I heard a hideous outcry behind me, which caused me +to run back again. I found my father expiring upon the ground, +with his head terribly injured. I dropped my gun and held him in +my arms, but he almost instantly expired. I knelt beside him for +some minutes, and then made my way to Mr. Turner's lodge-keeper, +his house being the nearest, to ask for assistance. I saw no one +near my father when I returned, and I have no idea how he came by +his injuries. He was not a popular man, being somewhat cold and +forbidding in his manners, but he had, as far as I know, no +active enemies. I know nothing further of the matter.' + +"The Coroner: Did your father make any statement to you before +he died? + +"Witness: He mumbled a few words, but I could only catch some +allusion to a rat. + +"The Coroner: What did you understand by that? + +"Witness: It conveyed no meaning to me. I thought that he was +delirious. + +"The Coroner: What was the point upon which you and your father +had this final quarrel? + +"Witness: I should prefer not to answer. + +"The Coroner: I am afraid that I must press it. + +"Witness: It is really impossible for me to tell you. I can +assure you that it has nothing to do with the sad tragedy which +followed. + +"The Coroner: That is for the court to decide. I need not point +out to you that your refusal to answer will prejudice your case +considerably in any future proceedings which may arise. + +"Witness: I must still refuse. + +"The Coroner: I understand that the cry of 'Cooee' was a common +signal between you and your father? + +"Witness: It was. + +"The Coroner: How was it, then, that he uttered it before he saw +you, and before he even knew that you had returned from Bristol? + +"Witness (with considerable confusion): I do not know. + +"A Juryman: Did you see nothing which aroused your suspicions +when you returned on hearing the cry and found your father +fatally injured? + +"Witness: Nothing definite. + +"The Coroner: What do you mean? + +"Witness: I was so disturbed and excited as I rushed out into +the open, that I could think of nothing except of my father. Yet +I have a vague impression that as I ran forward something lay +upon the ground to the left of me. It seemed to me to be +something grey in colour, a coat of some sort, or a plaid perhaps. +When I rose from my father I looked round for it, but it was +gone. + +"'Do you mean that it disappeared before you went for help?' + +"'Yes, it was gone.' + +"'You cannot say what it was?' + +"'No, I had a feeling something was there.' + +"'How far from the body?' + +"'A dozen yards or so.' + +"'And how far from the edge of the wood?' + +"'About the same.' + +"'Then if it was removed it was while you were within a dozen +yards of it?' + +"'Yes, but with my back towards it.' + +"This concluded the examination of the witness." + +"I see," said I as I glanced down the column, "that the coroner +in his concluding remarks was rather severe upon young McCarthy. +He calls attention, and with reason, to the discrepancy about his +father having signalled to him before seeing him, also to his +refusal to give details of his conversation with his father, and +his singular account of his father's dying words. They are all, +as he remarks, very much against the son." + +Holmes laughed softly to himself and stretched himself out upon +the cushioned seat. "Both you and the coroner have been at some +pains," said he, "to single out the very strongest points in the +young man's favour. Don't you see that you alternately give him +credit for having too much imagination and too little? Too +little, if he could not invent a cause of quarrel which would +give him the sympathy of the jury; too much, if he evolved from +his own inner consciousness anything so outr as a dying +reference to a rat, and the incident of the vanishing cloth. No, +sir, I shall approach this case from the point of view that what +this young man says is true, and we shall see whither that +hypothesis will lead us. And now here is my pocket Petrarch, and +not another word shall I say of this case until we are on the +scene of action. We lunch at Swindon, and I see that we shall be +there in twenty minutes." + +It was nearly four o'clock when we at last, after passing through +the beautiful Stroud Valley, and over the broad gleaming Severn, +found ourselves at the pretty little country-town of Ross. A +lean, ferret-like man, furtive and sly-looking, was waiting for +us upon the platform. In spite of the light brown dustcoat and +leather-leggings which he wore in deference to his rustic +surroundings, I had no difficulty in recognising Lestrade, of +Scotland Yard. With him we drove to the Hereford Arms where a +room had already been engaged for us. + +"I have ordered a carriage," said Lestrade as we sat over a cup +of tea. "I knew your energetic nature, and that you would not be +happy until you had been on the scene of the crime." + +"It was very nice and complimentary of you," Holmes answered. "It +is entirely a question of barometric pressure." + +Lestrade looked startled. "I do not quite follow," he said. + +"How is the glass? Twenty-nine, I see. No wind, and not a cloud +in the sky. I have a caseful of cigarettes here which need +smoking, and the sofa is very much superior to the usual country +hotel abomination. I do not think that it is probable that I +shall use the carriage to-night." + +Lestrade laughed indulgently. "You have, no doubt, already formed +your conclusions from the newspapers," he said. "The case is as +plain as a pikestaff, and the more one goes into it the plainer +it becomes. Still, of course, one can't refuse a lady, and such a +very positive one, too. She has heard of you, and would have your +opinion, though I repeatedly told her that there was nothing +which you could do which I had not already done. Why, bless my +soul! here is her carriage at the door." + +He had hardly spoken before there rushed into the room one of the +most lovely young women that I have ever seen in my life. Her +violet eyes shining, her lips parted, a pink flush upon her +cheeks, all thought of her natural reserve lost in her +overpowering excitement and concern. + +"Oh, Mr. Sherlock Holmes!" she cried, glancing from one to the +other of us, and finally, with a woman's quick intuition, +fastening upon my companion, "I am so glad that you have come. I +have driven down to tell you so. I know that James didn't do it. +I know it, and I want you to start upon your work knowing it, +too. Never let yourself doubt upon that point. We have known each +other since we were little children, and I know his faults as no +one else does; but he is too tender-hearted to hurt a fly. Such a +charge is absurd to anyone who really knows him." + +"I hope we may clear him, Miss Turner," said Sherlock Holmes. +"You may rely upon my doing all that I can." + +"But you have read the evidence. You have formed some conclusion? +Do you not see some loophole, some flaw? Do you not yourself +think that he is innocent?" + +"I think that it is very probable." + +"There, now!" she cried, throwing back her head and looking +defiantly at Lestrade. "You hear! He gives me hopes." + +Lestrade shrugged his shoulders. "I am afraid that my colleague +has been a little quick in forming his conclusions," he said. + +"But he is right. Oh! I know that he is right. James never did +it. And about his quarrel with his father, I am sure that the +reason why he would not speak about it to the coroner was because +I was concerned in it." + +"In what way?" asked Holmes. + +"It is no time for me to hide anything. James and his father had +many disagreements about me. Mr. McCarthy was very anxious that +there should be a marriage between us. James and I have always +loved each other as brother and sister; but of course he is young +and has seen very little of life yet, and--and--well, he +naturally did not wish to do anything like that yet. So there +were quarrels, and this, I am sure, was one of them." + +"And your father?" asked Holmes. "Was he in favour of such a +union?" + +"No, he was averse to it also. No one but Mr. McCarthy was in +favour of it." A quick blush passed over her fresh young face as +Holmes shot one of his keen, questioning glances at her. + +"Thank you for this information," said he. "May I see your father +if I call to-morrow?" + +"I am afraid the doctor won't allow it." + +"The doctor?" + +"Yes, have you not heard? Poor father has never been strong for +years back, but this has broken him down completely. He has taken +to his bed, and Dr. Willows says that he is a wreck and that his +nervous system is shattered. Mr. McCarthy was the only man alive +who had known dad in the old days in Victoria." + +"Ha! In Victoria! That is important." + +"Yes, at the mines." + +"Quite so; at the gold-mines, where, as I understand, Mr. Turner +made his money." + +"Yes, certainly." + +"Thank you, Miss Turner. You have been of material assistance to +me." + +"You will tell me if you have any news to-morrow. No doubt you +will go to the prison to see James. Oh, if you do, Mr. Holmes, do +tell him that I know him to be innocent." + +"I will, Miss Turner." + +"I must go home now, for dad is very ill, and he misses me so if +I leave him. Good-bye, and God help you in your undertaking." She +hurried from the room as impulsively as she had entered, and we +heard the wheels of her carriage rattle off down the street. + +"I am ashamed of you, Holmes," said Lestrade with dignity after a +few minutes' silence. "Why should you raise up hopes which you +are bound to disappoint? I am not over-tender of heart, but I +call it cruel." + +"I think that I see my way to clearing James McCarthy," said +Holmes. "Have you an order to see him in prison?" + +"Yes, but only for you and me." + +"Then I shall reconsider my resolution about going out. We have +still time to take a train to Hereford and see him to-night?" + +"Ample." + +"Then let us do so. Watson, I fear that you will find it very +slow, but I shall only be away a couple of hours." + +I walked down to the station with them, and then wandered through +the streets of the little town, finally returning to the hotel, +where I lay upon the sofa and tried to interest myself in a +yellow-backed novel. The puny plot of the story was so thin, +however, when compared to the deep mystery through which we were +groping, and I found my attention wander so continually from the +action to the fact, that I at last flung it across the room and +gave myself up entirely to a consideration of the events of the +day. Supposing that this unhappy young man's story were +absolutely true, then what hellish thing, what absolutely +unforeseen and extraordinary calamity could have occurred between +the time when he parted from his father, and the moment when, +drawn back by his screams, he rushed into the glade? It was +something terrible and deadly. What could it be? Might not the +nature of the injuries reveal something to my medical instincts? +I rang the bell and called for the weekly county paper, which +contained a verbatim account of the inquest. In the surgeon's +deposition it was stated that the posterior third of the left +parietal bone and the left half of the occipital bone had been +shattered by a heavy blow from a blunt weapon. I marked the spot +upon my own head. Clearly such a blow must have been struck from +behind. That was to some extent in favour of the accused, as when +seen quarrelling he was face to face with his father. Still, it +did not go for very much, for the older man might have turned his +back before the blow fell. Still, it might be worth while to call +Holmes' attention to it. Then there was the peculiar dying +reference to a rat. What could that mean? It could not be +delirium. A man dying from a sudden blow does not commonly become +delirious. No, it was more likely to be an attempt to explain how +he met his fate. But what could it indicate? I cudgelled my +brains to find some possible explanation. And then the incident +of the grey cloth seen by young McCarthy. If that were true the +murderer must have dropped some part of his dress, presumably his +overcoat, in his flight, and must have had the hardihood to +return and to carry it away at the instant when the son was +kneeling with his back turned not a dozen paces off. What a +tissue of mysteries and improbabilities the whole thing was! I +did not wonder at Lestrade's opinion, and yet I had so much faith +in Sherlock Holmes' insight that I could not lose hope as long +as every fresh fact seemed to strengthen his conviction of young +McCarthy's innocence. + +It was late before Sherlock Holmes returned. He came back alone, +for Lestrade was staying in lodgings in the town. + +"The glass still keeps very high," he remarked as he sat down. +"It is of importance that it should not rain before we are able +to go over the ground. On the other hand, a man should be at his +very best and keenest for such nice work as that, and I did not +wish to do it when fagged by a long journey. I have seen young +McCarthy." + +"And what did you learn from him?" + +"Nothing." + +"Could he throw no light?" + +"None at all. I was inclined to think at one time that he knew +who had done it and was screening him or her, but I am convinced +now that he is as puzzled as everyone else. He is not a very +quick-witted youth, though comely to look at and, I should think, +sound at heart." + +"I cannot admire his taste," I remarked, "if it is indeed a fact +that he was averse to a marriage with so charming a young lady as +this Miss Turner." + +"Ah, thereby hangs a rather painful tale. This fellow is madly, +insanely, in love with her, but some two years ago, when he was +only a lad, and before he really knew her, for she had been away +five years at a boarding-school, what does the idiot do but get +into the clutches of a barmaid in Bristol and marry her at a +registry office? No one knows a word of the matter, but you can +imagine how maddening it must be to him to be upbraided for not +doing what he would give his very eyes to do, but what he knows +to be absolutely impossible. It was sheer frenzy of this sort +which made him throw his hands up into the air when his father, +at their last interview, was goading him on to propose to Miss +Turner. On the other hand, he had no means of supporting himself, +and his father, who was by all accounts a very hard man, would +have thrown him over utterly had he known the truth. It was with +his barmaid wife that he had spent the last three days in +Bristol, and his father did not know where he was. Mark that +point. It is of importance. Good has come out of evil, however, +for the barmaid, finding from the papers that he is in serious +trouble and likely to be hanged, has thrown him over utterly and +has written to him to say that she has a husband already in the +Bermuda Dockyard, so that there is really no tie between them. I +think that that bit of news has consoled young McCarthy for all +that he has suffered." + +"But if he is innocent, who has done it?" + +"Ah! who? I would call your attention very particularly to two +points. One is that the murdered man had an appointment with +someone at the pool, and that the someone could not have been his +son, for his son was away, and he did not know when he would +return. The second is that the murdered man was heard to cry +'Cooee!' before he knew that his son had returned. Those are the +crucial points upon which the case depends. And now let us talk +about George Meredith, if you please, and we shall leave all +minor matters until to-morrow." + +There was no rain, as Holmes had foretold, and the morning broke +bright and cloudless. At nine o'clock Lestrade called for us with +the carriage, and we set off for Hatherley Farm and the Boscombe +Pool. + +"There is serious news this morning," Lestrade observed. "It is +said that Mr. Turner, of the Hall, is so ill that his life is +despaired of." + +"An elderly man, I presume?" said Holmes. + +"About sixty; but his constitution has been shattered by his life +abroad, and he has been in failing health for some time. This +business has had a very bad effect upon him. He was an old friend +of McCarthy's, and, I may add, a great benefactor to him, for I +have learned that he gave him Hatherley Farm rent free." + +"Indeed! That is interesting," said Holmes. + +"Oh, yes! In a hundred other ways he has helped him. Everybody +about here speaks of his kindness to him." + +"Really! Does it not strike you as a little singular that this +McCarthy, who appears to have had little of his own, and to have +been under such obligations to Turner, should still talk of +marrying his son to Turner's daughter, who is, presumably, +heiress to the estate, and that in such a very cocksure manner, +as if it were merely a case of a proposal and all else would +follow? It is the more strange, since we know that Turner himself +was averse to the idea. The daughter told us as much. Do you not +deduce something from that?" + +"We have got to the deductions and the inferences," said +Lestrade, winking at me. "I find it hard enough to tackle facts, +Holmes, without flying away after theories and fancies." + +"You are right," said Holmes demurely; "you do find it very hard +to tackle the facts." + +"Anyhow, I have grasped one fact which you seem to find it +difficult to get hold of," replied Lestrade with some warmth. + +"And that is--" + +"That McCarthy senior met his death from McCarthy junior and that +all theories to the contrary are the merest moonshine." + +"Well, moonshine is a brighter thing than fog," said Holmes, +laughing. "But I am very much mistaken if this is not Hatherley +Farm upon the left." + +"Yes, that is it." It was a widespread, comfortable-looking +building, two-storied, slate-roofed, with great yellow blotches +of lichen upon the grey walls. The drawn blinds and the smokeless +chimneys, however, gave it a stricken look, as though the weight +of this horror still lay heavy upon it. We called at the door, +when the maid, at Holmes' request, showed us the boots which her +master wore at the time of his death, and also a pair of the +son's, though not the pair which he had then had. Having measured +these very carefully from seven or eight different points, Holmes +desired to be led to the court-yard, from which we all followed +the winding track which led to Boscombe Pool. + +Sherlock Holmes was transformed when he was hot upon such a scent +as this. Men who had only known the quiet thinker and logician of +Baker Street would have failed to recognise him. His face flushed +and darkened. His brows were drawn into two hard black lines, +while his eyes shone out from beneath them with a steely glitter. +His face was bent downward, his shoulders bowed, his lips +compressed, and the veins stood out like whipcord in his long, +sinewy neck. His nostrils seemed to dilate with a purely animal +lust for the chase, and his mind was so absolutely concentrated +upon the matter before him that a question or remark fell +unheeded upon his ears, or, at the most, only provoked a quick, +impatient snarl in reply. Swiftly and silently he made his way +along the track which ran through the meadows, and so by way of +the woods to the Boscombe Pool. It was damp, marshy ground, as is +all that district, and there were marks of many feet, both upon +the path and amid the short grass which bounded it on either +side. Sometimes Holmes would hurry on, sometimes stop dead, and +once he made quite a little detour into the meadow. Lestrade and +I walked behind him, the detective indifferent and contemptuous, +while I watched my friend with the interest which sprang from the +conviction that every one of his actions was directed towards a +definite end. + +The Boscombe Pool, which is a little reed-girt sheet of water +some fifty yards across, is situated at the boundary between the +Hatherley Farm and the private park of the wealthy Mr. Turner. +Above the woods which lined it upon the farther side we could see +the red, jutting pinnacles which marked the site of the rich +landowner's dwelling. On the Hatherley side of the pool the woods +grew very thick, and there was a narrow belt of sodden grass +twenty paces across between the edge of the trees and the reeds +which lined the lake. Lestrade showed us the exact spot at which +the body had been found, and, indeed, so moist was the ground, +that I could plainly see the traces which had been left by the +fall of the stricken man. To Holmes, as I could see by his eager +face and peering eyes, very many other things were to be read +upon the trampled grass. He ran round, like a dog who is picking +up a scent, and then turned upon my companion. + +"What did you go into the pool for?" he asked. + +"I fished about with a rake. I thought there might be some weapon +or other trace. But how on earth--" + +"Oh, tut, tut! I have no time! That left foot of yours with its +inward twist is all over the place. A mole could trace it, and +there it vanishes among the reeds. Oh, how simple it would all +have been had I been here before they came like a herd of buffalo +and wallowed all over it. Here is where the party with the +lodge-keeper came, and they have covered all tracks for six or +eight feet round the body. But here are three separate tracks of +the same feet." He drew out a lens and lay down upon his +waterproof to have a better view, talking all the time rather to +himself than to us. "These are young McCarthy's feet. Twice he +was walking, and once he ran swiftly, so that the soles are +deeply marked and the heels hardly visible. That bears out his +story. He ran when he saw his father on the ground. Then here are +the father's feet as he paced up and down. What is this, then? It +is the butt-end of the gun as the son stood listening. And this? +Ha, ha! What have we here? Tiptoes! tiptoes! Square, too, quite +unusual boots! They come, they go, they come again--of course +that was for the cloak. Now where did they come from?" He ran up +and down, sometimes losing, sometimes finding the track until we +were well within the edge of the wood and under the shadow of a +great beech, the largest tree in the neighbourhood. Holmes traced +his way to the farther side of this and lay down once more upon +his face with a little cry of satisfaction. For a long time he +remained there, turning over the leaves and dried sticks, +gathering up what seemed to me to be dust into an envelope and +examining with his lens not only the ground but even the bark of +the tree as far as he could reach. A jagged stone was lying among +the moss, and this also he carefully examined and retained. Then +he followed a pathway through the wood until he came to the +highroad, where all traces were lost. + +"It has been a case of considerable interest," he remarked, +returning to his natural manner. "I fancy that this grey house on +the right must be the lodge. I think that I will go in and have a +word with Moran, and perhaps write a little note. Having done +that, we may drive back to our luncheon. You may walk to the cab, +and I shall be with you presently." + +It was about ten minutes before we regained our cab and drove +back into Ross, Holmes still carrying with him the stone which he +had picked up in the wood. + +"This may interest you, Lestrade," he remarked, holding it out. +"The murder was done with it." + +"I see no marks." + +"There are none." + +"How do you know, then?" + +"The grass was growing under it. It had only lain there a few +days. There was no sign of a place whence it had been taken. It +corresponds with the injuries. There is no sign of any other +weapon." + +"And the murderer?" + +"Is a tall man, left-handed, limps with the right leg, wears +thick-soled shooting-boots and a grey cloak, smokes Indian +cigars, uses a cigar-holder, and carries a blunt pen-knife in his +pocket. There are several other indications, but these may be +enough to aid us in our search." + +Lestrade laughed. "I am afraid that I am still a sceptic," he +said. "Theories are all very well, but we have to deal with a +hard-headed British jury." + +"Nous verrons," answered Holmes calmly. "You work your own +method, and I shall work mine. I shall be busy this afternoon, +and shall probably return to London by the evening train." + +"And leave your case unfinished?" + +"No, finished." + +"But the mystery?" + +"It is solved." + +"Who was the criminal, then?" + +"The gentleman I describe." + +"But who is he?" + +"Surely it would not be difficult to find out. This is not such a +populous neighbourhood." + +Lestrade shrugged his shoulders. "I am a practical man," he said, +"and I really cannot undertake to go about the country looking +for a left-handed gentleman with a game leg. I should become the +laughing-stock of Scotland Yard." + +"All right," said Holmes quietly. "I have given you the chance. +Here are your lodgings. Good-bye. I shall drop you a line before +I leave." + +Having left Lestrade at his rooms, we drove to our hotel, where +we found lunch upon the table. Holmes was silent and buried in +thought with a pained expression upon his face, as one who finds +himself in a perplexing position. + +"Look here, Watson," he said when the cloth was cleared "just sit +down in this chair and let me preach to you for a little. I don't +know quite what to do, and I should value your advice. Light a +cigar and let me expound." + + "Pray do so." + +"Well, now, in considering this case there are two points about +young McCarthy's narrative which struck us both instantly, +although they impressed me in his favour and you against him. One +was the fact that his father should, according to his account, +cry 'Cooee!' before seeing him. The other was his singular dying +reference to a rat. He mumbled several words, you understand, but +that was all that caught the son's ear. Now from this double +point our research must commence, and we will begin it by +presuming that what the lad says is absolutely true." + +"What of this 'Cooee!' then?" + +"Well, obviously it could not have been meant for the son. The +son, as far as he knew, was in Bristol. It was mere chance that +he was within earshot. The 'Cooee!' was meant to attract the +attention of whoever it was that he had the appointment with. But +'Cooee' is a distinctly Australian cry, and one which is used +between Australians. There is a strong presumption that the +person whom McCarthy expected to meet him at Boscombe Pool was +someone who had been in Australia." + +"What of the rat, then?" + +Sherlock Holmes took a folded paper from his pocket and flattened +it out on the table. "This is a map of the Colony of Victoria," +he said. "I wired to Bristol for it last night." He put his hand +over part of the map. "What do you read?" + +"ARAT," I read. + +"And now?" He raised his hand. + +"BALLARAT." + +"Quite so. That was the word the man uttered, and of which his +son only caught the last two syllables. He was trying to utter +the name of his murderer. So and so, of Ballarat." + +"It is wonderful!" I exclaimed. + +"It is obvious. And now, you see, I had narrowed the field down +considerably. The possession of a grey garment was a third point +which, granting the son's statement to be correct, was a +certainty. We have come now out of mere vagueness to the definite +conception of an Australian from Ballarat with a grey cloak." + +"Certainly." + +"And one who was at home in the district, for the pool can only +be approached by the farm or by the estate, where strangers could +hardly wander." + +"Quite so." + +"Then comes our expedition of to-day. By an examination of the +ground I gained the trifling details which I gave to that +imbecile Lestrade, as to the personality of the criminal." + +"But how did you gain them?" + +"You know my method. It is founded upon the observation of +trifles." + +"His height I know that you might roughly judge from the length +of his stride. His boots, too, might be told from their traces." + +"Yes, they were peculiar boots." + +"But his lameness?" + +"The impression of his right foot was always less distinct than +his left. He put less weight upon it. Why? Because he limped--he +was lame." + +"But his left-handedness." + +"You were yourself struck by the nature of the injury as recorded +by the surgeon at the inquest. The blow was struck from +immediately behind, and yet was upon the left side. Now, how can +that be unless it were by a left-handed man? He had stood behind +that tree during the interview between the father and son. He had +even smoked there. I found the ash of a cigar, which my special +knowledge of tobacco ashes enables me to pronounce as an Indian +cigar. I have, as you know, devoted some attention to this, and +written a little monograph on the ashes of 140 different +varieties of pipe, cigar, and cigarette tobacco. Having found the +ash, I then looked round and discovered the stump among the moss +where he had tossed it. It was an Indian cigar, of the variety +which are rolled in Rotterdam." + +"And the cigar-holder?" + +"I could see that the end had not been in his mouth. Therefore he +used a holder. The tip had been cut off, not bitten off, but the +cut was not a clean one, so I deduced a blunt pen-knife." + +"Holmes," I said, "you have drawn a net round this man from which +he cannot escape, and you have saved an innocent human life as +truly as if you had cut the cord which was hanging him. I see the +direction in which all this points. The culprit is--" + +"Mr. John Turner," cried the hotel waiter, opening the door of +our sitting-room, and ushering in a visitor. + +The man who entered was a strange and impressive figure. His +slow, limping step and bowed shoulders gave the appearance of +decrepitude, and yet his hard, deep-lined, craggy features, and +his enormous limbs showed that he was possessed of unusual +strength of body and of character. His tangled beard, grizzled +hair, and outstanding, drooping eyebrows combined to give an air +of dignity and power to his appearance, but his face was of an +ashen white, while his lips and the corners of his nostrils were +tinged with a shade of blue. It was clear to me at a glance that +he was in the grip of some deadly and chronic disease. + +"Pray sit down on the sofa," said Holmes gently. "You had my +note?" + +"Yes, the lodge-keeper brought it up. You said that you wished to +see me here to avoid scandal." + +"I thought people would talk if I went to the Hall." + +"And why did you wish to see me?" He looked across at my +companion with despair in his weary eyes, as though his question +was already answered. + +"Yes," said Holmes, answering the look rather than the words. "It +is so. I know all about McCarthy." + +The old man sank his face in his hands. "God help me!" he cried. +"But I would not have let the young man come to harm. I give you +my word that I would have spoken out if it went against him at +the Assizes." + +"I am glad to hear you say so," said Holmes gravely. + +"I would have spoken now had it not been for my dear girl. It +would break her heart--it will break her heart when she hears +that I am arrested." + +"It may not come to that," said Holmes. + +"What?" + +"I am no official agent. I understand that it was your daughter +who required my presence here, and I am acting in her interests. +Young McCarthy must be got off, however." + +"I am a dying man," said old Turner. "I have had diabetes for +years. My doctor says it is a question whether I shall live a +month. Yet I would rather die under my own roof than in a gaol." + +Holmes rose and sat down at the table with his pen in his hand +and a bundle of paper before him. "Just tell us the truth," he +said. "I shall jot down the facts. You will sign it, and Watson +here can witness it. Then I could produce your confession at the +last extremity to save young McCarthy. I promise you that I shall +not use it unless it is absolutely needed." + +"It's as well," said the old man; "it's a question whether I +shall live to the Assizes, so it matters little to me, but I +should wish to spare Alice the shock. And now I will make the +thing clear to you; it has been a long time in the acting, but +will not take me long to tell. + +"You didn't know this dead man, McCarthy. He was a devil +incarnate. I tell you that. God keep you out of the clutches of +such a man as he. His grip has been upon me these twenty years, +and he has blasted my life. I'll tell you first how I came to be +in his power. + +"It was in the early '60's at the diggings. I was a young chap +then, hot-blooded and reckless, ready to turn my hand at +anything; I got among bad companions, took to drink, had no luck +with my claim, took to the bush, and in a word became what you +would call over here a highway robber. There were six of us, and +we had a wild, free life of it, sticking up a station from time +to time, or stopping the wagons on the road to the diggings. +Black Jack of Ballarat was the name I went under, and our party +is still remembered in the colony as the Ballarat Gang. + +"One day a gold convoy came down from Ballarat to Melbourne, and +we lay in wait for it and attacked it. There were six troopers +and six of us, so it was a close thing, but we emptied four of +their saddles at the first volley. Three of our boys were killed, +however, before we got the swag. I put my pistol to the head of +the wagon-driver, who was this very man McCarthy. I wish to the +Lord that I had shot him then, but I spared him, though I saw his +wicked little eyes fixed on my face, as though to remember every +feature. We got away with the gold, became wealthy men, and made +our way over to England without being suspected. There I parted +from my old pals and determined to settle down to a quiet and +respectable life. I bought this estate, which chanced to be in +the market, and I set myself to do a little good with my money, +to make up for the way in which I had earned it. I married, too, +and though my wife died young she left me my dear little Alice. +Even when she was just a baby her wee hand seemed to lead me down +the right path as nothing else had ever done. In a word, I turned +over a new leaf and did my best to make up for the past. All was +going well when McCarthy laid his grip upon me. + +"I had gone up to town about an investment, and I met him in +Regent Street with hardly a coat to his back or a boot to his +foot. + +"'Here we are, Jack,' says he, touching me on the arm; 'we'll be +as good as a family to you. There's two of us, me and my son, and +you can have the keeping of us. If you don't--it's a fine, +law-abiding country is England, and there's always a policeman +within hail.' + +"Well, down they came to the west country, there was no shaking +them off, and there they have lived rent free on my best land +ever since. There was no rest for me, no peace, no forgetfulness; +turn where I would, there was his cunning, grinning face at my +elbow. It grew worse as Alice grew up, for he soon saw I was more +afraid of her knowing my past than of the police. Whatever he +wanted he must have, and whatever it was I gave him without +question, land, money, houses, until at last he asked a thing +which I could not give. He asked for Alice. + +"His son, you see, had grown up, and so had my girl, and as I was +known to be in weak health, it seemed a fine stroke to him that +his lad should step into the whole property. But there I was +firm. I would not have his cursed stock mixed with mine; not that +I had any dislike to the lad, but his blood was in him, and that +was enough. I stood firm. McCarthy threatened. I braved him to do +his worst. We were to meet at the pool midway between our houses +to talk it over. + +"When I went down there I found him talking with his son, so I +smoked a cigar and waited behind a tree until he should be alone. +But as I listened to his talk all that was black and bitter in +me seemed to come uppermost. He was urging his son to marry my +daughter with as little regard for what she might think as if she +were a slut from off the streets. It drove me mad to think that I +and all that I held most dear should be in the power of such a +man as this. Could I not snap the bond? I was already a dying and +a desperate man. Though clear of mind and fairly strong of limb, +I knew that my own fate was sealed. But my memory and my girl! +Both could be saved if I could but silence that foul tongue. I +did it, Mr. Holmes. I would do it again. Deeply as I have sinned, +I have led a life of martyrdom to atone for it. But that my girl +should be entangled in the same meshes which held me was more +than I could suffer. I struck him down with no more compunction +than if he had been some foul and venomous beast. His cry brought +back his son; but I had gained the cover of the wood, though I +was forced to go back to fetch the cloak which I had dropped in +my flight. That is the true story, gentlemen, of all that +occurred." + +"Well, it is not for me to judge you," said Holmes as the old man +signed the statement which had been drawn out. "I pray that we +may never be exposed to such a temptation." + +"I pray not, sir. And what do you intend to do?" + +"In view of your health, nothing. You are yourself aware that you +will soon have to answer for your deed at a higher court than the +Assizes. I will keep your confession, and if McCarthy is +condemned I shall be forced to use it. If not, it shall never be +seen by mortal eye; and your secret, whether you be alive or +dead, shall be safe with us." + +"Farewell, then," said the old man solemnly. "Your own deathbeds, +when they come, will be the easier for the thought of the peace +which you have given to mine." Tottering and shaking in all his +giant frame, he stumbled slowly from the room. + +"God help us!" said Holmes after a long silence. "Why does fate +play such tricks with poor, helpless worms? I never hear of such +a case as this that I do not think of Baxter's words, and say, +'There, but for the grace of God, goes Sherlock Holmes.'" + +James McCarthy was acquitted at the Assizes on the strength of a +number of objections which had been drawn out by Holmes and +submitted to the defending counsel. Old Turner lived for seven +months after our interview, but he is now dead; and there is +every prospect that the son and daughter may come to live happily +together in ignorance of the black cloud which rests upon their +past. + + + +ADVENTURE V. THE FIVE ORANGE PIPS + +When I glance over my notes and records of the Sherlock Holmes +cases between the years '82 and '90, I am faced by so many which +present strange and interesting features that it is no easy +matter to know which to choose and which to leave. Some, however, +have already gained publicity through the papers, and others have +not offered a field for those peculiar qualities which my friend +possessed in so high a degree, and which it is the object of +these papers to illustrate. Some, too, have baffled his +analytical skill, and would be, as narratives, beginnings without +an ending, while others have been but partially cleared up, and +have their explanations founded rather upon conjecture and +surmise than on that absolute logical proof which was so dear to +him. There is, however, one of these last which was so remarkable +in its details and so startling in its results that I am tempted +to give some account of it in spite of the fact that there are +points in connection with it which never have been, and probably +never will be, entirely cleared up. + +The year '87 furnished us with a long series of cases of greater +or less interest, of which I retain the records. Among my +headings under this one twelve months I find an account of the +adventure of the Paradol Chamber, of the Amateur Mendicant +Society, who held a luxurious club in the lower vault of a +furniture warehouse, of the facts connected with the loss of the +British barque "Sophy Anderson", of the singular adventures of the +Grice Patersons in the island of Uffa, and finally of the +Camberwell poisoning case. In the latter, as may be remembered, +Sherlock Holmes was able, by winding up the dead man's watch, to +prove that it had been wound up two hours before, and that +therefore the deceased had gone to bed within that time--a +deduction which was of the greatest importance in clearing up the +case. All these I may sketch out at some future date, but none of +them present such singular features as the strange train of +circumstances which I have now taken up my pen to describe. + +It was in the latter days of September, and the equinoctial gales +had set in with exceptional violence. All day the wind had +screamed and the rain had beaten against the windows, so that +even here in the heart of great, hand-made London we were forced +to raise our minds for the instant from the routine of life and +to recognise the presence of those great elemental forces which +shriek at mankind through the bars of his civilisation, like +untamed beasts in a cage. As evening drew in, the storm grew +higher and louder, and the wind cried and sobbed like a child in +the chimney. Sherlock Holmes sat moodily at one side of the +fireplace cross-indexing his records of crime, while I at the +other was deep in one of Clark Russell's fine sea-stories until +the howl of the gale from without seemed to blend with the text, +and the splash of the rain to lengthen out into the long swash of +the sea waves. My wife was on a visit to her mother's, and for a +few days I was a dweller once more in my old quarters at Baker +Street. + +"Why," said I, glancing up at my companion, "that was surely the +bell. Who could come to-night? Some friend of yours, perhaps?" + +"Except yourself I have none," he answered. "I do not encourage +visitors." + +"A client, then?" + +"If so, it is a serious case. Nothing less would bring a man out +on such a day and at such an hour. But I take it that it is more +likely to be some crony of the landlady's." + +Sherlock Holmes was wrong in his conjecture, however, for there +came a step in the passage and a tapping at the door. He +stretched out his long arm to turn the lamp away from himself and +towards the vacant chair upon which a newcomer must sit. + +"Come in!" said he. + +The man who entered was young, some two-and-twenty at the +outside, well-groomed and trimly clad, with something of +refinement and delicacy in his bearing. The streaming umbrella +which he held in his hand, and his long shining waterproof told +of the fierce weather through which he had come. He looked about +him anxiously in the glare of the lamp, and I could see that his +face was pale and his eyes heavy, like those of a man who is +weighed down with some great anxiety. + +"I owe you an apology," he said, raising his golden pince-nez to +his eyes. "I trust that I am not intruding. I fear that I have +brought some traces of the storm and rain into your snug +chamber." + +"Give me your coat and umbrella," said Holmes. "They may rest +here on the hook and will be dry presently. You have come up from +the south-west, I see." + +"Yes, from Horsham." + +"That clay and chalk mixture which I see upon your toe caps is +quite distinctive." + +"I have come for advice." + +"That is easily got." + +"And help." + +"That is not always so easy." + +"I have heard of you, Mr. Holmes. I heard from Major Prendergast +how you saved him in the Tankerville Club scandal." + +"Ah, of course. He was wrongfully accused of cheating at cards." + +"He said that you could solve anything." + +"He said too much." + +"That you are never beaten." + +"I have been beaten four times--three times by men, and once by a +woman." + +"But what is that compared with the number of your successes?" + +"It is true that I have been generally successful." + +"Then you may be so with me." + +"I beg that you will draw your chair up to the fire and favour me +with some details as to your case." + +"It is no ordinary one." + +"None of those which come to me are. I am the last court of +appeal." + +"And yet I question, sir, whether, in all your experience, you +have ever listened to a more mysterious and inexplicable chain of +events than those which have happened in my own family." + +"You fill me with interest," said Holmes. "Pray give us the +essential facts from the commencement, and I can afterwards +question you as to those details which seem to me to be most +important." + +The young man pulled his chair up and pushed his wet feet out +towards the blaze. + +"My name," said he, "is John Openshaw, but my own affairs have, +as far as I can understand, little to do with this awful +business. It is a hereditary matter; so in order to give you an +idea of the facts, I must go back to the commencement of the +affair. + +"You must know that my grandfather had two sons--my uncle Elias +and my father Joseph. My father had a small factory at Coventry, +which he enlarged at the time of the invention of bicycling. He +was a patentee of the Openshaw unbreakable tire, and his business +met with such success that he was able to sell it and to retire +upon a handsome competence. + +"My uncle Elias emigrated to America when he was a young man and +became a planter in Florida, where he was reported to have done +very well. At the time of the war he fought in Jackson's army, +and afterwards under Hood, where he rose to be a colonel. When +Lee laid down his arms my uncle returned to his plantation, where +he remained for three or four years. About 1869 or 1870 he came +back to Europe and took a small estate in Sussex, near Horsham. +He had made a very considerable fortune in the States, and his +reason for leaving them was his aversion to the negroes, and his +dislike of the Republican policy in extending the franchise to +them. He was a singular man, fierce and quick-tempered, very +foul-mouthed when he was angry, and of a most retiring +disposition. During all the years that he lived at Horsham, I +doubt if ever he set foot in the town. He had a garden and two or +three fields round his house, and there he would take his +exercise, though very often for weeks on end he would never leave +his room. He drank a great deal of brandy and smoked very +heavily, but he would see no society and did not want any +friends, not even his own brother. + +"He didn't mind me; in fact, he took a fancy to me, for at the +time when he saw me first I was a youngster of twelve or so. This +would be in the year 1878, after he had been eight or nine years +in England. He begged my father to let me live with him and he +was very kind to me in his way. When he was sober he used to be +fond of playing backgammon and draughts with me, and he would +make me his representative both with the servants and with the +tradespeople, so that by the time that I was sixteen I was quite +master of the house. I kept all the keys and could go where I +liked and do what I liked, so long as I did not disturb him in +his privacy. There was one singular exception, however, for he +had a single room, a lumber-room up among the attics, which was +invariably locked, and which he would never permit either me or +anyone else to enter. With a boy's curiosity I have peeped +through the keyhole, but I was never able to see more than such a +collection of old trunks and bundles as would be expected in such +a room. + +"One day--it was in March, 1883--a letter with a foreign stamp +lay upon the table in front of the colonel's plate. It was not a +common thing for him to receive letters, for his bills were all +paid in ready money, and he had no friends of any sort. 'From +India!' said he as he took it up, 'Pondicherry postmark! What can +this be?' Opening it hurriedly, out there jumped five little +dried orange pips, which pattered down upon his plate. I began to +laugh at this, but the laugh was struck from my lips at the sight +of his face. His lip had fallen, his eyes were protruding, his +skin the colour of putty, and he glared at the envelope which he +still held in his trembling hand, 'K. K. K.!' he shrieked, and +then, 'My God, my God, my sins have overtaken me!' + +"'What is it, uncle?' I cried. + +"'Death,' said he, and rising from the table he retired to his +room, leaving me palpitating with horror. I took up the envelope +and saw scrawled in red ink upon the inner flap, just above the +gum, the letter K three times repeated. There was nothing else +save the five dried pips. What could be the reason of his +overpowering terror? I left the breakfast-table, and as I +ascended the stair I met him coming down with an old rusty key, +which must have belonged to the attic, in one hand, and a small +brass box, like a cashbox, in the other. + +"'They may do what they like, but I'll checkmate them still,' +said he with an oath. 'Tell Mary that I shall want a fire in my +room to-day, and send down to Fordham, the Horsham lawyer.' + +"I did as he ordered, and when the lawyer arrived I was asked to +step up to the room. The fire was burning brightly, and in the +grate there was a mass of black, fluffy ashes, as of burned +paper, while the brass box stood open and empty beside it. As I +glanced at the box I noticed, with a start, that upon the lid was +printed the treble K which I had read in the morning upon the +envelope. + +"'I wish you, John,' said my uncle, 'to witness my will. I leave +my estate, with all its advantages and all its disadvantages, to +my brother, your father, whence it will, no doubt, descend to +you. If you can enjoy it in peace, well and good! If you find you +cannot, take my advice, my boy, and leave it to your deadliest +enemy. I am sorry to give you such a two-edged thing, but I can't +say what turn things are going to take. Kindly sign the paper +where Mr. Fordham shows you.' + +"I signed the paper as directed, and the lawyer took it away with +him. The singular incident made, as you may think, the deepest +impression upon me, and I pondered over it and turned it every +way in my mind without being able to make anything of it. Yet I +could not shake off the vague feeling of dread which it left +behind, though the sensation grew less keen as the weeks passed +and nothing happened to disturb the usual routine of our lives. I +could see a change in my uncle, however. He drank more than ever, +and he was less inclined for any sort of society. Most of his +time he would spend in his room, with the door locked upon the +inside, but sometimes he would emerge in a sort of drunken frenzy +and would burst out of the house and tear about the garden with a +revolver in his hand, screaming out that he was afraid of no man, +and that he was not to be cooped up, like a sheep in a pen, by +man or devil. When these hot fits were over, however, he would +rush tumultuously in at the door and lock and bar it behind him, +like a man who can brazen it out no longer against the terror +which lies at the roots of his soul. At such times I have seen +his face, even on a cold day, glisten with moisture, as though it +were new raised from a basin. + +"Well, to come to an end of the matter, Mr. Holmes, and not to +abuse your patience, there came a night when he made one of those +drunken sallies from which he never came back. We found him, when +we went to search for him, face downward in a little +green-scummed pool, which lay at the foot of the garden. There +was no sign of any violence, and the water was but two feet deep, +so that the jury, having regard to his known eccentricity, +brought in a verdict of 'suicide.' But I, who knew how he winced +from the very thought of death, had much ado to persuade myself +that he had gone out of his way to meet it. The matter passed, +however, and my father entered into possession of the estate, and +of some 14,000 pounds, which lay to his credit at the bank." + +"One moment," Holmes interposed, "your statement is, I foresee, +one of the most remarkable to which I have ever listened. Let me +have the date of the reception by your uncle of the letter, and +the date of his supposed suicide." + +"The letter arrived on March 10, 1883. His death was seven weeks +later, upon the night of May 2nd." + +"Thank you. Pray proceed." + +"When my father took over the Horsham property, he, at my +request, made a careful examination of the attic, which had been +always locked up. We found the brass box there, although its +contents had been destroyed. On the inside of the cover was a +paper label, with the initials of K. K. K. repeated upon it, and +'Letters, memoranda, receipts, and a register' written beneath. +These, we presume, indicated the nature of the papers which had +been destroyed by Colonel Openshaw. For the rest, there was +nothing of much importance in the attic save a great many +scattered papers and note-books bearing upon my uncle's life in +America. Some of them were of the war time and showed that he had +done his duty well and had borne the repute of a brave soldier. +Others were of a date during the reconstruction of the Southern +states, and were mostly concerned with politics, for he had +evidently taken a strong part in opposing the carpet-bag +politicians who had been sent down from the North. + +"Well, it was the beginning of '84 when my father came to live at +Horsham, and all went as well as possible with us until the +January of '85. On the fourth day after the new year I heard my +father give a sharp cry of surprise as we sat together at the +breakfast-table. There he was, sitting with a newly opened +envelope in one hand and five dried orange pips in the +outstretched palm of the other one. He had always laughed at what +he called my cock-and-bull story about the colonel, but he looked +very scared and puzzled now that the same thing had come upon +himself. + +"'Why, what on earth does this mean, John?' he stammered. + +"My heart had turned to lead. 'It is K. K. K.,' said I. + +"He looked inside the envelope. 'So it is,' he cried. 'Here are +the very letters. But what is this written above them?' + +"'Put the papers on the sundial,' I read, peeping over his +shoulder. + +"'What papers? What sundial?' he asked. + +"'The sundial in the garden. There is no other,' said I; 'but the +papers must be those that are destroyed.' + +"'Pooh!' said he, gripping hard at his courage. 'We are in a +civilised land here, and we can't have tomfoolery of this kind. +Where does the thing come from?' + +"'From Dundee,' I answered, glancing at the postmark. + +"'Some preposterous practical joke,' said he. 'What have I to do +with sundials and papers? I shall take no notice of such +nonsense.' + +"'I should certainly speak to the police,' I said. + +"'And be laughed at for my pains. Nothing of the sort.' + +"'Then let me do so?' + +"'No, I forbid you. I won't have a fuss made about such +nonsense.' + +"It was in vain to argue with him, for he was a very obstinate +man. I went about, however, with a heart which was full of +forebodings. + +"On the third day after the coming of the letter my father went +from home to visit an old friend of his, Major Freebody, who is +in command of one of the forts upon Portsdown Hill. I was glad +that he should go, for it seemed to me that he was farther from +danger when he was away from home. In that, however, I was in +error. Upon the second day of his absence I received a telegram +from the major, imploring me to come at once. My father had +fallen over one of the deep chalk-pits which abound in the +neighbourhood, and was lying senseless, with a shattered skull. I +hurried to him, but he passed away without having ever recovered +his consciousness. He had, as it appears, been returning from +Fareham in the twilight, and as the country was unknown to him, +and the chalk-pit unfenced, the jury had no hesitation in +bringing in a verdict of 'death from accidental causes.' +Carefully as I examined every fact connected with his death, I +was unable to find anything which could suggest the idea of +murder. There were no signs of violence, no footmarks, no +robbery, no record of strangers having been seen upon the roads. +And yet I need not tell you that my mind was far from at ease, +and that I was well-nigh certain that some foul plot had been +woven round him. + +"In this sinister way I came into my inheritance. You will ask me +why I did not dispose of it? I answer, because I was well +convinced that our troubles were in some way dependent upon an +incident in my uncle's life, and that the danger would be as +pressing in one house as in another. + +"It was in January, '85, that my poor father met his end, and two +years and eight months have elapsed since then. During that time +I have lived happily at Horsham, and I had begun to hope that +this curse had passed away from the family, and that it had ended +with the last generation. I had begun to take comfort too soon, +however; yesterday morning the blow fell in the very shape in +which it had come upon my father." + +The young man took from his waistcoat a crumpled envelope, and +turning to the table he shook out upon it five little dried +orange pips. + +"This is the envelope," he continued. "The postmark is +London--eastern division. Within are the very words which were +upon my father's last message: 'K. K. K.'; and then 'Put the +papers on the sundial.'" + +"What have you done?" asked Holmes. + +"Nothing." + +"Nothing?" + +"To tell the truth"--he sank his face into his thin, white +hands--"I have felt helpless. I have felt like one of those poor +rabbits when the snake is writhing towards it. I seem to be in +the grasp of some resistless, inexorable evil, which no foresight +and no precautions can guard against." + +"Tut! tut!" cried Sherlock Holmes. "You must act, man, or you are +lost. Nothing but energy can save you. This is no time for +despair." + +"I have seen the police." + +"Ah!" + +"But they listened to my story with a smile. I am convinced that +the inspector has formed the opinion that the letters are all +practical jokes, and that the deaths of my relations were really +accidents, as the jury stated, and were not to be connected with +the warnings." + +Holmes shook his clenched hands in the air. "Incredible +imbecility!" he cried. + +"They have, however, allowed me a policeman, who may remain in +the house with me." + +"Has he come with you to-night?" + +"No. His orders were to stay in the house." + +Again Holmes raved in the air. + +"Why did you come to me," he cried, "and, above all, why did you +not come at once?" + +"I did not know. It was only to-day that I spoke to Major +Prendergast about my troubles and was advised by him to come to +you." + +"It is really two days since you had the letter. We should have +acted before this. You have no further evidence, I suppose, than +that which you have placed before us--no suggestive detail which +might help us?" + +"There is one thing," said John Openshaw. He rummaged in his coat +pocket, and, drawing out a piece of discoloured, blue-tinted +paper, he laid it out upon the table. "I have some remembrance," +said he, "that on the day when my uncle burned the papers I +observed that the small, unburned margins which lay amid the +ashes were of this particular colour. I found this single sheet +upon the floor of his room, and I am inclined to think that it +may be one of the papers which has, perhaps, fluttered out from +among the others, and in that way has escaped destruction. Beyond +the mention of pips, I do not see that it helps us much. I think +myself that it is a page from some private diary. The writing is +undoubtedly my uncle's." + +Holmes moved the lamp, and we both bent over the sheet of paper, +which showed by its ragged edge that it had indeed been torn from +a book. It was headed, "March, 1869," and beneath were the +following enigmatical notices: + +"4th. Hudson came. Same old platform. + +"7th. Set the pips on McCauley, Paramore, and + John Swain, of St. Augustine. + +"9th. McCauley cleared. + +"10th. John Swain cleared. + +"12th. Visited Paramore. All well." + +"Thank you!" said Holmes, folding up the paper and returning it +to our visitor. "And now you must on no account lose another +instant. We cannot spare time even to discuss what you have told +me. You must get home instantly and act." + +"What shall I do?" + +"There is but one thing to do. It must be done at once. You must +put this piece of paper which you have shown us into the brass +box which you have described. You must also put in a note to say +that all the other papers were burned by your uncle, and that +this is the only one which remains. You must assert that in such +words as will carry conviction with them. Having done this, you +must at once put the box out upon the sundial, as directed. Do +you understand?" + +"Entirely." + +"Do not think of revenge, or anything of the sort, at present. I +think that we may gain that by means of the law; but we have our +web to weave, while theirs is already woven. The first +consideration is to remove the pressing danger which threatens +you. The second is to clear up the mystery and to punish the +guilty parties." + +"I thank you," said the young man, rising and pulling on his +overcoat. "You have given me fresh life and hope. I shall +certainly do as you advise." + +"Do not lose an instant. And, above all, take care of yourself in +the meanwhile, for I do not think that there can be a doubt that +you are threatened by a very real and imminent danger. How do you +go back?" + +"By train from Waterloo." + +"It is not yet nine. The streets will be crowded, so I trust that +you may be in safety. And yet you cannot guard yourself too +closely." + +"I am armed." + +"That is well. To-morrow I shall set to work upon your case." + +"I shall see you at Horsham, then?" + +"No, your secret lies in London. It is there that I shall seek +it." + +"Then I shall call upon you in a day, or in two days, with news +as to the box and the papers. I shall take your advice in every +particular." He shook hands with us and took his leave. Outside +the wind still screamed and the rain splashed and pattered +against the windows. This strange, wild story seemed to have come +to us from amid the mad elements--blown in upon us like a sheet +of sea-weed in a gale--and now to have been reabsorbed by them +once more. + +Sherlock Holmes sat for some time in silence, with his head sunk +forward and his eyes bent upon the red glow of the fire. Then he +lit his pipe, and leaning back in his chair he watched the blue +smoke-rings as they chased each other up to the ceiling. + +"I think, Watson," he remarked at last, "that of all our cases we +have had none more fantastic than this." + +"Save, perhaps, the Sign of Four." + +"Well, yes. Save, perhaps, that. And yet this John Openshaw seems +to me to be walking amid even greater perils than did the +Sholtos." + +"But have you," I asked, "formed any definite conception as to +what these perils are?" + +"There can be no question as to their nature," he answered. + +"Then what are they? Who is this K. K. K., and why does he pursue +this unhappy family?" + +Sherlock Holmes closed his eyes and placed his elbows upon the +arms of his chair, with his finger-tips together. "The ideal +reasoner," he remarked, "would, when he had once been shown a +single fact in all its bearings, deduce from it not only all the +chain of events which led up to it but also all the results which +would follow from it. As Cuvier could correctly describe a whole +animal by the contemplation of a single bone, so the observer who +has thoroughly understood one link in a series of incidents +should be able to accurately state all the other ones, both +before and after. We have not yet grasped the results which the +reason alone can attain to. Problems may be solved in the study +which have baffled all those who have sought a solution by the +aid of their senses. To carry the art, however, to its highest +pitch, it is necessary that the reasoner should be able to +utilise all the facts which have come to his knowledge; and this +in itself implies, as you will readily see, a possession of all +knowledge, which, even in these days of free education and +encyclopaedias, is a somewhat rare accomplishment. It is not so +impossible, however, that a man should possess all knowledge +which is likely to be useful to him in his work, and this I have +endeavoured in my case to do. If I remember rightly, you on one +occasion, in the early days of our friendship, defined my limits +in a very precise fashion." + +"Yes," I answered, laughing. "It was a singular document. +Philosophy, astronomy, and politics were marked at zero, I +remember. Botany variable, geology profound as regards the +mud-stains from any region within fifty miles of town, chemistry +eccentric, anatomy unsystematic, sensational literature and crime +records unique, violin-player, boxer, swordsman, lawyer, and +self-poisoner by cocaine and tobacco. Those, I think, were the +main points of my analysis." + +Holmes grinned at the last item. "Well," he said, "I say now, as +I said then, that a man should keep his little brain-attic +stocked with all the furniture that he is likely to use, and the +rest he can put away in the lumber-room of his library, where he +can get it if he wants it. Now, for such a case as the one which +has been submitted to us to-night, we need certainly to muster +all our resources. Kindly hand me down the letter K of the +'American Encyclopaedia' which stands upon the shelf beside you. +Thank you. Now let us consider the situation and see what may be +deduced from it. In the first place, we may start with a strong +presumption that Colonel Openshaw had some very strong reason for +leaving America. Men at his time of life do not change all their +habits and exchange willingly the charming climate of Florida for +the lonely life of an English provincial town. His extreme love +of solitude in England suggests the idea that he was in fear of +someone or something, so we may assume as a working hypothesis +that it was fear of someone or something which drove him from +America. As to what it was he feared, we can only deduce that by +considering the formidable letters which were received by himself +and his successors. Did you remark the postmarks of those +letters?" + +"The first was from Pondicherry, the second from Dundee, and the +third from London." + +"From East London. What do you deduce from that?" + +"They are all seaports. That the writer was on board of a ship." + +"Excellent. We have already a clue. There can be no doubt that +the probability--the strong probability--is that the writer was +on board of a ship. And now let us consider another point. In the +case of Pondicherry, seven weeks elapsed between the threat and +its fulfilment, in Dundee it was only some three or four days. +Does that suggest anything?" + +"A greater distance to travel." + +"But the letter had also a greater distance to come." + +"Then I do not see the point." + +"There is at least a presumption that the vessel in which the man +or men are is a sailing-ship. It looks as if they always send +their singular warning or token before them when starting upon +their mission. You see how quickly the deed followed the sign +when it came from Dundee. If they had come from Pondicherry in a +steamer they would have arrived almost as soon as their letter. +But, as a matter of fact, seven weeks elapsed. I think that those +seven weeks represented the difference between the mail-boat which +brought the letter and the sailing vessel which brought the +writer." + +"It is possible." + +"More than that. It is probable. And now you see the deadly +urgency of this new case, and why I urged young Openshaw to +caution. The blow has always fallen at the end of the time which +it would take the senders to travel the distance. But this one +comes from London, and therefore we cannot count upon delay." + +"Good God!" I cried. "What can it mean, this relentless +persecution?" + +"The papers which Openshaw carried are obviously of vital +importance to the person or persons in the sailing-ship. I think +that it is quite clear that there must be more than one of them. +A single man could not have carried out two deaths in such a way +as to deceive a coroner's jury. There must have been several in +it, and they must have been men of resource and determination. +Their papers they mean to have, be the holder of them who it may. +In this way you see K. K. K. ceases to be the initials of an +individual and becomes the badge of a society." + +"But of what society?" + +"Have you never--" said Sherlock Holmes, bending forward and +sinking his voice--"have you never heard of the Ku Klux Klan?" + +"I never have." + +Holmes turned over the leaves of the book upon his knee. "Here it +is," said he presently: + +"'Ku Klux Klan. A name derived from the fanciful resemblance to +the sound produced by cocking a rifle. This terrible secret +society was formed by some ex-Confederate soldiers in the +Southern states after the Civil War, and it rapidly formed local +branches in different parts of the country, notably in Tennessee, +Louisiana, the Carolinas, Georgia, and Florida. Its power was +used for political purposes, principally for the terrorising of +the negro voters and the murdering and driving from the country +of those who were opposed to its views. Its outrages were usually +preceded by a warning sent to the marked man in some fantastic +but generally recognised shape--a sprig of oak-leaves in some +parts, melon seeds or orange pips in others. On receiving this +the victim might either openly abjure his former ways, or might +fly from the country. If he braved the matter out, death would +unfailingly come upon him, and usually in some strange and +unforeseen manner. So perfect was the organisation of the +society, and so systematic its methods, that there is hardly a +case upon record where any man succeeded in braving it with +impunity, or in which any of its outrages were traced home to the +perpetrators. For some years the organisation flourished in spite +of the efforts of the United States government and of the better +classes of the community in the South. Eventually, in the year +1869, the movement rather suddenly collapsed, although there have +been sporadic outbreaks of the same sort since that date.' + +"You will observe," said Holmes, laying down the volume, "that +the sudden breaking up of the society was coincident with the +disappearance of Openshaw from America with their papers. It may +well have been cause and effect. It is no wonder that he and his +family have some of the more implacable spirits upon their track. +You can understand that this register and diary may implicate +some of the first men in the South, and that there may be many +who will not sleep easy at night until it is recovered." + +"Then the page we have seen--" + +"Is such as we might expect. It ran, if I remember right, 'sent +the pips to A, B, and C'--that is, sent the society's warning to +them. Then there are successive entries that A and B cleared, or +left the country, and finally that C was visited, with, I fear, a +sinister result for C. Well, I think, Doctor, that we may let +some light into this dark place, and I believe that the only +chance young Openshaw has in the meantime is to do what I have +told him. There is nothing more to be said or to be done +to-night, so hand me over my violin and let us try to forget for +half an hour the miserable weather and the still more miserable +ways of our fellow-men." + + +It had cleared in the morning, and the sun was shining with a +subdued brightness through the dim veil which hangs over the +great city. Sherlock Holmes was already at breakfast when I came +down. + +"You will excuse me for not waiting for you," said he; "I have, I +foresee, a very busy day before me in looking into this case of +young Openshaw's." + +"What steps will you take?" I asked. + +"It will very much depend upon the results of my first inquiries. +I may have to go down to Horsham, after all." + +"You will not go there first?" + +"No, I shall commence with the City. Just ring the bell and the +maid will bring up your coffee." + +As I waited, I lifted the unopened newspaper from the table and +glanced my eye over it. It rested upon a heading which sent a +chill to my heart. + +"Holmes," I cried, "you are too late." + +"Ah!" said he, laying down his cup, "I feared as much. How was it +done?" He spoke calmly, but I could see that he was deeply moved. + +"My eye caught the name of Openshaw, and the heading 'Tragedy +Near Waterloo Bridge.' Here is the account: + +"Between nine and ten last night Police-Constable Cook, of the H +Division, on duty near Waterloo Bridge, heard a cry for help and +a splash in the water. The night, however, was extremely dark and +stormy, so that, in spite of the help of several passers-by, it +was quite impossible to effect a rescue. The alarm, however, was +given, and, by the aid of the water-police, the body was +eventually recovered. It proved to be that of a young gentleman +whose name, as it appears from an envelope which was found in his +pocket, was John Openshaw, and whose residence is near Horsham. +It is conjectured that he may have been hurrying down to catch +the last train from Waterloo Station, and that in his haste and +the extreme darkness he missed his path and walked over the edge +of one of the small landing-places for river steamboats. The body +exhibited no traces of violence, and there can be no doubt that +the deceased had been the victim of an unfortunate accident, +which should have the effect of calling the attention of the +authorities to the condition of the riverside landing-stages." + +We sat in silence for some minutes, Holmes more depressed and +shaken than I had ever seen him. + +"That hurts my pride, Watson," he said at last. "It is a petty +feeling, no doubt, but it hurts my pride. It becomes a personal +matter with me now, and, if God sends me health, I shall set my +hand upon this gang. That he should come to me for help, and that +I should send him away to his death--!" He sprang from his chair +and paced about the room in uncontrollable agitation, with a +flush upon his sallow cheeks and a nervous clasping and +unclasping of his long thin hands. + +"They must be cunning devils," he exclaimed at last. "How could +they have decoyed him down there? The Embankment is not on the +direct line to the station. The bridge, no doubt, was too +crowded, even on such a night, for their purpose. Well, Watson, +we shall see who will win in the long run. I am going out now!" + +"To the police?" + +"No; I shall be my own police. When I have spun the web they may +take the flies, but not before." + +All day I was engaged in my professional work, and it was late in +the evening before I returned to Baker Street. Sherlock Holmes +had not come back yet. It was nearly ten o'clock before he +entered, looking pale and worn. He walked up to the sideboard, +and tearing a piece from the loaf he devoured it voraciously, +washing it down with a long draught of water. + +"You are hungry," I remarked. + +"Starving. It had escaped my memory. I have had nothing since +breakfast." + +"Nothing?" + +"Not a bite. I had no time to think of it." + +"And how have you succeeded?" + +"Well." + +"You have a clue?" + +"I have them in the hollow of my hand. Young Openshaw shall not +long remain unavenged. Why, Watson, let us put their own devilish +trade-mark upon them. It is well thought of!" + +"What do you mean?" + +He took an orange from the cupboard, and tearing it to pieces he +squeezed out the pips upon the table. Of these he took five and +thrust them into an envelope. On the inside of the flap he wrote +"S. H. for J. O." Then he sealed it and addressed it to "Captain +James Calhoun, Barque 'Lone Star,' Savannah, Georgia." + +"That will await him when he enters port," said he, chuckling. +"It may give him a sleepless night. He will find it as sure a +precursor of his fate as Openshaw did before him." + +"And who is this Captain Calhoun?" + +"The leader of the gang. I shall have the others, but he first." + +"How did you trace it, then?" + +He took a large sheet of paper from his pocket, all covered with +dates and names. + +"I have spent the whole day," said he, "over Lloyd's registers +and files of the old papers, following the future career of every +vessel which touched at Pondicherry in January and February in +'83. There were thirty-six ships of fair tonnage which were +reported there during those months. Of these, one, the 'Lone Star,' +instantly attracted my attention, since, although it was reported +as having cleared from London, the name is that which is given to +one of the states of the Union." + +"Texas, I think." + +"I was not and am not sure which; but I knew that the ship must +have an American origin." + +"What then?" + +"I searched the Dundee records, and when I found that the barque +'Lone Star' was there in January, '85, my suspicion became a +certainty. I then inquired as to the vessels which lay at present +in the port of London." + +"Yes?" + +"The 'Lone Star' had arrived here last week. I went down to the +Albert Dock and found that she had been taken down the river by +the early tide this morning, homeward bound to Savannah. I wired +to Gravesend and learned that she had passed some time ago, and +as the wind is easterly I have no doubt that she is now past the +Goodwins and not very far from the Isle of Wight." + +"What will you do, then?" + +"Oh, I have my hand upon him. He and the two mates, are as I +learn, the only native-born Americans in the ship. The others are +Finns and Germans. I know, also, that they were all three away +from the ship last night. I had it from the stevedore who has +been loading their cargo. By the time that their sailing-ship +reaches Savannah the mail-boat will have carried this letter, and +the cable will have informed the police of Savannah that these +three gentlemen are badly wanted here upon a charge of murder." + +There is ever a flaw, however, in the best laid of human plans, +and the murderers of John Openshaw were never to receive the +orange pips which would show them that another, as cunning and as +resolute as themselves, was upon their track. Very long and very +severe were the equinoctial gales that year. We waited long for +news of the "Lone Star" of Savannah, but none ever reached us. We +did at last hear that somewhere far out in the Atlantic a +shattered stern-post of a boat was seen swinging in the trough +of a wave, with the letters "L. S." carved upon it, and that is +all which we shall ever know of the fate of the "Lone Star." + + + +ADVENTURE VI. THE MAN WITH THE TWISTED LIP + +Isa Whitney, brother of the late Elias Whitney, D.D., Principal +of the Theological College of St. George's, was much addicted to +opium. The habit grew upon him, as I understand, from some +foolish freak when he was at college; for having read De +Quincey's description of his dreams and sensations, he had +drenched his tobacco with laudanum in an attempt to produce the +same effects. He found, as so many more have done, that the +practice is easier to attain than to get rid of, and for many +years he continued to be a slave to the drug, an object of +mingled horror and pity to his friends and relatives. I can see +him now, with yellow, pasty face, drooping lids, and pin-point +pupils, all huddled in a chair, the wreck and ruin of a noble +man. + +One night--it was in June, '89--there came a ring to my bell, +about the hour when a man gives his first yawn and glances at the +clock. I sat up in my chair, and my wife laid her needle-work +down in her lap and made a little face of disappointment. + +"A patient!" said she. "You'll have to go out." + +I groaned, for I was newly come back from a weary day. + +We heard the door open, a few hurried words, and then quick steps +upon the linoleum. Our own door flew open, and a lady, clad in +some dark-coloured stuff, with a black veil, entered the room. + +"You will excuse my calling so late," she began, and then, +suddenly losing her self-control, she ran forward, threw her arms +about my wife's neck, and sobbed upon her shoulder. "Oh, I'm in +such trouble!" she cried; "I do so want a little help." + +"Why," said my wife, pulling up her veil, "it is Kate Whitney. +How you startled me, Kate! I had not an idea who you were when +you came in." + +"I didn't know what to do, so I came straight to you." That was +always the way. Folk who were in grief came to my wife like birds +to a light-house. + +"It was very sweet of you to come. Now, you must have some wine +and water, and sit here comfortably and tell us all about it. Or +should you rather that I sent James off to bed?" + +"Oh, no, no! I want the doctor's advice and help, too. It's about +Isa. He has not been home for two days. I am so frightened about +him!" + +It was not the first time that she had spoken to us of her +husband's trouble, to me as a doctor, to my wife as an old friend +and school companion. We soothed and comforted her by such words +as we could find. Did she know where her husband was? Was it +possible that we could bring him back to her? + +It seems that it was. She had the surest information that of late +he had, when the fit was on him, made use of an opium den in the +farthest east of the City. Hitherto his orgies had always been +confined to one day, and he had come back, twitching and +shattered, in the evening. But now the spell had been upon him +eight-and-forty hours, and he lay there, doubtless among the +dregs of the docks, breathing in the poison or sleeping off the +effects. There he was to be found, she was sure of it, at the Bar +of Gold, in Upper Swandam Lane. But what was she to do? How could +she, a young and timid woman, make her way into such a place and +pluck her husband out from among the ruffians who surrounded him? + +There was the case, and of course there was but one way out of +it. Might I not escort her to this place? And then, as a second +thought, why should she come at all? I was Isa Whitney's medical +adviser, and as such I had influence over him. I could manage it +better if I were alone. I promised her on my word that I would +send him home in a cab within two hours if he were indeed at the +address which she had given me. And so in ten minutes I had left +my armchair and cheery sitting-room behind me, and was speeding +eastward in a hansom on a strange errand, as it seemed to me at +the time, though the future only could show how strange it was to +be. + +But there was no great difficulty in the first stage of my +adventure. Upper Swandam Lane is a vile alley lurking behind the +high wharves which line the north side of the river to the east +of London Bridge. Between a slop-shop and a gin-shop, approached +by a steep flight of steps leading down to a black gap like the +mouth of a cave, I found the den of which I was in search. +Ordering my cab to wait, I passed down the steps, worn hollow in +the centre by the ceaseless tread of drunken feet; and by the +light of a flickering oil-lamp above the door I found the latch +and made my way into a long, low room, thick and heavy with the +brown opium smoke, and terraced with wooden berths, like the +forecastle of an emigrant ship. + +Through the gloom one could dimly catch a glimpse of bodies lying +in strange fantastic poses, bowed shoulders, bent knees, heads +thrown back, and chins pointing upward, with here and there a +dark, lack-lustre eye turned upon the newcomer. Out of the black +shadows there glimmered little red circles of light, now bright, +now faint, as the burning poison waxed or waned in the bowls of +the metal pipes. The most lay silent, but some muttered to +themselves, and others talked together in a strange, low, +monotonous voice, their conversation coming in gushes, and then +suddenly tailing off into silence, each mumbling out his own +thoughts and paying little heed to the words of his neighbour. At +the farther end was a small brazier of burning charcoal, beside +which on a three-legged wooden stool there sat a tall, thin old +man, with his jaw resting upon his two fists, and his elbows upon +his knees, staring into the fire. + +As I entered, a sallow Malay attendant had hurried up with a pipe +for me and a supply of the drug, beckoning me to an empty berth. + +"Thank you. I have not come to stay," said I. "There is a friend +of mine here, Mr. Isa Whitney, and I wish to speak with him." + +There was a movement and an exclamation from my right, and +peering through the gloom, I saw Whitney, pale, haggard, and +unkempt, staring out at me. + +"My God! It's Watson," said he. He was in a pitiable state of +reaction, with every nerve in a twitter. "I say, Watson, what +o'clock is it?" + +"Nearly eleven." + +"Of what day?" + +"Of Friday, June 19th." + +"Good heavens! I thought it was Wednesday. It is Wednesday. What +d'you want to frighten a chap for?" He sank his face onto his +arms and began to sob in a high treble key. + +"I tell you that it is Friday, man. Your wife has been waiting +this two days for you. You should be ashamed of yourself!" + +"So I am. But you've got mixed, Watson, for I have only been here +a few hours, three pipes, four pipes--I forget how many. But I'll +go home with you. I wouldn't frighten Kate--poor little Kate. +Give me your hand! Have you a cab?" + +"Yes, I have one waiting." + +"Then I shall go in it. But I must owe something. Find what I +owe, Watson. I am all off colour. I can do nothing for myself." + +I walked down the narrow passage between the double row of +sleepers, holding my breath to keep out the vile, stupefying +fumes of the drug, and looking about for the manager. As I passed +the tall man who sat by the brazier I felt a sudden pluck at my +skirt, and a low voice whispered, "Walk past me, and then look +back at me." The words fell quite distinctly upon my ear. I +glanced down. They could only have come from the old man at my +side, and yet he sat now as absorbed as ever, very thin, very +wrinkled, bent with age, an opium pipe dangling down from between +his knees, as though it had dropped in sheer lassitude from his +fingers. I took two steps forward and looked back. It took all my +self-control to prevent me from breaking out into a cry of +astonishment. He had turned his back so that none could see him +but I. His form had filled out, his wrinkles were gone, the dull +eyes had regained their fire, and there, sitting by the fire and +grinning at my surprise, was none other than Sherlock Holmes. He +made a slight motion to me to approach him, and instantly, as he +turned his face half round to the company once more, subsided +into a doddering, loose-lipped senility. + +"Holmes!" I whispered, "what on earth are you doing in this den?" + +"As low as you can," he answered; "I have excellent ears. If you +would have the great kindness to get rid of that sottish friend +of yours I should be exceedingly glad to have a little talk with +you." + +"I have a cab outside." + +"Then pray send him home in it. You may safely trust him, for he +appears to be too limp to get into any mischief. I should +recommend you also to send a note by the cabman to your wife to +say that you have thrown in your lot with me. If you will wait +outside, I shall be with you in five minutes." + +It was difficult to refuse any of Sherlock Holmes' requests, for +they were always so exceedingly definite, and put forward with +such a quiet air of mastery. I felt, however, that when Whitney +was once confined in the cab my mission was practically +accomplished; and for the rest, I could not wish anything better +than to be associated with my friend in one of those singular +adventures which were the normal condition of his existence. In a +few minutes I had written my note, paid Whitney's bill, led him +out to the cab, and seen him driven through the darkness. In a +very short time a decrepit figure had emerged from the opium den, +and I was walking down the street with Sherlock Holmes. For two +streets he shuffled along with a bent back and an uncertain foot. +Then, glancing quickly round, he straightened himself out and +burst into a hearty fit of laughter. + +"I suppose, Watson," said he, "that you imagine that I have added +opium-smoking to cocaine injections, and all the other little +weaknesses on which you have favoured me with your medical +views." + +"I was certainly surprised to find you there." + +"But not more so than I to find you." + +"I came to find a friend." + +"And I to find an enemy." + +"An enemy?" + +"Yes; one of my natural enemies, or, shall I say, my natural +prey. Briefly, Watson, I am in the midst of a very remarkable +inquiry, and I have hoped to find a clue in the incoherent +ramblings of these sots, as I have done before now. Had I been +recognised in that den my life would not have been worth an +hour's purchase; for I have used it before now for my own +purposes, and the rascally Lascar who runs it has sworn to have +vengeance upon me. There is a trap-door at the back of that +building, near the corner of Paul's Wharf, which could tell some +strange tales of what has passed through it upon the moonless +nights." + +"What! You do not mean bodies?" + +"Ay, bodies, Watson. We should be rich men if we had 1000 pounds +for every poor devil who has been done to death in that den. It +is the vilest murder-trap on the whole riverside, and I fear that +Neville St. Clair has entered it never to leave it more. But our +trap should be here." He put his two forefingers between his +teeth and whistled shrilly--a signal which was answered by a +similar whistle from the distance, followed shortly by the rattle +of wheels and the clink of horses' hoofs. + +"Now, Watson," said Holmes, as a tall dog-cart dashed up through +the gloom, throwing out two golden tunnels of yellow light from +its side lanterns. "You'll come with me, won't you?" + +"If I can be of use." + +"Oh, a trusty comrade is always of use; and a chronicler still +more so. My room at The Cedars is a double-bedded one." + +"The Cedars?" + +"Yes; that is Mr. St. Clair's house. I am staying there while I +conduct the inquiry." + +"Where is it, then?" + +"Near Lee, in Kent. We have a seven-mile drive before us." + +"But I am all in the dark." + +"Of course you are. You'll know all about it presently. Jump up +here. All right, John; we shall not need you. Here's half a +crown. Look out for me to-morrow, about eleven. Give her her +head. So long, then!" + +He flicked the horse with his whip, and we dashed away through +the endless succession of sombre and deserted streets, which +widened gradually, until we were flying across a broad +balustraded bridge, with the murky river flowing sluggishly +beneath us. Beyond lay another dull wilderness of bricks and +mortar, its silence broken only by the heavy, regular footfall of +the policeman, or the songs and shouts of some belated party of +revellers. A dull wrack was drifting slowly across the sky, and a +star or two twinkled dimly here and there through the rifts of +the clouds. Holmes drove in silence, with his head sunk upon his +breast, and the air of a man who is lost in thought, while I sat +beside him, curious to learn what this new quest might be which +seemed to tax his powers so sorely, and yet afraid to break in +upon the current of his thoughts. We had driven several miles, +and were beginning to get to the fringe of the belt of suburban +villas, when he shook himself, shrugged his shoulders, and lit up +his pipe with the air of a man who has satisfied himself that he +is acting for the best. + +"You have a grand gift of silence, Watson," said he. "It makes +you quite invaluable as a companion. 'Pon my word, it is a great +thing for me to have someone to talk to, for my own thoughts are +not over-pleasant. I was wondering what I should say to this dear +little woman to-night when she meets me at the door." + +"You forget that I know nothing about it." + +"I shall just have time to tell you the facts of the case before +we get to Lee. It seems absurdly simple, and yet, somehow I can +get nothing to go upon. There's plenty of thread, no doubt, but I +can't get the end of it into my hand. Now, I'll state the case +clearly and concisely to you, Watson, and maybe you can see a +spark where all is dark to me." + +"Proceed, then." + +"Some years ago--to be definite, in May, 1884--there came to Lee +a gentleman, Neville St. Clair by name, who appeared to have +plenty of money. He took a large villa, laid out the grounds very +nicely, and lived generally in good style. By degrees he made +friends in the neighbourhood, and in 1887 he married the daughter +of a local brewer, by whom he now has two children. He had no +occupation, but was interested in several companies and went into +town as a rule in the morning, returning by the 5:14 from Cannon +Street every night. Mr. St. Clair is now thirty-seven years of +age, is a man of temperate habits, a good husband, a very +affectionate father, and a man who is popular with all who know +him. I may add that his whole debts at the present moment, as far +as we have been able to ascertain, amount to 88 pounds 10s., while +he has 220 pounds standing to his credit in the Capital and +Counties Bank. There is no reason, therefore, to think that money +troubles have been weighing upon his mind. + +"Last Monday Mr. Neville St. Clair went into town rather earlier +than usual, remarking before he started that he had two important +commissions to perform, and that he would bring his little boy +home a box of bricks. Now, by the merest chance, his wife +received a telegram upon this same Monday, very shortly after his +departure, to the effect that a small parcel of considerable +value which she had been expecting was waiting for her at the +offices of the Aberdeen Shipping Company. Now, if you are well up +in your London, you will know that the office of the company is +in Fresno Street, which branches out of Upper Swandam Lane, where +you found me to-night. Mrs. St. Clair had her lunch, started for +the City, did some shopping, proceeded to the company's office, +got her packet, and found herself at exactly 4:35 walking through +Swandam Lane on her way back to the station. Have you followed me +so far?" + +"It is very clear." + +"If you remember, Monday was an exceedingly hot day, and Mrs. St. +Clair walked slowly, glancing about in the hope of seeing a cab, +as she did not like the neighbourhood in which she found herself. +While she was walking in this way down Swandam Lane, she suddenly +heard an ejaculation or cry, and was struck cold to see her +husband looking down at her and, as it seemed to her, beckoning +to her from a second-floor window. The window was open, and she +distinctly saw his face, which she describes as being terribly +agitated. He waved his hands frantically to her, and then +vanished from the window so suddenly that it seemed to her that +he had been plucked back by some irresistible force from behind. +One singular point which struck her quick feminine eye was that +although he wore some dark coat, such as he had started to town +in, he had on neither collar nor necktie. + +"Convinced that something was amiss with him, she rushed down the +steps--for the house was none other than the opium den in which +you found me to-night--and running through the front room she +attempted to ascend the stairs which led to the first floor. At +the foot of the stairs, however, she met this Lascar scoundrel of +whom I have spoken, who thrust her back and, aided by a Dane, who +acts as assistant there, pushed her out into the street. Filled +with the most maddening doubts and fears, she rushed down the +lane and, by rare good-fortune, met in Fresno Street a number of +constables with an inspector, all on their way to their beat. The +inspector and two men accompanied her back, and in spite of the +continued resistance of the proprietor, they made their way to +the room in which Mr. St. Clair had last been seen. There was no +sign of him there. In fact, in the whole of that floor there was +no one to be found save a crippled wretch of hideous aspect, who, +it seems, made his home there. Both he and the Lascar stoutly +swore that no one else had been in the front room during the +afternoon. So determined was their denial that the inspector was +staggered, and had almost come to believe that Mrs. St. Clair had +been deluded when, with a cry, she sprang at a small deal box +which lay upon the table and tore the lid from it. Out there fell +a cascade of children's bricks. It was the toy which he had +promised to bring home. + +"This discovery, and the evident confusion which the cripple +showed, made the inspector realise that the matter was serious. +The rooms were carefully examined, and results all pointed to an +abominable crime. The front room was plainly furnished as a +sitting-room and led into a small bedroom, which looked out upon +the back of one of the wharves. Between the wharf and the bedroom +window is a narrow strip, which is dry at low tide but is covered +at high tide with at least four and a half feet of water. The +bedroom window was a broad one and opened from below. On +examination traces of blood were to be seen upon the windowsill, +and several scattered drops were visible upon the wooden floor of +the bedroom. Thrust away behind a curtain in the front room were +all the clothes of Mr. Neville St. Clair, with the exception of +his coat. His boots, his socks, his hat, and his watch--all were +there. There were no signs of violence upon any of these +garments, and there were no other traces of Mr. Neville St. +Clair. Out of the window he must apparently have gone for no +other exit could be discovered, and the ominous bloodstains upon +the sill gave little promise that he could save himself by +swimming, for the tide was at its very highest at the moment of +the tragedy. + +"And now as to the villains who seemed to be immediately +implicated in the matter. The Lascar was known to be a man of the +vilest antecedents, but as, by Mrs. St. Clair's story, he was +known to have been at the foot of the stair within a very few +seconds of her husband's appearance at the window, he could +hardly have been more than an accessory to the crime. His defence +was one of absolute ignorance, and he protested that he had no +knowledge as to the doings of Hugh Boone, his lodger, and that he +could not account in any way for the presence of the missing +gentleman's clothes. + +"So much for the Lascar manager. Now for the sinister cripple who +lives upon the second floor of the opium den, and who was +certainly the last human being whose eyes rested upon Neville St. +Clair. His name is Hugh Boone, and his hideous face is one which +is familiar to every man who goes much to the City. He is a +professional beggar, though in order to avoid the police +regulations he pretends to a small trade in wax vestas. Some +little distance down Threadneedle Street, upon the left-hand +side, there is, as you may have remarked, a small angle in the +wall. Here it is that this creature takes his daily seat, +cross-legged with his tiny stock of matches on his lap, and as he +is a piteous spectacle a small rain of charity descends into the +greasy leather cap which lies upon the pavement beside him. I +have watched the fellow more than once before ever I thought of +making his professional acquaintance, and I have been surprised +at the harvest which he has reaped in a short time. His +appearance, you see, is so remarkable that no one can pass him +without observing him. A shock of orange hair, a pale face +disfigured by a horrible scar, which, by its contraction, has +turned up the outer edge of his upper lip, a bulldog chin, and a +pair of very penetrating dark eyes, which present a singular +contrast to the colour of his hair, all mark him out from amid +the common crowd of mendicants and so, too, does his wit, for he +is ever ready with a reply to any piece of chaff which may be +thrown at him by the passers-by. This is the man whom we now +learn to have been the lodger at the opium den, and to have been +the last man to see the gentleman of whom we are in quest." + +"But a cripple!" said I. "What could he have done single-handed +against a man in the prime of life?" + +"He is a cripple in the sense that he walks with a limp; but in +other respects he appears to be a powerful and well-nurtured man. +Surely your medical experience would tell you, Watson, that +weakness in one limb is often compensated for by exceptional +strength in the others." + +"Pray continue your narrative." + +"Mrs. St. Clair had fainted at the sight of the blood upon the +window, and she was escorted home in a cab by the police, as her +presence could be of no help to them in their investigations. +Inspector Barton, who had charge of the case, made a very careful +examination of the premises, but without finding anything which +threw any light upon the matter. One mistake had been made in not +arresting Boone instantly, as he was allowed some few minutes +during which he might have communicated with his friend the +Lascar, but this fault was soon remedied, and he was seized and +searched, without anything being found which could incriminate +him. There were, it is true, some blood-stains upon his right +shirt-sleeve, but he pointed to his ring-finger, which had been +cut near the nail, and explained that the bleeding came from +there, adding that he had been to the window not long before, and +that the stains which had been observed there came doubtless from +the same source. He denied strenuously having ever seen Mr. +Neville St. Clair and swore that the presence of the clothes in +his room was as much a mystery to him as to the police. As to +Mrs. St. Clair's assertion that she had actually seen her husband +at the window, he declared that she must have been either mad or +dreaming. He was removed, loudly protesting, to the +police-station, while the inspector remained upon the premises in +the hope that the ebbing tide might afford some fresh clue. + +"And it did, though they hardly found upon the mud-bank what they +had feared to find. It was Neville St. Clair's coat, and not +Neville St. Clair, which lay uncovered as the tide receded. And +what do you think they found in the pockets?" + +"I cannot imagine." + +"No, I don't think you would guess. Every pocket stuffed with +pennies and half-pennies--421 pennies and 270 half-pennies. It +was no wonder that it had not been swept away by the tide. But a +human body is a different matter. There is a fierce eddy between +the wharf and the house. It seemed likely enough that the +weighted coat had remained when the stripped body had been sucked +away into the river." + +"But I understand that all the other clothes were found in the +room. Would the body be dressed in a coat alone?" + +"No, sir, but the facts might be met speciously enough. Suppose +that this man Boone had thrust Neville St. Clair through the +window, there is no human eye which could have seen the deed. +What would he do then? It would of course instantly strike him +that he must get rid of the tell-tale garments. He would seize +the coat, then, and be in the act of throwing it out, when it +would occur to him that it would swim and not sink. He has little +time, for he has heard the scuffle downstairs when the wife tried +to force her way up, and perhaps he has already heard from his +Lascar confederate that the police are hurrying up the street. +There is not an instant to be lost. He rushes to some secret +hoard, where he has accumulated the fruits of his beggary, and he +stuffs all the coins upon which he can lay his hands into the +pockets to make sure of the coat's sinking. He throws it out, and +would have done the same with the other garments had not he heard +the rush of steps below, and only just had time to close the +window when the police appeared." + +"It certainly sounds feasible." + +"Well, we will take it as a working hypothesis for want of a +better. Boone, as I have told you, was arrested and taken to the +station, but it could not be shown that there had ever before +been anything against him. He had for years been known as a +professional beggar, but his life appeared to have been a very +quiet and innocent one. There the matter stands at present, and +the questions which have to be solved--what Neville St. Clair was +doing in the opium den, what happened to him when there, where is +he now, and what Hugh Boone had to do with his disappearance--are +all as far from a solution as ever. I confess that I cannot +recall any case within my experience which looked at the first +glance so simple and yet which presented such difficulties." + +While Sherlock Holmes had been detailing this singular series of +events, we had been whirling through the outskirts of the great +town until the last straggling houses had been left behind, and +we rattled along with a country hedge upon either side of us. +Just as he finished, however, we drove through two scattered +villages, where a few lights still glimmered in the windows. + +"We are on the outskirts of Lee," said my companion. "We have +touched on three English counties in our short drive, starting in +Middlesex, passing over an angle of Surrey, and ending in Kent. +See that light among the trees? That is The Cedars, and beside +that lamp sits a woman whose anxious ears have already, I have +little doubt, caught the clink of our horse's feet." + +"But why are you not conducting the case from Baker Street?" I +asked. + +"Because there are many inquiries which must be made out here. +Mrs. St. Clair has most kindly put two rooms at my disposal, and +you may rest assured that she will have nothing but a welcome for +my friend and colleague. I hate to meet her, Watson, when I have +no news of her husband. Here we are. Whoa, there, whoa!" + +We had pulled up in front of a large villa which stood within its +own grounds. A stable-boy had run out to the horse's head, and +springing down, I followed Holmes up the small, winding +gravel-drive which led to the house. As we approached, the door +flew open, and a little blonde woman stood in the opening, clad +in some sort of light mousseline de soie, with a touch of fluffy +pink chiffon at her neck and wrists. She stood with her figure +outlined against the flood of light, one hand upon the door, one +half-raised in her eagerness, her body slightly bent, her head +and face protruded, with eager eyes and parted lips, a standing +question. + +"Well?" she cried, "well?" And then, seeing that there were two +of us, she gave a cry of hope which sank into a groan as she saw +that my companion shook his head and shrugged his shoulders. + +"No good news?" + +"None." + +"No bad?" + +"No." + +"Thank God for that. But come in. You must be weary, for you have +had a long day." + +"This is my friend, Dr. Watson. He has been of most vital use to +me in several of my cases, and a lucky chance has made it +possible for me to bring him out and associate him with this +investigation." + +"I am delighted to see you," said she, pressing my hand warmly. +"You will, I am sure, forgive anything that may be wanting in our +arrangements, when you consider the blow which has come so +suddenly upon us." + +"My dear madam," said I, "I am an old campaigner, and if I were +not I can very well see that no apology is needed. If I can be of +any assistance, either to you or to my friend here, I shall be +indeed happy." + +"Now, Mr. Sherlock Holmes," said the lady as we entered a +well-lit dining-room, upon the table of which a cold supper had +been laid out, "I should very much like to ask you one or two +plain questions, to which I beg that you will give a plain +answer." + +"Certainly, madam." + +"Do not trouble about my feelings. I am not hysterical, nor given +to fainting. I simply wish to hear your real, real opinion." + +"Upon what point?" + +"In your heart of hearts, do you think that Neville is alive?" + +Sherlock Holmes seemed to be embarrassed by the question. +"Frankly, now!" she repeated, standing upon the rug and looking +keenly down at him as he leaned back in a basket-chair. + +"Frankly, then, madam, I do not." + +"You think that he is dead?" + +"I do." + +"Murdered?" + +"I don't say that. Perhaps." + +"And on what day did he meet his death?" + +"On Monday." + +"Then perhaps, Mr. Holmes, you will be good enough to explain how +it is that I have received a letter from him to-day." + +Sherlock Holmes sprang out of his chair as if he had been +galvanised. + +"What!" he roared. + +"Yes, to-day." She stood smiling, holding up a little slip of +paper in the air. + +"May I see it?" + +"Certainly." + +He snatched it from her in his eagerness, and smoothing it out +upon the table he drew over the lamp and examined it intently. I +had left my chair and was gazing at it over his shoulder. The +envelope was a very coarse one and was stamped with the Gravesend +postmark and with the date of that very day, or rather of the day +before, for it was considerably after midnight. + +"Coarse writing," murmured Holmes. "Surely this is not your +husband's writing, madam." + +"No, but the enclosure is." + +"I perceive also that whoever addressed the envelope had to go +and inquire as to the address." + +"How can you tell that?" + +"The name, you see, is in perfectly black ink, which has dried +itself. The rest is of the greyish colour, which shows that +blotting-paper has been used. If it had been written straight +off, and then blotted, none would be of a deep black shade. This +man has written the name, and there has then been a pause before +he wrote the address, which can only mean that he was not +familiar with it. It is, of course, a trifle, but there is +nothing so important as trifles. Let us now see the letter. Ha! +there has been an enclosure here!" + +"Yes, there was a ring. His signet-ring." + +"And you are sure that this is your husband's hand?" + +"One of his hands." + +"One?" + +"His hand when he wrote hurriedly. It is very unlike his usual +writing, and yet I know it well." + +"'Dearest do not be frightened. All will come well. There is a +huge error which it may take some little time to rectify. +Wait in patience.--NEVILLE.' Written in pencil upon the fly-leaf +of a book, octavo size, no water-mark. Hum! Posted to-day in +Gravesend by a man with a dirty thumb. Ha! And the flap has been +gummed, if I am not very much in error, by a person who had been +chewing tobacco. And you have no doubt that it is your husband's +hand, madam?" + +"None. Neville wrote those words." + +"And they were posted to-day at Gravesend. Well, Mrs. St. Clair, +the clouds lighten, though I should not venture to say that the +danger is over." + +"But he must be alive, Mr. Holmes." + +"Unless this is a clever forgery to put us on the wrong scent. +The ring, after all, proves nothing. It may have been taken from +him." + +"No, no; it is, it is his very own writing!" + +"Very well. It may, however, have been written on Monday and only +posted to-day." + +"That is possible." + +"If so, much may have happened between." + +"Oh, you must not discourage me, Mr. Holmes. I know that all is +well with him. There is so keen a sympathy between us that I +should know if evil came upon him. On the very day that I saw him +last he cut himself in the bedroom, and yet I in the dining-room +rushed upstairs instantly with the utmost certainty that +something had happened. Do you think that I would respond to such +a trifle and yet be ignorant of his death?" + +"I have seen too much not to know that the impression of a woman +may be more valuable than the conclusion of an analytical +reasoner. And in this letter you certainly have a very strong +piece of evidence to corroborate your view. But if your husband +is alive and able to write letters, why should he remain away +from you?" + +"I cannot imagine. It is unthinkable." + +"And on Monday he made no remarks before leaving you?" + +"No." + +"And you were surprised to see him in Swandam Lane?" + +"Very much so." + +"Was the window open?" + +"Yes." + +"Then he might have called to you?" + +"He might." + +"He only, as I understand, gave an inarticulate cry?" + +"Yes." + +"A call for help, you thought?" + +"Yes. He waved his hands." + +"But it might have been a cry of surprise. Astonishment at the +unexpected sight of you might cause him to throw up his hands?" + +"It is possible." + +"And you thought he was pulled back?" + +"He disappeared so suddenly." + +"He might have leaped back. You did not see anyone else in the +room?" + +"No, but this horrible man confessed to having been there, and +the Lascar was at the foot of the stairs." + +"Quite so. Your husband, as far as you could see, had his +ordinary clothes on?" + +"But without his collar or tie. I distinctly saw his bare +throat." + +"Had he ever spoken of Swandam Lane?" + +"Never." + +"Had he ever showed any signs of having taken opium?" + +"Never." + +"Thank you, Mrs. St. Clair. Those are the principal points about +which I wished to be absolutely clear. We shall now have a little +supper and then retire, for we may have a very busy day +to-morrow." + +A large and comfortable double-bedded room had been placed at our +disposal, and I was quickly between the sheets, for I was weary +after my night of adventure. Sherlock Holmes was a man, however, +who, when he had an unsolved problem upon his mind, would go for +days, and even for a week, without rest, turning it over, +rearranging his facts, looking at it from every point of view +until he had either fathomed it or convinced himself that his +data were insufficient. It was soon evident to me that he was now +preparing for an all-night sitting. He took off his coat and +waistcoat, put on a large blue dressing-gown, and then wandered +about the room collecting pillows from his bed and cushions from +the sofa and armchairs. With these he constructed a sort of +Eastern divan, upon which he perched himself cross-legged, with +an ounce of shag tobacco and a box of matches laid out in front +of him. In the dim light of the lamp I saw him sitting there, an +old briar pipe between his lips, his eyes fixed vacantly upon the +corner of the ceiling, the blue smoke curling up from him, +silent, motionless, with the light shining upon his strong-set +aquiline features. So he sat as I dropped off to sleep, and so he +sat when a sudden ejaculation caused me to wake up, and I found +the summer sun shining into the apartment. The pipe was still +between his lips, the smoke still curled upward, and the room was +full of a dense tobacco haze, but nothing remained of the heap of +shag which I had seen upon the previous night. + +"Awake, Watson?" he asked. + +"Yes." + +"Game for a morning drive?" + +"Certainly." + +"Then dress. No one is stirring yet, but I know where the +stable-boy sleeps, and we shall soon have the trap out." He +chuckled to himself as he spoke, his eyes twinkled, and he seemed +a different man to the sombre thinker of the previous night. + +As I dressed I glanced at my watch. It was no wonder that no one +was stirring. It was twenty-five minutes past four. I had hardly +finished when Holmes returned with the news that the boy was +putting in the horse. + +"I want to test a little theory of mine," said he, pulling on his +boots. "I think, Watson, that you are now standing in the +presence of one of the most absolute fools in Europe. I deserve +to be kicked from here to Charing Cross. But I think I have the +key of the affair now." + +"And where is it?" I asked, smiling. + +"In the bathroom," he answered. "Oh, yes, I am not joking," he +continued, seeing my look of incredulity. "I have just been +there, and I have taken it out, and I have got it in this +Gladstone bag. Come on, my boy, and we shall see whether it will +not fit the lock." + +We made our way downstairs as quietly as possible, and out into +the bright morning sunshine. In the road stood our horse and +trap, with the half-clad stable-boy waiting at the head. We both +sprang in, and away we dashed down the London Road. A few country +carts were stirring, bearing in vegetables to the metropolis, but +the lines of villas on either side were as silent and lifeless as +some city in a dream. + +"It has been in some points a singular case," said Holmes, +flicking the horse on into a gallop. "I confess that I have been +as blind as a mole, but it is better to learn wisdom late than +never to learn it at all." + +In town the earliest risers were just beginning to look sleepily +from their windows as we drove through the streets of the Surrey +side. Passing down the Waterloo Bridge Road we crossed over the +river, and dashing up Wellington Street wheeled sharply to the +right and found ourselves in Bow Street. Sherlock Holmes was well +known to the force, and the two constables at the door saluted +him. One of them held the horse's head while the other led us in. + +"Who is on duty?" asked Holmes. + +"Inspector Bradstreet, sir." + +"Ah, Bradstreet, how are you?" A tall, stout official had come +down the stone-flagged passage, in a peaked cap and frogged +jacket. "I wish to have a quiet word with you, Bradstreet." +"Certainly, Mr. Holmes. Step into my room here." It was a small, +office-like room, with a huge ledger upon the table, and a +telephone projecting from the wall. The inspector sat down at his +desk. + +"What can I do for you, Mr. Holmes?" + +"I called about that beggarman, Boone--the one who was charged +with being concerned in the disappearance of Mr. Neville St. +Clair, of Lee." + +"Yes. He was brought up and remanded for further inquiries." + +"So I heard. You have him here?" + +"In the cells." + +"Is he quiet?" + +"Oh, he gives no trouble. But he is a dirty scoundrel." + +"Dirty?" + +"Yes, it is all we can do to make him wash his hands, and his +face is as black as a tinker's. Well, when once his case has been +settled, he will have a regular prison bath; and I think, if you +saw him, you would agree with me that he needed it." + +"I should like to see him very much." + +"Would you? That is easily done. Come this way. You can leave +your bag." + +"No, I think that I'll take it." + +"Very good. Come this way, if you please." He led us down a +passage, opened a barred door, passed down a winding stair, and +brought us to a whitewashed corridor with a line of doors on each +side. + +"The third on the right is his," said the inspector. "Here it +is!" He quietly shot back a panel in the upper part of the door +and glanced through. + +"He is asleep," said he. "You can see him very well." + +We both put our eyes to the grating. The prisoner lay with his +face towards us, in a very deep sleep, breathing slowly and +heavily. He was a middle-sized man, coarsely clad as became his +calling, with a coloured shirt protruding through the rent in his +tattered coat. He was, as the inspector had said, extremely +dirty, but the grime which covered his face could not conceal its +repulsive ugliness. A broad wheal from an old scar ran right +across it from eye to chin, and by its contraction had turned up +one side of the upper lip, so that three teeth were exposed in a +perpetual snarl. A shock of very bright red hair grew low over +his eyes and forehead. + +"He's a beauty, isn't he?" said the inspector. + +"He certainly needs a wash," remarked Holmes. "I had an idea that +he might, and I took the liberty of bringing the tools with me." +He opened the Gladstone bag as he spoke, and took out, to my +astonishment, a very large bath-sponge. + +"He! he! You are a funny one," chuckled the inspector. + +"Now, if you will have the great goodness to open that door very +quietly, we will soon make him cut a much more respectable +figure." + +"Well, I don't know why not," said the inspector. "He doesn't +look a credit to the Bow Street cells, does he?" He slipped his +key into the lock, and we all very quietly entered the cell. The +sleeper half turned, and then settled down once more into a deep +slumber. Holmes stooped to the water-jug, moistened his sponge, +and then rubbed it twice vigorously across and down the +prisoner's face. + +"Let me introduce you," he shouted, "to Mr. Neville St. Clair, of +Lee, in the county of Kent." + +Never in my life have I seen such a sight. The man's face peeled +off under the sponge like the bark from a tree. Gone was the +coarse brown tint! Gone, too, was the horrid scar which had +seamed it across, and the twisted lip which had given the +repulsive sneer to the face! A twitch brought away the tangled +red hair, and there, sitting up in his bed, was a pale, +sad-faced, refined-looking man, black-haired and smooth-skinned, +rubbing his eyes and staring about him with sleepy bewilderment. +Then suddenly realising the exposure, he broke into a scream and +threw himself down with his face to the pillow. + +"Great heavens!" cried the inspector, "it is, indeed, the missing +man. I know him from the photograph." + +The prisoner turned with the reckless air of a man who abandons +himself to his destiny. "Be it so," said he. "And pray what am I +charged with?" + +"With making away with Mr. Neville St.-- Oh, come, you can't be +charged with that unless they make a case of attempted suicide of +it," said the inspector with a grin. "Well, I have been +twenty-seven years in the force, but this really takes the cake." + +"If I am Mr. Neville St. Clair, then it is obvious that no crime +has been committed, and that, therefore, I am illegally +detained." + +"No crime, but a very great error has been committed," said +Holmes. "You would have done better to have trusted your wife." + +"It was not the wife; it was the children," groaned the prisoner. +"God help me, I would not have them ashamed of their father. My +God! What an exposure! What can I do?" + +Sherlock Holmes sat down beside him on the couch and patted him +kindly on the shoulder. + +"If you leave it to a court of law to clear the matter up," said +he, "of course you can hardly avoid publicity. On the other hand, +if you convince the police authorities that there is no possible +case against you, I do not know that there is any reason that the +details should find their way into the papers. Inspector +Bradstreet would, I am sure, make notes upon anything which you +might tell us and submit it to the proper authorities. The case +would then never go into court at all." + +"God bless you!" cried the prisoner passionately. "I would have +endured imprisonment, ay, even execution, rather than have left +my miserable secret as a family blot to my children. + +"You are the first who have ever heard my story. My father was a +schoolmaster in Chesterfield, where I received an excellent +education. I travelled in my youth, took to the stage, and +finally became a reporter on an evening paper in London. One day +my editor wished to have a series of articles upon begging in the +metropolis, and I volunteered to supply them. There was the point +from which all my adventures started. It was only by trying +begging as an amateur that I could get the facts upon which to +base my articles. When an actor I had, of course, learned all the +secrets of making up, and had been famous in the green-room for +my skill. I took advantage now of my attainments. I painted my +face, and to make myself as pitiable as possible I made a good +scar and fixed one side of my lip in a twist by the aid of a +small slip of flesh-coloured plaster. Then with a red head of +hair, and an appropriate dress, I took my station in the business +part of the city, ostensibly as a match-seller but really as a +beggar. For seven hours I plied my trade, and when I returned +home in the evening I found to my surprise that I had received no +less than 26s. 4d. + +"I wrote my articles and thought little more of the matter until, +some time later, I backed a bill for a friend and had a writ +served upon me for 25 pounds. I was at my wit's end where to get +the money, but a sudden idea came to me. I begged a fortnight's +grace from the creditor, asked for a holiday from my employers, +and spent the time in begging in the City under my disguise. In +ten days I had the money and had paid the debt. + +"Well, you can imagine how hard it was to settle down to arduous +work at 2 pounds a week when I knew that I could earn as much in +a day by smearing my face with a little paint, laying my cap on +the ground, and sitting still. It was a long fight between my +pride and the money, but the dollars won at last, and I threw up +reporting and sat day after day in the corner which I had first +chosen, inspiring pity by my ghastly face and filling my pockets +with coppers. Only one man knew my secret. He was the keeper of a +low den in which I used to lodge in Swandam Lane, where I could +every morning emerge as a squalid beggar and in the evenings +transform myself into a well-dressed man about town. This fellow, +a Lascar, was well paid by me for his rooms, so that I knew that +my secret was safe in his possession. + +"Well, very soon I found that I was saving considerable sums of +money. I do not mean that any beggar in the streets of London +could earn 700 pounds a year--which is less than my average +takings--but I had exceptional advantages in my power of making +up, and also in a facility of repartee, which improved by +practice and made me quite a recognised character in the City. +All day a stream of pennies, varied by silver, poured in upon me, +and it was a very bad day in which I failed to take 2 pounds. + +"As I grew richer I grew more ambitious, took a house in the +country, and eventually married, without anyone having a +suspicion as to my real occupation. My dear wife knew that I had +business in the City. She little knew what. + +"Last Monday I had finished for the day and was dressing in my +room above the opium den when I looked out of my window and saw, +to my horror and astonishment, that my wife was standing in the +street, with her eyes fixed full upon me. I gave a cry of +surprise, threw up my arms to cover my face, and, rushing to my +confidant, the Lascar, entreated him to prevent anyone from +coming up to me. I heard her voice downstairs, but I knew that +she could not ascend. Swiftly I threw off my clothes, pulled on +those of a beggar, and put on my pigments and wig. Even a wife's +eyes could not pierce so complete a disguise. But then it +occurred to me that there might be a search in the room, and that +the clothes might betray me. I threw open the window, reopening +by my violence a small cut which I had inflicted upon myself in +the bedroom that morning. Then I seized my coat, which was +weighted by the coppers which I had just transferred to it from +the leather bag in which I carried my takings. I hurled it out of +the window, and it disappeared into the Thames. The other clothes +would have followed, but at that moment there was a rush of +constables up the stair, and a few minutes after I found, rather, +I confess, to my relief, that instead of being identified as Mr. +Neville St. Clair, I was arrested as his murderer. + +"I do not know that there is anything else for me to explain. I +was determined to preserve my disguise as long as possible, and +hence my preference for a dirty face. Knowing that my wife would +be terribly anxious, I slipped off my ring and confided it to the +Lascar at a moment when no constable was watching me, together +with a hurried scrawl, telling her that she had no cause to +fear." + +"That note only reached her yesterday," said Holmes. + +"Good God! What a week she must have spent!" + +"The police have watched this Lascar," said Inspector Bradstreet, +"and I can quite understand that he might find it difficult to +post a letter unobserved. Probably he handed it to some sailor +customer of his, who forgot all about it for some days." + +"That was it," said Holmes, nodding approvingly; "I have no doubt +of it. But have you never been prosecuted for begging?" + +"Many times; but what was a fine to me?" + +"It must stop here, however," said Bradstreet. "If the police are +to hush this thing up, there must be no more of Hugh Boone." + +"I have sworn it by the most solemn oaths which a man can take." + +"In that case I think that it is probable that no further steps +may be taken. But if you are found again, then all must come out. +I am sure, Mr. Holmes, that we are very much indebted to you for +having cleared the matter up. I wish I knew how you reach your +results." + +"I reached this one," said my friend, "by sitting upon five +pillows and consuming an ounce of shag. I think, Watson, that if +we drive to Baker Street we shall just be in time for breakfast." + + + +VII. THE ADVENTURE OF THE BLUE CARBUNCLE + +I had called upon my friend Sherlock Holmes upon the second +morning after Christmas, with the intention of wishing him the +compliments of the season. He was lounging upon the sofa in a +purple dressing-gown, a pipe-rack within his reach upon the +right, and a pile of crumpled morning papers, evidently newly +studied, near at hand. Beside the couch was a wooden chair, and +on the angle of the back hung a very seedy and disreputable +hard-felt hat, much the worse for wear, and cracked in several +places. A lens and a forceps lying upon the seat of the chair +suggested that the hat had been suspended in this manner for the +purpose of examination. + +"You are engaged," said I; "perhaps I interrupt you." + +"Not at all. I am glad to have a friend with whom I can discuss +my results. The matter is a perfectly trivial one"--he jerked his +thumb in the direction of the old hat--"but there are points in +connection with it which are not entirely devoid of interest and +even of instruction." + +I seated myself in his armchair and warmed my hands before his +crackling fire, for a sharp frost had set in, and the windows +were thick with the ice crystals. "I suppose," I remarked, "that, +homely as it looks, this thing has some deadly story linked on to +it--that it is the clue which will guide you in the solution of +some mystery and the punishment of some crime." + +"No, no. No crime," said Sherlock Holmes, laughing. "Only one of +those whimsical little incidents which will happen when you have +four million human beings all jostling each other within the +space of a few square miles. Amid the action and reaction of so +dense a swarm of humanity, every possible combination of events +may be expected to take place, and many a little problem will be +presented which may be striking and bizarre without being +criminal. We have already had experience of such." + +"So much so," I remarked, "that of the last six cases which I +have added to my notes, three have been entirely free of any +legal crime." + +"Precisely. You allude to my attempt to recover the Irene Adler +papers, to the singular case of Miss Mary Sutherland, and to the +adventure of the man with the twisted lip. Well, I have no doubt +that this small matter will fall into the same innocent category. +You know Peterson, the commissionaire?" + +"Yes." + +"It is to him that this trophy belongs." + +"It is his hat." + +"No, no, he found it. Its owner is unknown. I beg that you will +look upon it not as a battered billycock but as an intellectual +problem. And, first, as to how it came here. It arrived upon +Christmas morning, in company with a good fat goose, which is, I +have no doubt, roasting at this moment in front of Peterson's +fire. The facts are these: about four o'clock on Christmas +morning, Peterson, who, as you know, is a very honest fellow, was +returning from some small jollification and was making his way +homeward down Tottenham Court Road. In front of him he saw, in +the gaslight, a tallish man, walking with a slight stagger, and +carrying a white goose slung over his shoulder. As he reached the +corner of Goodge Street, a row broke out between this stranger +and a little knot of roughs. One of the latter knocked off the +man's hat, on which he raised his stick to defend himself and, +swinging it over his head, smashed the shop window behind him. +Peterson had rushed forward to protect the stranger from his +assailants; but the man, shocked at having broken the window, and +seeing an official-looking person in uniform rushing towards him, +dropped his goose, took to his heels, and vanished amid the +labyrinth of small streets which lie at the back of Tottenham +Court Road. The roughs had also fled at the appearance of +Peterson, so that he was left in possession of the field of +battle, and also of the spoils of victory in the shape of this +battered hat and a most unimpeachable Christmas goose." + +"Which surely he restored to their owner?" + +"My dear fellow, there lies the problem. It is true that 'For +Mrs. Henry Baker' was printed upon a small card which was tied to +the bird's left leg, and it is also true that the initials 'H. +B.' are legible upon the lining of this hat, but as there are +some thousands of Bakers, and some hundreds of Henry Bakers in +this city of ours, it is not easy to restore lost property to any +one of them." + +"What, then, did Peterson do?" + +"He brought round both hat and goose to me on Christmas morning, +knowing that even the smallest problems are of interest to me. +The goose we retained until this morning, when there were signs +that, in spite of the slight frost, it would be well that it +should be eaten without unnecessary delay. Its finder has carried +it off, therefore, to fulfil the ultimate destiny of a goose, +while I continue to retain the hat of the unknown gentleman who +lost his Christmas dinner." + +"Did he not advertise?" + +"No." + +"Then, what clue could you have as to his identity?" + +"Only as much as we can deduce." + +"From his hat?" + +"Precisely." + +"But you are joking. What can you gather from this old battered +felt?" + +"Here is my lens. You know my methods. What can you gather +yourself as to the individuality of the man who has worn this +article?" + +I took the tattered object in my hands and turned it over rather +ruefully. It was a very ordinary black hat of the usual round +shape, hard and much the worse for wear. The lining had been of +red silk, but was a good deal discoloured. There was no maker's +name; but, as Holmes had remarked, the initials "H. B." were +scrawled upon one side. It was pierced in the brim for a +hat-securer, but the elastic was missing. For the rest, it was +cracked, exceedingly dusty, and spotted in several places, +although there seemed to have been some attempt to hide the +discoloured patches by smearing them with ink. + +"I can see nothing," said I, handing it back to my friend. + +"On the contrary, Watson, you can see everything. You fail, +however, to reason from what you see. You are too timid in +drawing your inferences." + +"Then, pray tell me what it is that you can infer from this hat?" + +He picked it up and gazed at it in the peculiar introspective +fashion which was characteristic of him. "It is perhaps less +suggestive than it might have been," he remarked, "and yet there +are a few inferences which are very distinct, and a few others +which represent at least a strong balance of probability. That +the man was highly intellectual is of course obvious upon the +face of it, and also that he was fairly well-to-do within the +last three years, although he has now fallen upon evil days. He +had foresight, but has less now than formerly, pointing to a +moral retrogression, which, when taken with the decline of his +fortunes, seems to indicate some evil influence, probably drink, +at work upon him. This may account also for the obvious fact that +his wife has ceased to love him." + +"My dear Holmes!" + +"He has, however, retained some degree of self-respect," he +continued, disregarding my remonstrance. "He is a man who leads a +sedentary life, goes out little, is out of training entirely, is +middle-aged, has grizzled hair which he has had cut within the +last few days, and which he anoints with lime-cream. These are +the more patent facts which are to be deduced from his hat. Also, +by the way, that it is extremely improbable that he has gas laid +on in his house." + +"You are certainly joking, Holmes." + +"Not in the least. Is it possible that even now, when I give you +these results, you are unable to see how they are attained?" + +"I have no doubt that I am very stupid, but I must confess that I +am unable to follow you. For example, how did you deduce that +this man was intellectual?" + +For answer Holmes clapped the hat upon his head. It came right +over the forehead and settled upon the bridge of his nose. "It is +a question of cubic capacity," said he; "a man with so large a +brain must have something in it." + +"The decline of his fortunes, then?" + +"This hat is three years old. These flat brims curled at the edge +came in then. It is a hat of the very best quality. Look at the +band of ribbed silk and the excellent lining. If this man could +afford to buy so expensive a hat three years ago, and has had no +hat since, then he has assuredly gone down in the world." + +"Well, that is clear enough, certainly. But how about the +foresight and the moral retrogression?" + +Sherlock Holmes laughed. "Here is the foresight," said he putting +his finger upon the little disc and loop of the hat-securer. +"They are never sold upon hats. If this man ordered one, it is a +sign of a certain amount of foresight, since he went out of his +way to take this precaution against the wind. But since we see +that he has broken the elastic and has not troubled to replace +it, it is obvious that he has less foresight now than formerly, +which is a distinct proof of a weakening nature. On the other +hand, he has endeavoured to conceal some of these stains upon the +felt by daubing them with ink, which is a sign that he has not +entirely lost his self-respect." + +"Your reasoning is certainly plausible." + +"The further points, that he is middle-aged, that his hair is +grizzled, that it has been recently cut, and that he uses +lime-cream, are all to be gathered from a close examination of the +lower part of the lining. The lens discloses a large number of +hair-ends, clean cut by the scissors of the barber. They all +appear to be adhesive, and there is a distinct odour of +lime-cream. This dust, you will observe, is not the gritty, grey +dust of the street but the fluffy brown dust of the house, +showing that it has been hung up indoors most of the time, while +the marks of moisture upon the inside are proof positive that the +wearer perspired very freely, and could therefore, hardly be in +the best of training." + +"But his wife--you said that she had ceased to love him." + +"This hat has not been brushed for weeks. When I see you, my dear +Watson, with a week's accumulation of dust upon your hat, and +when your wife allows you to go out in such a state, I shall fear +that you also have been unfortunate enough to lose your wife's +affection." + +"But he might be a bachelor." + +"Nay, he was bringing home the goose as a peace-offering to his +wife. Remember the card upon the bird's leg." + +"You have an answer to everything. But how on earth do you deduce +that the gas is not laid on in his house?" + +"One tallow stain, or even two, might come by chance; but when I +see no less than five, I think that there can be little doubt +that the individual must be brought into frequent contact with +burning tallow--walks upstairs at night probably with his hat in +one hand and a guttering candle in the other. Anyhow, he never +got tallow-stains from a gas-jet. Are you satisfied?" + +"Well, it is very ingenious," said I, laughing; "but since, as +you said just now, there has been no crime committed, and no harm +done save the loss of a goose, all this seems to be rather a +waste of energy." + +Sherlock Holmes had opened his mouth to reply, when the door flew +open, and Peterson, the commissionaire, rushed into the apartment +with flushed cheeks and the face of a man who is dazed with +astonishment. + +"The goose, Mr. Holmes! The goose, sir!" he gasped. + +"Eh? What of it, then? Has it returned to life and flapped off +through the kitchen window?" Holmes twisted himself round upon +the sofa to get a fairer view of the man's excited face. + +"See here, sir! See what my wife found in its crop!" He held out +his hand and displayed upon the centre of the palm a brilliantly +scintillating blue stone, rather smaller than a bean in size, but +of such purity and radiance that it twinkled like an electric +point in the dark hollow of his hand. + +Sherlock Holmes sat up with a whistle. "By Jove, Peterson!" said +he, "this is treasure trove indeed. I suppose you know what you +have got?" + +"A diamond, sir? A precious stone. It cuts into glass as though +it were putty." + +"It's more than a precious stone. It is the precious stone." + +"Not the Countess of Morcar's blue carbuncle!" I ejaculated. + +"Precisely so. I ought to know its size and shape, seeing that I +have read the advertisement about it in The Times every day +lately. It is absolutely unique, and its value can only be +conjectured, but the reward offered of 1000 pounds is certainly +not within a twentieth part of the market price." + +"A thousand pounds! Great Lord of mercy!" The commissionaire +plumped down into a chair and stared from one to the other of us. + +"That is the reward, and I have reason to know that there are +sentimental considerations in the background which would induce +the Countess to part with half her fortune if she could but +recover the gem." + +"It was lost, if I remember aright, at the Hotel Cosmopolitan," I +remarked. + +"Precisely so, on December 22nd, just five days ago. John Horner, +a plumber, was accused of having abstracted it from the lady's +jewel-case. The evidence against him was so strong that the case +has been referred to the Assizes. I have some account of the +matter here, I believe." He rummaged amid his newspapers, +glancing over the dates, until at last he smoothed one out, +doubled it over, and read the following paragraph: + +"Hotel Cosmopolitan Jewel Robbery. John Horner, 26, plumber, was +brought up upon the charge of having upon the 22nd inst., +abstracted from the jewel-case of the Countess of Morcar the +valuable gem known as the blue carbuncle. James Ryder, +upper-attendant at the hotel, gave his evidence to the effect +that he had shown Horner up to the dressing-room of the Countess +of Morcar upon the day of the robbery in order that he might +solder the second bar of the grate, which was loose. He had +remained with Horner some little time, but had finally been +called away. On returning, he found that Horner had disappeared, +that the bureau had been forced open, and that the small morocco +casket in which, as it afterwards transpired, the Countess was +accustomed to keep her jewel, was lying empty upon the +dressing-table. Ryder instantly gave the alarm, and Horner was +arrested the same evening; but the stone could not be found +either upon his person or in his rooms. Catherine Cusack, maid to +the Countess, deposed to having heard Ryder's cry of dismay on +discovering the robbery, and to having rushed into the room, +where she found matters as described by the last witness. +Inspector Bradstreet, B division, gave evidence as to the arrest +of Horner, who struggled frantically, and protested his innocence +in the strongest terms. Evidence of a previous conviction for +robbery having been given against the prisoner, the magistrate +refused to deal summarily with the offence, but referred it to +the Assizes. Horner, who had shown signs of intense emotion +during the proceedings, fainted away at the conclusion and was +carried out of court." + +"Hum! So much for the police-court," said Holmes thoughtfully, +tossing aside the paper. "The question for us now to solve is the +sequence of events leading from a rifled jewel-case at one end to +the crop of a goose in Tottenham Court Road at the other. You +see, Watson, our little deductions have suddenly assumed a much +more important and less innocent aspect. Here is the stone; the +stone came from the goose, and the goose came from Mr. Henry +Baker, the gentleman with the bad hat and all the other +characteristics with which I have bored you. So now we must set +ourselves very seriously to finding this gentleman and +ascertaining what part he has played in this little mystery. To +do this, we must try the simplest means first, and these lie +undoubtedly in an advertisement in all the evening papers. If +this fail, I shall have recourse to other methods." + +"What will you say?" + +"Give me a pencil and that slip of paper. Now, then: 'Found at +the corner of Goodge Street, a goose and a black felt hat. Mr. +Henry Baker can have the same by applying at 6:30 this evening at +221B, Baker Street.' That is clear and concise." + +"Very. But will he see it?" + +"Well, he is sure to keep an eye on the papers, since, to a poor +man, the loss was a heavy one. He was clearly so scared by his +mischance in breaking the window and by the approach of Peterson +that he thought of nothing but flight, but since then he must +have bitterly regretted the impulse which caused him to drop his +bird. Then, again, the introduction of his name will cause him to +see it, for everyone who knows him will direct his attention to +it. Here you are, Peterson, run down to the advertising agency +and have this put in the evening papers." + +"In which, sir?" + +"Oh, in the Globe, Star, Pall Mall, St. James's, Evening News, +Standard, Echo, and any others that occur to you." + +"Very well, sir. And this stone?" + +"Ah, yes, I shall keep the stone. Thank you. And, I say, +Peterson, just buy a goose on your way back and leave it here +with me, for we must have one to give to this gentleman in place +of the one which your family is now devouring." + +When the commissionaire had gone, Holmes took up the stone and +held it against the light. "It's a bonny thing," said he. "Just +see how it glints and sparkles. Of course it is a nucleus and +focus of crime. Every good stone is. They are the devil's pet +baits. In the larger and older jewels every facet may stand for a +bloody deed. This stone is not yet twenty years old. It was found +in the banks of the Amoy River in southern China and is remarkable +in having every characteristic of the carbuncle, save that it is +blue in shade instead of ruby red. In spite of its youth, it has +already a sinister history. There have been two murders, a +vitriol-throwing, a suicide, and several robberies brought about +for the sake of this forty-grain weight of crystallised charcoal. +Who would think that so pretty a toy would be a purveyor to the +gallows and the prison? I'll lock it up in my strong box now and +drop a line to the Countess to say that we have it." + +"Do you think that this man Horner is innocent?" + +"I cannot tell." + +"Well, then, do you imagine that this other one, Henry Baker, had +anything to do with the matter?" + +"It is, I think, much more likely that Henry Baker is an +absolutely innocent man, who had no idea that the bird which he +was carrying was of considerably more value than if it were made +of solid gold. That, however, I shall determine by a very simple +test if we have an answer to our advertisement." + +"And you can do nothing until then?" + +"Nothing." + +"In that case I shall continue my professional round. But I shall +come back in the evening at the hour you have mentioned, for I +should like to see the solution of so tangled a business." + +"Very glad to see you. I dine at seven. There is a woodcock, I +believe. By the way, in view of recent occurrences, perhaps I +ought to ask Mrs. Hudson to examine its crop." + +I had been delayed at a case, and it was a little after half-past +six when I found myself in Baker Street once more. As I +approached the house I saw a tall man in a Scotch bonnet with a +coat which was buttoned up to his chin waiting outside in the +bright semicircle which was thrown from the fanlight. Just as I +arrived the door was opened, and we were shown up together to +Holmes' room. + +"Mr. Henry Baker, I believe," said he, rising from his armchair +and greeting his visitor with the easy air of geniality which he +could so readily assume. "Pray take this chair by the fire, Mr. +Baker. It is a cold night, and I observe that your circulation is +more adapted for summer than for winter. Ah, Watson, you have +just come at the right time. Is that your hat, Mr. Baker?" + +"Yes, sir, that is undoubtedly my hat." + +He was a large man with rounded shoulders, a massive head, and a +broad, intelligent face, sloping down to a pointed beard of +grizzled brown. A touch of red in nose and cheeks, with a slight +tremor of his extended hand, recalled Holmes' surmise as to his +habits. His rusty black frock-coat was buttoned right up in +front, with the collar turned up, and his lank wrists protruded +from his sleeves without a sign of cuff or shirt. He spoke in a +slow staccato fashion, choosing his words with care, and gave the +impression generally of a man of learning and letters who had had +ill-usage at the hands of fortune. + +"We have retained these things for some days," said Holmes, +"because we expected to see an advertisement from you giving your +address. I am at a loss to know now why you did not advertise." + +Our visitor gave a rather shamefaced laugh. "Shillings have not +been so plentiful with me as they once were," he remarked. "I had +no doubt that the gang of roughs who assaulted me had carried off +both my hat and the bird. I did not care to spend more money in a +hopeless attempt at recovering them." + +"Very naturally. By the way, about the bird, we were compelled to +eat it." + +"To eat it!" Our visitor half rose from his chair in his +excitement. + +"Yes, it would have been of no use to anyone had we not done so. +But I presume that this other goose upon the sideboard, which is +about the same weight and perfectly fresh, will answer your +purpose equally well?" + +"Oh, certainly, certainly," answered Mr. Baker with a sigh of +relief. + +"Of course, we still have the feathers, legs, crop, and so on of +your own bird, so if you wish--" + +The man burst into a hearty laugh. "They might be useful to me as +relics of my adventure," said he, "but beyond that I can hardly +see what use the disjecta membra of my late acquaintance are +going to be to me. No, sir, I think that, with your permission, I +will confine my attentions to the excellent bird which I perceive +upon the sideboard." + +Sherlock Holmes glanced sharply across at me with a slight shrug +of his shoulders. + +"There is your hat, then, and there your bird," said he. "By the +way, would it bore you to tell me where you got the other one +from? I am somewhat of a fowl fancier, and I have seldom seen a +better grown goose." + +"Certainly, sir," said Baker, who had risen and tucked his newly +gained property under his arm. "There are a few of us who +frequent the Alpha Inn, near the Museum--we are to be found in +the Museum itself during the day, you understand. This year our +good host, Windigate by name, instituted a goose club, by which, +on consideration of some few pence every week, we were each to +receive a bird at Christmas. My pence were duly paid, and the +rest is familiar to you. I am much indebted to you, sir, for a +Scotch bonnet is fitted neither to my years nor my gravity." With +a comical pomposity of manner he bowed solemnly to both of us and +strode off upon his way. + +"So much for Mr. Henry Baker," said Holmes when he had closed the +door behind him. "It is quite certain that he knows nothing +whatever about the matter. Are you hungry, Watson?" + +"Not particularly." + +"Then I suggest that we turn our dinner into a supper and follow +up this clue while it is still hot." + +"By all means." + +It was a bitter night, so we drew on our ulsters and wrapped +cravats about our throats. Outside, the stars were shining coldly +in a cloudless sky, and the breath of the passers-by blew out +into smoke like so many pistol shots. Our footfalls rang out +crisply and loudly as we swung through the doctors' quarter, +Wimpole Street, Harley Street, and so through Wigmore Street into +Oxford Street. In a quarter of an hour we were in Bloomsbury at +the Alpha Inn, which is a small public-house at the corner of one +of the streets which runs down into Holborn. Holmes pushed open +the door of the private bar and ordered two glasses of beer from +the ruddy-faced, white-aproned landlord. + +"Your beer should be excellent if it is as good as your geese," +said he. + +"My geese!" The man seemed surprised. + +"Yes. I was speaking only half an hour ago to Mr. Henry Baker, +who was a member of your goose club." + +"Ah! yes, I see. But you see, sir, them's not our geese." + +"Indeed! Whose, then?" + +"Well, I got the two dozen from a salesman in Covent Garden." + +"Indeed? I know some of them. Which was it?" + +"Breckinridge is his name." + +"Ah! I don't know him. Well, here's your good health landlord, +and prosperity to your house. Good-night." + +"Now for Mr. Breckinridge," he continued, buttoning up his coat +as we came out into the frosty air. "Remember, Watson that though +we have so homely a thing as a goose at one end of this chain, we +have at the other a man who will certainly get seven years' penal +servitude unless we can establish his innocence. It is possible +that our inquiry may but confirm his guilt; but, in any case, we +have a line of investigation which has been missed by the police, +and which a singular chance has placed in our hands. Let us +follow it out to the bitter end. Faces to the south, then, and +quick march!" + +We passed across Holborn, down Endell Street, and so through a +zigzag of slums to Covent Garden Market. One of the largest +stalls bore the name of Breckinridge upon it, and the proprietor +a horsey-looking man, with a sharp face and trim side-whiskers was +helping a boy to put up the shutters. + +"Good-evening. It's a cold night," said Holmes. + +The salesman nodded and shot a questioning glance at my +companion. + +"Sold out of geese, I see," continued Holmes, pointing at the +bare slabs of marble. + +"Let you have five hundred to-morrow morning." + +"That's no good." + +"Well, there are some on the stall with the gas-flare." + +"Ah, but I was recommended to you." + +"Who by?" + +"The landlord of the Alpha." + +"Oh, yes; I sent him a couple of dozen." + +"Fine birds they were, too. Now where did you get them from?" + +To my surprise the question provoked a burst of anger from the +salesman. + +"Now, then, mister," said he, with his head cocked and his arms +akimbo, "what are you driving at? Let's have it straight, now." + +"It is straight enough. I should like to know who sold you the +geese which you supplied to the Alpha." + +"Well then, I shan't tell you. So now!" + +"Oh, it is a matter of no importance; but I don't know why you +should be so warm over such a trifle." + +"Warm! You'd be as warm, maybe, if you were as pestered as I am. +When I pay good money for a good article there should be an end +of the business; but it's 'Where are the geese?' and 'Who did you +sell the geese to?' and 'What will you take for the geese?' One +would think they were the only geese in the world, to hear the +fuss that is made over them." + +"Well, I have no connection with any other people who have been +making inquiries," said Holmes carelessly. "If you won't tell us +the bet is off, that is all. But I'm always ready to back my +opinion on a matter of fowls, and I have a fiver on it that the +bird I ate is country bred." + +"Well, then, you've lost your fiver, for it's town bred," snapped +the salesman. + +"It's nothing of the kind." + +"I say it is." + +"I don't believe it." + +"D'you think you know more about fowls than I, who have handled +them ever since I was a nipper? I tell you, all those birds that +went to the Alpha were town bred." + +"You'll never persuade me to believe that." + +"Will you bet, then?" + +"It's merely taking your money, for I know that I am right. But +I'll have a sovereign on with you, just to teach you not to be +obstinate." + +The salesman chuckled grimly. "Bring me the books, Bill," said +he. + +The small boy brought round a small thin volume and a great +greasy-backed one, laying them out together beneath the hanging +lamp. + +"Now then, Mr. Cocksure," said the salesman, "I thought that I +was out of geese, but before I finish you'll find that there is +still one left in my shop. You see this little book?" + +"Well?" + +"That's the list of the folk from whom I buy. D'you see? Well, +then, here on this page are the country folk, and the numbers +after their names are where their accounts are in the big ledger. +Now, then! You see this other page in red ink? Well, that is a +list of my town suppliers. Now, look at that third name. Just +read it out to me." + +"Mrs. Oakshott, 117, Brixton Road--249," read Holmes. + +"Quite so. Now turn that up in the ledger." + +Holmes turned to the page indicated. "Here you are, 'Mrs. +Oakshott, 117, Brixton Road, egg and poultry supplier.'" + +"Now, then, what's the last entry?" + +"'December 22nd. Twenty-four geese at 7s. 6d.'" + +"Quite so. There you are. And underneath?" + +"'Sold to Mr. Windigate of the Alpha, at 12s.'" + +"What have you to say now?" + +Sherlock Holmes looked deeply chagrined. He drew a sovereign from +his pocket and threw it down upon the slab, turning away with the +air of a man whose disgust is too deep for words. A few yards off +he stopped under a lamp-post and laughed in the hearty, noiseless +fashion which was peculiar to him. + +"When you see a man with whiskers of that cut and the 'Pink 'un' +protruding out of his pocket, you can always draw him by a bet," +said he. "I daresay that if I had put 100 pounds down in front of +him, that man would not have given me such complete information +as was drawn from him by the idea that he was doing me on a +wager. Well, Watson, we are, I fancy, nearing the end of our +quest, and the only point which remains to be determined is +whether we should go on to this Mrs. Oakshott to-night, or +whether we should reserve it for to-morrow. It is clear from what +that surly fellow said that there are others besides ourselves +who are anxious about the matter, and I should--" + +His remarks were suddenly cut short by a loud hubbub which broke +out from the stall which we had just left. Turning round we saw a +little rat-faced fellow standing in the centre of the circle of +yellow light which was thrown by the swinging lamp, while +Breckinridge, the salesman, framed in the door of his stall, was +shaking his fists fiercely at the cringing figure. + +"I've had enough of you and your geese," he shouted. "I wish you +were all at the devil together. If you come pestering me any more +with your silly talk I'll set the dog at you. You bring Mrs. +Oakshott here and I'll answer her, but what have you to do with +it? Did I buy the geese off you?" + +"No; but one of them was mine all the same," whined the little +man. + +"Well, then, ask Mrs. Oakshott for it." + +"She told me to ask you." + +"Well, you can ask the King of Proosia, for all I care. I've had +enough of it. Get out of this!" He rushed fiercely forward, and +the inquirer flitted away into the darkness. + +"Ha! this may save us a visit to Brixton Road," whispered Holmes. +"Come with me, and we will see what is to be made of this +fellow." Striding through the scattered knots of people who +lounged round the flaring stalls, my companion speedily overtook +the little man and touched him upon the shoulder. He sprang +round, and I could see in the gas-light that every vestige of +colour had been driven from his face. + +"Who are you, then? What do you want?" he asked in a quavering +voice. + +"You will excuse me," said Holmes blandly, "but I could not help +overhearing the questions which you put to the salesman just now. +I think that I could be of assistance to you." + +"You? Who are you? How could you know anything of the matter?" + +"My name is Sherlock Holmes. It is my business to know what other +people don't know." + +"But you can know nothing of this?" + +"Excuse me, I know everything of it. You are endeavouring to +trace some geese which were sold by Mrs. Oakshott, of Brixton +Road, to a salesman named Breckinridge, by him in turn to Mr. +Windigate, of the Alpha, and by him to his club, of which Mr. +Henry Baker is a member." + +"Oh, sir, you are the very man whom I have longed to meet," cried +the little fellow with outstretched hands and quivering fingers. +"I can hardly explain to you how interested I am in this matter." + +Sherlock Holmes hailed a four-wheeler which was passing. "In that +case we had better discuss it in a cosy room rather than in this +wind-swept market-place," said he. "But pray tell me, before we +go farther, who it is that I have the pleasure of assisting." + +The man hesitated for an instant. "My name is John Robinson," he +answered with a sidelong glance. + +"No, no; the real name," said Holmes sweetly. "It is always +awkward doing business with an alias." + +A flush sprang to the white cheeks of the stranger. "Well then," +said he, "my real name is James Ryder." + +"Precisely so. Head attendant at the Hotel Cosmopolitan. Pray +step into the cab, and I shall soon be able to tell you +everything which you would wish to know." + +The little man stood glancing from one to the other of us with +half-frightened, half-hopeful eyes, as one who is not sure +whether he is on the verge of a windfall or of a catastrophe. +Then he stepped into the cab, and in half an hour we were back in +the sitting-room at Baker Street. Nothing had been said during +our drive, but the high, thin breathing of our new companion, and +the claspings and unclaspings of his hands, spoke of the nervous +tension within him. + +"Here we are!" said Holmes cheerily as we filed into the room. +"The fire looks very seasonable in this weather. You look cold, +Mr. Ryder. Pray take the basket-chair. I will just put on my +slippers before we settle this little matter of yours. Now, then! +You want to know what became of those geese?" + +"Yes, sir." + +"Or rather, I fancy, of that goose. It was one bird, I imagine in +which you were interested--white, with a black bar across the +tail." + +Ryder quivered with emotion. "Oh, sir," he cried, "can you tell +me where it went to?" + +"It came here." + +"Here?" + +"Yes, and a most remarkable bird it proved. I don't wonder that +you should take an interest in it. It laid an egg after it was +dead--the bonniest, brightest little blue egg that ever was seen. +I have it here in my museum." + +Our visitor staggered to his feet and clutched the mantelpiece +with his right hand. Holmes unlocked his strong-box and held up +the blue carbuncle, which shone out like a star, with a cold, +brilliant, many-pointed radiance. Ryder stood glaring with a +drawn face, uncertain whether to claim or to disown it. + +"The game's up, Ryder," said Holmes quietly. "Hold up, man, or +you'll be into the fire! Give him an arm back into his chair, +Watson. He's not got blood enough to go in for felony with +impunity. Give him a dash of brandy. So! Now he looks a little +more human. What a shrimp it is, to be sure!" + +For a moment he had staggered and nearly fallen, but the brandy +brought a tinge of colour into his cheeks, and he sat staring +with frightened eyes at his accuser. + +"I have almost every link in my hands, and all the proofs which I +could possibly need, so there is little which you need tell me. +Still, that little may as well be cleared up to make the case +complete. You had heard, Ryder, of this blue stone of the +Countess of Morcar's?" + +"It was Catherine Cusack who told me of it," said he in a +crackling voice. + +"I see--her ladyship's waiting-maid. Well, the temptation of +sudden wealth so easily acquired was too much for you, as it has +been for better men before you; but you were not very scrupulous +in the means you used. It seems to me, Ryder, that there is the +making of a very pretty villain in you. You knew that this man +Horner, the plumber, had been concerned in some such matter +before, and that suspicion would rest the more readily upon him. +What did you do, then? You made some small job in my lady's +room--you and your confederate Cusack--and you managed that he +should be the man sent for. Then, when he had left, you rifled +the jewel-case, raised the alarm, and had this unfortunate man +arrested. You then--" + +Ryder threw himself down suddenly upon the rug and clutched at my +companion's knees. "For God's sake, have mercy!" he shrieked. +"Think of my father! Of my mother! It would break their hearts. I +never went wrong before! I never will again. I swear it. I'll +swear it on a Bible. Oh, don't bring it into court! For Christ's +sake, don't!" + +"Get back into your chair!" said Holmes sternly. "It is very well +to cringe and crawl now, but you thought little enough of this +poor Horner in the dock for a crime of which he knew nothing." + +"I will fly, Mr. Holmes. I will leave the country, sir. Then the +charge against him will break down." + +"Hum! We will talk about that. And now let us hear a true account +of the next act. How came the stone into the goose, and how came +the goose into the open market? Tell us the truth, for there lies +your only hope of safety." + +Ryder passed his tongue over his parched lips. "I will tell you +it just as it happened, sir," said he. "When Horner had been +arrested, it seemed to me that it would be best for me to get +away with the stone at once, for I did not know at what moment +the police might not take it into their heads to search me and my +room. There was no place about the hotel where it would be safe. +I went out, as if on some commission, and I made for my sister's +house. She had married a man named Oakshott, and lived in Brixton +Road, where she fattened fowls for the market. All the way there +every man I met seemed to me to be a policeman or a detective; +and, for all that it was a cold night, the sweat was pouring down +my face before I came to the Brixton Road. My sister asked me +what was the matter, and why I was so pale; but I told her that I +had been upset by the jewel robbery at the hotel. Then I went +into the back yard and smoked a pipe and wondered what it would +be best to do. + +"I had a friend once called Maudsley, who went to the bad, and +has just been serving his time in Pentonville. One day he had met +me, and fell into talk about the ways of thieves, and how they +could get rid of what they stole. I knew that he would be true to +me, for I knew one or two things about him; so I made up my mind +to go right on to Kilburn, where he lived, and take him into my +confidence. He would show me how to turn the stone into money. +But how to get to him in safety? I thought of the agonies I had +gone through in coming from the hotel. I might at any moment be +seized and searched, and there would be the stone in my waistcoat +pocket. I was leaning against the wall at the time and looking at +the geese which were waddling about round my feet, and suddenly +an idea came into my head which showed me how I could beat the +best detective that ever lived. + +"My sister had told me some weeks before that I might have the +pick of her geese for a Christmas present, and I knew that she +was always as good as her word. I would take my goose now, and in +it I would carry my stone to Kilburn. There was a little shed in +the yard, and behind this I drove one of the birds--a fine big +one, white, with a barred tail. I caught it, and prying its bill +open, I thrust the stone down its throat as far as my finger +could reach. The bird gave a gulp, and I felt the stone pass +along its gullet and down into its crop. But the creature flapped +and struggled, and out came my sister to know what was the +matter. As I turned to speak to her the brute broke loose and +fluttered off among the others. + +"'Whatever were you doing with that bird, Jem?' says she. + +"'Well,' said I, 'you said you'd give me one for Christmas, and I +was feeling which was the fattest.' + +"'Oh,' says she, 'we've set yours aside for you--Jem's bird, we +call it. It's the big white one over yonder. There's twenty-six +of them, which makes one for you, and one for us, and two dozen +for the market.' + +"'Thank you, Maggie,' says I; 'but if it is all the same to you, +I'd rather have that one I was handling just now.' + +"'The other is a good three pound heavier,' said she, 'and we +fattened it expressly for you.' + +"'Never mind. I'll have the other, and I'll take it now,' said I. + +"'Oh, just as you like,' said she, a little huffed. 'Which is it +you want, then?' + +"'That white one with the barred tail, right in the middle of the +flock.' + +"'Oh, very well. Kill it and take it with you.' + +"Well, I did what she said, Mr. Holmes, and I carried the bird +all the way to Kilburn. I told my pal what I had done, for he was +a man that it was easy to tell a thing like that to. He laughed +until he choked, and we got a knife and opened the goose. My +heart turned to water, for there was no sign of the stone, and I +knew that some terrible mistake had occurred. I left the bird, +rushed back to my sister's, and hurried into the back yard. There +was not a bird to be seen there. + +"'Where are they all, Maggie?' I cried. + +"'Gone to the dealer's, Jem.' + +"'Which dealer's?' + +"'Breckinridge, of Covent Garden.' + +"'But was there another with a barred tail?' I asked, 'the same +as the one I chose?' + +"'Yes, Jem; there were two barred-tailed ones, and I could never +tell them apart.' + +"Well, then, of course I saw it all, and I ran off as hard as my +feet would carry me to this man Breckinridge; but he had sold the +lot at once, and not one word would he tell me as to where they +had gone. You heard him yourselves to-night. Well, he has always +answered me like that. My sister thinks that I am going mad. +Sometimes I think that I am myself. And now--and now I am myself +a branded thief, without ever having touched the wealth for which +I sold my character. God help me! God help me!" He burst into +convulsive sobbing, with his face buried in his hands. + +There was a long silence, broken only by his heavy breathing and +by the measured tapping of Sherlock Holmes' finger-tips upon the +edge of the table. Then my friend rose and threw open the door. + +"Get out!" said he. + +"What, sir! Oh, Heaven bless you!" + +"No more words. Get out!" + +And no more words were needed. There was a rush, a clatter upon +the stairs, the bang of a door, and the crisp rattle of running +footfalls from the street. + +"After all, Watson," said Holmes, reaching up his hand for his +clay pipe, "I am not retained by the police to supply their +deficiencies. If Horner were in danger it would be another thing; +but this fellow will not appear against him, and the case must +collapse. I suppose that I am commuting a felony, but it is just +possible that I am saving a soul. This fellow will not go wrong +again; he is too terribly frightened. Send him to gaol now, and +you make him a gaol-bird for life. Besides, it is the season of +forgiveness. Chance has put in our way a most singular and +whimsical problem, and its solution is its own reward. If you +will have the goodness to touch the bell, Doctor, we will begin +another investigation, in which, also a bird will be the chief +feature." + + + +VIII. THE ADVENTURE OF THE SPECKLED BAND + +On glancing over my notes of the seventy odd cases in which I +have during the last eight years studied the methods of my friend +Sherlock Holmes, I find many tragic, some comic, a large number +merely strange, but none commonplace; for, working as he did +rather for the love of his art than for the acquirement of +wealth, he refused to associate himself with any investigation +which did not tend towards the unusual, and even the fantastic. +Of all these varied cases, however, I cannot recall any which +presented more singular features than that which was associated +with the well-known Surrey family of the Roylotts of Stoke Moran. +The events in question occurred in the early days of my +association with Holmes, when we were sharing rooms as bachelors +in Baker Street. It is possible that I might have placed them +upon record before, but a promise of secrecy was made at the +time, from which I have only been freed during the last month by +the untimely death of the lady to whom the pledge was given. It +is perhaps as well that the facts should now come to light, for I +have reasons to know that there are widespread rumours as to the +death of Dr. Grimesby Roylott which tend to make the matter even +more terrible than the truth. + +It was early in April in the year '83 that I woke one morning to +find Sherlock Holmes standing, fully dressed, by the side of my +bed. He was a late riser, as a rule, and as the clock on the +mantelpiece showed me that it was only a quarter-past seven, I +blinked up at him in some surprise, and perhaps just a little +resentment, for I was myself regular in my habits. + +"Very sorry to knock you up, Watson," said he, "but it's the +common lot this morning. Mrs. Hudson has been knocked up, she +retorted upon me, and I on you." + +"What is it, then--a fire?" + +"No; a client. It seems that a young lady has arrived in a +considerable state of excitement, who insists upon seeing me. She +is waiting now in the sitting-room. Now, when young ladies wander +about the metropolis at this hour of the morning, and knock +sleepy people up out of their beds, I presume that it is +something very pressing which they have to communicate. Should it +prove to be an interesting case, you would, I am sure, wish to +follow it from the outset. I thought, at any rate, that I should +call you and give you the chance." + +"My dear fellow, I would not miss it for anything." + +I had no keener pleasure than in following Holmes in his +professional investigations, and in admiring the rapid +deductions, as swift as intuitions, and yet always founded on a +logical basis with which he unravelled the problems which were +submitted to him. I rapidly threw on my clothes and was ready in +a few minutes to accompany my friend down to the sitting-room. A +lady dressed in black and heavily veiled, who had been sitting in +the window, rose as we entered. + +"Good-morning, madam," said Holmes cheerily. "My name is Sherlock +Holmes. This is my intimate friend and associate, Dr. Watson, +before whom you can speak as freely as before myself. Ha! I am +glad to see that Mrs. Hudson has had the good sense to light the +fire. Pray draw up to it, and I shall order you a cup of hot +coffee, for I observe that you are shivering." + +"It is not cold which makes me shiver," said the woman in a low +voice, changing her seat as requested. + +"What, then?" + +"It is fear, Mr. Holmes. It is terror." She raised her veil as +she spoke, and we could see that she was indeed in a pitiable +state of agitation, her face all drawn and grey, with restless +frightened eyes, like those of some hunted animal. Her features +and figure were those of a woman of thirty, but her hair was shot +with premature grey, and her expression was weary and haggard. +Sherlock Holmes ran her over with one of his quick, +all-comprehensive glances. + +"You must not fear," said he soothingly, bending forward and +patting her forearm. "We shall soon set matters right, I have no +doubt. You have come in by train this morning, I see." + +"You know me, then?" + +"No, but I observe the second half of a return ticket in the palm +of your left glove. You must have started early, and yet you had +a good drive in a dog-cart, along heavy roads, before you reached +the station." + +The lady gave a violent start and stared in bewilderment at my +companion. + +"There is no mystery, my dear madam," said he, smiling. "The left +arm of your jacket is spattered with mud in no less than seven +places. The marks are perfectly fresh. There is no vehicle save a +dog-cart which throws up mud in that way, and then only when you +sit on the left-hand side of the driver." + +"Whatever your reasons may be, you are perfectly correct," said +she. "I started from home before six, reached Leatherhead at +twenty past, and came in by the first train to Waterloo. Sir, I +can stand this strain no longer; I shall go mad if it continues. +I have no one to turn to--none, save only one, who cares for me, +and he, poor fellow, can be of little aid. I have heard of you, +Mr. Holmes; I have heard of you from Mrs. Farintosh, whom you +helped in the hour of her sore need. It was from her that I had +your address. Oh, sir, do you not think that you could help me, +too, and at least throw a little light through the dense darkness +which surrounds me? At present it is out of my power to reward +you for your services, but in a month or six weeks I shall be +married, with the control of my own income, and then at least you +shall not find me ungrateful." + +Holmes turned to his desk and, unlocking it, drew out a small +case-book, which he consulted. + +"Farintosh," said he. "Ah yes, I recall the case; it was +concerned with an opal tiara. I think it was before your time, +Watson. I can only say, madam, that I shall be happy to devote +the same care to your case as I did to that of your friend. As to +reward, my profession is its own reward; but you are at liberty +to defray whatever expenses I may be put to, at the time which +suits you best. And now I beg that you will lay before us +everything that may help us in forming an opinion upon the +matter." + +"Alas!" replied our visitor, "the very horror of my situation +lies in the fact that my fears are so vague, and my suspicions +depend so entirely upon small points, which might seem trivial to +another, that even he to whom of all others I have a right to +look for help and advice looks upon all that I tell him about it +as the fancies of a nervous woman. He does not say so, but I can +read it from his soothing answers and averted eyes. But I have +heard, Mr. Holmes, that you can see deeply into the manifold +wickedness of the human heart. You may advise me how to walk amid +the dangers which encompass me." + +"I am all attention, madam." + +"My name is Helen Stoner, and I am living with my stepfather, who +is the last survivor of one of the oldest Saxon families in +England, the Roylotts of Stoke Moran, on the western border of +Surrey." + +Holmes nodded his head. "The name is familiar to me," said he. + +"The family was at one time among the richest in England, and the +estates extended over the borders into Berkshire in the north, +and Hampshire in the west. In the last century, however, four +successive heirs were of a dissolute and wasteful disposition, +and the family ruin was eventually completed by a gambler in the +days of the Regency. Nothing was left save a few acres of ground, +and the two-hundred-year-old house, which is itself crushed under +a heavy mortgage. The last squire dragged out his existence +there, living the horrible life of an aristocratic pauper; but +his only son, my stepfather, seeing that he must adapt himself to +the new conditions, obtained an advance from a relative, which +enabled him to take a medical degree and went out to Calcutta, +where, by his professional skill and his force of character, he +established a large practice. In a fit of anger, however, caused +by some robberies which had been perpetrated in the house, he +beat his native butler to death and narrowly escaped a capital +sentence. As it was, he suffered a long term of imprisonment and +afterwards returned to England a morose and disappointed man. + +"When Dr. Roylott was in India he married my mother, Mrs. Stoner, +the young widow of Major-General Stoner, of the Bengal Artillery. +My sister Julia and I were twins, and we were only two years old +at the time of my mother's re-marriage. She had a considerable +sum of money--not less than 1000 pounds a year--and this she +bequeathed to Dr. Roylott entirely while we resided with him, +with a provision that a certain annual sum should be allowed to +each of us in the event of our marriage. Shortly after our return +to England my mother died--she was killed eight years ago in a +railway accident near Crewe. Dr. Roylott then abandoned his +attempts to establish himself in practice in London and took us +to live with him in the old ancestral house at Stoke Moran. The +money which my mother had left was enough for all our wants, and +there seemed to be no obstacle to our happiness. + +"But a terrible change came over our stepfather about this time. +Instead of making friends and exchanging visits with our +neighbours, who had at first been overjoyed to see a Roylott of +Stoke Moran back in the old family seat, he shut himself up in +his house and seldom came out save to indulge in ferocious +quarrels with whoever might cross his path. Violence of temper +approaching to mania has been hereditary in the men of the +family, and in my stepfather's case it had, I believe, been +intensified by his long residence in the tropics. A series of +disgraceful brawls took place, two of which ended in the +police-court, until at last he became the terror of the village, +and the folks would fly at his approach, for he is a man of +immense strength, and absolutely uncontrollable in his anger. + +"Last week he hurled the local blacksmith over a parapet into a +stream, and it was only by paying over all the money which I +could gather together that I was able to avert another public +exposure. He had no friends at all save the wandering gipsies, +and he would give these vagabonds leave to encamp upon the few +acres of bramble-covered land which represent the family estate, +and would accept in return the hospitality of their tents, +wandering away with them sometimes for weeks on end. He has a +passion also for Indian animals, which are sent over to him by a +correspondent, and he has at this moment a cheetah and a baboon, +which wander freely over his grounds and are feared by the +villagers almost as much as their master. + +"You can imagine from what I say that my poor sister Julia and I +had no great pleasure in our lives. No servant would stay with +us, and for a long time we did all the work of the house. She was +but thirty at the time of her death, and yet her hair had already +begun to whiten, even as mine has." + +"Your sister is dead, then?" + +"She died just two years ago, and it is of her death that I wish +to speak to you. You can understand that, living the life which I +have described, we were little likely to see anyone of our own +age and position. We had, however, an aunt, my mother's maiden +sister, Miss Honoria Westphail, who lives near Harrow, and we +were occasionally allowed to pay short visits at this lady's +house. Julia went there at Christmas two years ago, and met there +a half-pay major of marines, to whom she became engaged. My +stepfather learned of the engagement when my sister returned and +offered no objection to the marriage; but within a fortnight of +the day which had been fixed for the wedding, the terrible event +occurred which has deprived me of my only companion." + +Sherlock Holmes had been leaning back in his chair with his eyes +closed and his head sunk in a cushion, but he half opened his +lids now and glanced across at his visitor. + +"Pray be precise as to details," said he. + +"It is easy for me to be so, for every event of that dreadful +time is seared into my memory. The manor-house is, as I have +already said, very old, and only one wing is now inhabited. The +bedrooms in this wing are on the ground floor, the sitting-rooms +being in the central block of the buildings. Of these bedrooms +the first is Dr. Roylott's, the second my sister's, and the third +my own. There is no communication between them, but they all open +out into the same corridor. Do I make myself plain?" + +"Perfectly so." + +"The windows of the three rooms open out upon the lawn. That +fatal night Dr. Roylott had gone to his room early, though we +knew that he had not retired to rest, for my sister was troubled +by the smell of the strong Indian cigars which it was his custom +to smoke. She left her room, therefore, and came into mine, where +she sat for some time, chatting about her approaching wedding. At +eleven o'clock she rose to leave me, but she paused at the door +and looked back. + +"'Tell me, Helen,' said she, 'have you ever heard anyone whistle +in the dead of the night?' + +"'Never,' said I. + +"'I suppose that you could not possibly whistle, yourself, in +your sleep?' + +"'Certainly not. But why?' + +"'Because during the last few nights I have always, about three +in the morning, heard a low, clear whistle. I am a light sleeper, +and it has awakened me. I cannot tell where it came from--perhaps +from the next room, perhaps from the lawn. I thought that I would +just ask you whether you had heard it.' + +"'No, I have not. It must be those wretched gipsies in the +plantation.' + +"'Very likely. And yet if it were on the lawn, I wonder that you +did not hear it also.' + +"'Ah, but I sleep more heavily than you.' + +"'Well, it is of no great consequence, at any rate.' She smiled +back at me, closed my door, and a few moments later I heard her +key turn in the lock." + +"Indeed," said Holmes. "Was it your custom always to lock +yourselves in at night?" + +"Always." + +"And why?" + +"I think that I mentioned to you that the doctor kept a cheetah +and a baboon. We had no feeling of security unless our doors were +locked." + +"Quite so. Pray proceed with your statement." + +"I could not sleep that night. A vague feeling of impending +misfortune impressed me. My sister and I, you will recollect, +were twins, and you know how subtle are the links which bind two +souls which are so closely allied. It was a wild night. The wind +was howling outside, and the rain was beating and splashing +against the windows. Suddenly, amid all the hubbub of the gale, +there burst forth the wild scream of a terrified woman. I knew +that it was my sister's voice. I sprang from my bed, wrapped a +shawl round me, and rushed into the corridor. As I opened my door +I seemed to hear a low whistle, such as my sister described, and +a few moments later a clanging sound, as if a mass of metal had +fallen. As I ran down the passage, my sister's door was unlocked, +and revolved slowly upon its hinges. I stared at it +horror-stricken, not knowing what was about to issue from it. By +the light of the corridor-lamp I saw my sister appear at the +opening, her face blanched with terror, her hands groping for +help, her whole figure swaying to and fro like that of a +drunkard. I ran to her and threw my arms round her, but at that +moment her knees seemed to give way and she fell to the ground. +She writhed as one who is in terrible pain, and her limbs were +dreadfully convulsed. At first I thought that she had not +recognised me, but as I bent over her she suddenly shrieked out +in a voice which I shall never forget, 'Oh, my God! Helen! It was +the band! The speckled band!' There was something else which she +would fain have said, and she stabbed with her finger into the +air in the direction of the doctor's room, but a fresh convulsion +seized her and choked her words. I rushed out, calling loudly for +my stepfather, and I met him hastening from his room in his +dressing-gown. When he reached my sister's side she was +unconscious, and though he poured brandy down her throat and sent +for medical aid from the village, all efforts were in vain, for +she slowly sank and died without having recovered her +consciousness. Such was the dreadful end of my beloved sister." + +"One moment," said Holmes, "are you sure about this whistle and +metallic sound? Could you swear to it?" + +"That was what the county coroner asked me at the inquiry. It is +my strong impression that I heard it, and yet, among the crash of +the gale and the creaking of an old house, I may possibly have +been deceived." + +"Was your sister dressed?" + +"No, she was in her night-dress. In her right hand was found the +charred stump of a match, and in her left a match-box." + +"Showing that she had struck a light and looked about her when +the alarm took place. That is important. And what conclusions did +the coroner come to?" + +"He investigated the case with great care, for Dr. Roylott's +conduct had long been notorious in the county, but he was unable +to find any satisfactory cause of death. My evidence showed that +the door had been fastened upon the inner side, and the windows +were blocked by old-fashioned shutters with broad iron bars, +which were secured every night. The walls were carefully sounded, +and were shown to be quite solid all round, and the flooring was +also thoroughly examined, with the same result. The chimney is +wide, but is barred up by four large staples. It is certain, +therefore, that my sister was quite alone when she met her end. +Besides, there were no marks of any violence upon her." + +"How about poison?" + +"The doctors examined her for it, but without success." + +"What do you think that this unfortunate lady died of, then?" + +"It is my belief that she died of pure fear and nervous shock, +though what it was that frightened her I cannot imagine." + +"Were there gipsies in the plantation at the time?" + +"Yes, there are nearly always some there." + +"Ah, and what did you gather from this allusion to a band--a +speckled band?" + +"Sometimes I have thought that it was merely the wild talk of +delirium, sometimes that it may have referred to some band of +people, perhaps to these very gipsies in the plantation. I do not +know whether the spotted handkerchiefs which so many of them wear +over their heads might have suggested the strange adjective which +she used." + +Holmes shook his head like a man who is far from being satisfied. + +"These are very deep waters," said he; "pray go on with your +narrative." + +"Two years have passed since then, and my life has been until +lately lonelier than ever. A month ago, however, a dear friend, +whom I have known for many years, has done me the honour to ask +my hand in marriage. His name is Armitage--Percy Armitage--the +second son of Mr. Armitage, of Crane Water, near Reading. My +stepfather has offered no opposition to the match, and we are to +be married in the course of the spring. Two days ago some repairs +were started in the west wing of the building, and my bedroom +wall has been pierced, so that I have had to move into the +chamber in which my sister died, and to sleep in the very bed in +which she slept. Imagine, then, my thrill of terror when last +night, as I lay awake, thinking over her terrible fate, I +suddenly heard in the silence of the night the low whistle which +had been the herald of her own death. I sprang up and lit the +lamp, but nothing was to be seen in the room. I was too shaken to +go to bed again, however, so I dressed, and as soon as it was +daylight I slipped down, got a dog-cart at the Crown Inn, which +is opposite, and drove to Leatherhead, from whence I have come on +this morning with the one object of seeing you and asking your +advice." + +"You have done wisely," said my friend. "But have you told me +all?" + +"Yes, all." + +"Miss Roylott, you have not. You are screening your stepfather." + +"Why, what do you mean?" + +For answer Holmes pushed back the frill of black lace which +fringed the hand that lay upon our visitor's knee. Five little +livid spots, the marks of four fingers and a thumb, were printed +upon the white wrist. + +"You have been cruelly used," said Holmes. + +The lady coloured deeply and covered over her injured wrist. "He +is a hard man," she said, "and perhaps he hardly knows his own +strength." + +There was a long silence, during which Holmes leaned his chin +upon his hands and stared into the crackling fire. + +"This is a very deep business," he said at last. "There are a +thousand details which I should desire to know before I decide +upon our course of action. Yet we have not a moment to lose. If +we were to come to Stoke Moran to-day, would it be possible for +us to see over these rooms without the knowledge of your +stepfather?" + +"As it happens, he spoke of coming into town to-day upon some +most important business. It is probable that he will be away all +day, and that there would be nothing to disturb you. We have a +housekeeper now, but she is old and foolish, and I could easily +get her out of the way." + +"Excellent. You are not averse to this trip, Watson?" + +"By no means." + +"Then we shall both come. What are you going to do yourself?" + +"I have one or two things which I would wish to do now that I am +in town. But I shall return by the twelve o'clock train, so as to +be there in time for your coming." + +"And you may expect us early in the afternoon. I have myself some +small business matters to attend to. Will you not wait and +breakfast?" + +"No, I must go. My heart is lightened already since I have +confided my trouble to you. I shall look forward to seeing you +again this afternoon." She dropped her thick black veil over her +face and glided from the room. + +"And what do you think of it all, Watson?" asked Sherlock Holmes, +leaning back in his chair. + +"It seems to me to be a most dark and sinister business." + +"Dark enough and sinister enough." + +"Yet if the lady is correct in saying that the flooring and walls +are sound, and that the door, window, and chimney are impassable, +then her sister must have been undoubtedly alone when she met her +mysterious end." + +"What becomes, then, of these nocturnal whistles, and what of the +very peculiar words of the dying woman?" + +"I cannot think." + +"When you combine the ideas of whistles at night, the presence of +a band of gipsies who are on intimate terms with this old doctor, +the fact that we have every reason to believe that the doctor has +an interest in preventing his stepdaughter's marriage, the dying +allusion to a band, and, finally, the fact that Miss Helen Stoner +heard a metallic clang, which might have been caused by one of +those metal bars that secured the shutters falling back into its +place, I think that there is good ground to think that the +mystery may be cleared along those lines." + +"But what, then, did the gipsies do?" + +"I cannot imagine." + +"I see many objections to any such theory." + +"And so do I. It is precisely for that reason that we are going +to Stoke Moran this day. I want to see whether the objections are +fatal, or if they may be explained away. But what in the name of +the devil!" + +The ejaculation had been drawn from my companion by the fact that +our door had been suddenly dashed open, and that a huge man had +framed himself in the aperture. His costume was a peculiar +mixture of the professional and of the agricultural, having a +black top-hat, a long frock-coat, and a pair of high gaiters, +with a hunting-crop swinging in his hand. So tall was he that his +hat actually brushed the cross bar of the doorway, and his +breadth seemed to span it across from side to side. A large face, +seared with a thousand wrinkles, burned yellow with the sun, and +marked with every evil passion, was turned from one to the other +of us, while his deep-set, bile-shot eyes, and his high, thin, +fleshless nose, gave him somewhat the resemblance to a fierce old +bird of prey. + +"Which of you is Holmes?" asked this apparition. + +"My name, sir; but you have the advantage of me," said my +companion quietly. + +"I am Dr. Grimesby Roylott, of Stoke Moran." + +"Indeed, Doctor," said Holmes blandly. "Pray take a seat." + +"I will do nothing of the kind. My stepdaughter has been here. I +have traced her. What has she been saying to you?" + +"It is a little cold for the time of the year," said Holmes. + +"What has she been saying to you?" screamed the old man +furiously. + +"But I have heard that the crocuses promise well," continued my +companion imperturbably. + +"Ha! You put me off, do you?" said our new visitor, taking a step +forward and shaking his hunting-crop. "I know you, you scoundrel! +I have heard of you before. You are Holmes, the meddler." + +My friend smiled. + +"Holmes, the busybody!" + +His smile broadened. + +"Holmes, the Scotland Yard Jack-in-office!" + +Holmes chuckled heartily. "Your conversation is most +entertaining," said he. "When you go out close the door, for +there is a decided draught." + +"I will go when I have said my say. Don't you dare to meddle with +my affairs. I know that Miss Stoner has been here. I traced her! +I am a dangerous man to fall foul of! See here." He stepped +swiftly forward, seized the poker, and bent it into a curve with +his huge brown hands. + +"See that you keep yourself out of my grip," he snarled, and +hurling the twisted poker into the fireplace he strode out of the +room. + +"He seems a very amiable person," said Holmes, laughing. "I am +not quite so bulky, but if he had remained I might have shown him +that my grip was not much more feeble than his own." As he spoke +he picked up the steel poker and, with a sudden effort, +straightened it out again. + +"Fancy his having the insolence to confound me with the official +detective force! This incident gives zest to our investigation, +however, and I only trust that our little friend will not suffer +from her imprudence in allowing this brute to trace her. And now, +Watson, we shall order breakfast, and afterwards I shall walk +down to Doctors' Commons, where I hope to get some data which may +help us in this matter." + + +It was nearly one o'clock when Sherlock Holmes returned from his +excursion. He held in his hand a sheet of blue paper, scrawled +over with notes and figures. + +"I have seen the will of the deceased wife," said he. "To +determine its exact meaning I have been obliged to work out the +present prices of the investments with which it is concerned. The +total income, which at the time of the wife's death was little +short of 1100 pounds, is now, through the fall in agricultural +prices, not more than 750 pounds. Each daughter can claim an +income of 250 pounds, in case of marriage. It is evident, +therefore, that if both girls had married, this beauty would have +had a mere pittance, while even one of them would cripple him to +a very serious extent. My morning's work has not been wasted, +since it has proved that he has the very strongest motives for +standing in the way of anything of the sort. And now, Watson, +this is too serious for dawdling, especially as the old man is +aware that we are interesting ourselves in his affairs; so if you +are ready, we shall call a cab and drive to Waterloo. I should be +very much obliged if you would slip your revolver into your +pocket. An Eley's No. 2 is an excellent argument with gentlemen +who can twist steel pokers into knots. That and a tooth-brush +are, I think, all that we need." + +At Waterloo we were fortunate in catching a train for +Leatherhead, where we hired a trap at the station inn and drove +for four or five miles through the lovely Surrey lanes. It was a +perfect day, with a bright sun and a few fleecy clouds in the +heavens. The trees and wayside hedges were just throwing out +their first green shoots, and the air was full of the pleasant +smell of the moist earth. To me at least there was a strange +contrast between the sweet promise of the spring and this +sinister quest upon which we were engaged. My companion sat in +the front of the trap, his arms folded, his hat pulled down over +his eyes, and his chin sunk upon his breast, buried in the +deepest thought. Suddenly, however, he started, tapped me on the +shoulder, and pointed over the meadows. + +"Look there!" said he. + +A heavily timbered park stretched up in a gentle slope, +thickening into a grove at the highest point. From amid the +branches there jutted out the grey gables and high roof-tree of a +very old mansion. + +"Stoke Moran?" said he. + +"Yes, sir, that be the house of Dr. Grimesby Roylott," remarked +the driver. + +"There is some building going on there," said Holmes; "that is +where we are going." + +"There's the village," said the driver, pointing to a cluster of +roofs some distance to the left; "but if you want to get to the +house, you'll find it shorter to get over this stile, and so by +the foot-path over the fields. There it is, where the lady is +walking." + +"And the lady, I fancy, is Miss Stoner," observed Holmes, shading +his eyes. "Yes, I think we had better do as you suggest." + +We got off, paid our fare, and the trap rattled back on its way +to Leatherhead. + +"I thought it as well," said Holmes as we climbed the stile, +"that this fellow should think we had come here as architects, or +on some definite business. It may stop his gossip. +Good-afternoon, Miss Stoner. You see that we have been as good as +our word." + +Our client of the morning had hurried forward to meet us with a +face which spoke her joy. "I have been waiting so eagerly for +you," she cried, shaking hands with us warmly. "All has turned +out splendidly. Dr. Roylott has gone to town, and it is unlikely +that he will be back before evening." + +"We have had the pleasure of making the doctor's acquaintance," +said Holmes, and in a few words he sketched out what had +occurred. Miss Stoner turned white to the lips as she listened. + +"Good heavens!" she cried, "he has followed me, then." + +"So it appears." + +"He is so cunning that I never know when I am safe from him. What +will he say when he returns?" + +"He must guard himself, for he may find that there is someone +more cunning than himself upon his track. You must lock yourself +up from him to-night. If he is violent, we shall take you away to +your aunt's at Harrow. Now, we must make the best use of our +time, so kindly take us at once to the rooms which we are to +examine." + +The building was of grey, lichen-blotched stone, with a high +central portion and two curving wings, like the claws of a crab, +thrown out on each side. In one of these wings the windows were +broken and blocked with wooden boards, while the roof was partly +caved in, a picture of ruin. The central portion was in little +better repair, but the right-hand block was comparatively modern, +and the blinds in the windows, with the blue smoke curling up +from the chimneys, showed that this was where the family resided. +Some scaffolding had been erected against the end wall, and the +stone-work had been broken into, but there were no signs of any +workmen at the moment of our visit. Holmes walked slowly up and +down the ill-trimmed lawn and examined with deep attention the +outsides of the windows. + +"This, I take it, belongs to the room in which you used to sleep, +the centre one to your sister's, and the one next to the main +building to Dr. Roylott's chamber?" + +"Exactly so. But I am now sleeping in the middle one." + +"Pending the alterations, as I understand. By the way, there does +not seem to be any very pressing need for repairs at that end +wall." + +"There were none. I believe that it was an excuse to move me from +my room." + +"Ah! that is suggestive. Now, on the other side of this narrow +wing runs the corridor from which these three rooms open. There +are windows in it, of course?" + +"Yes, but very small ones. Too narrow for anyone to pass +through." + +"As you both locked your doors at night, your rooms were +unapproachable from that side. Now, would you have the kindness +to go into your room and bar your shutters?" + +Miss Stoner did so, and Holmes, after a careful examination +through the open window, endeavoured in every way to force the +shutter open, but without success. There was no slit through +which a knife could be passed to raise the bar. Then with his +lens he tested the hinges, but they were of solid iron, built +firmly into the massive masonry. "Hum!" said he, scratching his +chin in some perplexity, "my theory certainly presents some +difficulties. No one could pass these shutters if they were +bolted. Well, we shall see if the inside throws any light upon +the matter." + +A small side door led into the whitewashed corridor from which +the three bedrooms opened. Holmes refused to examine the third +chamber, so we passed at once to the second, that in which Miss +Stoner was now sleeping, and in which her sister had met with her +fate. It was a homely little room, with a low ceiling and a +gaping fireplace, after the fashion of old country-houses. A +brown chest of drawers stood in one corner, a narrow +white-counterpaned bed in another, and a dressing-table on the +left-hand side of the window. These articles, with two small +wicker-work chairs, made up all the furniture in the room save +for a square of Wilton carpet in the centre. The boards round and +the panelling of the walls were of brown, worm-eaten oak, so old +and discoloured that it may have dated from the original building +of the house. Holmes drew one of the chairs into a corner and sat +silent, while his eyes travelled round and round and up and down, +taking in every detail of the apartment. + +"Where does that bell communicate with?" he asked at last +pointing to a thick bell-rope which hung down beside the bed, the +tassel actually lying upon the pillow. + +"It goes to the housekeeper's room." + +"It looks newer than the other things?" + +"Yes, it was only put there a couple of years ago." + +"Your sister asked for it, I suppose?" + +"No, I never heard of her using it. We used always to get what we +wanted for ourselves." + +"Indeed, it seemed unnecessary to put so nice a bell-pull there. +You will excuse me for a few minutes while I satisfy myself as to +this floor." He threw himself down upon his face with his lens in +his hand and crawled swiftly backward and forward, examining +minutely the cracks between the boards. Then he did the same with +the wood-work with which the chamber was panelled. Finally he +walked over to the bed and spent some time in staring at it and +in running his eye up and down the wall. Finally he took the +bell-rope in his hand and gave it a brisk tug. + +"Why, it's a dummy," said he. + +"Won't it ring?" + +"No, it is not even attached to a wire. This is very interesting. +You can see now that it is fastened to a hook just above where +the little opening for the ventilator is." + +"How very absurd! I never noticed that before." + +"Very strange!" muttered Holmes, pulling at the rope. "There are +one or two very singular points about this room. For example, +what a fool a builder must be to open a ventilator into another +room, when, with the same trouble, he might have communicated +with the outside air!" + +"That is also quite modern," said the lady. + +"Done about the same time as the bell-rope?" remarked Holmes. + +"Yes, there were several little changes carried out about that +time." + +"They seem to have been of a most interesting character--dummy +bell-ropes, and ventilators which do not ventilate. With your +permission, Miss Stoner, we shall now carry our researches into +the inner apartment." + +Dr. Grimesby Roylott's chamber was larger than that of his +step-daughter, but was as plainly furnished. A camp-bed, a small +wooden shelf full of books, mostly of a technical character, an +armchair beside the bed, a plain wooden chair against the wall, a +round table, and a large iron safe were the principal things +which met the eye. Holmes walked slowly round and examined each +and all of them with the keenest interest. + +"What's in here?" he asked, tapping the safe. + +"My stepfather's business papers." + +"Oh! you have seen inside, then?" + +"Only once, some years ago. I remember that it was full of +papers." + +"There isn't a cat in it, for example?" + +"No. What a strange idea!" + +"Well, look at this!" He took up a small saucer of milk which +stood on the top of it. + +"No; we don't keep a cat. But there is a cheetah and a baboon." + +"Ah, yes, of course! Well, a cheetah is just a big cat, and yet a +saucer of milk does not go very far in satisfying its wants, I +daresay. There is one point which I should wish to determine." He +squatted down in front of the wooden chair and examined the seat +of it with the greatest attention. + +"Thank you. That is quite settled," said he, rising and putting +his lens in his pocket. "Hullo! Here is something interesting!" + +The object which had caught his eye was a small dog lash hung on +one corner of the bed. The lash, however, was curled upon itself +and tied so as to make a loop of whipcord. + +"What do you make of that, Watson?" + +"It's a common enough lash. But I don't know why it should be +tied." + +"That is not quite so common, is it? Ah, me! it's a wicked world, +and when a clever man turns his brains to crime it is the worst +of all. I think that I have seen enough now, Miss Stoner, and +with your permission we shall walk out upon the lawn." + +I had never seen my friend's face so grim or his brow so dark as +it was when we turned from the scene of this investigation. We +had walked several times up and down the lawn, neither Miss +Stoner nor myself liking to break in upon his thoughts before he +roused himself from his reverie. + +"It is very essential, Miss Stoner," said he, "that you should +absolutely follow my advice in every respect." + +"I shall most certainly do so." + +"The matter is too serious for any hesitation. Your life may +depend upon your compliance." + +"I assure you that I am in your hands." + +"In the first place, both my friend and I must spend the night in +your room." + +Both Miss Stoner and I gazed at him in astonishment. + +"Yes, it must be so. Let me explain. I believe that that is the +village inn over there?" + +"Yes, that is the Crown." + +"Very good. Your windows would be visible from there?" + +"Certainly." + +"You must confine yourself to your room, on pretence of a +headache, when your stepfather comes back. Then when you hear him +retire for the night, you must open the shutters of your window, +undo the hasp, put your lamp there as a signal to us, and then +withdraw quietly with everything which you are likely to want +into the room which you used to occupy. I have no doubt that, in +spite of the repairs, you could manage there for one night." + +"Oh, yes, easily." + +"The rest you will leave in our hands." + +"But what will you do?" + +"We shall spend the night in your room, and we shall investigate +the cause of this noise which has disturbed you." + +"I believe, Mr. Holmes, that you have already made up your mind," +said Miss Stoner, laying her hand upon my companion's sleeve. + +"Perhaps I have." + +"Then, for pity's sake, tell me what was the cause of my sister's +death." + +"I should prefer to have clearer proofs before I speak." + +"You can at least tell me whether my own thought is correct, and +if she died from some sudden fright." + +"No, I do not think so. I think that there was probably some more +tangible cause. And now, Miss Stoner, we must leave you for if +Dr. Roylott returned and saw us our journey would be in vain. +Good-bye, and be brave, for if you will do what I have told you, +you may rest assured that we shall soon drive away the dangers +that threaten you." + +Sherlock Holmes and I had no difficulty in engaging a bedroom and +sitting-room at the Crown Inn. They were on the upper floor, and +from our window we could command a view of the avenue gate, and +of the inhabited wing of Stoke Moran Manor House. At dusk we saw +Dr. Grimesby Roylott drive past, his huge form looming up beside +the little figure of the lad who drove him. The boy had some +slight difficulty in undoing the heavy iron gates, and we heard +the hoarse roar of the doctor's voice and saw the fury with which +he shook his clinched fists at him. The trap drove on, and a few +minutes later we saw a sudden light spring up among the trees as +the lamp was lit in one of the sitting-rooms. + +"Do you know, Watson," said Holmes as we sat together in the +gathering darkness, "I have really some scruples as to taking you +to-night. There is a distinct element of danger." + +"Can I be of assistance?" + +"Your presence might be invaluable." + +"Then I shall certainly come." + +"It is very kind of you." + +"You speak of danger. You have evidently seen more in these rooms +than was visible to me." + +"No, but I fancy that I may have deduced a little more. I imagine +that you saw all that I did." + +"I saw nothing remarkable save the bell-rope, and what purpose +that could answer I confess is more than I can imagine." + +"You saw the ventilator, too?" + +"Yes, but I do not think that it is such a very unusual thing to +have a small opening between two rooms. It was so small that a +rat could hardly pass through." + +"I knew that we should find a ventilator before ever we came to +Stoke Moran." + +"My dear Holmes!" + +"Oh, yes, I did. You remember in her statement she said that her +sister could smell Dr. Roylott's cigar. Now, of course that +suggested at once that there must be a communication between the +two rooms. It could only be a small one, or it would have been +remarked upon at the coroner's inquiry. I deduced a ventilator." + +"But what harm can there be in that?" + +"Well, there is at least a curious coincidence of dates. A +ventilator is made, a cord is hung, and a lady who sleeps in the +bed dies. Does not that strike you?" + +"I cannot as yet see any connection." + +"Did you observe anything very peculiar about that bed?" + +"No." + +"It was clamped to the floor. Did you ever see a bed fastened +like that before?" + +"I cannot say that I have." + +"The lady could not move her bed. It must always be in the same +relative position to the ventilator and to the rope--or so we may +call it, since it was clearly never meant for a bell-pull." + +"Holmes," I cried, "I seem to see dimly what you are hinting at. +We are only just in time to prevent some subtle and horrible +crime." + +"Subtle enough and horrible enough. When a doctor does go wrong +he is the first of criminals. He has nerve and he has knowledge. +Palmer and Pritchard were among the heads of their profession. +This man strikes even deeper, but I think, Watson, that we shall +be able to strike deeper still. But we shall have horrors enough +before the night is over; for goodness' sake let us have a quiet +pipe and turn our minds for a few hours to something more +cheerful." + + +About nine o'clock the light among the trees was extinguished, +and all was dark in the direction of the Manor House. Two hours +passed slowly away, and then, suddenly, just at the stroke of +eleven, a single bright light shone out right in front of us. + +"That is our signal," said Holmes, springing to his feet; "it +comes from the middle window." + +As we passed out he exchanged a few words with the landlord, +explaining that we were going on a late visit to an acquaintance, +and that it was possible that we might spend the night there. A +moment later we were out on the dark road, a chill wind blowing +in our faces, and one yellow light twinkling in front of us +through the gloom to guide us on our sombre errand. + +There was little difficulty in entering the grounds, for +unrepaired breaches gaped in the old park wall. Making our way +among the trees, we reached the lawn, crossed it, and were about +to enter through the window when out from a clump of laurel +bushes there darted what seemed to be a hideous and distorted +child, who threw itself upon the grass with writhing limbs and +then ran swiftly across the lawn into the darkness. + +"My God!" I whispered; "did you see it?" + +Holmes was for the moment as startled as I. His hand closed like +a vice upon my wrist in his agitation. Then he broke into a low +laugh and put his lips to my ear. + +"It is a nice household," he murmured. "That is the baboon." + +I had forgotten the strange pets which the doctor affected. There +was a cheetah, too; perhaps we might find it upon our shoulders +at any moment. I confess that I felt easier in my mind when, +after following Holmes' example and slipping off my shoes, I +found myself inside the bedroom. My companion noiselessly closed +the shutters, moved the lamp onto the table, and cast his eyes +round the room. All was as we had seen it in the daytime. Then +creeping up to me and making a trumpet of his hand, he whispered +into my ear again so gently that it was all that I could do to +distinguish the words: + +"The least sound would be fatal to our plans." + +I nodded to show that I had heard. + +"We must sit without light. He would see it through the +ventilator." + +I nodded again. + +"Do not go asleep; your very life may depend upon it. Have your +pistol ready in case we should need it. I will sit on the side of +the bed, and you in that chair." + +I took out my revolver and laid it on the corner of the table. + +Holmes had brought up a long thin cane, and this he placed upon +the bed beside him. By it he laid the box of matches and the +stump of a candle. Then he turned down the lamp, and we were left +in darkness. + +How shall I ever forget that dreadful vigil? I could not hear a +sound, not even the drawing of a breath, and yet I knew that my +companion sat open-eyed, within a few feet of me, in the same +state of nervous tension in which I was myself. The shutters cut +off the least ray of light, and we waited in absolute darkness. + +From outside came the occasional cry of a night-bird, and once at +our very window a long drawn catlike whine, which told us that +the cheetah was indeed at liberty. Far away we could hear the +deep tones of the parish clock, which boomed out every quarter of +an hour. How long they seemed, those quarters! Twelve struck, and +one and two and three, and still we sat waiting silently for +whatever might befall. + +Suddenly there was the momentary gleam of a light up in the +direction of the ventilator, which vanished immediately, but was +succeeded by a strong smell of burning oil and heated metal. +Someone in the next room had lit a dark-lantern. I heard a gentle +sound of movement, and then all was silent once more, though the +smell grew stronger. For half an hour I sat with straining ears. +Then suddenly another sound became audible--a very gentle, +soothing sound, like that of a small jet of steam escaping +continually from a kettle. The instant that we heard it, Holmes +sprang from the bed, struck a match, and lashed furiously with +his cane at the bell-pull. + +"You see it, Watson?" he yelled. "You see it?" + +But I saw nothing. At the moment when Holmes struck the light I +heard a low, clear whistle, but the sudden glare flashing into my +weary eyes made it impossible for me to tell what it was at which +my friend lashed so savagely. I could, however, see that his face +was deadly pale and filled with horror and loathing. He had +ceased to strike and was gazing up at the ventilator when +suddenly there broke from the silence of the night the most +horrible cry to which I have ever listened. It swelled up louder +and louder, a hoarse yell of pain and fear and anger all mingled +in the one dreadful shriek. They say that away down in the +village, and even in the distant parsonage, that cry raised the +sleepers from their beds. It struck cold to our hearts, and I +stood gazing at Holmes, and he at me, until the last echoes of it +had died away into the silence from which it rose. + +"What can it mean?" I gasped. + +"It means that it is all over," Holmes answered. "And perhaps, +after all, it is for the best. Take your pistol, and we will +enter Dr. Roylott's room." + +With a grave face he lit the lamp and led the way down the +corridor. Twice he struck at the chamber door without any reply +from within. Then he turned the handle and entered, I at his +heels, with the cocked pistol in my hand. + +It was a singular sight which met our eyes. On the table stood a +dark-lantern with the shutter half open, throwing a brilliant +beam of light upon the iron safe, the door of which was ajar. +Beside this table, on the wooden chair, sat Dr. Grimesby Roylott +clad in a long grey dressing-gown, his bare ankles protruding +beneath, and his feet thrust into red heelless Turkish slippers. +Across his lap lay the short stock with the long lash which we +had noticed during the day. His chin was cocked upward and his +eyes were fixed in a dreadful, rigid stare at the corner of the +ceiling. Round his brow he had a peculiar yellow band, with +brownish speckles, which seemed to be bound tightly round his +head. As we entered he made neither sound nor motion. + +"The band! the speckled band!" whispered Holmes. + +I took a step forward. In an instant his strange headgear began +to move, and there reared itself from among his hair the squat +diamond-shaped head and puffed neck of a loathsome serpent. + +"It is a swamp adder!" cried Holmes; "the deadliest snake in +India. He has died within ten seconds of being bitten. Violence +does, in truth, recoil upon the violent, and the schemer falls +into the pit which he digs for another. Let us thrust this +creature back into its den, and we can then remove Miss Stoner to +some place of shelter and let the county police know what has +happened." + +As he spoke he drew the dog-whip swiftly from the dead man's lap, +and throwing the noose round the reptile's neck he drew it from +its horrid perch and, carrying it at arm's length, threw it into +the iron safe, which he closed upon it. + +Such are the true facts of the death of Dr. Grimesby Roylott, of +Stoke Moran. It is not necessary that I should prolong a +narrative which has already run to too great a length by telling +how we broke the sad news to the terrified girl, how we conveyed +her by the morning train to the care of her good aunt at Harrow, +of how the slow process of official inquiry came to the +conclusion that the doctor met his fate while indiscreetly +playing with a dangerous pet. The little which I had yet to learn +of the case was told me by Sherlock Holmes as we travelled back +next day. + +"I had," said he, "come to an entirely erroneous conclusion which +shows, my dear Watson, how dangerous it always is to reason from +insufficient data. The presence of the gipsies, and the use of +the word 'band,' which was used by the poor girl, no doubt, to +explain the appearance which she had caught a hurried glimpse of +by the light of her match, were sufficient to put me upon an +entirely wrong scent. I can only claim the merit that I instantly +reconsidered my position when, however, it became clear to me +that whatever danger threatened an occupant of the room could not +come either from the window or the door. My attention was +speedily drawn, as I have already remarked to you, to this +ventilator, and to the bell-rope which hung down to the bed. The +discovery that this was a dummy, and that the bed was clamped to +the floor, instantly gave rise to the suspicion that the rope was +there as a bridge for something passing through the hole and +coming to the bed. The idea of a snake instantly occurred to me, +and when I coupled it with my knowledge that the doctor was +furnished with a supply of creatures from India, I felt that I +was probably on the right track. The idea of using a form of +poison which could not possibly be discovered by any chemical +test was just such a one as would occur to a clever and ruthless +man who had had an Eastern training. The rapidity with which such +a poison would take effect would also, from his point of view, be +an advantage. It would be a sharp-eyed coroner, indeed, who could +distinguish the two little dark punctures which would show where +the poison fangs had done their work. Then I thought of the +whistle. Of course he must recall the snake before the morning +light revealed it to the victim. He had trained it, probably by +the use of the milk which we saw, to return to him when summoned. +He would put it through this ventilator at the hour that he +thought best, with the certainty that it would crawl down the +rope and land on the bed. It might or might not bite the +occupant, perhaps she might escape every night for a week, but +sooner or later she must fall a victim. + +"I had come to these conclusions before ever I had entered his +room. An inspection of his chair showed me that he had been in +the habit of standing on it, which of course would be necessary +in order that he should reach the ventilator. The sight of the +safe, the saucer of milk, and the loop of whipcord were enough to +finally dispel any doubts which may have remained. The metallic +clang heard by Miss Stoner was obviously caused by her stepfather +hastily closing the door of his safe upon its terrible occupant. +Having once made up my mind, you know the steps which I took in +order to put the matter to the proof. I heard the creature hiss +as I have no doubt that you did also, and I instantly lit the +light and attacked it." + +"With the result of driving it through the ventilator." + +"And also with the result of causing it to turn upon its master +at the other side. Some of the blows of my cane came home and +roused its snakish temper, so that it flew upon the first person +it saw. In this way I am no doubt indirectly responsible for Dr. +Grimesby Roylott's death, and I cannot say that it is likely to +weigh very heavily upon my conscience." + + + +IX. THE ADVENTURE OF THE ENGINEER'S THUMB + +Of all the problems which have been submitted to my friend, Mr. +Sherlock Holmes, for solution during the years of our intimacy, +there were only two which I was the means of introducing to his +notice--that of Mr. Hatherley's thumb, and that of Colonel +Warburton's madness. Of these the latter may have afforded a +finer field for an acute and original observer, but the other was +so strange in its inception and so dramatic in its details that +it may be the more worthy of being placed upon record, even if it +gave my friend fewer openings for those deductive methods of +reasoning by which he achieved such remarkable results. The story +has, I believe, been told more than once in the newspapers, but, +like all such narratives, its effect is much less striking when +set forth en bloc in a single half-column of print than when the +facts slowly evolve before your own eyes, and the mystery clears +gradually away as each new discovery furnishes a step which leads +on to the complete truth. At the time the circumstances made a +deep impression upon me, and the lapse of two years has hardly +served to weaken the effect. + +It was in the summer of '89, not long after my marriage, that the +events occurred which I am now about to summarise. I had returned +to civil practice and had finally abandoned Holmes in his Baker +Street rooms, although I continually visited him and occasionally +even persuaded him to forgo his Bohemian habits so far as to come +and visit us. My practice had steadily increased, and as I +happened to live at no very great distance from Paddington +Station, I got a few patients from among the officials. One of +these, whom I had cured of a painful and lingering disease, was +never weary of advertising my virtues and of endeavouring to send +me on every sufferer over whom he might have any influence. + +One morning, at a little before seven o'clock, I was awakened by +the maid tapping at the door to announce that two men had come +from Paddington and were waiting in the consulting-room. I +dressed hurriedly, for I knew by experience that railway cases +were seldom trivial, and hastened downstairs. As I descended, my +old ally, the guard, came out of the room and closed the door +tightly behind him. + +"I've got him here," he whispered, jerking his thumb over his +shoulder; "he's all right." + +"What is it, then?" I asked, for his manner suggested that it was +some strange creature which he had caged up in my room. + +"It's a new patient," he whispered. "I thought I'd bring him +round myself; then he couldn't slip away. There he is, all safe +and sound. I must go now, Doctor; I have my dooties, just the +same as you." And off he went, this trusty tout, without even +giving me time to thank him. + +I entered my consulting-room and found a gentleman seated by the +table. He was quietly dressed in a suit of heather tweed with a +soft cloth cap which he had laid down upon my books. Round one of +his hands he had a handkerchief wrapped, which was mottled all +over with bloodstains. He was young, not more than +five-and-twenty, I should say, with a strong, masculine face; but +he was exceedingly pale and gave me the impression of a man who +was suffering from some strong agitation, which it took all his +strength of mind to control. + +"I am sorry to knock you up so early, Doctor," said he, "but I +have had a very serious accident during the night. I came in by +train this morning, and on inquiring at Paddington as to where I +might find a doctor, a worthy fellow very kindly escorted me +here. I gave the maid a card, but I see that she has left it upon +the side-table." + +I took it up and glanced at it. "Mr. Victor Hatherley, hydraulic +engineer, 16A, Victoria Street (3rd floor)." That was the name, +style, and abode of my morning visitor. "I regret that I have +kept you waiting," said I, sitting down in my library-chair. "You +are fresh from a night journey, I understand, which is in itself +a monotonous occupation." + +"Oh, my night could not be called monotonous," said he, and +laughed. He laughed very heartily, with a high, ringing note, +leaning back in his chair and shaking his sides. All my medical +instincts rose up against that laugh. + +"Stop it!" I cried; "pull yourself together!" and I poured out +some water from a caraffe. + +It was useless, however. He was off in one of those hysterical +outbursts which come upon a strong nature when some great crisis +is over and gone. Presently he came to himself once more, very +weary and pale-looking. + +"I have been making a fool of myself," he gasped. + +"Not at all. Drink this." I dashed some brandy into the water, +and the colour began to come back to his bloodless cheeks. + +"That's better!" said he. "And now, Doctor, perhaps you would +kindly attend to my thumb, or rather to the place where my thumb +used to be." + +He unwound the handkerchief and held out his hand. It gave even +my hardened nerves a shudder to look at it. There were four +protruding fingers and a horrid red, spongy surface where the +thumb should have been. It had been hacked or torn right out from +the roots. + +"Good heavens!" I cried, "this is a terrible injury. It must have +bled considerably." + +"Yes, it did. I fainted when it was done, and I think that I must +have been senseless for a long time. When I came to I found that +it was still bleeding, so I tied one end of my handkerchief very +tightly round the wrist and braced it up with a twig." + +"Excellent! You should have been a surgeon." + +"It is a question of hydraulics, you see, and came within my own +province." + +"This has been done," said I, examining the wound, "by a very +heavy and sharp instrument." + +"A thing like a cleaver," said he. + +"An accident, I presume?" + +"By no means." + +"What! a murderous attack?" + +"Very murderous indeed." + +"You horrify me." + +I sponged the wound, cleaned it, dressed it, and finally covered +it over with cotton wadding and carbolised bandages. He lay back +without wincing, though he bit his lip from time to time. + +"How is that?" I asked when I had finished. + +"Capital! Between your brandy and your bandage, I feel a new man. +I was very weak, but I have had a good deal to go through." + +"Perhaps you had better not speak of the matter. It is evidently +trying to your nerves." + +"Oh, no, not now. I shall have to tell my tale to the police; +but, between ourselves, if it were not for the convincing +evidence of this wound of mine, I should be surprised if they +believed my statement, for it is a very extraordinary one, and I +have not much in the way of proof with which to back it up; and, +even if they believe me, the clues which I can give them are so +vague that it is a question whether justice will be done." + +"Ha!" cried I, "if it is anything in the nature of a problem +which you desire to see solved, I should strongly recommend you +to come to my friend, Mr. Sherlock Holmes, before you go to the +official police." + +"Oh, I have heard of that fellow," answered my visitor, "and I +should be very glad if he would take the matter up, though of +course I must use the official police as well. Would you give me +an introduction to him?" + +"I'll do better. I'll take you round to him myself." + +"I should be immensely obliged to you." + +"We'll call a cab and go together. We shall just be in time to +have a little breakfast with him. Do you feel equal to it?" + +"Yes; I shall not feel easy until I have told my story." + +"Then my servant will call a cab, and I shall be with you in an +instant." I rushed upstairs, explained the matter shortly to my +wife, and in five minutes was inside a hansom, driving with my +new acquaintance to Baker Street. + +Sherlock Holmes was, as I expected, lounging about his +sitting-room in his dressing-gown, reading the agony column of The +Times and smoking his before-breakfast pipe, which was composed +of all the plugs and dottles left from his smokes of the day +before, all carefully dried and collected on the corner of the +mantelpiece. He received us in his quietly genial fashion, +ordered fresh rashers and eggs, and joined us in a hearty meal. +When it was concluded he settled our new acquaintance upon the +sofa, placed a pillow beneath his head, and laid a glass of +brandy and water within his reach. + +"It is easy to see that your experience has been no common one, +Mr. Hatherley," said he. "Pray, lie down there and make yourself +absolutely at home. Tell us what you can, but stop when you are +tired and keep up your strength with a little stimulant." + +"Thank you," said my patient, "but I have felt another man since +the doctor bandaged me, and I think that your breakfast has +completed the cure. I shall take up as little of your valuable +time as possible, so I shall start at once upon my peculiar +experiences." + +Holmes sat in his big armchair with the weary, heavy-lidded +expression which veiled his keen and eager nature, while I sat +opposite to him, and we listened in silence to the strange story +which our visitor detailed to us. + +"You must know," said he, "that I am an orphan and a bachelor, +residing alone in lodgings in London. By profession I am a +hydraulic engineer, and I have had considerable experience of my +work during the seven years that I was apprenticed to Venner & +Matheson, the well-known firm, of Greenwich. Two years ago, +having served my time, and having also come into a fair sum of +money through my poor father's death, I determined to start in +business for myself and took professional chambers in Victoria +Street. + +"I suppose that everyone finds his first independent start in +business a dreary experience. To me it has been exceptionally so. +During two years I have had three consultations and one small +job, and that is absolutely all that my profession has brought +me. My gross takings amount to 27 pounds 10s. Every day, from +nine in the morning until four in the afternoon, I waited in my +little den, until at last my heart began to sink, and I came to +believe that I should never have any practice at all. + +"Yesterday, however, just as I was thinking of leaving the +office, my clerk entered to say there was a gentleman waiting who +wished to see me upon business. He brought up a card, too, with +the name of 'Colonel Lysander Stark' engraved upon it. Close at +his heels came the colonel himself, a man rather over the middle +size, but of an exceeding thinness. I do not think that I have +ever seen so thin a man. His whole face sharpened away into nose +and chin, and the skin of his cheeks was drawn quite tense over +his outstanding bones. Yet this emaciation seemed to be his +natural habit, and due to no disease, for his eye was bright, his +step brisk, and his bearing assured. He was plainly but neatly +dressed, and his age, I should judge, would be nearer forty than +thirty. + +"'Mr. Hatherley?' said he, with something of a German accent. +'You have been recommended to me, Mr. Hatherley, as being a man +who is not only proficient in his profession but is also discreet +and capable of preserving a secret.' + +"I bowed, feeling as flattered as any young man would at such an +address. 'May I ask who it was who gave me so good a character?' + +"'Well, perhaps it is better that I should not tell you that just +at this moment. I have it from the same source that you are both +an orphan and a bachelor and are residing alone in London.' + +"'That is quite correct,' I answered; 'but you will excuse me if +I say that I cannot see how all this bears upon my professional +qualifications. I understand that it was on a professional matter +that you wished to speak to me?' + +"'Undoubtedly so. But you will find that all I say is really to +the point. I have a professional commission for you, but absolute +secrecy is quite essential--absolute secrecy, you understand, and +of course we may expect that more from a man who is alone than +from one who lives in the bosom of his family.' + +"'If I promise to keep a secret,' said I, 'you may absolutely +depend upon my doing so.' + +"He looked very hard at me as I spoke, and it seemed to me that I +had never seen so suspicious and questioning an eye. + +"'Do you promise, then?' said he at last. + +"'Yes, I promise.' + +"'Absolute and complete silence before, during, and after? No +reference to the matter at all, either in word or writing?' + +"'I have already given you my word.' + +"'Very good.' He suddenly sprang up, and darting like lightning +across the room he flung open the door. The passage outside was +empty. + +"'That's all right,' said he, coming back. 'I know that clerks are +sometimes curious as to their master's affairs. Now we can talk +in safety.' He drew up his chair very close to mine and began to +stare at me again with the same questioning and thoughtful look. + +"A feeling of repulsion, and of something akin to fear had begun +to rise within me at the strange antics of this fleshless man. +Even my dread of losing a client could not restrain me from +showing my impatience. + +"'I beg that you will state your business, sir,' said I; 'my time +is of value.' Heaven forgive me for that last sentence, but the +words came to my lips. + +"'How would fifty guineas for a night's work suit you?' he asked. + +"'Most admirably.' + +"'I say a night's work, but an hour's would be nearer the mark. I +simply want your opinion about a hydraulic stamping machine which +has got out of gear. If you show us what is wrong we shall soon +set it right ourselves. What do you think of such a commission as +that?' + +"'The work appears to be light and the pay munificent.' + +"'Precisely so. We shall want you to come to-night by the last +train.' + +"'Where to?' + +"'To Eyford, in Berkshire. It is a little place near the borders +of Oxfordshire, and within seven miles of Reading. There is a +train from Paddington which would bring you there at about +11:15.' + +"'Very good.' + +"'I shall come down in a carriage to meet you.' + +"'There is a drive, then?' + +"'Yes, our little place is quite out in the country. It is a good +seven miles from Eyford Station.' + +"'Then we can hardly get there before midnight. I suppose there +would be no chance of a train back. I should be compelled to stop +the night.' + +"'Yes, we could easily give you a shake-down.' + +"'That is very awkward. Could I not come at some more convenient +hour?' + +"'We have judged it best that you should come late. It is to +recompense you for any inconvenience that we are paying to you, a +young and unknown man, a fee which would buy an opinion from the +very heads of your profession. Still, of course, if you would +like to draw out of the business, there is plenty of time to do +so.' + +"I thought of the fifty guineas, and of how very useful they +would be to me. 'Not at all,' said I, 'I shall be very happy to +accommodate myself to your wishes. I should like, however, to +understand a little more clearly what it is that you wish me to +do.' + +"'Quite so. It is very natural that the pledge of secrecy which +we have exacted from you should have aroused your curiosity. I +have no wish to commit you to anything without your having it all +laid before you. I suppose that we are absolutely safe from +eavesdroppers?' + +"'Entirely.' + +"'Then the matter stands thus. You are probably aware that +fuller's-earth is a valuable product, and that it is only found +in one or two places in England?' + +"'I have heard so.' + +"'Some little time ago I bought a small place--a very small +place--within ten miles of Reading. I was fortunate enough to +discover that there was a deposit of fuller's-earth in one of my +fields. On examining it, however, I found that this deposit was a +comparatively small one, and that it formed a link between two +very much larger ones upon the right and left--both of them, +however, in the grounds of my neighbours. These good people were +absolutely ignorant that their land contained that which was +quite as valuable as a gold-mine. Naturally, it was to my +interest to buy their land before they discovered its true value, +but unfortunately I had no capital by which I could do this. I +took a few of my friends into the secret, however, and they +suggested that we should quietly and secretly work our own little +deposit and that in this way we should earn the money which would +enable us to buy the neighbouring fields. This we have now been +doing for some time, and in order to help us in our operations we +erected a hydraulic press. This press, as I have already +explained, has got out of order, and we wish your advice upon the +subject. We guard our secret very jealously, however, and if it +once became known that we had hydraulic engineers coming to our +little house, it would soon rouse inquiry, and then, if the facts +came out, it would be good-bye to any chance of getting these +fields and carrying out our plans. That is why I have made you +promise me that you will not tell a human being that you are +going to Eyford to-night. I hope that I make it all plain?' + +"'I quite follow you,' said I. 'The only point which I could not +quite understand was what use you could make of a hydraulic press +in excavating fuller's-earth, which, as I understand, is dug out +like gravel from a pit.' + +"'Ah!' said he carelessly, 'we have our own process. We compress +the earth into bricks, so as to remove them without revealing +what they are. But that is a mere detail. I have taken you fully +into my confidence now, Mr. Hatherley, and I have shown you how I +trust you.' He rose as he spoke. 'I shall expect you, then, at +Eyford at 11:15.' + +"'I shall certainly be there.' + +"'And not a word to a soul.' He looked at me with a last long, +questioning gaze, and then, pressing my hand in a cold, dank +grasp, he hurried from the room. + +"Well, when I came to think it all over in cool blood I was very +much astonished, as you may both think, at this sudden commission +which had been intrusted to me. On the one hand, of course, I was +glad, for the fee was at least tenfold what I should have asked +had I set a price upon my own services, and it was possible that +this order might lead to other ones. On the other hand, the face +and manner of my patron had made an unpleasant impression upon +me, and I could not think that his explanation of the +fuller's-earth was sufficient to explain the necessity for my +coming at midnight, and his extreme anxiety lest I should tell +anyone of my errand. However, I threw all fears to the winds, ate +a hearty supper, drove to Paddington, and started off, having +obeyed to the letter the injunction as to holding my tongue. + +"At Reading I had to change not only my carriage but my station. +However, I was in time for the last train to Eyford, and I +reached the little dim-lit station after eleven o'clock. I was the +only passenger who got out there, and there was no one upon the +platform save a single sleepy porter with a lantern. As I passed +out through the wicket gate, however, I found my acquaintance of +the morning waiting in the shadow upon the other side. Without a +word he grasped my arm and hurried me into a carriage, the door +of which was standing open. He drew up the windows on either +side, tapped on the wood-work, and away we went as fast as the +horse could go." + +"One horse?" interjected Holmes. + +"Yes, only one." + +"Did you observe the colour?" + +"Yes, I saw it by the side-lights when I was stepping into the +carriage. It was a chestnut." + +"Tired-looking or fresh?" + +"Oh, fresh and glossy." + +"Thank you. I am sorry to have interrupted you. Pray continue +your most interesting statement." + +"Away we went then, and we drove for at least an hour. Colonel +Lysander Stark had said that it was only seven miles, but I +should think, from the rate that we seemed to go, and from the +time that we took, that it must have been nearer twelve. He sat +at my side in silence all the time, and I was aware, more than +once when I glanced in his direction, that he was looking at me +with great intensity. The country roads seem to be not very good +in that part of the world, for we lurched and jolted terribly. I +tried to look out of the windows to see something of where we +were, but they were made of frosted glass, and I could make out +nothing save the occasional bright blur of a passing light. Now +and then I hazarded some remark to break the monotony of the +journey, but the colonel answered only in monosyllables, and the +conversation soon flagged. At last, however, the bumping of the +road was exchanged for the crisp smoothness of a gravel-drive, +and the carriage came to a stand. Colonel Lysander Stark sprang +out, and, as I followed after him, pulled me swiftly into a porch +which gaped in front of us. We stepped, as it were, right out of +the carriage and into the hall, so that I failed to catch the +most fleeting glance of the front of the house. The instant that +I had crossed the threshold the door slammed heavily behind us, +and I heard faintly the rattle of the wheels as the carriage +drove away. + +"It was pitch dark inside the house, and the colonel fumbled +about looking for matches and muttering under his breath. +Suddenly a door opened at the other end of the passage, and a +long, golden bar of light shot out in our direction. It grew +broader, and a woman appeared with a lamp in her hand, which she +held above her head, pushing her face forward and peering at us. +I could see that she was pretty, and from the gloss with which +the light shone upon her dark dress I knew that it was a rich +material. She spoke a few words in a foreign tongue in a tone as +though asking a question, and when my companion answered in a +gruff monosyllable she gave such a start that the lamp nearly +fell from her hand. Colonel Stark went up to her, whispered +something in her ear, and then, pushing her back into the room +from whence she had come, he walked towards me again with the +lamp in his hand. + +"'Perhaps you will have the kindness to wait in this room for a +few minutes,' said he, throwing open another door. It was a +quiet, little, plainly furnished room, with a round table in the +centre, on which several German books were scattered. Colonel +Stark laid down the lamp on the top of a harmonium beside the +door. 'I shall not keep you waiting an instant,' said he, and +vanished into the darkness. + +"I glanced at the books upon the table, and in spite of my +ignorance of German I could see that two of them were treatises +on science, the others being volumes of poetry. Then I walked +across to the window, hoping that I might catch some glimpse of +the country-side, but an oak shutter, heavily barred, was folded +across it. It was a wonderfully silent house. There was an old +clock ticking loudly somewhere in the passage, but otherwise +everything was deadly still. A vague feeling of uneasiness began +to steal over me. Who were these German people, and what were +they doing living in this strange, out-of-the-way place? And +where was the place? I was ten miles or so from Eyford, that was +all I knew, but whether north, south, east, or west I had no +idea. For that matter, Reading, and possibly other large towns, +were within that radius, so the place might not be so secluded, +after all. Yet it was quite certain, from the absolute stillness, +that we were in the country. I paced up and down the room, +humming a tune under my breath to keep up my spirits and feeling +that I was thoroughly earning my fifty-guinea fee. + +"Suddenly, without any preliminary sound in the midst of the +utter stillness, the door of my room swung slowly open. The woman +was standing in the aperture, the darkness of the hall behind +her, the yellow light from my lamp beating upon her eager and +beautiful face. I could see at a glance that she was sick with +fear, and the sight sent a chill to my own heart. She held up one +shaking finger to warn me to be silent, and she shot a few +whispered words of broken English at me, her eyes glancing back, +like those of a frightened horse, into the gloom behind her. + +"'I would go,' said she, trying hard, as it seemed to me, to +speak calmly; 'I would go. I should not stay here. There is no +good for you to do.' + +"'But, madam,' said I, 'I have not yet done what I came for. I +cannot possibly leave until I have seen the machine.' + +"'It is not worth your while to wait,' she went on. 'You can pass +through the door; no one hinders.' And then, seeing that I smiled +and shook my head, she suddenly threw aside her constraint and +made a step forward, with her hands wrung together. 'For the love +of Heaven!' she whispered, 'get away from here before it is too +late!' + +"But I am somewhat headstrong by nature, and the more ready to +engage in an affair when there is some obstacle in the way. I +thought of my fifty-guinea fee, of my wearisome journey, and of +the unpleasant night which seemed to be before me. Was it all to +go for nothing? Why should I slink away without having carried +out my commission, and without the payment which was my due? This +woman might, for all I knew, be a monomaniac. With a stout +bearing, therefore, though her manner had shaken me more than I +cared to confess, I still shook my head and declared my intention +of remaining where I was. She was about to renew her entreaties +when a door slammed overhead, and the sound of several footsteps +was heard upon the stairs. She listened for an instant, threw up +her hands with a despairing gesture, and vanished as suddenly and +as noiselessly as she had come. + +"The newcomers were Colonel Lysander Stark and a short thick man +with a chinchilla beard growing out of the creases of his double +chin, who was introduced to me as Mr. Ferguson. + +"'This is my secretary and manager,' said the colonel. 'By the +way, I was under the impression that I left this door shut just +now. I fear that you have felt the draught.' + +"'On the contrary,' said I, 'I opened the door myself because I +felt the room to be a little close.' + +"He shot one of his suspicious looks at me. 'Perhaps we had +better proceed to business, then,' said he. 'Mr. Ferguson and I +will take you up to see the machine.' + +"'I had better put my hat on, I suppose.' + +"'Oh, no, it is in the house.' + +"'What, you dig fuller's-earth in the house?' + +"'No, no. This is only where we compress it. But never mind that. +All we wish you to do is to examine the machine and to let us +know what is wrong with it.' + +"We went upstairs together, the colonel first with the lamp, the +fat manager and I behind him. It was a labyrinth of an old house, +with corridors, passages, narrow winding staircases, and little +low doors, the thresholds of which were hollowed out by the +generations who had crossed them. There were no carpets and no +signs of any furniture above the ground floor, while the plaster +was peeling off the walls, and the damp was breaking through in +green, unhealthy blotches. I tried to put on as unconcerned an +air as possible, but I had not forgotten the warnings of the +lady, even though I disregarded them, and I kept a keen eye upon +my two companions. Ferguson appeared to be a morose and silent +man, but I could see from the little that he said that he was at +least a fellow-countryman. + +"Colonel Lysander Stark stopped at last before a low door, which +he unlocked. Within was a small, square room, in which the three +of us could hardly get at one time. Ferguson remained outside, +and the colonel ushered me in. + +"'We are now,' said he, 'actually within the hydraulic press, and +it would be a particularly unpleasant thing for us if anyone were +to turn it on. The ceiling of this small chamber is really the +end of the descending piston, and it comes down with the force of +many tons upon this metal floor. There are small lateral columns +of water outside which receive the force, and which transmit and +multiply it in the manner which is familiar to you. The machine +goes readily enough, but there is some stiffness in the working +of it, and it has lost a little of its force. Perhaps you will +have the goodness to look it over and to show us how we can set +it right.' + +"I took the lamp from him, and I examined the machine very +thoroughly. It was indeed a gigantic one, and capable of +exercising enormous pressure. When I passed outside, however, and +pressed down the levers which controlled it, I knew at once by +the whishing sound that there was a slight leakage, which allowed +a regurgitation of water through one of the side cylinders. An +examination showed that one of the india-rubber bands which was +round the head of a driving-rod had shrunk so as not quite to +fill the socket along which it worked. This was clearly the cause +of the loss of power, and I pointed it out to my companions, who +followed my remarks very carefully and asked several practical +questions as to how they should proceed to set it right. When I +had made it clear to them, I returned to the main chamber of the +machine and took a good look at it to satisfy my own curiosity. +It was obvious at a glance that the story of the fuller's-earth +was the merest fabrication, for it would be absurd to suppose +that so powerful an engine could be designed for so inadequate a +purpose. The walls were of wood, but the floor consisted of a +large iron trough, and when I came to examine it I could see a +crust of metallic deposit all over it. I had stooped and was +scraping at this to see exactly what it was when I heard a +muttered exclamation in German and saw the cadaverous face of the +colonel looking down at me. + +"'What are you doing there?' he asked. + +"I felt angry at having been tricked by so elaborate a story as +that which he had told me. 'I was admiring your fuller's-earth,' +said I; 'I think that I should be better able to advise you as to +your machine if I knew what the exact purpose was for which it +was used.' + +"The instant that I uttered the words I regretted the rashness of +my speech. His face set hard, and a baleful light sprang up in +his grey eyes. + +"'Very well,' said he, 'you shall know all about the machine.' He +took a step backward, slammed the little door, and turned the key +in the lock. I rushed towards it and pulled at the handle, but it +was quite secure, and did not give in the least to my kicks and +shoves. 'Hullo!' I yelled. 'Hullo! Colonel! Let me out!' + +"And then suddenly in the silence I heard a sound which sent my +heart into my mouth. It was the clank of the levers and the swish +of the leaking cylinder. He had set the engine at work. The lamp +still stood upon the floor where I had placed it when examining +the trough. By its light I saw that the black ceiling was coming +down upon me, slowly, jerkily, but, as none knew better than +myself, with a force which must within a minute grind me to a +shapeless pulp. I threw myself, screaming, against the door, and +dragged with my nails at the lock. I implored the colonel to let +me out, but the remorseless clanking of the levers drowned my +cries. The ceiling was only a foot or two above my head, and with +my hand upraised I could feel its hard, rough surface. Then it +flashed through my mind that the pain of my death would depend +very much upon the position in which I met it. If I lay on my +face the weight would come upon my spine, and I shuddered to +think of that dreadful snap. Easier the other way, perhaps; and +yet, had I the nerve to lie and look up at that deadly black +shadow wavering down upon me? Already I was unable to stand +erect, when my eye caught something which brought a gush of hope +back to my heart. + +"I have said that though the floor and ceiling were of iron, the +walls were of wood. As I gave a last hurried glance around, I saw +a thin line of yellow light between two of the boards, which +broadened and broadened as a small panel was pushed backward. For +an instant I could hardly believe that here was indeed a door +which led away from death. The next instant I threw myself +through, and lay half-fainting upon the other side. The panel had +closed again behind me, but the crash of the lamp, and a few +moments afterwards the clang of the two slabs of metal, told me +how narrow had been my escape. + +"I was recalled to myself by a frantic plucking at my wrist, and +I found myself lying upon the stone floor of a narrow corridor, +while a woman bent over me and tugged at me with her left hand, +while she held a candle in her right. It was the same good friend +whose warning I had so foolishly rejected. + +"'Come! come!' she cried breathlessly. 'They will be here in a +moment. They will see that you are not there. Oh, do not waste +the so-precious time, but come!' + +"This time, at least, I did not scorn her advice. I staggered to +my feet and ran with her along the corridor and down a winding +stair. The latter led to another broad passage, and just as we +reached it we heard the sound of running feet and the shouting of +two voices, one answering the other from the floor on which we +were and from the one beneath. My guide stopped and looked about +her like one who is at her wit's end. Then she threw open a door +which led into a bedroom, through the window of which the moon +was shining brightly. + +"'It is your only chance,' said she. 'It is high, but it may be +that you can jump it.' + +"As she spoke a light sprang into view at the further end of the +passage, and I saw the lean figure of Colonel Lysander Stark +rushing forward with a lantern in one hand and a weapon like a +butcher's cleaver in the other. I rushed across the bedroom, +flung open the window, and looked out. How quiet and sweet and +wholesome the garden looked in the moonlight, and it could not be +more than thirty feet down. I clambered out upon the sill, but I +hesitated to jump until I should have heard what passed between +my saviour and the ruffian who pursued me. If she were ill-used, +then at any risks I was determined to go back to her assistance. +The thought had hardly flashed through my mind before he was at +the door, pushing his way past her; but she threw her arms round +him and tried to hold him back. + +"'Fritz! Fritz!' she cried in English, 'remember your promise +after the last time. You said it should not be again. He will be +silent! Oh, he will be silent!' + +"'You are mad, Elise!' he shouted, struggling to break away from +her. 'You will be the ruin of us. He has seen too much. Let me +pass, I say!' He dashed her to one side, and, rushing to the +window, cut at me with his heavy weapon. I had let myself go, and +was hanging by the hands to the sill, when his blow fell. I was +conscious of a dull pain, my grip loosened, and I fell into the +garden below. + +"I was shaken but not hurt by the fall; so I picked myself up and +rushed off among the bushes as hard as I could run, for I +understood that I was far from being out of danger yet. Suddenly, +however, as I ran, a deadly dizziness and sickness came over me. +I glanced down at my hand, which was throbbing painfully, and +then, for the first time, saw that my thumb had been cut off and +that the blood was pouring from my wound. I endeavoured to tie my +handkerchief round it, but there came a sudden buzzing in my +ears, and next moment I fell in a dead faint among the +rose-bushes. + +"How long I remained unconscious I cannot tell. It must have been +a very long time, for the moon had sunk, and a bright morning was +breaking when I came to myself. My clothes were all sodden with +dew, and my coat-sleeve was drenched with blood from my wounded +thumb. The smarting of it recalled in an instant all the +particulars of my night's adventure, and I sprang to my feet with +the feeling that I might hardly yet be safe from my pursuers. But +to my astonishment, when I came to look round me, neither house +nor garden were to be seen. I had been lying in an angle of the +hedge close by the highroad, and just a little lower down was a +long building, which proved, upon my approaching it, to be the +very station at which I had arrived upon the previous night. Were +it not for the ugly wound upon my hand, all that had passed +during those dreadful hours might have been an evil dream. + +"Half dazed, I went into the station and asked about the morning +train. There would be one to Reading in less than an hour. The +same porter was on duty, I found, as had been there when I +arrived. I inquired of him whether he had ever heard of Colonel +Lysander Stark. The name was strange to him. Had he observed a +carriage the night before waiting for me? No, he had not. Was +there a police-station anywhere near? There was one about three +miles off. + +"It was too far for me to go, weak and ill as I was. I determined +to wait until I got back to town before telling my story to the +police. It was a little past six when I arrived, so I went first +to have my wound dressed, and then the doctor was kind enough to +bring me along here. I put the case into your hands and shall do +exactly what you advise." + +We both sat in silence for some little time after listening to +this extraordinary narrative. Then Sherlock Holmes pulled down +from the shelf one of the ponderous commonplace books in which he +placed his cuttings. + +"Here is an advertisement which will interest you," said he. "It +appeared in all the papers about a year ago. Listen to this: +'Lost, on the 9th inst., Mr. Jeremiah Hayling, aged +twenty-six, a hydraulic engineer. Left his lodgings at ten +o'clock at night, and has not been heard of since. Was +dressed in,' etc., etc. Ha! That represents the last time that +the colonel needed to have his machine overhauled, I fancy." + +"Good heavens!" cried my patient. "Then that explains what the +girl said." + +"Undoubtedly. It is quite clear that the colonel was a cool and +desperate man, who was absolutely determined that nothing should +stand in the way of his little game, like those out-and-out +pirates who will leave no survivor from a captured ship. Well, +every moment now is precious, so if you feel equal to it we shall +go down to Scotland Yard at once as a preliminary to starting for +Eyford." + +Some three hours or so afterwards we were all in the train +together, bound from Reading to the little Berkshire village. +There were Sherlock Holmes, the hydraulic engineer, Inspector +Bradstreet, of Scotland Yard, a plain-clothes man, and myself. +Bradstreet had spread an ordnance map of the county out upon the +seat and was busy with his compasses drawing a circle with Eyford +for its centre. + +"There you are," said he. "That circle is drawn at a radius of +ten miles from the village. The place we want must be somewhere +near that line. You said ten miles, I think, sir." + +"It was an hour's good drive." + +"And you think that they brought you back all that way when you +were unconscious?" + +"They must have done so. I have a confused memory, too, of having +been lifted and conveyed somewhere." + +"What I cannot understand," said I, "is why they should have +spared you when they found you lying fainting in the garden. +Perhaps the villain was softened by the woman's entreaties." + +"I hardly think that likely. I never saw a more inexorable face +in my life." + +"Oh, we shall soon clear up all that," said Bradstreet. "Well, I +have drawn my circle, and I only wish I knew at what point upon +it the folk that we are in search of are to be found." + +"I think I could lay my finger on it," said Holmes quietly. + +"Really, now!" cried the inspector, "you have formed your +opinion! Come, now, we shall see who agrees with you. I say it is +south, for the country is more deserted there." + +"And I say east," said my patient. + +"I am for west," remarked the plain-clothes man. "There are +several quiet little villages up there." + +"And I am for north," said I, "because there are no hills there, +and our friend says that he did not notice the carriage go up +any." + +"Come," cried the inspector, laughing; "it's a very pretty +diversity of opinion. We have boxed the compass among us. Who do +you give your casting vote to?" + +"You are all wrong." + +"But we can't all be." + +"Oh, yes, you can. This is my point." He placed his finger in the +centre of the circle. "This is where we shall find them." + +"But the twelve-mile drive?" gasped Hatherley. + +"Six out and six back. Nothing simpler. You say yourself that the +horse was fresh and glossy when you got in. How could it be that +if it had gone twelve miles over heavy roads?" + +"Indeed, it is a likely ruse enough," observed Bradstreet +thoughtfully. "Of course there can be no doubt as to the nature +of this gang." + +"None at all," said Holmes. "They are coiners on a large scale, +and have used the machine to form the amalgam which has taken the +place of silver." + +"We have known for some time that a clever gang was at work," +said the inspector. "They have been turning out half-crowns by +the thousand. We even traced them as far as Reading, but could +get no farther, for they had covered their traces in a way that +showed that they were very old hands. But now, thanks to this +lucky chance, I think that we have got them right enough." + +But the inspector was mistaken, for those criminals were not +destined to fall into the hands of justice. As we rolled into +Eyford Station we saw a gigantic column of smoke which streamed +up from behind a small clump of trees in the neighbourhood and +hung like an immense ostrich feather over the landscape. + +"A house on fire?" asked Bradstreet as the train steamed off +again on its way. + +"Yes, sir!" said the station-master. + +"When did it break out?" + +"I hear that it was during the night, sir, but it has got worse, +and the whole place is in a blaze." + +"Whose house is it?" + +"Dr. Becher's." + +"Tell me," broke in the engineer, "is Dr. Becher a German, very +thin, with a long, sharp nose?" + +The station-master laughed heartily. "No, sir, Dr. Becher is an +Englishman, and there isn't a man in the parish who has a +better-lined waistcoat. But he has a gentleman staying with him, +a patient, as I understand, who is a foreigner, and he looks as +if a little good Berkshire beef would do him no harm." + +The station-master had not finished his speech before we were all +hastening in the direction of the fire. The road topped a low +hill, and there was a great widespread whitewashed building in +front of us, spouting fire at every chink and window, while in +the garden in front three fire-engines were vainly striving to +keep the flames under. + +"That's it!" cried Hatherley, in intense excitement. "There is +the gravel-drive, and there are the rose-bushes where I lay. That +second window is the one that I jumped from." + +"Well, at least," said Holmes, "you have had your revenge upon +them. There can be no question that it was your oil-lamp which, +when it was crushed in the press, set fire to the wooden walls, +though no doubt they were too excited in the chase after you to +observe it at the time. Now keep your eyes open in this crowd for +your friends of last night, though I very much fear that they are +a good hundred miles off by now." + +And Holmes' fears came to be realised, for from that day to this +no word has ever been heard either of the beautiful woman, the +sinister German, or the morose Englishman. Early that morning a +peasant had met a cart containing several people and some very +bulky boxes driving rapidly in the direction of Reading, but +there all traces of the fugitives disappeared, and even Holmes' +ingenuity failed ever to discover the least clue as to their +whereabouts. + +The firemen had been much perturbed at the strange arrangements +which they had found within, and still more so by discovering a +newly severed human thumb upon a window-sill of the second floor. +About sunset, however, their efforts were at last successful, and +they subdued the flames, but not before the roof had fallen in, +and the whole place been reduced to such absolute ruin that, save +some twisted cylinders and iron piping, not a trace remained of +the machinery which had cost our unfortunate acquaintance so +dearly. Large masses of nickel and of tin were discovered stored +in an out-house, but no coins were to be found, which may have +explained the presence of those bulky boxes which have been +already referred to. + +How our hydraulic engineer had been conveyed from the garden to +the spot where he recovered his senses might have remained +forever a mystery were it not for the soft mould, which told us a +very plain tale. He had evidently been carried down by two +persons, one of whom had remarkably small feet and the other +unusually large ones. On the whole, it was most probable that the +silent Englishman, being less bold or less murderous than his +companion, had assisted the woman to bear the unconscious man out +of the way of danger. + +"Well," said our engineer ruefully as we took our seats to return +once more to London, "it has been a pretty business for me! I +have lost my thumb and I have lost a fifty-guinea fee, and what +have I gained?" + +"Experience," said Holmes, laughing. "Indirectly it may be of +value, you know; you have only to put it into words to gain the +reputation of being excellent company for the remainder of your +existence." + + + +X. THE ADVENTURE OF THE NOBLE BACHELOR + +The Lord St. Simon marriage, and its curious termination, have +long ceased to be a subject of interest in those exalted circles +in which the unfortunate bridegroom moves. Fresh scandals have +eclipsed it, and their more piquant details have drawn the +gossips away from this four-year-old drama. As I have reason to +believe, however, that the full facts have never been revealed to +the general public, and as my friend Sherlock Holmes had a +considerable share in clearing the matter up, I feel that no +memoir of him would be complete without some little sketch of +this remarkable episode. + +It was a few weeks before my own marriage, during the days when I +was still sharing rooms with Holmes in Baker Street, that he came +home from an afternoon stroll to find a letter on the table +waiting for him. I had remained indoors all day, for the weather +had taken a sudden turn to rain, with high autumnal winds, and +the Jezail bullet which I had brought back in one of my limbs as +a relic of my Afghan campaign throbbed with dull persistence. +With my body in one easy-chair and my legs upon another, I had +surrounded myself with a cloud of newspapers until at last, +saturated with the news of the day, I tossed them all aside and +lay listless, watching the huge crest and monogram upon the +envelope upon the table and wondering lazily who my friend's +noble correspondent could be. + +"Here is a very fashionable epistle," I remarked as he entered. +"Your morning letters, if I remember right, were from a +fish-monger and a tide-waiter." + +"Yes, my correspondence has certainly the charm of variety," he +answered, smiling, "and the humbler are usually the more +interesting. This looks like one of those unwelcome social +summonses which call upon a man either to be bored or to lie." + +He broke the seal and glanced over the contents. + +"Oh, come, it may prove to be something of interest, after all." + +"Not social, then?" + +"No, distinctly professional." + +"And from a noble client?" + +"One of the highest in England." + +"My dear fellow, I congratulate you." + +"I assure you, Watson, without affectation, that the status of my +client is a matter of less moment to me than the interest of his +case. It is just possible, however, that that also may not be +wanting in this new investigation. You have been reading the +papers diligently of late, have you not?" + +"It looks like it," said I ruefully, pointing to a huge bundle in +the corner. "I have had nothing else to do." + +"It is fortunate, for you will perhaps be able to post me up. I +read nothing except the criminal news and the agony column. The +latter is always instructive. But if you have followed recent +events so closely you must have read about Lord St. Simon and his +wedding?" + +"Oh, yes, with the deepest interest." + +"That is well. The letter which I hold in my hand is from Lord +St. Simon. I will read it to you, and in return you must turn +over these papers and let me have whatever bears upon the matter. +This is what he says: + +"'MY DEAR MR. SHERLOCK HOLMES:--Lord Backwater tells me that I +may place implicit reliance upon your judgment and discretion. I +have determined, therefore, to call upon you and to consult you +in reference to the very painful event which has occurred in +connection with my wedding. Mr. Lestrade, of Scotland Yard, is +acting already in the matter, but he assures me that he sees no +objection to your co-operation, and that he even thinks that +it might be of some assistance. I will call at four o'clock in +the afternoon, and, should you have any other engagement at that +time, I hope that you will postpone it, as this matter is of +paramount importance. Yours faithfully, ST. SIMON.' + +"It is dated from Grosvenor Mansions, written with a quill pen, +and the noble lord has had the misfortune to get a smear of ink +upon the outer side of his right little finger," remarked Holmes +as he folded up the epistle. + +"He says four o'clock. It is three now. He will be here in an +hour." + +"Then I have just time, with your assistance, to get clear upon +the subject. Turn over those papers and arrange the extracts in +their order of time, while I take a glance as to who our client +is." He picked a red-covered volume from a line of books of +reference beside the mantelpiece. "Here he is," said he, sitting +down and flattening it out upon his knee. "'Lord Robert Walsingham +de Vere St. Simon, second son of the Duke of Balmoral.' Hum! 'Arms: +Azure, three caltrops in chief over a fess sable. Born in 1846.' +He's forty-one years of age, which is mature for marriage. Was +Under-Secretary for the colonies in a late administration. The +Duke, his father, was at one time Secretary for Foreign Affairs. +They inherit Plantagenet blood by direct descent, and Tudor on +the distaff side. Ha! Well, there is nothing very instructive in +all this. I think that I must turn to you Watson, for something +more solid." + +"I have very little difficulty in finding what I want," said I, +"for the facts are quite recent, and the matter struck me as +remarkable. I feared to refer them to you, however, as I knew +that you had an inquiry on hand and that you disliked the +intrusion of other matters." + +"Oh, you mean the little problem of the Grosvenor Square +furniture van. That is quite cleared up now--though, indeed, it +was obvious from the first. Pray give me the results of your +newspaper selections." + +"Here is the first notice which I can find. It is in the personal +column of the Morning Post, and dates, as you see, some weeks +back: 'A marriage has been arranged,' it says, 'and will, if +rumour is correct, very shortly take place, between Lord Robert +St. Simon, second son of the Duke of Balmoral, and Miss Hatty +Doran, the only daughter of Aloysius Doran. Esq., of San +Francisco, Cal., U.S.A.' That is all." + +"Terse and to the point," remarked Holmes, stretching his long, +thin legs towards the fire. + +"There was a paragraph amplifying this in one of the society +papers of the same week. Ah, here it is: 'There will soon be a +call for protection in the marriage market, for the present +free-trade principle appears to tell heavily against our home +product. One by one the management of the noble houses of Great +Britain is passing into the hands of our fair cousins from across +the Atlantic. An important addition has been made during the last +week to the list of the prizes which have been borne away by +these charming invaders. Lord St. Simon, who has shown himself +for over twenty years proof against the little god's arrows, has +now definitely announced his approaching marriage with Miss Hatty +Doran, the fascinating daughter of a California millionaire. Miss +Doran, whose graceful figure and striking face attracted much +attention at the Westbury House festivities, is an only child, +and it is currently reported that her dowry will run to +considerably over the six figures, with expectancies for the +future. As it is an open secret that the Duke of Balmoral has +been compelled to sell his pictures within the last few years, +and as Lord St. Simon has no property of his own save the small +estate of Birchmoor, it is obvious that the Californian heiress +is not the only gainer by an alliance which will enable her to +make the easy and common transition from a Republican lady to a +British peeress.'" + +"Anything else?" asked Holmes, yawning. + +"Oh, yes; plenty. Then there is another note in the Morning Post +to say that the marriage would be an absolutely quiet one, that it +would be at St. George's, Hanover Square, that only half a dozen +intimate friends would be invited, and that the party would +return to the furnished house at Lancaster Gate which has been +taken by Mr. Aloysius Doran. Two days later--that is, on +Wednesday last--there is a curt announcement that the wedding had +taken place, and that the honeymoon would be passed at Lord +Backwater's place, near Petersfield. Those are all the notices +which appeared before the disappearance of the bride." + +"Before the what?" asked Holmes with a start. + +"The vanishing of the lady." + +"When did she vanish, then?" + +"At the wedding breakfast." + +"Indeed. This is more interesting than it promised to be; quite +dramatic, in fact." + +"Yes; it struck me as being a little out of the common." + +"They often vanish before the ceremony, and occasionally during +the honeymoon; but I cannot call to mind anything quite so prompt +as this. Pray let me have the details." + +"I warn you that they are very incomplete." + +"Perhaps we may make them less so." + +"Such as they are, they are set forth in a single article of a +morning paper of yesterday, which I will read to you. It is +headed, 'Singular Occurrence at a Fashionable Wedding': + +"'The family of Lord Robert St. Simon has been thrown into the +greatest consternation by the strange and painful episodes which +have taken place in connection with his wedding. The ceremony, as +shortly announced in the papers of yesterday, occurred on the +previous morning; but it is only now that it has been possible to +confirm the strange rumours which have been so persistently +floating about. In spite of the attempts of the friends to hush +the matter up, so much public attention has now been drawn to it +that no good purpose can be served by affecting to disregard what +is a common subject for conversation. + +"'The ceremony, which was performed at St. George's, Hanover +Square, was a very quiet one, no one being present save the +father of the bride, Mr. Aloysius Doran, the Duchess of Balmoral, +Lord Backwater, Lord Eustace and Lady Clara St. Simon (the +younger brother and sister of the bridegroom), and Lady Alicia +Whittington. The whole party proceeded afterwards to the house of +Mr. Aloysius Doran, at Lancaster Gate, where breakfast had been +prepared. It appears that some little trouble was caused by a +woman, whose name has not been ascertained, who endeavoured to +force her way into the house after the bridal party, alleging +that she had some claim upon Lord St. Simon. It was only after a +painful and prolonged scene that she was ejected by the butler +and the footman. The bride, who had fortunately entered the house +before this unpleasant interruption, had sat down to breakfast +with the rest, when she complained of a sudden indisposition and +retired to her room. Her prolonged absence having caused some +comment, her father followed her, but learned from her maid that +she had only come up to her chamber for an instant, caught up an +ulster and bonnet, and hurried down to the passage. One of the +footmen declared that he had seen a lady leave the house thus +apparelled, but had refused to credit that it was his mistress, +believing her to be with the company. On ascertaining that his +daughter had disappeared, Mr. Aloysius Doran, in conjunction with +the bridegroom, instantly put themselves in communication with +the police, and very energetic inquiries are being made, which +will probably result in a speedy clearing up of this very +singular business. Up to a late hour last night, however, nothing +had transpired as to the whereabouts of the missing lady. There +are rumours of foul play in the matter, and it is said that the +police have caused the arrest of the woman who had caused the +original disturbance, in the belief that, from jealousy or some +other motive, she may have been concerned in the strange +disappearance of the bride.'" + +"And is that all?" + +"Only one little item in another of the morning papers, but it is +a suggestive one." + +"And it is--" + +"That Miss Flora Millar, the lady who had caused the disturbance, +has actually been arrested. It appears that she was formerly a +danseuse at the Allegro, and that she has known the bridegroom +for some years. There are no further particulars, and the whole +case is in your hands now--so far as it has been set forth in the +public press." + +"And an exceedingly interesting case it appears to be. I would +not have missed it for worlds. But there is a ring at the bell, +Watson, and as the clock makes it a few minutes after four, I +have no doubt that this will prove to be our noble client. Do not +dream of going, Watson, for I very much prefer having a witness, +if only as a check to my own memory." + +"Lord Robert St. Simon," announced our page-boy, throwing open +the door. A gentleman entered, with a pleasant, cultured face, +high-nosed and pale, with something perhaps of petulance about +the mouth, and with the steady, well-opened eye of a man whose +pleasant lot it had ever been to command and to be obeyed. His +manner was brisk, and yet his general appearance gave an undue +impression of age, for he had a slight forward stoop and a little +bend of the knees as he walked. His hair, too, as he swept off +his very curly-brimmed hat, was grizzled round the edges and thin +upon the top. As to his dress, it was careful to the verge of +foppishness, with high collar, black frock-coat, white waistcoat, +yellow gloves, patent-leather shoes, and light-coloured gaiters. +He advanced slowly into the room, turning his head from left to +right, and swinging in his right hand the cord which held his +golden eyeglasses. + +"Good-day, Lord St. Simon," said Holmes, rising and bowing. "Pray +take the basket-chair. This is my friend and colleague, Dr. +Watson. Draw up a little to the fire, and we will talk this +matter over." + +"A most painful matter to me, as you can most readily imagine, +Mr. Holmes. I have been cut to the quick. I understand that you +have already managed several delicate cases of this sort, sir, +though I presume that they were hardly from the same class of +society." + +"No, I am descending." + +"I beg pardon." + +"My last client of the sort was a king." + +"Oh, really! I had no idea. And which king?" + +"The King of Scandinavia." + +"What! Had he lost his wife?" + +"You can understand," said Holmes suavely, "that I extend to the +affairs of my other clients the same secrecy which I promise to +you in yours." + +"Of course! Very right! very right! I'm sure I beg pardon. As to +my own case, I am ready to give you any information which may +assist you in forming an opinion." + +"Thank you. I have already learned all that is in the public +prints, nothing more. I presume that I may take it as correct--this +article, for example, as to the disappearance of the bride." + +Lord St. Simon glanced over it. "Yes, it is correct, as far as it +goes." + +"But it needs a great deal of supplementing before anyone could +offer an opinion. I think that I may arrive at my facts most +directly by questioning you." + +"Pray do so." + +"When did you first meet Miss Hatty Doran?" + +"In San Francisco, a year ago." + +"You were travelling in the States?" + +"Yes." + +"Did you become engaged then?" + +"No." + +"But you were on a friendly footing?" + +"I was amused by her society, and she could see that I was +amused." + +"Her father is very rich?" + +"He is said to be the richest man on the Pacific slope." + +"And how did he make his money?" + +"In mining. He had nothing a few years ago. Then he struck gold, +invested it, and came up by leaps and bounds." + +"Now, what is your own impression as to the young lady's--your +wife's character?" + +The nobleman swung his glasses a little faster and stared down +into the fire. "You see, Mr. Holmes," said he, "my wife was +twenty before her father became a rich man. During that time she +ran free in a mining camp and wandered through woods or +mountains, so that her education has come from Nature rather than +from the schoolmaster. She is what we call in England a tomboy, +with a strong nature, wild and free, unfettered by any sort of +traditions. She is impetuous--volcanic, I was about to say. She +is swift in making up her mind and fearless in carrying out her +resolutions. On the other hand, I would not have given her the +name which I have the honour to bear"--he gave a little stately +cough--"had not I thought her to be at bottom a noble woman. I +believe that she is capable of heroic self-sacrifice and that +anything dishonourable would be repugnant to her." + +"Have you her photograph?" + +"I brought this with me." He opened a locket and showed us the +full face of a very lovely woman. It was not a photograph but an +ivory miniature, and the artist had brought out the full effect +of the lustrous black hair, the large dark eyes, and the +exquisite mouth. Holmes gazed long and earnestly at it. Then he +closed the locket and handed it back to Lord St. Simon. + +"The young lady came to London, then, and you renewed your +acquaintance?" + +"Yes, her father brought her over for this last London season. I +met her several times, became engaged to her, and have now +married her." + +"She brought, I understand, a considerable dowry?" + +"A fair dowry. Not more than is usual in my family." + +"And this, of course, remains to you, since the marriage is a +fait accompli?" + +"I really have made no inquiries on the subject." + +"Very naturally not. Did you see Miss Doran on the day before the +wedding?" + +"Yes." + +"Was she in good spirits?" + +"Never better. She kept talking of what we should do in our +future lives." + +"Indeed! That is very interesting. And on the morning of the +wedding?" + +"She was as bright as possible--at least until after the +ceremony." + +"And did you observe any change in her then?" + +"Well, to tell the truth, I saw then the first signs that I had +ever seen that her temper was just a little sharp. The incident +however, was too trivial to relate and can have no possible +bearing upon the case." + +"Pray let us have it, for all that." + +"Oh, it is childish. She dropped her bouquet as we went towards +the vestry. She was passing the front pew at the time, and it +fell over into the pew. There was a moment's delay, but the +gentleman in the pew handed it up to her again, and it did not +appear to be the worse for the fall. Yet when I spoke to her of +the matter, she answered me abruptly; and in the carriage, on our +way home, she seemed absurdly agitated over this trifling cause." + +"Indeed! You say that there was a gentleman in the pew. Some of +the general public were present, then?" + +"Oh, yes. It is impossible to exclude them when the church is +open." + +"This gentleman was not one of your wife's friends?" + +"No, no; I call him a gentleman by courtesy, but he was quite a +common-looking person. I hardly noticed his appearance. But +really I think that we are wandering rather far from the point." + +"Lady St. Simon, then, returned from the wedding in a less +cheerful frame of mind than she had gone to it. What did she do +on re-entering her father's house?" + +"I saw her in conversation with her maid." + +"And who is her maid?" + +"Alice is her name. She is an American and came from California +with her." + +"A confidential servant?" + +"A little too much so. It seemed to me that her mistress allowed +her to take great liberties. Still, of course, in America they +look upon these things in a different way." + +"How long did she speak to this Alice?" + +"Oh, a few minutes. I had something else to think of." + +"You did not overhear what they said?" + +"Lady St. Simon said something about 'jumping a claim.' She was +accustomed to use slang of the kind. I have no idea what she +meant." + +"American slang is very expressive sometimes. And what did your +wife do when she finished speaking to her maid?" + +"She walked into the breakfast-room." + +"On your arm?" + +"No, alone. She was very independent in little matters like that. +Then, after we had sat down for ten minutes or so, she rose +hurriedly, muttered some words of apology, and left the room. She +never came back." + +"But this maid, Alice, as I understand, deposes that she went to +her room, covered her bride's dress with a long ulster, put on a +bonnet, and went out." + +"Quite so. And she was afterwards seen walking into Hyde Park in +company with Flora Millar, a woman who is now in custody, and who +had already made a disturbance at Mr. Doran's house that +morning." + +"Ah, yes. I should like a few particulars as to this young lady, +and your relations to her." + +Lord St. Simon shrugged his shoulders and raised his eyebrows. +"We have been on a friendly footing for some years--I may say on +a very friendly footing. She used to be at the Allegro. I have +not treated her ungenerously, and she had no just cause of +complaint against me, but you know what women are, Mr. Holmes. +Flora was a dear little thing, but exceedingly hot-headed and +devotedly attached to me. She wrote me dreadful letters when she +heard that I was about to be married, and, to tell the truth, the +reason why I had the marriage celebrated so quietly was that I +feared lest there might be a scandal in the church. She came to +Mr. Doran's door just after we returned, and she endeavoured to +push her way in, uttering very abusive expressions towards my +wife, and even threatening her, but I had foreseen the +possibility of something of the sort, and I had two police +fellows there in private clothes, who soon pushed her out again. +She was quiet when she saw that there was no good in making a +row." + +"Did your wife hear all this?" + +"No, thank goodness, she did not." + +"And she was seen walking with this very woman afterwards?" + +"Yes. That is what Mr. Lestrade, of Scotland Yard, looks upon as +so serious. It is thought that Flora decoyed my wife out and laid +some terrible trap for her." + +"Well, it is a possible supposition." + +"You think so, too?" + +"I did not say a probable one. But you do not yourself look upon +this as likely?" + +"I do not think Flora would hurt a fly." + +"Still, jealousy is a strange transformer of characters. Pray +what is your own theory as to what took place?" + +"Well, really, I came to seek a theory, not to propound one. I +have given you all the facts. Since you ask me, however, I may +say that it has occurred to me as possible that the excitement of +this affair, the consciousness that she had made so immense a +social stride, had the effect of causing some little nervous +disturbance in my wife." + +"In short, that she had become suddenly deranged?" + +"Well, really, when I consider that she has turned her back--I +will not say upon me, but upon so much that many have aspired to +without success--I can hardly explain it in any other fashion." + +"Well, certainly that is also a conceivable hypothesis," said +Holmes, smiling. "And now, Lord St. Simon, I think that I have +nearly all my data. May I ask whether you were seated at the +breakfast-table so that you could see out of the window?" + +"We could see the other side of the road and the Park." + +"Quite so. Then I do not think that I need to detain you longer. +I shall communicate with you." + +"Should you be fortunate enough to solve this problem," said our +client, rising. + +"I have solved it." + +"Eh? What was that?" + +"I say that I have solved it." + +"Where, then, is my wife?" + +"That is a detail which I shall speedily supply." + +Lord St. Simon shook his head. "I am afraid that it will take +wiser heads than yours or mine," he remarked, and bowing in a +stately, old-fashioned manner he departed. + +"It is very good of Lord St. Simon to honour my head by putting +it on a level with his own," said Sherlock Holmes, laughing. "I +think that I shall have a whisky and soda and a cigar after all +this cross-questioning. I had formed my conclusions as to the +case before our client came into the room." + +"My dear Holmes!" + +"I have notes of several similar cases, though none, as I +remarked before, which were quite as prompt. My whole examination +served to turn my conjecture into a certainty. Circumstantial +evidence is occasionally very convincing, as when you find a +trout in the milk, to quote Thoreau's example." + +"But I have heard all that you have heard." + +"Without, however, the knowledge of pre-existing cases which +serves me so well. There was a parallel instance in Aberdeen some +years back, and something on very much the same lines at Munich +the year after the Franco-Prussian War. It is one of these +cases--but, hullo, here is Lestrade! Good-afternoon, Lestrade! +You will find an extra tumbler upon the sideboard, and there are +cigars in the box." + +The official detective was attired in a pea-jacket and cravat, +which gave him a decidedly nautical appearance, and he carried a +black canvas bag in his hand. With a short greeting he seated +himself and lit the cigar which had been offered to him. + +"What's up, then?" asked Holmes with a twinkle in his eye. "You +look dissatisfied." + +"And I feel dissatisfied. It is this infernal St. Simon marriage +case. I can make neither head nor tail of the business." + +"Really! You surprise me." + +"Who ever heard of such a mixed affair? Every clue seems to slip +through my fingers. I have been at work upon it all day." + +"And very wet it seems to have made you," said Holmes laying his +hand upon the arm of the pea-jacket. + +"Yes, I have been dragging the Serpentine." + +"In heaven's name, what for?" + +"In search of the body of Lady St. Simon." + +Sherlock Holmes leaned back in his chair and laughed heartily. + +"Have you dragged the basin of Trafalgar Square fountain?" he +asked. + +"Why? What do you mean?" + +"Because you have just as good a chance of finding this lady in +the one as in the other." + +Lestrade shot an angry glance at my companion. "I suppose you +know all about it," he snarled. + +"Well, I have only just heard the facts, but my mind is made up." + +"Oh, indeed! Then you think that the Serpentine plays no part in +the matter?" + +"I think it very unlikely." + +"Then perhaps you will kindly explain how it is that we found +this in it?" He opened his bag as he spoke, and tumbled onto the +floor a wedding-dress of watered silk, a pair of white satin +shoes and a bride's wreath and veil, all discoloured and soaked +in water. "There," said he, putting a new wedding-ring upon the +top of the pile. "There is a little nut for you to crack, Master +Holmes." + +"Oh, indeed!" said my friend, blowing blue rings into the air. +"You dragged them from the Serpentine?" + +"No. They were found floating near the margin by a park-keeper. +They have been identified as her clothes, and it seemed to me +that if the clothes were there the body would not be far off." + +"By the same brilliant reasoning, every man's body is to be found +in the neighbourhood of his wardrobe. And pray what did you hope +to arrive at through this?" + +"At some evidence implicating Flora Millar in the disappearance." + +"I am afraid that you will find it difficult." + +"Are you, indeed, now?" cried Lestrade with some bitterness. "I +am afraid, Holmes, that you are not very practical with your +deductions and your inferences. You have made two blunders in as +many minutes. This dress does implicate Miss Flora Millar." + +"And how?" + +"In the dress is a pocket. In the pocket is a card-case. In the +card-case is a note. And here is the very note." He slapped it +down upon the table in front of him. "Listen to this: 'You will +see me when all is ready. Come at once. F.H.M.' Now my theory all +along has been that Lady St. Simon was decoyed away by Flora +Millar, and that she, with confederates, no doubt, was +responsible for her disappearance. Here, signed with her +initials, is the very note which was no doubt quietly slipped +into her hand at the door and which lured her within their +reach." + +"Very good, Lestrade," said Holmes, laughing. "You really are +very fine indeed. Let me see it." He took up the paper in a +listless way, but his attention instantly became riveted, and he +gave a little cry of satisfaction. "This is indeed important," +said he. + +"Ha! you find it so?" + +"Extremely so. I congratulate you warmly." + +Lestrade rose in his triumph and bent his head to look. "Why," he +shrieked, "you're looking at the wrong side!" + +"On the contrary, this is the right side." + +"The right side? You're mad! Here is the note written in pencil +over here." + +"And over here is what appears to be the fragment of a hotel +bill, which interests me deeply." + +"There's nothing in it. I looked at it before," said Lestrade. +"'Oct. 4th, rooms 8s., breakfast 2s. 6d., cocktail 1s., lunch 2s. +6d., glass sherry, 8d.' I see nothing in that." + +"Very likely not. It is most important, all the same. As to the +note, it is important also, or at least the initials are, so I +congratulate you again." + +"I've wasted time enough," said Lestrade, rising. "I believe in +hard work and not in sitting by the fire spinning fine theories. +Good-day, Mr. Holmes, and we shall see which gets to the bottom +of the matter first." He gathered up the garments, thrust them +into the bag, and made for the door. + +"Just one hint to you, Lestrade," drawled Holmes before his rival +vanished; "I will tell you the true solution of the matter. Lady +St. Simon is a myth. There is not, and there never has been, any +such person." + +Lestrade looked sadly at my companion. Then he turned to me, +tapped his forehead three times, shook his head solemnly, and +hurried away. + +He had hardly shut the door behind him when Holmes rose to put on +his overcoat. "There is something in what the fellow says about +outdoor work," he remarked, "so I think, Watson, that I must +leave you to your papers for a little." + +It was after five o'clock when Sherlock Holmes left me, but I had +no time to be lonely, for within an hour there arrived a +confectioner's man with a very large flat box. This he unpacked +with the help of a youth whom he had brought with him, and +presently, to my very great astonishment, a quite epicurean +little cold supper began to be laid out upon our humble +lodging-house mahogany. There were a couple of brace of cold +woodcock, a pheasant, a pt de foie gras pie with a group of +ancient and cobwebby bottles. Having laid out all these luxuries, +my two visitors vanished away, like the genii of the Arabian +Nights, with no explanation save that the things had been paid +for and were ordered to this address. + +Just before nine o'clock Sherlock Holmes stepped briskly into the +room. His features were gravely set, but there was a light in his +eye which made me think that he had not been disappointed in his +conclusions. + +"They have laid the supper, then," he said, rubbing his hands. + +"You seem to expect company. They have laid for five." + +"Yes, I fancy we may have some company dropping in," said he. "I +am surprised that Lord St. Simon has not already arrived. Ha! I +fancy that I hear his step now upon the stairs." + +It was indeed our visitor of the afternoon who came bustling in, +dangling his glasses more vigorously than ever, and with a very +perturbed expression upon his aristocratic features. + +"My messenger reached you, then?" asked Holmes. + +"Yes, and I confess that the contents startled me beyond measure. +Have you good authority for what you say?" + +"The best possible." + +Lord St. Simon sank into a chair and passed his hand over his +forehead. + +"What will the Duke say," he murmured, "when he hears that one of +the family has been subjected to such humiliation?" + +"It is the purest accident. I cannot allow that there is any +humiliation." + +"Ah, you look on these things from another standpoint." + +"I fail to see that anyone is to blame. I can hardly see how the +lady could have acted otherwise, though her abrupt method of +doing it was undoubtedly to be regretted. Having no mother, she +had no one to advise her at such a crisis." + +"It was a slight, sir, a public slight," said Lord St. Simon, +tapping his fingers upon the table. + +"You must make allowance for this poor girl, placed in so +unprecedented a position." + +"I will make no allowance. I am very angry indeed, and I have +been shamefully used." + +"I think that I heard a ring," said Holmes. "Yes, there are steps +on the landing. If I cannot persuade you to take a lenient view +of the matter, Lord St. Simon, I have brought an advocate here +who may be more successful." He opened the door and ushered in a +lady and gentleman. "Lord St. Simon," said he "allow me to +introduce you to Mr. and Mrs. Francis Hay Moulton. The lady, I +think, you have already met." + +At the sight of these newcomers our client had sprung from his +seat and stood very erect, with his eyes cast down and his hand +thrust into the breast of his frock-coat, a picture of offended +dignity. The lady had taken a quick step forward and had held out +her hand to him, but he still refused to raise his eyes. It was +as well for his resolution, perhaps, for her pleading face was +one which it was hard to resist. + +"You're angry, Robert," said she. "Well, I guess you have every +cause to be." + +"Pray make no apology to me," said Lord St. Simon bitterly. + +"Oh, yes, I know that I have treated you real bad and that I +should have spoken to you before I went; but I was kind of +rattled, and from the time when I saw Frank here again I just +didn't know what I was doing or saying. I only wonder I didn't +fall down and do a faint right there before the altar." + +"Perhaps, Mrs. Moulton, you would like my friend and me to leave +the room while you explain this matter?" + +"If I may give an opinion," remarked the strange gentleman, +"we've had just a little too much secrecy over this business +already. For my part, I should like all Europe and America to +hear the rights of it." He was a small, wiry, sunburnt man, +clean-shaven, with a sharp face and alert manner. + +"Then I'll tell our story right away," said the lady. "Frank here +and I met in '84, in McQuire's camp, near the Rockies, where pa +was working a claim. We were engaged to each other, Frank and I; +but then one day father struck a rich pocket and made a pile, +while poor Frank here had a claim that petered out and came to +nothing. The richer pa grew the poorer was Frank; so at last pa +wouldn't hear of our engagement lasting any longer, and he took +me away to 'Frisco. Frank wouldn't throw up his hand, though; so +he followed me there, and he saw me without pa knowing anything +about it. It would only have made him mad to know, so we just +fixed it all up for ourselves. Frank said that he would go and +make his pile, too, and never come back to claim me until he had +as much as pa. So then I promised to wait for him to the end of +time and pledged myself not to marry anyone else while he lived. +'Why shouldn't we be married right away, then,' said he, 'and +then I will feel sure of you; and I won't claim to be your +husband until I come back?' Well, we talked it over, and he had +fixed it all up so nicely, with a clergyman all ready in waiting, +that we just did it right there; and then Frank went off to seek +his fortune, and I went back to pa. + +"The next I heard of Frank was that he was in Montana, and then +he went prospecting in Arizona, and then I heard of him from New +Mexico. After that came a long newspaper story about how a +miners' camp had been attacked by Apache Indians, and there was +my Frank's name among the killed. I fainted dead away, and I was +very sick for months after. Pa thought I had a decline and took +me to half the doctors in 'Frisco. Not a word of news came for a +year and more, so that I never doubted that Frank was really +dead. Then Lord St. Simon came to 'Frisco, and we came to London, +and a marriage was arranged, and pa was very pleased, but I felt +all the time that no man on this earth would ever take the place +in my heart that had been given to my poor Frank. + +"Still, if I had married Lord St. Simon, of course I'd have done +my duty by him. We can't command our love, but we can our +actions. I went to the altar with him with the intention to make +him just as good a wife as it was in me to be. But you may +imagine what I felt when, just as I came to the altar rails, I +glanced back and saw Frank standing and looking at me out of the +first pew. I thought it was his ghost at first; but when I looked +again there he was still, with a kind of question in his eyes, as +if to ask me whether I were glad or sorry to see him. I wonder I +didn't drop. I know that everything was turning round, and the +words of the clergyman were just like the buzz of a bee in my +ear. I didn't know what to do. Should I stop the service and make +a scene in the church? I glanced at him again, and he seemed to +know what I was thinking, for he raised his finger to his lips to +tell me to be still. Then I saw him scribble on a piece of paper, +and I knew that he was writing me a note. As I passed his pew on +the way out I dropped my bouquet over to him, and he slipped the +note into my hand when he returned me the flowers. It was only a +line asking me to join him when he made the sign to me to do so. +Of course I never doubted for a moment that my first duty was now +to him, and I determined to do just whatever he might direct. + +"When I got back I told my maid, who had known him in California, +and had always been his friend. I ordered her to say nothing, but +to get a few things packed and my ulster ready. I know I ought to +have spoken to Lord St. Simon, but it was dreadful hard before +his mother and all those great people. I just made up my mind to +run away and explain afterwards. I hadn't been at the table ten +minutes before I saw Frank out of the window at the other side of +the road. He beckoned to me and then began walking into the Park. +I slipped out, put on my things, and followed him. Some woman +came talking something or other about Lord St. Simon to +me--seemed to me from the little I heard as if he had a little +secret of his own before marriage also--but I managed to get away +from her and soon overtook Frank. We got into a cab together, and +away we drove to some lodgings he had taken in Gordon Square, and +that was my true wedding after all those years of waiting. Frank +had been a prisoner among the Apaches, had escaped, came on to +'Frisco, found that I had given him up for dead and had gone to +England, followed me there, and had come upon me at last on the +very morning of my second wedding." + +"I saw it in a paper," explained the American. "It gave the name +and the church but not where the lady lived." + +"Then we had a talk as to what we should do, and Frank was all +for openness, but I was so ashamed of it all that I felt as if I +should like to vanish away and never see any of them again--just +sending a line to pa, perhaps, to show him that I was alive. It +was awful to me to think of all those lords and ladies sitting +round that breakfast-table and waiting for me to come back. So +Frank took my wedding-clothes and things and made a bundle of +them, so that I should not be traced, and dropped them away +somewhere where no one could find them. It is likely that we +should have gone on to Paris to-morrow, only that this good +gentleman, Mr. Holmes, came round to us this evening, though how +he found us is more than I can think, and he showed us very +clearly and kindly that I was wrong and that Frank was right, and +that we should be putting ourselves in the wrong if we were so +secret. Then he offered to give us a chance of talking to Lord +St. Simon alone, and so we came right away round to his rooms at +once. Now, Robert, you have heard it all, and I am very sorry if +I have given you pain, and I hope that you do not think very +meanly of me." + +Lord St. Simon had by no means relaxed his rigid attitude, but +had listened with a frowning brow and a compressed lip to this +long narrative. + +"Excuse me," he said, "but it is not my custom to discuss my most +intimate personal affairs in this public manner." + +"Then you won't forgive me? You won't shake hands before I go?" + +"Oh, certainly, if it would give you any pleasure." He put out +his hand and coldly grasped that which she extended to him. + +"I had hoped," suggested Holmes, "that you would have joined us +in a friendly supper." + +"I think that there you ask a little too much," responded his +Lordship. "I may be forced to acquiesce in these recent +developments, but I can hardly be expected to make merry over +them. I think that with your permission I will now wish you all a +very good-night." He included us all in a sweeping bow and +stalked out of the room. + +"Then I trust that you at least will honour me with your +company," said Sherlock Holmes. "It is always a joy to meet an +American, Mr. Moulton, for I am one of those who believe that the +folly of a monarch and the blundering of a minister in far-gone +years will not prevent our children from being some day citizens +of the same world-wide country under a flag which shall be a +quartering of the Union Jack with the Stars and Stripes." + +"The case has been an interesting one," remarked Holmes when our +visitors had left us, "because it serves to show very clearly how +simple the explanation may be of an affair which at first sight +seems to be almost inexplicable. Nothing could be more natural +than the sequence of events as narrated by this lady, and nothing +stranger than the result when viewed, for instance, by Mr. +Lestrade of Scotland Yard." + +"You were not yourself at fault at all, then?" + +"From the first, two facts were very obvious to me, the one that +the lady had been quite willing to undergo the wedding ceremony, +the other that she had repented of it within a few minutes of +returning home. Obviously something had occurred during the +morning, then, to cause her to change her mind. What could that +something be? She could not have spoken to anyone when she was +out, for she had been in the company of the bridegroom. Had she +seen someone, then? If she had, it must be someone from America +because she had spent so short a time in this country that she +could hardly have allowed anyone to acquire so deep an influence +over her that the mere sight of him would induce her to change +her plans so completely. You see we have already arrived, by a +process of exclusion, at the idea that she might have seen an +American. Then who could this American be, and why should he +possess so much influence over her? It might be a lover; it might +be a husband. Her young womanhood had, I knew, been spent in +rough scenes and under strange conditions. So far I had got +before I ever heard Lord St. Simon's narrative. When he told us +of a man in a pew, of the change in the bride's manner, of so +transparent a device for obtaining a note as the dropping of a +bouquet, of her resort to her confidential maid, and of her very +significant allusion to claim-jumping--which in miners' parlance +means taking possession of that which another person has a prior +claim to--the whole situation became absolutely clear. She had +gone off with a man, and the man was either a lover or was a +previous husband--the chances being in favour of the latter." + +"And how in the world did you find them?" + +"It might have been difficult, but friend Lestrade held +information in his hands the value of which he did not himself +know. The initials were, of course, of the highest importance, +but more valuable still was it to know that within a week he had +settled his bill at one of the most select London hotels." + +"How did you deduce the select?" + +"By the select prices. Eight shillings for a bed and eightpence +for a glass of sherry pointed to one of the most expensive +hotels. There are not many in London which charge at that rate. +In the second one which I visited in Northumberland Avenue, I +learned by an inspection of the book that Francis H. Moulton, an +American gentleman, had left only the day before, and on looking +over the entries against him, I came upon the very items which I +had seen in the duplicate bill. His letters were to be forwarded +to 226 Gordon Square; so thither I travelled, and being fortunate +enough to find the loving couple at home, I ventured to give them +some paternal advice and to point out to them that it would be +better in every way that they should make their position a little +clearer both to the general public and to Lord St. Simon in +particular. I invited them to meet him here, and, as you see, I +made him keep the appointment." + +"But with no very good result," I remarked. "His conduct was +certainly not very gracious." + +"Ah, Watson," said Holmes, smiling, "perhaps you would not be +very gracious either, if, after all the trouble of wooing and +wedding, you found yourself deprived in an instant of wife and of +fortune. I think that we may judge Lord St. Simon very mercifully +and thank our stars that we are never likely to find ourselves in +the same position. Draw your chair up and hand me my violin, for +the only problem we have still to solve is how to while away +these bleak autumnal evenings." + + + +XI. THE ADVENTURE OF THE BERYL CORONET + +"Holmes," said I as I stood one morning in our bow-window looking +down the street, "here is a madman coming along. It seems rather +sad that his relatives should allow him to come out alone." + +My friend rose lazily from his armchair and stood with his hands +in the pockets of his dressing-gown, looking over my shoulder. It +was a bright, crisp February morning, and the snow of the day +before still lay deep upon the ground, shimmering brightly in the +wintry sun. Down the centre of Baker Street it had been ploughed +into a brown crumbly band by the traffic, but at either side and +on the heaped-up edges of the foot-paths it still lay as white as +when it fell. The grey pavement had been cleaned and scraped, but +was still dangerously slippery, so that there were fewer +passengers than usual. Indeed, from the direction of the +Metropolitan Station no one was coming save the single gentleman +whose eccentric conduct had drawn my attention. + +He was a man of about fifty, tall, portly, and imposing, with a +massive, strongly marked face and a commanding figure. He was +dressed in a sombre yet rich style, in black frock-coat, shining +hat, neat brown gaiters, and well-cut pearl-grey trousers. Yet +his actions were in absurd contrast to the dignity of his dress +and features, for he was running hard, with occasional little +springs, such as a weary man gives who is little accustomed to +set any tax upon his legs. As he ran he jerked his hands up and +down, waggled his head, and writhed his face into the most +extraordinary contortions. + +"What on earth can be the matter with him?" I asked. "He is +looking up at the numbers of the houses." + +"I believe that he is coming here," said Holmes, rubbing his +hands. + +"Here?" + +"Yes; I rather think he is coming to consult me professionally. I +think that I recognise the symptoms. Ha! did I not tell you?" As +he spoke, the man, puffing and blowing, rushed at our door and +pulled at our bell until the whole house resounded with the +clanging. + +A few moments later he was in our room, still puffing, still +gesticulating, but with so fixed a look of grief and despair in +his eyes that our smiles were turned in an instant to horror and +pity. For a while he could not get his words out, but swayed his +body and plucked at his hair like one who has been driven to the +extreme limits of his reason. Then, suddenly springing to his +feet, he beat his head against the wall with such force that we +both rushed upon him and tore him away to the centre of the room. +Sherlock Holmes pushed him down into the easy-chair and, sitting +beside him, patted his hand and chatted with him in the easy, +soothing tones which he knew so well how to employ. + +"You have come to me to tell your story, have you not?" said he. +"You are fatigued with your haste. Pray wait until you have +recovered yourself, and then I shall be most happy to look into +any little problem which you may submit to me." + +The man sat for a minute or more with a heaving chest, fighting +against his emotion. Then he passed his handkerchief over his +brow, set his lips tight, and turned his face towards us. + +"No doubt you think me mad?" said he. + +"I see that you have had some great trouble," responded Holmes. + +"God knows I have!--a trouble which is enough to unseat my +reason, so sudden and so terrible is it. Public disgrace I might +have faced, although I am a man whose character has never yet +borne a stain. Private affliction also is the lot of every man; +but the two coming together, and in so frightful a form, have +been enough to shake my very soul. Besides, it is not I alone. +The very noblest in the land may suffer unless some way be found +out of this horrible affair." + +"Pray compose yourself, sir," said Holmes, "and let me have a +clear account of who you are and what it is that has befallen +you." + +"My name," answered our visitor, "is probably familiar to your +ears. I am Alexander Holder, of the banking firm of Holder & +Stevenson, of Threadneedle Street." + +The name was indeed well known to us as belonging to the senior +partner in the second largest private banking concern in the City +of London. What could have happened, then, to bring one of the +foremost citizens of London to this most pitiable pass? We +waited, all curiosity, until with another effort he braced +himself to tell his story. + +"I feel that time is of value," said he; "that is why I hastened +here when the police inspector suggested that I should secure +your co-operation. I came to Baker Street by the Underground and +hurried from there on foot, for the cabs go slowly through this +snow. That is why I was so out of breath, for I am a man who +takes very little exercise. I feel better now, and I will put the +facts before you as shortly and yet as clearly as I can. + +"It is, of course, well known to you that in a successful banking +business as much depends upon our being able to find remunerative +investments for our funds as upon our increasing our connection +and the number of our depositors. One of our most lucrative means +of laying out money is in the shape of loans, where the security +is unimpeachable. We have done a good deal in this direction +during the last few years, and there are many noble families to +whom we have advanced large sums upon the security of their +pictures, libraries, or plate. + +"Yesterday morning I was seated in my office at the bank when a +card was brought in to me by one of the clerks. I started when I +saw the name, for it was that of none other than--well, perhaps +even to you I had better say no more than that it was a name +which is a household word all over the earth--one of the highest, +noblest, most exalted names in England. I was overwhelmed by the +honour and attempted, when he entered, to say so, but he plunged +at once into business with the air of a man who wishes to hurry +quickly through a disagreeable task. + +"'Mr. Holder,' said he, 'I have been informed that you are in the +habit of advancing money.' + +"'The firm does so when the security is good.' I answered. + +"'It is absolutely essential to me,' said he, 'that I should have +50,000 pounds at once. I could, of course, borrow so trifling a +sum ten times over from my friends, but I much prefer to make it +a matter of business and to carry out that business myself. In my +position you can readily understand that it is unwise to place +one's self under obligations.' + +"'For how long, may I ask, do you want this sum?' I asked. + +"'Next Monday I have a large sum due to me, and I shall then most +certainly repay what you advance, with whatever interest you +think it right to charge. But it is very essential to me that the +money should be paid at once.' + +"'I should be happy to advance it without further parley from my +own private purse,' said I, 'were it not that the strain would be +rather more than it could bear. If, on the other hand, I am to do +it in the name of the firm, then in justice to my partner I must +insist that, even in your case, every businesslike precaution +should be taken.' + +"'I should much prefer to have it so,' said he, raising up a +square, black morocco case which he had laid beside his chair. +'You have doubtless heard of the Beryl Coronet?' + +"'One of the most precious public possessions of the empire,' +said I. + +"'Precisely.' He opened the case, and there, imbedded in soft, +flesh-coloured velvet, lay the magnificent piece of jewellery +which he had named. 'There are thirty-nine enormous beryls,' said +he, 'and the price of the gold chasing is incalculable. The +lowest estimate would put the worth of the coronet at double the +sum which I have asked. I am prepared to leave it with you as my +security.' + +"I took the precious case into my hands and looked in some +perplexity from it to my illustrious client. + +"'You doubt its value?' he asked. + +"'Not at all. I only doubt--' + +"'The propriety of my leaving it. You may set your mind at rest +about that. I should not dream of doing so were it not absolutely +certain that I should be able in four days to reclaim it. It is a +pure matter of form. Is the security sufficient?' + +"'Ample.' + +"'You understand, Mr. Holder, that I am giving you a strong proof +of the confidence which I have in you, founded upon all that I +have heard of you. I rely upon you not only to be discreet and to +refrain from all gossip upon the matter but, above all, to +preserve this coronet with every possible precaution because I +need not say that a great public scandal would be caused if any +harm were to befall it. Any injury to it would be almost as +serious as its complete loss, for there are no beryls in the +world to match these, and it would be impossible to replace them. +I leave it with you, however, with every confidence, and I shall +call for it in person on Monday morning.' + +"Seeing that my client was anxious to leave, I said no more but, +calling for my cashier, I ordered him to pay over fifty 1000 +pound notes. When I was alone once more, however, with the +precious case lying upon the table in front of me, I could not +but think with some misgivings of the immense responsibility +which it entailed upon me. There could be no doubt that, as it +was a national possession, a horrible scandal would ensue if any +misfortune should occur to it. I already regretted having ever +consented to take charge of it. However, it was too late to alter +the matter now, so I locked it up in my private safe and turned +once more to my work. + +"When evening came I felt that it would be an imprudence to leave +so precious a thing in the office behind me. Bankers' safes had +been forced before now, and why should not mine be? If so, how +terrible would be the position in which I should find myself! I +determined, therefore, that for the next few days I would always +carry the case backward and forward with me, so that it might +never be really out of my reach. With this intention, I called a +cab and drove out to my house at Streatham, carrying the jewel +with me. I did not breathe freely until I had taken it upstairs +and locked it in the bureau of my dressing-room. + +"And now a word as to my household, Mr. Holmes, for I wish you to +thoroughly understand the situation. My groom and my page sleep +out of the house, and may be set aside altogether. I have three +maid-servants who have been with me a number of years and whose +absolute reliability is quite above suspicion. Another, Lucy +Parr, the second waiting-maid, has only been in my service a few +months. She came with an excellent character, however, and has +always given me satisfaction. She is a very pretty girl and has +attracted admirers who have occasionally hung about the place. +That is the only drawback which we have found to her, but we +believe her to be a thoroughly good girl in every way. + +"So much for the servants. My family itself is so small that it +will not take me long to describe it. I am a widower and have an +only son, Arthur. He has been a disappointment to me, Mr. +Holmes--a grievous disappointment. I have no doubt that I am +myself to blame. People tell me that I have spoiled him. Very +likely I have. When my dear wife died I felt that he was all I +had to love. I could not bear to see the smile fade even for a +moment from his face. I have never denied him a wish. Perhaps it +would have been better for both of us had I been sterner, but I +meant it for the best. + +"It was naturally my intention that he should succeed me in my +business, but he was not of a business turn. He was wild, +wayward, and, to speak the truth, I could not trust him in the +handling of large sums of money. When he was young he became a +member of an aristocratic club, and there, having charming +manners, he was soon the intimate of a number of men with long +purses and expensive habits. He learned to play heavily at cards +and to squander money on the turf, until he had again and again +to come to me and implore me to give him an advance upon his +allowance, that he might settle his debts of honour. He tried +more than once to break away from the dangerous company which he +was keeping, but each time the influence of his friend, Sir +George Burnwell, was enough to draw him back again. + +"And, indeed, I could not wonder that such a man as Sir George +Burnwell should gain an influence over him, for he has frequently +brought him to my house, and I have found myself that I could +hardly resist the fascination of his manner. He is older than +Arthur, a man of the world to his finger-tips, one who had been +everywhere, seen everything, a brilliant talker, and a man of +great personal beauty. Yet when I think of him in cold blood, far +away from the glamour of his presence, I am convinced from his +cynical speech and the look which I have caught in his eyes that +he is one who should be deeply distrusted. So I think, and so, +too, thinks my little Mary, who has a woman's quick insight into +character. + +"And now there is only she to be described. She is my niece; but +when my brother died five years ago and left her alone in the +world I adopted her, and have looked upon her ever since as my +daughter. She is a sunbeam in my house--sweet, loving, beautiful, +a wonderful manager and housekeeper, yet as tender and quiet and +gentle as a woman could be. She is my right hand. I do not know +what I could do without her. In only one matter has she ever gone +against my wishes. Twice my boy has asked her to marry him, for +he loves her devotedly, but each time she has refused him. I +think that if anyone could have drawn him into the right path it +would have been she, and that his marriage might have changed his +whole life; but now, alas! it is too late--forever too late! + +"Now, Mr. Holmes, you know the people who live under my roof, and +I shall continue with my miserable story. + +"When we were taking coffee in the drawing-room that night after +dinner, I told Arthur and Mary my experience, and of the precious +treasure which we had under our roof, suppressing only the name +of my client. Lucy Parr, who had brought in the coffee, had, I am +sure, left the room; but I cannot swear that the door was closed. +Mary and Arthur were much interested and wished to see the famous +coronet, but I thought it better not to disturb it. + +"'Where have you put it?' asked Arthur. + +"'In my own bureau.' + +"'Well, I hope to goodness the house won't be burgled during the +night.' said he. + +"'It is locked up,' I answered. + +"'Oh, any old key will fit that bureau. When I was a youngster I +have opened it myself with the key of the box-room cupboard.' + +"He often had a wild way of talking, so that I thought little of +what he said. He followed me to my room, however, that night with +a very grave face. + +"'Look here, dad,' said he with his eyes cast down, 'can you let +me have 200 pounds?' + +"'No, I cannot!' I answered sharply. 'I have been far too +generous with you in money matters.' + +"'You have been very kind,' said he, 'but I must have this money, +or else I can never show my face inside the club again.' + +"'And a very good thing, too!' I cried. + +"'Yes, but you would not have me leave it a dishonoured man,' +said he. 'I could not bear the disgrace. I must raise the money +in some way, and if you will not let me have it, then I must try +other means.' + +"I was very angry, for this was the third demand during the +month. 'You shall not have a farthing from me,' I cried, on which +he bowed and left the room without another word. + +"When he was gone I unlocked my bureau, made sure that my +treasure was safe, and locked it again. Then I started to go +round the house to see that all was secure--a duty which I +usually leave to Mary but which I thought it well to perform +myself that night. As I came down the stairs I saw Mary herself +at the side window of the hall, which she closed and fastened as +I approached. + +"'Tell me, dad,' said she, looking, I thought, a little +disturbed, 'did you give Lucy, the maid, leave to go out +to-night?' + +"'Certainly not.' + +"'She came in just now by the back door. I have no doubt that she +has only been to the side gate to see someone, but I think that +it is hardly safe and should be stopped.' + +"'You must speak to her in the morning, or I will if you prefer +it. Are you sure that everything is fastened?' + +"'Quite sure, dad.' + +"'Then, good-night.' I kissed her and went up to my bedroom +again, where I was soon asleep. + +"I am endeavouring to tell you everything, Mr. Holmes, which may +have any bearing upon the case, but I beg that you will question +me upon any point which I do not make clear." + +"On the contrary, your statement is singularly lucid." + +"I come to a part of my story now in which I should wish to be +particularly so. I am not a very heavy sleeper, and the anxiety +in my mind tended, no doubt, to make me even less so than usual. +About two in the morning, then, I was awakened by some sound in +the house. It had ceased ere I was wide awake, but it had left an +impression behind it as though a window had gently closed +somewhere. I lay listening with all my ears. Suddenly, to my +horror, there was a distinct sound of footsteps moving softly in +the next room. I slipped out of bed, all palpitating with fear, +and peeped round the corner of my dressing-room door. + +"'Arthur!' I screamed, 'you villain! you thief! How dare you +touch that coronet?' + +"The gas was half up, as I had left it, and my unhappy boy, +dressed only in his shirt and trousers, was standing beside the +light, holding the coronet in his hands. He appeared to be +wrenching at it, or bending it with all his strength. At my cry +he dropped it from his grasp and turned as pale as death. I +snatched it up and examined it. One of the gold corners, with +three of the beryls in it, was missing. + +"'You blackguard!' I shouted, beside myself with rage. 'You have +destroyed it! You have dishonoured me forever! Where are the +jewels which you have stolen?' + +"'Stolen!' he cried. + +"'Yes, thief!' I roared, shaking him by the shoulder. + +"'There are none missing. There cannot be any missing,' said he. + +"'There are three missing. And you know where they are. Must I +call you a liar as well as a thief? Did I not see you trying to +tear off another piece?' + +"'You have called me names enough,' said he, 'I will not stand it +any longer. I shall not say another word about this business, +since you have chosen to insult me. I will leave your house in +the morning and make my own way in the world.' + +"'You shall leave it in the hands of the police!' I cried +half-mad with grief and rage. 'I shall have this matter probed to +the bottom.' + +"'You shall learn nothing from me,' said he with a passion such +as I should not have thought was in his nature. 'If you choose to +call the police, let the police find what they can.' + +"By this time the whole house was astir, for I had raised my +voice in my anger. Mary was the first to rush into my room, and, +at the sight of the coronet and of Arthur's face, she read the +whole story and, with a scream, fell down senseless on the +ground. I sent the house-maid for the police and put the +investigation into their hands at once. When the inspector and a +constable entered the house, Arthur, who had stood sullenly with +his arms folded, asked me whether it was my intention to charge +him with theft. I answered that it had ceased to be a private +matter, but had become a public one, since the ruined coronet was +national property. I was determined that the law should have its +way in everything. + +"'At least,' said he, 'you will not have me arrested at once. It +would be to your advantage as well as mine if I might leave the +house for five minutes.' + +"'That you may get away, or perhaps that you may conceal what you +have stolen,' said I. And then, realising the dreadful position +in which I was placed, I implored him to remember that not only +my honour but that of one who was far greater than I was at +stake; and that he threatened to raise a scandal which would +convulse the nation. He might avert it all if he would but tell +me what he had done with the three missing stones. + +"'You may as well face the matter,' said I; 'you have been caught +in the act, and no confession could make your guilt more heinous. +If you but make such reparation as is in your power, by telling +us where the beryls are, all shall be forgiven and forgotten.' + +"'Keep your forgiveness for those who ask for it,' he answered, +turning away from me with a sneer. I saw that he was too hardened +for any words of mine to influence him. There was but one way for +it. I called in the inspector and gave him into custody. A search +was made at once not only of his person but of his room and of +every portion of the house where he could possibly have concealed +the gems; but no trace of them could be found, nor would the +wretched boy open his mouth for all our persuasions and our +threats. This morning he was removed to a cell, and I, after +going through all the police formalities, have hurried round to +you to implore you to use your skill in unravelling the matter. +The police have openly confessed that they can at present make +nothing of it. You may go to any expense which you think +necessary. I have already offered a reward of 1000 pounds. My +God, what shall I do! I have lost my honour, my gems, and my son +in one night. Oh, what shall I do!" + +He put a hand on either side of his head and rocked himself to +and fro, droning to himself like a child whose grief has got +beyond words. + +Sherlock Holmes sat silent for some few minutes, with his brows +knitted and his eyes fixed upon the fire. + +"Do you receive much company?" he asked. + +"None save my partner with his family and an occasional friend of +Arthur's. Sir George Burnwell has been several times lately. No +one else, I think." + +"Do you go out much in society?" + +"Arthur does. Mary and I stay at home. We neither of us care for +it." + +"That is unusual in a young girl." + +"She is of a quiet nature. Besides, she is not so very young. She +is four-and-twenty." + +"This matter, from what you say, seems to have been a shock to +her also." + +"Terrible! She is even more affected than I." + +"You have neither of you any doubt as to your son's guilt?" + +"How can we have when I saw him with my own eyes with the coronet +in his hands." + +"I hardly consider that a conclusive proof. Was the remainder of +the coronet at all injured?" + +"Yes, it was twisted." + +"Do you not think, then, that he might have been trying to +straighten it?" + +"God bless you! You are doing what you can for him and for me. +But it is too heavy a task. What was he doing there at all? If +his purpose were innocent, why did he not say so?" + +"Precisely. And if it were guilty, why did he not invent a lie? +His silence appears to me to cut both ways. There are several +singular points about the case. What did the police think of the +noise which awoke you from your sleep?" + +"They considered that it might be caused by Arthur's closing his +bedroom door." + +"A likely story! As if a man bent on felony would slam his door +so as to wake a household. What did they say, then, of the +disappearance of these gems?" + +"They are still sounding the planking and probing the furniture +in the hope of finding them." + +"Have they thought of looking outside the house?" + +"Yes, they have shown extraordinary energy. The whole garden has +already been minutely examined." + +"Now, my dear sir," said Holmes, "is it not obvious to you now +that this matter really strikes very much deeper than either you +or the police were at first inclined to think? It appeared to you +to be a simple case; to me it seems exceedingly complex. Consider +what is involved by your theory. You suppose that your son came +down from his bed, went, at great risk, to your dressing-room, +opened your bureau, took out your coronet, broke off by main +force a small portion of it, went off to some other place, +concealed three gems out of the thirty-nine, with such skill that +nobody can find them, and then returned with the other thirty-six +into the room in which he exposed himself to the greatest danger +of being discovered. I ask you now, is such a theory tenable?" + +"But what other is there?" cried the banker with a gesture of +despair. "If his motives were innocent, why does he not explain +them?" + +"It is our task to find that out," replied Holmes; "so now, if +you please, Mr. Holder, we will set off for Streatham together, +and devote an hour to glancing a little more closely into +details." + +My friend insisted upon my accompanying them in their expedition, +which I was eager enough to do, for my curiosity and sympathy +were deeply stirred by the story to which we had listened. I +confess that the guilt of the banker's son appeared to me to be +as obvious as it did to his unhappy father, but still I had such +faith in Holmes' judgment that I felt that there must be some +grounds for hope as long as he was dissatisfied with the accepted +explanation. He hardly spoke a word the whole way out to the +southern suburb, but sat with his chin upon his breast and his +hat drawn over his eyes, sunk in the deepest thought. Our client +appeared to have taken fresh heart at the little glimpse of hope +which had been presented to him, and he even broke into a +desultory chat with me over his business affairs. A short railway +journey and a shorter walk brought us to Fairbank, the modest +residence of the great financier. + +Fairbank was a good-sized square house of white stone, standing +back a little from the road. A double carriage-sweep, with a +snow-clad lawn, stretched down in front to two large iron gates +which closed the entrance. On the right side was a small wooden +thicket, which led into a narrow path between two neat hedges +stretching from the road to the kitchen door, and forming the +tradesmen's entrance. On the left ran a lane which led to the +stables, and was not itself within the grounds at all, being a +public, though little used, thoroughfare. Holmes left us standing +at the door and walked slowly all round the house, across the +front, down the tradesmen's path, and so round by the garden +behind into the stable lane. So long was he that Mr. Holder and I +went into the dining-room and waited by the fire until he should +return. We were sitting there in silence when the door opened and +a young lady came in. She was rather above the middle height, +slim, with dark hair and eyes, which seemed the darker against +the absolute pallor of her skin. I do not think that I have ever +seen such deadly paleness in a woman's face. Her lips, too, were +bloodless, but her eyes were flushed with crying. As she swept +silently into the room she impressed me with a greater sense of +grief than the banker had done in the morning, and it was the +more striking in her as she was evidently a woman of strong +character, with immense capacity for self-restraint. Disregarding +my presence, she went straight to her uncle and passed her hand +over his head with a sweet womanly caress. + +"You have given orders that Arthur should be liberated, have you +not, dad?" she asked. + +"No, no, my girl, the matter must be probed to the bottom." + +"But I am so sure that he is innocent. You know what woman's +instincts are. I know that he has done no harm and that you will +be sorry for having acted so harshly." + +"Why is he silent, then, if he is innocent?" + +"Who knows? Perhaps because he was so angry that you should +suspect him." + +"How could I help suspecting him, when I actually saw him with +the coronet in his hand?" + +"Oh, but he had only picked it up to look at it. Oh, do, do take +my word for it that he is innocent. Let the matter drop and say +no more. It is so dreadful to think of our dear Arthur in +prison!" + +"I shall never let it drop until the gems are found--never, Mary! +Your affection for Arthur blinds you as to the awful consequences +to me. Far from hushing the thing up, I have brought a gentleman +down from London to inquire more deeply into it." + +"This gentleman?" she asked, facing round to me. + +"No, his friend. He wished us to leave him alone. He is round in +the stable lane now." + +"The stable lane?" She raised her dark eyebrows. "What can he +hope to find there? Ah! this, I suppose, is he. I trust, sir, +that you will succeed in proving, what I feel sure is the truth, +that my cousin Arthur is innocent of this crime." + +"I fully share your opinion, and I trust, with you, that we may +prove it," returned Holmes, going back to the mat to knock the +snow from his shoes. "I believe I have the honour of addressing +Miss Mary Holder. Might I ask you a question or two?" + +"Pray do, sir, if it may help to clear this horrible affair up." + +"You heard nothing yourself last night?" + +"Nothing, until my uncle here began to speak loudly. I heard +that, and I came down." + +"You shut up the windows and doors the night before. Did you +fasten all the windows?" + +"Yes." + +"Were they all fastened this morning?" + +"Yes." + +"You have a maid who has a sweetheart? I think that you remarked +to your uncle last night that she had been out to see him?" + +"Yes, and she was the girl who waited in the drawing-room, and +who may have heard uncle's remarks about the coronet." + +"I see. You infer that she may have gone out to tell her +sweetheart, and that the two may have planned the robbery." + +"But what is the good of all these vague theories," cried the +banker impatiently, "when I have told you that I saw Arthur with +the coronet in his hands?" + +"Wait a little, Mr. Holder. We must come back to that. About this +girl, Miss Holder. You saw her return by the kitchen door, I +presume?" + +"Yes; when I went to see if the door was fastened for the night I +met her slipping in. I saw the man, too, in the gloom." + +"Do you know him?" + +"Oh, yes! he is the green-grocer who brings our vegetables round. +His name is Francis Prosper." + +"He stood," said Holmes, "to the left of the door--that is to +say, farther up the path than is necessary to reach the door?" + +"Yes, he did." + +"And he is a man with a wooden leg?" + +Something like fear sprang up in the young lady's expressive +black eyes. "Why, you are like a magician," said she. "How do you +know that?" She smiled, but there was no answering smile in +Holmes' thin, eager face. + +"I should be very glad now to go upstairs," said he. "I shall +probably wish to go over the outside of the house again. Perhaps +I had better take a look at the lower windows before I go up." + +He walked swiftly round from one to the other, pausing only at +the large one which looked from the hall onto the stable lane. +This he opened and made a very careful examination of the sill +with his powerful magnifying lens. "Now we shall go upstairs," +said he at last. + +The banker's dressing-room was a plainly furnished little +chamber, with a grey carpet, a large bureau, and a long mirror. +Holmes went to the bureau first and looked hard at the lock. + +"Which key was used to open it?" he asked. + +"That which my son himself indicated--that of the cupboard of the +lumber-room." + +"Have you it here?" + +"That is it on the dressing-table." + +Sherlock Holmes took it up and opened the bureau. + +"It is a noiseless lock," said he. "It is no wonder that it did +not wake you. This case, I presume, contains the coronet. We must +have a look at it." He opened the case, and taking out the diadem +he laid it upon the table. It was a magnificent specimen of the +jeweller's art, and the thirty-six stones were the finest that I +have ever seen. At one side of the coronet was a cracked edge, +where a corner holding three gems had been torn away. + +"Now, Mr. Holder," said Holmes, "here is the corner which +corresponds to that which has been so unfortunately lost. Might I +beg that you will break it off." + +The banker recoiled in horror. "I should not dream of trying," +said he. + +"Then I will." Holmes suddenly bent his strength upon it, but +without result. "I feel it give a little," said he; "but, though +I am exceptionally strong in the fingers, it would take me all my +time to break it. An ordinary man could not do it. Now, what do +you think would happen if I did break it, Mr. Holder? There would +be a noise like a pistol shot. Do you tell me that all this +happened within a few yards of your bed and that you heard +nothing of it?" + +"I do not know what to think. It is all dark to me." + +"But perhaps it may grow lighter as we go. What do you think, +Miss Holder?" + +"I confess that I still share my uncle's perplexity." + +"Your son had no shoes or slippers on when you saw him?" + +"He had nothing on save only his trousers and shirt." + +"Thank you. We have certainly been favoured with extraordinary +luck during this inquiry, and it will be entirely our own fault +if we do not succeed in clearing the matter up. With your +permission, Mr. Holder, I shall now continue my investigations +outside." + +He went alone, at his own request, for he explained that any +unnecessary footmarks might make his task more difficult. For an +hour or more he was at work, returning at last with his feet +heavy with snow and his features as inscrutable as ever. + +"I think that I have seen now all that there is to see, Mr. +Holder," said he; "I can serve you best by returning to my +rooms." + +"But the gems, Mr. Holmes. Where are they?" + +"I cannot tell." + +The banker wrung his hands. "I shall never see them again!" he +cried. "And my son? You give me hopes?" + +"My opinion is in no way altered." + +"Then, for God's sake, what was this dark business which was +acted in my house last night?" + +"If you can call upon me at my Baker Street rooms to-morrow +morning between nine and ten I shall be happy to do what I can to +make it clearer. I understand that you give me carte blanche to +act for you, provided only that I get back the gems, and that you +place no limit on the sum I may draw." + +"I would give my fortune to have them back." + +"Very good. I shall look into the matter between this and then. +Good-bye; it is just possible that I may have to come over here +again before evening." + +It was obvious to me that my companion's mind was now made up +about the case, although what his conclusions were was more than +I could even dimly imagine. Several times during our homeward +journey I endeavoured to sound him upon the point, but he always +glided away to some other topic, until at last I gave it over in +despair. It was not yet three when we found ourselves in our +rooms once more. He hurried to his chamber and was down again in +a few minutes dressed as a common loafer. With his collar turned +up, his shiny, seedy coat, his red cravat, and his worn boots, he +was a perfect sample of the class. + +"I think that this should do," said he, glancing into the glass +above the fireplace. "I only wish that you could come with me, +Watson, but I fear that it won't do. I may be on the trail in +this matter, or I may be following a will-o'-the-wisp, but I +shall soon know which it is. I hope that I may be back in a few +hours." He cut a slice of beef from the joint upon the sideboard, +sandwiched it between two rounds of bread, and thrusting this +rude meal into his pocket he started off upon his expedition. + +I had just finished my tea when he returned, evidently in +excellent spirits, swinging an old elastic-sided boot in his +hand. He chucked it down into a corner and helped himself to a +cup of tea. + +"I only looked in as I passed," said he. "I am going right on." + +"Where to?" + +"Oh, to the other side of the West End. It may be some time +before I get back. Don't wait up for me in case I should be +late." + +"How are you getting on?" + +"Oh, so so. Nothing to complain of. I have been out to Streatham +since I saw you last, but I did not call at the house. It is a +very sweet little problem, and I would not have missed it for a +good deal. However, I must not sit gossiping here, but must get +these disreputable clothes off and return to my highly +respectable self." + +I could see by his manner that he had stronger reasons for +satisfaction than his words alone would imply. His eyes twinkled, +and there was even a touch of colour upon his sallow cheeks. He +hastened upstairs, and a few minutes later I heard the slam of +the hall door, which told me that he was off once more upon his +congenial hunt. + +I waited until midnight, but there was no sign of his return, so +I retired to my room. It was no uncommon thing for him to be away +for days and nights on end when he was hot upon a scent, so that +his lateness caused me no surprise. I do not know at what hour he +came in, but when I came down to breakfast in the morning there +he was with a cup of coffee in one hand and the paper in the +other, as fresh and trim as possible. + +"You will excuse my beginning without you, Watson," said he, "but +you remember that our client has rather an early appointment this +morning." + +"Why, it is after nine now," I answered. "I should not be +surprised if that were he. I thought I heard a ring." + +It was, indeed, our friend the financier. I was shocked by the +change which had come over him, for his face which was naturally +of a broad and massive mould, was now pinched and fallen in, +while his hair seemed to me at least a shade whiter. He entered +with a weariness and lethargy which was even more painful than +his violence of the morning before, and he dropped heavily into +the armchair which I pushed forward for him. + +"I do not know what I have done to be so severely tried," said +he. "Only two days ago I was a happy and prosperous man, without +a care in the world. Now I am left to a lonely and dishonoured +age. One sorrow comes close upon the heels of another. My niece, +Mary, has deserted me." + +"Deserted you?" + +"Yes. Her bed this morning had not been slept in, her room was +empty, and a note for me lay upon the hall table. I had said to +her last night, in sorrow and not in anger, that if she had +married my boy all might have been well with him. Perhaps it was +thoughtless of me to say so. It is to that remark that she refers +in this note: + +"'MY DEAREST UNCLE:--I feel that I have brought trouble upon you, +and that if I had acted differently this terrible misfortune +might never have occurred. I cannot, with this thought in my +mind, ever again be happy under your roof, and I feel that I must +leave you forever. Do not worry about my future, for that is +provided for; and, above all, do not search for me, for it will +be fruitless labour and an ill-service to me. In life or in +death, I am ever your loving,--MARY.' + +"What could she mean by that note, Mr. Holmes? Do you think it +points to suicide?" + +"No, no, nothing of the kind. It is perhaps the best possible +solution. I trust, Mr. Holder, that you are nearing the end of +your troubles." + +"Ha! You say so! You have heard something, Mr. Holmes; you have +learned something! Where are the gems?" + +"You would not think 1000 pounds apiece an excessive sum for +them?" + +"I would pay ten." + +"That would be unnecessary. Three thousand will cover the matter. +And there is a little reward, I fancy. Have you your check-book? +Here is a pen. Better make it out for 4000 pounds." + +With a dazed face the banker made out the required check. Holmes +walked over to his desk, took out a little triangular piece of +gold with three gems in it, and threw it down upon the table. + +With a shriek of joy our client clutched it up. + +"You have it!" he gasped. "I am saved! I am saved!" + +The reaction of joy was as passionate as his grief had been, and +he hugged his recovered gems to his bosom. + +"There is one other thing you owe, Mr. Holder," said Sherlock +Holmes rather sternly. + +"Owe!" He caught up a pen. "Name the sum, and I will pay it." + +"No, the debt is not to me. You owe a very humble apology to that +noble lad, your son, who has carried himself in this matter as I +should be proud to see my own son do, should I ever chance to +have one." + +"Then it was not Arthur who took them?" + +"I told you yesterday, and I repeat to-day, that it was not." + +"You are sure of it! Then let us hurry to him at once to let him +know that the truth is known." + +"He knows it already. When I had cleared it all up I had an +interview with him, and finding that he would not tell me the +story, I told it to him, on which he had to confess that I was +right and to add the very few details which were not yet quite +clear to me. Your news of this morning, however, may open his +lips." + +"For heaven's sake, tell me, then, what is this extraordinary +mystery!" + +"I will do so, and I will show you the steps by which I reached +it. And let me say to you, first, that which it is hardest for me +to say and for you to hear: there has been an understanding +between Sir George Burnwell and your niece Mary. They have now +fled together." + +"My Mary? Impossible!" + +"It is unfortunately more than possible; it is certain. Neither +you nor your son knew the true character of this man when you +admitted him into your family circle. He is one of the most +dangerous men in England--a ruined gambler, an absolutely +desperate villain, a man without heart or conscience. Your niece +knew nothing of such men. When he breathed his vows to her, as he +had done to a hundred before her, she flattered herself that she +alone had touched his heart. The devil knows best what he said, +but at least she became his tool and was in the habit of seeing +him nearly every evening." + +"I cannot, and I will not, believe it!" cried the banker with an +ashen face. + +"I will tell you, then, what occurred in your house last night. +Your niece, when you had, as she thought, gone to your room, +slipped down and talked to her lover through the window which +leads into the stable lane. His footmarks had pressed right +through the snow, so long had he stood there. She told him of the +coronet. His wicked lust for gold kindled at the news, and he +bent her to his will. I have no doubt that she loved you, but +there are women in whom the love of a lover extinguishes all +other loves, and I think that she must have been one. She had +hardly listened to his instructions when she saw you coming +downstairs, on which she closed the window rapidly and told you +about one of the servants' escapade with her wooden-legged lover, +which was all perfectly true. + +"Your boy, Arthur, went to bed after his interview with you but +he slept badly on account of his uneasiness about his club debts. +In the middle of the night he heard a soft tread pass his door, +so he rose and, looking out, was surprised to see his cousin +walking very stealthily along the passage until she disappeared +into your dressing-room. Petrified with astonishment, the lad +slipped on some clothes and waited there in the dark to see what +would come of this strange affair. Presently she emerged from the +room again, and in the light of the passage-lamp your son saw +that she carried the precious coronet in her hands. She passed +down the stairs, and he, thrilling with horror, ran along and +slipped behind the curtain near your door, whence he could see +what passed in the hall beneath. He saw her stealthily open the +window, hand out the coronet to someone in the gloom, and then +closing it once more hurry back to her room, passing quite close +to where he stood hid behind the curtain. + +"As long as she was on the scene he could not take any action +without a horrible exposure of the woman whom he loved. But the +instant that she was gone he realised how crushing a misfortune +this would be for you, and how all-important it was to set it +right. He rushed down, just as he was, in his bare feet, opened +the window, sprang out into the snow, and ran down the lane, +where he could see a dark figure in the moonlight. Sir George +Burnwell tried to get away, but Arthur caught him, and there was +a struggle between them, your lad tugging at one side of the +coronet, and his opponent at the other. In the scuffle, your son +struck Sir George and cut him over the eye. Then something +suddenly snapped, and your son, finding that he had the coronet +in his hands, rushed back, closed the window, ascended to your +room, and had just observed that the coronet had been twisted in +the struggle and was endeavouring to straighten it when you +appeared upon the scene." + +"Is it possible?" gasped the banker. + +"You then roused his anger by calling him names at a moment when +he felt that he had deserved your warmest thanks. He could not +explain the true state of affairs without betraying one who +certainly deserved little enough consideration at his hands. He +took the more chivalrous view, however, and preserved her +secret." + +"And that was why she shrieked and fainted when she saw the +coronet," cried Mr. Holder. "Oh, my God! what a blind fool I have +been! And his asking to be allowed to go out for five minutes! +The dear fellow wanted to see if the missing piece were at the +scene of the struggle. How cruelly I have misjudged him!" + +"When I arrived at the house," continued Holmes, "I at once went +very carefully round it to observe if there were any traces in +the snow which might help me. I knew that none had fallen since +the evening before, and also that there had been a strong frost +to preserve impressions. I passed along the tradesmen's path, but +found it all trampled down and indistinguishable. Just beyond it, +however, at the far side of the kitchen door, a woman had stood +and talked with a man, whose round impressions on one side showed +that he had a wooden leg. I could even tell that they had been +disturbed, for the woman had run back swiftly to the door, as was +shown by the deep toe and light heel marks, while Wooden-leg had +waited a little, and then had gone away. I thought at the time +that this might be the maid and her sweetheart, of whom you had +already spoken to me, and inquiry showed it was so. I passed +round the garden without seeing anything more than random tracks, +which I took to be the police; but when I got into the stable +lane a very long and complex story was written in the snow in +front of me. + +"There was a double line of tracks of a booted man, and a second +double line which I saw with delight belonged to a man with naked +feet. I was at once convinced from what you had told me that the +latter was your son. The first had walked both ways, but the +other had run swiftly, and as his tread was marked in places over +the depression of the boot, it was obvious that he had passed +after the other. I followed them up and found they led to the +hall window, where Boots had worn all the snow away while +waiting. Then I walked to the other end, which was a hundred +yards or more down the lane. I saw where Boots had faced round, +where the snow was cut up as though there had been a struggle, +and, finally, where a few drops of blood had fallen, to show me +that I was not mistaken. Boots had then run down the lane, and +another little smudge of blood showed that it was he who had been +hurt. When he came to the highroad at the other end, I found that +the pavement had been cleared, so there was an end to that clue. + +"On entering the house, however, I examined, as you remember, the +sill and framework of the hall window with my lens, and I could +at once see that someone had passed out. I could distinguish the +outline of an instep where the wet foot had been placed in coming +in. I was then beginning to be able to form an opinion as to what +had occurred. A man had waited outside the window; someone had +brought the gems; the deed had been overseen by your son; he had +pursued the thief; had struggled with him; they had each tugged +at the coronet, their united strength causing injuries which +neither alone could have effected. He had returned with the +prize, but had left a fragment in the grasp of his opponent. So +far I was clear. The question now was, who was the man and who +was it brought him the coronet? + +"It is an old maxim of mine that when you have excluded the +impossible, whatever remains, however improbable, must be the +truth. Now, I knew that it was not you who had brought it down, +so there only remained your niece and the maids. But if it were +the maids, why should your son allow himself to be accused in +their place? There could be no possible reason. As he loved his +cousin, however, there was an excellent explanation why he should +retain her secret--the more so as the secret was a disgraceful +one. When I remembered that you had seen her at that window, and +how she had fainted on seeing the coronet again, my conjecture +became a certainty. + +"And who could it be who was her confederate? A lover evidently, +for who else could outweigh the love and gratitude which she must +feel to you? I knew that you went out little, and that your +circle of friends was a very limited one. But among them was Sir +George Burnwell. I had heard of him before as being a man of evil +reputation among women. It must have been he who wore those boots +and retained the missing gems. Even though he knew that Arthur +had discovered him, he might still flatter himself that he was +safe, for the lad could not say a word without compromising his +own family. + +"Well, your own good sense will suggest what measures I took +next. I went in the shape of a loafer to Sir George's house, +managed to pick up an acquaintance with his valet, learned that +his master had cut his head the night before, and, finally, at +the expense of six shillings, made all sure by buying a pair of +his cast-off shoes. With these I journeyed down to Streatham and +saw that they exactly fitted the tracks." + +"I saw an ill-dressed vagabond in the lane yesterday evening," +said Mr. Holder. + +"Precisely. It was I. I found that I had my man, so I came home +and changed my clothes. It was a delicate part which I had to +play then, for I saw that a prosecution must be avoided to avert +scandal, and I knew that so astute a villain would see that our +hands were tied in the matter. I went and saw him. At first, of +course, he denied everything. But when I gave him every +particular that had occurred, he tried to bluster and took down a +life-preserver from the wall. I knew my man, however, and I +clapped a pistol to his head before he could strike. Then he +became a little more reasonable. I told him that we would give +him a price for the stones he held--1000 pounds apiece. That +brought out the first signs of grief that he had shown. 'Why, +dash it all!' said he, 'I've let them go at six hundred for the +three!' I soon managed to get the address of the receiver who had +them, on promising him that there would be no prosecution. Off I +set to him, and after much chaffering I got our stones at 1000 +pounds apiece. Then I looked in upon your son, told him that all +was right, and eventually got to my bed about two o'clock, after +what I may call a really hard day's work." + +"A day which has saved England from a great public scandal," said +the banker, rising. "Sir, I cannot find words to thank you, but +you shall not find me ungrateful for what you have done. Your +skill has indeed exceeded all that I have heard of it. And now I +must fly to my dear boy to apologise to him for the wrong which I +have done him. As to what you tell me of poor Mary, it goes to my +very heart. Not even your skill can inform me where she is now." + +"I think that we may safely say," returned Holmes, "that she is +wherever Sir George Burnwell is. It is equally certain, too, that +whatever her sins are, they will soon receive a more than +sufficient punishment." + + + +XII. THE ADVENTURE OF THE COPPER BEECHES + +"To the man who loves art for its own sake," remarked Sherlock +Holmes, tossing aside the advertisement sheet of the Daily +Telegraph, "it is frequently in its least important and lowliest +manifestations that the keenest pleasure is to be derived. It is +pleasant to me to observe, Watson, that you have so far grasped +this truth that in these little records of our cases which you +have been good enough to draw up, and, I am bound to say, +occasionally to embellish, you have given prominence not so much +to the many causes clbres and sensational trials in which I +have figured but rather to those incidents which may have been +trivial in themselves, but which have given room for those +faculties of deduction and of logical synthesis which I have made +my special province." + +"And yet," said I, smiling, "I cannot quite hold myself absolved +from the charge of sensationalism which has been urged against my +records." + +"You have erred, perhaps," he observed, taking up a glowing +cinder with the tongs and lighting with it the long cherry-wood +pipe which was wont to replace his clay when he was in a +disputatious rather than a meditative mood--"you have erred +perhaps in attempting to put colour and life into each of your +statements instead of confining yourself to the task of placing +upon record that severe reasoning from cause to effect which is +really the only notable feature about the thing." + +"It seems to me that I have done you full justice in the matter," +I remarked with some coldness, for I was repelled by the egotism +which I had more than once observed to be a strong factor in my +friend's singular character. + +"No, it is not selfishness or conceit," said he, answering, as +was his wont, my thoughts rather than my words. "If I claim full +justice for my art, it is because it is an impersonal thing--a +thing beyond myself. Crime is common. Logic is rare. Therefore it +is upon the logic rather than upon the crime that you should +dwell. You have degraded what should have been a course of +lectures into a series of tales." + +It was a cold morning of the early spring, and we sat after +breakfast on either side of a cheery fire in the old room at +Baker Street. A thick fog rolled down between the lines of +dun-coloured houses, and the opposing windows loomed like dark, +shapeless blurs through the heavy yellow wreaths. Our gas was lit +and shone on the white cloth and glimmer of china and metal, for +the table had not been cleared yet. Sherlock Holmes had been +silent all the morning, dipping continuously into the +advertisement columns of a succession of papers until at last, +having apparently given up his search, he had emerged in no very +sweet temper to lecture me upon my literary shortcomings. + +"At the same time," he remarked after a pause, during which he +had sat puffing at his long pipe and gazing down into the fire, +"you can hardly be open to a charge of sensationalism, for out of +these cases which you have been so kind as to interest yourself +in, a fair proportion do not treat of crime, in its legal sense, +at all. The small matter in which I endeavoured to help the King +of Bohemia, the singular experience of Miss Mary Sutherland, the +problem connected with the man with the twisted lip, and the +incident of the noble bachelor, were all matters which are +outside the pale of the law. But in avoiding the sensational, I +fear that you may have bordered on the trivial." + +"The end may have been so," I answered, "but the methods I hold +to have been novel and of interest." + +"Pshaw, my dear fellow, what do the public, the great unobservant +public, who could hardly tell a weaver by his tooth or a +compositor by his left thumb, care about the finer shades of +analysis and deduction! But, indeed, if you are trivial, I cannot +blame you, for the days of the great cases are past. Man, or at +least criminal man, has lost all enterprise and originality. As +to my own little practice, it seems to be degenerating into an +agency for recovering lost lead pencils and giving advice to +young ladies from boarding-schools. I think that I have touched +bottom at last, however. This note I had this morning marks my +zero-point, I fancy. Read it!" He tossed a crumpled letter across +to me. + +It was dated from Montague Place upon the preceding evening, and +ran thus: + +"DEAR MR. HOLMES:--I am very anxious to consult you as to whether +I should or should not accept a situation which has been offered +to me as governess. I shall call at half-past ten to-morrow if I +do not inconvenience you. Yours faithfully, + "VIOLET HUNTER." + +"Do you know the young lady?" I asked. + +"Not I." + +"It is half-past ten now." + +"Yes, and I have no doubt that is her ring." + +"It may turn out to be of more interest than you think. You +remember that the affair of the blue carbuncle, which appeared to +be a mere whim at first, developed into a serious investigation. +It may be so in this case, also." + +"Well, let us hope so. But our doubts will very soon be solved, +for here, unless I am much mistaken, is the person in question." + +As he spoke the door opened and a young lady entered the room. +She was plainly but neatly dressed, with a bright, quick face, +freckled like a plover's egg, and with the brisk manner of a +woman who has had her own way to make in the world. + +"You will excuse my troubling you, I am sure," said she, as my +companion rose to greet her, "but I have had a very strange +experience, and as I have no parents or relations of any sort +from whom I could ask advice, I thought that perhaps you would be +kind enough to tell me what I should do." + +"Pray take a seat, Miss Hunter. I shall be happy to do anything +that I can to serve you." + +I could see that Holmes was favourably impressed by the manner +and speech of his new client. He looked her over in his searching +fashion, and then composed himself, with his lids drooping and +his finger-tips together, to listen to her story. + +"I have been a governess for five years," said she, "in the +family of Colonel Spence Munro, but two months ago the colonel +received an appointment at Halifax, in Nova Scotia, and took his +children over to America with him, so that I found myself without +a situation. I advertised, and I answered advertisements, but +without success. At last the little money which I had saved began +to run short, and I was at my wit's end as to what I should do. + +"There is a well-known agency for governesses in the West End +called Westaway's, and there I used to call about once a week in +order to see whether anything had turned up which might suit me. +Westaway was the name of the founder of the business, but it is +really managed by Miss Stoper. She sits in her own little office, +and the ladies who are seeking employment wait in an anteroom, +and are then shown in one by one, when she consults her ledgers +and sees whether she has anything which would suit them. + +"Well, when I called last week I was shown into the little office +as usual, but I found that Miss Stoper was not alone. A +prodigiously stout man with a very smiling face and a great heavy +chin which rolled down in fold upon fold over his throat sat at +her elbow with a pair of glasses on his nose, looking very +earnestly at the ladies who entered. As I came in he gave quite a +jump in his chair and turned quickly to Miss Stoper. + +"'That will do,' said he; 'I could not ask for anything better. +Capital! capital!' He seemed quite enthusiastic and rubbed his +hands together in the most genial fashion. He was such a +comfortable-looking man that it was quite a pleasure to look at +him. + +"'You are looking for a situation, miss?' he asked. + +"'Yes, sir.' + +"'As governess?' + +"'Yes, sir.' + +"'And what salary do you ask?' + +"'I had 4 pounds a month in my last place with Colonel Spence +Munro.' + +"'Oh, tut, tut! sweating--rank sweating!' he cried, throwing his +fat hands out into the air like a man who is in a boiling +passion. 'How could anyone offer so pitiful a sum to a lady with +such attractions and accomplishments?' + +"'My accomplishments, sir, may be less than you imagine,' said I. +'A little French, a little German, music, and drawing--' + +"'Tut, tut!' he cried. 'This is all quite beside the question. +The point is, have you or have you not the bearing and deportment +of a lady? There it is in a nutshell. If you have not, you are +not fitted for the rearing of a child who may some day play a +considerable part in the history of the country. But if you have +why, then, how could any gentleman ask you to condescend to +accept anything under the three figures? Your salary with me, +madam, would commence at 100 pounds a year.' + +"You may imagine, Mr. Holmes, that to me, destitute as I was, +such an offer seemed almost too good to be true. The gentleman, +however, seeing perhaps the look of incredulity upon my face, +opened a pocket-book and took out a note. + +"'It is also my custom,' said he, smiling in the most pleasant +fashion until his eyes were just two little shining slits amid +the white creases of his face, 'to advance to my young ladies +half their salary beforehand, so that they may meet any little +expenses of their journey and their wardrobe.' + +"It seemed to me that I had never met so fascinating and so +thoughtful a man. As I was already in debt to my tradesmen, the +advance was a great convenience, and yet there was something +unnatural about the whole transaction which made me wish to know +a little more before I quite committed myself. + +"'May I ask where you live, sir?' said I. + +"'Hampshire. Charming rural place. The Copper Beeches, five miles +on the far side of Winchester. It is the most lovely country, my +dear young lady, and the dearest old country-house.' + +"'And my duties, sir? I should be glad to know what they would +be.' + +"'One child--one dear little romper just six years old. Oh, if +you could see him killing cockroaches with a slipper! Smack! +smack! smack! Three gone before you could wink!' He leaned back +in his chair and laughed his eyes into his head again. + +"I was a little startled at the nature of the child's amusement, +but the father's laughter made me think that perhaps he was +joking. + +"'My sole duties, then,' I asked, 'are to take charge of a single +child?' + +"'No, no, not the sole, not the sole, my dear young lady,' he +cried. 'Your duty would be, as I am sure your good sense would +suggest, to obey any little commands my wife might give, provided +always that they were such commands as a lady might with +propriety obey. You see no difficulty, heh?' + +"'I should be happy to make myself useful.' + +"'Quite so. In dress now, for example. We are faddy people, you +know--faddy but kind-hearted. If you were asked to wear any dress +which we might give you, you would not object to our little whim. +Heh?' + +"'No,' said I, considerably astonished at his words. + +"'Or to sit here, or sit there, that would not be offensive to +you?' + +"'Oh, no.' + +"'Or to cut your hair quite short before you come to us?' + +"I could hardly believe my ears. As you may observe, Mr. Holmes, +my hair is somewhat luxuriant, and of a rather peculiar tint of +chestnut. It has been considered artistic. I could not dream of +sacrificing it in this offhand fashion. + +"'I am afraid that that is quite impossible,' said I. He had been +watching me eagerly out of his small eyes, and I could see a +shadow pass over his face as I spoke. + +"'I am afraid that it is quite essential,' said he. 'It is a +little fancy of my wife's, and ladies' fancies, you know, madam, +ladies' fancies must be consulted. And so you won't cut your +hair?' + +"'No, sir, I really could not,' I answered firmly. + +"'Ah, very well; then that quite settles the matter. It is a +pity, because in other respects you would really have done very +nicely. In that case, Miss Stoper, I had best inspect a few more +of your young ladies.' + +"The manageress had sat all this while busy with her papers +without a word to either of us, but she glanced at me now with so +much annoyance upon her face that I could not help suspecting +that she had lost a handsome commission through my refusal. + +"'Do you desire your name to be kept upon the books?' she asked. + +"'If you please, Miss Stoper.' + +"'Well, really, it seems rather useless, since you refuse the +most excellent offers in this fashion,' said she sharply. 'You +can hardly expect us to exert ourselves to find another such +opening for you. Good-day to you, Miss Hunter.' She struck a gong +upon the table, and I was shown out by the page. + +"Well, Mr. Holmes, when I got back to my lodgings and found +little enough in the cupboard, and two or three bills upon the +table, I began to ask myself whether I had not done a very +foolish thing. After all, if these people had strange fads and +expected obedience on the most extraordinary matters, they were +at least ready to pay for their eccentricity. Very few +governesses in England are getting 100 pounds a year. Besides, +what use was my hair to me? Many people are improved by wearing +it short and perhaps I should be among the number. Next day I was +inclined to think that I had made a mistake, and by the day after +I was sure of it. I had almost overcome my pride so far as to go +back to the agency and inquire whether the place was still open +when I received this letter from the gentleman himself. I have it +here and I will read it to you: + + "'The Copper Beeches, near Winchester. +"'DEAR MISS HUNTER:--Miss Stoper has very kindly given me your +address, and I write from here to ask you whether you have +reconsidered your decision. My wife is very anxious that you +should come, for she has been much attracted by my description of +you. We are willing to give 30 pounds a quarter, or 120 pounds a +year, so as to recompense you for any little inconvenience which +our fads may cause you. They are not very exacting, after all. My +wife is fond of a particular shade of electric blue and would +like you to wear such a dress indoors in the morning. You need +not, however, go to the expense of purchasing one, as we have one +belonging to my dear daughter Alice (now in Philadelphia), which +would, I should think, fit you very well. Then, as to sitting +here or there, or amusing yourself in any manner indicated, that +need cause you no inconvenience. As regards your hair, it is no +doubt a pity, especially as I could not help remarking its beauty +during our short interview, but I am afraid that I must remain +firm upon this point, and I only hope that the increased salary +may recompense you for the loss. Your duties, as far as the child +is concerned, are very light. Now do try to come, and I shall +meet you with the dog-cart at Winchester. Let me know your train. +Yours faithfully, JEPHRO RUCASTLE.' + +"That is the letter which I have just received, Mr. Holmes, and +my mind is made up that I will accept it. I thought, however, +that before taking the final step I should like to submit the +whole matter to your consideration." + +"Well, Miss Hunter, if your mind is made up, that settles the +question," said Holmes, smiling. + +"But you would not advise me to refuse?" + +"I confess that it is not the situation which I should like to +see a sister of mine apply for." + +"What is the meaning of it all, Mr. Holmes?" + +"Ah, I have no data. I cannot tell. Perhaps you have yourself +formed some opinion?" + +"Well, there seems to me to be only one possible solution. Mr. +Rucastle seemed to be a very kind, good-natured man. Is it not +possible that his wife is a lunatic, that he desires to keep the +matter quiet for fear she should be taken to an asylum, and that +he humours her fancies in every way in order to prevent an +outbreak?" + +"That is a possible solution--in fact, as matters stand, it is +the most probable one. But in any case it does not seem to be a +nice household for a young lady." + +"But the money, Mr. Holmes, the money!" + +"Well, yes, of course the pay is good--too good. That is what +makes me uneasy. Why should they give you 120 pounds a year, when +they could have their pick for 40 pounds? There must be some +strong reason behind." + +"I thought that if I told you the circumstances you would +understand afterwards if I wanted your help. I should feel so +much stronger if I felt that you were at the back of me." + +"Oh, you may carry that feeling away with you. I assure you that +your little problem promises to be the most interesting which has +come my way for some months. There is something distinctly novel +about some of the features. If you should find yourself in doubt +or in danger--" + +"Danger! What danger do you foresee?" + +Holmes shook his head gravely. "It would cease to be a danger if +we could define it," said he. "But at any time, day or night, a +telegram would bring me down to your help." + +"That is enough." She rose briskly from her chair with the +anxiety all swept from her face. "I shall go down to Hampshire +quite easy in my mind now. I shall write to Mr. Rucastle at once, +sacrifice my poor hair to-night, and start for Winchester +to-morrow." With a few grateful words to Holmes she bade us both +good-night and bustled off upon her way. + +"At least," said I as we heard her quick, firm steps descending +the stairs, "she seems to be a young lady who is very well able +to take care of herself." + +"And she would need to be," said Holmes gravely. "I am much +mistaken if we do not hear from her before many days are past." + +It was not very long before my friend's prediction was fulfilled. +A fortnight went by, during which I frequently found my thoughts +turning in her direction and wondering what strange side-alley of +human experience this lonely woman had strayed into. The unusual +salary, the curious conditions, the light duties, all pointed to +something abnormal, though whether a fad or a plot, or whether +the man were a philanthropist or a villain, it was quite beyond +my powers to determine. As to Holmes, I observed that he sat +frequently for half an hour on end, with knitted brows and an +abstracted air, but he swept the matter away with a wave of his +hand when I mentioned it. "Data! data! data!" he cried +impatiently. "I can't make bricks without clay." And yet he would +always wind up by muttering that no sister of his should ever +have accepted such a situation. + +The telegram which we eventually received came late one night +just as I was thinking of turning in and Holmes was settling down +to one of those all-night chemical researches which he frequently +indulged in, when I would leave him stooping over a retort and a +test-tube at night and find him in the same position when I came +down to breakfast in the morning. He opened the yellow envelope, +and then, glancing at the message, threw it across to me. + +"Just look up the trains in Bradshaw," said he, and turned back +to his chemical studies. + +The summons was a brief and urgent one. + +"Please be at the Black Swan Hotel at Winchester at midday +to-morrow," it said. "Do come! I am at my wit's end. HUNTER." + +"Will you come with me?" asked Holmes, glancing up. + +"I should wish to." + +"Just look it up, then." + +"There is a train at half-past nine," said I, glancing over my +Bradshaw. "It is due at Winchester at 11:30." + +"That will do very nicely. Then perhaps I had better postpone my +analysis of the acetones, as we may need to be at our best in the +morning." + +By eleven o'clock the next day we were well upon our way to the +old English capital. Holmes had been buried in the morning papers +all the way down, but after we had passed the Hampshire border he +threw them down and began to admire the scenery. It was an ideal +spring day, a light blue sky, flecked with little fleecy white +clouds drifting across from west to east. The sun was shining +very brightly, and yet there was an exhilarating nip in the air, +which set an edge to a man's energy. All over the countryside, +away to the rolling hills around Aldershot, the little red and +grey roofs of the farm-steadings peeped out from amid the light +green of the new foliage. + +"Are they not fresh and beautiful?" I cried with all the +enthusiasm of a man fresh from the fogs of Baker Street. + +But Holmes shook his head gravely. + +"Do you know, Watson," said he, "that it is one of the curses of +a mind with a turn like mine that I must look at everything with +reference to my own special subject. You look at these scattered +houses, and you are impressed by their beauty. I look at them, +and the only thought which comes to me is a feeling of their +isolation and of the impunity with which crime may be committed +there." + +"Good heavens!" I cried. "Who would associate crime with these +dear old homesteads?" + +"They always fill me with a certain horror. It is my belief, +Watson, founded upon my experience, that the lowest and vilest +alleys in London do not present a more dreadful record of sin +than does the smiling and beautiful countryside." + +"You horrify me!" + +"But the reason is very obvious. The pressure of public opinion +can do in the town what the law cannot accomplish. There is no +lane so vile that the scream of a tortured child, or the thud of +a drunkard's blow, does not beget sympathy and indignation among +the neighbours, and then the whole machinery of justice is ever +so close that a word of complaint can set it going, and there is +but a step between the crime and the dock. But look at these +lonely houses, each in its own fields, filled for the most part +with poor ignorant folk who know little of the law. Think of the +deeds of hellish cruelty, the hidden wickedness which may go on, +year in, year out, in such places, and none the wiser. Had this +lady who appeals to us for help gone to live in Winchester, I +should never have had a fear for her. It is the five miles of +country which makes the danger. Still, it is clear that she is +not personally threatened." + +"No. If she can come to Winchester to meet us she can get away." + +"Quite so. She has her freedom." + +"What CAN be the matter, then? Can you suggest no explanation?" + +"I have devised seven separate explanations, each of which would +cover the facts as far as we know them. But which of these is +correct can only be determined by the fresh information which we +shall no doubt find waiting for us. Well, there is the tower of +the cathedral, and we shall soon learn all that Miss Hunter has +to tell." + +The Black Swan is an inn of repute in the High Street, at no +distance from the station, and there we found the young lady +waiting for us. She had engaged a sitting-room, and our lunch +awaited us upon the table. + +"I am so delighted that you have come," she said earnestly. "It +is so very kind of you both; but indeed I do not know what I +should do. Your advice will be altogether invaluable to me." + +"Pray tell us what has happened to you." + +"I will do so, and I must be quick, for I have promised Mr. +Rucastle to be back before three. I got his leave to come into +town this morning, though he little knew for what purpose." + +"Let us have everything in its due order." Holmes thrust his long +thin legs out towards the fire and composed himself to listen. + +"In the first place, I may say that I have met, on the whole, +with no actual ill-treatment from Mr. and Mrs. Rucastle. It is +only fair to them to say that. But I cannot understand them, and +I am not easy in my mind about them." + +"What can you not understand?" + +"Their reasons for their conduct. But you shall have it all just +as it occurred. When I came down, Mr. Rucastle met me here and +drove me in his dog-cart to the Copper Beeches. It is, as he +said, beautifully situated, but it is not beautiful in itself, +for it is a large square block of a house, whitewashed, but all +stained and streaked with damp and bad weather. There are grounds +round it, woods on three sides, and on the fourth a field which +slopes down to the Southampton highroad, which curves past about +a hundred yards from the front door. This ground in front belongs +to the house, but the woods all round are part of Lord +Southerton's preserves. A clump of copper beeches immediately in +front of the hall door has given its name to the place. + +"I was driven over by my employer, who was as amiable as ever, +and was introduced by him that evening to his wife and the child. +There was no truth, Mr. Holmes, in the conjecture which seemed to +us to be probable in your rooms at Baker Street. Mrs. Rucastle is +not mad. I found her to be a silent, pale-faced woman, much +younger than her husband, not more than thirty, I should think, +while he can hardly be less than forty-five. From their +conversation I have gathered that they have been married about +seven years, that he was a widower, and that his only child by +the first wife was the daughter who has gone to Philadelphia. Mr. +Rucastle told me in private that the reason why she had left them +was that she had an unreasoning aversion to her stepmother. As +the daughter could not have been less than twenty, I can quite +imagine that her position must have been uncomfortable with her +father's young wife. + +"Mrs. Rucastle seemed to me to be colourless in mind as well as +in feature. She impressed me neither favourably nor the reverse. +She was a nonentity. It was easy to see that she was passionately +devoted both to her husband and to her little son. Her light grey +eyes wandered continually from one to the other, noting every +little want and forestalling it if possible. He was kind to her +also in his bluff, boisterous fashion, and on the whole they +seemed to be a happy couple. And yet she had some secret sorrow, +this woman. She would often be lost in deep thought, with the +saddest look upon her face. More than once I have surprised her +in tears. I have thought sometimes that it was the disposition of +her child which weighed upon her mind, for I have never met so +utterly spoiled and so ill-natured a little creature. He is small +for his age, with a head which is quite disproportionately large. +His whole life appears to be spent in an alternation between +savage fits of passion and gloomy intervals of sulking. Giving +pain to any creature weaker than himself seems to be his one idea +of amusement, and he shows quite remarkable talent in planning +the capture of mice, little birds, and insects. But I would +rather not talk about the creature, Mr. Holmes, and, indeed, he +has little to do with my story." + +"I am glad of all details," remarked my friend, "whether they +seem to you to be relevant or not." + +"I shall try not to miss anything of importance. The one +unpleasant thing about the house, which struck me at once, was +the appearance and conduct of the servants. There are only two, a +man and his wife. Toller, for that is his name, is a rough, +uncouth man, with grizzled hair and whiskers, and a perpetual +smell of drink. Twice since I have been with them he has been +quite drunk, and yet Mr. Rucastle seemed to take no notice of it. +His wife is a very tall and strong woman with a sour face, as +silent as Mrs. Rucastle and much less amiable. They are a most +unpleasant couple, but fortunately I spend most of my time in the +nursery and my own room, which are next to each other in one +corner of the building. + +"For two days after my arrival at the Copper Beeches my life was +very quiet; on the third, Mrs. Rucastle came down just after +breakfast and whispered something to her husband. + +"'Oh, yes,' said he, turning to me, 'we are very much obliged to +you, Miss Hunter, for falling in with our whims so far as to cut +your hair. I assure you that it has not detracted in the tiniest +iota from your appearance. We shall now see how the electric-blue +dress will become you. You will find it laid out upon the bed in +your room, and if you would be so good as to put it on we should +both be extremely obliged.' + +"The dress which I found waiting for me was of a peculiar shade +of blue. It was of excellent material, a sort of beige, but it +bore unmistakable signs of having been worn before. It could not +have been a better fit if I had been measured for it. Both Mr. +and Mrs. Rucastle expressed a delight at the look of it, which +seemed quite exaggerated in its vehemence. They were waiting for +me in the drawing-room, which is a very large room, stretching +along the entire front of the house, with three long windows +reaching down to the floor. A chair had been placed close to the +central window, with its back turned towards it. In this I was +asked to sit, and then Mr. Rucastle, walking up and down on the +other side of the room, began to tell me a series of the funniest +stories that I have ever listened to. You cannot imagine how +comical he was, and I laughed until I was quite weary. Mrs. +Rucastle, however, who has evidently no sense of humour, never so +much as smiled, but sat with her hands in her lap, and a sad, +anxious look upon her face. After an hour or so, Mr. Rucastle +suddenly remarked that it was time to commence the duties of the +day, and that I might change my dress and go to little Edward in +the nursery. + +"Two days later this same performance was gone through under +exactly similar circumstances. Again I changed my dress, again I +sat in the window, and again I laughed very heartily at the funny +stories of which my employer had an immense rpertoire, and which +he told inimitably. Then he handed me a yellow-backed novel, and +moving my chair a little sideways, that my own shadow might not +fall upon the page, he begged me to read aloud to him. I read for +about ten minutes, beginning in the heart of a chapter, and then +suddenly, in the middle of a sentence, he ordered me to cease and +to change my dress. + +"You can easily imagine, Mr. Holmes, how curious I became as to +what the meaning of this extraordinary performance could possibly +be. They were always very careful, I observed, to turn my face +away from the window, so that I became consumed with the desire +to see what was going on behind my back. At first it seemed to be +impossible, but I soon devised a means. My hand-mirror had been +broken, so a happy thought seized me, and I concealed a piece of +the glass in my handkerchief. On the next occasion, in the midst +of my laughter, I put my handkerchief up to my eyes, and was able +with a little management to see all that there was behind me. I +confess that I was disappointed. There was nothing. At least that +was my first impression. At the second glance, however, I +perceived that there was a man standing in the Southampton Road, +a small bearded man in a grey suit, who seemed to be looking in +my direction. The road is an important highway, and there are +usually people there. This man, however, was leaning against the +railings which bordered our field and was looking earnestly up. I +lowered my handkerchief and glanced at Mrs. Rucastle to find her +eyes fixed upon me with a most searching gaze. She said nothing, +but I am convinced that she had divined that I had a mirror in my +hand and had seen what was behind me. She rose at once. + +"'Jephro,' said she, 'there is an impertinent fellow upon the +road there who stares up at Miss Hunter.' + +"'No friend of yours, Miss Hunter?' he asked. + +"'No, I know no one in these parts.' + +"'Dear me! How very impertinent! Kindly turn round and motion to +him to go away.' + +"'Surely it would be better to take no notice.' + +"'No, no, we should have him loitering here always. Kindly turn +round and wave him away like that.' + +"I did as I was told, and at the same instant Mrs. Rucastle drew +down the blind. That was a week ago, and from that time I have +not sat again in the window, nor have I worn the blue dress, nor +seen the man in the road." + +"Pray continue," said Holmes. "Your narrative promises to be a +most interesting one." + +"You will find it rather disconnected, I fear, and there may +prove to be little relation between the different incidents of +which I speak. On the very first day that I was at the Copper +Beeches, Mr. Rucastle took me to a small outhouse which stands +near the kitchen door. As we approached it I heard the sharp +rattling of a chain, and the sound as of a large animal moving +about. + +"'Look in here!' said Mr. Rucastle, showing me a slit between two +planks. 'Is he not a beauty?' + +"I looked through and was conscious of two glowing eyes, and of a +vague figure huddled up in the darkness. + +"'Don't be frightened,' said my employer, laughing at the start +which I had given. 'It's only Carlo, my mastiff. I call him mine, +but really old Toller, my groom, is the only man who can do +anything with him. We feed him once a day, and not too much then, +so that he is always as keen as mustard. Toller lets him loose +every night, and God help the trespasser whom he lays his fangs +upon. For goodness' sake don't you ever on any pretext set your +foot over the threshold at night, for it's as much as your life +is worth.' + +"The warning was no idle one, for two nights later I happened to +look out of my bedroom window about two o'clock in the morning. +It was a beautiful moonlight night, and the lawn in front of the +house was silvered over and almost as bright as day. I was +standing, rapt in the peaceful beauty of the scene, when I was +aware that something was moving under the shadow of the copper +beeches. As it emerged into the moonshine I saw what it was. It +was a giant dog, as large as a calf, tawny tinted, with hanging +jowl, black muzzle, and huge projecting bones. It walked slowly +across the lawn and vanished into the shadow upon the other side. +That dreadful sentinel sent a chill to my heart which I do not +think that any burglar could have done. + +"And now I have a very strange experience to tell you. I had, as +you know, cut off my hair in London, and I had placed it in a +great coil at the bottom of my trunk. One evening, after the +child was in bed, I began to amuse myself by examining the +furniture of my room and by rearranging my own little things. +There was an old chest of drawers in the room, the two upper ones +empty and open, the lower one locked. I had filled the first two +with my linen, and as I had still much to pack away I was +naturally annoyed at not having the use of the third drawer. It +struck me that it might have been fastened by a mere oversight, +so I took out my bunch of keys and tried to open it. The very +first key fitted to perfection, and I drew the drawer open. There +was only one thing in it, but I am sure that you would never +guess what it was. It was my coil of hair. + +"I took it up and examined it. It was of the same peculiar tint, +and the same thickness. But then the impossibility of the thing +obtruded itself upon me. How could my hair have been locked in +the drawer? With trembling hands I undid my trunk, turned out the +contents, and drew from the bottom my own hair. I laid the two +tresses together, and I assure you that they were identical. Was +it not extraordinary? Puzzle as I would, I could make nothing at +all of what it meant. I returned the strange hair to the drawer, +and I said nothing of the matter to the Rucastles as I felt that +I had put myself in the wrong by opening a drawer which they had +locked. + +"I am naturally observant, as you may have remarked, Mr. Holmes, +and I soon had a pretty good plan of the whole house in my head. +There was one wing, however, which appeared not to be inhabited +at all. A door which faced that which led into the quarters of +the Tollers opened into this suite, but it was invariably locked. +One day, however, as I ascended the stair, I met Mr. Rucastle +coming out through this door, his keys in his hand, and a look on +his face which made him a very different person to the round, +jovial man to whom I was accustomed. His cheeks were red, his +brow was all crinkled with anger, and the veins stood out at his +temples with passion. He locked the door and hurried past me +without a word or a look. + +"This aroused my curiosity, so when I went out for a walk in the +grounds with my charge, I strolled round to the side from which I +could see the windows of this part of the house. There were four +of them in a row, three of which were simply dirty, while the +fourth was shuttered up. They were evidently all deserted. As I +strolled up and down, glancing at them occasionally, Mr. Rucastle +came out to me, looking as merry and jovial as ever. + +"'Ah!' said he, 'you must not think me rude if I passed you +without a word, my dear young lady. I was preoccupied with +business matters.' + +"I assured him that I was not offended. 'By the way,' said I, +'you seem to have quite a suite of spare rooms up there, and one +of them has the shutters up.' + +"He looked surprised and, as it seemed to me, a little startled +at my remark. + +"'Photography is one of my hobbies,' said he. 'I have made my +dark room up there. But, dear me! what an observant young lady we +have come upon. Who would have believed it? Who would have ever +believed it?' He spoke in a jesting tone, but there was no jest +in his eyes as he looked at me. I read suspicion there and +annoyance, but no jest. + +"Well, Mr. Holmes, from the moment that I understood that there +was something about that suite of rooms which I was not to know, +I was all on fire to go over them. It was not mere curiosity, +though I have my share of that. It was more a feeling of duty--a +feeling that some good might come from my penetrating to this +place. They talk of woman's instinct; perhaps it was woman's +instinct which gave me that feeling. At any rate, it was there, +and I was keenly on the lookout for any chance to pass the +forbidden door. + +"It was only yesterday that the chance came. I may tell you that, +besides Mr. Rucastle, both Toller and his wife find something to +do in these deserted rooms, and I once saw him carrying a large +black linen bag with him through the door. Recently he has been +drinking hard, and yesterday evening he was very drunk; and when +I came upstairs there was the key in the door. I have no doubt at +all that he had left it there. Mr. and Mrs. Rucastle were both +downstairs, and the child was with them, so that I had an +admirable opportunity. I turned the key gently in the lock, +opened the door, and slipped through. + +"There was a little passage in front of me, unpapered and +uncarpeted, which turned at a right angle at the farther end. +Round this corner were three doors in a line, the first and third +of which were open. They each led into an empty room, dusty and +cheerless, with two windows in the one and one in the other, so +thick with dirt that the evening light glimmered dimly through +them. The centre door was closed, and across the outside of it +had been fastened one of the broad bars of an iron bed, padlocked +at one end to a ring in the wall, and fastened at the other with +stout cord. The door itself was locked as well, and the key was +not there. This barricaded door corresponded clearly with the +shuttered window outside, and yet I could see by the glimmer from +beneath it that the room was not in darkness. Evidently there was +a skylight which let in light from above. As I stood in the +passage gazing at the sinister door and wondering what secret it +might veil, I suddenly heard the sound of steps within the room +and saw a shadow pass backward and forward against the little +slit of dim light which shone out from under the door. A mad, +unreasoning terror rose up in me at the sight, Mr. Holmes. My +overstrung nerves failed me suddenly, and I turned and ran--ran +as though some dreadful hand were behind me clutching at the +skirt of my dress. I rushed down the passage, through the door, +and straight into the arms of Mr. Rucastle, who was waiting +outside. + +"'So,' said he, smiling, 'it was you, then. I thought that it +must be when I saw the door open.' + +"'Oh, I am so frightened!' I panted. + +"'My dear young lady! my dear young lady!'--you cannot think how +caressing and soothing his manner was--'and what has frightened +you, my dear young lady?' + +"But his voice was just a little too coaxing. He overdid it. I +was keenly on my guard against him. + +"'I was foolish enough to go into the empty wing,' I answered. +'But it is so lonely and eerie in this dim light that I was +frightened and ran out again. Oh, it is so dreadfully still in +there!' + +"'Only that?' said he, looking at me keenly. + +"'Why, what did you think?' I asked. + +"'Why do you think that I lock this door?' + +"'I am sure that I do not know.' + +"'It is to keep people out who have no business there. Do you +see?' He was still smiling in the most amiable manner. + +"'I am sure if I had known--' + +"'Well, then, you know now. And if you ever put your foot over +that threshold again'--here in an instant the smile hardened into +a grin of rage, and he glared down at me with the face of a +demon--'I'll throw you to the mastiff.' + +"I was so terrified that I do not know what I did. I suppose that +I must have rushed past him into my room. I remember nothing +until I found myself lying on my bed trembling all over. Then I +thought of you, Mr. Holmes. I could not live there longer without +some advice. I was frightened of the house, of the man, of the +woman, of the servants, even of the child. They were all horrible +to me. If I could only bring you down all would be well. Of +course I might have fled from the house, but my curiosity was +almost as strong as my fears. My mind was soon made up. I would +send you a wire. I put on my hat and cloak, went down to the +office, which is about half a mile from the house, and then +returned, feeling very much easier. A horrible doubt came into my +mind as I approached the door lest the dog might be loose, but I +remembered that Toller had drunk himself into a state of +insensibility that evening, and I knew that he was the only one +in the household who had any influence with the savage creature, +or who would venture to set him free. I slipped in in safety and +lay awake half the night in my joy at the thought of seeing you. +I had no difficulty in getting leave to come into Winchester this +morning, but I must be back before three o'clock, for Mr. and +Mrs. Rucastle are going on a visit, and will be away all the +evening, so that I must look after the child. Now I have told you +all my adventures, Mr. Holmes, and I should be very glad if you +could tell me what it all means, and, above all, what I should +do." + +Holmes and I had listened spellbound to this extraordinary story. +My friend rose now and paced up and down the room, his hands in +his pockets, and an expression of the most profound gravity upon +his face. + +"Is Toller still drunk?" he asked. + +"Yes. I heard his wife tell Mrs. Rucastle that she could do +nothing with him." + +"That is well. And the Rucastles go out to-night?" + +"Yes." + +"Is there a cellar with a good strong lock?" + +"Yes, the wine-cellar." + +"You seem to me to have acted all through this matter like a very +brave and sensible girl, Miss Hunter. Do you think that you could +perform one more feat? I should not ask it of you if I did not +think you a quite exceptional woman." + +"I will try. What is it?" + +"We shall be at the Copper Beeches by seven o'clock, my friend +and I. The Rucastles will be gone by that time, and Toller will, +we hope, be incapable. There only remains Mrs. Toller, who might +give the alarm. If you could send her into the cellar on some +errand, and then turn the key upon her, you would facilitate +matters immensely." + +"I will do it." + +"Excellent! We shall then look thoroughly into the affair. Of +course there is only one feasible explanation. You have been +brought there to personate someone, and the real person is +imprisoned in this chamber. That is obvious. As to who this +prisoner is, I have no doubt that it is the daughter, Miss Alice +Rucastle, if I remember right, who was said to have gone to +America. You were chosen, doubtless, as resembling her in height, +figure, and the colour of your hair. Hers had been cut off, very +possibly in some illness through which she has passed, and so, of +course, yours had to be sacrificed also. By a curious chance you +came upon her tresses. The man in the road was undoubtedly some +friend of hers--possibly her fianc--and no doubt, as you wore +the girl's dress and were so like her, he was convinced from your +laughter, whenever he saw you, and afterwards from your gesture, +that Miss Rucastle was perfectly happy, and that she no longer +desired his attentions. The dog is let loose at night to prevent +him from endeavouring to communicate with her. So much is fairly +clear. The most serious point in the case is the disposition of +the child." + +"What on earth has that to do with it?" I ejaculated. + +"My dear Watson, you as a medical man are continually gaining +light as to the tendencies of a child by the study of the +parents. Don't you see that the converse is equally valid. I have +frequently gained my first real insight into the character of +parents by studying their children. This child's disposition is +abnormally cruel, merely for cruelty's sake, and whether he +derives this from his smiling father, as I should suspect, or +from his mother, it bodes evil for the poor girl who is in their +power." + +"I am sure that you are right, Mr. Holmes," cried our client. "A +thousand things come back to me which make me certain that you +have hit it. Oh, let us lose not an instant in bringing help to +this poor creature." + +"We must be circumspect, for we are dealing with a very cunning +man. We can do nothing until seven o'clock. At that hour we shall +be with you, and it will not be long before we solve the +mystery." + +We were as good as our word, for it was just seven when we +reached the Copper Beeches, having put up our trap at a wayside +public-house. The group of trees, with their dark leaves shining +like burnished metal in the light of the setting sun, were +sufficient to mark the house even had Miss Hunter not been +standing smiling on the door-step. + +"Have you managed it?" asked Holmes. + +A loud thudding noise came from somewhere downstairs. "That is +Mrs. Toller in the cellar," said she. "Her husband lies snoring +on the kitchen rug. Here are his keys, which are the duplicates +of Mr. Rucastle's." + +"You have done well indeed!" cried Holmes with enthusiasm. "Now +lead the way, and we shall soon see the end of this black +business." + +We passed up the stair, unlocked the door, followed on down a +passage, and found ourselves in front of the barricade which Miss +Hunter had described. Holmes cut the cord and removed the +transverse bar. Then he tried the various keys in the lock, but +without success. No sound came from within, and at the silence +Holmes' face clouded over. + +"I trust that we are not too late," said he. "I think, Miss +Hunter, that we had better go in without you. Now, Watson, put +your shoulder to it, and we shall see whether we cannot make our +way in." + +It was an old rickety door and gave at once before our united +strength. Together we rushed into the room. It was empty. There +was no furniture save a little pallet bed, a small table, and a +basketful of linen. The skylight above was open, and the prisoner +gone. + +"There has been some villainy here," said Holmes; "this beauty +has guessed Miss Hunter's intentions and has carried his victim +off." + +"But how?" + +"Through the skylight. We shall soon see how he managed it." He +swung himself up onto the roof. "Ah, yes," he cried, "here's the +end of a long light ladder against the eaves. That is how he did +it." + +"But it is impossible," said Miss Hunter; "the ladder was not +there when the Rucastles went away." + +"He has come back and done it. I tell you that he is a clever and +dangerous man. I should not be very much surprised if this were +he whose step I hear now upon the stair. I think, Watson, that it +would be as well for you to have your pistol ready." + +The words were hardly out of his mouth before a man appeared at +the door of the room, a very fat and burly man, with a heavy +stick in his hand. Miss Hunter screamed and shrunk against the +wall at the sight of him, but Sherlock Holmes sprang forward and +confronted him. + +"You villain!" said he, "where's your daughter?" + +The fat man cast his eyes round, and then up at the open +skylight. + +"It is for me to ask you that," he shrieked, "you thieves! Spies +and thieves! I have caught you, have I? You are in my power. I'll +serve you!" He turned and clattered down the stairs as hard as he +could go. + +"He's gone for the dog!" cried Miss Hunter. + +"I have my revolver," said I. + +"Better close the front door," cried Holmes, and we all rushed +down the stairs together. We had hardly reached the hall when we +heard the baying of a hound, and then a scream of agony, with a +horrible worrying sound which it was dreadful to listen to. An +elderly man with a red face and shaking limbs came staggering out +at a side door. + +"My God!" he cried. "Someone has loosed the dog. It's not been +fed for two days. Quick, quick, or it'll be too late!" + +Holmes and I rushed out and round the angle of the house, with +Toller hurrying behind us. There was the huge famished brute, its +black muzzle buried in Rucastle's throat, while he writhed and +screamed upon the ground. Running up, I blew its brains out, and +it fell over with its keen white teeth still meeting in the great +creases of his neck. With much labour we separated them and +carried him, living but horribly mangled, into the house. We laid +him upon the drawing-room sofa, and having dispatched the sobered +Toller to bear the news to his wife, I did what I could to +relieve his pain. We were all assembled round him when the door +opened, and a tall, gaunt woman entered the room. + +"Mrs. Toller!" cried Miss Hunter. + +"Yes, miss. Mr. Rucastle let me out when he came back before he +went up to you. Ah, miss, it is a pity you didn't let me know +what you were planning, for I would have told you that your pains +were wasted." + +"Ha!" said Holmes, looking keenly at her. "It is clear that Mrs. +Toller knows more about this matter than anyone else." + +"Yes, sir, I do, and I am ready enough to tell what I know." + +"Then, pray, sit down, and let us hear it for there are several +points on which I must confess that I am still in the dark." + +"I will soon make it clear to you," said she; "and I'd have done +so before now if I could ha' got out from the cellar. If there's +police-court business over this, you'll remember that I was the +one that stood your friend, and that I was Miss Alice's friend +too. + +"She was never happy at home, Miss Alice wasn't, from the time +that her father married again. She was slighted like and had no +say in anything, but it never really became bad for her until +after she met Mr. Fowler at a friend's house. As well as I could +learn, Miss Alice had rights of her own by will, but she was so +quiet and patient, she was, that she never said a word about them +but just left everything in Mr. Rucastle's hands. He knew he was +safe with her; but when there was a chance of a husband coming +forward, who would ask for all that the law would give him, then +her father thought it time to put a stop on it. He wanted her to +sign a paper, so that whether she married or not, he could use +her money. When she wouldn't do it, he kept on worrying her until +she got brain-fever, and for six weeks was at death's door. Then +she got better at last, all worn to a shadow, and with her +beautiful hair cut off; but that didn't make no change in her +young man, and he stuck to her as true as man could be." + +"Ah," said Holmes, "I think that what you have been good enough +to tell us makes the matter fairly clear, and that I can deduce +all that remains. Mr. Rucastle then, I presume, took to this +system of imprisonment?" + +"Yes, sir." + +"And brought Miss Hunter down from London in order to get rid of +the disagreeable persistence of Mr. Fowler." + +"That was it, sir." + +"But Mr. Fowler being a persevering man, as a good seaman should +be, blockaded the house, and having met you succeeded by certain +arguments, metallic or otherwise, in convincing you that your +interests were the same as his." + +"Mr. Fowler was a very kind-spoken, free-handed gentleman," said +Mrs. Toller serenely. + +"And in this way he managed that your good man should have no +want of drink, and that a ladder should be ready at the moment +when your master had gone out." + +"You have it, sir, just as it happened." + +"I am sure we owe you an apology, Mrs. Toller," said Holmes, "for +you have certainly cleared up everything which puzzled us. And +here comes the country surgeon and Mrs. Rucastle, so I think, +Watson, that we had best escort Miss Hunter back to Winchester, +as it seems to me that our locus standi now is rather a +questionable one." + +And thus was solved the mystery of the sinister house with the +copper beeches in front of the door. Mr. Rucastle survived, but +was always a broken man, kept alive solely through the care of +his devoted wife. They still live with their old servants, who +probably know so much of Rucastle's past life that he finds it +difficult to part from them. Mr. Fowler and Miss Rucastle were +married, by special license, in Southampton the day after their +flight, and he is now the holder of a government appointment in +the island of Mauritius. As to Miss Violet Hunter, my friend +Holmes, rather to my disappointment, manifested no further +interest in her when once she had ceased to be the centre of one +of his problems, and she is now the head of a private school at +Walsall, where I believe that she has met with considerable success. + + + + + + + + + +End of the Project Gutenberg EBook of The Adventures of Sherlock Holmes, by +Arthur Conan Doyle + +*** END OF THIS PROJECT GUTENBERG EBOOK THE ADVENTURES OF SHERLOCK HOLMES *** + +***** This file should be named 1661-8.txt or 1661-8.zip ***** +This and all associated files of various formats will be found in: + http://www.gutenberg.org/1/6/6/1661/ + +Produced by an anonymous Project Gutenberg volunteer and Jose Menendez + +Updated editions will replace the previous one--the old editions +will be renamed. + +Creating the works from public domain print editions means that no +one owns a United States copyright in these works, so the Foundation +(and you!) can copy and distribute it in the United States without +permission and without paying copyright royalties. Special rules, +set forth in the General Terms of Use part of this license, apply to +copying and distributing Project Gutenberg-tm electronic works to +protect the PROJECT GUTENBERG-tm concept and trademark. Project +Gutenberg is a registered trademark, and may not be used if you +charge for the eBooks, unless you receive specific permission. If you +do not charge anything for copies of this eBook, complying with the +rules is very easy. You may use this eBook for nearly any purpose +such as creation of derivative works, reports, performances and +research. They may be modified and printed and given away--you may do +practically ANYTHING with public domain eBooks. Redistribution is +subject to the trademark license, especially commercial +redistribution. + + + +*** START: FULL LICENSE *** + +THE FULL PROJECT GUTENBERG LICENSE +PLEASE READ THIS BEFORE YOU DISTRIBUTE OR USE THIS WORK + +To protect the Project Gutenberg-tm mission of promoting the free +distribution of electronic works, by using or distributing this work +(or any other work associated in any way with the phrase "Project +Gutenberg"), you agree to comply with all the terms of the Full Project +Gutenberg-tm License (available with this file or online at +http://gutenberg.net/license). + + +Section 1. General Terms of Use and Redistributing Project Gutenberg-tm +electronic works + +1.A. By reading or using any part of this Project Gutenberg-tm +electronic work, you indicate that you have read, understand, agree to +and accept all the terms of this license and intellectual property +(trademark/copyright) agreement. If you do not agree to abide by all +the terms of this agreement, you must cease using and return or destroy +all copies of Project Gutenberg-tm electronic works in your possession. +If you paid a fee for obtaining a copy of or access to a Project +Gutenberg-tm electronic work and you do not agree to be bound by the +terms of this agreement, you may obtain a refund from the person or +entity to whom you paid the fee as set forth in paragraph 1.E.8. + +1.B. "Project Gutenberg" is a registered trademark. It may only be +used on or associated in any way with an electronic work by people who +agree to be bound by the terms of this agreement. There are a few +things that you can do with most Project Gutenberg-tm electronic works +even without complying with the full terms of this agreement. See +paragraph 1.C below. There are a lot of things you can do with Project +Gutenberg-tm electronic works if you follow the terms of this agreement +and help preserve free future access to Project Gutenberg-tm electronic +works. See paragraph 1.E below. + +1.C. The Project Gutenberg Literary Archive Foundation ("the Foundation" +or PGLAF), owns a compilation copyright in the collection of Project +Gutenberg-tm electronic works. Nearly all the individual works in the +collection are in the public domain in the United States. If an +individual work is in the public domain in the United States and you are +located in the United States, we do not claim a right to prevent you from +copying, distributing, performing, displaying or creating derivative +works based on the work as long as all references to Project Gutenberg +are removed. Of course, we hope that you will support the Project +Gutenberg-tm mission of promoting free access to electronic works by +freely sharing Project Gutenberg-tm works in compliance with the terms of +this agreement for keeping the Project Gutenberg-tm name associated with +the work. You can easily comply with the terms of this agreement by +keeping this work in the same format with its attached full Project +Gutenberg-tm License when you share it without charge with others. + +1.D. The copyright laws of the place where you are located also govern +what you can do with this work. Copyright laws in most countries are in +a constant state of change. If you are outside the United States, check +the laws of your country in addition to the terms of this agreement +before downloading, copying, displaying, performing, distributing or +creating derivative works based on this work or any other Project +Gutenberg-tm work. The Foundation makes no representations concerning +the copyright status of any work in any country outside the United +States. + +1.E. Unless you have removed all references to Project Gutenberg: + +1.E.1. The following sentence, with active links to, or other immediate +access to, the full Project Gutenberg-tm License must appear prominently +whenever any copy of a Project Gutenberg-tm work (any work on which the +phrase "Project Gutenberg" appears, or with which the phrase "Project +Gutenberg" is associated) is accessed, displayed, performed, viewed, +copied or distributed: + +This eBook is for the use of anyone anywhere at no cost and with +almost no restrictions whatsoever. You may copy it, give it away or +re-use it under the terms of the Project Gutenberg License included +with this eBook or online at www.gutenberg.net + +1.E.2. If an individual Project Gutenberg-tm electronic work is derived +from the public domain (does not contain a notice indicating that it is +posted with permission of the copyright holder), the work can be copied +and distributed to anyone in the United States without paying any fees +or charges. If you are redistributing or providing access to a work +with the phrase "Project Gutenberg" associated with or appearing on the +work, you must comply either with the requirements of paragraphs 1.E.1 +through 1.E.7 or obtain permission for the use of the work and the +Project Gutenberg-tm trademark as set forth in paragraphs 1.E.8 or +1.E.9. + +1.E.3. If an individual Project Gutenberg-tm electronic work is posted +with the permission of the copyright holder, your use and distribution +must comply with both paragraphs 1.E.1 through 1.E.7 and any additional +terms imposed by the copyright holder. Additional terms will be linked +to the Project Gutenberg-tm License for all works posted with the +permission of the copyright holder found at the beginning of this work. + +1.E.4. Do not unlink or detach or remove the full Project Gutenberg-tm +License terms from this work, or any files containing a part of this +work or any other work associated with Project Gutenberg-tm. + +1.E.5. Do not copy, display, perform, distribute or redistribute this +electronic work, or any part of this electronic work, without +prominently displaying the sentence set forth in paragraph 1.E.1 with +active links or immediate access to the full terms of the Project +Gutenberg-tm License. + +1.E.6. You may convert to and distribute this work in any binary, +compressed, marked up, nonproprietary or proprietary form, including any +word processing or hypertext form. However, if you provide access to or +distribute copies of a Project Gutenberg-tm work in a format other than +"Plain Vanilla ASCII" or other format used in the official version +posted on the official Project Gutenberg-tm web site (www.gutenberg.net), +you must, at no additional cost, fee or expense to the user, provide a +copy, a means of exporting a copy, or a means of obtaining a copy upon +request, of the work in its original "Plain Vanilla ASCII" or other +form. Any alternate format must include the full Project Gutenberg-tm +License as specified in paragraph 1.E.1. + +1.E.7. Do not charge a fee for access to, viewing, displaying, +performing, copying or distributing any Project Gutenberg-tm works +unless you comply with paragraph 1.E.8 or 1.E.9. + +1.E.8. You may charge a reasonable fee for copies of or providing +access to or distributing Project Gutenberg-tm electronic works provided +that + +- You pay a royalty fee of 20% of the gross profits you derive from + the use of Project Gutenberg-tm works calculated using the method + you already use to calculate your applicable taxes. The fee is + owed to the owner of the Project Gutenberg-tm trademark, but he + has agreed to donate royalties under this paragraph to the + Project Gutenberg Literary Archive Foundation. Royalty payments + must be paid within 60 days following each date on which you + prepare (or are legally required to prepare) your periodic tax + returns. Royalty payments should be clearly marked as such and + sent to the Project Gutenberg Literary Archive Foundation at the + address specified in Section 4, "Information about donations to + the Project Gutenberg Literary Archive Foundation." + +- You provide a full refund of any money paid by a user who notifies + you in writing (or by e-mail) within 30 days of receipt that s/he + does not agree to the terms of the full Project Gutenberg-tm + License. You must require such a user to return or + destroy all copies of the works possessed in a physical medium + and discontinue all use of and all access to other copies of + Project Gutenberg-tm works. + +- You provide, in accordance with paragraph 1.F.3, a full refund of any + money paid for a work or a replacement copy, if a defect in the + electronic work is discovered and reported to you within 90 days + of receipt of the work. + +- You comply with all other terms of this agreement for free + distribution of Project Gutenberg-tm works. + +1.E.9. If you wish to charge a fee or distribute a Project Gutenberg-tm +electronic work or group of works on different terms than are set +forth in this agreement, you must obtain permission in writing from +both the Project Gutenberg Literary Archive Foundation and Michael +Hart, the owner of the Project Gutenberg-tm trademark. Contact the +Foundation as set forth in Section 3 below. + +1.F. + +1.F.1. Project Gutenberg volunteers and employees expend considerable +effort to identify, do copyright research on, transcribe and proofread +public domain works in creating the Project Gutenberg-tm +collection. Despite these efforts, Project Gutenberg-tm electronic +works, and the medium on which they may be stored, may contain +"Defects," such as, but not limited to, incomplete, inaccurate or +corrupt data, transcription errors, a copyright or other intellectual +property infringement, a defective or damaged disk or other medium, a +computer virus, or computer codes that damage or cannot be read by +your equipment. + +1.F.2. LIMITED WARRANTY, DISCLAIMER OF DAMAGES - Except for the "Right +of Replacement or Refund" described in paragraph 1.F.3, the Project +Gutenberg Literary Archive Foundation, the owner of the Project +Gutenberg-tm trademark, and any other party distributing a Project +Gutenberg-tm electronic work under this agreement, disclaim all +liability to you for damages, costs and expenses, including legal +fees. YOU AGREE THAT YOU HAVE NO REMEDIES FOR NEGLIGENCE, STRICT +LIABILITY, BREACH OF WARRANTY OR BREACH OF CONTRACT EXCEPT THOSE +PROVIDED IN PARAGRAPH 1.F.3. YOU AGREE THAT THE FOUNDATION, THE +TRADEMARK OWNER, AND ANY DISTRIBUTOR UNDER THIS AGREEMENT WILL NOT BE +LIABLE TO YOU FOR ACTUAL, DIRECT, INDIRECT, CONSEQUENTIAL, PUNITIVE OR +INCIDENTAL DAMAGES EVEN IF YOU GIVE NOTICE OF THE POSSIBILITY OF SUCH +DAMAGE. + +1.F.3. LIMITED RIGHT OF REPLACEMENT OR REFUND - If you discover a +defect in this electronic work within 90 days of receiving it, you can +receive a refund of the money (if any) you paid for it by sending a +written explanation to the person you received the work from. If you +received the work on a physical medium, you must return the medium with +your written explanation. The person or entity that provided you with +the defective work may elect to provide a replacement copy in lieu of a +refund. If you received the work electronically, the person or entity +providing it to you may choose to give you a second opportunity to +receive the work electronically in lieu of a refund. If the second copy +is also defective, you may demand a refund in writing without further +opportunities to fix the problem. + +1.F.4. Except for the limited right of replacement or refund set forth +in paragraph 1.F.3, this work is provided to you 'AS-IS' WITH NO OTHER +WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +WARRANTIES OF MERCHANTIBILITY OR FITNESS FOR ANY PURPOSE. + +1.F.5. Some states do not allow disclaimers of certain implied +warranties or the exclusion or limitation of certain types of damages. +If any disclaimer or limitation set forth in this agreement violates the +law of the state applicable to this agreement, the agreement shall be +interpreted to make the maximum disclaimer or limitation permitted by +the applicable state law. The invalidity or unenforceability of any +provision of this agreement shall not void the remaining provisions. + +1.F.6. INDEMNITY - You agree to indemnify and hold the Foundation, the +trademark owner, any agent or employee of the Foundation, anyone +providing copies of Project Gutenberg-tm electronic works in accordance +with this agreement, and any volunteers associated with the production, +promotion and distribution of Project Gutenberg-tm electronic works, +harmless from all liability, costs and expenses, including legal fees, +that arise directly or indirectly from any of the following which you do +or cause to occur: (a) distribution of this or any Project Gutenberg-tm +work, (b) alteration, modification, or additions or deletions to any +Project Gutenberg-tm work, and (c) any Defect you cause. + + +Section 2. Information about the Mission of Project Gutenberg-tm + +Project Gutenberg-tm is synonymous with the free distribution of +electronic works in formats readable by the widest variety of computers +including obsolete, old, middle-aged and new computers. It exists +because of the efforts of hundreds of volunteers and donations from +people in all walks of life. + +Volunteers and financial support to provide volunteers with the +assistance they need are critical to reaching Project Gutenberg-tm's +goals and ensuring that the Project Gutenberg-tm collection will +remain freely available for generations to come. In 2001, the Project +Gutenberg Literary Archive Foundation was created to provide a secure +and permanent future for Project Gutenberg-tm and future generations. +To learn more about the Project Gutenberg Literary Archive Foundation +and how your efforts and donations can help, see Sections 3 and 4 +and the Foundation web page at http://www.pglaf.org. + + +Section 3. Information about the Project Gutenberg Literary Archive +Foundation + +The Project Gutenberg Literary Archive Foundation is a non profit +501(c)(3) educational corporation organized under the laws of the +state of Mississippi and granted tax exempt status by the Internal +Revenue Service. The Foundation's EIN or federal tax identification +number is 64-6221541. Its 501(c)(3) letter is posted at +http://pglaf.org/fundraising. Contributions to the Project Gutenberg +Literary Archive Foundation are tax deductible to the full extent +permitted by U.S. federal laws and your state's laws. + +The Foundation's principal office is located at 4557 Melan Dr. S. +Fairbanks, AK, 99712., but its volunteers and employees are scattered +throughout numerous locations. Its business office is located at +809 North 1500 West, Salt Lake City, UT 84116, (801) 596-1887, email +business@pglaf.org. Email contact links and up to date contact +information can be found at the Foundation's web site and official +page at http://pglaf.org + +For additional contact information: + Dr. Gregory B. Newby + Chief Executive and Director + gbnewby@pglaf.org + + +Section 4. Information about Donations to the Project Gutenberg +Literary Archive Foundation + +Project Gutenberg-tm depends upon and cannot survive without wide +spread public support and donations to carry out its mission of +increasing the number of public domain and licensed works that can be +freely distributed in machine readable form accessible by the widest +array of equipment including outdated equipment. Many small donations +($1 to $5,000) are particularly important to maintaining tax exempt +status with the IRS. + +The Foundation is committed to complying with the laws regulating +charities and charitable donations in all 50 states of the United +States. Compliance requirements are not uniform and it takes a +considerable effort, much paperwork and many fees to meet and keep up +with these requirements. We do not solicit donations in locations +where we have not received written confirmation of compliance. To +SEND DONATIONS or determine the status of compliance for any +particular state visit http://pglaf.org + +While we cannot and do not solicit contributions from states where we +have not met the solicitation requirements, we know of no prohibition +against accepting unsolicited donations from donors in such states who +approach us with offers to donate. + +International donations are gratefully accepted, but we cannot make +any statements concerning tax treatment of donations received from +outside the United States. U.S. laws alone swamp our small staff. + +Please check the Project Gutenberg Web pages for current donation +methods and addresses. Donations are accepted in a number of other +ways including including checks, online payments and credit card +donations. To donate, please visit: http://pglaf.org/donate + + +Section 5. General Information About Project Gutenberg-tm electronic +works. + +Professor Michael S. Hart is the originator of the Project Gutenberg-tm +concept of a library of electronic works that could be freely shared +with anyone. For thirty years, he produced and distributed Project +Gutenberg-tm eBooks with only a loose network of volunteer support. + + +Project Gutenberg-tm eBooks are often created from several printed +editions, all of which are confirmed as Public Domain in the U.S. +unless a copyright notice is included. Thus, we do not necessarily +keep eBooks in compliance with any particular paper edition. + + +Most people start at our Web site which has the main PG search facility: + + http://www.gutenberg.net + +This Web site includes information about Project Gutenberg-tm, +including how to make donations to the Project Gutenberg Literary +Archive Foundation, how to help produce our new eBooks, and how to +subscribe to our email newsletter to hear about new eBooks. diff --git a/_downloads/18981fb71a1d906bd8ce331570c49328/test_html_output3.html b/_downloads/18981fb71a1d906bd8ce331570c49328/test_html_output3.html new file mode 100644 index 0000000..3e19aa1 --- /dev/null +++ b/_downloads/18981fb71a1d906bd8ce331570c49328/test_html_output3.html @@ -0,0 +1,13 @@ + + +PythonClass = Revision 1087: + + +

+Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+

+And here is another piece of text -- you should be able to add any number +

+ + diff --git a/_downloads/18b5bb39eb5cb68d9c0a0e9462e98b92/except_exercise.py b/_downloads/18b5bb39eb5cb68d9c0a0e9462e98b92/except_exercise.py new file mode 100644 index 0000000..96c8870 --- /dev/null +++ b/_downloads/18b5bb39eb5cb68d9c0a0e9462e98b92/except_exercise.py @@ -0,0 +1,43 @@ +#!/usr/bin/python + +""" +An exercise in playing with Exceptions. +Make lots of try/except blocks for fun and profit. + +Make sure to catch specifically the error you find, rather than all errors. +""" + +from except_test import fun, more_fun, last_fun + + +# Figure out what the exception is, catch it and while still +# in that catch block, try again with the second item in the list +first_try = ['spam', 'cheese', 'mr death'] + +joke = fun(first_try[0]) + +# Here is a try/except block. Add an else that prints not_joke +try: + not_joke = fun(first_try[2]) +except SyntaxError: + print('Run Away!') + +# What did that do? You can think of else in this context, as well as in +# loops as meaning: "else if nothing went wrong" +# (no breaks in loops, no exceptions in try blocks) + +# Figure out what the exception is, catch it and in that same block +# +# try calling the more_fun function with the 2nd language in the list, +# again assigning it to more_joke. +# +# If there are no exceptions, call the more_fun function with the last +# language in the list + +# Finally, while still in the try/except block and regardless of whether +# there were any exceptions, call the function last_fun with no +# parameters. (pun intended) + +langs = ['java', 'c', 'python'] + +more_joke = more_fun(langs[0]) diff --git a/_downloads/191e6a93916b7b7c0c37980e4702ef1a/test_object_canvas.py b/_downloads/191e6a93916b7b7c0c37980e4702ef1a/test_object_canvas.py new file mode 100644 index 0000000..f88a58c --- /dev/null +++ b/_downloads/191e6a93916b7b7c0c37980e4702ef1a/test_object_canvas.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 + +""" +test code for the object_canvas + +Note: Testing image generation is hard. So for now, this mostly just + tests that the rendering function runs. + And during development, you can look at the resulting files. + + One could store "properly" rendered results for future tests to + check against. +""" + +# import os +import pathlib +import object_canvas as oc + +SAVE_ALL=True # save all the temp files? + + +def render_to_file(canvas, filename="test_image.png", save=False): + """ + utility to render a canvas to a file + + :param filename: name of file to render to it will be put in a test_images dir. + + :param remove=True: whether to remove the file after rendering. + """ + path = pathlib.Path("test_images") + path.mkdir(exist_ok=True) + path /= filename + canvas.render(str(path)) + assert path.is_file() + if not (SAVE_ALL or save): + path.unlink() + + +def test_init(): + canvas = oc.ObjectCanvas() + + assert canvas + +def test_backgound(): + canvas = oc.ObjectCanvas(background='blue') + render_to_file(canvas, "blue_background.png") + +def test_polyline(): + """ + can we draw a polyline? + """ + canvas = oc.ObjectCanvas() + points = ((10, 10), # this should be a triangle + (10, 400), + (400, 10), + (10, 10), + ) + + pl = oc.PolyLine(points) + canvas.add_object(pl) + render_to_file(canvas, "polyline.png") + + +def test_circle(): + canvas = oc.ObjectCanvas() + center = (100, 100) + diameter = 75 + for line_width in range(1, 5): + c = oc.Circle(center, + diameter, + line_color="red", + fill_color="blue", + line_width=line_width, + ) + canvas.add_object(c) + center = (center[0] + 50, center[0] + 50) + render_to_file(canvas, "circle.png") + diff --git a/_downloads/19fa18763a1e251e2664f14fd8609477/students.txt b/_downloads/19fa18763a1e251e2664f14fd8609477/students.txt new file mode 100644 index 0000000..a85853a --- /dev/null +++ b/_downloads/19fa18763a1e251e2664f14fd8609477/students.txt @@ -0,0 +1,31 @@ +Name: Nickname, languages +Swift, Taylor: python, java, perl +Swift, Samuel: Sam +Brooks, Garth: fortran, java, matlab, bash +Buble, Michael: python, powershell +Stefani, Gwen: c#, javascript, python, typescript +Gaye, Marvin: c++, java +Jagger, Michael: Mick, shell, python +Lennon, John: +Nelson, Willy: python, java +McCartney, Paul: nothing +Dylan, Bob: java, python, ruby +Springsteen, Bruce: c++, python +Jackson, Michael: java, c#, python +Berry, Charles: Chuck c++, matlab, +Wonder, Steven: Stevie, c, perl, java, erlang, python +Nicks, Stevie: java, perl, c#, c++, python +Turner, Tina: bash, python +Plant, Robert: Bob, bash, python, ansible +Townshend, Peter: Pete, c#, powershell, python, sql +Moon, Keith: python, r, visualbasic +Mitchell, Joni: php, mysql, python +Ramone, John: Johnny, rex, db +King, Carol: r +Waters, Muddy: perl, python +Star, Richard: Ringo, shell, python, vb +Smith, Patricia: Patti, python +Morrison, Jim: fortran, perl, sql, python +Marley, Robert: Bob, c, c++, lisp +Simon, Paul: bash, python, sql +Charles, Ray: Chuck, java, python diff --git a/_downloads/1a51769567a10ff0d20491c9117ad604/vector.py b/_downloads/1a51769567a10ff0d20491c9117ad604/vector.py new file mode 100644 index 0000000..7b04a4f --- /dev/null +++ b/_downloads/1a51769567a10ff0d20491c9117ad604/vector.py @@ -0,0 +1,50 @@ +""" +Vector type with +, * redefined as Vector addition and dot product +""" + + +class Vector(list): + def __repr__(self): + """ + String representation, uses list (superclass) representation + """ + return 'Vector({})'.format(super().__repr__()) + + def __add__(self, v): + """ + redefine + as element-wise Vector sum + """ + if len(self) != len(v): + raise TypeError("Vector can only be added to a sequence of the same length") + else: + return Vector([x1 + x2 for x1, x2 in zip(self, v)]) + + def __mul__(self, v): + """ + redefine * as Vector dot product + """ + if len(self) != len(v): + raise TypeError("Vector can only be multiplied with a sequence of the same length") + else: + return sum([x1 * x2 for x1, x2 in zip(self, v)]) + + +if __name__ == '__main__': + l1 = [1, 2, 3] + l2 = [4, 5, 6] + v1 = Vector(l1) + v2 = Vector(l2) + + print('l1') + print(l1) + print('l1 + l2') + print(l1 + l2) + # print(l1 * l2) # TypeError + print('zip(l1, l2)') + print(zip(l1, l2)) + print('v1') + print(v1) + print('v1 + v2') + print(v1 + v2) + print('v1 * v2') + print(v1 * v2) diff --git a/_downloads/1cd83ce40e4950c45c2c6377ed05f15b/context_manager.py b/_downloads/1cd83ce40e4950c45c2c6377ed05f15b/context_manager.py new file mode 100644 index 0000000..7e6f74c --- /dev/null +++ b/_downloads/1cd83ce40e4950c45c2c6377ed05f15b/context_manager.py @@ -0,0 +1,21 @@ +# Demo of a contextmanager + + +class Context(object): + """ + from Doug Hellmann, PyMOTW + https://pymotw.com/3/contextlib/#module-contextlib + """ + + def __init__(self, handle_error): + print('__init__({})'.format(handle_error)) + self.handle_error = handle_error + + def __enter__(self): + print('__enter__()') + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + print(exc_val.args) + print('__exit__({}, {}, {})'.format(exc_type, exc_val, exc_tb)) + return self.handle_error diff --git a/_downloads/1cf25777c3ecfb8cba104487ed28d1d1/neo4j-developer-manual-3.3-python.pdf b/_downloads/1cf25777c3ecfb8cba104487ed28d1d1/neo4j-developer-manual-3.3-python.pdf new file mode 100644 index 0000000..7b0767e Binary files /dev/null and b/_downloads/1cf25777c3ecfb8cba104487ed28d1d1/neo4j-developer-manual-3.3-python.pdf differ diff --git a/_downloads/1e3d7760d32a4a9dca2e2779ee15a58c/mangler.py b/_downloads/1e3d7760d32a4a9dca2e2779ee15a58c/mangler.py new file mode 100644 index 0000000..a8e2206 --- /dev/null +++ b/_downloads/1e3d7760d32a4a9dca2e2779ee15a58c/mangler.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +""" +Simple metaclass example that creates upper and lower case versions of +all non-dunder class attributes +""" + + +class NameMangler(type): # deriving from type makes it a metaclass. + + def __new__(cls, clsname, bases, _dict): + uppercase_attr = {} + for name, val in _dict.items(): + if not name.startswith('__'): + uppercase_attr[name.upper()] = val + uppercase_attr[name.lower()] = val + else: + uppercase_attr[name] = val + + return super().__new__(cls, clsname, bases, uppercase_attr) + + +class Foo(metaclass=NameMangler): + x = 1 + Y = 2 + + +# note that it works for methods, too! +class Bar(metaclass=NameMangler): + x = 1 + + def a_method(self): + print("in a_method") + + +if __name__ == "__main__": + f = Foo() + print(f.x) + print(f.X) + print(f.y) + print(f.Y) + + b = Bar() + b.A_METHOD() diff --git a/_downloads/20c2b8dec0016f4a222d42c9fddfe3b2/calculator_test_suite.py b/_downloads/20c2b8dec0016f4a222d42c9fddfe3b2/calculator_test_suite.py new file mode 100644 index 0000000..7557084 --- /dev/null +++ b/_downloads/20c2b8dec0016f4a222d42c9fddfe3b2/calculator_test_suite.py @@ -0,0 +1,6 @@ +import unittest + +from test_calculator import TestCalculatorFunctions + +suite = unittest.TestLoader().loadTestsFromTestCase(TestCalculatorFunctions) +unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/_downloads/27905613ee14e092bacae957a478f7f6/play_with_scope.py b/_downloads/27905613ee14e092bacae957a478f7f6/play_with_scope.py new file mode 100644 index 0000000..a71292b --- /dev/null +++ b/_downloads/27905613ee14e092bacae957a478f7f6/play_with_scope.py @@ -0,0 +1,47 @@ +""" +some example code to play with scope +""" + +def start_at(x): + def increment_by(y): + return x + y + return increment_by + +closure_1 = start_at(3) +closure_2 = start_at(5) +closure_1(2) +start_at(2)(4) + + +def make_um_counter(): + series = [] + def um_counter(new_word): + series.append(new_word) # free variable + count = 0 + for i in series: + if i == 'um': + count += 1 + return count + return um_counter + + +def make_um_counter2(): + count = 0 + + def um_counter2(new_word): + if new_word == 'um': + count += 1 + return count + return um_counter2 + + +# try using nonlocal: +def make_um_counter3(): + count = 0 + + def um_counter3(new_word): + nonlocal count + if new_word == 'um': + count += 1 + return count + return um_counter3 diff --git a/_downloads/299e5e2c4d30fe9e8474152465bc4f8b/test_generator.py b/_downloads/299e5e2c4d30fe9e8474152465bc4f8b/test_generator.py new file mode 100644 index 0000000..06409e6 --- /dev/null +++ b/_downloads/299e5e2c4d30fe9e8474152465bc4f8b/test_generator.py @@ -0,0 +1,61 @@ +""" +test_generator.py + +tests the solution to the generator lab + +can be run with pytest +""" + +import generator_solution as gen + + +def test_intsum(): + + g = gen.intsum() + + assert next(g) == 0 + assert next(g) == 1 + assert next(g) == 3 + assert next(g) == 6 + assert next(g) == 10 + assert next(g) == 15 + + +def test_intsum2(): + + g = gen.intsum2() + + assert next(g) == 0 + assert next(g) == 1 + assert next(g) == 3 + assert next(g) == 6 + assert next(g) == 10 + assert next(g) == 15 + + +def test_doubler(): + + g = gen.doubler() + + assert next(g) == 1 + assert next(g) == 2 + assert next(g) == 4 + assert next(g) == 8 + assert next(g) == 16 + assert next(g) == 32 + + for i in range(10): + j = next(g) + + assert j == 2**15 + + +def test_fib(): + g = gen.fib() + assert [next(g) for i in range(9)] == [1, 1, 2, 3, 5, 8, 13, 21, 34] + + +def test_prime(): + g = gen.prime() + for val in [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]: + assert next(g) == val diff --git a/_downloads/2ec4e76ed272153e19f609362ec9af0c/roman4.py b/_downloads/2ec4e76ed272153e19f609362ec9af0c/roman4.py new file mode 100644 index 0000000..e96336c --- /dev/null +++ b/_downloads/2ec4e76ed272153e19f609362ec9af0c/roman4.py @@ -0,0 +1,119 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + +import pytest + +roman_numeral_map = (('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1)) + + +def to_roman(n): + '''convert integer to Roman numeral''' + if n > 3999: + raise ValueError("number out of range (must be less than 4000)") + + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + return result + + +## 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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result + + +def test_too_large(): + """ + to_roman should raise an ValueError when passed + values over 3999 + """ + with pytest.raises(ValueError): + to_roman(4000) + + diff --git a/_downloads/3139e3e9d1b6b308facc523beeda6e57/series_template.py b/_downloads/3139e3e9d1b6b308facc523beeda6e57/series_template.py new file mode 100644 index 0000000..fb161d7 --- /dev/null +++ b/_downloads/3139e3e9d1b6b308facc523beeda6e57/series_template.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 + +""" +a template for the series assignment +""" + + +def fibonacci(n): + """ compute the nth Fibonacci number """ + pass + + +def lucas(n): + """ compute the nth Lucas number """ + pass + + +def sum_series(n, n0=0, n1=1): + """ + compute the nth value of a summation series. + + :param n0=0: value of zeroth element in the series + :param n1=1: value of first element in the series + + This function should generalize the fibonacci() and the lucas(), + so that this function works for any first two numbers for a sum series. + Once generalized that way, sum_series(n, 0, 1) should be equivalent to fibonacci(n). + And sum_series(n, 2, 1) should be equivalent to lucas(n). + + sum_series(n, 3, 2) should generate antoehr series with no specific name + + The defaults are set to 0, 1, so if you don't pass in any values, you'll + get the fibonacci sercies + """ + pass + +if __name__ == "__main__": + # run some tests + assert fibonacci(0) == 0 + assert fibonacci(1) == 1 + assert fibonacci(2) == 1 + assert fibonacci(3) == 2 + assert fibonacci(4) == 3 + assert fibonacci(5) == 5 + assert fibonacci(6) == 8 + assert fibonacci(7) == 13 + + assert lucas(0) == 2 + assert lucas(1) == 1 + + assert lucas(4) == 7 + + # test that sum_series matches fibonacci + assert sum_series(5) == fibonacci(5) + assert sum_series(7, 0, 1) == fibonacci(7) + + # test if sum_series matched lucas + assert sum_series(5, 2, 1) == lucas(5) + + # test if sum_series works for arbitrary initial values + assert sum_series(0, 3, 2) == 3 + assert sum_series(1, 3, 2) == 2 + assert sum_series(2, 3, 2) == 5 + assert sum_series(3, 3, 2) == 7 + assert sum_series(4, 3, 2) == 12 + assert sum_series(5, 3, 2) == 19 + + print("tests passed") diff --git a/_downloads/348f5dbfabfa7d9ee9f70955645ff009/sherlock_small.txt b/_downloads/348f5dbfabfa7d9ee9f70955645ff009/sherlock_small.txt new file mode 100644 index 0000000..dcccaab --- /dev/null +++ b/_downloads/348f5dbfabfa7d9ee9f70955645ff009/sherlock_small.txt @@ -0,0 +1,17 @@ +One night--it was on the twentieth of March, 1888--I was +returning from a journey to a patient (for I had now returned to +civil practice), when my way led me through Baker Street. As I +passed the well-remembered door, which must always be associated +in my mind with my wooing, and with the dark incidents of the +Study in Scarlet, I was seized with a keen desire to see Holmes +again, and to know how he was employing his extraordinary powers. +His rooms were brilliantly lit, and, even as I looked up, I saw +his tall, spare figure pass twice in a dark silhouette against +the blind. He was pacing the room swiftly, eagerly, with his head +sunk upon his chest and his hands clasped behind him. To me, who +knew his every mood and habit, his attitude and manner told their +own story. He was at work again. He had risen out of his +drug-created dreams and was hot upon the scent of some new +problem. I rang the bell and was shown up to the chamber which +had formerly been in part my own. + diff --git a/_downloads/35f2dfffa997e2acb10d5b41052228f0/calculator.py b/_downloads/35f2dfffa997e2acb10d5b41052228f0/calculator.py new file mode 100644 index 0000000..8bd0c75 --- /dev/null +++ b/_downloads/35f2dfffa997e2acb10d5b41052228f0/calculator.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 + +"""calculator + + Usage: + + calculator.py 1 + 3 + +""" + +import sys + +import calculator_functions as functions + + +# put the real code in a function so we can test it +def main(): + + if len(sys.argv) != 4: + error_message = """ + + Invalid arguments. + + Usage: + + calculator.py 1 + 3 + + or 1 x 3 + or 1 / 3 + or 1 - 3 + """ + raise ValueError(error_message + "\n") + + x = sys.argv[1] + operator = sys.argv[2] + y = sys.argv[3] + + if operator == "+": + return functions.add(x, y) + + elif operator == "-": + return functions.subtract(x, y) + + elif operator == "x": + return functions.multiply(x, y) + + elif operator == "/": + return functions.divide(x, y) + + else: + return "invalid input" + +if __name__ == "__main__": + try: + print(main()) + except ValueError as err: + sys.stderr.write(err.args[0]) + sys.exit(1) diff --git a/_downloads/364bb62cb353315701441396645b6a53/test_html_output9.html b/_downloads/364bb62cb353315701441396645b6a53/test_html_output9.html new file mode 100644 index 0000000..05dfff2 --- /dev/null +++ b/_downloads/364bb62cb353315701441396645b6a53/test_html_output9.html @@ -0,0 +1,27 @@ + + + + + PythonClass = Revision 1087: + + +

PythonClass - Class 6 example

+

+ Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+
+ + + \ No newline at end of file diff --git a/_downloads/366509eb0a8e44364a5ebd1270c18204/calculator_test.sh b/_downloads/366509eb0a8e44364a5ebd1270c18204/calculator_test.sh new file mode 100644 index 0000000..f43dc38 --- /dev/null +++ b/_downloads/366509eb0a8e44364a5ebd1270c18204/calculator_test.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +test1="2 + 3" +test2="2 - 3" + +echo -n $test1 "= " +./calculator.py $test1 +echo -n $test2 "= " +./calculator.py $test2 diff --git a/_downloads/36cde6fb34f0afa8b462b69367aebb73/except_test.py b/_downloads/36cde6fb34f0afa8b462b69367aebb73/except_test.py new file mode 100644 index 0000000..905dd67 --- /dev/null +++ b/_downloads/36cde6fb34f0afa8b462b69367aebb73/except_test.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +""" +silly little test module that is designed to trigger Exceptions when +run from the except_exercise.py file +""" + +import time + +conclude = "And what leads you to that conclusion?" +district = "Finest in the district, sir." +cheese = "It's certainly uncontaminated by cheese." +clean = "Well, it's so clean." +shop = "Not much of a cheese shop really, is it?" +cust = "Customer: " +clerk = "Shopkeeper: " + + +def fun(reaper): + if reaper == 'spam': + print(s) + elif reaper == 'cheese': + print() + print('Spam, Spam, Spam, Spam, Beautiful Spam') + elif reaper == 'mr death': + print() + return('{}{}\n{}{}'.format(cust, shop, clerk, district)) + + +def more_fun(language): + if language == 'java': + test = [1, 2, 3] + test[5] = language + elif language == 'c': + print('{}{}\n{}{}'.format(cust, conclude, clerk, clean)) + + +def last_fun(): + print(cust, cheese) + time.sleep(1) + import antigravity diff --git a/_downloads/37f2d0abd497b27b3fed8a8b1d89d6fc/test_address_book_mongo.py b/_downloads/37f2d0abd497b27b3fed8a8b1d89d6fc/test_address_book_mongo.py new file mode 100644 index 0000000..83d714b --- /dev/null +++ b/_downloads/37f2d0abd497b27b3fed8a8b1d89d6fc/test_address_book_mongo.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python + +""" +test code for the mongo specific addressbook model +remember to start mongo database first + +$ mongod --dbpath=mongo_data/ +""" + +import pytest + +import address_book_mongo as model + + +@pytest.fixture +def chris(): + chris = model.Person(last_name='Barker', + first_name='Chris', + middle_name='H', + cell_phone='(123) 555-7890', + email='PythonCHB@gmail.com', + ) + return chris + + +@pytest.fixture +def fred(): + fred = model.Person(last_name='Jones', + first_name='Fred', + middle_name='B', + cell_phone='(123) 555-7890', + email='FredJones@gmail.com', + ) + return fred + + +def test_person_to_dict(chris): + dct = chris.to_dict() + + assert dct['last_name'] == "Barker" + assert dct['email'] == 'PythonCHB@gmail.com' + + +def test_person_to_from_dict(chris): + + dct = chris.to_dict() + chris2 = model.Person.from_dict(dct) + + print(chris2) + + assert chris2.last_name == 'Barker' + assert chris2.first_name == 'Chris' + assert chris2.middle_name == 'H' + assert chris2.cell_phone == '(123) 555-7890' + assert chris2.email == 'PythonCHB@gmail.com' + + +def test_household_to_dict(chris): + home = model.Household(name="The Barkers", + people=(chris,), + address=model.Address(line_1='123 Some St', + line_2='Apt 1234', + city='Seattle', + state='WA', + zip_code='98000',), + phone='123-456-8762', + ) + + home_dct = home.to_dict() + + assert home_dct['name'] == "The Barkers" + assert home_dct['address']['city'] == 'Seattle' + + +def test_household_to_dict_to_object(chris, fred): + + home = model.Household(name="The Barkers", + people=(chris, fred), + address=model.Address(line_1='123 Some St', + line_2='Apt 1234', + city='Seattle', + state='WA', + zip_code='98000',), + phone='123-456-8762', + ) + + home2 = model.Household.from_dict(home.to_dict()) + + assert home2.name == home.name + assert home2.phone == home.phone + assert home2.address.line_1 == home.address.line_1 + assert home2.address.line_2 == home.address.line_2 + assert home2.people == home.people diff --git a/_downloads/399e36e44f4ba5aec7db0351a5f6802e/sort_key.py b/_downloads/399e36e44f4ba5aec7db0351a5f6802e/sort_key.py new file mode 100644 index 0000000..e10df5a --- /dev/null +++ b/_downloads/399e36e44f4ba5aec7db0351a5f6802e/sort_key.py @@ -0,0 +1,55 @@ +""" +demonstration of defining a sort_key method for sorting +""" + +import random +import time + + +class Simple: + """ + simple class to demonstrate a simple sorting key method + """ + + def __init__(self, val): + self.val = val + + def sort_key(self): + """ + sorting key function --used to pass in to sort functions + to get faster sorting + + Example:: + + sorted(list_of_simple_objects, key=Simple.sort_key) + + """ + return self.val + + def __lt__(self, other): + """ + less than --required for regular sorting + """ + return self.val < other.val + + def __repr__(self): + return "Simple({})".format(self.val) + + +if __name__ == "__main__": + N = 10000 + a_list = [Simple(random.randint(0, 10000)) for i in range(N)] + # print("Before sorting:", a_list) + + print("Timing for {} items".format(N)) + start = time.clock() + sorted(a_list) + reg_time = time.clock() - start + print("regular sort took: {:.4g}s".format(reg_time)) + + start = time.clock() + sorted(a_list, key=Simple.sort_key) + key_time = time.clock() - start + print("key sort took: {:.4g}s".format(key_time)) + + print("performance improvement factor: {:.4f}".format((reg_time / key_time))) diff --git a/_downloads/39ca0e08e448d94dc2220876d643da86/test_generator.py b/_downloads/39ca0e08e448d94dc2220876d643da86/test_generator.py new file mode 100644 index 0000000..06409e6 --- /dev/null +++ b/_downloads/39ca0e08e448d94dc2220876d643da86/test_generator.py @@ -0,0 +1,61 @@ +""" +test_generator.py + +tests the solution to the generator lab + +can be run with pytest +""" + +import generator_solution as gen + + +def test_intsum(): + + g = gen.intsum() + + assert next(g) == 0 + assert next(g) == 1 + assert next(g) == 3 + assert next(g) == 6 + assert next(g) == 10 + assert next(g) == 15 + + +def test_intsum2(): + + g = gen.intsum2() + + assert next(g) == 0 + assert next(g) == 1 + assert next(g) == 3 + assert next(g) == 6 + assert next(g) == 10 + assert next(g) == 15 + + +def test_doubler(): + + g = gen.doubler() + + assert next(g) == 1 + assert next(g) == 2 + assert next(g) == 4 + assert next(g) == 8 + assert next(g) == 16 + assert next(g) == 32 + + for i in range(10): + j = next(g) + + assert j == 2**15 + + +def test_fib(): + g = gen.fib() + assert [next(g) for i in range(9)] == [1, 1, 2, 3, 5, 8, 13, 21, 34] + + +def test_prime(): + g = gen.prime() + for val in [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]: + assert next(g) == val diff --git a/_downloads/3d642a64786adc4483a66074a502119c/ICanEatGlass.utf8.txt b/_downloads/3d642a64786adc4483a66074a502119c/ICanEatGlass.utf8.txt new file mode 100644 index 0000000..9ecba2b --- /dev/null +++ b/_downloads/3d642a64786adc4483a66074a502119c/ICanEatGlass.utf8.txt @@ -0,0 +1,23 @@ +I Can Eat Glass: + +And from the sublime to the ridiculous, here is a certain phrase in an assortment of languages: + +Sanskrit: काचं शक्नोम्यत्तुम् । नोपहिनस्ति माम् ॥ + +Sanskrit (standard transcription): kācaṃ śaknomyattum; nopahinasti mām. + +Classical Greek: ὕαλον ϕαγεῖν δύναμαι· τοῦτο οὔ με βλάπτει. + +Greek (monotonic): Μπορώ να φάω σπασμένα γυαλιά χωρίς να πάθω τίποτα. + +Greek (polytonic): Μπορῶ νὰ φάω σπασμένα γυαλιὰ χωρὶς νὰ πάθω τίποτα. + +Latin: Vitrum edere possum; mihi non nocet. + +Old French: Je puis mangier del voirre. Ne me nuit. + +French: Je peux manger du verre, ça ne me fait pas mal. + +Provençal / Occitan: Pòdi manjar de veire, me nafrariá pas. + +Québécois: J'peux manger d'la vitre, ça m'fa pas mal. \ No newline at end of file diff --git a/_downloads/3e29de91d4027f47dcec7e9c4da617dc/roman4.py b/_downloads/3e29de91d4027f47dcec7e9c4da617dc/roman4.py new file mode 100644 index 0000000..e96336c --- /dev/null +++ b/_downloads/3e29de91d4027f47dcec7e9c4da617dc/roman4.py @@ -0,0 +1,119 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + +import pytest + +roman_numeral_map = (('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1)) + + +def to_roman(n): + '''convert integer to Roman numeral''' + if n > 3999: + raise ValueError("number out of range (must be less than 4000)") + + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + return result + + +## 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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result + + +def test_too_large(): + """ + to_roman should raise an ValueError when passed + values over 3999 + """ + with pytest.raises(ValueError): + to_roman(4000) + + diff --git a/_downloads/3f0c6d7154b2c25aaad1a76a5ad04c19/roman17.py b/_downloads/3f0c6d7154b2c25aaad1a76a5ad04c19/roman17.py new file mode 100644 index 0000000..6ad7bf1 --- /dev/null +++ b/_downloads/3f0c6d7154b2c25aaad1a76a5ad04c19/roman17.py @@ -0,0 +1,274 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + +import pytest + +roman_numeral_map = (('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1)) + + +def to_roman(n): + """convert integer to Roman numeral""" + if not (0 < n < 4000): + raise ValueError("number out of range (must be 1..3999)") + + if int(n) != n: + raise ValueError("Only integers can be converted to Roman numerals") + + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + return result + + +def is_valid_roman_numeral(s): + """ + parse a Roman numeral as a human would: left to right, + looking for valid characters and removing them to determine + if this is, indeed, a valid Roman numeral + """ + # first look for the thousands -- up to three Ms + for _ in range(3): + if s[:1] == "M": + s = s[1:] + # then look for the hundreds: + # there can be only one of CM, CD, or D: + if s[:2] == "CM": # 900 + s = s[2:] + elif s[:2] == "CD": # 400 + s = s[2:] + else: + if s[:1] == "D": # 500 + s = s[1:] + # there can be from 1 to 3 Cs + for _ in range(3): + if s[:1] == "C": + s = s[1:] + # now the tens + # There can be one of either XC, XL or L + if s[:2] == "XC": # 90 + s = s[2:] + elif s[:2] == "XL": # 40 + s = s[2:] + else: + if s[:1] == "L": # 50 + s = s[1:] + # there can be up to three Xs + for _ in range(3): + if s[:1] == "X": + s = s[1:] + # and the ones + # There can be one of IX, IV or V + if s[:2] == "IX": # 9 + s = s[2:] + elif s[:2] == "IV": # 4 + s = s[2:] + else: + if s[:1] == "V": # 5 + s = s[1:] + # There can be up to three Is + for _ in range(3): + if s[:1] == "I": # 1 + s = s[1:] + # if there is anything left, it's not a valid Roman numeral + if s: + return False + else: + return True + + +def from_roman(s): + """convert Roman numeral to integer""" + if not is_valid_roman_numeral(s): + raise ValueError(f"{s} is not a valid Roman numeral") + result = 0 + index = 0 + for numeral, integer in roman_numeral_map: + while s[index:index + len(numeral)] == numeral: + result += integer + index += len(numeral) + return result + + +##################################### +# 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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result + + +def test_too_large(): + """ + to_roman should raise a ValueError when passed + values over 3999 + """ + with pytest.raises(ValueError): + to_roman(4000) + + +def test_zero(): + """to_roman should raise a ValueError with 0 input""" + with pytest.raises(ValueError): + to_roman(0) + + +def test_negative(): + """to_roman should raise a ValueError with negative input""" + with pytest.raises(ValueError): + to_roman(-1) + + +def test_non_integer(): + """to_roman should raise a ValueError with non-integer input""" + with pytest.raises(ValueError): + to_roman(0.5) + + +def test_float_with_integer_value(): + """to_roman should work for floats with integer values""" + assert to_roman(3.0) == "III" + + +# #################### +# Tests for from_roman +# #################### + + +def test_from_roman_known_values(): + """from_roman should give known result with known input""" + for integer, numeral in KNOWN_VALUES: + result = from_roman(numeral) + assert integer == result + + +def test_roundtrip(): + """from_roman(to_roman(n))==n for all n""" + for integer in range(1, 4000): + numeral = to_roman(integer) + result = from_roman(numeral) + assert integer == result + + +# The following to test for valid roman numerals + +def test_invalid_character(): + """ + Roman numerals can only use these characters: + + M, D, C, L, X, V, I + + This tests that other characters will cause a failure + """ + for s in ['Z', 'XXIIIQ', 'QXXIII', 'XXYIII']: + with pytest.raises(ValueError): + from_roman(s) + + +def test_too_many_repeated_numerals(): + '''from_roman should fail with too many repeated numerals''' + for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'): + with pytest.raises(ValueError): + print(f"trying: {s}") + from_roman(s) + + +def test_repeated_pairs(): + '''from_roman should fail with repeated pairs of numerals''' + for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'): + with pytest.raises(ValueError): + print(f"trying: {s}") + from_roman(s) + + +def test_malformed_antecedents(): + '''from_roman should fail with malformed antecedents''' + for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV', + 'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'): + with pytest.raises(ValueError): + print(f"trying: {s}") + from_roman(s) diff --git a/_downloads/409972d036b41f742f82ab8275e77818/address_book_model.py b/_downloads/409972d036b41f742f82ab8275e77818/address_book_model.py new file mode 100644 index 0000000..6cc28cb --- /dev/null +++ b/_downloads/409972d036b41f742f82ab8275e77818/address_book_model.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python + +""" +sample data for NOSQL examples + +This version has a not completely-trival data model +""" + + +class Person(object): + """ + class to represent an individual person + """ + + def __init__(self, + last_name, + first_name='', + middle_name='', + cell_phone='', + email='', + ): + """ + initialize a Person object: + """ + self.first_name = first_name.strip() + self.last_name = last_name.strip() + self.middle_name = middle_name.strip() + self.cell_phone = cell_phone.strip() + self.email = email.strip() + + @property + def name(self): + return " ".join([self.first_name, self.middle_name, self.last_name]) + + def __str__(self): + msg = '{first_name} {middle_name} {last_name}'.format(**self.__dict__) + return msg + + def __repr__(self): + """ + not a good ___repr__, but want to have something here + """ + return self.__str__() + + +class Address(object): + """ + class that represents an address + """ + + def __init__(self, + line_1='', + line_2='', + city='', + state='', + zip_code='', + ): + """ + initialize an address + """ + + self.line_1 = line_1.strip() + self.line_2 = line_2.strip() + self.city = city.strip() + self.state = state.strip() + self.zip_code = str(zip_code).strip() + + def __str__(self): + msg = "{line_1}\n{line_2}\n{city} {state} {zip_code}\n".format(**self.__dict__) + return msg + + +class Household(object): + """ + Class that represents a Household. + + A household has one or more people, and a Location + """ + + def __init__(self, + name='', + people=(), + address=None, + phone='' + ): + self.name = name.strip() + self.people = list(people) + self.address = address + self.phone = phone.strip() + + def __str__(self): + msg = [self.name + ":"] + msg += [str(self.address)] + return "\n".join(msg) + + def __repr__(self): + return self.__str__() + + +class Business(Household): + """ + Class that represents a Business + + A business has one or more people, + and address and a phone number + """ + # Same as household now, but you never know. + pass + + +class AddressBook(object): + """ + And address book -- has people, households, businesses. + + All fully cross-referenced + """ + + def __init__(self, + people=(), + businesses=(), + households=(), + ): + self.people = list(people) + self.businesses = list(businesses) + self.households = list(households) + + def add_person(self, person): + self.people.append(person) + + def add_household(self, household): + self.households.append(household) + + def add_business(self, business): + self.businesses.append(business) + + def __str__(self): + msg = ["An Address Book:"] + msg += ["People:"] + msg += [" " + person.name for person in self.find_people()] + msg += ["Households:"] + msg += [" " + house.name for house in self.find_households()] + msg += ["Businesses:"] + msg += [" " + bus.name for bus in self.find_businesses()] + + return "\n".join(msg) + + @property + def locations(self): + return self.households + self.businesses + + def find_people(self, name=''): + """ + find all the people with name in their name somewhere + """ + return [person for person in self.people if name.lower() in person.name.lower()] + + def find_zip_codes(self, zip_code): + """ + find all the locations with this zip_code + """ + zip_code = str(zip_code).strip() + return [loc for loc in self.locations if loc.address.zip_code == zip_code] + + def find_state(self, state): + """ + find all the locations in this state + """ + return [location for location in self.locations if location.address.state == state] + + +def create_sample(): + """ + Create a sample Address Book + """ + chris = Person(last_name='Barker', + first_name='Chris', + middle_name='H', + cell_phone='(123) 555-7890', + email='PythonCHB@gmail.com', + ) + + emma = Person(last_name='Barker', + first_name='Emma', + middle_name='L', + cell_phone='(345) 555-9012', + email='emma@something.com', + ) + + donna = Person(last_name='Barker', + first_name='Donna', + middle_name='L', + cell_phone='(111) 555-1111', + email='dbarker@something.com', + ) + + barker_address = Address(line_1='123 Some St', + line_2='Apt 1234', + city='Seattle', + state='WA', + zip_code='98000',) + + the_barkers = Household(name="The Barkers", + people=(chris, donna, emma), + address=barker_address) + + joseph = Person(last_name='Sheedy', + first_name='Joseph', + cell_phone='(234) 555-8910', + email='js@some_thing.com', + ) + + cris = Person(last_name='Ewing', + first_name='Cris', + cell_phone='(345) 555-6789', + email='cris@a_fake_domain.com', + ) + + fulvio = Person(last_name='Casali', + first_name='Fulvio', + cell_phone='(345) 555-1234', + email='fulvio@a_fake_domain.com', + ) + + fred = Person(first_name="Fred", + last_name="Jones", + email='FredJones@some_company.com', + cell_phone="403-561-8911", + ) + + python_cert_address = Address('UW Professional and Continuing Education', + line_2='4333 Brooklyn Ave. NE', + city='Seattle', + state='WA', + zip_code='98105', + ) + + python_cert = Business(name='Python Certification Program', + people=(chris, joseph, cris, fulvio), + address=python_cert_address + ) + + + address_book = AddressBook() + + address_book.add_person(chris) + address_book.add_person(donna) + address_book.add_person(emma) + address_book.add_person(cris) + address_book.add_person(joseph) + address_book.add_person(fulvio) + address_book.add_person(fred) + + address_book.add_household(the_barkers) + + address_book.add_business(python_cert) + + return address_book + + +if __name__ == "__main__": + address_book = create_sample() + + print("Here is the address book") + print(address_book) + + diff --git a/_downloads/429df84ad67b90296d8e26a683f2ae13/yield_example.py b/_downloads/429df84ad67b90296d8e26a683f2ae13/yield_example.py new file mode 100644 index 0000000..adf9448 --- /dev/null +++ b/_downloads/429df84ad67b90296d8e26a683f2ae13/yield_example.py @@ -0,0 +1,37 @@ +def counter(): + print('counter: starting counter') + i = -3 + while i < 3: + i = i + 1 + print('counter: yield', i) + yield i + +def y_range(start, stop, step=1): + print("at the start") + i = start + while i < stop: + print("about to yield") + yield i + print("after yield") + i += step + print("at end of func") + +def test(): + yield 4 + yield 45 + yield 12 + + +# if __name__ == '__main__': +# print "the generator function:" +# print repr(counter) +# print "call generator function" + +# c = counter() +# print " note that nothing printed" +# print "the generator:" +# print repr(c) + +# print 'iterate' +# for item in c: +# print 'received:', item diff --git a/_downloads/45c4e42b686cdce6e053dee42cf8429a/test_html_render.py b/_downloads/45c4e42b686cdce6e053dee42cf8429a/test_html_render.py new file mode 100644 index 0000000..5833d50 --- /dev/null +++ b/_downloads/45c4e42b686cdce6e053dee42cf8429a/test_html_render.py @@ -0,0 +1,264 @@ +""" +test code for html_render.py + +This is just a start -- you will need more tests! +""" + +import io +import pytest + +# import * is often bad form, but makes it easier to test everything in a module. +from html_render import * + + +# utility function for testing render methods +# needs to be used in multiple tests, so we write it once here. +def render_result(element, ind=""): + """ + calls the element's render method, and returns what got rendered as a + string + """ + # the StringIO object is a "file-like" object -- something that + # provides the methods of a file, but keeps everything in memory + # so it can be used to test code that writes to a file, without + # having to actually write to disk. + outfile = io.StringIO() + # this so the tests will work before we tackle indentation + if ind: + element.render(outfile, ind) + else: + element.render(outfile) + return outfile.getvalue() + +######## +# Step 1 +######## + +def test_init(): + """ + This only tests that it can be initialized with and without + some content -- but it's a start + """ + e = Element() + + e = Element("this is some text") + + +def test_append(): + """ + This tests that you can append text + + It doesn't test if it works -- + that will be covered by the render test later + """ + e = Element("this is some text") + e.append("some more text") + + +def test_render_element(): + """ + Tests whether the Element can render two pieces of text + So it is also testing that the append method works correctly. + + It is not testing whether indentation or line feeds are correct. + """ + e = Element("this is some text") + e.append("and this is some more text") + + # This uses the render_results utility above + file_contents = render_result(e).strip() + + # making sure the content got in there. + assert("this is some text") in file_contents + assert("and this is some more text") in file_contents + + # make sure it's in the right order + assert file_contents.index("this is") < file_contents.index("and this") + + # making sure the opening and closing tags are right. + assert file_contents.startswith("") + assert file_contents.endswith("") + +# # Uncomment this one after you get the one above to pass +# # Does it pass right away? +# def test_render_element2(): +# """ +# Tests whether the Element can render two pieces of text +# So it is also testing that the append method works correctly. + +# It is not testing whether indentation or line feeds are correct. +# """ +# e = Element() +# e.append("this is some text") +# e.append("and this is some more text") + +# # This uses the render_results utility above +# file_contents = render_result(e).strip() + +# # making sure the content got in there. +# assert("this is some text") in file_contents +# assert("and this is some more text") in file_contents + +# # make sure it's in the right order +# assert file_contents.index("this is") < file_contents.index("and this") + +# # making sure the opening and closing tags are right. +# assert file_contents.startswith("") +# assert file_contents.endswith("") + + + +# # ######## +# # # Step 2 +# # ######## + +# # tests for the new tags +# def test_html(): +# e = Html("this is some text") +# e.append("and this is some more text") + +# file_contents = render_result(e).strip() + +# assert("this is some text") in file_contents +# assert("and this is some more text") in file_contents +# print(file_contents) +# assert file_contents.endswith("") + + +# def test_body(): +# e = Body("this is some text") +# e.append("and this is some more text") + +# file_contents = render_result(e).strip() + +# assert("this is some text") in file_contents +# assert("and this is some more text") in file_contents + +# assert file_contents.startswith("") +# assert file_contents.endswith("") + + +# def test_p(): +# e = P("this is some text") +# e.append("and this is some more text") + +# file_contents = render_result(e).strip() + +# assert("this is some text") in file_contents +# assert("and this is some more text") in file_contents + +# assert file_contents.startswith("

") +# assert file_contents.endswith("

") + + +# def test_sub_element(): +# """ +# tests that you can add another element and still render properly +# """ +# page = Html() +# page.append("some plain text.") +# page.append(P("A simple paragraph of text")) +# page.append("Some more plain text.") + +# file_contents = render_result(page) +# print(file_contents) # so we can see it if the test fails + +# # note: The previous tests should make sure that the tags are getting +# # properly rendered, so we don't need to test that here. +# assert "some plain text" in file_contents +# assert "A simple paragraph of text" in file_contents +# assert "Some more plain text." in file_contents +# assert "some plain text" in file_contents +# # but make sure the embedded element's tags get rendered! +# assert "

" in file_contents +# assert "

" in file_contents + + + + +######## +# Step 3 +######## + +# Add your tests here! + +# ##################### +# # indentation testing +# # Uncomment for Step 9 -- adding indentation +# ##################### + + +# def test_indent(): +# """ +# Tests that the indentation gets passed through to the renderer +# """ +# html = Html("some content") +# file_contents = render_result(html, ind=" ").rstrip() #remove the end newline + +# print(file_contents) +# lines = file_contents.split("\n") +# assert lines[0].startswith(" <") +# print(repr(lines[-1])) +# assert lines[-1].startswith(" <") + + +# def test_indent_contents(): +# """ +# The contents in a element should be indented more than the tag +# by the amount in the indent class attribute +# """ +# html = Element("some content") +# file_contents = render_result(html, ind="") + +# print(file_contents) +# lines = file_contents.split("\n") +# assert lines[1].startswith(Element.indent) + + +# def test_multiple_indent(): +# """ +# make sure multiple levels get indented fully +# """ +# body = Body() +# body.append(P("some text")) +# html = Html(body) + +# file_contents = render_result(html) + +# print(file_contents) +# lines = file_contents.split("\n") +# for i in range(3): # this needed to be adapted to the tag +# assert lines[i + 1].startswith(i * Element.indent + "<") + +# assert lines[4].startswith(3 * Element.indent + "some") + + +# def test_element_indent1(): +# """ +# Tests whether the Element indents at least simple content + +# we are expecting to to look like this: + +# +# this is some text +# <\html> + +# More complex indentation should be tested later. +# """ +# e = Element("this is some text") + +# # This uses the render_results utility above +# file_contents = render_result(e).strip() + +# # making sure the content got in there. +# assert("this is some text") in file_contents + +# # break into lines to check indentation +# lines = file_contents.split('\n') +# # making sure the opening and closing tags are right. +# assert lines[0] == "" +# # this line should be indented by the amount specified +# # by the class attribute: "indent" +# assert lines[1].startswith(Element.indent + "thi") +# assert lines[2] == "" +# assert file_contents.endswith("") diff --git a/_downloads/4885db03ed9fb66cf70e84169cdd3ce9/ICanEatGlass.utf16.txt b/_downloads/4885db03ed9fb66cf70e84169cdd3ce9/ICanEatGlass.utf16.txt new file mode 100644 index 0000000..24a0858 Binary files /dev/null and b/_downloads/4885db03ed9fb66cf70e84169cdd3ce9/ICanEatGlass.utf16.txt differ diff --git a/_downloads/4d500a4c2144e6798d7356e83fbaaadc/html_render.py b/_downloads/4d500a4c2144e6798d7356e83fbaaadc/html_render.py new file mode 100644 index 0000000..508400f --- /dev/null +++ b/_downloads/4d500a4c2144e6798d7356e83fbaaadc/html_render.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +""" +A class-based system for rendering html. +""" + + +# This is the framework for the base class +class Element(object): + + def __init__(self, content=None): + pass + + def append(self, new_content): + pass + + def render(self, out_file): + out_file.write("just something as a place holder...") diff --git a/_downloads/4d6c220fd82d44ba66aaa0feefb1c356/json_save.zip b/_downloads/4d6c220fd82d44ba66aaa0feefb1c356/json_save.zip new file mode 100644 index 0000000..86b3357 Binary files /dev/null and b/_downloads/4d6c220fd82d44ba66aaa0feefb1c356/json_save.zip differ diff --git a/_downloads/4ed287fdca0fbdd67450a61a31e447ae/test_random_pytest.py b/_downloads/4ed287fdca0fbdd67450a61a31e447ae/test_random_pytest.py new file mode 100644 index 0000000..478aa6c --- /dev/null +++ b/_downloads/4ed287fdca0fbdd67450a61a31e447ae/test_random_pytest.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +""" +port of the random unit tests from the python docs to py.test +""" + +import random +import pytest + + +example_seq = list(range(10)) + + +def test_choice(): + """ + A choice selected should be in the sequence + """ + element = random.choice(example_seq) + assert (element in example_seq) + + +def test_sample(): + """ + All the items in a sample should be in the sequence + """ + for element in random.sample(example_seq, 5): + assert element in example_seq + + +def test_shuffle(): + """ + Make sure a shuffled sequence does not lose any elements + """ + seq = list(range(10)) + random.shuffle(seq) + seq.sort() # If you comment this out, it will fail, so you can see output + print("seq:", seq) # only see output if it fails + assert seq == list(range(10)) + + +def test_shuffle_immutable(): + """ + Trying to shuffle an immutable sequence raises an Exception + """ + with pytest.raises(TypeError): + random.shuffle((1, 2, 3)) + + +def test_sample_too_large(): + """ + Trying to sample more than exist should raise an error + """ + with pytest.raises(ValueError): + random.sample(example_seq, 20) diff --git a/_downloads/50d58e5495b0eae8c0bbe741381110a9/capitalize.zip b/_downloads/50d58e5495b0eae8c0bbe741381110a9/capitalize.zip new file mode 100644 index 0000000..e6679b7 Binary files /dev/null and b/_downloads/50d58e5495b0eae8c0bbe741381110a9/capitalize.zip differ diff --git a/_downloads/50e6adcf4150453a18997da5016f9b3a/capitalize.zip b/_downloads/50e6adcf4150453a18997da5016f9b3a/capitalize.zip new file mode 100644 index 0000000..2c7d361 Binary files /dev/null and b/_downloads/50e6adcf4150453a18997da5016f9b3a/capitalize.zip differ diff --git a/_downloads/5191f18c29e0304aa9ecf3a3c24d288a/gather.py b/_downloads/5191f18c29e0304aa9ecf3a3c24d288a/gather.py new file mode 100644 index 0000000..c5e4b31 --- /dev/null +++ b/_downloads/5191f18c29e0304aa9ecf3a3c24d288a/gather.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +""" +test of gather() + +adapted from: + +https://docs.python.org/3/library/asyncio-task.html + +""" + +import asyncio + + +async def factorial(name, number): + f = 1 + for i in range(2, number + 1): + print("Task %s: Compute factorial(%s)..." % (name, i)) + await asyncio.sleep(1) # to simulate a longer process... + f *= i + print("Task %s: factorial(%s) = %s" % (name, number, f)) + +loop = asyncio.get_event_loop() +loop.run_until_complete(asyncio.gather(factorial("A", 2), + factorial("B", 3), + factorial("C", 4), + )) +loop.close() diff --git a/_downloads/51e0be5fba30ccd96d0af007dd7dadd1/except_exercise.py b/_downloads/51e0be5fba30ccd96d0af007dd7dadd1/except_exercise.py new file mode 100644 index 0000000..96c8870 --- /dev/null +++ b/_downloads/51e0be5fba30ccd96d0af007dd7dadd1/except_exercise.py @@ -0,0 +1,43 @@ +#!/usr/bin/python + +""" +An exercise in playing with Exceptions. +Make lots of try/except blocks for fun and profit. + +Make sure to catch specifically the error you find, rather than all errors. +""" + +from except_test import fun, more_fun, last_fun + + +# Figure out what the exception is, catch it and while still +# in that catch block, try again with the second item in the list +first_try = ['spam', 'cheese', 'mr death'] + +joke = fun(first_try[0]) + +# Here is a try/except block. Add an else that prints not_joke +try: + not_joke = fun(first_try[2]) +except SyntaxError: + print('Run Away!') + +# What did that do? You can think of else in this context, as well as in +# loops as meaning: "else if nothing went wrong" +# (no breaks in loops, no exceptions in try blocks) + +# Figure out what the exception is, catch it and in that same block +# +# try calling the more_fun function with the 2nd language in the list, +# again assigning it to more_joke. +# +# If there are no exceptions, call the more_fun function with the last +# language in the list + +# Finally, while still in the try/except block and regardless of whether +# there were any exceptions, call the function last_fun with no +# parameters. (pun intended) + +langs = ['java', 'c', 'python'] + +more_joke = more_fun(langs[0]) diff --git a/_downloads/521482e4d637cea6ea62c9301a1c4eb6/cool_meta.py b/_downloads/521482e4d637cea6ea62c9301a1c4eb6/cool_meta.py new file mode 100644 index 0000000..ef3e821 --- /dev/null +++ b/_downloads/521482e4d637cea6ea62c9301a1c4eb6/cool_meta.py @@ -0,0 +1,30 @@ +""" +Complete do-nothing metaclass example + +It serves to show when each special method of the metaclass is called. + +""" + + +class CoolMeta(type): + def __new__(meta, name, bases, dct): + print('Creating class in CoolMeta.__new__', name) + return super().__new__(meta, name, bases, dct) + + def __init__(cls, name, bases, dct): + print('Initializing class in CoolMeta.__init__', name) + super().__init__(name, bases, dct) + + def __call__(cls, *args, **kw): + print('calling CoolMeta to instantiate ', cls) + return type.__call__(cls, *args, **kw) + + +class CoolClass(metaclass=CoolMeta): + def __init__(self): + print('And now my CoolClass object exists') + + +print('everything loaded, instantiate a CoolClass instance now') + +foo = CoolClass() diff --git a/_downloads/53ac773066adb686cc0ec745da0172cf/test_html_output2.html b/_downloads/53ac773066adb686cc0ec745da0172cf/test_html_output2.html new file mode 100644 index 0000000..4908a12 --- /dev/null +++ b/_downloads/53ac773066adb686cc0ec745da0172cf/test_html_output2.html @@ -0,0 +1,10 @@ + + +

+Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+

+And here is another piece of text -- you should be able to add any number +

+ + \ No newline at end of file diff --git a/_downloads/542beff2efdf06b15b303acf100630f1/integrate_threads.py b/_downloads/542beff2efdf06b15b303acf100630f1/integrate_threads.py new file mode 100644 index 0000000..16c0d7f --- /dev/null +++ b/_downloads/542beff2efdf06b15b303acf100630f1/integrate_threads.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 + +import threading +import queue + +# from integrate.integrate import integrate, f +from integrate import f, integrate_numpy as integrate +from decorators import timer + + +@timer +def threading_integrate(f, a, b, N, thread_count=2): + """break work into N chunks""" + N_chunk = int(float(N) / thread_count) + dx = float(b - a) / thread_count + + results = queue.Queue() + + def worker(*args): + results.put(integrate(*args)) + + for i in range(thread_count): + x0 = dx * i + x1 = x0 + dx + thread = threading.Thread(target=worker, args=(f, x0, x1, N_chunk)) + thread.start() + print("Thread %s started" % thread.name) + + return sum((results.get() for i in range(thread_count))) + + +if __name__ == "__main__": + + # parameters of the integration + a = 0.0 + b = 10.0 + N = 10**8 + thread_count = 1 + + print("Numerical solution with N=%(N)d : %(x)f" % + {'N': N, 'x': threading_integrate(f, a, b, N, thread_count=thread_count)}) + diff --git a/_downloads/55ad87a5810d2526f7a268685e2c593d/ultra_simple.py b/_downloads/55ad87a5810d2526f7a268685e2c593d/ultra_simple.py new file mode 100644 index 0000000..bc602fe --- /dev/null +++ b/_downloads/55ad87a5810d2526f7a268685e2c593d/ultra_simple.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +""" +Ultra Simple async example +""" + +import asyncio + + +async def say_lots(num): + for i in range(num): + print(f'This was run by the loop ({i}) :') + await asyncio.sleep(1.0) + +# getting the event loop +loop = asyncio.get_event_loop() +# run it: +loop.run_until_complete(say_lots(5)) +print("done with loop") diff --git a/_downloads/566a212d55865ad8b5ed1c32fec3eeaa/test_walnut_party.py b/_downloads/566a212d55865ad8b5ed1c32fec3eeaa/test_walnut_party.py new file mode 100644 index 0000000..62f226d --- /dev/null +++ b/_downloads/566a212d55865ad8b5ed1c32fec3eeaa/test_walnut_party.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python + +""" +test code for the walnut party example + +Adapted from the "coding bat" site: https://codingbat.com/python + +When squirrels get together for a party, they like to have walnuts. +A squirrel party is successful when the number of walnuts is between +40 and 60, inclusive. Unless it is the weekend, in which case there +is no upper bound on the number of walnuts. + +Return True if the party with the given values is successful, +or False otherwise. +""" + + +# you can change this import to test different versions +from walnut_party import walnut_party +# from walnut_party import walnut_party2 as walnut_party +# from walnut_party import walnut_party3 as walnut_party + + +def test_1(): + assert walnut_party(30, False) is False + + +def test_2(): + assert walnut_party(50, False) is True + + +def test_3(): + assert walnut_party(70, True) is True + + +def test_4(): + assert walnut_party(30, True) is False + + +def test_5(): + assert walnut_party(50, True) is True + + +def test_6(): + assert walnut_party(60, False) is True + + +def test_7(): + assert walnut_party(61, False) is False + + +def test_8(): + assert walnut_party(40, False) is True + + +def test_9(): + assert walnut_party(39, False) is False + + +def test_10(): + assert walnut_party(40, True) is True + + +def test_11(): + assert walnut_party(39, True) is False diff --git a/_downloads/56c8a64286905e2661129d04085c7408/index_slicing.py b/_downloads/56c8a64286905e2661129d04085c7408/index_slicing.py new file mode 100644 index 0000000..dedd464 --- /dev/null +++ b/_downloads/56c8a64286905e2661129d04085c7408/index_slicing.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +""" +examples / test code for __getindex__ + +Doesn't really do anything, but you can see what happens with different indexing. +""" + +import operator + + +class IndexTest: + + def __getitem__(self, index): + # print("In getindex, indexes is:", index) + if isinstance(index, slice): + print("it's a single slice:", index) + elif isinstance(index, tuple): + print("it's a multi-dimensional slice:", index) + else: + try: + ind = operator.index(index) # this converts arbitrary objects to an int. + print("it's an index: ", ind) + except TypeError: # not a simple index + raise + print("It's a simple index") + + +if __name__ == "__main__": + + it = IndexTest() + + print("calling with simple index") + it[4] + + print("calling with single slice") + it[3:4] + + print("calling with two slices") + it[3:4, 7:8] + + print("calling with an invalid index") + it["this"] + + + + + + + diff --git a/_downloads/5859f3c42ca7d8674b8b2a98d2c7fc14/roman5.py b/_downloads/5859f3c42ca7d8674b8b2a98d2c7fc14/roman5.py new file mode 100644 index 0000000..fba421c --- /dev/null +++ b/_downloads/5859f3c42ca7d8674b8b2a98d2c7fc14/roman5.py @@ -0,0 +1,132 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + +import pytest + +roman_numeral_map = (('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1)) + + +def to_roman(n): + """convert integer to Roman numeral""" + if n > 3999: + raise ValueError("number out of range (must be less than 4000)") + + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + return result + + +## 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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result + + +def test_too_large(): + """ + to_roman should raise an ValueError when passed + values over 3999 + """ + with pytest.raises(ValueError): + to_roman(4000) + + +def test_zero(): + """to_roman should raise an ValueError with 0 input""" + 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) + + + diff --git a/_downloads/5a5fdff5de7f7f1944ea29e74393d4b9/test_calculator_pytest.py b/_downloads/5a5fdff5de7f7f1944ea29e74393d4b9/test_calculator_pytest.py new file mode 100644 index 0000000..0482978 --- /dev/null +++ b/_downloads/5a5fdff5de7f7f1944ea29e74393d4b9/test_calculator_pytest.py @@ -0,0 +1,39 @@ +#!/use/bin/env python + +""" +tests for the calculator module + +designed to be run with pytest +""" + +import pytest + +import calculator_functions as calc + + +# a very simple test +def test_add(): + assert calc.add(2, 3) == 5 + + +# testing with a variety of parameters: +def test_multiply_ugly(): + """ + the ugly, not very robust way.... + """ + assert calc.multiply(2, 2) == 4 + assert calc.multiply(2, -1) == -2 + assert calc.multiply(-2, -3) == 6 + assert calc.multiply(3, 0) == 0 + assert calc.multiply(0, 3) == 0 + + +param_names = "arg1, arg2, result" +params = [(2, 2, 4), + (2, -1, -2), + (-2, -2, 4), + (3, 0, 0), + ] +@pytest.mark.parametrize(param_names, params) +def test_multiply(arg1, arg2, result): + assert calc.multiply(arg1, arg2) == result diff --git a/_downloads/5aa816096a0d060405a1f5d2c6bce87f/address_book_mongo.py b/_downloads/5aa816096a0d060405a1f5d2c6bce87f/address_book_mongo.py new file mode 100644 index 0000000..9d0a96a --- /dev/null +++ b/_downloads/5aa816096a0d060405a1f5d2c6bce87f/address_book_mongo.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python + +""" +sample data for NOSQL examples + +This version uses mongoDB to store the data. + +NOTE: you need to start up the DB first: + +$ mongod --dbpath=mongo_data/ + +""" + +# ObjectID provides a new unique ID when you need one. +from bson import ObjectId + + +class PersistObject: + """ + mix-in class for object you want to be able to put in a mongoDB + + defines the basic to_dict and from_dict methods + + This could be a metaclass, or class decorator, or... + """ + def to_dict(self): + """ + returns a dictionary of all the data in the object + + pretty simple in this case, but might be more to it + for a more complicated class + """ + return self.__dict__ + + @classmethod + def from_dict(cls, dct): + """ + Returns a new object initialized from the values in dct + + Just calls the usual __init__ in this case. but could be more + to it in a more complex case. + """ + return cls(**dct) + + +class Person(PersistObject): + """ + class to represent an individual person + """ + def __init__(self, + last_name, + first_name='', + middle_name='', + cell_phone='', + email='', + _id=None, + ): + """ + initialize a Person object: + """ + self.first_name = first_name.strip() + self.last_name = last_name.strip() + self.middle_name = middle_name.strip() + self.cell_phone = cell_phone.strip() + self.email = email.strip() + self._id = ObjectId() if _id is None else _id + + @property + def name(self): + return " ".join([self.first_name, self.middle_name, self.last_name]) + + def __str__(self): + msg = '{first_name} {middle_name} {last_name}'.format(**self.__dict__) + return msg + + def __repr__(self): + """ + Not a good ___repr__, but want to have something here + """ + return self.__str__() + + +class Address(PersistObject): + """ + class that represents an address + """ + def __init__(self, + line_1='', + line_2='', + city='', + state='', + zip_code='', +# **kwargs + ): + """ + Initialize an address + """ + + self.line_1 = line_1.strip() + self.line_2 = line_2.strip() + self.city = city.strip() + self.state = state.strip().upper() + self.zip_code = str(zip_code).strip() + + def __str__(self): + msg = "{line_1}\n{line_2}\n{city} {state} {zip_code}\n".format(**self.__dict__) + return msg + + +class Household(PersistObject): + """ + Class that represents a Household. + + A household has one or more people, and a Location + """ + + def __init__(self, + name = '', + people=(), + address=None, + phone='', + **kwargs + ): + + self.name = name.strip() + # if it's already a ObjectID, then don't need to extract it. + try: + self.people = [p._id for p in people] + except AttributeError: + self.people = people + self.address = address + self.phone = phone.strip() + + def to_dict(self): + """ + convert to dict -- stores ids of people + """ + dct = self.__dict__ + dct['address'] = self.address.to_dict() + return dct + + @classmethod + def from_dict(cls, dct): + """ + creates a household object from a dict representation + unpacks the people _ids and address. + """ + dct['address'] = Address(**dct['address']) + return cls(**dct) + + def __str__(self): + msg = [self.name + ":"] + msg += [str(self.address)] + return "\n".join(msg) + + def __repr__(self): + return self.__str__() + + +class Business(Household): + """ + Class that represents a Business + + A business has one or more people, + and address and a phone number + """ + # Same as household now, but you never know. + pass + + +class AddressBook(object): + """ + An address book -- has people, households, businesses. + + All cross-referenced (normalized) + """ + + def __init__(self, + people=(), + businesses=(), + households=(), + fresh=True, + ): + + # create the DB + from pymongo import MongoClient + + client = MongoClient('localhost', 27017) + db = client.address_book + if fresh: + ## clean out old one + db.people.drop() + db.businesses.drop() + db.households.drop() + + # Use the DB to hold the data + self.people = db.people + self.businesses = db.businesses + self.households = db.households + + def add_person(self, person): + self.people.insert(person.to_dict()) + + def add_household(self, household): + self.households.insert(household.to_dict()) + + def add_business(self, business): + self.businesses.insert(business.to_dict()) + + def __str__(self): + msg = ["An Address Book:"] + msg += ["People:"] + msg += [" " + person.name for person in self.find_people()] + msg += ["Households:"] + msg += [" " + house.name for house in self.find_households()] + msg += ["Businesses:"] + msg += [" " + bus.name for bus in self.find_businesses()] + + return "\n".join(msg) + + def find_people(self, name=''): + """ + Find all the people with name in their name somewhere + """ + # fixme -- can this query be combined? + # like this: db.inventory.find( { $or: [ { qty: { $lt: 20 } }, { sale: true } ] } ) + + cursor = self.people.find({"first_name": {'$regex': '.*' + name + '.*', + '$options': 'i'}}) + results = [Person.from_dict(p) for p in cursor] + + cursor = self.people.find({"last_name": {'$regex': '.*' + name + '.*', + '$options': 'i'}}) + + return results + [Person.from_dict(p) for p in cursor] + + def find_households(self): + cursor = self.households.find() + return [Household.from_dict(p) for p in cursor] + + def find_businesses(self): + cursor = self.businesses.find() + return [Business.from_dict(p) for p in cursor] + + def find_zip_codes(self, zip_code): + """ + find all the locations with this zip_code + """ + zip_code = str(zip_code).strip() + cursor = self.households.find({"addresses.zip_code": zip_code}) + results = [Household.from_dict(dct) for dct in cursor] + + cursor = self.businesses.find({"address.zip_code": zip_code}) + results += [Business.from_dict(dct) for dct in cursor] + + return results + + def find_state(self, state): + """ + find all the locations in this state + """ + state = state.strip().upper() + cursor = self.households.find({"address.state": state}) + results = [Household.from_dict(dct) for dct in cursor] + + cursor = self.businesses.find({"address.state": state}) + results += [Business.from_dict(dct) for dct in cursor] + + return results + + +def create_sample(): + """ + Create a sample Address Book + """ + chris = Person(last_name='Barker', + first_name='Chris', + middle_name='H', + cell_phone='(123) 555-7890', + email='PythonCHB@gmail.com', + ) + + emma = Person(last_name='Barker', + first_name='Emma', + middle_name='L', + cell_phone='(345) 555-9012', + email='emma@something.com', + ) + + donna = Person(last_name='Barker', + first_name='Donna', + middle_name='L', + cell_phone='(111) 555-1111', + email='dbarker@something.com', + ) + + barker_address = Address(line_1='123 Some St', + line_2='Apt 1234', + city='Seattle', + state='WA', + zip_code='98000',) + + the_barkers = Household(name="The Barkers", + people=(chris, donna, emma), + address=barker_address) + + joseph = Person(last_name='Sheedy', + first_name='Joseph', + cell_phone='(234) 555-8910', + email='js@some_thing.com', + ) + + cris = Person(last_name='Ewing', + first_name='Cris', + cell_phone='(345) 555-6789', + email='cris@a_fake_domain.com', + ) + + fulvio = Person(last_name='Casali', + first_name='Fulvio', + cell_phone='(345) 555-1234', + email='fulvio@a_fake_domain.com', + ) + + fred = Person(first_name="Fred", + last_name="Jones", + email='FredJones@some_company.com', + cell_phone="403-561-8911", + ) + + python_cert_address = Address('UW Professional and Continuing Education', + line_2='4333 Brooklyn Ave. NE', + city='Seattle', + state='WA', + zip_code='98105', + ) + + python_cert = Business(name='Python Certification Program', + people=(chris, joseph, cris, fulvio), + address=python_cert_address + ) + + + address_book = AddressBook() + + address_book.add_person(chris) + address_book.add_person(donna) + address_book.add_person(emma) + address_book.add_person(cris) + address_book.add_person(joseph) + address_book.add_person(fulvio) + address_book.add_person(fred) + + address_book.add_household(the_barkers) + + address_book.add_business(python_cert) + + return address_book + + +if __name__ == "__main__": + address_book = create_sample() + + print("Here is the address book") + print(address_book) + + diff --git a/_downloads/5cbd2d019dd7b865fe9e576fc8f42d5b/wikidef.zip b/_downloads/5cbd2d019dd7b865fe9e576fc8f42d5b/wikidef.zip new file mode 100644 index 0000000..981fcfe Binary files /dev/null and b/_downloads/5cbd2d019dd7b865fe9e576fc8f42d5b/wikidef.zip differ diff --git a/_downloads/5cdf55a8048eb097483f483519f663d2/calculator_functions.py b/_downloads/5cdf55a8048eb097483f483519f663d2/calculator_functions.py new file mode 100644 index 0000000..532d776 --- /dev/null +++ b/_downloads/5cdf55a8048eb097483f483519f663d2/calculator_functions.py @@ -0,0 +1,22 @@ +"""calculator functions""" + + +def add(x, y): + """ Add two numbers + + >>> add(1, 2) + 3 + >>> add(-7, 2) + -5 + """ + return int(x) + int(y) + + +def subtract(x, y): + return int(x) - int(y) + +def multiply(x, y): + return int(x) * int(y) + +def divide(x, y): + return int(x) / int(y) diff --git a/_downloads/5d01595023d1aac45b0a74309a3fc5fe/text.utf8 b/_downloads/5d01595023d1aac45b0a74309a3fc5fe/text.utf8 new file mode 100644 index 0000000..9de1889 --- /dev/null +++ b/_downloads/5d01595023d1aac45b0a74309a3fc5fe/text.utf8 @@ -0,0 +1,17 @@ +Origin (in native language) Name (in native language) +Հայաստան Արամ Խաչատրյան + Australia Nicole Kidman + Österreich Johann Strauß + Azərbaycan Vaqif Səmədoğlu + Азәрбајҹан Вагиф Сәмәдоғлу + Azərbaycan Heydər Əliyev + Азәрбајҹан Һејдәр Әлијев + België René Magritte + Belgique René Magritte + Belgien René Magritte + বাংলা সুকুমার রায় + འབྲུག་ཡུལ། མགོན་པོ་རྡོ་རྗེ། + ប្រទេស​​​កម្ពុជា ព្រះ​ពុទ្ឋឃោសាចារ‌្យ​ជួន​ណាត +Canada Céline Dion + ᓄᓇᕗᒻᒥᐅᑦ ᓱᓴᓐ ᐊᒡᓗᒃᑲᖅ + \ No newline at end of file diff --git a/_downloads/5e1282999f1e4220e04f86c1639afa54/object_canvas.py b/_downloads/5e1282999f1e4220e04f86c1639afa54/object_canvas.py new file mode 100644 index 0000000..8c28631 --- /dev/null +++ b/_downloads/5e1282999f1e4220e04f86c1639afa54/object_canvas.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python + +""" +object canvas: an example of multiple inheritance and mix-ins + +This is a simplified version of FloatCanvas -- an extension to the +wxPython desktop GUI library + +FloatCanvas is a system for handling zoomable and scalable graphics in +a object-persistant way. That is, graphic objects like circles and +rectangles and what not can be created ahead of time, and then the Canvas +can render them accoding to the current zoom level and pan position, etc. + +This lets the user think about their graphics object should look like, +and not have to worry about exactly how to draw them -- or their pixel +coordinates, or anything else. + +If you want to see all this in all its full complexity, the FloatCanvas +code in part of the wxPython project, and can be seen here: + +https://github.com/wxWidgets/Phoenix/tree/master/wx/lib/floatcanvas + +This code: object_canvas is a simplified version. It doesn't allow scaling +or zooming, and only renders in pixel coordinates. But it does allow +object-persistance, and is a nice demo of the use of mixins. + +This version requires the Python Imaging Library to do the rendering. + +You can get it by installing the "pillow" package from PyPi: + +python -m pip install pillow + +Its docs are here: + +https://pillow.readthedocs.io/en/4.3.x/index.html + +""" +from math import ceil + +from PIL import Image, ImageDraw + + +class ObjectCanvas(): + """ + An object-oriented canvas for drawing things + """ + + def __init__(self, + size=(500, 500), + background=(255, 255, 255, 0) + ): + self.size = size + self.draw_objects = [] + self.background = background + + def add_object(self, draw_object, position="top"): + #fixme: maybe overload the inplace addition operator? + """ + Add a new object to the canvas. + + :param: draw_object -- DrawObject to add + + :param position="top": Position to add the object. "top" puts + the object on top of teh other objects. + "bottom" puts them on the bottom of the stack. + A integer puts it in that place in the order + -- 0 is the bottom. + """ + if position == "top": + self.draw_objects.append(draw_object) + elif position == "bottom": + self.draw_objects.insert(0, draw_object) + else: + self.draw_objects.insert(position, draw_object) + + def render(self, filename): + """ + render the drawing to a file with the given name + """ + image = Image.new('RGBA', self.size, color=self.background) + drawer = ImageDraw.Draw(image) + + for do in self.draw_objects: + do.draw(drawer) + image.save(filename) + + +class DrawObject: + """ + base class for all draw objects + """ + + def __init__(self, *args, **kwargs): + print("in DrawObject __init__", kwargs) + # do nothing, but to make super happy + super().__init__(*args, **kwargs) + + +class LineObject: + """ + mixin for classes with a line + """ + + def __init__(self, + line_color='black', + line_width=1, + **kwargs, + ): + print("in LineObject __init__", kwargs) + super().__init__(**kwargs) + self.line_color = line_color + self.line_width = line_width + + +class FillObject: + """ + mixin for classes with a fill + """ + + def __init__(self, + fill_color=None, + **kwargs + ): + print("in FillObject __init__", kwargs) + self.fill_color = fill_color + + +class PolyLine(DrawObject, LineObject): + def __init__(self, + vertices, + **kwargs + ): + self.vertices = vertices + print("in PolyLine init", kwargs) + super().__init__(**kwargs) + + def draw(self, drawer): + """ + draw the object + + :param drawer: PIL.ImageDraw object to draw to + """ + drawer.line(self.vertices, fill=self.line_color, width=self.line_width) + + +class Circle(DrawObject, LineObject, FillObject): + def __init__(self, center, diameter, **kwargs): + self.center = center + self.diameter = diameter + super().__init__(**kwargs) + + def draw(self, drawer): + """ + Draw the object + :param drawer: PIL.ImageDraw object to draw to + """ + r = self.diameter // 2 + c = self.center + # PIL doesn't support different line widths for ellipses, + # so we fake it. + lw2 = self.line_width / 2 + bounds = ((c[0] - r, c[1] - r), (c[0] + r, c[1] + r)) + drawer.ellipse(bounds, fill=self.fill_color, outline=None) + for i in range(int(ceil(lw2)), int(-lw2), -1): + r = self.diameter // 2 + i + bounds = ((c[0] - r, c[1] - r), (c[0] + r, c[1] + r)) + drawer.ellipse(bounds, fill=None, outline=self.line_color) + + + + + + diff --git a/_downloads/5fcc922e232dba94d872d8bb3693f766/async_executor.py b/_downloads/5fcc922e232dba94d872d8bb3693f766/async_executor.py new file mode 100644 index 0000000..d0d33cb --- /dev/null +++ b/_downloads/5fcc922e232dba94d872d8bb3693f766/async_executor.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python + +""" +An example of runing a blocking task in an Executor: +""" + +import asyncio +import time +import datetime +import random + + +async def small_task(num): + """ + Just something to give us little tasks that run at random intervals + These will go on forever + """ + while True: # keep doing this until break + print("task: {} run".format(num)) + # pause for a random amount of time between 0 and 2 seconds + await asyncio.sleep(random.random() * 2) + +async def slow_task(): + while True: # keep going forever + print("running the slow task- blocking!") + # This will block for 2-10 seconds! + # result = slow_function(random.random() * 8 + 2) + # uncomment to put it on a different thread: + # result = slow_function(random.random() * 8 + 2) + result = await loop.run_in_executor(None, + slow_function, + random.random() * 8 + 2) + print("slow function done: result", result) + # await asyncio.sleep(0.0) # to release the loop + + +def slow_function(duration): + """ + this is a fake function that takes a long time, and blocks + """ + time.sleep(duration) + print("slow task complete") + return duration + + +# get a loop going: +loop = asyncio.get_event_loop() + +# or add tasks to the loop like this: +loop.create_task(small_task(1)) +loop.create_task(small_task(2)) +loop.create_task(small_task(3)) +loop.create_task(small_task(4)) + +# Add the slow one +loop.create_task(slow_task()) + +print("about to run loop") +# this is a blocking call +# we will need to hit ^C to stop it... +loop.run_forever() +print("loop exited") diff --git a/_downloads/635b3b2d18f105d5824bbb9721a88320/test_trigrams.py b/_downloads/635b3b2d18f105d5824bbb9721a88320/test_trigrams.py new file mode 100644 index 0000000..200bce1 --- /dev/null +++ b/_downloads/635b3b2d18f105d5824bbb9721a88320/test_trigrams.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python + +""" +test code for the "trigrams" exercise + +NOTE: This is NOT a complete set of tests! You will want to add more of + your own to get complete coverage. + +As you develop your code, if you are about to add a feature, or think +of an edge case: + + Write a new test for that first! + +Also note that you may want to do some different things with processing +the text -- if so, then update the tests (and add more) to reflect how you want your code to work. +""" + +# this is expecting a "trigrams.py" file with your code in it. + +import random +import trigrams + +IWISH = "I wish I may I wish I might".split() + +LONGER_TEXT = """I was seized with a keen desire to see Holmes +again and to know how he was employing his extraordinary powers +His rooms were brilliantly lit and even as I looked up I saw +his tall spare figure pass twice in a dark silhouette against +the blind""".split() + + +def test_trigrams_pairs(): + """ + test that the build_trigram function creates the right pairs of words + """ + tris = trigrams.build_trigram(IWISH) + + pairs = tris.keys() + + # using a set here, as the dict_keys object is a set as well + # And keys are always unique and hashable + # and the order does not matter, so perfect for a set + assert pairs == {("I", "wish"), + ("wish", "I"), + ("may", "I"), + ("I", "may"), + } + + +def test_trigrams_following_words(): + """ + test that the following words are correct + """ + tris = trigrams.build_trigram(IWISH) + + # this will only print if the test fails + # but if if does, you can see what's going on to try to fix it. + print(tris) + + # a separate assert for each pair: + assert tris[("I", "wish")] == ["I", "I"] + assert tris[("wish", "I")] == ["may", "might"] + assert tris[("may", "I")] == ["wish"] + assert tris[("I", "may")] == ["I"] + + +def test_pick_random_pair(): + test_pairs = {("one", "two"): [], + ("two", "three"): [], + ("four", "five"): [], + ("six", "seven"): [], + ("eight", "nine"): [], + } + # set the seed so we'll always get the same one + random.seed(1234) + pair = trigrams.pick_random_pair(test_pairs) + print("the pair is:", pair) + assert pair == ('six', 'seven') + + +def test_get_last_pair(): + words = ["this", "that", "the", "other"] + + assert trigrams.get_last_pair(words) == ("the", "other") + + +def test_get_random_follower(): + """ + test getting a random word from the trigrams dict + """ + # we only need one entry for this test + tri_dict = {("one", "two"): ["four", "five", "six", "seven"]} + + # set the seed so the answer will be consistent + random.seed(1234) + word = trigrams.get_random_follower(tri_dict, ("one", "two")) + print("got word:", word) + assert word == "seven" + + +def test_get_random_follower_not_there(): + """ + test what happens when the word pair is not there + """ + # we only need one entry for this test + tri_dict = {("one", "two"): ["four", "five", "six", "seven"]} + + # here's a word pair that isn't there + # make sure you get something back! + word = trigrams.get_random_follower(tri_dict, ("one", "one")) + print("got word:", word) + assert word # this asserts that you got a non-empty string + + +def test_make_sentence(): + """ + test making a trigrams sentence + + as it is supposed to be random, this tests for things other than + the actual results. + + Which means that it does NOT test everything! This test could pass + with a very broken make_sentence function. So you should probably + add a few more things to this test. + + NOTE that this test relies on the build_trigram() function, so it + will fail if that doesn't work. + """ + + # reset the seed, sop that we won't always get the same answer + random.seed() + + # use the already tested build_trigram function to make the dict + tri_dict = trigrams.build_trigram(LONGER_TEXT) + + + # make a sentence of 6 words + sentence = trigrams.make_sentence(tri_dict, 6) + + print(sentence) + # check that it has 6 words + assert len(sentence.split()) == 6 + # check that the first letter is a capital + assert sentence[0] == sentence[0].upper() + # check that it ends with a period + assert sentence[-1] == "." + # check that there is not a space between the period and the last word. + assert not sentence[-2].isspace() + +# the following tests are for the "make_words" function, +# which takes a text string, and returns a list of words +# It also cleans up the punctuation, while preserving things +# like apostrophes, and capitalized "I". +# you may choose to handle punctuation differently +# feel free to adapt the tests to your choices + + +# TEXT_WITH_PUNC = """ +# One night--it was on the twentieth of March, 1888--I was +# returning from a journey to a patient (for I had now returned to +# civil practice), when my way led me through Baker Street. As I +# passed the well-remembered door, which must always be associated +# in my mind with my wooing, and with the dark incidents of the +# Study in Scarlet, I was seized with a keen desire to see Holmes +# again, and to know how he was employing his extraordinary powers. +# His rooms were brilliantly lit, and, even as I looked up, I saw +# his tall, spare figure pass twice in a dark silhouette against +# the blind. +# """ + + +# def test_make_words_simple(): +# """ +# make sure the basics work! +# """ +# all_words = trigrams.make_words("A really simple sentence.") + +# assert len(all_words) == 4 + + +# def test_make_words_commas(): +# """ +# all commas should be removed +# """ +# # put them all back together for easier checking +# all_words = " ".join(trigrams.make_words(TEXT_WITH_PUNC)) + +# assert "," not in all_words + + +# def test_make_words_parentheses(): +# """ +# all parenthesis should be removed +# """ +# # put them all back together for easier checking +# all_words = " ".join(trigrams.make_words(TEXT_WITH_PUNC)) + +# assert "(" not in all_words +# assert ")" not in all_words + + +# def test_make_words_dashes(): +# """ +# all dashes should be removed +# """ +# # put them all back together for easier checking +# all_words = " ".join(trigrams.make_words(TEXT_WITH_PUNC)) + +# assert "-" not in all_words + + +# def test_make_words_I(): +# """ +# I should be capitalized +# """ +# all_words = trigrams.make_words(TEXT_WITH_PUNC) + +# assert "i" not in all_words +# assert "I" in all_words + + +# def test_make_words_single_quote(): +# """ +# no double quotes +# no single quotes by themselves, but preserved when an apostrophe +# """ +# # put them all back together for easier checking +# text = """ +# "Not at all. The 'G' with the small 't' stands for +# 'Gesellschaft,' which isn't the German for 'Company.' +# """ +# all_words = trigrams.make_words(text) + +# print(all_words) +# # no double quotes +# assert '"' not in " ".join(all_words) +# # apostophe preserved +# assert "isn't" in all_words + +# # none of the words should start or end with a single quote +# for word in all_words: +# assert not word.startswith("'") +# assert not word.endswith("'") + +# assert False + + diff --git a/_downloads/642914b2ed03ec925362d45ac03ce421/nba_stats_async.py b/_downloads/642914b2ed03ec925362d45ac03ce421/nba_stats_async.py new file mode 100644 index 0000000..78aacf8 --- /dev/null +++ b/_downloads/642914b2ed03ec925362d45ac03ce421/nba_stats_async.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python + +""" +Gathering statistics on NBA players asynchronously with the + +aiohttp library + +Running this on my machine, on my home network, took: + + +***NOTE*** +On my OS-X box, a regular user is limited to 256 open files per process. +A socket is considered a file -- so this can crash out when it hits that limit. + +(as of now, there are 491 players listed) + +You can increase it with: + +ulimit -n 2048 + +And see what it's set to with: + +ulimit -a +*********** + +Borrowed from: + +http://terriblecode.com/blog/asynchronous-http-requests-in-python/ +""" +import pdb +import asyncio +import aiohttp +import json +import time +import requests + +base_url = 'http://stats.nba.com/stats' + +HEADERS = { + 'user-agent': ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/45.0.2454.101 Safari/537.36'), +} + +# # this needs to be run first before we can start -- so no need for async +# # but making it async so we can use the aiohttp lib. +# async def get_players(players): +# """ +# get the names of all the players we are interested in + +# This request will get JSON of the players for the 2016-17 season: + +# http://stats.nba.com/stats/commonallplayers?LeagueID=00&season=2016-17&isonlycurrentseason=1 +# """ +# endpoint = '/commonallplayers' +# params = {'leagueid': '00', 'season': '2016-17', 'isonlycurrentseason': '1'} +# url = base_url + endpoint +# print('Getting all players...') +# async with aiohttp.ClientSession() as session: +# print("got the session") +# async with session.get(url, headers=HEADERS, params=params) as resp: +# print("got the response") +# data = await resp.json() +# players.append([(item[0], item[2]) for item in data['resultSets'][0]['rowSet']]) + + +def get_players(player_args): + """ + get the names of all the players we are interested in + + This request will get JSON of the players for the 2016-17 season: + + http://stats.nba.com/stats/commonallplayers?LeagueID=00&season=2016-17&isonlycurrentseason=1 + + """ + endpoint = '/commonallplayers' + params = {'leagueid': '00', 'season': '2016-17', 'isonlycurrentseason': '1'} + url = base_url + endpoint + print('Getting all players...') + print("about to make request") + resp = requests.get(url, headers=HEADERS, params=params) + print("got the response") + data = resp.json() + player_args.extend( + [(item[0], item[2]) for item in data['resultSets'][0]['rowSet']]) + + +# this is what we want to make concurrent +async def get_player(player_id, player_name): + endpoint = '/commonplayerinfo' + params = {'playerid': player_id} + url = base_url + endpoint + print("Getting player", player_name) + async with aiohttp.ClientSession() as session: + print("session created") + async with session.get(url, + skip_auto_headers=["User-Agent"], + headers=HEADERS, + params=params) as resp: + print("response:", resp) + all_players[player_name] = await resp.json() + print("got:", player_name) + print("Done with get_player:", player_name) + +# async def get_all_stats(players): +# for id, name in players: +# print("getting:", name) +# all_players[name] = await get_player(id, name) + +all_players = {} +players = [] + +start = time.time() +loop = asyncio.get_event_loop() + +print("getting the players") +# loop.run_until_complete(get_players(players)) + +get_players(players) +print("got the players") +print("there are {} players".format(len(players))) + +# print("getting the stats") +# loop.run_until_complete(get_all_stats(players[:200])) +# print("got the stats") + +loop.run_until_complete(asyncio.gather( + *(get_player(*args) for args in players[:10]) + ) + ) + +# loop.run_until_complete(get_player(*players[0])) + +# for id, name in players: +# all_players[name] = get_player(id, name) + +print("Done getting data: it took {:.2F} seconds".format(time.time() - start)) + +# write it out to a file +with open("NBA_stats_2.json", 'w') as outfile: + json.dump(all_players, outfile, indent=2) + +print("File written out") diff --git a/_downloads/6671576baac0eca6174afd1270877a72/singleton.py b/_downloads/6671576baac0eca6174afd1270877a72/singleton.py new file mode 100644 index 0000000..890082b --- /dev/null +++ b/_downloads/6671576baac0eca6174afd1270877a72/singleton.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +""" +example of using __metaclass__ to impliment the singleton pattern +""" + + +class Singleton(type): + instance = None + + def __call__(cls, *args, **kwargs): + if cls.instance is None: + cls.instance = super().__call__(*args, **kwargs) + return cls.instance + + +class MyClass(metaclass=Singleton): + pass + +object1 = MyClass() +object2 = MyClass() + +print(id(object1)) +print(id(object2)) diff --git a/_downloads/66a363e2c87413f5419e3e039a731e36/sherlock.txt b/_downloads/66a363e2c87413f5419e3e039a731e36/sherlock.txt new file mode 100644 index 0000000..4dec201 --- /dev/null +++ b/_downloads/66a363e2c87413f5419e3e039a731e36/sherlock.txt @@ -0,0 +1,13052 @@ +Project Gutenberg's The Adventures of Sherlock Holmes, by Arthur Conan Doyle + +This eBook is for the use of anyone anywhere at no cost and with +almost no restrictions whatsoever. You may copy it, give it away or +re-use it under the terms of the Project Gutenberg License included +with this eBook or online at www.gutenberg.net + + +Title: The Adventures of Sherlock Holmes + +Author: Arthur Conan Doyle + +Posting Date: April 18, 2011 [EBook #1661] +First Posted: November 29, 2002 + +Language: English + + +*** START OF THIS PROJECT GUTENBERG EBOOK THE ADVENTURES OF SHERLOCK HOLMES *** + + + + +Produced by an anonymous Project Gutenberg volunteer and Jose Menendez + + + + + + + + + +THE ADVENTURES OF SHERLOCK HOLMES + +by + +SIR ARTHUR CONAN DOYLE + + + + I. A Scandal in Bohemia + II. The Red-headed League + III. A Case of Identity + IV. The Boscombe Valley Mystery + V. The Five Orange Pips + VI. The Man with the Twisted Lip + VII. The Adventure of the Blue Carbuncle +VIII. The Adventure of the Speckled Band + IX. The Adventure of the Engineer's Thumb + X. The Adventure of the Noble Bachelor + XI. The Adventure of the Beryl Coronet + XII. The Adventure of the Copper Beeches + + + + +ADVENTURE I. A SCANDAL IN BOHEMIA + +I. + +To Sherlock Holmes she is always THE woman. I have seldom heard +him mention her under any other name. In his eyes she eclipses +and predominates the whole of her sex. It was not that he felt +any emotion akin to love for Irene Adler. All emotions, and that +one particularly, were abhorrent to his cold, precise but +admirably balanced mind. He was, I take it, the most perfect +reasoning and observing machine that the world has seen, but as a +lover he would have placed himself in a false position. He never +spoke of the softer passions, save with a gibe and a sneer. They +were admirable things for the observer--excellent for drawing the +veil from men's motives and actions. But for the trained reasoner +to admit such intrusions into his own delicate and finely +adjusted temperament was to introduce a distracting factor which +might throw a doubt upon all his mental results. Grit in a +sensitive instrument, or a crack in one of his own high-power +lenses, would not be more disturbing than a strong emotion in a +nature such as his. And yet there was but one woman to him, and +that woman was the late Irene Adler, of dubious and questionable +memory. + +I had seen little of Holmes lately. My marriage had drifted us +away from each other. My own complete happiness, and the +home-centred interests which rise up around the man who first +finds himself master of his own establishment, were sufficient to +absorb all my attention, while Holmes, who loathed every form of +society with his whole Bohemian soul, remained in our lodgings in +Baker Street, buried among his old books, and alternating from +week to week between cocaine and ambition, the drowsiness of the +drug, and the fierce energy of his own keen nature. He was still, +as ever, deeply attracted by the study of crime, and occupied his +immense faculties and extraordinary powers of observation in +following out those clues, and clearing up those mysteries which +had been abandoned as hopeless by the official police. From time +to time I heard some vague account of his doings: of his summons +to Odessa in the case of the Trepoff murder, of his clearing up +of the singular tragedy of the Atkinson brothers at Trincomalee, +and finally of the mission which he had accomplished so +delicately and successfully for the reigning family of Holland. +Beyond these signs of his activity, however, which I merely +shared with all the readers of the daily press, I knew little of +my former friend and companion. + +One night--it was on the twentieth of March, 1888--I was +returning from a journey to a patient (for I had now returned to +civil practice), when my way led me through Baker Street. As I +passed the well-remembered door, which must always be associated +in my mind with my wooing, and with the dark incidents of the +Study in Scarlet, I was seized with a keen desire to see Holmes +again, and to know how he was employing his extraordinary powers. +His rooms were brilliantly lit, and, even as I looked up, I saw +his tall, spare figure pass twice in a dark silhouette against +the blind. He was pacing the room swiftly, eagerly, with his head +sunk upon his chest and his hands clasped behind him. To me, who +knew his every mood and habit, his attitude and manner told their +own story. He was at work again. He had risen out of his +drug-created dreams and was hot upon the scent of some new +problem. I rang the bell and was shown up to the chamber which +had formerly been in part my own. + +His manner was not effusive. It seldom was; but he was glad, I +think, to see me. With hardly a word spoken, but with a kindly +eye, he waved me to an armchair, threw across his case of cigars, +and indicated a spirit case and a gasogene in the corner. Then he +stood before the fire and looked me over in his singular +introspective fashion. + +"Wedlock suits you," he remarked. "I think, Watson, that you have +put on seven and a half pounds since I saw you." + +"Seven!" I answered. + +"Indeed, I should have thought a little more. Just a trifle more, +I fancy, Watson. And in practice again, I observe. You did not +tell me that you intended to go into harness." + +"Then, how do you know?" + +"I see it, I deduce it. How do I know that you have been getting +yourself very wet lately, and that you have a most clumsy and +careless servant girl?" + +"My dear Holmes," said I, "this is too much. You would certainly +have been burned, had you lived a few centuries ago. It is true +that I had a country walk on Thursday and came home in a dreadful +mess, but as I have changed my clothes I can't imagine how you +deduce it. As to Mary Jane, she is incorrigible, and my wife has +given her notice, but there, again, I fail to see how you work it +out." + +He chuckled to himself and rubbed his long, nervous hands +together. + +"It is simplicity itself," said he; "my eyes tell me that on the +inside of your left shoe, just where the firelight strikes it, +the leather is scored by six almost parallel cuts. Obviously they +have been caused by someone who has very carelessly scraped round +the edges of the sole in order to remove crusted mud from it. +Hence, you see, my double deduction that you had been out in vile +weather, and that you had a particularly malignant boot-slitting +specimen of the London slavey. As to your practice, if a +gentleman walks into my rooms smelling of iodoform, with a black +mark of nitrate of silver upon his right forefinger, and a bulge +on the right side of his top-hat to show where he has secreted +his stethoscope, I must be dull, indeed, if I do not pronounce +him to be an active member of the medical profession." + +I could not help laughing at the ease with which he explained his +process of deduction. "When I hear you give your reasons," I +remarked, "the thing always appears to me to be so ridiculously +simple that I could easily do it myself, though at each +successive instance of your reasoning I am baffled until you +explain your process. And yet I believe that my eyes are as good +as yours." + +"Quite so," he answered, lighting a cigarette, and throwing +himself down into an armchair. "You see, but you do not observe. +The distinction is clear. For example, you have frequently seen +the steps which lead up from the hall to this room." + +"Frequently." + +"How often?" + +"Well, some hundreds of times." + +"Then how many are there?" + +"How many? I don't know." + +"Quite so! You have not observed. And yet you have seen. That is +just my point. Now, I know that there are seventeen steps, +because I have both seen and observed. By-the-way, since you are +interested in these little problems, and since you are good +enough to chronicle one or two of my trifling experiences, you +may be interested in this." He threw over a sheet of thick, +pink-tinted note-paper which had been lying open upon the table. +"It came by the last post," said he. "Read it aloud." + +The note was undated, and without either signature or address. + +"There will call upon you to-night, at a quarter to eight +o'clock," it said, "a gentleman who desires to consult you upon a +matter of the very deepest moment. Your recent services to one of +the royal houses of Europe have shown that you are one who may +safely be trusted with matters which are of an importance which +can hardly be exaggerated. This account of you we have from all +quarters received. Be in your chamber then at that hour, and do +not take it amiss if your visitor wear a mask." + +"This is indeed a mystery," I remarked. "What do you imagine that +it means?" + +"I have no data yet. It is a capital mistake to theorize before +one has data. Insensibly one begins to twist facts to suit +theories, instead of theories to suit facts. But the note itself. +What do you deduce from it?" + +I carefully examined the writing, and the paper upon which it was +written. + +"The man who wrote it was presumably well to do," I remarked, +endeavouring to imitate my companion's processes. "Such paper +could not be bought under half a crown a packet. It is peculiarly +strong and stiff." + +"Peculiar--that is the very word," said Holmes. "It is not an +English paper at all. Hold it up to the light." + +I did so, and saw a large "E" with a small "g," a "P," and a +large "G" with a small "t" woven into the texture of the paper. + +"What do you make of that?" asked Holmes. + +"The name of the maker, no doubt; or his monogram, rather." + +"Not at all. The 'G' with the small 't' stands for +'Gesellschaft,' which is the German for 'Company.' It is a +customary contraction like our 'Co.' 'P,' of course, stands for +'Papier.' Now for the 'Eg.' Let us glance at our Continental +Gazetteer." He took down a heavy brown volume from his shelves. +"Eglow, Eglonitz--here we are, Egria. It is in a German-speaking +country--in Bohemia, not far from Carlsbad. 'Remarkable as being +the scene of the death of Wallenstein, and for its numerous +glass-factories and paper-mills.' Ha, ha, my boy, what do you +make of that?" His eyes sparkled, and he sent up a great blue +triumphant cloud from his cigarette. + +"The paper was made in Bohemia," I said. + +"Precisely. And the man who wrote the note is a German. Do you +note the peculiar construction of the sentence--'This account of +you we have from all quarters received.' A Frenchman or Russian +could not have written that. It is the German who is so +uncourteous to his verbs. It only remains, therefore, to discover +what is wanted by this German who writes upon Bohemian paper and +prefers wearing a mask to showing his face. And here he comes, if +I am not mistaken, to resolve all our doubts." + +As he spoke there was the sharp sound of horses' hoofs and +grating wheels against the curb, followed by a sharp pull at the +bell. Holmes whistled. + +"A pair, by the sound," said he. "Yes," he continued, glancing +out of the window. "A nice little brougham and a pair of +beauties. A hundred and fifty guineas apiece. There's money in +this case, Watson, if there is nothing else." + +"I think that I had better go, Holmes." + +"Not a bit, Doctor. Stay where you are. I am lost without my +Boswell. And this promises to be interesting. It would be a pity +to miss it." + +"But your client--" + +"Never mind him. I may want your help, and so may he. Here he +comes. Sit down in that armchair, Doctor, and give us your best +attention." + +A slow and heavy step, which had been heard upon the stairs and +in the passage, paused immediately outside the door. Then there +was a loud and authoritative tap. + +"Come in!" said Holmes. + +A man entered who could hardly have been less than six feet six +inches in height, with the chest and limbs of a Hercules. His +dress was rich with a richness which would, in England, be looked +upon as akin to bad taste. Heavy bands of astrakhan were slashed +across the sleeves and fronts of his double-breasted coat, while +the deep blue cloak which was thrown over his shoulders was lined +with flame-coloured silk and secured at the neck with a brooch +which consisted of a single flaming beryl. Boots which extended +halfway up his calves, and which were trimmed at the tops with +rich brown fur, completed the impression of barbaric opulence +which was suggested by his whole appearance. He carried a +broad-brimmed hat in his hand, while he wore across the upper +part of his face, extending down past the cheekbones, a black +vizard mask, which he had apparently adjusted that very moment, +for his hand was still raised to it as he entered. From the lower +part of the face he appeared to be a man of strong character, +with a thick, hanging lip, and a long, straight chin suggestive +of resolution pushed to the length of obstinacy. + +"You had my note?" he asked with a deep harsh voice and a +strongly marked German accent. "I told you that I would call." He +looked from one to the other of us, as if uncertain which to +address. + +"Pray take a seat," said Holmes. "This is my friend and +colleague, Dr. Watson, who is occasionally good enough to help me +in my cases. Whom have I the honour to address?" + +"You may address me as the Count Von Kramm, a Bohemian nobleman. +I understand that this gentleman, your friend, is a man of honour +and discretion, whom I may trust with a matter of the most +extreme importance. If not, I should much prefer to communicate +with you alone." + +I rose to go, but Holmes caught me by the wrist and pushed me +back into my chair. "It is both, or none," said he. "You may say +before this gentleman anything which you may say to me." + +The Count shrugged his broad shoulders. "Then I must begin," said +he, "by binding you both to absolute secrecy for two years; at +the end of that time the matter will be of no importance. At +present it is not too much to say that it is of such weight it +may have an influence upon European history." + +"I promise," said Holmes. + +"And I." + +"You will excuse this mask," continued our strange visitor. "The +august person who employs me wishes his agent to be unknown to +you, and I may confess at once that the title by which I have +just called myself is not exactly my own." + +"I was aware of it," said Holmes dryly. + +"The circumstances are of great delicacy, and every precaution +has to be taken to quench what might grow to be an immense +scandal and seriously compromise one of the reigning families of +Europe. To speak plainly, the matter implicates the great House +of Ormstein, hereditary kings of Bohemia." + +"I was also aware of that," murmured Holmes, settling himself +down in his armchair and closing his eyes. + +Our visitor glanced with some apparent surprise at the languid, +lounging figure of the man who had been no doubt depicted to him +as the most incisive reasoner and most energetic agent in Europe. +Holmes slowly reopened his eyes and looked impatiently at his +gigantic client. + +"If your Majesty would condescend to state your case," he +remarked, "I should be better able to advise you." + +The man sprang from his chair and paced up and down the room in +uncontrollable agitation. Then, with a gesture of desperation, he +tore the mask from his face and hurled it upon the ground. "You +are right," he cried; "I am the King. Why should I attempt to +conceal it?" + +"Why, indeed?" murmured Holmes. "Your Majesty had not spoken +before I was aware that I was addressing Wilhelm Gottsreich +Sigismond von Ormstein, Grand Duke of Cassel-Felstein, and +hereditary King of Bohemia." + +"But you can understand," said our strange visitor, sitting down +once more and passing his hand over his high white forehead, "you +can understand that I am not accustomed to doing such business in +my own person. Yet the matter was so delicate that I could not +confide it to an agent without putting myself in his power. I +have come incognito from Prague for the purpose of consulting +you." + +"Then, pray consult," said Holmes, shutting his eyes once more. + +"The facts are briefly these: Some five years ago, during a +lengthy visit to Warsaw, I made the acquaintance of the well-known +adventuress, Irene Adler. The name is no doubt familiar to you." + +"Kindly look her up in my index, Doctor," murmured Holmes without +opening his eyes. For many years he had adopted a system of +docketing all paragraphs concerning men and things, so that it +was difficult to name a subject or a person on which he could not +at once furnish information. In this case I found her biography +sandwiched in between that of a Hebrew rabbi and that of a +staff-commander who had written a monograph upon the deep-sea +fishes. + +"Let me see!" said Holmes. "Hum! Born in New Jersey in the year +1858. Contralto--hum! La Scala, hum! Prima donna Imperial Opera +of Warsaw--yes! Retired from operatic stage--ha! Living in +London--quite so! Your Majesty, as I understand, became entangled +with this young person, wrote her some compromising letters, and +is now desirous of getting those letters back." + +"Precisely so. But how--" + +"Was there a secret marriage?" + +"None." + +"No legal papers or certificates?" + +"None." + +"Then I fail to follow your Majesty. If this young person should +produce her letters for blackmailing or other purposes, how is +she to prove their authenticity?" + +"There is the writing." + +"Pooh, pooh! Forgery." + +"My private note-paper." + +"Stolen." + +"My own seal." + +"Imitated." + +"My photograph." + +"Bought." + +"We were both in the photograph." + +"Oh, dear! That is very bad! Your Majesty has indeed committed an +indiscretion." + +"I was mad--insane." + +"You have compromised yourself seriously." + +"I was only Crown Prince then. I was young. I am but thirty now." + +"It must be recovered." + +"We have tried and failed." + +"Your Majesty must pay. It must be bought." + +"She will not sell." + +"Stolen, then." + +"Five attempts have been made. Twice burglars in my pay ransacked +her house. Once we diverted her luggage when she travelled. Twice +she has been waylaid. There has been no result." + +"No sign of it?" + +"Absolutely none." + +Holmes laughed. "It is quite a pretty little problem," said he. + +"But a very serious one to me," returned the King reproachfully. + +"Very, indeed. And what does she propose to do with the +photograph?" + +"To ruin me." + +"But how?" + +"I am about to be married." + +"So I have heard." + +"To Clotilde Lothman von Saxe-Meningen, second daughter of the +King of Scandinavia. You may know the strict principles of her +family. She is herself the very soul of delicacy. A shadow of a +doubt as to my conduct would bring the matter to an end." + +"And Irene Adler?" + +"Threatens to send them the photograph. And she will do it. I +know that she will do it. You do not know her, but she has a soul +of steel. She has the face of the most beautiful of women, and +the mind of the most resolute of men. Rather than I should marry +another woman, there are no lengths to which she would not +go--none." + +"You are sure that she has not sent it yet?" + +"I am sure." + +"And why?" + +"Because she has said that she would send it on the day when the +betrothal was publicly proclaimed. That will be next Monday." + +"Oh, then we have three days yet," said Holmes with a yawn. "That +is very fortunate, as I have one or two matters of importance to +look into just at present. Your Majesty will, of course, stay in +London for the present?" + +"Certainly. You will find me at the Langham under the name of the +Count Von Kramm." + +"Then I shall drop you a line to let you know how we progress." + +"Pray do so. I shall be all anxiety." + +"Then, as to money?" + +"You have carte blanche." + +"Absolutely?" + +"I tell you that I would give one of the provinces of my kingdom +to have that photograph." + +"And for present expenses?" + +The King took a heavy chamois leather bag from under his cloak +and laid it on the table. + +"There are three hundred pounds in gold and seven hundred in +notes," he said. + +Holmes scribbled a receipt upon a sheet of his note-book and +handed it to him. + +"And Mademoiselle's address?" he asked. + +"Is Briony Lodge, Serpentine Avenue, St. John's Wood." + +Holmes took a note of it. "One other question," said he. "Was the +photograph a cabinet?" + +"It was." + +"Then, good-night, your Majesty, and I trust that we shall soon +have some good news for you. And good-night, Watson," he added, +as the wheels of the royal brougham rolled down the street. "If +you will be good enough to call to-morrow afternoon at three +o'clock I should like to chat this little matter over with you." + + +II. + +At three o'clock precisely I was at Baker Street, but Holmes had +not yet returned. The landlady informed me that he had left the +house shortly after eight o'clock in the morning. I sat down +beside the fire, however, with the intention of awaiting him, +however long he might be. I was already deeply interested in his +inquiry, for, though it was surrounded by none of the grim and +strange features which were associated with the two crimes which +I have already recorded, still, the nature of the case and the +exalted station of his client gave it a character of its own. +Indeed, apart from the nature of the investigation which my +friend had on hand, there was something in his masterly grasp of +a situation, and his keen, incisive reasoning, which made it a +pleasure to me to study his system of work, and to follow the +quick, subtle methods by which he disentangled the most +inextricable mysteries. So accustomed was I to his invariable +success that the very possibility of his failing had ceased to +enter into my head. + +It was close upon four before the door opened, and a +drunken-looking groom, ill-kempt and side-whiskered, with an +inflamed face and disreputable clothes, walked into the room. +Accustomed as I was to my friend's amazing powers in the use of +disguises, I had to look three times before I was certain that it +was indeed he. With a nod he vanished into the bedroom, whence he +emerged in five minutes tweed-suited and respectable, as of old. +Putting his hands into his pockets, he stretched out his legs in +front of the fire and laughed heartily for some minutes. + +"Well, really!" he cried, and then he choked and laughed again +until he was obliged to lie back, limp and helpless, in the +chair. + +"What is it?" + +"It's quite too funny. I am sure you could never guess how I +employed my morning, or what I ended by doing." + +"I can't imagine. I suppose that you have been watching the +habits, and perhaps the house, of Miss Irene Adler." + +"Quite so; but the sequel was rather unusual. I will tell you, +however. I left the house a little after eight o'clock this +morning in the character of a groom out of work. There is a +wonderful sympathy and freemasonry among horsey men. Be one of +them, and you will know all that there is to know. I soon found +Briony Lodge. It is a bijou villa, with a garden at the back, but +built out in front right up to the road, two stories. Chubb lock +to the door. Large sitting-room on the right side, well +furnished, with long windows almost to the floor, and those +preposterous English window fasteners which a child could open. +Behind there was nothing remarkable, save that the passage window +could be reached from the top of the coach-house. I walked round +it and examined it closely from every point of view, but without +noting anything else of interest. + +"I then lounged down the street and found, as I expected, that +there was a mews in a lane which runs down by one wall of the +garden. I lent the ostlers a hand in rubbing down their horses, +and received in exchange twopence, a glass of half and half, two +fills of shag tobacco, and as much information as I could desire +about Miss Adler, to say nothing of half a dozen other people in +the neighbourhood in whom I was not in the least interested, but +whose biographies I was compelled to listen to." + +"And what of Irene Adler?" I asked. + +"Oh, she has turned all the men's heads down in that part. She is +the daintiest thing under a bonnet on this planet. So say the +Serpentine-mews, to a man. She lives quietly, sings at concerts, +drives out at five every day, and returns at seven sharp for +dinner. Seldom goes out at other times, except when she sings. +Has only one male visitor, but a good deal of him. He is dark, +handsome, and dashing, never calls less than once a day, and +often twice. He is a Mr. Godfrey Norton, of the Inner Temple. See +the advantages of a cabman as a confidant. They had driven him +home a dozen times from Serpentine-mews, and knew all about him. +When I had listened to all they had to tell, I began to walk up +and down near Briony Lodge once more, and to think over my plan +of campaign. + +"This Godfrey Norton was evidently an important factor in the +matter. He was a lawyer. That sounded ominous. What was the +relation between them, and what the object of his repeated +visits? Was she his client, his friend, or his mistress? If the +former, she had probably transferred the photograph to his +keeping. If the latter, it was less likely. On the issue of this +question depended whether I should continue my work at Briony +Lodge, or turn my attention to the gentleman's chambers in the +Temple. It was a delicate point, and it widened the field of my +inquiry. I fear that I bore you with these details, but I have to +let you see my little difficulties, if you are to understand the +situation." + +"I am following you closely," I answered. + +"I was still balancing the matter in my mind when a hansom cab +drove up to Briony Lodge, and a gentleman sprang out. He was a +remarkably handsome man, dark, aquiline, and moustached--evidently +the man of whom I had heard. He appeared to be in a +great hurry, shouted to the cabman to wait, and brushed past the +maid who opened the door with the air of a man who was thoroughly +at home. + +"He was in the house about half an hour, and I could catch +glimpses of him in the windows of the sitting-room, pacing up and +down, talking excitedly, and waving his arms. Of her I could see +nothing. Presently he emerged, looking even more flurried than +before. As he stepped up to the cab, he pulled a gold watch from +his pocket and looked at it earnestly, 'Drive like the devil,' he +shouted, 'first to Gross & Hankey's in Regent Street, and then to +the Church of St. Monica in the Edgeware Road. Half a guinea if +you do it in twenty minutes!' + +"Away they went, and I was just wondering whether I should not do +well to follow them when up the lane came a neat little landau, +the coachman with his coat only half-buttoned, and his tie under +his ear, while all the tags of his harness were sticking out of +the buckles. It hadn't pulled up before she shot out of the hall +door and into it. I only caught a glimpse of her at the moment, +but she was a lovely woman, with a face that a man might die for. + +"'The Church of St. Monica, John,' she cried, 'and half a +sovereign if you reach it in twenty minutes.' + +"This was quite too good to lose, Watson. I was just balancing +whether I should run for it, or whether I should perch behind her +landau when a cab came through the street. The driver looked +twice at such a shabby fare, but I jumped in before he could +object. 'The Church of St. Monica,' said I, 'and half a sovereign +if you reach it in twenty minutes.' It was twenty-five minutes to +twelve, and of course it was clear enough what was in the wind. + +"My cabby drove fast. I don't think I ever drove faster, but the +others were there before us. The cab and the landau with their +steaming horses were in front of the door when I arrived. I paid +the man and hurried into the church. There was not a soul there +save the two whom I had followed and a surpliced clergyman, who +seemed to be expostulating with them. They were all three +standing in a knot in front of the altar. I lounged up the side +aisle like any other idler who has dropped into a church. +Suddenly, to my surprise, the three at the altar faced round to +me, and Godfrey Norton came running as hard as he could towards +me. + +"'Thank God,' he cried. 'You'll do. Come! Come!' + +"'What then?' I asked. + +"'Come, man, come, only three minutes, or it won't be legal.' + +"I was half-dragged up to the altar, and before I knew where I was +I found myself mumbling responses which were whispered in my ear, +and vouching for things of which I knew nothing, and generally +assisting in the secure tying up of Irene Adler, spinster, to +Godfrey Norton, bachelor. It was all done in an instant, and +there was the gentleman thanking me on the one side and the lady +on the other, while the clergyman beamed on me in front. It was +the most preposterous position in which I ever found myself in my +life, and it was the thought of it that started me laughing just +now. It seems that there had been some informality about their +license, that the clergyman absolutely refused to marry them +without a witness of some sort, and that my lucky appearance +saved the bridegroom from having to sally out into the streets in +search of a best man. The bride gave me a sovereign, and I mean +to wear it on my watch-chain in memory of the occasion." + +"This is a very unexpected turn of affairs," said I; "and what +then?" + +"Well, I found my plans very seriously menaced. It looked as if +the pair might take an immediate departure, and so necessitate +very prompt and energetic measures on my part. At the church +door, however, they separated, he driving back to the Temple, and +she to her own house. 'I shall drive out in the park at five as +usual,' she said as she left him. I heard no more. They drove +away in different directions, and I went off to make my own +arrangements." + +"Which are?" + +"Some cold beef and a glass of beer," he answered, ringing the +bell. "I have been too busy to think of food, and I am likely to +be busier still this evening. By the way, Doctor, I shall want +your co-operation." + +"I shall be delighted." + +"You don't mind breaking the law?" + +"Not in the least." + +"Nor running a chance of arrest?" + +"Not in a good cause." + +"Oh, the cause is excellent!" + +"Then I am your man." + +"I was sure that I might rely on you." + +"But what is it you wish?" + +"When Mrs. Turner has brought in the tray I will make it clear to +you. Now," he said as he turned hungrily on the simple fare that +our landlady had provided, "I must discuss it while I eat, for I +have not much time. It is nearly five now. In two hours we must +be on the scene of action. Miss Irene, or Madame, rather, returns +from her drive at seven. We must be at Briony Lodge to meet her." + +"And what then?" + +"You must leave that to me. I have already arranged what is to +occur. There is only one point on which I must insist. You must +not interfere, come what may. You understand?" + +"I am to be neutral?" + +"To do nothing whatever. There will probably be some small +unpleasantness. Do not join in it. It will end in my being +conveyed into the house. Four or five minutes afterwards the +sitting-room window will open. You are to station yourself close +to that open window." + +"Yes." + +"You are to watch me, for I will be visible to you." + +"Yes." + +"And when I raise my hand--so--you will throw into the room what +I give you to throw, and will, at the same time, raise the cry of +fire. You quite follow me?" + +"Entirely." + +"It is nothing very formidable," he said, taking a long cigar-shaped +roll from his pocket. "It is an ordinary plumber's smoke-rocket, +fitted with a cap at either end to make it self-lighting. +Your task is confined to that. When you raise your cry of fire, +it will be taken up by quite a number of people. You may then +walk to the end of the street, and I will rejoin you in ten +minutes. I hope that I have made myself clear?" + +"I am to remain neutral, to get near the window, to watch you, +and at the signal to throw in this object, then to raise the cry +of fire, and to wait you at the corner of the street." + +"Precisely." + +"Then you may entirely rely on me." + +"That is excellent. I think, perhaps, it is almost time that I +prepare for the new role I have to play." + +He disappeared into his bedroom and returned in a few minutes in +the character of an amiable and simple-minded Nonconformist +clergyman. His broad black hat, his baggy trousers, his white +tie, his sympathetic smile, and general look of peering and +benevolent curiosity were such as Mr. John Hare alone could have +equalled. It was not merely that Holmes changed his costume. His +expression, his manner, his very soul seemed to vary with every +fresh part that he assumed. The stage lost a fine actor, even as +science lost an acute reasoner, when he became a specialist in +crime. + +It was a quarter past six when we left Baker Street, and it still +wanted ten minutes to the hour when we found ourselves in +Serpentine Avenue. It was already dusk, and the lamps were just +being lighted as we paced up and down in front of Briony Lodge, +waiting for the coming of its occupant. The house was just such +as I had pictured it from Sherlock Holmes' succinct description, +but the locality appeared to be less private than I expected. On +the contrary, for a small street in a quiet neighbourhood, it was +remarkably animated. There was a group of shabbily dressed men +smoking and laughing in a corner, a scissors-grinder with his +wheel, two guardsmen who were flirting with a nurse-girl, and +several well-dressed young men who were lounging up and down with +cigars in their mouths. + +"You see," remarked Holmes, as we paced to and fro in front of +the house, "this marriage rather simplifies matters. The +photograph becomes a double-edged weapon now. The chances are +that she would be as averse to its being seen by Mr. Godfrey +Norton, as our client is to its coming to the eyes of his +princess. Now the question is, Where are we to find the +photograph?" + +"Where, indeed?" + +"It is most unlikely that she carries it about with her. It is +cabinet size. Too large for easy concealment about a woman's +dress. She knows that the King is capable of having her waylaid +and searched. Two attempts of the sort have already been made. We +may take it, then, that she does not carry it about with her." + +"Where, then?" + +"Her banker or her lawyer. There is that double possibility. But +I am inclined to think neither. Women are naturally secretive, +and they like to do their own secreting. Why should she hand it +over to anyone else? She could trust her own guardianship, but +she could not tell what indirect or political influence might be +brought to bear upon a business man. Besides, remember that she +had resolved to use it within a few days. It must be where she +can lay her hands upon it. It must be in her own house." + +"But it has twice been burgled." + +"Pshaw! They did not know how to look." + +"But how will you look?" + +"I will not look." + +"What then?" + +"I will get her to show me." + +"But she will refuse." + +"She will not be able to. But I hear the rumble of wheels. It is +her carriage. Now carry out my orders to the letter." + +As he spoke the gleam of the side-lights of a carriage came round +the curve of the avenue. It was a smart little landau which +rattled up to the door of Briony Lodge. As it pulled up, one of +the loafing men at the corner dashed forward to open the door in +the hope of earning a copper, but was elbowed away by another +loafer, who had rushed up with the same intention. A fierce +quarrel broke out, which was increased by the two guardsmen, who +took sides with one of the loungers, and by the scissors-grinder, +who was equally hot upon the other side. A blow was struck, and +in an instant the lady, who had stepped from her carriage, was +the centre of a little knot of flushed and struggling men, who +struck savagely at each other with their fists and sticks. Holmes +dashed into the crowd to protect the lady; but just as he reached +her he gave a cry and dropped to the ground, with the blood +running freely down his face. At his fall the guardsmen took to +their heels in one direction and the loungers in the other, while +a number of better-dressed people, who had watched the scuffle +without taking part in it, crowded in to help the lady and to +attend to the injured man. Irene Adler, as I will still call her, +had hurried up the steps; but she stood at the top with her +superb figure outlined against the lights of the hall, looking +back into the street. + +"Is the poor gentleman much hurt?" she asked. + +"He is dead," cried several voices. + +"No, no, there's life in him!" shouted another. "But he'll be +gone before you can get him to hospital." + +"He's a brave fellow," said a woman. "They would have had the +lady's purse and watch if it hadn't been for him. They were a +gang, and a rough one, too. Ah, he's breathing now." + +"He can't lie in the street. May we bring him in, marm?" + +"Surely. Bring him into the sitting-room. There is a comfortable +sofa. This way, please!" + +Slowly and solemnly he was borne into Briony Lodge and laid out +in the principal room, while I still observed the proceedings +from my post by the window. The lamps had been lit, but the +blinds had not been drawn, so that I could see Holmes as he lay +upon the couch. I do not know whether he was seized with +compunction at that moment for the part he was playing, but I +know that I never felt more heartily ashamed of myself in my life +than when I saw the beautiful creature against whom I was +conspiring, or the grace and kindliness with which she waited +upon the injured man. And yet it would be the blackest treachery +to Holmes to draw back now from the part which he had intrusted +to me. I hardened my heart, and took the smoke-rocket from under +my ulster. After all, I thought, we are not injuring her. We are +but preventing her from injuring another. + +Holmes had sat up upon the couch, and I saw him motion like a man +who is in need of air. A maid rushed across and threw open the +window. At the same instant I saw him raise his hand and at the +signal I tossed my rocket into the room with a cry of "Fire!" The +word was no sooner out of my mouth than the whole crowd of +spectators, well dressed and ill--gentlemen, ostlers, and +servant-maids--joined in a general shriek of "Fire!" Thick clouds +of smoke curled through the room and out at the open window. I +caught a glimpse of rushing figures, and a moment later the voice +of Holmes from within assuring them that it was a false alarm. +Slipping through the shouting crowd I made my way to the corner +of the street, and in ten minutes was rejoiced to find my +friend's arm in mine, and to get away from the scene of uproar. +He walked swiftly and in silence for some few minutes until we +had turned down one of the quiet streets which lead towards the +Edgeware Road. + +"You did it very nicely, Doctor," he remarked. "Nothing could +have been better. It is all right." + +"You have the photograph?" + +"I know where it is." + +"And how did you find out?" + +"She showed me, as I told you she would." + +"I am still in the dark." + +"I do not wish to make a mystery," said he, laughing. "The matter +was perfectly simple. You, of course, saw that everyone in the +street was an accomplice. They were all engaged for the evening." + +"I guessed as much." + +"Then, when the row broke out, I had a little moist red paint in +the palm of my hand. I rushed forward, fell down, clapped my hand +to my face, and became a piteous spectacle. It is an old trick." + +"That also I could fathom." + +"Then they carried me in. She was bound to have me in. What else +could she do? And into her sitting-room, which was the very room +which I suspected. It lay between that and her bedroom, and I was +determined to see which. They laid me on a couch, I motioned for +air, they were compelled to open the window, and you had your +chance." + +"How did that help you?" + +"It was all-important. When a woman thinks that her house is on +fire, her instinct is at once to rush to the thing which she +values most. It is a perfectly overpowering impulse, and I have +more than once taken advantage of it. In the case of the +Darlington substitution scandal it was of use to me, and also in +the Arnsworth Castle business. A married woman grabs at her baby; +an unmarried one reaches for her jewel-box. Now it was clear to +me that our lady of to-day had nothing in the house more precious +to her than what we are in quest of. She would rush to secure it. +The alarm of fire was admirably done. The smoke and shouting were +enough to shake nerves of steel. She responded beautifully. The +photograph is in a recess behind a sliding panel just above the +right bell-pull. She was there in an instant, and I caught a +glimpse of it as she half-drew it out. When I cried out that it +was a false alarm, she replaced it, glanced at the rocket, rushed +from the room, and I have not seen her since. I rose, and, making +my excuses, escaped from the house. I hesitated whether to +attempt to secure the photograph at once; but the coachman had +come in, and as he was watching me narrowly it seemed safer to +wait. A little over-precipitance may ruin all." + +"And now?" I asked. + +"Our quest is practically finished. I shall call with the King +to-morrow, and with you, if you care to come with us. We will be +shown into the sitting-room to wait for the lady, but it is +probable that when she comes she may find neither us nor the +photograph. It might be a satisfaction to his Majesty to regain +it with his own hands." + +"And when will you call?" + +"At eight in the morning. She will not be up, so that we shall +have a clear field. Besides, we must be prompt, for this marriage +may mean a complete change in her life and habits. I must wire to +the King without delay." + +We had reached Baker Street and had stopped at the door. He was +searching his pockets for the key when someone passing said: + +"Good-night, Mister Sherlock Holmes." + +There were several people on the pavement at the time, but the +greeting appeared to come from a slim youth in an ulster who had +hurried by. + +"I've heard that voice before," said Holmes, staring down the +dimly lit street. "Now, I wonder who the deuce that could have +been." + + +III. + +I slept at Baker Street that night, and we were engaged upon our +toast and coffee in the morning when the King of Bohemia rushed +into the room. + +"You have really got it!" he cried, grasping Sherlock Holmes by +either shoulder and looking eagerly into his face. + +"Not yet." + +"But you have hopes?" + +"I have hopes." + +"Then, come. I am all impatience to be gone." + +"We must have a cab." + +"No, my brougham is waiting." + +"Then that will simplify matters." We descended and started off +once more for Briony Lodge. + +"Irene Adler is married," remarked Holmes. + +"Married! When?" + +"Yesterday." + +"But to whom?" + +"To an English lawyer named Norton." + +"But she could not love him." + +"I am in hopes that she does." + +"And why in hopes?" + +"Because it would spare your Majesty all fear of future +annoyance. If the lady loves her husband, she does not love your +Majesty. If she does not love your Majesty, there is no reason +why she should interfere with your Majesty's plan." + +"It is true. And yet--Well! I wish she had been of my own +station! What a queen she would have made!" He relapsed into a +moody silence, which was not broken until we drew up in +Serpentine Avenue. + +The door of Briony Lodge was open, and an elderly woman stood +upon the steps. She watched us with a sardonic eye as we stepped +from the brougham. + +"Mr. Sherlock Holmes, I believe?" said she. + +"I am Mr. Holmes," answered my companion, looking at her with a +questioning and rather startled gaze. + +"Indeed! My mistress told me that you were likely to call. She +left this morning with her husband by the 5:15 train from Charing +Cross for the Continent." + +"What!" Sherlock Holmes staggered back, white with chagrin and +surprise. "Do you mean that she has left England?" + +"Never to return." + +"And the papers?" asked the King hoarsely. "All is lost." + +"We shall see." He pushed past the servant and rushed into the +drawing-room, followed by the King and myself. The furniture was +scattered about in every direction, with dismantled shelves and +open drawers, as if the lady had hurriedly ransacked them before +her flight. Holmes rushed at the bell-pull, tore back a small +sliding shutter, and, plunging in his hand, pulled out a +photograph and a letter. The photograph was of Irene Adler +herself in evening dress, the letter was superscribed to +"Sherlock Holmes, Esq. To be left till called for." My friend +tore it open and we all three read it together. It was dated at +midnight of the preceding night and ran in this way: + +"MY DEAR MR. SHERLOCK HOLMES,--You really did it very well. You +took me in completely. Until after the alarm of fire, I had not a +suspicion. But then, when I found how I had betrayed myself, I +began to think. I had been warned against you months ago. I had +been told that if the King employed an agent it would certainly +be you. And your address had been given me. Yet, with all this, +you made me reveal what you wanted to know. Even after I became +suspicious, I found it hard to think evil of such a dear, kind +old clergyman. But, you know, I have been trained as an actress +myself. Male costume is nothing new to me. I often take advantage +of the freedom which it gives. I sent John, the coachman, to +watch you, ran up stairs, got into my walking-clothes, as I call +them, and came down just as you departed. + +"Well, I followed you to your door, and so made sure that I was +really an object of interest to the celebrated Mr. Sherlock +Holmes. Then I, rather imprudently, wished you good-night, and +started for the Temple to see my husband. + +"We both thought the best resource was flight, when pursued by +so formidable an antagonist; so you will find the nest empty when +you call to-morrow. As to the photograph, your client may rest in +peace. I love and am loved by a better man than he. The King may +do what he will without hindrance from one whom he has cruelly +wronged. I keep it only to safeguard myself, and to preserve a +weapon which will always secure me from any steps which he might +take in the future. I leave a photograph which he might care to +possess; and I remain, dear Mr. Sherlock Holmes, + + "Very truly yours, + "IRENE NORTON, ne ADLER." + +"What a woman--oh, what a woman!" cried the King of Bohemia, when +we had all three read this epistle. "Did I not tell you how quick +and resolute she was? Would she not have made an admirable queen? +Is it not a pity that she was not on my level?" + +"From what I have seen of the lady she seems indeed to be on a +very different level to your Majesty," said Holmes coldly. "I am +sorry that I have not been able to bring your Majesty's business +to a more successful conclusion." + +"On the contrary, my dear sir," cried the King; "nothing could be +more successful. I know that her word is inviolate. The +photograph is now as safe as if it were in the fire." + +"I am glad to hear your Majesty say so." + +"I am immensely indebted to you. Pray tell me in what way I can +reward you. This ring--" He slipped an emerald snake ring from +his finger and held it out upon the palm of his hand. + +"Your Majesty has something which I should value even more +highly," said Holmes. + +"You have but to name it." + +"This photograph!" + +The King stared at him in amazement. + +"Irene's photograph!" he cried. "Certainly, if you wish it." + +"I thank your Majesty. Then there is no more to be done in the +matter. I have the honour to wish you a very good-morning." He +bowed, and, turning away without observing the hand which the +King had stretched out to him, he set off in my company for his +chambers. + +And that was how a great scandal threatened to affect the kingdom +of Bohemia, and how the best plans of Mr. Sherlock Holmes were +beaten by a woman's wit. He used to make merry over the +cleverness of women, but I have not heard him do it of late. And +when he speaks of Irene Adler, or when he refers to her +photograph, it is always under the honourable title of the woman. + + + +ADVENTURE II. THE RED-HEADED LEAGUE + +I had called upon my friend, Mr. Sherlock Holmes, one day in the +autumn of last year and found him in deep conversation with a +very stout, florid-faced, elderly gentleman with fiery red hair. +With an apology for my intrusion, I was about to withdraw when +Holmes pulled me abruptly into the room and closed the door +behind me. + +"You could not possibly have come at a better time, my dear +Watson," he said cordially. + +"I was afraid that you were engaged." + +"So I am. Very much so." + +"Then I can wait in the next room." + +"Not at all. This gentleman, Mr. Wilson, has been my partner and +helper in many of my most successful cases, and I have no +doubt that he will be of the utmost use to me in yours also." + +The stout gentleman half rose from his chair and gave a bob of +greeting, with a quick little questioning glance from his small +fat-encircled eyes. + +"Try the settee," said Holmes, relapsing into his armchair and +putting his fingertips together, as was his custom when in +judicial moods. "I know, my dear Watson, that you share my love +of all that is bizarre and outside the conventions and humdrum +routine of everyday life. You have shown your relish for it by +the enthusiasm which has prompted you to chronicle, and, if you +will excuse my saying so, somewhat to embellish so many of my own +little adventures." + +"Your cases have indeed been of the greatest interest to me," I +observed. + +"You will remember that I remarked the other day, just before we +went into the very simple problem presented by Miss Mary +Sutherland, that for strange effects and extraordinary +combinations we must go to life itself, which is always far more +daring than any effort of the imagination." + +"A proposition which I took the liberty of doubting." + +"You did, Doctor, but none the less you must come round to my +view, for otherwise I shall keep on piling fact upon fact on you +until your reason breaks down under them and acknowledges me to +be right. Now, Mr. Jabez Wilson here has been good enough to call +upon me this morning, and to begin a narrative which promises to +be one of the most singular which I have listened to for some +time. You have heard me remark that the strangest and most unique +things are very often connected not with the larger but with the +smaller crimes, and occasionally, indeed, where there is room for +doubt whether any positive crime has been committed. As far as I +have heard it is impossible for me to say whether the present +case is an instance of crime or not, but the course of events is +certainly among the most singular that I have ever listened to. +Perhaps, Mr. Wilson, you would have the great kindness to +recommence your narrative. I ask you not merely because my friend +Dr. Watson has not heard the opening part but also because the +peculiar nature of the story makes me anxious to have every +possible detail from your lips. As a rule, when I have heard some +slight indication of the course of events, I am able to guide +myself by the thousands of other similar cases which occur to my +memory. In the present instance I am forced to admit that the +facts are, to the best of my belief, unique." + +The portly client puffed out his chest with an appearance of some +little pride and pulled a dirty and wrinkled newspaper from the +inside pocket of his greatcoat. As he glanced down the +advertisement column, with his head thrust forward and the paper +flattened out upon his knee, I took a good look at the man and +endeavoured, after the fashion of my companion, to read the +indications which might be presented by his dress or appearance. + +I did not gain very much, however, by my inspection. Our visitor +bore every mark of being an average commonplace British +tradesman, obese, pompous, and slow. He wore rather baggy grey +shepherd's check trousers, a not over-clean black frock-coat, +unbuttoned in the front, and a drab waistcoat with a heavy brassy +Albert chain, and a square pierced bit of metal dangling down as +an ornament. A frayed top-hat and a faded brown overcoat with a +wrinkled velvet collar lay upon a chair beside him. Altogether, +look as I would, there was nothing remarkable about the man save +his blazing red head, and the expression of extreme chagrin and +discontent upon his features. + +Sherlock Holmes' quick eye took in my occupation, and he shook +his head with a smile as he noticed my questioning glances. +"Beyond the obvious facts that he has at some time done manual +labour, that he takes snuff, that he is a Freemason, that he has +been in China, and that he has done a considerable amount of +writing lately, I can deduce nothing else." + +Mr. Jabez Wilson started up in his chair, with his forefinger +upon the paper, but his eyes upon my companion. + +"How, in the name of good-fortune, did you know all that, Mr. +Holmes?" he asked. "How did you know, for example, that I did +manual labour. It's as true as gospel, for I began as a ship's +carpenter." + +"Your hands, my dear sir. Your right hand is quite a size larger +than your left. You have worked with it, and the muscles are more +developed." + +"Well, the snuff, then, and the Freemasonry?" + +"I won't insult your intelligence by telling you how I read that, +especially as, rather against the strict rules of your order, you +use an arc-and-compass breastpin." + +"Ah, of course, I forgot that. But the writing?" + +"What else can be indicated by that right cuff so very shiny for +five inches, and the left one with the smooth patch near the +elbow where you rest it upon the desk?" + +"Well, but China?" + +"The fish that you have tattooed immediately above your right +wrist could only have been done in China. I have made a small +study of tattoo marks and have even contributed to the literature +of the subject. That trick of staining the fishes' scales of a +delicate pink is quite peculiar to China. When, in addition, I +see a Chinese coin hanging from your watch-chain, the matter +becomes even more simple." + +Mr. Jabez Wilson laughed heavily. "Well, I never!" said he. "I +thought at first that you had done something clever, but I see +that there was nothing in it, after all." + +"I begin to think, Watson," said Holmes, "that I make a mistake +in explaining. 'Omne ignotum pro magnifico,' you know, and my +poor little reputation, such as it is, will suffer shipwreck if I +am so candid. Can you not find the advertisement, Mr. Wilson?" + +"Yes, I have got it now," he answered with his thick red finger +planted halfway down the column. "Here it is. This is what began +it all. You just read it for yourself, sir." + +I took the paper from him and read as follows: + +"TO THE RED-HEADED LEAGUE: On account of the bequest of the late +Ezekiah Hopkins, of Lebanon, Pennsylvania, U. S. A., there is now +another vacancy open which entitles a member of the League to a +salary of 4 pounds a week for purely nominal services. All +red-headed men who are sound in body and mind and above the age +of twenty-one years, are eligible. Apply in person on Monday, at +eleven o'clock, to Duncan Ross, at the offices of the League, 7 +Pope's Court, Fleet Street." + +"What on earth does this mean?" I ejaculated after I had twice +read over the extraordinary announcement. + +Holmes chuckled and wriggled in his chair, as was his habit when +in high spirits. "It is a little off the beaten track, isn't it?" +said he. "And now, Mr. Wilson, off you go at scratch and tell us +all about yourself, your household, and the effect which this +advertisement had upon your fortunes. You will first make a note, +Doctor, of the paper and the date." + +"It is The Morning Chronicle of April 27, 1890. Just two months +ago." + +"Very good. Now, Mr. Wilson?" + +"Well, it is just as I have been telling you, Mr. Sherlock +Holmes," said Jabez Wilson, mopping his forehead; "I have a small +pawnbroker's business at Coburg Square, near the City. It's not a +very large affair, and of late years it has not done more than +just give me a living. I used to be able to keep two assistants, +but now I only keep one; and I would have a job to pay him but +that he is willing to come for half wages so as to learn the +business." + +"What is the name of this obliging youth?" asked Sherlock Holmes. + +"His name is Vincent Spaulding, and he's not such a youth, +either. It's hard to say his age. I should not wish a smarter +assistant, Mr. Holmes; and I know very well that he could better +himself and earn twice what I am able to give him. But, after +all, if he is satisfied, why should I put ideas in his head?" + +"Why, indeed? You seem most fortunate in having an employ who +comes under the full market price. It is not a common experience +among employers in this age. I don't know that your assistant is +not as remarkable as your advertisement." + +"Oh, he has his faults, too," said Mr. Wilson. "Never was such a +fellow for photography. Snapping away with a camera when he ought +to be improving his mind, and then diving down into the cellar +like a rabbit into its hole to develop his pictures. That is his +main fault, but on the whole he's a good worker. There's no vice +in him." + +"He is still with you, I presume?" + +"Yes, sir. He and a girl of fourteen, who does a bit of simple +cooking and keeps the place clean--that's all I have in the +house, for I am a widower and never had any family. We live very +quietly, sir, the three of us; and we keep a roof over our heads +and pay our debts, if we do nothing more. + +"The first thing that put us out was that advertisement. +Spaulding, he came down into the office just this day eight +weeks, with this very paper in his hand, and he says: + +"'I wish to the Lord, Mr. Wilson, that I was a red-headed man.' + +"'Why that?' I asks. + +"'Why,' says he, 'here's another vacancy on the League of the +Red-headed Men. It's worth quite a little fortune to any man who +gets it, and I understand that there are more vacancies than +there are men, so that the trustees are at their wits' end what +to do with the money. If my hair would only change colour, here's +a nice little crib all ready for me to step into.' + +"'Why, what is it, then?' I asked. You see, Mr. Holmes, I am a +very stay-at-home man, and as my business came to me instead of +my having to go to it, I was often weeks on end without putting +my foot over the door-mat. In that way I didn't know much of what +was going on outside, and I was always glad of a bit of news. + +"'Have you never heard of the League of the Red-headed Men?' he +asked with his eyes open. + +"'Never.' + +"'Why, I wonder at that, for you are eligible yourself for one +of the vacancies.' + +"'And what are they worth?' I asked. + +"'Oh, merely a couple of hundred a year, but the work is slight, +and it need not interfere very much with one's other +occupations.' + +"Well, you can easily think that that made me prick up my ears, +for the business has not been over-good for some years, and an +extra couple of hundred would have been very handy. + +"'Tell me all about it,' said I. + +"'Well,' said he, showing me the advertisement, 'you can see for +yourself that the League has a vacancy, and there is the address +where you should apply for particulars. As far as I can make out, +the League was founded by an American millionaire, Ezekiah +Hopkins, who was very peculiar in his ways. He was himself +red-headed, and he had a great sympathy for all red-headed men; +so when he died it was found that he had left his enormous +fortune in the hands of trustees, with instructions to apply the +interest to the providing of easy berths to men whose hair is of +that colour. From all I hear it is splendid pay and very little to +do.' + +"'But,' said I, 'there would be millions of red-headed men who +would apply.' + +"'Not so many as you might think,' he answered. 'You see it is +really confined to Londoners, and to grown men. This American had +started from London when he was young, and he wanted to do the +old town a good turn. Then, again, I have heard it is no use your +applying if your hair is light red, or dark red, or anything but +real bright, blazing, fiery red. Now, if you cared to apply, Mr. +Wilson, you would just walk in; but perhaps it would hardly be +worth your while to put yourself out of the way for the sake of a +few hundred pounds.' + +"Now, it is a fact, gentlemen, as you may see for yourselves, +that my hair is of a very full and rich tint, so that it seemed +to me that if there was to be any competition in the matter I +stood as good a chance as any man that I had ever met. Vincent +Spaulding seemed to know so much about it that I thought he might +prove useful, so I just ordered him to put up the shutters for +the day and to come right away with me. He was very willing to +have a holiday, so we shut the business up and started off for +the address that was given us in the advertisement. + +"I never hope to see such a sight as that again, Mr. Holmes. From +north, south, east, and west every man who had a shade of red in +his hair had tramped into the city to answer the advertisement. +Fleet Street was choked with red-headed folk, and Pope's Court +looked like a coster's orange barrow. I should not have thought +there were so many in the whole country as were brought together +by that single advertisement. Every shade of colour they +were--straw, lemon, orange, brick, Irish-setter, liver, clay; +but, as Spaulding said, there were not many who had the real +vivid flame-coloured tint. When I saw how many were waiting, I +would have given it up in despair; but Spaulding would not hear +of it. How he did it I could not imagine, but he pushed and +pulled and butted until he got me through the crowd, and right up +to the steps which led to the office. There was a double stream +upon the stair, some going up in hope, and some coming back +dejected; but we wedged in as well as we could and soon found +ourselves in the office." + +"Your experience has been a most entertaining one," remarked +Holmes as his client paused and refreshed his memory with a huge +pinch of snuff. "Pray continue your very interesting statement." + +"There was nothing in the office but a couple of wooden chairs +and a deal table, behind which sat a small man with a head that +was even redder than mine. He said a few words to each candidate +as he came up, and then he always managed to find some fault in +them which would disqualify them. Getting a vacancy did not seem +to be such a very easy matter, after all. However, when our turn +came the little man was much more favourable to me than to any of +the others, and he closed the door as we entered, so that he +might have a private word with us. + +"'This is Mr. Jabez Wilson,' said my assistant, 'and he is +willing to fill a vacancy in the League.' + +"'And he is admirably suited for it,' the other answered. 'He has +every requirement. I cannot recall when I have seen anything so +fine.' He took a step backward, cocked his head on one side, and +gazed at my hair until I felt quite bashful. Then suddenly he +plunged forward, wrung my hand, and congratulated me warmly on my +success. + +"'It would be injustice to hesitate,' said he. 'You will, +however, I am sure, excuse me for taking an obvious precaution.' +With that he seized my hair in both his hands, and tugged until I +yelled with the pain. 'There is water in your eyes,' said he as +he released me. 'I perceive that all is as it should be. But we +have to be careful, for we have twice been deceived by wigs and +once by paint. I could tell you tales of cobbler's wax which +would disgust you with human nature.' He stepped over to the +window and shouted through it at the top of his voice that the +vacancy was filled. A groan of disappointment came up from below, +and the folk all trooped away in different directions until there +was not a red-head to be seen except my own and that of the +manager. + +"'My name,' said he, 'is Mr. Duncan Ross, and I am myself one of +the pensioners upon the fund left by our noble benefactor. Are +you a married man, Mr. Wilson? Have you a family?' + +"I answered that I had not. + +"His face fell immediately. + +"'Dear me!' he said gravely, 'that is very serious indeed! I am +sorry to hear you say that. The fund was, of course, for the +propagation and spread of the red-heads as well as for their +maintenance. It is exceedingly unfortunate that you should be a +bachelor.' + +"My face lengthened at this, Mr. Holmes, for I thought that I was +not to have the vacancy after all; but after thinking it over for +a few minutes he said that it would be all right. + +"'In the case of another,' said he, 'the objection might be +fatal, but we must stretch a point in favour of a man with such a +head of hair as yours. When shall you be able to enter upon your +new duties?' + +"'Well, it is a little awkward, for I have a business already,' +said I. + +"'Oh, never mind about that, Mr. Wilson!' said Vincent Spaulding. +'I should be able to look after that for you.' + +"'What would be the hours?' I asked. + +"'Ten to two.' + +"Now a pawnbroker's business is mostly done of an evening, Mr. +Holmes, especially Thursday and Friday evening, which is just +before pay-day; so it would suit me very well to earn a little in +the mornings. Besides, I knew that my assistant was a good man, +and that he would see to anything that turned up. + +"'That would suit me very well,' said I. 'And the pay?' + +"'Is 4 pounds a week.' + +"'And the work?' + +"'Is purely nominal.' + +"'What do you call purely nominal?' + +"'Well, you have to be in the office, or at least in the +building, the whole time. If you leave, you forfeit your whole +position forever. The will is very clear upon that point. You +don't comply with the conditions if you budge from the office +during that time.' + +"'It's only four hours a day, and I should not think of leaving,' +said I. + +"'No excuse will avail,' said Mr. Duncan Ross; 'neither sickness +nor business nor anything else. There you must stay, or you lose +your billet.' + +"'And the work?' + +"'Is to copy out the "Encyclopaedia Britannica." There is the first +volume of it in that press. You must find your own ink, pens, and +blotting-paper, but we provide this table and chair. Will you be +ready to-morrow?' + +"'Certainly,' I answered. + +"'Then, good-bye, Mr. Jabez Wilson, and let me congratulate you +once more on the important position which you have been fortunate +enough to gain.' He bowed me out of the room and I went home with +my assistant, hardly knowing what to say or do, I was so pleased +at my own good fortune. + +"Well, I thought over the matter all day, and by evening I was in +low spirits again; for I had quite persuaded myself that the +whole affair must be some great hoax or fraud, though what its +object might be I could not imagine. It seemed altogether past +belief that anyone could make such a will, or that they would pay +such a sum for doing anything so simple as copying out the +'Encyclopaedia Britannica.' Vincent Spaulding did what he could to +cheer me up, but by bedtime I had reasoned myself out of the +whole thing. However, in the morning I determined to have a look +at it anyhow, so I bought a penny bottle of ink, and with a +quill-pen, and seven sheets of foolscap paper, I started off for +Pope's Court. + +"Well, to my surprise and delight, everything was as right as +possible. The table was set out ready for me, and Mr. Duncan Ross +was there to see that I got fairly to work. He started me off +upon the letter A, and then he left me; but he would drop in from +time to time to see that all was right with me. At two o'clock he +bade me good-day, complimented me upon the amount that I had +written, and locked the door of the office after me. + +"This went on day after day, Mr. Holmes, and on Saturday the +manager came in and planked down four golden sovereigns for my +week's work. It was the same next week, and the same the week +after. Every morning I was there at ten, and every afternoon I +left at two. By degrees Mr. Duncan Ross took to coming in only +once of a morning, and then, after a time, he did not come in at +all. Still, of course, I never dared to leave the room for an +instant, for I was not sure when he might come, and the billet +was such a good one, and suited me so well, that I would not risk +the loss of it. + +"Eight weeks passed away like this, and I had written about +Abbots and Archery and Armour and Architecture and Attica, and +hoped with diligence that I might get on to the B's before very +long. It cost me something in foolscap, and I had pretty nearly +filled a shelf with my writings. And then suddenly the whole +business came to an end." + +"To an end?" + +"Yes, sir. And no later than this morning. I went to my work as +usual at ten o'clock, but the door was shut and locked, with a +little square of cardboard hammered on to the middle of the +panel with a tack. Here it is, and you can read for yourself." + +He held up a piece of white cardboard about the size of a sheet +of note-paper. It read in this fashion: + + THE RED-HEADED LEAGUE + + IS + + DISSOLVED. + + October 9, 1890. + +Sherlock Holmes and I surveyed this curt announcement and the +rueful face behind it, until the comical side of the affair so +completely overtopped every other consideration that we both +burst out into a roar of laughter. + +"I cannot see that there is anything very funny," cried our +client, flushing up to the roots of his flaming head. "If you can +do nothing better than laugh at me, I can go elsewhere." + +"No, no," cried Holmes, shoving him back into the chair from +which he had half risen. "I really wouldn't miss your case for +the world. It is most refreshingly unusual. But there is, if you +will excuse my saying so, something just a little funny about it. +Pray what steps did you take when you found the card upon the +door?" + +"I was staggered, sir. I did not know what to do. Then I called +at the offices round, but none of them seemed to know anything +about it. Finally, I went to the landlord, who is an accountant +living on the ground-floor, and I asked him if he could tell me +what had become of the Red-headed League. He said that he had +never heard of any such body. Then I asked him who Mr. Duncan +Ross was. He answered that the name was new to him. + +"'Well,' said I, 'the gentleman at No. 4.' + +"'What, the red-headed man?' + +"'Yes.' + +"'Oh,' said he, 'his name was William Morris. He was a solicitor +and was using my room as a temporary convenience until his new +premises were ready. He moved out yesterday.' + +"'Where could I find him?' + +"'Oh, at his new offices. He did tell me the address. Yes, 17 +King Edward Street, near St. Paul's.' + +"I started off, Mr. Holmes, but when I got to that address it was +a manufactory of artificial knee-caps, and no one in it had ever +heard of either Mr. William Morris or Mr. Duncan Ross." + +"And what did you do then?" asked Holmes. + +"I went home to Saxe-Coburg Square, and I took the advice of my +assistant. But he could not help me in any way. He could only say +that if I waited I should hear by post. But that was not quite +good enough, Mr. Holmes. I did not wish to lose such a place +without a struggle, so, as I had heard that you were good enough +to give advice to poor folk who were in need of it, I came right +away to you." + +"And you did very wisely," said Holmes. "Your case is an +exceedingly remarkable one, and I shall be happy to look into it. +From what you have told me I think that it is possible that +graver issues hang from it than might at first sight appear." + +"Grave enough!" said Mr. Jabez Wilson. "Why, I have lost four +pound a week." + +"As far as you are personally concerned," remarked Holmes, "I do +not see that you have any grievance against this extraordinary +league. On the contrary, you are, as I understand, richer by some +30 pounds, to say nothing of the minute knowledge which you have +gained on every subject which comes under the letter A. You have +lost nothing by them." + +"No, sir. But I want to find out about them, and who they are, +and what their object was in playing this prank--if it was a +prank--upon me. It was a pretty expensive joke for them, for it +cost them two and thirty pounds." + +"We shall endeavour to clear up these points for you. And, first, +one or two questions, Mr. Wilson. This assistant of yours who +first called your attention to the advertisement--how long had he +been with you?" + +"About a month then." + +"How did he come?" + +"In answer to an advertisement." + +"Was he the only applicant?" + +"No, I had a dozen." + +"Why did you pick him?" + +"Because he was handy and would come cheap." + +"At half-wages, in fact." + +"Yes." + +"What is he like, this Vincent Spaulding?" + +"Small, stout-built, very quick in his ways, no hair on his face, +though he's not short of thirty. Has a white splash of acid upon +his forehead." + +Holmes sat up in his chair in considerable excitement. "I thought +as much," said he. "Have you ever observed that his ears are +pierced for earrings?" + +"Yes, sir. He told me that a gipsy had done it for him when he +was a lad." + +"Hum!" said Holmes, sinking back in deep thought. "He is still +with you?" + +"Oh, yes, sir; I have only just left him." + +"And has your business been attended to in your absence?" + +"Nothing to complain of, sir. There's never very much to do of a +morning." + +"That will do, Mr. Wilson. I shall be happy to give you an +opinion upon the subject in the course of a day or two. To-day is +Saturday, and I hope that by Monday we may come to a conclusion." + +"Well, Watson," said Holmes when our visitor had left us, "what +do you make of it all?" + +"I make nothing of it," I answered frankly. "It is a most +mysterious business." + +"As a rule," said Holmes, "the more bizarre a thing is the less +mysterious it proves to be. It is your commonplace, featureless +crimes which are really puzzling, just as a commonplace face is +the most difficult to identify. But I must be prompt over this +matter." + +"What are you going to do, then?" I asked. + +"To smoke," he answered. "It is quite a three pipe problem, and I +beg that you won't speak to me for fifty minutes." He curled +himself up in his chair, with his thin knees drawn up to his +hawk-like nose, and there he sat with his eyes closed and his +black clay pipe thrusting out like the bill of some strange bird. +I had come to the conclusion that he had dropped asleep, and +indeed was nodding myself, when he suddenly sprang out of his +chair with the gesture of a man who has made up his mind and put +his pipe down upon the mantelpiece. + +"Sarasate plays at the St. James's Hall this afternoon," he +remarked. "What do you think, Watson? Could your patients spare +you for a few hours?" + +"I have nothing to do to-day. My practice is never very +absorbing." + +"Then put on your hat and come. I am going through the City +first, and we can have some lunch on the way. I observe that +there is a good deal of German music on the programme, which is +rather more to my taste than Italian or French. It is +introspective, and I want to introspect. Come along!" + +We travelled by the Underground as far as Aldersgate; and a short +walk took us to Saxe-Coburg Square, the scene of the singular +story which we had listened to in the morning. It was a poky, +little, shabby-genteel place, where four lines of dingy +two-storied brick houses looked out into a small railed-in +enclosure, where a lawn of weedy grass and a few clumps of faded +laurel-bushes made a hard fight against a smoke-laden and +uncongenial atmosphere. Three gilt balls and a brown board with +"JABEZ WILSON" in white letters, upon a corner house, announced +the place where our red-headed client carried on his business. +Sherlock Holmes stopped in front of it with his head on one side +and looked it all over, with his eyes shining brightly between +puckered lids. Then he walked slowly up the street, and then down +again to the corner, still looking keenly at the houses. Finally +he returned to the pawnbroker's, and, having thumped vigorously +upon the pavement with his stick two or three times, he went up +to the door and knocked. It was instantly opened by a +bright-looking, clean-shaven young fellow, who asked him to step +in. + +"Thank you," said Holmes, "I only wished to ask you how you would +go from here to the Strand." + +"Third right, fourth left," answered the assistant promptly, +closing the door. + +"Smart fellow, that," observed Holmes as we walked away. "He is, +in my judgment, the fourth smartest man in London, and for daring +I am not sure that he has not a claim to be third. I have known +something of him before." + +"Evidently," said I, "Mr. Wilson's assistant counts for a good +deal in this mystery of the Red-headed League. I am sure that you +inquired your way merely in order that you might see him." + +"Not him." + +"What then?" + +"The knees of his trousers." + +"And what did you see?" + +"What I expected to see." + +"Why did you beat the pavement?" + +"My dear doctor, this is a time for observation, not for talk. We +are spies in an enemy's country. We know something of Saxe-Coburg +Square. Let us now explore the parts which lie behind it." + +The road in which we found ourselves as we turned round the +corner from the retired Saxe-Coburg Square presented as great a +contrast to it as the front of a picture does to the back. It was +one of the main arteries which conveyed the traffic of the City +to the north and west. The roadway was blocked with the immense +stream of commerce flowing in a double tide inward and outward, +while the footpaths were black with the hurrying swarm of +pedestrians. It was difficult to realise as we looked at the line +of fine shops and stately business premises that they really +abutted on the other side upon the faded and stagnant square +which we had just quitted. + +"Let me see," said Holmes, standing at the corner and glancing +along the line, "I should like just to remember the order of the +houses here. It is a hobby of mine to have an exact knowledge of +London. There is Mortimer's, the tobacconist, the little +newspaper shop, the Coburg branch of the City and Suburban Bank, +the Vegetarian Restaurant, and McFarlane's carriage-building +depot. That carries us right on to the other block. And now, +Doctor, we've done our work, so it's time we had some play. A +sandwich and a cup of coffee, and then off to violin-land, where +all is sweetness and delicacy and harmony, and there are no +red-headed clients to vex us with their conundrums." + +My friend was an enthusiastic musician, being himself not only a +very capable performer but a composer of no ordinary merit. All +the afternoon he sat in the stalls wrapped in the most perfect +happiness, gently waving his long, thin fingers in time to the +music, while his gently smiling face and his languid, dreamy eyes +were as unlike those of Holmes the sleuth-hound, Holmes the +relentless, keen-witted, ready-handed criminal agent, as it was +possible to conceive. In his singular character the dual nature +alternately asserted itself, and his extreme exactness and +astuteness represented, as I have often thought, the reaction +against the poetic and contemplative mood which occasionally +predominated in him. The swing of his nature took him from +extreme languor to devouring energy; and, as I knew well, he was +never so truly formidable as when, for days on end, he had been +lounging in his armchair amid his improvisations and his +black-letter editions. Then it was that the lust of the chase +would suddenly come upon him, and that his brilliant reasoning +power would rise to the level of intuition, until those who were +unacquainted with his methods would look askance at him as on a +man whose knowledge was not that of other mortals. When I saw him +that afternoon so enwrapped in the music at St. James's Hall I +felt that an evil time might be coming upon those whom he had set +himself to hunt down. + +"You want to go home, no doubt, Doctor," he remarked as we +emerged. + +"Yes, it would be as well." + +"And I have some business to do which will take some hours. This +business at Coburg Square is serious." + +"Why serious?" + +"A considerable crime is in contemplation. I have every reason to +believe that we shall be in time to stop it. But to-day being +Saturday rather complicates matters. I shall want your help +to-night." + +"At what time?" + +"Ten will be early enough." + +"I shall be at Baker Street at ten." + +"Very well. And, I say, Doctor, there may be some little danger, +so kindly put your army revolver in your pocket." He waved his +hand, turned on his heel, and disappeared in an instant among the +crowd. + +I trust that I am not more dense than my neighbours, but I was +always oppressed with a sense of my own stupidity in my dealings +with Sherlock Holmes. Here I had heard what he had heard, I had +seen what he had seen, and yet from his words it was evident that +he saw clearly not only what had happened but what was about to +happen, while to me the whole business was still confused and +grotesque. As I drove home to my house in Kensington I thought +over it all, from the extraordinary story of the red-headed +copier of the "Encyclopaedia" down to the visit to Saxe-Coburg +Square, and the ominous words with which he had parted from me. +What was this nocturnal expedition, and why should I go armed? +Where were we going, and what were we to do? I had the hint from +Holmes that this smooth-faced pawnbroker's assistant was a +formidable man--a man who might play a deep game. I tried to +puzzle it out, but gave it up in despair and set the matter aside +until night should bring an explanation. + +It was a quarter-past nine when I started from home and made my +way across the Park, and so through Oxford Street to Baker +Street. Two hansoms were standing at the door, and as I entered +the passage I heard the sound of voices from above. On entering +his room I found Holmes in animated conversation with two men, +one of whom I recognised as Peter Jones, the official police +agent, while the other was a long, thin, sad-faced man, with a +very shiny hat and oppressively respectable frock-coat. + +"Ha! Our party is complete," said Holmes, buttoning up his +pea-jacket and taking his heavy hunting crop from the rack. +"Watson, I think you know Mr. Jones, of Scotland Yard? Let me +introduce you to Mr. Merryweather, who is to be our companion in +to-night's adventure." + +"We're hunting in couples again, Doctor, you see," said Jones in +his consequential way. "Our friend here is a wonderful man for +starting a chase. All he wants is an old dog to help him to do +the running down." + +"I hope a wild goose may not prove to be the end of our chase," +observed Mr. Merryweather gloomily. + +"You may place considerable confidence in Mr. Holmes, sir," said +the police agent loftily. "He has his own little methods, which +are, if he won't mind my saying so, just a little too theoretical +and fantastic, but he has the makings of a detective in him. It +is not too much to say that once or twice, as in that business of +the Sholto murder and the Agra treasure, he has been more nearly +correct than the official force." + +"Oh, if you say so, Mr. Jones, it is all right," said the +stranger with deference. "Still, I confess that I miss my rubber. +It is the first Saturday night for seven-and-twenty years that I +have not had my rubber." + +"I think you will find," said Sherlock Holmes, "that you will +play for a higher stake to-night than you have ever done yet, and +that the play will be more exciting. For you, Mr. Merryweather, +the stake will be some 30,000 pounds; and for you, Jones, it will +be the man upon whom you wish to lay your hands." + +"John Clay, the murderer, thief, smasher, and forger. He's a +young man, Mr. Merryweather, but he is at the head of his +profession, and I would rather have my bracelets on him than on +any criminal in London. He's a remarkable man, is young John +Clay. His grandfather was a royal duke, and he himself has been +to Eton and Oxford. His brain is as cunning as his fingers, and +though we meet signs of him at every turn, we never know where to +find the man himself. He'll crack a crib in Scotland one week, +and be raising money to build an orphanage in Cornwall the next. +I've been on his track for years and have never set eyes on him +yet." + +"I hope that I may have the pleasure of introducing you to-night. +I've had one or two little turns also with Mr. John Clay, and I +agree with you that he is at the head of his profession. It is +past ten, however, and quite time that we started. If you two +will take the first hansom, Watson and I will follow in the +second." + +Sherlock Holmes was not very communicative during the long drive +and lay back in the cab humming the tunes which he had heard in +the afternoon. We rattled through an endless labyrinth of gas-lit +streets until we emerged into Farrington Street. + +"We are close there now," my friend remarked. "This fellow +Merryweather is a bank director, and personally interested in the +matter. I thought it as well to have Jones with us also. He is +not a bad fellow, though an absolute imbecile in his profession. +He has one positive virtue. He is as brave as a bulldog and as +tenacious as a lobster if he gets his claws upon anyone. Here we +are, and they are waiting for us." + +We had reached the same crowded thoroughfare in which we had +found ourselves in the morning. Our cabs were dismissed, and, +following the guidance of Mr. Merryweather, we passed down a +narrow passage and through a side door, which he opened for us. +Within there was a small corridor, which ended in a very massive +iron gate. This also was opened, and led down a flight of winding +stone steps, which terminated at another formidable gate. Mr. +Merryweather stopped to light a lantern, and then conducted us +down a dark, earth-smelling passage, and so, after opening a +third door, into a huge vault or cellar, which was piled all +round with crates and massive boxes. + +"You are not very vulnerable from above," Holmes remarked as he +held up the lantern and gazed about him. + +"Nor from below," said Mr. Merryweather, striking his stick upon +the flags which lined the floor. "Why, dear me, it sounds quite +hollow!" he remarked, looking up in surprise. + +"I must really ask you to be a little more quiet!" said Holmes +severely. "You have already imperilled the whole success of our +expedition. Might I beg that you would have the goodness to sit +down upon one of those boxes, and not to interfere?" + +The solemn Mr. Merryweather perched himself upon a crate, with a +very injured expression upon his face, while Holmes fell upon his +knees upon the floor and, with the lantern and a magnifying lens, +began to examine minutely the cracks between the stones. A few +seconds sufficed to satisfy him, for he sprang to his feet again +and put his glass in his pocket. + +"We have at least an hour before us," he remarked, "for they can +hardly take any steps until the good pawnbroker is safely in bed. +Then they will not lose a minute, for the sooner they do their +work the longer time they will have for their escape. We are at +present, Doctor--as no doubt you have divined--in the cellar of +the City branch of one of the principal London banks. Mr. +Merryweather is the chairman of directors, and he will explain to +you that there are reasons why the more daring criminals of +London should take a considerable interest in this cellar at +present." + +"It is our French gold," whispered the director. "We have had +several warnings that an attempt might be made upon it." + +"Your French gold?" + +"Yes. We had occasion some months ago to strengthen our resources +and borrowed for that purpose 30,000 napoleons from the Bank of +France. It has become known that we have never had occasion to +unpack the money, and that it is still lying in our cellar. The +crate upon which I sit contains 2,000 napoleons packed between +layers of lead foil. Our reserve of bullion is much larger at +present than is usually kept in a single branch office, and the +directors have had misgivings upon the subject." + +"Which were very well justified," observed Holmes. "And now it is +time that we arranged our little plans. I expect that within an +hour matters will come to a head. In the meantime Mr. +Merryweather, we must put the screen over that dark lantern." + +"And sit in the dark?" + +"I am afraid so. I had brought a pack of cards in my pocket, and +I thought that, as we were a partie carre, you might have your +rubber after all. But I see that the enemy's preparations have +gone so far that we cannot risk the presence of a light. And, +first of all, we must choose our positions. These are daring men, +and though we shall take them at a disadvantage, they may do us +some harm unless we are careful. I shall stand behind this crate, +and do you conceal yourselves behind those. Then, when I flash a +light upon them, close in swiftly. If they fire, Watson, have no +compunction about shooting them down." + +I placed my revolver, cocked, upon the top of the wooden case +behind which I crouched. Holmes shot the slide across the front +of his lantern and left us in pitch darkness--such an absolute +darkness as I have never before experienced. The smell of hot +metal remained to assure us that the light was still there, ready +to flash out at a moment's notice. To me, with my nerves worked +up to a pitch of expectancy, there was something depressing and +subduing in the sudden gloom, and in the cold dank air of the +vault. + +"They have but one retreat," whispered Holmes. "That is back +through the house into Saxe-Coburg Square. I hope that you have +done what I asked you, Jones?" + +"I have an inspector and two officers waiting at the front door." + +"Then we have stopped all the holes. And now we must be silent +and wait." + +What a time it seemed! From comparing notes afterwards it was but +an hour and a quarter, yet it appeared to me that the night must +have almost gone and the dawn be breaking above us. My limbs +were weary and stiff, for I feared to change my position; yet my +nerves were worked up to the highest pitch of tension, and my +hearing was so acute that I could not only hear the gentle +breathing of my companions, but I could distinguish the deeper, +heavier in-breath of the bulky Jones from the thin, sighing note +of the bank director. From my position I could look over the case +in the direction of the floor. Suddenly my eyes caught the glint +of a light. + +At first it was but a lurid spark upon the stone pavement. Then +it lengthened out until it became a yellow line, and then, +without any warning or sound, a gash seemed to open and a hand +appeared, a white, almost womanly hand, which felt about in the +centre of the little area of light. For a minute or more the +hand, with its writhing fingers, protruded out of the floor. Then +it was withdrawn as suddenly as it appeared, and all was dark +again save the single lurid spark which marked a chink between +the stones. + +Its disappearance, however, was but momentary. With a rending, +tearing sound, one of the broad, white stones turned over upon +its side and left a square, gaping hole, through which streamed +the light of a lantern. Over the edge there peeped a clean-cut, +boyish face, which looked keenly about it, and then, with a hand +on either side of the aperture, drew itself shoulder-high and +waist-high, until one knee rested upon the edge. In another +instant he stood at the side of the hole and was hauling after +him a companion, lithe and small like himself, with a pale face +and a shock of very red hair. + +"It's all clear," he whispered. "Have you the chisel and the +bags? Great Scott! Jump, Archie, jump, and I'll swing for it!" + +Sherlock Holmes had sprung out and seized the intruder by the +collar. The other dived down the hole, and I heard the sound of +rending cloth as Jones clutched at his skirts. The light flashed +upon the barrel of a revolver, but Holmes' hunting crop came +down on the man's wrist, and the pistol clinked upon the stone +floor. + +"It's no use, John Clay," said Holmes blandly. "You have no +chance at all." + +"So I see," the other answered with the utmost coolness. "I fancy +that my pal is all right, though I see you have got his +coat-tails." + +"There are three men waiting for him at the door," said Holmes. + +"Oh, indeed! You seem to have done the thing very completely. I +must compliment you." + +"And I you," Holmes answered. "Your red-headed idea was very new +and effective." + +"You'll see your pal again presently," said Jones. "He's quicker +at climbing down holes than I am. Just hold out while I fix the +derbies." + +"I beg that you will not touch me with your filthy hands," +remarked our prisoner as the handcuffs clattered upon his wrists. +"You may not be aware that I have royal blood in my veins. Have +the goodness, also, when you address me always to say 'sir' and +'please.'" + +"All right," said Jones with a stare and a snigger. "Well, would +you please, sir, march upstairs, where we can get a cab to carry +your Highness to the police-station?" + +"That is better," said John Clay serenely. He made a sweeping bow +to the three of us and walked quietly off in the custody of the +detective. + +"Really, Mr. Holmes," said Mr. Merryweather as we followed them +from the cellar, "I do not know how the bank can thank you or +repay you. There is no doubt that you have detected and defeated +in the most complete manner one of the most determined attempts +at bank robbery that have ever come within my experience." + +"I have had one or two little scores of my own to settle with Mr. +John Clay," said Holmes. "I have been at some small expense over +this matter, which I shall expect the bank to refund, but beyond +that I am amply repaid by having had an experience which is in +many ways unique, and by hearing the very remarkable narrative of +the Red-headed League." + + +"You see, Watson," he explained in the early hours of the morning +as we sat over a glass of whisky and soda in Baker Street, "it +was perfectly obvious from the first that the only possible +object of this rather fantastic business of the advertisement of +the League, and the copying of the 'Encyclopaedia,' must be to get +this not over-bright pawnbroker out of the way for a number of +hours every day. It was a curious way of managing it, but, +really, it would be difficult to suggest a better. The method was +no doubt suggested to Clay's ingenious mind by the colour of his +accomplice's hair. The 4 pounds a week was a lure which must draw +him, and what was it to them, who were playing for thousands? +They put in the advertisement, one rogue has the temporary +office, the other rogue incites the man to apply for it, and +together they manage to secure his absence every morning in the +week. From the time that I heard of the assistant having come for +half wages, it was obvious to me that he had some strong motive +for securing the situation." + +"But how could you guess what the motive was?" + +"Had there been women in the house, I should have suspected a +mere vulgar intrigue. That, however, was out of the question. The +man's business was a small one, and there was nothing in his +house which could account for such elaborate preparations, and +such an expenditure as they were at. It must, then, be something +out of the house. What could it be? I thought of the assistant's +fondness for photography, and his trick of vanishing into the +cellar. The cellar! There was the end of this tangled clue. Then +I made inquiries as to this mysterious assistant and found that I +had to deal with one of the coolest and most daring criminals in +London. He was doing something in the cellar--something which +took many hours a day for months on end. What could it be, once +more? I could think of nothing save that he was running a tunnel +to some other building. + +"So far I had got when we went to visit the scene of action. I +surprised you by beating upon the pavement with my stick. I was +ascertaining whether the cellar stretched out in front or behind. +It was not in front. Then I rang the bell, and, as I hoped, the +assistant answered it. We have had some skirmishes, but we had +never set eyes upon each other before. I hardly looked at his +face. His knees were what I wished to see. You must yourself have +remarked how worn, wrinkled, and stained they were. They spoke of +those hours of burrowing. The only remaining point was what they +were burrowing for. I walked round the corner, saw the City and +Suburban Bank abutted on our friend's premises, and felt that I +had solved my problem. When you drove home after the concert I +called upon Scotland Yard and upon the chairman of the bank +directors, with the result that you have seen." + +"And how could you tell that they would make their attempt +to-night?" I asked. + +"Well, when they closed their League offices that was a sign that +they cared no longer about Mr. Jabez Wilson's presence--in other +words, that they had completed their tunnel. But it was essential +that they should use it soon, as it might be discovered, or the +bullion might be removed. Saturday would suit them better than +any other day, as it would give them two days for their escape. +For all these reasons I expected them to come to-night." + +"You reasoned it out beautifully," I exclaimed in unfeigned +admiration. "It is so long a chain, and yet every link rings +true." + +"It saved me from ennui," he answered, yawning. "Alas! I already +feel it closing in upon me. My life is spent in one long effort +to escape from the commonplaces of existence. These little +problems help me to do so." + +"And you are a benefactor of the race," said I. + +He shrugged his shoulders. "Well, perhaps, after all, it is of +some little use," he remarked. "'L'homme c'est rien--l'oeuvre +c'est tout,' as Gustave Flaubert wrote to George Sand." + + + +ADVENTURE III. A CASE OF IDENTITY + +"My dear fellow," said Sherlock Holmes as we sat on either side +of the fire in his lodgings at Baker Street, "life is infinitely +stranger than anything which the mind of man could invent. We +would not dare to conceive the things which are really mere +commonplaces of existence. If we could fly out of that window +hand in hand, hover over this great city, gently remove the +roofs, and peep in at the queer things which are going on, the +strange coincidences, the plannings, the cross-purposes, the +wonderful chains of events, working through generations, and +leading to the most outr results, it would make all fiction with +its conventionalities and foreseen conclusions most stale and +unprofitable." + +"And yet I am not convinced of it," I answered. "The cases which +come to light in the papers are, as a rule, bald enough, and +vulgar enough. We have in our police reports realism pushed to +its extreme limits, and yet the result is, it must be confessed, +neither fascinating nor artistic." + +"A certain selection and discretion must be used in producing a +realistic effect," remarked Holmes. "This is wanting in the +police report, where more stress is laid, perhaps, upon the +platitudes of the magistrate than upon the details, which to an +observer contain the vital essence of the whole matter. Depend +upon it, there is nothing so unnatural as the commonplace." + +I smiled and shook my head. "I can quite understand your thinking +so," I said. "Of course, in your position of unofficial adviser +and helper to everybody who is absolutely puzzled, throughout +three continents, you are brought in contact with all that is +strange and bizarre. But here"--I picked up the morning paper +from the ground--"let us put it to a practical test. Here is the +first heading upon which I come. 'A husband's cruelty to his +wife.' There is half a column of print, but I know without +reading it that it is all perfectly familiar to me. There is, of +course, the other woman, the drink, the push, the blow, the +bruise, the sympathetic sister or landlady. The crudest of +writers could invent nothing more crude." + +"Indeed, your example is an unfortunate one for your argument," +said Holmes, taking the paper and glancing his eye down it. "This +is the Dundas separation case, and, as it happens, I was engaged +in clearing up some small points in connection with it. The +husband was a teetotaler, there was no other woman, and the +conduct complained of was that he had drifted into the habit of +winding up every meal by taking out his false teeth and hurling +them at his wife, which, you will allow, is not an action likely +to occur to the imagination of the average story-teller. Take a +pinch of snuff, Doctor, and acknowledge that I have scored over +you in your example." + +He held out his snuffbox of old gold, with a great amethyst in +the centre of the lid. Its splendour was in such contrast to his +homely ways and simple life that I could not help commenting upon +it. + +"Ah," said he, "I forgot that I had not seen you for some weeks. +It is a little souvenir from the King of Bohemia in return for my +assistance in the case of the Irene Adler papers." + +"And the ring?" I asked, glancing at a remarkable brilliant which +sparkled upon his finger. + +"It was from the reigning family of Holland, though the matter in +which I served them was of such delicacy that I cannot confide it +even to you, who have been good enough to chronicle one or two of +my little problems." + +"And have you any on hand just now?" I asked with interest. + +"Some ten or twelve, but none which present any feature of +interest. They are important, you understand, without being +interesting. Indeed, I have found that it is usually in +unimportant matters that there is a field for the observation, +and for the quick analysis of cause and effect which gives the +charm to an investigation. The larger crimes are apt to be the +simpler, for the bigger the crime the more obvious, as a rule, is +the motive. In these cases, save for one rather intricate matter +which has been referred to me from Marseilles, there is nothing +which presents any features of interest. It is possible, however, +that I may have something better before very many minutes are +over, for this is one of my clients, or I am much mistaken." + +He had risen from his chair and was standing between the parted +blinds gazing down into the dull neutral-tinted London street. +Looking over his shoulder, I saw that on the pavement opposite +there stood a large woman with a heavy fur boa round her neck, +and a large curling red feather in a broad-brimmed hat which was +tilted in a coquettish Duchess of Devonshire fashion over her +ear. From under this great panoply she peeped up in a nervous, +hesitating fashion at our windows, while her body oscillated +backward and forward, and her fingers fidgeted with her glove +buttons. Suddenly, with a plunge, as of the swimmer who leaves +the bank, she hurried across the road, and we heard the sharp +clang of the bell. + +"I have seen those symptoms before," said Holmes, throwing his +cigarette into the fire. "Oscillation upon the pavement always +means an affaire de coeur. She would like advice, but is not sure +that the matter is not too delicate for communication. And yet +even here we may discriminate. When a woman has been seriously +wronged by a man she no longer oscillates, and the usual symptom +is a broken bell wire. Here we may take it that there is a love +matter, but that the maiden is not so much angry as perplexed, or +grieved. But here she comes in person to resolve our doubts." + +As he spoke there was a tap at the door, and the boy in buttons +entered to announce Miss Mary Sutherland, while the lady herself +loomed behind his small black figure like a full-sailed +merchant-man behind a tiny pilot boat. Sherlock Holmes welcomed +her with the easy courtesy for which he was remarkable, and, +having closed the door and bowed her into an armchair, he looked +her over in the minute and yet abstracted fashion which was +peculiar to him. + +"Do you not find," he said, "that with your short sight it is a +little trying to do so much typewriting?" + +"I did at first," she answered, "but now I know where the letters +are without looking." Then, suddenly realising the full purport +of his words, she gave a violent start and looked up, with fear +and astonishment upon her broad, good-humoured face. "You've +heard about me, Mr. Holmes," she cried, "else how could you know +all that?" + +"Never mind," said Holmes, laughing; "it is my business to know +things. Perhaps I have trained myself to see what others +overlook. If not, why should you come to consult me?" + +"I came to you, sir, because I heard of you from Mrs. Etherege, +whose husband you found so easy when the police and everyone had +given him up for dead. Oh, Mr. Holmes, I wish you would do as +much for me. I'm not rich, but still I have a hundred a year in +my own right, besides the little that I make by the machine, and +I would give it all to know what has become of Mr. Hosmer Angel." + +"Why did you come away to consult me in such a hurry?" asked +Sherlock Holmes, with his finger-tips together and his eyes to +the ceiling. + +Again a startled look came over the somewhat vacuous face of Miss +Mary Sutherland. "Yes, I did bang out of the house," she said, +"for it made me angry to see the easy way in which Mr. +Windibank--that is, my father--took it all. He would not go to +the police, and he would not go to you, and so at last, as he +would do nothing and kept on saying that there was no harm done, +it made me mad, and I just on with my things and came right away +to you." + +"Your father," said Holmes, "your stepfather, surely, since the +name is different." + +"Yes, my stepfather. I call him father, though it sounds funny, +too, for he is only five years and two months older than myself." + +"And your mother is alive?" + +"Oh, yes, mother is alive and well. I wasn't best pleased, Mr. +Holmes, when she married again so soon after father's death, and +a man who was nearly fifteen years younger than herself. Father +was a plumber in the Tottenham Court Road, and he left a tidy +business behind him, which mother carried on with Mr. Hardy, the +foreman; but when Mr. Windibank came he made her sell the +business, for he was very superior, being a traveller in wines. +They got 4700 pounds for the goodwill and interest, which wasn't +near as much as father could have got if he had been alive." + +I had expected to see Sherlock Holmes impatient under this +rambling and inconsequential narrative, but, on the contrary, he +had listened with the greatest concentration of attention. + +"Your own little income," he asked, "does it come out of the +business?" + +"Oh, no, sir. It is quite separate and was left me by my uncle +Ned in Auckland. It is in New Zealand stock, paying 4 1/2 per +cent. Two thousand five hundred pounds was the amount, but I can +only touch the interest." + +"You interest me extremely," said Holmes. "And since you draw so +large a sum as a hundred a year, with what you earn into the +bargain, you no doubt travel a little and indulge yourself in +every way. I believe that a single lady can get on very nicely +upon an income of about 60 pounds." + +"I could do with much less than that, Mr. Holmes, but you +understand that as long as I live at home I don't wish to be a +burden to them, and so they have the use of the money just while +I am staying with them. Of course, that is only just for the +time. Mr. Windibank draws my interest every quarter and pays it +over to mother, and I find that I can do pretty well with what I +earn at typewriting. It brings me twopence a sheet, and I can +often do from fifteen to twenty sheets in a day." + +"You have made your position very clear to me," said Holmes. +"This is my friend, Dr. Watson, before whom you can speak as +freely as before myself. Kindly tell us now all about your +connection with Mr. Hosmer Angel." + +A flush stole over Miss Sutherland's face, and she picked +nervously at the fringe of her jacket. "I met him first at the +gasfitters' ball," she said. "They used to send father tickets +when he was alive, and then afterwards they remembered us, and +sent them to mother. Mr. Windibank did not wish us to go. He +never did wish us to go anywhere. He would get quite mad if I +wanted so much as to join a Sunday-school treat. But this time I +was set on going, and I would go; for what right had he to +prevent? He said the folk were not fit for us to know, when all +father's friends were to be there. And he said that I had nothing +fit to wear, when I had my purple plush that I had never so much +as taken out of the drawer. At last, when nothing else would do, +he went off to France upon the business of the firm, but we went, +mother and I, with Mr. Hardy, who used to be our foreman, and it +was there I met Mr. Hosmer Angel." + +"I suppose," said Holmes, "that when Mr. Windibank came back from +France he was very annoyed at your having gone to the ball." + +"Oh, well, he was very good about it. He laughed, I remember, and +shrugged his shoulders, and said there was no use denying +anything to a woman, for she would have her way." + +"I see. Then at the gasfitters' ball you met, as I understand, a +gentleman called Mr. Hosmer Angel." + +"Yes, sir. I met him that night, and he called next day to ask if +we had got home all safe, and after that we met him--that is to +say, Mr. Holmes, I met him twice for walks, but after that father +came back again, and Mr. Hosmer Angel could not come to the house +any more." + +"No?" + +"Well, you know father didn't like anything of the sort. He +wouldn't have any visitors if he could help it, and he used to +say that a woman should be happy in her own family circle. But +then, as I used to say to mother, a woman wants her own circle to +begin with, and I had not got mine yet." + +"But how about Mr. Hosmer Angel? Did he make no attempt to see +you?" + +"Well, father was going off to France again in a week, and Hosmer +wrote and said that it would be safer and better not to see each +other until he had gone. We could write in the meantime, and he +used to write every day. I took the letters in in the morning, so +there was no need for father to know." + +"Were you engaged to the gentleman at this time?" + +"Oh, yes, Mr. Holmes. We were engaged after the first walk that +we took. Hosmer--Mr. Angel--was a cashier in an office in +Leadenhall Street--and--" + +"What office?" + +"That's the worst of it, Mr. Holmes, I don't know." + +"Where did he live, then?" + +"He slept on the premises." + +"And you don't know his address?" + +"No--except that it was Leadenhall Street." + +"Where did you address your letters, then?" + +"To the Leadenhall Street Post Office, to be left till called +for. He said that if they were sent to the office he would be +chaffed by all the other clerks about having letters from a lady, +so I offered to typewrite them, like he did his, but he wouldn't +have that, for he said that when I wrote them they seemed to come +from me, but when they were typewritten he always felt that the +machine had come between us. That will just show you how fond he +was of me, Mr. Holmes, and the little things that he would think +of." + +"It was most suggestive," said Holmes. "It has long been an axiom +of mine that the little things are infinitely the most important. +Can you remember any other little things about Mr. Hosmer Angel?" + +"He was a very shy man, Mr. Holmes. He would rather walk with me +in the evening than in the daylight, for he said that he hated to +be conspicuous. Very retiring and gentlemanly he was. Even his +voice was gentle. He'd had the quinsy and swollen glands when he +was young, he told me, and it had left him with a weak throat, +and a hesitating, whispering fashion of speech. He was always +well dressed, very neat and plain, but his eyes were weak, just +as mine are, and he wore tinted glasses against the glare." + +"Well, and what happened when Mr. Windibank, your stepfather, +returned to France?" + +"Mr. Hosmer Angel came to the house again and proposed that we +should marry before father came back. He was in dreadful earnest +and made me swear, with my hands on the Testament, that whatever +happened I would always be true to him. Mother said he was quite +right to make me swear, and that it was a sign of his passion. +Mother was all in his favour from the first and was even fonder +of him than I was. Then, when they talked of marrying within the +week, I began to ask about father; but they both said never to +mind about father, but just to tell him afterwards, and mother +said she would make it all right with him. I didn't quite like +that, Mr. Holmes. It seemed funny that I should ask his leave, as +he was only a few years older than me; but I didn't want to do +anything on the sly, so I wrote to father at Bordeaux, where the +company has its French offices, but the letter came back to me on +the very morning of the wedding." + +"It missed him, then?" + +"Yes, sir; for he had started to England just before it arrived." + +"Ha! that was unfortunate. Your wedding was arranged, then, for +the Friday. Was it to be in church?" + +"Yes, sir, but very quietly. It was to be at St. Saviour's, near +King's Cross, and we were to have breakfast afterwards at the St. +Pancras Hotel. Hosmer came for us in a hansom, but as there were +two of us he put us both into it and stepped himself into a +four-wheeler, which happened to be the only other cab in the +street. We got to the church first, and when the four-wheeler +drove up we waited for him to step out, but he never did, and +when the cabman got down from the box and looked there was no one +there! The cabman said that he could not imagine what had become +of him, for he had seen him get in with his own eyes. That was +last Friday, Mr. Holmes, and I have never seen or heard anything +since then to throw any light upon what became of him." + +"It seems to me that you have been very shamefully treated," said +Holmes. + +"Oh, no, sir! He was too good and kind to leave me so. Why, all +the morning he was saying to me that, whatever happened, I was to +be true; and that even if something quite unforeseen occurred to +separate us, I was always to remember that I was pledged to him, +and that he would claim his pledge sooner or later. It seemed +strange talk for a wedding-morning, but what has happened since +gives a meaning to it." + +"Most certainly it does. Your own opinion is, then, that some +unforeseen catastrophe has occurred to him?" + +"Yes, sir. I believe that he foresaw some danger, or else he +would not have talked so. And then I think that what he foresaw +happened." + +"But you have no notion as to what it could have been?" + +"None." + +"One more question. How did your mother take the matter?" + +"She was angry, and said that I was never to speak of the matter +again." + +"And your father? Did you tell him?" + +"Yes; and he seemed to think, with me, that something had +happened, and that I should hear of Hosmer again. As he said, +what interest could anyone have in bringing me to the doors of +the church, and then leaving me? Now, if he had borrowed my +money, or if he had married me and got my money settled on him, +there might be some reason, but Hosmer was very independent about +money and never would look at a shilling of mine. And yet, what +could have happened? And why could he not write? Oh, it drives me +half-mad to think of it, and I can't sleep a wink at night." She +pulled a little handkerchief out of her muff and began to sob +heavily into it. + +"I shall glance into the case for you," said Holmes, rising, "and +I have no doubt that we shall reach some definite result. Let the +weight of the matter rest upon me now, and do not let your mind +dwell upon it further. Above all, try to let Mr. Hosmer Angel +vanish from your memory, as he has done from your life." + +"Then you don't think I'll see him again?" + +"I fear not." + +"Then what has happened to him?" + +"You will leave that question in my hands. I should like an +accurate description of him and any letters of his which you can +spare." + +"I advertised for him in last Saturday's Chronicle," said she. +"Here is the slip and here are four letters from him." + +"Thank you. And your address?" + +"No. 31 Lyon Place, Camberwell." + +"Mr. Angel's address you never had, I understand. Where is your +father's place of business?" + +"He travels for Westhouse & Marbank, the great claret importers +of Fenchurch Street." + +"Thank you. You have made your statement very clearly. You will +leave the papers here, and remember the advice which I have given +you. Let the whole incident be a sealed book, and do not allow it +to affect your life." + +"You are very kind, Mr. Holmes, but I cannot do that. I shall be +true to Hosmer. He shall find me ready when he comes back." + +For all the preposterous hat and the vacuous face, there was +something noble in the simple faith of our visitor which +compelled our respect. She laid her little bundle of papers upon +the table and went her way, with a promise to come again whenever +she might be summoned. + +Sherlock Holmes sat silent for a few minutes with his fingertips +still pressed together, his legs stretched out in front of him, +and his gaze directed upward to the ceiling. Then he took down +from the rack the old and oily clay pipe, which was to him as a +counsellor, and, having lit it, he leaned back in his chair, with +the thick blue cloud-wreaths spinning up from him, and a look of +infinite languor in his face. + +"Quite an interesting study, that maiden," he observed. "I found +her more interesting than her little problem, which, by the way, +is rather a trite one. You will find parallel cases, if you +consult my index, in Andover in '77, and there was something of +the sort at The Hague last year. Old as is the idea, however, +there were one or two details which were new to me. But the +maiden herself was most instructive." + +"You appeared to read a good deal upon her which was quite +invisible to me," I remarked. + +"Not invisible but unnoticed, Watson. You did not know where to +look, and so you missed all that was important. I can never bring +you to realise the importance of sleeves, the suggestiveness of +thumb-nails, or the great issues that may hang from a boot-lace. +Now, what did you gather from that woman's appearance? Describe +it." + +"Well, she had a slate-coloured, broad-brimmed straw hat, with a +feather of a brickish red. Her jacket was black, with black beads +sewn upon it, and a fringe of little black jet ornaments. Her +dress was brown, rather darker than coffee colour, with a little +purple plush at the neck and sleeves. Her gloves were greyish and +were worn through at the right forefinger. Her boots I didn't +observe. She had small round, hanging gold earrings, and a +general air of being fairly well-to-do in a vulgar, comfortable, +easy-going way." + +Sherlock Holmes clapped his hands softly together and chuckled. + +"'Pon my word, Watson, you are coming along wonderfully. You have +really done very well indeed. It is true that you have missed +everything of importance, but you have hit upon the method, and +you have a quick eye for colour. Never trust to general +impressions, my boy, but concentrate yourself upon details. My +first glance is always at a woman's sleeve. In a man it is +perhaps better first to take the knee of the trouser. As you +observe, this woman had plush upon her sleeves, which is a most +useful material for showing traces. The double line a little +above the wrist, where the typewritist presses against the table, +was beautifully defined. The sewing-machine, of the hand type, +leaves a similar mark, but only on the left arm, and on the side +of it farthest from the thumb, instead of being right across the +broadest part, as this was. I then glanced at her face, and, +observing the dint of a pince-nez at either side of her nose, I +ventured a remark upon short sight and typewriting, which seemed +to surprise her." + +"It surprised me." + +"But, surely, it was obvious. I was then much surprised and +interested on glancing down to observe that, though the boots +which she was wearing were not unlike each other, they were +really odd ones; the one having a slightly decorated toe-cap, and +the other a plain one. One was buttoned only in the two lower +buttons out of five, and the other at the first, third, and +fifth. Now, when you see that a young lady, otherwise neatly +dressed, has come away from home with odd boots, half-buttoned, +it is no great deduction to say that she came away in a hurry." + +"And what else?" I asked, keenly interested, as I always was, by +my friend's incisive reasoning. + +"I noted, in passing, that she had written a note before leaving +home but after being fully dressed. You observed that her right +glove was torn at the forefinger, but you did not apparently see +that both glove and finger were stained with violet ink. She had +written in a hurry and dipped her pen too deep. It must have been +this morning, or the mark would not remain clear upon the finger. +All this is amusing, though rather elementary, but I must go back +to business, Watson. Would you mind reading me the advertised +description of Mr. Hosmer Angel?" + +I held the little printed slip to the light. + +"Missing," it said, "on the morning of the fourteenth, a gentleman +named Hosmer Angel. About five ft. seven in. in height; +strongly built, sallow complexion, black hair, a little bald in +the centre, bushy, black side-whiskers and moustache; tinted +glasses, slight infirmity of speech. Was dressed, when last seen, +in black frock-coat faced with silk, black waistcoat, gold Albert +chain, and grey Harris tweed trousers, with brown gaiters over +elastic-sided boots. Known to have been employed in an office in +Leadenhall Street. Anybody bringing--" + +"That will do," said Holmes. "As to the letters," he continued, +glancing over them, "they are very commonplace. Absolutely no +clue in them to Mr. Angel, save that he quotes Balzac once. There +is one remarkable point, however, which will no doubt strike +you." + +"They are typewritten," I remarked. + +"Not only that, but the signature is typewritten. Look at the +neat little 'Hosmer Angel' at the bottom. There is a date, you +see, but no superscription except Leadenhall Street, which is +rather vague. The point about the signature is very suggestive--in +fact, we may call it conclusive." + +"Of what?" + +"My dear fellow, is it possible you do not see how strongly it +bears upon the case?" + +"I cannot say that I do unless it were that he wished to be able +to deny his signature if an action for breach of promise were +instituted." + +"No, that was not the point. However, I shall write two letters, +which should settle the matter. One is to a firm in the City, the +other is to the young lady's stepfather, Mr. Windibank, asking +him whether he could meet us here at six o'clock tomorrow +evening. It is just as well that we should do business with the +male relatives. And now, Doctor, we can do nothing until the +answers to those letters come, so we may put our little problem +upon the shelf for the interim." + +I had had so many reasons to believe in my friend's subtle powers +of reasoning and extraordinary energy in action that I felt that +he must have some solid grounds for the assured and easy +demeanour with which he treated the singular mystery which he had +been called upon to fathom. Once only had I known him to fail, in +the case of the King of Bohemia and of the Irene Adler +photograph; but when I looked back to the weird business of the +Sign of Four, and the extraordinary circumstances connected with +the Study in Scarlet, I felt that it would be a strange tangle +indeed which he could not unravel. + +I left him then, still puffing at his black clay pipe, with the +conviction that when I came again on the next evening I would +find that he held in his hands all the clues which would lead up +to the identity of the disappearing bridegroom of Miss Mary +Sutherland. + +A professional case of great gravity was engaging my own +attention at the time, and the whole of next day I was busy at +the bedside of the sufferer. It was not until close upon six +o'clock that I found myself free and was able to spring into a +hansom and drive to Baker Street, half afraid that I might be too +late to assist at the dnouement of the little mystery. I found +Sherlock Holmes alone, however, half asleep, with his long, thin +form curled up in the recesses of his armchair. A formidable +array of bottles and test-tubes, with the pungent cleanly smell +of hydrochloric acid, told me that he had spent his day in the +chemical work which was so dear to him. + +"Well, have you solved it?" I asked as I entered. + +"Yes. It was the bisulphate of baryta." + +"No, no, the mystery!" I cried. + +"Oh, that! I thought of the salt that I have been working upon. +There was never any mystery in the matter, though, as I said +yesterday, some of the details are of interest. The only drawback +is that there is no law, I fear, that can touch the scoundrel." + +"Who was he, then, and what was his object in deserting Miss +Sutherland?" + +The question was hardly out of my mouth, and Holmes had not yet +opened his lips to reply, when we heard a heavy footfall in the +passage and a tap at the door. + +"This is the girl's stepfather, Mr. James Windibank," said +Holmes. "He has written to me to say that he would be here at +six. Come in!" + +The man who entered was a sturdy, middle-sized fellow, some +thirty years of age, clean-shaven, and sallow-skinned, with a +bland, insinuating manner, and a pair of wonderfully sharp and +penetrating grey eyes. He shot a questioning glance at each of +us, placed his shiny top-hat upon the sideboard, and with a +slight bow sidled down into the nearest chair. + +"Good-evening, Mr. James Windibank," said Holmes. "I think that +this typewritten letter is from you, in which you made an +appointment with me for six o'clock?" + +"Yes, sir. I am afraid that I am a little late, but I am not +quite my own master, you know. I am sorry that Miss Sutherland +has troubled you about this little matter, for I think it is far +better not to wash linen of the sort in public. It was quite +against my wishes that she came, but she is a very excitable, +impulsive girl, as you may have noticed, and she is not easily +controlled when she has made up her mind on a point. Of course, I +did not mind you so much, as you are not connected with the +official police, but it is not pleasant to have a family +misfortune like this noised abroad. Besides, it is a useless +expense, for how could you possibly find this Hosmer Angel?" + +"On the contrary," said Holmes quietly; "I have every reason to +believe that I will succeed in discovering Mr. Hosmer Angel." + +Mr. Windibank gave a violent start and dropped his gloves. "I am +delighted to hear it," he said. + +"It is a curious thing," remarked Holmes, "that a typewriter has +really quite as much individuality as a man's handwriting. Unless +they are quite new, no two of them write exactly alike. Some +letters get more worn than others, and some wear only on one +side. Now, you remark in this note of yours, Mr. Windibank, that +in every case there is some little slurring over of the 'e,' and +a slight defect in the tail of the 'r.' There are fourteen other +characteristics, but those are the more obvious." + +"We do all our correspondence with this machine at the office, +and no doubt it is a little worn," our visitor answered, glancing +keenly at Holmes with his bright little eyes. + +"And now I will show you what is really a very interesting study, +Mr. Windibank," Holmes continued. "I think of writing another +little monograph some of these days on the typewriter and its +relation to crime. It is a subject to which I have devoted some +little attention. I have here four letters which purport to come +from the missing man. They are all typewritten. In each case, not +only are the 'e's' slurred and the 'r's' tailless, but you will +observe, if you care to use my magnifying lens, that the fourteen +other characteristics to which I have alluded are there as well." + +Mr. Windibank sprang out of his chair and picked up his hat. "I +cannot waste time over this sort of fantastic talk, Mr. Holmes," +he said. "If you can catch the man, catch him, and let me know +when you have done it." + +"Certainly," said Holmes, stepping over and turning the key in +the door. "I let you know, then, that I have caught him!" + +"What! where?" shouted Mr. Windibank, turning white to his lips +and glancing about him like a rat in a trap. + +"Oh, it won't do--really it won't," said Holmes suavely. "There +is no possible getting out of it, Mr. Windibank. It is quite too +transparent, and it was a very bad compliment when you said that +it was impossible for me to solve so simple a question. That's +right! Sit down and let us talk it over." + +Our visitor collapsed into a chair, with a ghastly face and a +glitter of moisture on his brow. "It--it's not actionable," he +stammered. + +"I am very much afraid that it is not. But between ourselves, +Windibank, it was as cruel and selfish and heartless a trick in a +petty way as ever came before me. Now, let me just run over the +course of events, and you will contradict me if I go wrong." + +The man sat huddled up in his chair, with his head sunk upon his +breast, like one who is utterly crushed. Holmes stuck his feet up +on the corner of the mantelpiece and, leaning back with his hands +in his pockets, began talking, rather to himself, as it seemed, +than to us. + +"The man married a woman very much older than himself for her +money," said he, "and he enjoyed the use of the money of the +daughter as long as she lived with them. It was a considerable +sum, for people in their position, and the loss of it would have +made a serious difference. It was worth an effort to preserve it. +The daughter was of a good, amiable disposition, but affectionate +and warm-hearted in her ways, so that it was evident that with +her fair personal advantages, and her little income, she would +not be allowed to remain single long. Now her marriage would +mean, of course, the loss of a hundred a year, so what does her +stepfather do to prevent it? He takes the obvious course of +keeping her at home and forbidding her to seek the company of +people of her own age. But soon he found that that would not +answer forever. She became restive, insisted upon her rights, and +finally announced her positive intention of going to a certain +ball. What does her clever stepfather do then? He conceives an +idea more creditable to his head than to his heart. With the +connivance and assistance of his wife he disguised himself, +covered those keen eyes with tinted glasses, masked the face with +a moustache and a pair of bushy whiskers, sunk that clear voice +into an insinuating whisper, and doubly secure on account of the +girl's short sight, he appears as Mr. Hosmer Angel, and keeps off +other lovers by making love himself." + +"It was only a joke at first," groaned our visitor. "We never +thought that she would have been so carried away." + +"Very likely not. However that may be, the young lady was very +decidedly carried away, and, having quite made up her mind that +her stepfather was in France, the suspicion of treachery never +for an instant entered her mind. She was flattered by the +gentleman's attentions, and the effect was increased by the +loudly expressed admiration of her mother. Then Mr. Angel began +to call, for it was obvious that the matter should be pushed as +far as it would go if a real effect were to be produced. There +were meetings, and an engagement, which would finally secure the +girl's affections from turning towards anyone else. But the +deception could not be kept up forever. These pretended journeys +to France were rather cumbrous. The thing to do was clearly to +bring the business to an end in such a dramatic manner that it +would leave a permanent impression upon the young lady's mind and +prevent her from looking upon any other suitor for some time to +come. Hence those vows of fidelity exacted upon a Testament, and +hence also the allusions to a possibility of something happening +on the very morning of the wedding. James Windibank wished Miss +Sutherland to be so bound to Hosmer Angel, and so uncertain as to +his fate, that for ten years to come, at any rate, she would not +listen to another man. As far as the church door he brought her, +and then, as he could go no farther, he conveniently vanished +away by the old trick of stepping in at one door of a +four-wheeler and out at the other. I think that was the chain of +events, Mr. Windibank!" + +Our visitor had recovered something of his assurance while Holmes +had been talking, and he rose from his chair now with a cold +sneer upon his pale face. + +"It may be so, or it may not, Mr. Holmes," said he, "but if you +are so very sharp you ought to be sharp enough to know that it is +you who are breaking the law now, and not me. I have done nothing +actionable from the first, but as long as you keep that door +locked you lay yourself open to an action for assault and illegal +constraint." + +"The law cannot, as you say, touch you," said Holmes, unlocking +and throwing open the door, "yet there never was a man who +deserved punishment more. If the young lady has a brother or a +friend, he ought to lay a whip across your shoulders. By Jove!" +he continued, flushing up at the sight of the bitter sneer upon +the man's face, "it is not part of my duties to my client, but +here's a hunting crop handy, and I think I shall just treat +myself to--" He took two swift steps to the whip, but before he +could grasp it there was a wild clatter of steps upon the stairs, +the heavy hall door banged, and from the window we could see Mr. +James Windibank running at the top of his speed down the road. + +"There's a cold-blooded scoundrel!" said Holmes, laughing, as he +threw himself down into his chair once more. "That fellow will +rise from crime to crime until he does something very bad, and +ends on a gallows. The case has, in some respects, been not +entirely devoid of interest." + +"I cannot now entirely see all the steps of your reasoning," I +remarked. + +"Well, of course it was obvious from the first that this Mr. +Hosmer Angel must have some strong object for his curious +conduct, and it was equally clear that the only man who really +profited by the incident, as far as we could see, was the +stepfather. Then the fact that the two men were never together, +but that the one always appeared when the other was away, was +suggestive. So were the tinted spectacles and the curious voice, +which both hinted at a disguise, as did the bushy whiskers. My +suspicions were all confirmed by his peculiar action in +typewriting his signature, which, of course, inferred that his +handwriting was so familiar to her that she would recognise even +the smallest sample of it. You see all these isolated facts, +together with many minor ones, all pointed in the same +direction." + +"And how did you verify them?" + +"Having once spotted my man, it was easy to get corroboration. I +knew the firm for which this man worked. Having taken the printed +description. I eliminated everything from it which could be the +result of a disguise--the whiskers, the glasses, the voice, and I +sent it to the firm, with a request that they would inform me +whether it answered to the description of any of their +travellers. I had already noticed the peculiarities of the +typewriter, and I wrote to the man himself at his business +address asking him if he would come here. As I expected, his +reply was typewritten and revealed the same trivial but +characteristic defects. The same post brought me a letter from +Westhouse & Marbank, of Fenchurch Street, to say that the +description tallied in every respect with that of their employ, +James Windibank. Voil tout!" + +"And Miss Sutherland?" + +"If I tell her she will not believe me. You may remember the old +Persian saying, 'There is danger for him who taketh the tiger +cub, and danger also for whoso snatches a delusion from a woman.' +There is as much sense in Hafiz as in Horace, and as much +knowledge of the world." + + + +ADVENTURE IV. THE BOSCOMBE VALLEY MYSTERY + +We were seated at breakfast one morning, my wife and I, when the +maid brought in a telegram. It was from Sherlock Holmes and ran +in this way: + +"Have you a couple of days to spare? Have just been wired for from +the west of England in connection with Boscombe Valley tragedy. +Shall be glad if you will come with me. Air and scenery perfect. +Leave Paddington by the 11:15." + +"What do you say, dear?" said my wife, looking across at me. +"Will you go?" + +"I really don't know what to say. I have a fairly long list at +present." + +"Oh, Anstruther would do your work for you. You have been looking +a little pale lately. I think that the change would do you good, +and you are always so interested in Mr. Sherlock Holmes' cases." + +"I should be ungrateful if I were not, seeing what I gained +through one of them," I answered. "But if I am to go, I must pack +at once, for I have only half an hour." + +My experience of camp life in Afghanistan had at least had the +effect of making me a prompt and ready traveller. My wants were +few and simple, so that in less than the time stated I was in a +cab with my valise, rattling away to Paddington Station. Sherlock +Holmes was pacing up and down the platform, his tall, gaunt +figure made even gaunter and taller by his long grey +travelling-cloak and close-fitting cloth cap. + +"It is really very good of you to come, Watson," said he. "It +makes a considerable difference to me, having someone with me on +whom I can thoroughly rely. Local aid is always either worthless +or else biassed. If you will keep the two corner seats I shall +get the tickets." + +We had the carriage to ourselves save for an immense litter of +papers which Holmes had brought with him. Among these he rummaged +and read, with intervals of note-taking and of meditation, until +we were past Reading. Then he suddenly rolled them all into a +gigantic ball and tossed them up onto the rack. + +"Have you heard anything of the case?" he asked. + +"Not a word. I have not seen a paper for some days." + +"The London press has not had very full accounts. I have just +been looking through all the recent papers in order to master the +particulars. It seems, from what I gather, to be one of those +simple cases which are so extremely difficult." + +"That sounds a little paradoxical." + +"But it is profoundly true. Singularity is almost invariably a +clue. The more featureless and commonplace a crime is, the more +difficult it is to bring it home. In this case, however, they +have established a very serious case against the son of the +murdered man." + +"It is a murder, then?" + +"Well, it is conjectured to be so. I shall take nothing for +granted until I have the opportunity of looking personally into +it. I will explain the state of things to you, as far as I have +been able to understand it, in a very few words. + +"Boscombe Valley is a country district not very far from Ross, in +Herefordshire. The largest landed proprietor in that part is a +Mr. John Turner, who made his money in Australia and returned +some years ago to the old country. One of the farms which he +held, that of Hatherley, was let to Mr. Charles McCarthy, who was +also an ex-Australian. The men had known each other in the +colonies, so that it was not unnatural that when they came to +settle down they should do so as near each other as possible. +Turner was apparently the richer man, so McCarthy became his +tenant but still remained, it seems, upon terms of perfect +equality, as they were frequently together. McCarthy had one son, +a lad of eighteen, and Turner had an only daughter of the same +age, but neither of them had wives living. They appear to have +avoided the society of the neighbouring English families and to +have led retired lives, though both the McCarthys were fond of +sport and were frequently seen at the race-meetings of the +neighbourhood. McCarthy kept two servants--a man and a girl. +Turner had a considerable household, some half-dozen at the +least. That is as much as I have been able to gather about the +families. Now for the facts. + +"On June 3rd, that is, on Monday last, McCarthy left his house at +Hatherley about three in the afternoon and walked down to the +Boscombe Pool, which is a small lake formed by the spreading out +of the stream which runs down the Boscombe Valley. He had been +out with his serving-man in the morning at Ross, and he had told +the man that he must hurry, as he had an appointment of +importance to keep at three. From that appointment he never came +back alive. + +"From Hatherley Farm-house to the Boscombe Pool is a quarter of a +mile, and two people saw him as he passed over this ground. One +was an old woman, whose name is not mentioned, and the other was +William Crowder, a game-keeper in the employ of Mr. Turner. Both +these witnesses depose that Mr. McCarthy was walking alone. The +game-keeper adds that within a few minutes of his seeing Mr. +McCarthy pass he had seen his son, Mr. James McCarthy, going the +same way with a gun under his arm. To the best of his belief, the +father was actually in sight at the time, and the son was +following him. He thought no more of the matter until he heard in +the evening of the tragedy that had occurred. + +"The two McCarthys were seen after the time when William Crowder, +the game-keeper, lost sight of them. The Boscombe Pool is thickly +wooded round, with just a fringe of grass and of reeds round the +edge. A girl of fourteen, Patience Moran, who is the daughter of +the lodge-keeper of the Boscombe Valley estate, was in one of the +woods picking flowers. She states that while she was there she +saw, at the border of the wood and close by the lake, Mr. +McCarthy and his son, and that they appeared to be having a +violent quarrel. She heard Mr. McCarthy the elder using very +strong language to his son, and she saw the latter raise up his +hand as if to strike his father. She was so frightened by their +violence that she ran away and told her mother when she reached +home that she had left the two McCarthys quarrelling near +Boscombe Pool, and that she was afraid that they were going to +fight. She had hardly said the words when young Mr. McCarthy came +running up to the lodge to say that he had found his father dead +in the wood, and to ask for the help of the lodge-keeper. He was +much excited, without either his gun or his hat, and his right +hand and sleeve were observed to be stained with fresh blood. On +following him they found the dead body stretched out upon the +grass beside the pool. The head had been beaten in by repeated +blows of some heavy and blunt weapon. The injuries were such as +might very well have been inflicted by the butt-end of his son's +gun, which was found lying on the grass within a few paces of the +body. Under these circumstances the young man was instantly +arrested, and a verdict of 'wilful murder' having been returned +at the inquest on Tuesday, he was on Wednesday brought before the +magistrates at Ross, who have referred the case to the next +Assizes. Those are the main facts of the case as they came out +before the coroner and the police-court." + +"I could hardly imagine a more damning case," I remarked. "If +ever circumstantial evidence pointed to a criminal it does so +here." + +"Circumstantial evidence is a very tricky thing," answered Holmes +thoughtfully. "It may seem to point very straight to one thing, +but if you shift your own point of view a little, you may find it +pointing in an equally uncompromising manner to something +entirely different. It must be confessed, however, that the case +looks exceedingly grave against the young man, and it is very +possible that he is indeed the culprit. There are several people +in the neighbourhood, however, and among them Miss Turner, the +daughter of the neighbouring landowner, who believe in his +innocence, and who have retained Lestrade, whom you may recollect +in connection with the Study in Scarlet, to work out the case in +his interest. Lestrade, being rather puzzled, has referred the +case to me, and hence it is that two middle-aged gentlemen are +flying westward at fifty miles an hour instead of quietly +digesting their breakfasts at home." + +"I am afraid," said I, "that the facts are so obvious that you +will find little credit to be gained out of this case." + +"There is nothing more deceptive than an obvious fact," he +answered, laughing. "Besides, we may chance to hit upon some +other obvious facts which may have been by no means obvious to +Mr. Lestrade. You know me too well to think that I am boasting +when I say that I shall either confirm or destroy his theory by +means which he is quite incapable of employing, or even of +understanding. To take the first example to hand, I very clearly +perceive that in your bedroom the window is upon the right-hand +side, and yet I question whether Mr. Lestrade would have noted +even so self-evident a thing as that." + +"How on earth--" + +"My dear fellow, I know you well. I know the military neatness +which characterises you. You shave every morning, and in this +season you shave by the sunlight; but since your shaving is less +and less complete as we get farther back on the left side, until +it becomes positively slovenly as we get round the angle of the +jaw, it is surely very clear that that side is less illuminated +than the other. I could not imagine a man of your habits looking +at himself in an equal light and being satisfied with such a +result. I only quote this as a trivial example of observation and +inference. Therein lies my mtier, and it is just possible that +it may be of some service in the investigation which lies before +us. There are one or two minor points which were brought out in +the inquest, and which are worth considering." + +"What are they?" + +"It appears that his arrest did not take place at once, but after +the return to Hatherley Farm. On the inspector of constabulary +informing him that he was a prisoner, he remarked that he was not +surprised to hear it, and that it was no more than his deserts. +This observation of his had the natural effect of removing any +traces of doubt which might have remained in the minds of the +coroner's jury." + +"It was a confession," I ejaculated. + +"No, for it was followed by a protestation of innocence." + +"Coming on the top of such a damning series of events, it was at +least a most suspicious remark." + +"On the contrary," said Holmes, "it is the brightest rift which I +can at present see in the clouds. However innocent he might be, +he could not be such an absolute imbecile as not to see that the +circumstances were very black against him. Had he appeared +surprised at his own arrest, or feigned indignation at it, I +should have looked upon it as highly suspicious, because such +surprise or anger would not be natural under the circumstances, +and yet might appear to be the best policy to a scheming man. His +frank acceptance of the situation marks him as either an innocent +man, or else as a man of considerable self-restraint and +firmness. As to his remark about his deserts, it was also not +unnatural if you consider that he stood beside the dead body of +his father, and that there is no doubt that he had that very day +so far forgotten his filial duty as to bandy words with him, and +even, according to the little girl whose evidence is so +important, to raise his hand as if to strike him. The +self-reproach and contrition which are displayed in his remark +appear to me to be the signs of a healthy mind rather than of a +guilty one." + +I shook my head. "Many men have been hanged on far slighter +evidence," I remarked. + +"So they have. And many men have been wrongfully hanged." + +"What is the young man's own account of the matter?" + +"It is, I am afraid, not very encouraging to his supporters, +though there are one or two points in it which are suggestive. +You will find it here, and may read it for yourself." + +He picked out from his bundle a copy of the local Herefordshire +paper, and having turned down the sheet he pointed out the +paragraph in which the unfortunate young man had given his own +statement of what had occurred. I settled myself down in the +corner of the carriage and read it very carefully. It ran in this +way: + +"Mr. James McCarthy, the only son of the deceased, was then called +and gave evidence as follows: 'I had been away from home for +three days at Bristol, and had only just returned upon the +morning of last Monday, the 3rd. My father was absent from home at +the time of my arrival, and I was informed by the maid that he +had driven over to Ross with John Cobb, the groom. Shortly after +my return I heard the wheels of his trap in the yard, and, +looking out of my window, I saw him get out and walk rapidly out +of the yard, though I was not aware in which direction he was +going. I then took my gun and strolled out in the direction of +the Boscombe Pool, with the intention of visiting the rabbit +warren which is upon the other side. On my way I saw William +Crowder, the game-keeper, as he had stated in his evidence; but +he is mistaken in thinking that I was following my father. I had +no idea that he was in front of me. When about a hundred yards +from the pool I heard a cry of "Cooee!" which was a usual signal +between my father and myself. I then hurried forward, and found +him standing by the pool. He appeared to be much surprised at +seeing me and asked me rather roughly what I was doing there. A +conversation ensued which led to high words and almost to blows, +for my father was a man of a very violent temper. Seeing that his +passion was becoming ungovernable, I left him and returned +towards Hatherley Farm. I had not gone more than 150 yards, +however, when I heard a hideous outcry behind me, which caused me +to run back again. I found my father expiring upon the ground, +with his head terribly injured. I dropped my gun and held him in +my arms, but he almost instantly expired. I knelt beside him for +some minutes, and then made my way to Mr. Turner's lodge-keeper, +his house being the nearest, to ask for assistance. I saw no one +near my father when I returned, and I have no idea how he came by +his injuries. He was not a popular man, being somewhat cold and +forbidding in his manners, but he had, as far as I know, no +active enemies. I know nothing further of the matter.' + +"The Coroner: Did your father make any statement to you before +he died? + +"Witness: He mumbled a few words, but I could only catch some +allusion to a rat. + +"The Coroner: What did you understand by that? + +"Witness: It conveyed no meaning to me. I thought that he was +delirious. + +"The Coroner: What was the point upon which you and your father +had this final quarrel? + +"Witness: I should prefer not to answer. + +"The Coroner: I am afraid that I must press it. + +"Witness: It is really impossible for me to tell you. I can +assure you that it has nothing to do with the sad tragedy which +followed. + +"The Coroner: That is for the court to decide. I need not point +out to you that your refusal to answer will prejudice your case +considerably in any future proceedings which may arise. + +"Witness: I must still refuse. + +"The Coroner: I understand that the cry of 'Cooee' was a common +signal between you and your father? + +"Witness: It was. + +"The Coroner: How was it, then, that he uttered it before he saw +you, and before he even knew that you had returned from Bristol? + +"Witness (with considerable confusion): I do not know. + +"A Juryman: Did you see nothing which aroused your suspicions +when you returned on hearing the cry and found your father +fatally injured? + +"Witness: Nothing definite. + +"The Coroner: What do you mean? + +"Witness: I was so disturbed and excited as I rushed out into +the open, that I could think of nothing except of my father. Yet +I have a vague impression that as I ran forward something lay +upon the ground to the left of me. It seemed to me to be +something grey in colour, a coat of some sort, or a plaid perhaps. +When I rose from my father I looked round for it, but it was +gone. + +"'Do you mean that it disappeared before you went for help?' + +"'Yes, it was gone.' + +"'You cannot say what it was?' + +"'No, I had a feeling something was there.' + +"'How far from the body?' + +"'A dozen yards or so.' + +"'And how far from the edge of the wood?' + +"'About the same.' + +"'Then if it was removed it was while you were within a dozen +yards of it?' + +"'Yes, but with my back towards it.' + +"This concluded the examination of the witness." + +"I see," said I as I glanced down the column, "that the coroner +in his concluding remarks was rather severe upon young McCarthy. +He calls attention, and with reason, to the discrepancy about his +father having signalled to him before seeing him, also to his +refusal to give details of his conversation with his father, and +his singular account of his father's dying words. They are all, +as he remarks, very much against the son." + +Holmes laughed softly to himself and stretched himself out upon +the cushioned seat. "Both you and the coroner have been at some +pains," said he, "to single out the very strongest points in the +young man's favour. Don't you see that you alternately give him +credit for having too much imagination and too little? Too +little, if he could not invent a cause of quarrel which would +give him the sympathy of the jury; too much, if he evolved from +his own inner consciousness anything so outr as a dying +reference to a rat, and the incident of the vanishing cloth. No, +sir, I shall approach this case from the point of view that what +this young man says is true, and we shall see whither that +hypothesis will lead us. And now here is my pocket Petrarch, and +not another word shall I say of this case until we are on the +scene of action. We lunch at Swindon, and I see that we shall be +there in twenty minutes." + +It was nearly four o'clock when we at last, after passing through +the beautiful Stroud Valley, and over the broad gleaming Severn, +found ourselves at the pretty little country-town of Ross. A +lean, ferret-like man, furtive and sly-looking, was waiting for +us upon the platform. In spite of the light brown dustcoat and +leather-leggings which he wore in deference to his rustic +surroundings, I had no difficulty in recognising Lestrade, of +Scotland Yard. With him we drove to the Hereford Arms where a +room had already been engaged for us. + +"I have ordered a carriage," said Lestrade as we sat over a cup +of tea. "I knew your energetic nature, and that you would not be +happy until you had been on the scene of the crime." + +"It was very nice and complimentary of you," Holmes answered. "It +is entirely a question of barometric pressure." + +Lestrade looked startled. "I do not quite follow," he said. + +"How is the glass? Twenty-nine, I see. No wind, and not a cloud +in the sky. I have a caseful of cigarettes here which need +smoking, and the sofa is very much superior to the usual country +hotel abomination. I do not think that it is probable that I +shall use the carriage to-night." + +Lestrade laughed indulgently. "You have, no doubt, already formed +your conclusions from the newspapers," he said. "The case is as +plain as a pikestaff, and the more one goes into it the plainer +it becomes. Still, of course, one can't refuse a lady, and such a +very positive one, too. She has heard of you, and would have your +opinion, though I repeatedly told her that there was nothing +which you could do which I had not already done. Why, bless my +soul! here is her carriage at the door." + +He had hardly spoken before there rushed into the room one of the +most lovely young women that I have ever seen in my life. Her +violet eyes shining, her lips parted, a pink flush upon her +cheeks, all thought of her natural reserve lost in her +overpowering excitement and concern. + +"Oh, Mr. Sherlock Holmes!" she cried, glancing from one to the +other of us, and finally, with a woman's quick intuition, +fastening upon my companion, "I am so glad that you have come. I +have driven down to tell you so. I know that James didn't do it. +I know it, and I want you to start upon your work knowing it, +too. Never let yourself doubt upon that point. We have known each +other since we were little children, and I know his faults as no +one else does; but he is too tender-hearted to hurt a fly. Such a +charge is absurd to anyone who really knows him." + +"I hope we may clear him, Miss Turner," said Sherlock Holmes. +"You may rely upon my doing all that I can." + +"But you have read the evidence. You have formed some conclusion? +Do you not see some loophole, some flaw? Do you not yourself +think that he is innocent?" + +"I think that it is very probable." + +"There, now!" she cried, throwing back her head and looking +defiantly at Lestrade. "You hear! He gives me hopes." + +Lestrade shrugged his shoulders. "I am afraid that my colleague +has been a little quick in forming his conclusions," he said. + +"But he is right. Oh! I know that he is right. James never did +it. And about his quarrel with his father, I am sure that the +reason why he would not speak about it to the coroner was because +I was concerned in it." + +"In what way?" asked Holmes. + +"It is no time for me to hide anything. James and his father had +many disagreements about me. Mr. McCarthy was very anxious that +there should be a marriage between us. James and I have always +loved each other as brother and sister; but of course he is young +and has seen very little of life yet, and--and--well, he +naturally did not wish to do anything like that yet. So there +were quarrels, and this, I am sure, was one of them." + +"And your father?" asked Holmes. "Was he in favour of such a +union?" + +"No, he was averse to it also. No one but Mr. McCarthy was in +favour of it." A quick blush passed over her fresh young face as +Holmes shot one of his keen, questioning glances at her. + +"Thank you for this information," said he. "May I see your father +if I call to-morrow?" + +"I am afraid the doctor won't allow it." + +"The doctor?" + +"Yes, have you not heard? Poor father has never been strong for +years back, but this has broken him down completely. He has taken +to his bed, and Dr. Willows says that he is a wreck and that his +nervous system is shattered. Mr. McCarthy was the only man alive +who had known dad in the old days in Victoria." + +"Ha! In Victoria! That is important." + +"Yes, at the mines." + +"Quite so; at the gold-mines, where, as I understand, Mr. Turner +made his money." + +"Yes, certainly." + +"Thank you, Miss Turner. You have been of material assistance to +me." + +"You will tell me if you have any news to-morrow. No doubt you +will go to the prison to see James. Oh, if you do, Mr. Holmes, do +tell him that I know him to be innocent." + +"I will, Miss Turner." + +"I must go home now, for dad is very ill, and he misses me so if +I leave him. Good-bye, and God help you in your undertaking." She +hurried from the room as impulsively as she had entered, and we +heard the wheels of her carriage rattle off down the street. + +"I am ashamed of you, Holmes," said Lestrade with dignity after a +few minutes' silence. "Why should you raise up hopes which you +are bound to disappoint? I am not over-tender of heart, but I +call it cruel." + +"I think that I see my way to clearing James McCarthy," said +Holmes. "Have you an order to see him in prison?" + +"Yes, but only for you and me." + +"Then I shall reconsider my resolution about going out. We have +still time to take a train to Hereford and see him to-night?" + +"Ample." + +"Then let us do so. Watson, I fear that you will find it very +slow, but I shall only be away a couple of hours." + +I walked down to the station with them, and then wandered through +the streets of the little town, finally returning to the hotel, +where I lay upon the sofa and tried to interest myself in a +yellow-backed novel. The puny plot of the story was so thin, +however, when compared to the deep mystery through which we were +groping, and I found my attention wander so continually from the +action to the fact, that I at last flung it across the room and +gave myself up entirely to a consideration of the events of the +day. Supposing that this unhappy young man's story were +absolutely true, then what hellish thing, what absolutely +unforeseen and extraordinary calamity could have occurred between +the time when he parted from his father, and the moment when, +drawn back by his screams, he rushed into the glade? It was +something terrible and deadly. What could it be? Might not the +nature of the injuries reveal something to my medical instincts? +I rang the bell and called for the weekly county paper, which +contained a verbatim account of the inquest. In the surgeon's +deposition it was stated that the posterior third of the left +parietal bone and the left half of the occipital bone had been +shattered by a heavy blow from a blunt weapon. I marked the spot +upon my own head. Clearly such a blow must have been struck from +behind. That was to some extent in favour of the accused, as when +seen quarrelling he was face to face with his father. Still, it +did not go for very much, for the older man might have turned his +back before the blow fell. Still, it might be worth while to call +Holmes' attention to it. Then there was the peculiar dying +reference to a rat. What could that mean? It could not be +delirium. A man dying from a sudden blow does not commonly become +delirious. No, it was more likely to be an attempt to explain how +he met his fate. But what could it indicate? I cudgelled my +brains to find some possible explanation. And then the incident +of the grey cloth seen by young McCarthy. If that were true the +murderer must have dropped some part of his dress, presumably his +overcoat, in his flight, and must have had the hardihood to +return and to carry it away at the instant when the son was +kneeling with his back turned not a dozen paces off. What a +tissue of mysteries and improbabilities the whole thing was! I +did not wonder at Lestrade's opinion, and yet I had so much faith +in Sherlock Holmes' insight that I could not lose hope as long +as every fresh fact seemed to strengthen his conviction of young +McCarthy's innocence. + +It was late before Sherlock Holmes returned. He came back alone, +for Lestrade was staying in lodgings in the town. + +"The glass still keeps very high," he remarked as he sat down. +"It is of importance that it should not rain before we are able +to go over the ground. On the other hand, a man should be at his +very best and keenest for such nice work as that, and I did not +wish to do it when fagged by a long journey. I have seen young +McCarthy." + +"And what did you learn from him?" + +"Nothing." + +"Could he throw no light?" + +"None at all. I was inclined to think at one time that he knew +who had done it and was screening him or her, but I am convinced +now that he is as puzzled as everyone else. He is not a very +quick-witted youth, though comely to look at and, I should think, +sound at heart." + +"I cannot admire his taste," I remarked, "if it is indeed a fact +that he was averse to a marriage with so charming a young lady as +this Miss Turner." + +"Ah, thereby hangs a rather painful tale. This fellow is madly, +insanely, in love with her, but some two years ago, when he was +only a lad, and before he really knew her, for she had been away +five years at a boarding-school, what does the idiot do but get +into the clutches of a barmaid in Bristol and marry her at a +registry office? No one knows a word of the matter, but you can +imagine how maddening it must be to him to be upbraided for not +doing what he would give his very eyes to do, but what he knows +to be absolutely impossible. It was sheer frenzy of this sort +which made him throw his hands up into the air when his father, +at their last interview, was goading him on to propose to Miss +Turner. On the other hand, he had no means of supporting himself, +and his father, who was by all accounts a very hard man, would +have thrown him over utterly had he known the truth. It was with +his barmaid wife that he had spent the last three days in +Bristol, and his father did not know where he was. Mark that +point. It is of importance. Good has come out of evil, however, +for the barmaid, finding from the papers that he is in serious +trouble and likely to be hanged, has thrown him over utterly and +has written to him to say that she has a husband already in the +Bermuda Dockyard, so that there is really no tie between them. I +think that that bit of news has consoled young McCarthy for all +that he has suffered." + +"But if he is innocent, who has done it?" + +"Ah! who? I would call your attention very particularly to two +points. One is that the murdered man had an appointment with +someone at the pool, and that the someone could not have been his +son, for his son was away, and he did not know when he would +return. The second is that the murdered man was heard to cry +'Cooee!' before he knew that his son had returned. Those are the +crucial points upon which the case depends. And now let us talk +about George Meredith, if you please, and we shall leave all +minor matters until to-morrow." + +There was no rain, as Holmes had foretold, and the morning broke +bright and cloudless. At nine o'clock Lestrade called for us with +the carriage, and we set off for Hatherley Farm and the Boscombe +Pool. + +"There is serious news this morning," Lestrade observed. "It is +said that Mr. Turner, of the Hall, is so ill that his life is +despaired of." + +"An elderly man, I presume?" said Holmes. + +"About sixty; but his constitution has been shattered by his life +abroad, and he has been in failing health for some time. This +business has had a very bad effect upon him. He was an old friend +of McCarthy's, and, I may add, a great benefactor to him, for I +have learned that he gave him Hatherley Farm rent free." + +"Indeed! That is interesting," said Holmes. + +"Oh, yes! In a hundred other ways he has helped him. Everybody +about here speaks of his kindness to him." + +"Really! Does it not strike you as a little singular that this +McCarthy, who appears to have had little of his own, and to have +been under such obligations to Turner, should still talk of +marrying his son to Turner's daughter, who is, presumably, +heiress to the estate, and that in such a very cocksure manner, +as if it were merely a case of a proposal and all else would +follow? It is the more strange, since we know that Turner himself +was averse to the idea. The daughter told us as much. Do you not +deduce something from that?" + +"We have got to the deductions and the inferences," said +Lestrade, winking at me. "I find it hard enough to tackle facts, +Holmes, without flying away after theories and fancies." + +"You are right," said Holmes demurely; "you do find it very hard +to tackle the facts." + +"Anyhow, I have grasped one fact which you seem to find it +difficult to get hold of," replied Lestrade with some warmth. + +"And that is--" + +"That McCarthy senior met his death from McCarthy junior and that +all theories to the contrary are the merest moonshine." + +"Well, moonshine is a brighter thing than fog," said Holmes, +laughing. "But I am very much mistaken if this is not Hatherley +Farm upon the left." + +"Yes, that is it." It was a widespread, comfortable-looking +building, two-storied, slate-roofed, with great yellow blotches +of lichen upon the grey walls. The drawn blinds and the smokeless +chimneys, however, gave it a stricken look, as though the weight +of this horror still lay heavy upon it. We called at the door, +when the maid, at Holmes' request, showed us the boots which her +master wore at the time of his death, and also a pair of the +son's, though not the pair which he had then had. Having measured +these very carefully from seven or eight different points, Holmes +desired to be led to the court-yard, from which we all followed +the winding track which led to Boscombe Pool. + +Sherlock Holmes was transformed when he was hot upon such a scent +as this. Men who had only known the quiet thinker and logician of +Baker Street would have failed to recognise him. His face flushed +and darkened. His brows were drawn into two hard black lines, +while his eyes shone out from beneath them with a steely glitter. +His face was bent downward, his shoulders bowed, his lips +compressed, and the veins stood out like whipcord in his long, +sinewy neck. His nostrils seemed to dilate with a purely animal +lust for the chase, and his mind was so absolutely concentrated +upon the matter before him that a question or remark fell +unheeded upon his ears, or, at the most, only provoked a quick, +impatient snarl in reply. Swiftly and silently he made his way +along the track which ran through the meadows, and so by way of +the woods to the Boscombe Pool. It was damp, marshy ground, as is +all that district, and there were marks of many feet, both upon +the path and amid the short grass which bounded it on either +side. Sometimes Holmes would hurry on, sometimes stop dead, and +once he made quite a little detour into the meadow. Lestrade and +I walked behind him, the detective indifferent and contemptuous, +while I watched my friend with the interest which sprang from the +conviction that every one of his actions was directed towards a +definite end. + +The Boscombe Pool, which is a little reed-girt sheet of water +some fifty yards across, is situated at the boundary between the +Hatherley Farm and the private park of the wealthy Mr. Turner. +Above the woods which lined it upon the farther side we could see +the red, jutting pinnacles which marked the site of the rich +landowner's dwelling. On the Hatherley side of the pool the woods +grew very thick, and there was a narrow belt of sodden grass +twenty paces across between the edge of the trees and the reeds +which lined the lake. Lestrade showed us the exact spot at which +the body had been found, and, indeed, so moist was the ground, +that I could plainly see the traces which had been left by the +fall of the stricken man. To Holmes, as I could see by his eager +face and peering eyes, very many other things were to be read +upon the trampled grass. He ran round, like a dog who is picking +up a scent, and then turned upon my companion. + +"What did you go into the pool for?" he asked. + +"I fished about with a rake. I thought there might be some weapon +or other trace. But how on earth--" + +"Oh, tut, tut! I have no time! That left foot of yours with its +inward twist is all over the place. A mole could trace it, and +there it vanishes among the reeds. Oh, how simple it would all +have been had I been here before they came like a herd of buffalo +and wallowed all over it. Here is where the party with the +lodge-keeper came, and they have covered all tracks for six or +eight feet round the body. But here are three separate tracks of +the same feet." He drew out a lens and lay down upon his +waterproof to have a better view, talking all the time rather to +himself than to us. "These are young McCarthy's feet. Twice he +was walking, and once he ran swiftly, so that the soles are +deeply marked and the heels hardly visible. That bears out his +story. He ran when he saw his father on the ground. Then here are +the father's feet as he paced up and down. What is this, then? It +is the butt-end of the gun as the son stood listening. And this? +Ha, ha! What have we here? Tiptoes! tiptoes! Square, too, quite +unusual boots! They come, they go, they come again--of course +that was for the cloak. Now where did they come from?" He ran up +and down, sometimes losing, sometimes finding the track until we +were well within the edge of the wood and under the shadow of a +great beech, the largest tree in the neighbourhood. Holmes traced +his way to the farther side of this and lay down once more upon +his face with a little cry of satisfaction. For a long time he +remained there, turning over the leaves and dried sticks, +gathering up what seemed to me to be dust into an envelope and +examining with his lens not only the ground but even the bark of +the tree as far as he could reach. A jagged stone was lying among +the moss, and this also he carefully examined and retained. Then +he followed a pathway through the wood until he came to the +highroad, where all traces were lost. + +"It has been a case of considerable interest," he remarked, +returning to his natural manner. "I fancy that this grey house on +the right must be the lodge. I think that I will go in and have a +word with Moran, and perhaps write a little note. Having done +that, we may drive back to our luncheon. You may walk to the cab, +and I shall be with you presently." + +It was about ten minutes before we regained our cab and drove +back into Ross, Holmes still carrying with him the stone which he +had picked up in the wood. + +"This may interest you, Lestrade," he remarked, holding it out. +"The murder was done with it." + +"I see no marks." + +"There are none." + +"How do you know, then?" + +"The grass was growing under it. It had only lain there a few +days. There was no sign of a place whence it had been taken. It +corresponds with the injuries. There is no sign of any other +weapon." + +"And the murderer?" + +"Is a tall man, left-handed, limps with the right leg, wears +thick-soled shooting-boots and a grey cloak, smokes Indian +cigars, uses a cigar-holder, and carries a blunt pen-knife in his +pocket. There are several other indications, but these may be +enough to aid us in our search." + +Lestrade laughed. "I am afraid that I am still a sceptic," he +said. "Theories are all very well, but we have to deal with a +hard-headed British jury." + +"Nous verrons," answered Holmes calmly. "You work your own +method, and I shall work mine. I shall be busy this afternoon, +and shall probably return to London by the evening train." + +"And leave your case unfinished?" + +"No, finished." + +"But the mystery?" + +"It is solved." + +"Who was the criminal, then?" + +"The gentleman I describe." + +"But who is he?" + +"Surely it would not be difficult to find out. This is not such a +populous neighbourhood." + +Lestrade shrugged his shoulders. "I am a practical man," he said, +"and I really cannot undertake to go about the country looking +for a left-handed gentleman with a game leg. I should become the +laughing-stock of Scotland Yard." + +"All right," said Holmes quietly. "I have given you the chance. +Here are your lodgings. Good-bye. I shall drop you a line before +I leave." + +Having left Lestrade at his rooms, we drove to our hotel, where +we found lunch upon the table. Holmes was silent and buried in +thought with a pained expression upon his face, as one who finds +himself in a perplexing position. + +"Look here, Watson," he said when the cloth was cleared "just sit +down in this chair and let me preach to you for a little. I don't +know quite what to do, and I should value your advice. Light a +cigar and let me expound." + + "Pray do so." + +"Well, now, in considering this case there are two points about +young McCarthy's narrative which struck us both instantly, +although they impressed me in his favour and you against him. One +was the fact that his father should, according to his account, +cry 'Cooee!' before seeing him. The other was his singular dying +reference to a rat. He mumbled several words, you understand, but +that was all that caught the son's ear. Now from this double +point our research must commence, and we will begin it by +presuming that what the lad says is absolutely true." + +"What of this 'Cooee!' then?" + +"Well, obviously it could not have been meant for the son. The +son, as far as he knew, was in Bristol. It was mere chance that +he was within earshot. The 'Cooee!' was meant to attract the +attention of whoever it was that he had the appointment with. But +'Cooee' is a distinctly Australian cry, and one which is used +between Australians. There is a strong presumption that the +person whom McCarthy expected to meet him at Boscombe Pool was +someone who had been in Australia." + +"What of the rat, then?" + +Sherlock Holmes took a folded paper from his pocket and flattened +it out on the table. "This is a map of the Colony of Victoria," +he said. "I wired to Bristol for it last night." He put his hand +over part of the map. "What do you read?" + +"ARAT," I read. + +"And now?" He raised his hand. + +"BALLARAT." + +"Quite so. That was the word the man uttered, and of which his +son only caught the last two syllables. He was trying to utter +the name of his murderer. So and so, of Ballarat." + +"It is wonderful!" I exclaimed. + +"It is obvious. And now, you see, I had narrowed the field down +considerably. The possession of a grey garment was a third point +which, granting the son's statement to be correct, was a +certainty. We have come now out of mere vagueness to the definite +conception of an Australian from Ballarat with a grey cloak." + +"Certainly." + +"And one who was at home in the district, for the pool can only +be approached by the farm or by the estate, where strangers could +hardly wander." + +"Quite so." + +"Then comes our expedition of to-day. By an examination of the +ground I gained the trifling details which I gave to that +imbecile Lestrade, as to the personality of the criminal." + +"But how did you gain them?" + +"You know my method. It is founded upon the observation of +trifles." + +"His height I know that you might roughly judge from the length +of his stride. His boots, too, might be told from their traces." + +"Yes, they were peculiar boots." + +"But his lameness?" + +"The impression of his right foot was always less distinct than +his left. He put less weight upon it. Why? Because he limped--he +was lame." + +"But his left-handedness." + +"You were yourself struck by the nature of the injury as recorded +by the surgeon at the inquest. The blow was struck from +immediately behind, and yet was upon the left side. Now, how can +that be unless it were by a left-handed man? He had stood behind +that tree during the interview between the father and son. He had +even smoked there. I found the ash of a cigar, which my special +knowledge of tobacco ashes enables me to pronounce as an Indian +cigar. I have, as you know, devoted some attention to this, and +written a little monograph on the ashes of 140 different +varieties of pipe, cigar, and cigarette tobacco. Having found the +ash, I then looked round and discovered the stump among the moss +where he had tossed it. It was an Indian cigar, of the variety +which are rolled in Rotterdam." + +"And the cigar-holder?" + +"I could see that the end had not been in his mouth. Therefore he +used a holder. The tip had been cut off, not bitten off, but the +cut was not a clean one, so I deduced a blunt pen-knife." + +"Holmes," I said, "you have drawn a net round this man from which +he cannot escape, and you have saved an innocent human life as +truly as if you had cut the cord which was hanging him. I see the +direction in which all this points. The culprit is--" + +"Mr. John Turner," cried the hotel waiter, opening the door of +our sitting-room, and ushering in a visitor. + +The man who entered was a strange and impressive figure. His +slow, limping step and bowed shoulders gave the appearance of +decrepitude, and yet his hard, deep-lined, craggy features, and +his enormous limbs showed that he was possessed of unusual +strength of body and of character. His tangled beard, grizzled +hair, and outstanding, drooping eyebrows combined to give an air +of dignity and power to his appearance, but his face was of an +ashen white, while his lips and the corners of his nostrils were +tinged with a shade of blue. It was clear to me at a glance that +he was in the grip of some deadly and chronic disease. + +"Pray sit down on the sofa," said Holmes gently. "You had my +note?" + +"Yes, the lodge-keeper brought it up. You said that you wished to +see me here to avoid scandal." + +"I thought people would talk if I went to the Hall." + +"And why did you wish to see me?" He looked across at my +companion with despair in his weary eyes, as though his question +was already answered. + +"Yes," said Holmes, answering the look rather than the words. "It +is so. I know all about McCarthy." + +The old man sank his face in his hands. "God help me!" he cried. +"But I would not have let the young man come to harm. I give you +my word that I would have spoken out if it went against him at +the Assizes." + +"I am glad to hear you say so," said Holmes gravely. + +"I would have spoken now had it not been for my dear girl. It +would break her heart--it will break her heart when she hears +that I am arrested." + +"It may not come to that," said Holmes. + +"What?" + +"I am no official agent. I understand that it was your daughter +who required my presence here, and I am acting in her interests. +Young McCarthy must be got off, however." + +"I am a dying man," said old Turner. "I have had diabetes for +years. My doctor says it is a question whether I shall live a +month. Yet I would rather die under my own roof than in a gaol." + +Holmes rose and sat down at the table with his pen in his hand +and a bundle of paper before him. "Just tell us the truth," he +said. "I shall jot down the facts. You will sign it, and Watson +here can witness it. Then I could produce your confession at the +last extremity to save young McCarthy. I promise you that I shall +not use it unless it is absolutely needed." + +"It's as well," said the old man; "it's a question whether I +shall live to the Assizes, so it matters little to me, but I +should wish to spare Alice the shock. And now I will make the +thing clear to you; it has been a long time in the acting, but +will not take me long to tell. + +"You didn't know this dead man, McCarthy. He was a devil +incarnate. I tell you that. God keep you out of the clutches of +such a man as he. His grip has been upon me these twenty years, +and he has blasted my life. I'll tell you first how I came to be +in his power. + +"It was in the early '60's at the diggings. I was a young chap +then, hot-blooded and reckless, ready to turn my hand at +anything; I got among bad companions, took to drink, had no luck +with my claim, took to the bush, and in a word became what you +would call over here a highway robber. There were six of us, and +we had a wild, free life of it, sticking up a station from time +to time, or stopping the wagons on the road to the diggings. +Black Jack of Ballarat was the name I went under, and our party +is still remembered in the colony as the Ballarat Gang. + +"One day a gold convoy came down from Ballarat to Melbourne, and +we lay in wait for it and attacked it. There were six troopers +and six of us, so it was a close thing, but we emptied four of +their saddles at the first volley. Three of our boys were killed, +however, before we got the swag. I put my pistol to the head of +the wagon-driver, who was this very man McCarthy. I wish to the +Lord that I had shot him then, but I spared him, though I saw his +wicked little eyes fixed on my face, as though to remember every +feature. We got away with the gold, became wealthy men, and made +our way over to England without being suspected. There I parted +from my old pals and determined to settle down to a quiet and +respectable life. I bought this estate, which chanced to be in +the market, and I set myself to do a little good with my money, +to make up for the way in which I had earned it. I married, too, +and though my wife died young she left me my dear little Alice. +Even when she was just a baby her wee hand seemed to lead me down +the right path as nothing else had ever done. In a word, I turned +over a new leaf and did my best to make up for the past. All was +going well when McCarthy laid his grip upon me. + +"I had gone up to town about an investment, and I met him in +Regent Street with hardly a coat to his back or a boot to his +foot. + +"'Here we are, Jack,' says he, touching me on the arm; 'we'll be +as good as a family to you. There's two of us, me and my son, and +you can have the keeping of us. If you don't--it's a fine, +law-abiding country is England, and there's always a policeman +within hail.' + +"Well, down they came to the west country, there was no shaking +them off, and there they have lived rent free on my best land +ever since. There was no rest for me, no peace, no forgetfulness; +turn where I would, there was his cunning, grinning face at my +elbow. It grew worse as Alice grew up, for he soon saw I was more +afraid of her knowing my past than of the police. Whatever he +wanted he must have, and whatever it was I gave him without +question, land, money, houses, until at last he asked a thing +which I could not give. He asked for Alice. + +"His son, you see, had grown up, and so had my girl, and as I was +known to be in weak health, it seemed a fine stroke to him that +his lad should step into the whole property. But there I was +firm. I would not have his cursed stock mixed with mine; not that +I had any dislike to the lad, but his blood was in him, and that +was enough. I stood firm. McCarthy threatened. I braved him to do +his worst. We were to meet at the pool midway between our houses +to talk it over. + +"When I went down there I found him talking with his son, so I +smoked a cigar and waited behind a tree until he should be alone. +But as I listened to his talk all that was black and bitter in +me seemed to come uppermost. He was urging his son to marry my +daughter with as little regard for what she might think as if she +were a slut from off the streets. It drove me mad to think that I +and all that I held most dear should be in the power of such a +man as this. Could I not snap the bond? I was already a dying and +a desperate man. Though clear of mind and fairly strong of limb, +I knew that my own fate was sealed. But my memory and my girl! +Both could be saved if I could but silence that foul tongue. I +did it, Mr. Holmes. I would do it again. Deeply as I have sinned, +I have led a life of martyrdom to atone for it. But that my girl +should be entangled in the same meshes which held me was more +than I could suffer. I struck him down with no more compunction +than if he had been some foul and venomous beast. His cry brought +back his son; but I had gained the cover of the wood, though I +was forced to go back to fetch the cloak which I had dropped in +my flight. That is the true story, gentlemen, of all that +occurred." + +"Well, it is not for me to judge you," said Holmes as the old man +signed the statement which had been drawn out. "I pray that we +may never be exposed to such a temptation." + +"I pray not, sir. And what do you intend to do?" + +"In view of your health, nothing. You are yourself aware that you +will soon have to answer for your deed at a higher court than the +Assizes. I will keep your confession, and if McCarthy is +condemned I shall be forced to use it. If not, it shall never be +seen by mortal eye; and your secret, whether you be alive or +dead, shall be safe with us." + +"Farewell, then," said the old man solemnly. "Your own deathbeds, +when they come, will be the easier for the thought of the peace +which you have given to mine." Tottering and shaking in all his +giant frame, he stumbled slowly from the room. + +"God help us!" said Holmes after a long silence. "Why does fate +play such tricks with poor, helpless worms? I never hear of such +a case as this that I do not think of Baxter's words, and say, +'There, but for the grace of God, goes Sherlock Holmes.'" + +James McCarthy was acquitted at the Assizes on the strength of a +number of objections which had been drawn out by Holmes and +submitted to the defending counsel. Old Turner lived for seven +months after our interview, but he is now dead; and there is +every prospect that the son and daughter may come to live happily +together in ignorance of the black cloud which rests upon their +past. + + + +ADVENTURE V. THE FIVE ORANGE PIPS + +When I glance over my notes and records of the Sherlock Holmes +cases between the years '82 and '90, I am faced by so many which +present strange and interesting features that it is no easy +matter to know which to choose and which to leave. Some, however, +have already gained publicity through the papers, and others have +not offered a field for those peculiar qualities which my friend +possessed in so high a degree, and which it is the object of +these papers to illustrate. Some, too, have baffled his +analytical skill, and would be, as narratives, beginnings without +an ending, while others have been but partially cleared up, and +have their explanations founded rather upon conjecture and +surmise than on that absolute logical proof which was so dear to +him. There is, however, one of these last which was so remarkable +in its details and so startling in its results that I am tempted +to give some account of it in spite of the fact that there are +points in connection with it which never have been, and probably +never will be, entirely cleared up. + +The year '87 furnished us with a long series of cases of greater +or less interest, of which I retain the records. Among my +headings under this one twelve months I find an account of the +adventure of the Paradol Chamber, of the Amateur Mendicant +Society, who held a luxurious club in the lower vault of a +furniture warehouse, of the facts connected with the loss of the +British barque "Sophy Anderson", of the singular adventures of the +Grice Patersons in the island of Uffa, and finally of the +Camberwell poisoning case. In the latter, as may be remembered, +Sherlock Holmes was able, by winding up the dead man's watch, to +prove that it had been wound up two hours before, and that +therefore the deceased had gone to bed within that time--a +deduction which was of the greatest importance in clearing up the +case. All these I may sketch out at some future date, but none of +them present such singular features as the strange train of +circumstances which I have now taken up my pen to describe. + +It was in the latter days of September, and the equinoctial gales +had set in with exceptional violence. All day the wind had +screamed and the rain had beaten against the windows, so that +even here in the heart of great, hand-made London we were forced +to raise our minds for the instant from the routine of life and +to recognise the presence of those great elemental forces which +shriek at mankind through the bars of his civilisation, like +untamed beasts in a cage. As evening drew in, the storm grew +higher and louder, and the wind cried and sobbed like a child in +the chimney. Sherlock Holmes sat moodily at one side of the +fireplace cross-indexing his records of crime, while I at the +other was deep in one of Clark Russell's fine sea-stories until +the howl of the gale from without seemed to blend with the text, +and the splash of the rain to lengthen out into the long swash of +the sea waves. My wife was on a visit to her mother's, and for a +few days I was a dweller once more in my old quarters at Baker +Street. + +"Why," said I, glancing up at my companion, "that was surely the +bell. Who could come to-night? Some friend of yours, perhaps?" + +"Except yourself I have none," he answered. "I do not encourage +visitors." + +"A client, then?" + +"If so, it is a serious case. Nothing less would bring a man out +on such a day and at such an hour. But I take it that it is more +likely to be some crony of the landlady's." + +Sherlock Holmes was wrong in his conjecture, however, for there +came a step in the passage and a tapping at the door. He +stretched out his long arm to turn the lamp away from himself and +towards the vacant chair upon which a newcomer must sit. + +"Come in!" said he. + +The man who entered was young, some two-and-twenty at the +outside, well-groomed and trimly clad, with something of +refinement and delicacy in his bearing. The streaming umbrella +which he held in his hand, and his long shining waterproof told +of the fierce weather through which he had come. He looked about +him anxiously in the glare of the lamp, and I could see that his +face was pale and his eyes heavy, like those of a man who is +weighed down with some great anxiety. + +"I owe you an apology," he said, raising his golden pince-nez to +his eyes. "I trust that I am not intruding. I fear that I have +brought some traces of the storm and rain into your snug +chamber." + +"Give me your coat and umbrella," said Holmes. "They may rest +here on the hook and will be dry presently. You have come up from +the south-west, I see." + +"Yes, from Horsham." + +"That clay and chalk mixture which I see upon your toe caps is +quite distinctive." + +"I have come for advice." + +"That is easily got." + +"And help." + +"That is not always so easy." + +"I have heard of you, Mr. Holmes. I heard from Major Prendergast +how you saved him in the Tankerville Club scandal." + +"Ah, of course. He was wrongfully accused of cheating at cards." + +"He said that you could solve anything." + +"He said too much." + +"That you are never beaten." + +"I have been beaten four times--three times by men, and once by a +woman." + +"But what is that compared with the number of your successes?" + +"It is true that I have been generally successful." + +"Then you may be so with me." + +"I beg that you will draw your chair up to the fire and favour me +with some details as to your case." + +"It is no ordinary one." + +"None of those which come to me are. I am the last court of +appeal." + +"And yet I question, sir, whether, in all your experience, you +have ever listened to a more mysterious and inexplicable chain of +events than those which have happened in my own family." + +"You fill me with interest," said Holmes. "Pray give us the +essential facts from the commencement, and I can afterwards +question you as to those details which seem to me to be most +important." + +The young man pulled his chair up and pushed his wet feet out +towards the blaze. + +"My name," said he, "is John Openshaw, but my own affairs have, +as far as I can understand, little to do with this awful +business. It is a hereditary matter; so in order to give you an +idea of the facts, I must go back to the commencement of the +affair. + +"You must know that my grandfather had two sons--my uncle Elias +and my father Joseph. My father had a small factory at Coventry, +which he enlarged at the time of the invention of bicycling. He +was a patentee of the Openshaw unbreakable tire, and his business +met with such success that he was able to sell it and to retire +upon a handsome competence. + +"My uncle Elias emigrated to America when he was a young man and +became a planter in Florida, where he was reported to have done +very well. At the time of the war he fought in Jackson's army, +and afterwards under Hood, where he rose to be a colonel. When +Lee laid down his arms my uncle returned to his plantation, where +he remained for three or four years. About 1869 or 1870 he came +back to Europe and took a small estate in Sussex, near Horsham. +He had made a very considerable fortune in the States, and his +reason for leaving them was his aversion to the negroes, and his +dislike of the Republican policy in extending the franchise to +them. He was a singular man, fierce and quick-tempered, very +foul-mouthed when he was angry, and of a most retiring +disposition. During all the years that he lived at Horsham, I +doubt if ever he set foot in the town. He had a garden and two or +three fields round his house, and there he would take his +exercise, though very often for weeks on end he would never leave +his room. He drank a great deal of brandy and smoked very +heavily, but he would see no society and did not want any +friends, not even his own brother. + +"He didn't mind me; in fact, he took a fancy to me, for at the +time when he saw me first I was a youngster of twelve or so. This +would be in the year 1878, after he had been eight or nine years +in England. He begged my father to let me live with him and he +was very kind to me in his way. When he was sober he used to be +fond of playing backgammon and draughts with me, and he would +make me his representative both with the servants and with the +tradespeople, so that by the time that I was sixteen I was quite +master of the house. I kept all the keys and could go where I +liked and do what I liked, so long as I did not disturb him in +his privacy. There was one singular exception, however, for he +had a single room, a lumber-room up among the attics, which was +invariably locked, and which he would never permit either me or +anyone else to enter. With a boy's curiosity I have peeped +through the keyhole, but I was never able to see more than such a +collection of old trunks and bundles as would be expected in such +a room. + +"One day--it was in March, 1883--a letter with a foreign stamp +lay upon the table in front of the colonel's plate. It was not a +common thing for him to receive letters, for his bills were all +paid in ready money, and he had no friends of any sort. 'From +India!' said he as he took it up, 'Pondicherry postmark! What can +this be?' Opening it hurriedly, out there jumped five little +dried orange pips, which pattered down upon his plate. I began to +laugh at this, but the laugh was struck from my lips at the sight +of his face. His lip had fallen, his eyes were protruding, his +skin the colour of putty, and he glared at the envelope which he +still held in his trembling hand, 'K. K. K.!' he shrieked, and +then, 'My God, my God, my sins have overtaken me!' + +"'What is it, uncle?' I cried. + +"'Death,' said he, and rising from the table he retired to his +room, leaving me palpitating with horror. I took up the envelope +and saw scrawled in red ink upon the inner flap, just above the +gum, the letter K three times repeated. There was nothing else +save the five dried pips. What could be the reason of his +overpowering terror? I left the breakfast-table, and as I +ascended the stair I met him coming down with an old rusty key, +which must have belonged to the attic, in one hand, and a small +brass box, like a cashbox, in the other. + +"'They may do what they like, but I'll checkmate them still,' +said he with an oath. 'Tell Mary that I shall want a fire in my +room to-day, and send down to Fordham, the Horsham lawyer.' + +"I did as he ordered, and when the lawyer arrived I was asked to +step up to the room. The fire was burning brightly, and in the +grate there was a mass of black, fluffy ashes, as of burned +paper, while the brass box stood open and empty beside it. As I +glanced at the box I noticed, with a start, that upon the lid was +printed the treble K which I had read in the morning upon the +envelope. + +"'I wish you, John,' said my uncle, 'to witness my will. I leave +my estate, with all its advantages and all its disadvantages, to +my brother, your father, whence it will, no doubt, descend to +you. If you can enjoy it in peace, well and good! If you find you +cannot, take my advice, my boy, and leave it to your deadliest +enemy. I am sorry to give you such a two-edged thing, but I can't +say what turn things are going to take. Kindly sign the paper +where Mr. Fordham shows you.' + +"I signed the paper as directed, and the lawyer took it away with +him. The singular incident made, as you may think, the deepest +impression upon me, and I pondered over it and turned it every +way in my mind without being able to make anything of it. Yet I +could not shake off the vague feeling of dread which it left +behind, though the sensation grew less keen as the weeks passed +and nothing happened to disturb the usual routine of our lives. I +could see a change in my uncle, however. He drank more than ever, +and he was less inclined for any sort of society. Most of his +time he would spend in his room, with the door locked upon the +inside, but sometimes he would emerge in a sort of drunken frenzy +and would burst out of the house and tear about the garden with a +revolver in his hand, screaming out that he was afraid of no man, +and that he was not to be cooped up, like a sheep in a pen, by +man or devil. When these hot fits were over, however, he would +rush tumultuously in at the door and lock and bar it behind him, +like a man who can brazen it out no longer against the terror +which lies at the roots of his soul. At such times I have seen +his face, even on a cold day, glisten with moisture, as though it +were new raised from a basin. + +"Well, to come to an end of the matter, Mr. Holmes, and not to +abuse your patience, there came a night when he made one of those +drunken sallies from which he never came back. We found him, when +we went to search for him, face downward in a little +green-scummed pool, which lay at the foot of the garden. There +was no sign of any violence, and the water was but two feet deep, +so that the jury, having regard to his known eccentricity, +brought in a verdict of 'suicide.' But I, who knew how he winced +from the very thought of death, had much ado to persuade myself +that he had gone out of his way to meet it. The matter passed, +however, and my father entered into possession of the estate, and +of some 14,000 pounds, which lay to his credit at the bank." + +"One moment," Holmes interposed, "your statement is, I foresee, +one of the most remarkable to which I have ever listened. Let me +have the date of the reception by your uncle of the letter, and +the date of his supposed suicide." + +"The letter arrived on March 10, 1883. His death was seven weeks +later, upon the night of May 2nd." + +"Thank you. Pray proceed." + +"When my father took over the Horsham property, he, at my +request, made a careful examination of the attic, which had been +always locked up. We found the brass box there, although its +contents had been destroyed. On the inside of the cover was a +paper label, with the initials of K. K. K. repeated upon it, and +'Letters, memoranda, receipts, and a register' written beneath. +These, we presume, indicated the nature of the papers which had +been destroyed by Colonel Openshaw. For the rest, there was +nothing of much importance in the attic save a great many +scattered papers and note-books bearing upon my uncle's life in +America. Some of them were of the war time and showed that he had +done his duty well and had borne the repute of a brave soldier. +Others were of a date during the reconstruction of the Southern +states, and were mostly concerned with politics, for he had +evidently taken a strong part in opposing the carpet-bag +politicians who had been sent down from the North. + +"Well, it was the beginning of '84 when my father came to live at +Horsham, and all went as well as possible with us until the +January of '85. On the fourth day after the new year I heard my +father give a sharp cry of surprise as we sat together at the +breakfast-table. There he was, sitting with a newly opened +envelope in one hand and five dried orange pips in the +outstretched palm of the other one. He had always laughed at what +he called my cock-and-bull story about the colonel, but he looked +very scared and puzzled now that the same thing had come upon +himself. + +"'Why, what on earth does this mean, John?' he stammered. + +"My heart had turned to lead. 'It is K. K. K.,' said I. + +"He looked inside the envelope. 'So it is,' he cried. 'Here are +the very letters. But what is this written above them?' + +"'Put the papers on the sundial,' I read, peeping over his +shoulder. + +"'What papers? What sundial?' he asked. + +"'The sundial in the garden. There is no other,' said I; 'but the +papers must be those that are destroyed.' + +"'Pooh!' said he, gripping hard at his courage. 'We are in a +civilised land here, and we can't have tomfoolery of this kind. +Where does the thing come from?' + +"'From Dundee,' I answered, glancing at the postmark. + +"'Some preposterous practical joke,' said he. 'What have I to do +with sundials and papers? I shall take no notice of such +nonsense.' + +"'I should certainly speak to the police,' I said. + +"'And be laughed at for my pains. Nothing of the sort.' + +"'Then let me do so?' + +"'No, I forbid you. I won't have a fuss made about such +nonsense.' + +"It was in vain to argue with him, for he was a very obstinate +man. I went about, however, with a heart which was full of +forebodings. + +"On the third day after the coming of the letter my father went +from home to visit an old friend of his, Major Freebody, who is +in command of one of the forts upon Portsdown Hill. I was glad +that he should go, for it seemed to me that he was farther from +danger when he was away from home. In that, however, I was in +error. Upon the second day of his absence I received a telegram +from the major, imploring me to come at once. My father had +fallen over one of the deep chalk-pits which abound in the +neighbourhood, and was lying senseless, with a shattered skull. I +hurried to him, but he passed away without having ever recovered +his consciousness. He had, as it appears, been returning from +Fareham in the twilight, and as the country was unknown to him, +and the chalk-pit unfenced, the jury had no hesitation in +bringing in a verdict of 'death from accidental causes.' +Carefully as I examined every fact connected with his death, I +was unable to find anything which could suggest the idea of +murder. There were no signs of violence, no footmarks, no +robbery, no record of strangers having been seen upon the roads. +And yet I need not tell you that my mind was far from at ease, +and that I was well-nigh certain that some foul plot had been +woven round him. + +"In this sinister way I came into my inheritance. You will ask me +why I did not dispose of it? I answer, because I was well +convinced that our troubles were in some way dependent upon an +incident in my uncle's life, and that the danger would be as +pressing in one house as in another. + +"It was in January, '85, that my poor father met his end, and two +years and eight months have elapsed since then. During that time +I have lived happily at Horsham, and I had begun to hope that +this curse had passed away from the family, and that it had ended +with the last generation. I had begun to take comfort too soon, +however; yesterday morning the blow fell in the very shape in +which it had come upon my father." + +The young man took from his waistcoat a crumpled envelope, and +turning to the table he shook out upon it five little dried +orange pips. + +"This is the envelope," he continued. "The postmark is +London--eastern division. Within are the very words which were +upon my father's last message: 'K. K. K.'; and then 'Put the +papers on the sundial.'" + +"What have you done?" asked Holmes. + +"Nothing." + +"Nothing?" + +"To tell the truth"--he sank his face into his thin, white +hands--"I have felt helpless. I have felt like one of those poor +rabbits when the snake is writhing towards it. I seem to be in +the grasp of some resistless, inexorable evil, which no foresight +and no precautions can guard against." + +"Tut! tut!" cried Sherlock Holmes. "You must act, man, or you are +lost. Nothing but energy can save you. This is no time for +despair." + +"I have seen the police." + +"Ah!" + +"But they listened to my story with a smile. I am convinced that +the inspector has formed the opinion that the letters are all +practical jokes, and that the deaths of my relations were really +accidents, as the jury stated, and were not to be connected with +the warnings." + +Holmes shook his clenched hands in the air. "Incredible +imbecility!" he cried. + +"They have, however, allowed me a policeman, who may remain in +the house with me." + +"Has he come with you to-night?" + +"No. His orders were to stay in the house." + +Again Holmes raved in the air. + +"Why did you come to me," he cried, "and, above all, why did you +not come at once?" + +"I did not know. It was only to-day that I spoke to Major +Prendergast about my troubles and was advised by him to come to +you." + +"It is really two days since you had the letter. We should have +acted before this. You have no further evidence, I suppose, than +that which you have placed before us--no suggestive detail which +might help us?" + +"There is one thing," said John Openshaw. He rummaged in his coat +pocket, and, drawing out a piece of discoloured, blue-tinted +paper, he laid it out upon the table. "I have some remembrance," +said he, "that on the day when my uncle burned the papers I +observed that the small, unburned margins which lay amid the +ashes were of this particular colour. I found this single sheet +upon the floor of his room, and I am inclined to think that it +may be one of the papers which has, perhaps, fluttered out from +among the others, and in that way has escaped destruction. Beyond +the mention of pips, I do not see that it helps us much. I think +myself that it is a page from some private diary. The writing is +undoubtedly my uncle's." + +Holmes moved the lamp, and we both bent over the sheet of paper, +which showed by its ragged edge that it had indeed been torn from +a book. It was headed, "March, 1869," and beneath were the +following enigmatical notices: + +"4th. Hudson came. Same old platform. + +"7th. Set the pips on McCauley, Paramore, and + John Swain, of St. Augustine. + +"9th. McCauley cleared. + +"10th. John Swain cleared. + +"12th. Visited Paramore. All well." + +"Thank you!" said Holmes, folding up the paper and returning it +to our visitor. "And now you must on no account lose another +instant. We cannot spare time even to discuss what you have told +me. You must get home instantly and act." + +"What shall I do?" + +"There is but one thing to do. It must be done at once. You must +put this piece of paper which you have shown us into the brass +box which you have described. You must also put in a note to say +that all the other papers were burned by your uncle, and that +this is the only one which remains. You must assert that in such +words as will carry conviction with them. Having done this, you +must at once put the box out upon the sundial, as directed. Do +you understand?" + +"Entirely." + +"Do not think of revenge, or anything of the sort, at present. I +think that we may gain that by means of the law; but we have our +web to weave, while theirs is already woven. The first +consideration is to remove the pressing danger which threatens +you. The second is to clear up the mystery and to punish the +guilty parties." + +"I thank you," said the young man, rising and pulling on his +overcoat. "You have given me fresh life and hope. I shall +certainly do as you advise." + +"Do not lose an instant. And, above all, take care of yourself in +the meanwhile, for I do not think that there can be a doubt that +you are threatened by a very real and imminent danger. How do you +go back?" + +"By train from Waterloo." + +"It is not yet nine. The streets will be crowded, so I trust that +you may be in safety. And yet you cannot guard yourself too +closely." + +"I am armed." + +"That is well. To-morrow I shall set to work upon your case." + +"I shall see you at Horsham, then?" + +"No, your secret lies in London. It is there that I shall seek +it." + +"Then I shall call upon you in a day, or in two days, with news +as to the box and the papers. I shall take your advice in every +particular." He shook hands with us and took his leave. Outside +the wind still screamed and the rain splashed and pattered +against the windows. This strange, wild story seemed to have come +to us from amid the mad elements--blown in upon us like a sheet +of sea-weed in a gale--and now to have been reabsorbed by them +once more. + +Sherlock Holmes sat for some time in silence, with his head sunk +forward and his eyes bent upon the red glow of the fire. Then he +lit his pipe, and leaning back in his chair he watched the blue +smoke-rings as they chased each other up to the ceiling. + +"I think, Watson," he remarked at last, "that of all our cases we +have had none more fantastic than this." + +"Save, perhaps, the Sign of Four." + +"Well, yes. Save, perhaps, that. And yet this John Openshaw seems +to me to be walking amid even greater perils than did the +Sholtos." + +"But have you," I asked, "formed any definite conception as to +what these perils are?" + +"There can be no question as to their nature," he answered. + +"Then what are they? Who is this K. K. K., and why does he pursue +this unhappy family?" + +Sherlock Holmes closed his eyes and placed his elbows upon the +arms of his chair, with his finger-tips together. "The ideal +reasoner," he remarked, "would, when he had once been shown a +single fact in all its bearings, deduce from it not only all the +chain of events which led up to it but also all the results which +would follow from it. As Cuvier could correctly describe a whole +animal by the contemplation of a single bone, so the observer who +has thoroughly understood one link in a series of incidents +should be able to accurately state all the other ones, both +before and after. We have not yet grasped the results which the +reason alone can attain to. Problems may be solved in the study +which have baffled all those who have sought a solution by the +aid of their senses. To carry the art, however, to its highest +pitch, it is necessary that the reasoner should be able to +utilise all the facts which have come to his knowledge; and this +in itself implies, as you will readily see, a possession of all +knowledge, which, even in these days of free education and +encyclopaedias, is a somewhat rare accomplishment. It is not so +impossible, however, that a man should possess all knowledge +which is likely to be useful to him in his work, and this I have +endeavoured in my case to do. If I remember rightly, you on one +occasion, in the early days of our friendship, defined my limits +in a very precise fashion." + +"Yes," I answered, laughing. "It was a singular document. +Philosophy, astronomy, and politics were marked at zero, I +remember. Botany variable, geology profound as regards the +mud-stains from any region within fifty miles of town, chemistry +eccentric, anatomy unsystematic, sensational literature and crime +records unique, violin-player, boxer, swordsman, lawyer, and +self-poisoner by cocaine and tobacco. Those, I think, were the +main points of my analysis." + +Holmes grinned at the last item. "Well," he said, "I say now, as +I said then, that a man should keep his little brain-attic +stocked with all the furniture that he is likely to use, and the +rest he can put away in the lumber-room of his library, where he +can get it if he wants it. Now, for such a case as the one which +has been submitted to us to-night, we need certainly to muster +all our resources. Kindly hand me down the letter K of the +'American Encyclopaedia' which stands upon the shelf beside you. +Thank you. Now let us consider the situation and see what may be +deduced from it. In the first place, we may start with a strong +presumption that Colonel Openshaw had some very strong reason for +leaving America. Men at his time of life do not change all their +habits and exchange willingly the charming climate of Florida for +the lonely life of an English provincial town. His extreme love +of solitude in England suggests the idea that he was in fear of +someone or something, so we may assume as a working hypothesis +that it was fear of someone or something which drove him from +America. As to what it was he feared, we can only deduce that by +considering the formidable letters which were received by himself +and his successors. Did you remark the postmarks of those +letters?" + +"The first was from Pondicherry, the second from Dundee, and the +third from London." + +"From East London. What do you deduce from that?" + +"They are all seaports. That the writer was on board of a ship." + +"Excellent. We have already a clue. There can be no doubt that +the probability--the strong probability--is that the writer was +on board of a ship. And now let us consider another point. In the +case of Pondicherry, seven weeks elapsed between the threat and +its fulfilment, in Dundee it was only some three or four days. +Does that suggest anything?" + +"A greater distance to travel." + +"But the letter had also a greater distance to come." + +"Then I do not see the point." + +"There is at least a presumption that the vessel in which the man +or men are is a sailing-ship. It looks as if they always send +their singular warning or token before them when starting upon +their mission. You see how quickly the deed followed the sign +when it came from Dundee. If they had come from Pondicherry in a +steamer they would have arrived almost as soon as their letter. +But, as a matter of fact, seven weeks elapsed. I think that those +seven weeks represented the difference between the mail-boat which +brought the letter and the sailing vessel which brought the +writer." + +"It is possible." + +"More than that. It is probable. And now you see the deadly +urgency of this new case, and why I urged young Openshaw to +caution. The blow has always fallen at the end of the time which +it would take the senders to travel the distance. But this one +comes from London, and therefore we cannot count upon delay." + +"Good God!" I cried. "What can it mean, this relentless +persecution?" + +"The papers which Openshaw carried are obviously of vital +importance to the person or persons in the sailing-ship. I think +that it is quite clear that there must be more than one of them. +A single man could not have carried out two deaths in such a way +as to deceive a coroner's jury. There must have been several in +it, and they must have been men of resource and determination. +Their papers they mean to have, be the holder of them who it may. +In this way you see K. K. K. ceases to be the initials of an +individual and becomes the badge of a society." + +"But of what society?" + +"Have you never--" said Sherlock Holmes, bending forward and +sinking his voice--"have you never heard of the Ku Klux Klan?" + +"I never have." + +Holmes turned over the leaves of the book upon his knee. "Here it +is," said he presently: + +"'Ku Klux Klan. A name derived from the fanciful resemblance to +the sound produced by cocking a rifle. This terrible secret +society was formed by some ex-Confederate soldiers in the +Southern states after the Civil War, and it rapidly formed local +branches in different parts of the country, notably in Tennessee, +Louisiana, the Carolinas, Georgia, and Florida. Its power was +used for political purposes, principally for the terrorising of +the negro voters and the murdering and driving from the country +of those who were opposed to its views. Its outrages were usually +preceded by a warning sent to the marked man in some fantastic +but generally recognised shape--a sprig of oak-leaves in some +parts, melon seeds or orange pips in others. On receiving this +the victim might either openly abjure his former ways, or might +fly from the country. If he braved the matter out, death would +unfailingly come upon him, and usually in some strange and +unforeseen manner. So perfect was the organisation of the +society, and so systematic its methods, that there is hardly a +case upon record where any man succeeded in braving it with +impunity, or in which any of its outrages were traced home to the +perpetrators. For some years the organisation flourished in spite +of the efforts of the United States government and of the better +classes of the community in the South. Eventually, in the year +1869, the movement rather suddenly collapsed, although there have +been sporadic outbreaks of the same sort since that date.' + +"You will observe," said Holmes, laying down the volume, "that +the sudden breaking up of the society was coincident with the +disappearance of Openshaw from America with their papers. It may +well have been cause and effect. It is no wonder that he and his +family have some of the more implacable spirits upon their track. +You can understand that this register and diary may implicate +some of the first men in the South, and that there may be many +who will not sleep easy at night until it is recovered." + +"Then the page we have seen--" + +"Is such as we might expect. It ran, if I remember right, 'sent +the pips to A, B, and C'--that is, sent the society's warning to +them. Then there are successive entries that A and B cleared, or +left the country, and finally that C was visited, with, I fear, a +sinister result for C. Well, I think, Doctor, that we may let +some light into this dark place, and I believe that the only +chance young Openshaw has in the meantime is to do what I have +told him. There is nothing more to be said or to be done +to-night, so hand me over my violin and let us try to forget for +half an hour the miserable weather and the still more miserable +ways of our fellow-men." + + +It had cleared in the morning, and the sun was shining with a +subdued brightness through the dim veil which hangs over the +great city. Sherlock Holmes was already at breakfast when I came +down. + +"You will excuse me for not waiting for you," said he; "I have, I +foresee, a very busy day before me in looking into this case of +young Openshaw's." + +"What steps will you take?" I asked. + +"It will very much depend upon the results of my first inquiries. +I may have to go down to Horsham, after all." + +"You will not go there first?" + +"No, I shall commence with the City. Just ring the bell and the +maid will bring up your coffee." + +As I waited, I lifted the unopened newspaper from the table and +glanced my eye over it. It rested upon a heading which sent a +chill to my heart. + +"Holmes," I cried, "you are too late." + +"Ah!" said he, laying down his cup, "I feared as much. How was it +done?" He spoke calmly, but I could see that he was deeply moved. + +"My eye caught the name of Openshaw, and the heading 'Tragedy +Near Waterloo Bridge.' Here is the account: + +"Between nine and ten last night Police-Constable Cook, of the H +Division, on duty near Waterloo Bridge, heard a cry for help and +a splash in the water. The night, however, was extremely dark and +stormy, so that, in spite of the help of several passers-by, it +was quite impossible to effect a rescue. The alarm, however, was +given, and, by the aid of the water-police, the body was +eventually recovered. It proved to be that of a young gentleman +whose name, as it appears from an envelope which was found in his +pocket, was John Openshaw, and whose residence is near Horsham. +It is conjectured that he may have been hurrying down to catch +the last train from Waterloo Station, and that in his haste and +the extreme darkness he missed his path and walked over the edge +of one of the small landing-places for river steamboats. The body +exhibited no traces of violence, and there can be no doubt that +the deceased had been the victim of an unfortunate accident, +which should have the effect of calling the attention of the +authorities to the condition of the riverside landing-stages." + +We sat in silence for some minutes, Holmes more depressed and +shaken than I had ever seen him. + +"That hurts my pride, Watson," he said at last. "It is a petty +feeling, no doubt, but it hurts my pride. It becomes a personal +matter with me now, and, if God sends me health, I shall set my +hand upon this gang. That he should come to me for help, and that +I should send him away to his death--!" He sprang from his chair +and paced about the room in uncontrollable agitation, with a +flush upon his sallow cheeks and a nervous clasping and +unclasping of his long thin hands. + +"They must be cunning devils," he exclaimed at last. "How could +they have decoyed him down there? The Embankment is not on the +direct line to the station. The bridge, no doubt, was too +crowded, even on such a night, for their purpose. Well, Watson, +we shall see who will win in the long run. I am going out now!" + +"To the police?" + +"No; I shall be my own police. When I have spun the web they may +take the flies, but not before." + +All day I was engaged in my professional work, and it was late in +the evening before I returned to Baker Street. Sherlock Holmes +had not come back yet. It was nearly ten o'clock before he +entered, looking pale and worn. He walked up to the sideboard, +and tearing a piece from the loaf he devoured it voraciously, +washing it down with a long draught of water. + +"You are hungry," I remarked. + +"Starving. It had escaped my memory. I have had nothing since +breakfast." + +"Nothing?" + +"Not a bite. I had no time to think of it." + +"And how have you succeeded?" + +"Well." + +"You have a clue?" + +"I have them in the hollow of my hand. Young Openshaw shall not +long remain unavenged. Why, Watson, let us put their own devilish +trade-mark upon them. It is well thought of!" + +"What do you mean?" + +He took an orange from the cupboard, and tearing it to pieces he +squeezed out the pips upon the table. Of these he took five and +thrust them into an envelope. On the inside of the flap he wrote +"S. H. for J. O." Then he sealed it and addressed it to "Captain +James Calhoun, Barque 'Lone Star,' Savannah, Georgia." + +"That will await him when he enters port," said he, chuckling. +"It may give him a sleepless night. He will find it as sure a +precursor of his fate as Openshaw did before him." + +"And who is this Captain Calhoun?" + +"The leader of the gang. I shall have the others, but he first." + +"How did you trace it, then?" + +He took a large sheet of paper from his pocket, all covered with +dates and names. + +"I have spent the whole day," said he, "over Lloyd's registers +and files of the old papers, following the future career of every +vessel which touched at Pondicherry in January and February in +'83. There were thirty-six ships of fair tonnage which were +reported there during those months. Of these, one, the 'Lone Star,' +instantly attracted my attention, since, although it was reported +as having cleared from London, the name is that which is given to +one of the states of the Union." + +"Texas, I think." + +"I was not and am not sure which; but I knew that the ship must +have an American origin." + +"What then?" + +"I searched the Dundee records, and when I found that the barque +'Lone Star' was there in January, '85, my suspicion became a +certainty. I then inquired as to the vessels which lay at present +in the port of London." + +"Yes?" + +"The 'Lone Star' had arrived here last week. I went down to the +Albert Dock and found that she had been taken down the river by +the early tide this morning, homeward bound to Savannah. I wired +to Gravesend and learned that she had passed some time ago, and +as the wind is easterly I have no doubt that she is now past the +Goodwins and not very far from the Isle of Wight." + +"What will you do, then?" + +"Oh, I have my hand upon him. He and the two mates, are as I +learn, the only native-born Americans in the ship. The others are +Finns and Germans. I know, also, that they were all three away +from the ship last night. I had it from the stevedore who has +been loading their cargo. By the time that their sailing-ship +reaches Savannah the mail-boat will have carried this letter, and +the cable will have informed the police of Savannah that these +three gentlemen are badly wanted here upon a charge of murder." + +There is ever a flaw, however, in the best laid of human plans, +and the murderers of John Openshaw were never to receive the +orange pips which would show them that another, as cunning and as +resolute as themselves, was upon their track. Very long and very +severe were the equinoctial gales that year. We waited long for +news of the "Lone Star" of Savannah, but none ever reached us. We +did at last hear that somewhere far out in the Atlantic a +shattered stern-post of a boat was seen swinging in the trough +of a wave, with the letters "L. S." carved upon it, and that is +all which we shall ever know of the fate of the "Lone Star." + + + +ADVENTURE VI. THE MAN WITH THE TWISTED LIP + +Isa Whitney, brother of the late Elias Whitney, D.D., Principal +of the Theological College of St. George's, was much addicted to +opium. The habit grew upon him, as I understand, from some +foolish freak when he was at college; for having read De +Quincey's description of his dreams and sensations, he had +drenched his tobacco with laudanum in an attempt to produce the +same effects. He found, as so many more have done, that the +practice is easier to attain than to get rid of, and for many +years he continued to be a slave to the drug, an object of +mingled horror and pity to his friends and relatives. I can see +him now, with yellow, pasty face, drooping lids, and pin-point +pupils, all huddled in a chair, the wreck and ruin of a noble +man. + +One night--it was in June, '89--there came a ring to my bell, +about the hour when a man gives his first yawn and glances at the +clock. I sat up in my chair, and my wife laid her needle-work +down in her lap and made a little face of disappointment. + +"A patient!" said she. "You'll have to go out." + +I groaned, for I was newly come back from a weary day. + +We heard the door open, a few hurried words, and then quick steps +upon the linoleum. Our own door flew open, and a lady, clad in +some dark-coloured stuff, with a black veil, entered the room. + +"You will excuse my calling so late," she began, and then, +suddenly losing her self-control, she ran forward, threw her arms +about my wife's neck, and sobbed upon her shoulder. "Oh, I'm in +such trouble!" she cried; "I do so want a little help." + +"Why," said my wife, pulling up her veil, "it is Kate Whitney. +How you startled me, Kate! I had not an idea who you were when +you came in." + +"I didn't know what to do, so I came straight to you." That was +always the way. Folk who were in grief came to my wife like birds +to a light-house. + +"It was very sweet of you to come. Now, you must have some wine +and water, and sit here comfortably and tell us all about it. Or +should you rather that I sent James off to bed?" + +"Oh, no, no! I want the doctor's advice and help, too. It's about +Isa. He has not been home for two days. I am so frightened about +him!" + +It was not the first time that she had spoken to us of her +husband's trouble, to me as a doctor, to my wife as an old friend +and school companion. We soothed and comforted her by such words +as we could find. Did she know where her husband was? Was it +possible that we could bring him back to her? + +It seems that it was. She had the surest information that of late +he had, when the fit was on him, made use of an opium den in the +farthest east of the City. Hitherto his orgies had always been +confined to one day, and he had come back, twitching and +shattered, in the evening. But now the spell had been upon him +eight-and-forty hours, and he lay there, doubtless among the +dregs of the docks, breathing in the poison or sleeping off the +effects. There he was to be found, she was sure of it, at the Bar +of Gold, in Upper Swandam Lane. But what was she to do? How could +she, a young and timid woman, make her way into such a place and +pluck her husband out from among the ruffians who surrounded him? + +There was the case, and of course there was but one way out of +it. Might I not escort her to this place? And then, as a second +thought, why should she come at all? I was Isa Whitney's medical +adviser, and as such I had influence over him. I could manage it +better if I were alone. I promised her on my word that I would +send him home in a cab within two hours if he were indeed at the +address which she had given me. And so in ten minutes I had left +my armchair and cheery sitting-room behind me, and was speeding +eastward in a hansom on a strange errand, as it seemed to me at +the time, though the future only could show how strange it was to +be. + +But there was no great difficulty in the first stage of my +adventure. Upper Swandam Lane is a vile alley lurking behind the +high wharves which line the north side of the river to the east +of London Bridge. Between a slop-shop and a gin-shop, approached +by a steep flight of steps leading down to a black gap like the +mouth of a cave, I found the den of which I was in search. +Ordering my cab to wait, I passed down the steps, worn hollow in +the centre by the ceaseless tread of drunken feet; and by the +light of a flickering oil-lamp above the door I found the latch +and made my way into a long, low room, thick and heavy with the +brown opium smoke, and terraced with wooden berths, like the +forecastle of an emigrant ship. + +Through the gloom one could dimly catch a glimpse of bodies lying +in strange fantastic poses, bowed shoulders, bent knees, heads +thrown back, and chins pointing upward, with here and there a +dark, lack-lustre eye turned upon the newcomer. Out of the black +shadows there glimmered little red circles of light, now bright, +now faint, as the burning poison waxed or waned in the bowls of +the metal pipes. The most lay silent, but some muttered to +themselves, and others talked together in a strange, low, +monotonous voice, their conversation coming in gushes, and then +suddenly tailing off into silence, each mumbling out his own +thoughts and paying little heed to the words of his neighbour. At +the farther end was a small brazier of burning charcoal, beside +which on a three-legged wooden stool there sat a tall, thin old +man, with his jaw resting upon his two fists, and his elbows upon +his knees, staring into the fire. + +As I entered, a sallow Malay attendant had hurried up with a pipe +for me and a supply of the drug, beckoning me to an empty berth. + +"Thank you. I have not come to stay," said I. "There is a friend +of mine here, Mr. Isa Whitney, and I wish to speak with him." + +There was a movement and an exclamation from my right, and +peering through the gloom, I saw Whitney, pale, haggard, and +unkempt, staring out at me. + +"My God! It's Watson," said he. He was in a pitiable state of +reaction, with every nerve in a twitter. "I say, Watson, what +o'clock is it?" + +"Nearly eleven." + +"Of what day?" + +"Of Friday, June 19th." + +"Good heavens! I thought it was Wednesday. It is Wednesday. What +d'you want to frighten a chap for?" He sank his face onto his +arms and began to sob in a high treble key. + +"I tell you that it is Friday, man. Your wife has been waiting +this two days for you. You should be ashamed of yourself!" + +"So I am. But you've got mixed, Watson, for I have only been here +a few hours, three pipes, four pipes--I forget how many. But I'll +go home with you. I wouldn't frighten Kate--poor little Kate. +Give me your hand! Have you a cab?" + +"Yes, I have one waiting." + +"Then I shall go in it. But I must owe something. Find what I +owe, Watson. I am all off colour. I can do nothing for myself." + +I walked down the narrow passage between the double row of +sleepers, holding my breath to keep out the vile, stupefying +fumes of the drug, and looking about for the manager. As I passed +the tall man who sat by the brazier I felt a sudden pluck at my +skirt, and a low voice whispered, "Walk past me, and then look +back at me." The words fell quite distinctly upon my ear. I +glanced down. They could only have come from the old man at my +side, and yet he sat now as absorbed as ever, very thin, very +wrinkled, bent with age, an opium pipe dangling down from between +his knees, as though it had dropped in sheer lassitude from his +fingers. I took two steps forward and looked back. It took all my +self-control to prevent me from breaking out into a cry of +astonishment. He had turned his back so that none could see him +but I. His form had filled out, his wrinkles were gone, the dull +eyes had regained their fire, and there, sitting by the fire and +grinning at my surprise, was none other than Sherlock Holmes. He +made a slight motion to me to approach him, and instantly, as he +turned his face half round to the company once more, subsided +into a doddering, loose-lipped senility. + +"Holmes!" I whispered, "what on earth are you doing in this den?" + +"As low as you can," he answered; "I have excellent ears. If you +would have the great kindness to get rid of that sottish friend +of yours I should be exceedingly glad to have a little talk with +you." + +"I have a cab outside." + +"Then pray send him home in it. You may safely trust him, for he +appears to be too limp to get into any mischief. I should +recommend you also to send a note by the cabman to your wife to +say that you have thrown in your lot with me. If you will wait +outside, I shall be with you in five minutes." + +It was difficult to refuse any of Sherlock Holmes' requests, for +they were always so exceedingly definite, and put forward with +such a quiet air of mastery. I felt, however, that when Whitney +was once confined in the cab my mission was practically +accomplished; and for the rest, I could not wish anything better +than to be associated with my friend in one of those singular +adventures which were the normal condition of his existence. In a +few minutes I had written my note, paid Whitney's bill, led him +out to the cab, and seen him driven through the darkness. In a +very short time a decrepit figure had emerged from the opium den, +and I was walking down the street with Sherlock Holmes. For two +streets he shuffled along with a bent back and an uncertain foot. +Then, glancing quickly round, he straightened himself out and +burst into a hearty fit of laughter. + +"I suppose, Watson," said he, "that you imagine that I have added +opium-smoking to cocaine injections, and all the other little +weaknesses on which you have favoured me with your medical +views." + +"I was certainly surprised to find you there." + +"But not more so than I to find you." + +"I came to find a friend." + +"And I to find an enemy." + +"An enemy?" + +"Yes; one of my natural enemies, or, shall I say, my natural +prey. Briefly, Watson, I am in the midst of a very remarkable +inquiry, and I have hoped to find a clue in the incoherent +ramblings of these sots, as I have done before now. Had I been +recognised in that den my life would not have been worth an +hour's purchase; for I have used it before now for my own +purposes, and the rascally Lascar who runs it has sworn to have +vengeance upon me. There is a trap-door at the back of that +building, near the corner of Paul's Wharf, which could tell some +strange tales of what has passed through it upon the moonless +nights." + +"What! You do not mean bodies?" + +"Ay, bodies, Watson. We should be rich men if we had 1000 pounds +for every poor devil who has been done to death in that den. It +is the vilest murder-trap on the whole riverside, and I fear that +Neville St. Clair has entered it never to leave it more. But our +trap should be here." He put his two forefingers between his +teeth and whistled shrilly--a signal which was answered by a +similar whistle from the distance, followed shortly by the rattle +of wheels and the clink of horses' hoofs. + +"Now, Watson," said Holmes, as a tall dog-cart dashed up through +the gloom, throwing out two golden tunnels of yellow light from +its side lanterns. "You'll come with me, won't you?" + +"If I can be of use." + +"Oh, a trusty comrade is always of use; and a chronicler still +more so. My room at The Cedars is a double-bedded one." + +"The Cedars?" + +"Yes; that is Mr. St. Clair's house. I am staying there while I +conduct the inquiry." + +"Where is it, then?" + +"Near Lee, in Kent. We have a seven-mile drive before us." + +"But I am all in the dark." + +"Of course you are. You'll know all about it presently. Jump up +here. All right, John; we shall not need you. Here's half a +crown. Look out for me to-morrow, about eleven. Give her her +head. So long, then!" + +He flicked the horse with his whip, and we dashed away through +the endless succession of sombre and deserted streets, which +widened gradually, until we were flying across a broad +balustraded bridge, with the murky river flowing sluggishly +beneath us. Beyond lay another dull wilderness of bricks and +mortar, its silence broken only by the heavy, regular footfall of +the policeman, or the songs and shouts of some belated party of +revellers. A dull wrack was drifting slowly across the sky, and a +star or two twinkled dimly here and there through the rifts of +the clouds. Holmes drove in silence, with his head sunk upon his +breast, and the air of a man who is lost in thought, while I sat +beside him, curious to learn what this new quest might be which +seemed to tax his powers so sorely, and yet afraid to break in +upon the current of his thoughts. We had driven several miles, +and were beginning to get to the fringe of the belt of suburban +villas, when he shook himself, shrugged his shoulders, and lit up +his pipe with the air of a man who has satisfied himself that he +is acting for the best. + +"You have a grand gift of silence, Watson," said he. "It makes +you quite invaluable as a companion. 'Pon my word, it is a great +thing for me to have someone to talk to, for my own thoughts are +not over-pleasant. I was wondering what I should say to this dear +little woman to-night when she meets me at the door." + +"You forget that I know nothing about it." + +"I shall just have time to tell you the facts of the case before +we get to Lee. It seems absurdly simple, and yet, somehow I can +get nothing to go upon. There's plenty of thread, no doubt, but I +can't get the end of it into my hand. Now, I'll state the case +clearly and concisely to you, Watson, and maybe you can see a +spark where all is dark to me." + +"Proceed, then." + +"Some years ago--to be definite, in May, 1884--there came to Lee +a gentleman, Neville St. Clair by name, who appeared to have +plenty of money. He took a large villa, laid out the grounds very +nicely, and lived generally in good style. By degrees he made +friends in the neighbourhood, and in 1887 he married the daughter +of a local brewer, by whom he now has two children. He had no +occupation, but was interested in several companies and went into +town as a rule in the morning, returning by the 5:14 from Cannon +Street every night. Mr. St. Clair is now thirty-seven years of +age, is a man of temperate habits, a good husband, a very +affectionate father, and a man who is popular with all who know +him. I may add that his whole debts at the present moment, as far +as we have been able to ascertain, amount to 88 pounds 10s., while +he has 220 pounds standing to his credit in the Capital and +Counties Bank. There is no reason, therefore, to think that money +troubles have been weighing upon his mind. + +"Last Monday Mr. Neville St. Clair went into town rather earlier +than usual, remarking before he started that he had two important +commissions to perform, and that he would bring his little boy +home a box of bricks. Now, by the merest chance, his wife +received a telegram upon this same Monday, very shortly after his +departure, to the effect that a small parcel of considerable +value which she had been expecting was waiting for her at the +offices of the Aberdeen Shipping Company. Now, if you are well up +in your London, you will know that the office of the company is +in Fresno Street, which branches out of Upper Swandam Lane, where +you found me to-night. Mrs. St. Clair had her lunch, started for +the City, did some shopping, proceeded to the company's office, +got her packet, and found herself at exactly 4:35 walking through +Swandam Lane on her way back to the station. Have you followed me +so far?" + +"It is very clear." + +"If you remember, Monday was an exceedingly hot day, and Mrs. St. +Clair walked slowly, glancing about in the hope of seeing a cab, +as she did not like the neighbourhood in which she found herself. +While she was walking in this way down Swandam Lane, she suddenly +heard an ejaculation or cry, and was struck cold to see her +husband looking down at her and, as it seemed to her, beckoning +to her from a second-floor window. The window was open, and she +distinctly saw his face, which she describes as being terribly +agitated. He waved his hands frantically to her, and then +vanished from the window so suddenly that it seemed to her that +he had been plucked back by some irresistible force from behind. +One singular point which struck her quick feminine eye was that +although he wore some dark coat, such as he had started to town +in, he had on neither collar nor necktie. + +"Convinced that something was amiss with him, she rushed down the +steps--for the house was none other than the opium den in which +you found me to-night--and running through the front room she +attempted to ascend the stairs which led to the first floor. At +the foot of the stairs, however, she met this Lascar scoundrel of +whom I have spoken, who thrust her back and, aided by a Dane, who +acts as assistant there, pushed her out into the street. Filled +with the most maddening doubts and fears, she rushed down the +lane and, by rare good-fortune, met in Fresno Street a number of +constables with an inspector, all on their way to their beat. The +inspector and two men accompanied her back, and in spite of the +continued resistance of the proprietor, they made their way to +the room in which Mr. St. Clair had last been seen. There was no +sign of him there. In fact, in the whole of that floor there was +no one to be found save a crippled wretch of hideous aspect, who, +it seems, made his home there. Both he and the Lascar stoutly +swore that no one else had been in the front room during the +afternoon. So determined was their denial that the inspector was +staggered, and had almost come to believe that Mrs. St. Clair had +been deluded when, with a cry, she sprang at a small deal box +which lay upon the table and tore the lid from it. Out there fell +a cascade of children's bricks. It was the toy which he had +promised to bring home. + +"This discovery, and the evident confusion which the cripple +showed, made the inspector realise that the matter was serious. +The rooms were carefully examined, and results all pointed to an +abominable crime. The front room was plainly furnished as a +sitting-room and led into a small bedroom, which looked out upon +the back of one of the wharves. Between the wharf and the bedroom +window is a narrow strip, which is dry at low tide but is covered +at high tide with at least four and a half feet of water. The +bedroom window was a broad one and opened from below. On +examination traces of blood were to be seen upon the windowsill, +and several scattered drops were visible upon the wooden floor of +the bedroom. Thrust away behind a curtain in the front room were +all the clothes of Mr. Neville St. Clair, with the exception of +his coat. His boots, his socks, his hat, and his watch--all were +there. There were no signs of violence upon any of these +garments, and there were no other traces of Mr. Neville St. +Clair. Out of the window he must apparently have gone for no +other exit could be discovered, and the ominous bloodstains upon +the sill gave little promise that he could save himself by +swimming, for the tide was at its very highest at the moment of +the tragedy. + +"And now as to the villains who seemed to be immediately +implicated in the matter. The Lascar was known to be a man of the +vilest antecedents, but as, by Mrs. St. Clair's story, he was +known to have been at the foot of the stair within a very few +seconds of her husband's appearance at the window, he could +hardly have been more than an accessory to the crime. His defence +was one of absolute ignorance, and he protested that he had no +knowledge as to the doings of Hugh Boone, his lodger, and that he +could not account in any way for the presence of the missing +gentleman's clothes. + +"So much for the Lascar manager. Now for the sinister cripple who +lives upon the second floor of the opium den, and who was +certainly the last human being whose eyes rested upon Neville St. +Clair. His name is Hugh Boone, and his hideous face is one which +is familiar to every man who goes much to the City. He is a +professional beggar, though in order to avoid the police +regulations he pretends to a small trade in wax vestas. Some +little distance down Threadneedle Street, upon the left-hand +side, there is, as you may have remarked, a small angle in the +wall. Here it is that this creature takes his daily seat, +cross-legged with his tiny stock of matches on his lap, and as he +is a piteous spectacle a small rain of charity descends into the +greasy leather cap which lies upon the pavement beside him. I +have watched the fellow more than once before ever I thought of +making his professional acquaintance, and I have been surprised +at the harvest which he has reaped in a short time. His +appearance, you see, is so remarkable that no one can pass him +without observing him. A shock of orange hair, a pale face +disfigured by a horrible scar, which, by its contraction, has +turned up the outer edge of his upper lip, a bulldog chin, and a +pair of very penetrating dark eyes, which present a singular +contrast to the colour of his hair, all mark him out from amid +the common crowd of mendicants and so, too, does his wit, for he +is ever ready with a reply to any piece of chaff which may be +thrown at him by the passers-by. This is the man whom we now +learn to have been the lodger at the opium den, and to have been +the last man to see the gentleman of whom we are in quest." + +"But a cripple!" said I. "What could he have done single-handed +against a man in the prime of life?" + +"He is a cripple in the sense that he walks with a limp; but in +other respects he appears to be a powerful and well-nurtured man. +Surely your medical experience would tell you, Watson, that +weakness in one limb is often compensated for by exceptional +strength in the others." + +"Pray continue your narrative." + +"Mrs. St. Clair had fainted at the sight of the blood upon the +window, and she was escorted home in a cab by the police, as her +presence could be of no help to them in their investigations. +Inspector Barton, who had charge of the case, made a very careful +examination of the premises, but without finding anything which +threw any light upon the matter. One mistake had been made in not +arresting Boone instantly, as he was allowed some few minutes +during which he might have communicated with his friend the +Lascar, but this fault was soon remedied, and he was seized and +searched, without anything being found which could incriminate +him. There were, it is true, some blood-stains upon his right +shirt-sleeve, but he pointed to his ring-finger, which had been +cut near the nail, and explained that the bleeding came from +there, adding that he had been to the window not long before, and +that the stains which had been observed there came doubtless from +the same source. He denied strenuously having ever seen Mr. +Neville St. Clair and swore that the presence of the clothes in +his room was as much a mystery to him as to the police. As to +Mrs. St. Clair's assertion that she had actually seen her husband +at the window, he declared that she must have been either mad or +dreaming. He was removed, loudly protesting, to the +police-station, while the inspector remained upon the premises in +the hope that the ebbing tide might afford some fresh clue. + +"And it did, though they hardly found upon the mud-bank what they +had feared to find. It was Neville St. Clair's coat, and not +Neville St. Clair, which lay uncovered as the tide receded. And +what do you think they found in the pockets?" + +"I cannot imagine." + +"No, I don't think you would guess. Every pocket stuffed with +pennies and half-pennies--421 pennies and 270 half-pennies. It +was no wonder that it had not been swept away by the tide. But a +human body is a different matter. There is a fierce eddy between +the wharf and the house. It seemed likely enough that the +weighted coat had remained when the stripped body had been sucked +away into the river." + +"But I understand that all the other clothes were found in the +room. Would the body be dressed in a coat alone?" + +"No, sir, but the facts might be met speciously enough. Suppose +that this man Boone had thrust Neville St. Clair through the +window, there is no human eye which could have seen the deed. +What would he do then? It would of course instantly strike him +that he must get rid of the tell-tale garments. He would seize +the coat, then, and be in the act of throwing it out, when it +would occur to him that it would swim and not sink. He has little +time, for he has heard the scuffle downstairs when the wife tried +to force her way up, and perhaps he has already heard from his +Lascar confederate that the police are hurrying up the street. +There is not an instant to be lost. He rushes to some secret +hoard, where he has accumulated the fruits of his beggary, and he +stuffs all the coins upon which he can lay his hands into the +pockets to make sure of the coat's sinking. He throws it out, and +would have done the same with the other garments had not he heard +the rush of steps below, and only just had time to close the +window when the police appeared." + +"It certainly sounds feasible." + +"Well, we will take it as a working hypothesis for want of a +better. Boone, as I have told you, was arrested and taken to the +station, but it could not be shown that there had ever before +been anything against him. He had for years been known as a +professional beggar, but his life appeared to have been a very +quiet and innocent one. There the matter stands at present, and +the questions which have to be solved--what Neville St. Clair was +doing in the opium den, what happened to him when there, where is +he now, and what Hugh Boone had to do with his disappearance--are +all as far from a solution as ever. I confess that I cannot +recall any case within my experience which looked at the first +glance so simple and yet which presented such difficulties." + +While Sherlock Holmes had been detailing this singular series of +events, we had been whirling through the outskirts of the great +town until the last straggling houses had been left behind, and +we rattled along with a country hedge upon either side of us. +Just as he finished, however, we drove through two scattered +villages, where a few lights still glimmered in the windows. + +"We are on the outskirts of Lee," said my companion. "We have +touched on three English counties in our short drive, starting in +Middlesex, passing over an angle of Surrey, and ending in Kent. +See that light among the trees? That is The Cedars, and beside +that lamp sits a woman whose anxious ears have already, I have +little doubt, caught the clink of our horse's feet." + +"But why are you not conducting the case from Baker Street?" I +asked. + +"Because there are many inquiries which must be made out here. +Mrs. St. Clair has most kindly put two rooms at my disposal, and +you may rest assured that she will have nothing but a welcome for +my friend and colleague. I hate to meet her, Watson, when I have +no news of her husband. Here we are. Whoa, there, whoa!" + +We had pulled up in front of a large villa which stood within its +own grounds. A stable-boy had run out to the horse's head, and +springing down, I followed Holmes up the small, winding +gravel-drive which led to the house. As we approached, the door +flew open, and a little blonde woman stood in the opening, clad +in some sort of light mousseline de soie, with a touch of fluffy +pink chiffon at her neck and wrists. She stood with her figure +outlined against the flood of light, one hand upon the door, one +half-raised in her eagerness, her body slightly bent, her head +and face protruded, with eager eyes and parted lips, a standing +question. + +"Well?" she cried, "well?" And then, seeing that there were two +of us, she gave a cry of hope which sank into a groan as she saw +that my companion shook his head and shrugged his shoulders. + +"No good news?" + +"None." + +"No bad?" + +"No." + +"Thank God for that. But come in. You must be weary, for you have +had a long day." + +"This is my friend, Dr. Watson. He has been of most vital use to +me in several of my cases, and a lucky chance has made it +possible for me to bring him out and associate him with this +investigation." + +"I am delighted to see you," said she, pressing my hand warmly. +"You will, I am sure, forgive anything that may be wanting in our +arrangements, when you consider the blow which has come so +suddenly upon us." + +"My dear madam," said I, "I am an old campaigner, and if I were +not I can very well see that no apology is needed. If I can be of +any assistance, either to you or to my friend here, I shall be +indeed happy." + +"Now, Mr. Sherlock Holmes," said the lady as we entered a +well-lit dining-room, upon the table of which a cold supper had +been laid out, "I should very much like to ask you one or two +plain questions, to which I beg that you will give a plain +answer." + +"Certainly, madam." + +"Do not trouble about my feelings. I am not hysterical, nor given +to fainting. I simply wish to hear your real, real opinion." + +"Upon what point?" + +"In your heart of hearts, do you think that Neville is alive?" + +Sherlock Holmes seemed to be embarrassed by the question. +"Frankly, now!" she repeated, standing upon the rug and looking +keenly down at him as he leaned back in a basket-chair. + +"Frankly, then, madam, I do not." + +"You think that he is dead?" + +"I do." + +"Murdered?" + +"I don't say that. Perhaps." + +"And on what day did he meet his death?" + +"On Monday." + +"Then perhaps, Mr. Holmes, you will be good enough to explain how +it is that I have received a letter from him to-day." + +Sherlock Holmes sprang out of his chair as if he had been +galvanised. + +"What!" he roared. + +"Yes, to-day." She stood smiling, holding up a little slip of +paper in the air. + +"May I see it?" + +"Certainly." + +He snatched it from her in his eagerness, and smoothing it out +upon the table he drew over the lamp and examined it intently. I +had left my chair and was gazing at it over his shoulder. The +envelope was a very coarse one and was stamped with the Gravesend +postmark and with the date of that very day, or rather of the day +before, for it was considerably after midnight. + +"Coarse writing," murmured Holmes. "Surely this is not your +husband's writing, madam." + +"No, but the enclosure is." + +"I perceive also that whoever addressed the envelope had to go +and inquire as to the address." + +"How can you tell that?" + +"The name, you see, is in perfectly black ink, which has dried +itself. The rest is of the greyish colour, which shows that +blotting-paper has been used. If it had been written straight +off, and then blotted, none would be of a deep black shade. This +man has written the name, and there has then been a pause before +he wrote the address, which can only mean that he was not +familiar with it. It is, of course, a trifle, but there is +nothing so important as trifles. Let us now see the letter. Ha! +there has been an enclosure here!" + +"Yes, there was a ring. His signet-ring." + +"And you are sure that this is your husband's hand?" + +"One of his hands." + +"One?" + +"His hand when he wrote hurriedly. It is very unlike his usual +writing, and yet I know it well." + +"'Dearest do not be frightened. All will come well. There is a +huge error which it may take some little time to rectify. +Wait in patience.--NEVILLE.' Written in pencil upon the fly-leaf +of a book, octavo size, no water-mark. Hum! Posted to-day in +Gravesend by a man with a dirty thumb. Ha! And the flap has been +gummed, if I am not very much in error, by a person who had been +chewing tobacco. And you have no doubt that it is your husband's +hand, madam?" + +"None. Neville wrote those words." + +"And they were posted to-day at Gravesend. Well, Mrs. St. Clair, +the clouds lighten, though I should not venture to say that the +danger is over." + +"But he must be alive, Mr. Holmes." + +"Unless this is a clever forgery to put us on the wrong scent. +The ring, after all, proves nothing. It may have been taken from +him." + +"No, no; it is, it is his very own writing!" + +"Very well. It may, however, have been written on Monday and only +posted to-day." + +"That is possible." + +"If so, much may have happened between." + +"Oh, you must not discourage me, Mr. Holmes. I know that all is +well with him. There is so keen a sympathy between us that I +should know if evil came upon him. On the very day that I saw him +last he cut himself in the bedroom, and yet I in the dining-room +rushed upstairs instantly with the utmost certainty that +something had happened. Do you think that I would respond to such +a trifle and yet be ignorant of his death?" + +"I have seen too much not to know that the impression of a woman +may be more valuable than the conclusion of an analytical +reasoner. And in this letter you certainly have a very strong +piece of evidence to corroborate your view. But if your husband +is alive and able to write letters, why should he remain away +from you?" + +"I cannot imagine. It is unthinkable." + +"And on Monday he made no remarks before leaving you?" + +"No." + +"And you were surprised to see him in Swandam Lane?" + +"Very much so." + +"Was the window open?" + +"Yes." + +"Then he might have called to you?" + +"He might." + +"He only, as I understand, gave an inarticulate cry?" + +"Yes." + +"A call for help, you thought?" + +"Yes. He waved his hands." + +"But it might have been a cry of surprise. Astonishment at the +unexpected sight of you might cause him to throw up his hands?" + +"It is possible." + +"And you thought he was pulled back?" + +"He disappeared so suddenly." + +"He might have leaped back. You did not see anyone else in the +room?" + +"No, but this horrible man confessed to having been there, and +the Lascar was at the foot of the stairs." + +"Quite so. Your husband, as far as you could see, had his +ordinary clothes on?" + +"But without his collar or tie. I distinctly saw his bare +throat." + +"Had he ever spoken of Swandam Lane?" + +"Never." + +"Had he ever showed any signs of having taken opium?" + +"Never." + +"Thank you, Mrs. St. Clair. Those are the principal points about +which I wished to be absolutely clear. We shall now have a little +supper and then retire, for we may have a very busy day +to-morrow." + +A large and comfortable double-bedded room had been placed at our +disposal, and I was quickly between the sheets, for I was weary +after my night of adventure. Sherlock Holmes was a man, however, +who, when he had an unsolved problem upon his mind, would go for +days, and even for a week, without rest, turning it over, +rearranging his facts, looking at it from every point of view +until he had either fathomed it or convinced himself that his +data were insufficient. It was soon evident to me that he was now +preparing for an all-night sitting. He took off his coat and +waistcoat, put on a large blue dressing-gown, and then wandered +about the room collecting pillows from his bed and cushions from +the sofa and armchairs. With these he constructed a sort of +Eastern divan, upon which he perched himself cross-legged, with +an ounce of shag tobacco and a box of matches laid out in front +of him. In the dim light of the lamp I saw him sitting there, an +old briar pipe between his lips, his eyes fixed vacantly upon the +corner of the ceiling, the blue smoke curling up from him, +silent, motionless, with the light shining upon his strong-set +aquiline features. So he sat as I dropped off to sleep, and so he +sat when a sudden ejaculation caused me to wake up, and I found +the summer sun shining into the apartment. The pipe was still +between his lips, the smoke still curled upward, and the room was +full of a dense tobacco haze, but nothing remained of the heap of +shag which I had seen upon the previous night. + +"Awake, Watson?" he asked. + +"Yes." + +"Game for a morning drive?" + +"Certainly." + +"Then dress. No one is stirring yet, but I know where the +stable-boy sleeps, and we shall soon have the trap out." He +chuckled to himself as he spoke, his eyes twinkled, and he seemed +a different man to the sombre thinker of the previous night. + +As I dressed I glanced at my watch. It was no wonder that no one +was stirring. It was twenty-five minutes past four. I had hardly +finished when Holmes returned with the news that the boy was +putting in the horse. + +"I want to test a little theory of mine," said he, pulling on his +boots. "I think, Watson, that you are now standing in the +presence of one of the most absolute fools in Europe. I deserve +to be kicked from here to Charing Cross. But I think I have the +key of the affair now." + +"And where is it?" I asked, smiling. + +"In the bathroom," he answered. "Oh, yes, I am not joking," he +continued, seeing my look of incredulity. "I have just been +there, and I have taken it out, and I have got it in this +Gladstone bag. Come on, my boy, and we shall see whether it will +not fit the lock." + +We made our way downstairs as quietly as possible, and out into +the bright morning sunshine. In the road stood our horse and +trap, with the half-clad stable-boy waiting at the head. We both +sprang in, and away we dashed down the London Road. A few country +carts were stirring, bearing in vegetables to the metropolis, but +the lines of villas on either side were as silent and lifeless as +some city in a dream. + +"It has been in some points a singular case," said Holmes, +flicking the horse on into a gallop. "I confess that I have been +as blind as a mole, but it is better to learn wisdom late than +never to learn it at all." + +In town the earliest risers were just beginning to look sleepily +from their windows as we drove through the streets of the Surrey +side. Passing down the Waterloo Bridge Road we crossed over the +river, and dashing up Wellington Street wheeled sharply to the +right and found ourselves in Bow Street. Sherlock Holmes was well +known to the force, and the two constables at the door saluted +him. One of them held the horse's head while the other led us in. + +"Who is on duty?" asked Holmes. + +"Inspector Bradstreet, sir." + +"Ah, Bradstreet, how are you?" A tall, stout official had come +down the stone-flagged passage, in a peaked cap and frogged +jacket. "I wish to have a quiet word with you, Bradstreet." +"Certainly, Mr. Holmes. Step into my room here." It was a small, +office-like room, with a huge ledger upon the table, and a +telephone projecting from the wall. The inspector sat down at his +desk. + +"What can I do for you, Mr. Holmes?" + +"I called about that beggarman, Boone--the one who was charged +with being concerned in the disappearance of Mr. Neville St. +Clair, of Lee." + +"Yes. He was brought up and remanded for further inquiries." + +"So I heard. You have him here?" + +"In the cells." + +"Is he quiet?" + +"Oh, he gives no trouble. But he is a dirty scoundrel." + +"Dirty?" + +"Yes, it is all we can do to make him wash his hands, and his +face is as black as a tinker's. Well, when once his case has been +settled, he will have a regular prison bath; and I think, if you +saw him, you would agree with me that he needed it." + +"I should like to see him very much." + +"Would you? That is easily done. Come this way. You can leave +your bag." + +"No, I think that I'll take it." + +"Very good. Come this way, if you please." He led us down a +passage, opened a barred door, passed down a winding stair, and +brought us to a whitewashed corridor with a line of doors on each +side. + +"The third on the right is his," said the inspector. "Here it +is!" He quietly shot back a panel in the upper part of the door +and glanced through. + +"He is asleep," said he. "You can see him very well." + +We both put our eyes to the grating. The prisoner lay with his +face towards us, in a very deep sleep, breathing slowly and +heavily. He was a middle-sized man, coarsely clad as became his +calling, with a coloured shirt protruding through the rent in his +tattered coat. He was, as the inspector had said, extremely +dirty, but the grime which covered his face could not conceal its +repulsive ugliness. A broad wheal from an old scar ran right +across it from eye to chin, and by its contraction had turned up +one side of the upper lip, so that three teeth were exposed in a +perpetual snarl. A shock of very bright red hair grew low over +his eyes and forehead. + +"He's a beauty, isn't he?" said the inspector. + +"He certainly needs a wash," remarked Holmes. "I had an idea that +he might, and I took the liberty of bringing the tools with me." +He opened the Gladstone bag as he spoke, and took out, to my +astonishment, a very large bath-sponge. + +"He! he! You are a funny one," chuckled the inspector. + +"Now, if you will have the great goodness to open that door very +quietly, we will soon make him cut a much more respectable +figure." + +"Well, I don't know why not," said the inspector. "He doesn't +look a credit to the Bow Street cells, does he?" He slipped his +key into the lock, and we all very quietly entered the cell. The +sleeper half turned, and then settled down once more into a deep +slumber. Holmes stooped to the water-jug, moistened his sponge, +and then rubbed it twice vigorously across and down the +prisoner's face. + +"Let me introduce you," he shouted, "to Mr. Neville St. Clair, of +Lee, in the county of Kent." + +Never in my life have I seen such a sight. The man's face peeled +off under the sponge like the bark from a tree. Gone was the +coarse brown tint! Gone, too, was the horrid scar which had +seamed it across, and the twisted lip which had given the +repulsive sneer to the face! A twitch brought away the tangled +red hair, and there, sitting up in his bed, was a pale, +sad-faced, refined-looking man, black-haired and smooth-skinned, +rubbing his eyes and staring about him with sleepy bewilderment. +Then suddenly realising the exposure, he broke into a scream and +threw himself down with his face to the pillow. + +"Great heavens!" cried the inspector, "it is, indeed, the missing +man. I know him from the photograph." + +The prisoner turned with the reckless air of a man who abandons +himself to his destiny. "Be it so," said he. "And pray what am I +charged with?" + +"With making away with Mr. Neville St.-- Oh, come, you can't be +charged with that unless they make a case of attempted suicide of +it," said the inspector with a grin. "Well, I have been +twenty-seven years in the force, but this really takes the cake." + +"If I am Mr. Neville St. Clair, then it is obvious that no crime +has been committed, and that, therefore, I am illegally +detained." + +"No crime, but a very great error has been committed," said +Holmes. "You would have done better to have trusted your wife." + +"It was not the wife; it was the children," groaned the prisoner. +"God help me, I would not have them ashamed of their father. My +God! What an exposure! What can I do?" + +Sherlock Holmes sat down beside him on the couch and patted him +kindly on the shoulder. + +"If you leave it to a court of law to clear the matter up," said +he, "of course you can hardly avoid publicity. On the other hand, +if you convince the police authorities that there is no possible +case against you, I do not know that there is any reason that the +details should find their way into the papers. Inspector +Bradstreet would, I am sure, make notes upon anything which you +might tell us and submit it to the proper authorities. The case +would then never go into court at all." + +"God bless you!" cried the prisoner passionately. "I would have +endured imprisonment, ay, even execution, rather than have left +my miserable secret as a family blot to my children. + +"You are the first who have ever heard my story. My father was a +schoolmaster in Chesterfield, where I received an excellent +education. I travelled in my youth, took to the stage, and +finally became a reporter on an evening paper in London. One day +my editor wished to have a series of articles upon begging in the +metropolis, and I volunteered to supply them. There was the point +from which all my adventures started. It was only by trying +begging as an amateur that I could get the facts upon which to +base my articles. When an actor I had, of course, learned all the +secrets of making up, and had been famous in the green-room for +my skill. I took advantage now of my attainments. I painted my +face, and to make myself as pitiable as possible I made a good +scar and fixed one side of my lip in a twist by the aid of a +small slip of flesh-coloured plaster. Then with a red head of +hair, and an appropriate dress, I took my station in the business +part of the city, ostensibly as a match-seller but really as a +beggar. For seven hours I plied my trade, and when I returned +home in the evening I found to my surprise that I had received no +less than 26s. 4d. + +"I wrote my articles and thought little more of the matter until, +some time later, I backed a bill for a friend and had a writ +served upon me for 25 pounds. I was at my wit's end where to get +the money, but a sudden idea came to me. I begged a fortnight's +grace from the creditor, asked for a holiday from my employers, +and spent the time in begging in the City under my disguise. In +ten days I had the money and had paid the debt. + +"Well, you can imagine how hard it was to settle down to arduous +work at 2 pounds a week when I knew that I could earn as much in +a day by smearing my face with a little paint, laying my cap on +the ground, and sitting still. It was a long fight between my +pride and the money, but the dollars won at last, and I threw up +reporting and sat day after day in the corner which I had first +chosen, inspiring pity by my ghastly face and filling my pockets +with coppers. Only one man knew my secret. He was the keeper of a +low den in which I used to lodge in Swandam Lane, where I could +every morning emerge as a squalid beggar and in the evenings +transform myself into a well-dressed man about town. This fellow, +a Lascar, was well paid by me for his rooms, so that I knew that +my secret was safe in his possession. + +"Well, very soon I found that I was saving considerable sums of +money. I do not mean that any beggar in the streets of London +could earn 700 pounds a year--which is less than my average +takings--but I had exceptional advantages in my power of making +up, and also in a facility of repartee, which improved by +practice and made me quite a recognised character in the City. +All day a stream of pennies, varied by silver, poured in upon me, +and it was a very bad day in which I failed to take 2 pounds. + +"As I grew richer I grew more ambitious, took a house in the +country, and eventually married, without anyone having a +suspicion as to my real occupation. My dear wife knew that I had +business in the City. She little knew what. + +"Last Monday I had finished for the day and was dressing in my +room above the opium den when I looked out of my window and saw, +to my horror and astonishment, that my wife was standing in the +street, with her eyes fixed full upon me. I gave a cry of +surprise, threw up my arms to cover my face, and, rushing to my +confidant, the Lascar, entreated him to prevent anyone from +coming up to me. I heard her voice downstairs, but I knew that +she could not ascend. Swiftly I threw off my clothes, pulled on +those of a beggar, and put on my pigments and wig. Even a wife's +eyes could not pierce so complete a disguise. But then it +occurred to me that there might be a search in the room, and that +the clothes might betray me. I threw open the window, reopening +by my violence a small cut which I had inflicted upon myself in +the bedroom that morning. Then I seized my coat, which was +weighted by the coppers which I had just transferred to it from +the leather bag in which I carried my takings. I hurled it out of +the window, and it disappeared into the Thames. The other clothes +would have followed, but at that moment there was a rush of +constables up the stair, and a few minutes after I found, rather, +I confess, to my relief, that instead of being identified as Mr. +Neville St. Clair, I was arrested as his murderer. + +"I do not know that there is anything else for me to explain. I +was determined to preserve my disguise as long as possible, and +hence my preference for a dirty face. Knowing that my wife would +be terribly anxious, I slipped off my ring and confided it to the +Lascar at a moment when no constable was watching me, together +with a hurried scrawl, telling her that she had no cause to +fear." + +"That note only reached her yesterday," said Holmes. + +"Good God! What a week she must have spent!" + +"The police have watched this Lascar," said Inspector Bradstreet, +"and I can quite understand that he might find it difficult to +post a letter unobserved. Probably he handed it to some sailor +customer of his, who forgot all about it for some days." + +"That was it," said Holmes, nodding approvingly; "I have no doubt +of it. But have you never been prosecuted for begging?" + +"Many times; but what was a fine to me?" + +"It must stop here, however," said Bradstreet. "If the police are +to hush this thing up, there must be no more of Hugh Boone." + +"I have sworn it by the most solemn oaths which a man can take." + +"In that case I think that it is probable that no further steps +may be taken. But if you are found again, then all must come out. +I am sure, Mr. Holmes, that we are very much indebted to you for +having cleared the matter up. I wish I knew how you reach your +results." + +"I reached this one," said my friend, "by sitting upon five +pillows and consuming an ounce of shag. I think, Watson, that if +we drive to Baker Street we shall just be in time for breakfast." + + + +VII. THE ADVENTURE OF THE BLUE CARBUNCLE + +I had called upon my friend Sherlock Holmes upon the second +morning after Christmas, with the intention of wishing him the +compliments of the season. He was lounging upon the sofa in a +purple dressing-gown, a pipe-rack within his reach upon the +right, and a pile of crumpled morning papers, evidently newly +studied, near at hand. Beside the couch was a wooden chair, and +on the angle of the back hung a very seedy and disreputable +hard-felt hat, much the worse for wear, and cracked in several +places. A lens and a forceps lying upon the seat of the chair +suggested that the hat had been suspended in this manner for the +purpose of examination. + +"You are engaged," said I; "perhaps I interrupt you." + +"Not at all. I am glad to have a friend with whom I can discuss +my results. The matter is a perfectly trivial one"--he jerked his +thumb in the direction of the old hat--"but there are points in +connection with it which are not entirely devoid of interest and +even of instruction." + +I seated myself in his armchair and warmed my hands before his +crackling fire, for a sharp frost had set in, and the windows +were thick with the ice crystals. "I suppose," I remarked, "that, +homely as it looks, this thing has some deadly story linked on to +it--that it is the clue which will guide you in the solution of +some mystery and the punishment of some crime." + +"No, no. No crime," said Sherlock Holmes, laughing. "Only one of +those whimsical little incidents which will happen when you have +four million human beings all jostling each other within the +space of a few square miles. Amid the action and reaction of so +dense a swarm of humanity, every possible combination of events +may be expected to take place, and many a little problem will be +presented which may be striking and bizarre without being +criminal. We have already had experience of such." + +"So much so," I remarked, "that of the last six cases which I +have added to my notes, three have been entirely free of any +legal crime." + +"Precisely. You allude to my attempt to recover the Irene Adler +papers, to the singular case of Miss Mary Sutherland, and to the +adventure of the man with the twisted lip. Well, I have no doubt +that this small matter will fall into the same innocent category. +You know Peterson, the commissionaire?" + +"Yes." + +"It is to him that this trophy belongs." + +"It is his hat." + +"No, no, he found it. Its owner is unknown. I beg that you will +look upon it not as a battered billycock but as an intellectual +problem. And, first, as to how it came here. It arrived upon +Christmas morning, in company with a good fat goose, which is, I +have no doubt, roasting at this moment in front of Peterson's +fire. The facts are these: about four o'clock on Christmas +morning, Peterson, who, as you know, is a very honest fellow, was +returning from some small jollification and was making his way +homeward down Tottenham Court Road. In front of him he saw, in +the gaslight, a tallish man, walking with a slight stagger, and +carrying a white goose slung over his shoulder. As he reached the +corner of Goodge Street, a row broke out between this stranger +and a little knot of roughs. One of the latter knocked off the +man's hat, on which he raised his stick to defend himself and, +swinging it over his head, smashed the shop window behind him. +Peterson had rushed forward to protect the stranger from his +assailants; but the man, shocked at having broken the window, and +seeing an official-looking person in uniform rushing towards him, +dropped his goose, took to his heels, and vanished amid the +labyrinth of small streets which lie at the back of Tottenham +Court Road. The roughs had also fled at the appearance of +Peterson, so that he was left in possession of the field of +battle, and also of the spoils of victory in the shape of this +battered hat and a most unimpeachable Christmas goose." + +"Which surely he restored to their owner?" + +"My dear fellow, there lies the problem. It is true that 'For +Mrs. Henry Baker' was printed upon a small card which was tied to +the bird's left leg, and it is also true that the initials 'H. +B.' are legible upon the lining of this hat, but as there are +some thousands of Bakers, and some hundreds of Henry Bakers in +this city of ours, it is not easy to restore lost property to any +one of them." + +"What, then, did Peterson do?" + +"He brought round both hat and goose to me on Christmas morning, +knowing that even the smallest problems are of interest to me. +The goose we retained until this morning, when there were signs +that, in spite of the slight frost, it would be well that it +should be eaten without unnecessary delay. Its finder has carried +it off, therefore, to fulfil the ultimate destiny of a goose, +while I continue to retain the hat of the unknown gentleman who +lost his Christmas dinner." + +"Did he not advertise?" + +"No." + +"Then, what clue could you have as to his identity?" + +"Only as much as we can deduce." + +"From his hat?" + +"Precisely." + +"But you are joking. What can you gather from this old battered +felt?" + +"Here is my lens. You know my methods. What can you gather +yourself as to the individuality of the man who has worn this +article?" + +I took the tattered object in my hands and turned it over rather +ruefully. It was a very ordinary black hat of the usual round +shape, hard and much the worse for wear. The lining had been of +red silk, but was a good deal discoloured. There was no maker's +name; but, as Holmes had remarked, the initials "H. B." were +scrawled upon one side. It was pierced in the brim for a +hat-securer, but the elastic was missing. For the rest, it was +cracked, exceedingly dusty, and spotted in several places, +although there seemed to have been some attempt to hide the +discoloured patches by smearing them with ink. + +"I can see nothing," said I, handing it back to my friend. + +"On the contrary, Watson, you can see everything. You fail, +however, to reason from what you see. You are too timid in +drawing your inferences." + +"Then, pray tell me what it is that you can infer from this hat?" + +He picked it up and gazed at it in the peculiar introspective +fashion which was characteristic of him. "It is perhaps less +suggestive than it might have been," he remarked, "and yet there +are a few inferences which are very distinct, and a few others +which represent at least a strong balance of probability. That +the man was highly intellectual is of course obvious upon the +face of it, and also that he was fairly well-to-do within the +last three years, although he has now fallen upon evil days. He +had foresight, but has less now than formerly, pointing to a +moral retrogression, which, when taken with the decline of his +fortunes, seems to indicate some evil influence, probably drink, +at work upon him. This may account also for the obvious fact that +his wife has ceased to love him." + +"My dear Holmes!" + +"He has, however, retained some degree of self-respect," he +continued, disregarding my remonstrance. "He is a man who leads a +sedentary life, goes out little, is out of training entirely, is +middle-aged, has grizzled hair which he has had cut within the +last few days, and which he anoints with lime-cream. These are +the more patent facts which are to be deduced from his hat. Also, +by the way, that it is extremely improbable that he has gas laid +on in his house." + +"You are certainly joking, Holmes." + +"Not in the least. Is it possible that even now, when I give you +these results, you are unable to see how they are attained?" + +"I have no doubt that I am very stupid, but I must confess that I +am unable to follow you. For example, how did you deduce that +this man was intellectual?" + +For answer Holmes clapped the hat upon his head. It came right +over the forehead and settled upon the bridge of his nose. "It is +a question of cubic capacity," said he; "a man with so large a +brain must have something in it." + +"The decline of his fortunes, then?" + +"This hat is three years old. These flat brims curled at the edge +came in then. It is a hat of the very best quality. Look at the +band of ribbed silk and the excellent lining. If this man could +afford to buy so expensive a hat three years ago, and has had no +hat since, then he has assuredly gone down in the world." + +"Well, that is clear enough, certainly. But how about the +foresight and the moral retrogression?" + +Sherlock Holmes laughed. "Here is the foresight," said he putting +his finger upon the little disc and loop of the hat-securer. +"They are never sold upon hats. If this man ordered one, it is a +sign of a certain amount of foresight, since he went out of his +way to take this precaution against the wind. But since we see +that he has broken the elastic and has not troubled to replace +it, it is obvious that he has less foresight now than formerly, +which is a distinct proof of a weakening nature. On the other +hand, he has endeavoured to conceal some of these stains upon the +felt by daubing them with ink, which is a sign that he has not +entirely lost his self-respect." + +"Your reasoning is certainly plausible." + +"The further points, that he is middle-aged, that his hair is +grizzled, that it has been recently cut, and that he uses +lime-cream, are all to be gathered from a close examination of the +lower part of the lining. The lens discloses a large number of +hair-ends, clean cut by the scissors of the barber. They all +appear to be adhesive, and there is a distinct odour of +lime-cream. This dust, you will observe, is not the gritty, grey +dust of the street but the fluffy brown dust of the house, +showing that it has been hung up indoors most of the time, while +the marks of moisture upon the inside are proof positive that the +wearer perspired very freely, and could therefore, hardly be in +the best of training." + +"But his wife--you said that she had ceased to love him." + +"This hat has not been brushed for weeks. When I see you, my dear +Watson, with a week's accumulation of dust upon your hat, and +when your wife allows you to go out in such a state, I shall fear +that you also have been unfortunate enough to lose your wife's +affection." + +"But he might be a bachelor." + +"Nay, he was bringing home the goose as a peace-offering to his +wife. Remember the card upon the bird's leg." + +"You have an answer to everything. But how on earth do you deduce +that the gas is not laid on in his house?" + +"One tallow stain, or even two, might come by chance; but when I +see no less than five, I think that there can be little doubt +that the individual must be brought into frequent contact with +burning tallow--walks upstairs at night probably with his hat in +one hand and a guttering candle in the other. Anyhow, he never +got tallow-stains from a gas-jet. Are you satisfied?" + +"Well, it is very ingenious," said I, laughing; "but since, as +you said just now, there has been no crime committed, and no harm +done save the loss of a goose, all this seems to be rather a +waste of energy." + +Sherlock Holmes had opened his mouth to reply, when the door flew +open, and Peterson, the commissionaire, rushed into the apartment +with flushed cheeks and the face of a man who is dazed with +astonishment. + +"The goose, Mr. Holmes! The goose, sir!" he gasped. + +"Eh? What of it, then? Has it returned to life and flapped off +through the kitchen window?" Holmes twisted himself round upon +the sofa to get a fairer view of the man's excited face. + +"See here, sir! See what my wife found in its crop!" He held out +his hand and displayed upon the centre of the palm a brilliantly +scintillating blue stone, rather smaller than a bean in size, but +of such purity and radiance that it twinkled like an electric +point in the dark hollow of his hand. + +Sherlock Holmes sat up with a whistle. "By Jove, Peterson!" said +he, "this is treasure trove indeed. I suppose you know what you +have got?" + +"A diamond, sir? A precious stone. It cuts into glass as though +it were putty." + +"It's more than a precious stone. It is the precious stone." + +"Not the Countess of Morcar's blue carbuncle!" I ejaculated. + +"Precisely so. I ought to know its size and shape, seeing that I +have read the advertisement about it in The Times every day +lately. It is absolutely unique, and its value can only be +conjectured, but the reward offered of 1000 pounds is certainly +not within a twentieth part of the market price." + +"A thousand pounds! Great Lord of mercy!" The commissionaire +plumped down into a chair and stared from one to the other of us. + +"That is the reward, and I have reason to know that there are +sentimental considerations in the background which would induce +the Countess to part with half her fortune if she could but +recover the gem." + +"It was lost, if I remember aright, at the Hotel Cosmopolitan," I +remarked. + +"Precisely so, on December 22nd, just five days ago. John Horner, +a plumber, was accused of having abstracted it from the lady's +jewel-case. The evidence against him was so strong that the case +has been referred to the Assizes. I have some account of the +matter here, I believe." He rummaged amid his newspapers, +glancing over the dates, until at last he smoothed one out, +doubled it over, and read the following paragraph: + +"Hotel Cosmopolitan Jewel Robbery. John Horner, 26, plumber, was +brought up upon the charge of having upon the 22nd inst., +abstracted from the jewel-case of the Countess of Morcar the +valuable gem known as the blue carbuncle. James Ryder, +upper-attendant at the hotel, gave his evidence to the effect +that he had shown Horner up to the dressing-room of the Countess +of Morcar upon the day of the robbery in order that he might +solder the second bar of the grate, which was loose. He had +remained with Horner some little time, but had finally been +called away. On returning, he found that Horner had disappeared, +that the bureau had been forced open, and that the small morocco +casket in which, as it afterwards transpired, the Countess was +accustomed to keep her jewel, was lying empty upon the +dressing-table. Ryder instantly gave the alarm, and Horner was +arrested the same evening; but the stone could not be found +either upon his person or in his rooms. Catherine Cusack, maid to +the Countess, deposed to having heard Ryder's cry of dismay on +discovering the robbery, and to having rushed into the room, +where she found matters as described by the last witness. +Inspector Bradstreet, B division, gave evidence as to the arrest +of Horner, who struggled frantically, and protested his innocence +in the strongest terms. Evidence of a previous conviction for +robbery having been given against the prisoner, the magistrate +refused to deal summarily with the offence, but referred it to +the Assizes. Horner, who had shown signs of intense emotion +during the proceedings, fainted away at the conclusion and was +carried out of court." + +"Hum! So much for the police-court," said Holmes thoughtfully, +tossing aside the paper. "The question for us now to solve is the +sequence of events leading from a rifled jewel-case at one end to +the crop of a goose in Tottenham Court Road at the other. You +see, Watson, our little deductions have suddenly assumed a much +more important and less innocent aspect. Here is the stone; the +stone came from the goose, and the goose came from Mr. Henry +Baker, the gentleman with the bad hat and all the other +characteristics with which I have bored you. So now we must set +ourselves very seriously to finding this gentleman and +ascertaining what part he has played in this little mystery. To +do this, we must try the simplest means first, and these lie +undoubtedly in an advertisement in all the evening papers. If +this fail, I shall have recourse to other methods." + +"What will you say?" + +"Give me a pencil and that slip of paper. Now, then: 'Found at +the corner of Goodge Street, a goose and a black felt hat. Mr. +Henry Baker can have the same by applying at 6:30 this evening at +221B, Baker Street.' That is clear and concise." + +"Very. But will he see it?" + +"Well, he is sure to keep an eye on the papers, since, to a poor +man, the loss was a heavy one. He was clearly so scared by his +mischance in breaking the window and by the approach of Peterson +that he thought of nothing but flight, but since then he must +have bitterly regretted the impulse which caused him to drop his +bird. Then, again, the introduction of his name will cause him to +see it, for everyone who knows him will direct his attention to +it. Here you are, Peterson, run down to the advertising agency +and have this put in the evening papers." + +"In which, sir?" + +"Oh, in the Globe, Star, Pall Mall, St. James's, Evening News, +Standard, Echo, and any others that occur to you." + +"Very well, sir. And this stone?" + +"Ah, yes, I shall keep the stone. Thank you. And, I say, +Peterson, just buy a goose on your way back and leave it here +with me, for we must have one to give to this gentleman in place +of the one which your family is now devouring." + +When the commissionaire had gone, Holmes took up the stone and +held it against the light. "It's a bonny thing," said he. "Just +see how it glints and sparkles. Of course it is a nucleus and +focus of crime. Every good stone is. They are the devil's pet +baits. In the larger and older jewels every facet may stand for a +bloody deed. This stone is not yet twenty years old. It was found +in the banks of the Amoy River in southern China and is remarkable +in having every characteristic of the carbuncle, save that it is +blue in shade instead of ruby red. In spite of its youth, it has +already a sinister history. There have been two murders, a +vitriol-throwing, a suicide, and several robberies brought about +for the sake of this forty-grain weight of crystallised charcoal. +Who would think that so pretty a toy would be a purveyor to the +gallows and the prison? I'll lock it up in my strong box now and +drop a line to the Countess to say that we have it." + +"Do you think that this man Horner is innocent?" + +"I cannot tell." + +"Well, then, do you imagine that this other one, Henry Baker, had +anything to do with the matter?" + +"It is, I think, much more likely that Henry Baker is an +absolutely innocent man, who had no idea that the bird which he +was carrying was of considerably more value than if it were made +of solid gold. That, however, I shall determine by a very simple +test if we have an answer to our advertisement." + +"And you can do nothing until then?" + +"Nothing." + +"In that case I shall continue my professional round. But I shall +come back in the evening at the hour you have mentioned, for I +should like to see the solution of so tangled a business." + +"Very glad to see you. I dine at seven. There is a woodcock, I +believe. By the way, in view of recent occurrences, perhaps I +ought to ask Mrs. Hudson to examine its crop." + +I had been delayed at a case, and it was a little after half-past +six when I found myself in Baker Street once more. As I +approached the house I saw a tall man in a Scotch bonnet with a +coat which was buttoned up to his chin waiting outside in the +bright semicircle which was thrown from the fanlight. Just as I +arrived the door was opened, and we were shown up together to +Holmes' room. + +"Mr. Henry Baker, I believe," said he, rising from his armchair +and greeting his visitor with the easy air of geniality which he +could so readily assume. "Pray take this chair by the fire, Mr. +Baker. It is a cold night, and I observe that your circulation is +more adapted for summer than for winter. Ah, Watson, you have +just come at the right time. Is that your hat, Mr. Baker?" + +"Yes, sir, that is undoubtedly my hat." + +He was a large man with rounded shoulders, a massive head, and a +broad, intelligent face, sloping down to a pointed beard of +grizzled brown. A touch of red in nose and cheeks, with a slight +tremor of his extended hand, recalled Holmes' surmise as to his +habits. His rusty black frock-coat was buttoned right up in +front, with the collar turned up, and his lank wrists protruded +from his sleeves without a sign of cuff or shirt. He spoke in a +slow staccato fashion, choosing his words with care, and gave the +impression generally of a man of learning and letters who had had +ill-usage at the hands of fortune. + +"We have retained these things for some days," said Holmes, +"because we expected to see an advertisement from you giving your +address. I am at a loss to know now why you did not advertise." + +Our visitor gave a rather shamefaced laugh. "Shillings have not +been so plentiful with me as they once were," he remarked. "I had +no doubt that the gang of roughs who assaulted me had carried off +both my hat and the bird. I did not care to spend more money in a +hopeless attempt at recovering them." + +"Very naturally. By the way, about the bird, we were compelled to +eat it." + +"To eat it!" Our visitor half rose from his chair in his +excitement. + +"Yes, it would have been of no use to anyone had we not done so. +But I presume that this other goose upon the sideboard, which is +about the same weight and perfectly fresh, will answer your +purpose equally well?" + +"Oh, certainly, certainly," answered Mr. Baker with a sigh of +relief. + +"Of course, we still have the feathers, legs, crop, and so on of +your own bird, so if you wish--" + +The man burst into a hearty laugh. "They might be useful to me as +relics of my adventure," said he, "but beyond that I can hardly +see what use the disjecta membra of my late acquaintance are +going to be to me. No, sir, I think that, with your permission, I +will confine my attentions to the excellent bird which I perceive +upon the sideboard." + +Sherlock Holmes glanced sharply across at me with a slight shrug +of his shoulders. + +"There is your hat, then, and there your bird," said he. "By the +way, would it bore you to tell me where you got the other one +from? I am somewhat of a fowl fancier, and I have seldom seen a +better grown goose." + +"Certainly, sir," said Baker, who had risen and tucked his newly +gained property under his arm. "There are a few of us who +frequent the Alpha Inn, near the Museum--we are to be found in +the Museum itself during the day, you understand. This year our +good host, Windigate by name, instituted a goose club, by which, +on consideration of some few pence every week, we were each to +receive a bird at Christmas. My pence were duly paid, and the +rest is familiar to you. I am much indebted to you, sir, for a +Scotch bonnet is fitted neither to my years nor my gravity." With +a comical pomposity of manner he bowed solemnly to both of us and +strode off upon his way. + +"So much for Mr. Henry Baker," said Holmes when he had closed the +door behind him. "It is quite certain that he knows nothing +whatever about the matter. Are you hungry, Watson?" + +"Not particularly." + +"Then I suggest that we turn our dinner into a supper and follow +up this clue while it is still hot." + +"By all means." + +It was a bitter night, so we drew on our ulsters and wrapped +cravats about our throats. Outside, the stars were shining coldly +in a cloudless sky, and the breath of the passers-by blew out +into smoke like so many pistol shots. Our footfalls rang out +crisply and loudly as we swung through the doctors' quarter, +Wimpole Street, Harley Street, and so through Wigmore Street into +Oxford Street. In a quarter of an hour we were in Bloomsbury at +the Alpha Inn, which is a small public-house at the corner of one +of the streets which runs down into Holborn. Holmes pushed open +the door of the private bar and ordered two glasses of beer from +the ruddy-faced, white-aproned landlord. + +"Your beer should be excellent if it is as good as your geese," +said he. + +"My geese!" The man seemed surprised. + +"Yes. I was speaking only half an hour ago to Mr. Henry Baker, +who was a member of your goose club." + +"Ah! yes, I see. But you see, sir, them's not our geese." + +"Indeed! Whose, then?" + +"Well, I got the two dozen from a salesman in Covent Garden." + +"Indeed? I know some of them. Which was it?" + +"Breckinridge is his name." + +"Ah! I don't know him. Well, here's your good health landlord, +and prosperity to your house. Good-night." + +"Now for Mr. Breckinridge," he continued, buttoning up his coat +as we came out into the frosty air. "Remember, Watson that though +we have so homely a thing as a goose at one end of this chain, we +have at the other a man who will certainly get seven years' penal +servitude unless we can establish his innocence. It is possible +that our inquiry may but confirm his guilt; but, in any case, we +have a line of investigation which has been missed by the police, +and which a singular chance has placed in our hands. Let us +follow it out to the bitter end. Faces to the south, then, and +quick march!" + +We passed across Holborn, down Endell Street, and so through a +zigzag of slums to Covent Garden Market. One of the largest +stalls bore the name of Breckinridge upon it, and the proprietor +a horsey-looking man, with a sharp face and trim side-whiskers was +helping a boy to put up the shutters. + +"Good-evening. It's a cold night," said Holmes. + +The salesman nodded and shot a questioning glance at my +companion. + +"Sold out of geese, I see," continued Holmes, pointing at the +bare slabs of marble. + +"Let you have five hundred to-morrow morning." + +"That's no good." + +"Well, there are some on the stall with the gas-flare." + +"Ah, but I was recommended to you." + +"Who by?" + +"The landlord of the Alpha." + +"Oh, yes; I sent him a couple of dozen." + +"Fine birds they were, too. Now where did you get them from?" + +To my surprise the question provoked a burst of anger from the +salesman. + +"Now, then, mister," said he, with his head cocked and his arms +akimbo, "what are you driving at? Let's have it straight, now." + +"It is straight enough. I should like to know who sold you the +geese which you supplied to the Alpha." + +"Well then, I shan't tell you. So now!" + +"Oh, it is a matter of no importance; but I don't know why you +should be so warm over such a trifle." + +"Warm! You'd be as warm, maybe, if you were as pestered as I am. +When I pay good money for a good article there should be an end +of the business; but it's 'Where are the geese?' and 'Who did you +sell the geese to?' and 'What will you take for the geese?' One +would think they were the only geese in the world, to hear the +fuss that is made over them." + +"Well, I have no connection with any other people who have been +making inquiries," said Holmes carelessly. "If you won't tell us +the bet is off, that is all. But I'm always ready to back my +opinion on a matter of fowls, and I have a fiver on it that the +bird I ate is country bred." + +"Well, then, you've lost your fiver, for it's town bred," snapped +the salesman. + +"It's nothing of the kind." + +"I say it is." + +"I don't believe it." + +"D'you think you know more about fowls than I, who have handled +them ever since I was a nipper? I tell you, all those birds that +went to the Alpha were town bred." + +"You'll never persuade me to believe that." + +"Will you bet, then?" + +"It's merely taking your money, for I know that I am right. But +I'll have a sovereign on with you, just to teach you not to be +obstinate." + +The salesman chuckled grimly. "Bring me the books, Bill," said +he. + +The small boy brought round a small thin volume and a great +greasy-backed one, laying them out together beneath the hanging +lamp. + +"Now then, Mr. Cocksure," said the salesman, "I thought that I +was out of geese, but before I finish you'll find that there is +still one left in my shop. You see this little book?" + +"Well?" + +"That's the list of the folk from whom I buy. D'you see? Well, +then, here on this page are the country folk, and the numbers +after their names are where their accounts are in the big ledger. +Now, then! You see this other page in red ink? Well, that is a +list of my town suppliers. Now, look at that third name. Just +read it out to me." + +"Mrs. Oakshott, 117, Brixton Road--249," read Holmes. + +"Quite so. Now turn that up in the ledger." + +Holmes turned to the page indicated. "Here you are, 'Mrs. +Oakshott, 117, Brixton Road, egg and poultry supplier.'" + +"Now, then, what's the last entry?" + +"'December 22nd. Twenty-four geese at 7s. 6d.'" + +"Quite so. There you are. And underneath?" + +"'Sold to Mr. Windigate of the Alpha, at 12s.'" + +"What have you to say now?" + +Sherlock Holmes looked deeply chagrined. He drew a sovereign from +his pocket and threw it down upon the slab, turning away with the +air of a man whose disgust is too deep for words. A few yards off +he stopped under a lamp-post and laughed in the hearty, noiseless +fashion which was peculiar to him. + +"When you see a man with whiskers of that cut and the 'Pink 'un' +protruding out of his pocket, you can always draw him by a bet," +said he. "I daresay that if I had put 100 pounds down in front of +him, that man would not have given me such complete information +as was drawn from him by the idea that he was doing me on a +wager. Well, Watson, we are, I fancy, nearing the end of our +quest, and the only point which remains to be determined is +whether we should go on to this Mrs. Oakshott to-night, or +whether we should reserve it for to-morrow. It is clear from what +that surly fellow said that there are others besides ourselves +who are anxious about the matter, and I should--" + +His remarks were suddenly cut short by a loud hubbub which broke +out from the stall which we had just left. Turning round we saw a +little rat-faced fellow standing in the centre of the circle of +yellow light which was thrown by the swinging lamp, while +Breckinridge, the salesman, framed in the door of his stall, was +shaking his fists fiercely at the cringing figure. + +"I've had enough of you and your geese," he shouted. "I wish you +were all at the devil together. If you come pestering me any more +with your silly talk I'll set the dog at you. You bring Mrs. +Oakshott here and I'll answer her, but what have you to do with +it? Did I buy the geese off you?" + +"No; but one of them was mine all the same," whined the little +man. + +"Well, then, ask Mrs. Oakshott for it." + +"She told me to ask you." + +"Well, you can ask the King of Proosia, for all I care. I've had +enough of it. Get out of this!" He rushed fiercely forward, and +the inquirer flitted away into the darkness. + +"Ha! this may save us a visit to Brixton Road," whispered Holmes. +"Come with me, and we will see what is to be made of this +fellow." Striding through the scattered knots of people who +lounged round the flaring stalls, my companion speedily overtook +the little man and touched him upon the shoulder. He sprang +round, and I could see in the gas-light that every vestige of +colour had been driven from his face. + +"Who are you, then? What do you want?" he asked in a quavering +voice. + +"You will excuse me," said Holmes blandly, "but I could not help +overhearing the questions which you put to the salesman just now. +I think that I could be of assistance to you." + +"You? Who are you? How could you know anything of the matter?" + +"My name is Sherlock Holmes. It is my business to know what other +people don't know." + +"But you can know nothing of this?" + +"Excuse me, I know everything of it. You are endeavouring to +trace some geese which were sold by Mrs. Oakshott, of Brixton +Road, to a salesman named Breckinridge, by him in turn to Mr. +Windigate, of the Alpha, and by him to his club, of which Mr. +Henry Baker is a member." + +"Oh, sir, you are the very man whom I have longed to meet," cried +the little fellow with outstretched hands and quivering fingers. +"I can hardly explain to you how interested I am in this matter." + +Sherlock Holmes hailed a four-wheeler which was passing. "In that +case we had better discuss it in a cosy room rather than in this +wind-swept market-place," said he. "But pray tell me, before we +go farther, who it is that I have the pleasure of assisting." + +The man hesitated for an instant. "My name is John Robinson," he +answered with a sidelong glance. + +"No, no; the real name," said Holmes sweetly. "It is always +awkward doing business with an alias." + +A flush sprang to the white cheeks of the stranger. "Well then," +said he, "my real name is James Ryder." + +"Precisely so. Head attendant at the Hotel Cosmopolitan. Pray +step into the cab, and I shall soon be able to tell you +everything which you would wish to know." + +The little man stood glancing from one to the other of us with +half-frightened, half-hopeful eyes, as one who is not sure +whether he is on the verge of a windfall or of a catastrophe. +Then he stepped into the cab, and in half an hour we were back in +the sitting-room at Baker Street. Nothing had been said during +our drive, but the high, thin breathing of our new companion, and +the claspings and unclaspings of his hands, spoke of the nervous +tension within him. + +"Here we are!" said Holmes cheerily as we filed into the room. +"The fire looks very seasonable in this weather. You look cold, +Mr. Ryder. Pray take the basket-chair. I will just put on my +slippers before we settle this little matter of yours. Now, then! +You want to know what became of those geese?" + +"Yes, sir." + +"Or rather, I fancy, of that goose. It was one bird, I imagine in +which you were interested--white, with a black bar across the +tail." + +Ryder quivered with emotion. "Oh, sir," he cried, "can you tell +me where it went to?" + +"It came here." + +"Here?" + +"Yes, and a most remarkable bird it proved. I don't wonder that +you should take an interest in it. It laid an egg after it was +dead--the bonniest, brightest little blue egg that ever was seen. +I have it here in my museum." + +Our visitor staggered to his feet and clutched the mantelpiece +with his right hand. Holmes unlocked his strong-box and held up +the blue carbuncle, which shone out like a star, with a cold, +brilliant, many-pointed radiance. Ryder stood glaring with a +drawn face, uncertain whether to claim or to disown it. + +"The game's up, Ryder," said Holmes quietly. "Hold up, man, or +you'll be into the fire! Give him an arm back into his chair, +Watson. He's not got blood enough to go in for felony with +impunity. Give him a dash of brandy. So! Now he looks a little +more human. What a shrimp it is, to be sure!" + +For a moment he had staggered and nearly fallen, but the brandy +brought a tinge of colour into his cheeks, and he sat staring +with frightened eyes at his accuser. + +"I have almost every link in my hands, and all the proofs which I +could possibly need, so there is little which you need tell me. +Still, that little may as well be cleared up to make the case +complete. You had heard, Ryder, of this blue stone of the +Countess of Morcar's?" + +"It was Catherine Cusack who told me of it," said he in a +crackling voice. + +"I see--her ladyship's waiting-maid. Well, the temptation of +sudden wealth so easily acquired was too much for you, as it has +been for better men before you; but you were not very scrupulous +in the means you used. It seems to me, Ryder, that there is the +making of a very pretty villain in you. You knew that this man +Horner, the plumber, had been concerned in some such matter +before, and that suspicion would rest the more readily upon him. +What did you do, then? You made some small job in my lady's +room--you and your confederate Cusack--and you managed that he +should be the man sent for. Then, when he had left, you rifled +the jewel-case, raised the alarm, and had this unfortunate man +arrested. You then--" + +Ryder threw himself down suddenly upon the rug and clutched at my +companion's knees. "For God's sake, have mercy!" he shrieked. +"Think of my father! Of my mother! It would break their hearts. I +never went wrong before! I never will again. I swear it. I'll +swear it on a Bible. Oh, don't bring it into court! For Christ's +sake, don't!" + +"Get back into your chair!" said Holmes sternly. "It is very well +to cringe and crawl now, but you thought little enough of this +poor Horner in the dock for a crime of which he knew nothing." + +"I will fly, Mr. Holmes. I will leave the country, sir. Then the +charge against him will break down." + +"Hum! We will talk about that. And now let us hear a true account +of the next act. How came the stone into the goose, and how came +the goose into the open market? Tell us the truth, for there lies +your only hope of safety." + +Ryder passed his tongue over his parched lips. "I will tell you +it just as it happened, sir," said he. "When Horner had been +arrested, it seemed to me that it would be best for me to get +away with the stone at once, for I did not know at what moment +the police might not take it into their heads to search me and my +room. There was no place about the hotel where it would be safe. +I went out, as if on some commission, and I made for my sister's +house. She had married a man named Oakshott, and lived in Brixton +Road, where she fattened fowls for the market. All the way there +every man I met seemed to me to be a policeman or a detective; +and, for all that it was a cold night, the sweat was pouring down +my face before I came to the Brixton Road. My sister asked me +what was the matter, and why I was so pale; but I told her that I +had been upset by the jewel robbery at the hotel. Then I went +into the back yard and smoked a pipe and wondered what it would +be best to do. + +"I had a friend once called Maudsley, who went to the bad, and +has just been serving his time in Pentonville. One day he had met +me, and fell into talk about the ways of thieves, and how they +could get rid of what they stole. I knew that he would be true to +me, for I knew one or two things about him; so I made up my mind +to go right on to Kilburn, where he lived, and take him into my +confidence. He would show me how to turn the stone into money. +But how to get to him in safety? I thought of the agonies I had +gone through in coming from the hotel. I might at any moment be +seized and searched, and there would be the stone in my waistcoat +pocket. I was leaning against the wall at the time and looking at +the geese which were waddling about round my feet, and suddenly +an idea came into my head which showed me how I could beat the +best detective that ever lived. + +"My sister had told me some weeks before that I might have the +pick of her geese for a Christmas present, and I knew that she +was always as good as her word. I would take my goose now, and in +it I would carry my stone to Kilburn. There was a little shed in +the yard, and behind this I drove one of the birds--a fine big +one, white, with a barred tail. I caught it, and prying its bill +open, I thrust the stone down its throat as far as my finger +could reach. The bird gave a gulp, and I felt the stone pass +along its gullet and down into its crop. But the creature flapped +and struggled, and out came my sister to know what was the +matter. As I turned to speak to her the brute broke loose and +fluttered off among the others. + +"'Whatever were you doing with that bird, Jem?' says she. + +"'Well,' said I, 'you said you'd give me one for Christmas, and I +was feeling which was the fattest.' + +"'Oh,' says she, 'we've set yours aside for you--Jem's bird, we +call it. It's the big white one over yonder. There's twenty-six +of them, which makes one for you, and one for us, and two dozen +for the market.' + +"'Thank you, Maggie,' says I; 'but if it is all the same to you, +I'd rather have that one I was handling just now.' + +"'The other is a good three pound heavier,' said she, 'and we +fattened it expressly for you.' + +"'Never mind. I'll have the other, and I'll take it now,' said I. + +"'Oh, just as you like,' said she, a little huffed. 'Which is it +you want, then?' + +"'That white one with the barred tail, right in the middle of the +flock.' + +"'Oh, very well. Kill it and take it with you.' + +"Well, I did what she said, Mr. Holmes, and I carried the bird +all the way to Kilburn. I told my pal what I had done, for he was +a man that it was easy to tell a thing like that to. He laughed +until he choked, and we got a knife and opened the goose. My +heart turned to water, for there was no sign of the stone, and I +knew that some terrible mistake had occurred. I left the bird, +rushed back to my sister's, and hurried into the back yard. There +was not a bird to be seen there. + +"'Where are they all, Maggie?' I cried. + +"'Gone to the dealer's, Jem.' + +"'Which dealer's?' + +"'Breckinridge, of Covent Garden.' + +"'But was there another with a barred tail?' I asked, 'the same +as the one I chose?' + +"'Yes, Jem; there were two barred-tailed ones, and I could never +tell them apart.' + +"Well, then, of course I saw it all, and I ran off as hard as my +feet would carry me to this man Breckinridge; but he had sold the +lot at once, and not one word would he tell me as to where they +had gone. You heard him yourselves to-night. Well, he has always +answered me like that. My sister thinks that I am going mad. +Sometimes I think that I am myself. And now--and now I am myself +a branded thief, without ever having touched the wealth for which +I sold my character. God help me! God help me!" He burst into +convulsive sobbing, with his face buried in his hands. + +There was a long silence, broken only by his heavy breathing and +by the measured tapping of Sherlock Holmes' finger-tips upon the +edge of the table. Then my friend rose and threw open the door. + +"Get out!" said he. + +"What, sir! Oh, Heaven bless you!" + +"No more words. Get out!" + +And no more words were needed. There was a rush, a clatter upon +the stairs, the bang of a door, and the crisp rattle of running +footfalls from the street. + +"After all, Watson," said Holmes, reaching up his hand for his +clay pipe, "I am not retained by the police to supply their +deficiencies. If Horner were in danger it would be another thing; +but this fellow will not appear against him, and the case must +collapse. I suppose that I am commuting a felony, but it is just +possible that I am saving a soul. This fellow will not go wrong +again; he is too terribly frightened. Send him to gaol now, and +you make him a gaol-bird for life. Besides, it is the season of +forgiveness. Chance has put in our way a most singular and +whimsical problem, and its solution is its own reward. If you +will have the goodness to touch the bell, Doctor, we will begin +another investigation, in which, also a bird will be the chief +feature." + + + +VIII. THE ADVENTURE OF THE SPECKLED BAND + +On glancing over my notes of the seventy odd cases in which I +have during the last eight years studied the methods of my friend +Sherlock Holmes, I find many tragic, some comic, a large number +merely strange, but none commonplace; for, working as he did +rather for the love of his art than for the acquirement of +wealth, he refused to associate himself with any investigation +which did not tend towards the unusual, and even the fantastic. +Of all these varied cases, however, I cannot recall any which +presented more singular features than that which was associated +with the well-known Surrey family of the Roylotts of Stoke Moran. +The events in question occurred in the early days of my +association with Holmes, when we were sharing rooms as bachelors +in Baker Street. It is possible that I might have placed them +upon record before, but a promise of secrecy was made at the +time, from which I have only been freed during the last month by +the untimely death of the lady to whom the pledge was given. It +is perhaps as well that the facts should now come to light, for I +have reasons to know that there are widespread rumours as to the +death of Dr. Grimesby Roylott which tend to make the matter even +more terrible than the truth. + +It was early in April in the year '83 that I woke one morning to +find Sherlock Holmes standing, fully dressed, by the side of my +bed. He was a late riser, as a rule, and as the clock on the +mantelpiece showed me that it was only a quarter-past seven, I +blinked up at him in some surprise, and perhaps just a little +resentment, for I was myself regular in my habits. + +"Very sorry to knock you up, Watson," said he, "but it's the +common lot this morning. Mrs. Hudson has been knocked up, she +retorted upon me, and I on you." + +"What is it, then--a fire?" + +"No; a client. It seems that a young lady has arrived in a +considerable state of excitement, who insists upon seeing me. She +is waiting now in the sitting-room. Now, when young ladies wander +about the metropolis at this hour of the morning, and knock +sleepy people up out of their beds, I presume that it is +something very pressing which they have to communicate. Should it +prove to be an interesting case, you would, I am sure, wish to +follow it from the outset. I thought, at any rate, that I should +call you and give you the chance." + +"My dear fellow, I would not miss it for anything." + +I had no keener pleasure than in following Holmes in his +professional investigations, and in admiring the rapid +deductions, as swift as intuitions, and yet always founded on a +logical basis with which he unravelled the problems which were +submitted to him. I rapidly threw on my clothes and was ready in +a few minutes to accompany my friend down to the sitting-room. A +lady dressed in black and heavily veiled, who had been sitting in +the window, rose as we entered. + +"Good-morning, madam," said Holmes cheerily. "My name is Sherlock +Holmes. This is my intimate friend and associate, Dr. Watson, +before whom you can speak as freely as before myself. Ha! I am +glad to see that Mrs. Hudson has had the good sense to light the +fire. Pray draw up to it, and I shall order you a cup of hot +coffee, for I observe that you are shivering." + +"It is not cold which makes me shiver," said the woman in a low +voice, changing her seat as requested. + +"What, then?" + +"It is fear, Mr. Holmes. It is terror." She raised her veil as +she spoke, and we could see that she was indeed in a pitiable +state of agitation, her face all drawn and grey, with restless +frightened eyes, like those of some hunted animal. Her features +and figure were those of a woman of thirty, but her hair was shot +with premature grey, and her expression was weary and haggard. +Sherlock Holmes ran her over with one of his quick, +all-comprehensive glances. + +"You must not fear," said he soothingly, bending forward and +patting her forearm. "We shall soon set matters right, I have no +doubt. You have come in by train this morning, I see." + +"You know me, then?" + +"No, but I observe the second half of a return ticket in the palm +of your left glove. You must have started early, and yet you had +a good drive in a dog-cart, along heavy roads, before you reached +the station." + +The lady gave a violent start and stared in bewilderment at my +companion. + +"There is no mystery, my dear madam," said he, smiling. "The left +arm of your jacket is spattered with mud in no less than seven +places. The marks are perfectly fresh. There is no vehicle save a +dog-cart which throws up mud in that way, and then only when you +sit on the left-hand side of the driver." + +"Whatever your reasons may be, you are perfectly correct," said +she. "I started from home before six, reached Leatherhead at +twenty past, and came in by the first train to Waterloo. Sir, I +can stand this strain no longer; I shall go mad if it continues. +I have no one to turn to--none, save only one, who cares for me, +and he, poor fellow, can be of little aid. I have heard of you, +Mr. Holmes; I have heard of you from Mrs. Farintosh, whom you +helped in the hour of her sore need. It was from her that I had +your address. Oh, sir, do you not think that you could help me, +too, and at least throw a little light through the dense darkness +which surrounds me? At present it is out of my power to reward +you for your services, but in a month or six weeks I shall be +married, with the control of my own income, and then at least you +shall not find me ungrateful." + +Holmes turned to his desk and, unlocking it, drew out a small +case-book, which he consulted. + +"Farintosh," said he. "Ah yes, I recall the case; it was +concerned with an opal tiara. I think it was before your time, +Watson. I can only say, madam, that I shall be happy to devote +the same care to your case as I did to that of your friend. As to +reward, my profession is its own reward; but you are at liberty +to defray whatever expenses I may be put to, at the time which +suits you best. And now I beg that you will lay before us +everything that may help us in forming an opinion upon the +matter." + +"Alas!" replied our visitor, "the very horror of my situation +lies in the fact that my fears are so vague, and my suspicions +depend so entirely upon small points, which might seem trivial to +another, that even he to whom of all others I have a right to +look for help and advice looks upon all that I tell him about it +as the fancies of a nervous woman. He does not say so, but I can +read it from his soothing answers and averted eyes. But I have +heard, Mr. Holmes, that you can see deeply into the manifold +wickedness of the human heart. You may advise me how to walk amid +the dangers which encompass me." + +"I am all attention, madam." + +"My name is Helen Stoner, and I am living with my stepfather, who +is the last survivor of one of the oldest Saxon families in +England, the Roylotts of Stoke Moran, on the western border of +Surrey." + +Holmes nodded his head. "The name is familiar to me," said he. + +"The family was at one time among the richest in England, and the +estates extended over the borders into Berkshire in the north, +and Hampshire in the west. In the last century, however, four +successive heirs were of a dissolute and wasteful disposition, +and the family ruin was eventually completed by a gambler in the +days of the Regency. Nothing was left save a few acres of ground, +and the two-hundred-year-old house, which is itself crushed under +a heavy mortgage. The last squire dragged out his existence +there, living the horrible life of an aristocratic pauper; but +his only son, my stepfather, seeing that he must adapt himself to +the new conditions, obtained an advance from a relative, which +enabled him to take a medical degree and went out to Calcutta, +where, by his professional skill and his force of character, he +established a large practice. In a fit of anger, however, caused +by some robberies which had been perpetrated in the house, he +beat his native butler to death and narrowly escaped a capital +sentence. As it was, he suffered a long term of imprisonment and +afterwards returned to England a morose and disappointed man. + +"When Dr. Roylott was in India he married my mother, Mrs. Stoner, +the young widow of Major-General Stoner, of the Bengal Artillery. +My sister Julia and I were twins, and we were only two years old +at the time of my mother's re-marriage. She had a considerable +sum of money--not less than 1000 pounds a year--and this she +bequeathed to Dr. Roylott entirely while we resided with him, +with a provision that a certain annual sum should be allowed to +each of us in the event of our marriage. Shortly after our return +to England my mother died--she was killed eight years ago in a +railway accident near Crewe. Dr. Roylott then abandoned his +attempts to establish himself in practice in London and took us +to live with him in the old ancestral house at Stoke Moran. The +money which my mother had left was enough for all our wants, and +there seemed to be no obstacle to our happiness. + +"But a terrible change came over our stepfather about this time. +Instead of making friends and exchanging visits with our +neighbours, who had at first been overjoyed to see a Roylott of +Stoke Moran back in the old family seat, he shut himself up in +his house and seldom came out save to indulge in ferocious +quarrels with whoever might cross his path. Violence of temper +approaching to mania has been hereditary in the men of the +family, and in my stepfather's case it had, I believe, been +intensified by his long residence in the tropics. A series of +disgraceful brawls took place, two of which ended in the +police-court, until at last he became the terror of the village, +and the folks would fly at his approach, for he is a man of +immense strength, and absolutely uncontrollable in his anger. + +"Last week he hurled the local blacksmith over a parapet into a +stream, and it was only by paying over all the money which I +could gather together that I was able to avert another public +exposure. He had no friends at all save the wandering gipsies, +and he would give these vagabonds leave to encamp upon the few +acres of bramble-covered land which represent the family estate, +and would accept in return the hospitality of their tents, +wandering away with them sometimes for weeks on end. He has a +passion also for Indian animals, which are sent over to him by a +correspondent, and he has at this moment a cheetah and a baboon, +which wander freely over his grounds and are feared by the +villagers almost as much as their master. + +"You can imagine from what I say that my poor sister Julia and I +had no great pleasure in our lives. No servant would stay with +us, and for a long time we did all the work of the house. She was +but thirty at the time of her death, and yet her hair had already +begun to whiten, even as mine has." + +"Your sister is dead, then?" + +"She died just two years ago, and it is of her death that I wish +to speak to you. You can understand that, living the life which I +have described, we were little likely to see anyone of our own +age and position. We had, however, an aunt, my mother's maiden +sister, Miss Honoria Westphail, who lives near Harrow, and we +were occasionally allowed to pay short visits at this lady's +house. Julia went there at Christmas two years ago, and met there +a half-pay major of marines, to whom she became engaged. My +stepfather learned of the engagement when my sister returned and +offered no objection to the marriage; but within a fortnight of +the day which had been fixed for the wedding, the terrible event +occurred which has deprived me of my only companion." + +Sherlock Holmes had been leaning back in his chair with his eyes +closed and his head sunk in a cushion, but he half opened his +lids now and glanced across at his visitor. + +"Pray be precise as to details," said he. + +"It is easy for me to be so, for every event of that dreadful +time is seared into my memory. The manor-house is, as I have +already said, very old, and only one wing is now inhabited. The +bedrooms in this wing are on the ground floor, the sitting-rooms +being in the central block of the buildings. Of these bedrooms +the first is Dr. Roylott's, the second my sister's, and the third +my own. There is no communication between them, but they all open +out into the same corridor. Do I make myself plain?" + +"Perfectly so." + +"The windows of the three rooms open out upon the lawn. That +fatal night Dr. Roylott had gone to his room early, though we +knew that he had not retired to rest, for my sister was troubled +by the smell of the strong Indian cigars which it was his custom +to smoke. She left her room, therefore, and came into mine, where +she sat for some time, chatting about her approaching wedding. At +eleven o'clock she rose to leave me, but she paused at the door +and looked back. + +"'Tell me, Helen,' said she, 'have you ever heard anyone whistle +in the dead of the night?' + +"'Never,' said I. + +"'I suppose that you could not possibly whistle, yourself, in +your sleep?' + +"'Certainly not. But why?' + +"'Because during the last few nights I have always, about three +in the morning, heard a low, clear whistle. I am a light sleeper, +and it has awakened me. I cannot tell where it came from--perhaps +from the next room, perhaps from the lawn. I thought that I would +just ask you whether you had heard it.' + +"'No, I have not. It must be those wretched gipsies in the +plantation.' + +"'Very likely. And yet if it were on the lawn, I wonder that you +did not hear it also.' + +"'Ah, but I sleep more heavily than you.' + +"'Well, it is of no great consequence, at any rate.' She smiled +back at me, closed my door, and a few moments later I heard her +key turn in the lock." + +"Indeed," said Holmes. "Was it your custom always to lock +yourselves in at night?" + +"Always." + +"And why?" + +"I think that I mentioned to you that the doctor kept a cheetah +and a baboon. We had no feeling of security unless our doors were +locked." + +"Quite so. Pray proceed with your statement." + +"I could not sleep that night. A vague feeling of impending +misfortune impressed me. My sister and I, you will recollect, +were twins, and you know how subtle are the links which bind two +souls which are so closely allied. It was a wild night. The wind +was howling outside, and the rain was beating and splashing +against the windows. Suddenly, amid all the hubbub of the gale, +there burst forth the wild scream of a terrified woman. I knew +that it was my sister's voice. I sprang from my bed, wrapped a +shawl round me, and rushed into the corridor. As I opened my door +I seemed to hear a low whistle, such as my sister described, and +a few moments later a clanging sound, as if a mass of metal had +fallen. As I ran down the passage, my sister's door was unlocked, +and revolved slowly upon its hinges. I stared at it +horror-stricken, not knowing what was about to issue from it. By +the light of the corridor-lamp I saw my sister appear at the +opening, her face blanched with terror, her hands groping for +help, her whole figure swaying to and fro like that of a +drunkard. I ran to her and threw my arms round her, but at that +moment her knees seemed to give way and she fell to the ground. +She writhed as one who is in terrible pain, and her limbs were +dreadfully convulsed. At first I thought that she had not +recognised me, but as I bent over her she suddenly shrieked out +in a voice which I shall never forget, 'Oh, my God! Helen! It was +the band! The speckled band!' There was something else which she +would fain have said, and she stabbed with her finger into the +air in the direction of the doctor's room, but a fresh convulsion +seized her and choked her words. I rushed out, calling loudly for +my stepfather, and I met him hastening from his room in his +dressing-gown. When he reached my sister's side she was +unconscious, and though he poured brandy down her throat and sent +for medical aid from the village, all efforts were in vain, for +she slowly sank and died without having recovered her +consciousness. Such was the dreadful end of my beloved sister." + +"One moment," said Holmes, "are you sure about this whistle and +metallic sound? Could you swear to it?" + +"That was what the county coroner asked me at the inquiry. It is +my strong impression that I heard it, and yet, among the crash of +the gale and the creaking of an old house, I may possibly have +been deceived." + +"Was your sister dressed?" + +"No, she was in her night-dress. In her right hand was found the +charred stump of a match, and in her left a match-box." + +"Showing that she had struck a light and looked about her when +the alarm took place. That is important. And what conclusions did +the coroner come to?" + +"He investigated the case with great care, for Dr. Roylott's +conduct had long been notorious in the county, but he was unable +to find any satisfactory cause of death. My evidence showed that +the door had been fastened upon the inner side, and the windows +were blocked by old-fashioned shutters with broad iron bars, +which were secured every night. The walls were carefully sounded, +and were shown to be quite solid all round, and the flooring was +also thoroughly examined, with the same result. The chimney is +wide, but is barred up by four large staples. It is certain, +therefore, that my sister was quite alone when she met her end. +Besides, there were no marks of any violence upon her." + +"How about poison?" + +"The doctors examined her for it, but without success." + +"What do you think that this unfortunate lady died of, then?" + +"It is my belief that she died of pure fear and nervous shock, +though what it was that frightened her I cannot imagine." + +"Were there gipsies in the plantation at the time?" + +"Yes, there are nearly always some there." + +"Ah, and what did you gather from this allusion to a band--a +speckled band?" + +"Sometimes I have thought that it was merely the wild talk of +delirium, sometimes that it may have referred to some band of +people, perhaps to these very gipsies in the plantation. I do not +know whether the spotted handkerchiefs which so many of them wear +over their heads might have suggested the strange adjective which +she used." + +Holmes shook his head like a man who is far from being satisfied. + +"These are very deep waters," said he; "pray go on with your +narrative." + +"Two years have passed since then, and my life has been until +lately lonelier than ever. A month ago, however, a dear friend, +whom I have known for many years, has done me the honour to ask +my hand in marriage. His name is Armitage--Percy Armitage--the +second son of Mr. Armitage, of Crane Water, near Reading. My +stepfather has offered no opposition to the match, and we are to +be married in the course of the spring. Two days ago some repairs +were started in the west wing of the building, and my bedroom +wall has been pierced, so that I have had to move into the +chamber in which my sister died, and to sleep in the very bed in +which she slept. Imagine, then, my thrill of terror when last +night, as I lay awake, thinking over her terrible fate, I +suddenly heard in the silence of the night the low whistle which +had been the herald of her own death. I sprang up and lit the +lamp, but nothing was to be seen in the room. I was too shaken to +go to bed again, however, so I dressed, and as soon as it was +daylight I slipped down, got a dog-cart at the Crown Inn, which +is opposite, and drove to Leatherhead, from whence I have come on +this morning with the one object of seeing you and asking your +advice." + +"You have done wisely," said my friend. "But have you told me +all?" + +"Yes, all." + +"Miss Roylott, you have not. You are screening your stepfather." + +"Why, what do you mean?" + +For answer Holmes pushed back the frill of black lace which +fringed the hand that lay upon our visitor's knee. Five little +livid spots, the marks of four fingers and a thumb, were printed +upon the white wrist. + +"You have been cruelly used," said Holmes. + +The lady coloured deeply and covered over her injured wrist. "He +is a hard man," she said, "and perhaps he hardly knows his own +strength." + +There was a long silence, during which Holmes leaned his chin +upon his hands and stared into the crackling fire. + +"This is a very deep business," he said at last. "There are a +thousand details which I should desire to know before I decide +upon our course of action. Yet we have not a moment to lose. If +we were to come to Stoke Moran to-day, would it be possible for +us to see over these rooms without the knowledge of your +stepfather?" + +"As it happens, he spoke of coming into town to-day upon some +most important business. It is probable that he will be away all +day, and that there would be nothing to disturb you. We have a +housekeeper now, but she is old and foolish, and I could easily +get her out of the way." + +"Excellent. You are not averse to this trip, Watson?" + +"By no means." + +"Then we shall both come. What are you going to do yourself?" + +"I have one or two things which I would wish to do now that I am +in town. But I shall return by the twelve o'clock train, so as to +be there in time for your coming." + +"And you may expect us early in the afternoon. I have myself some +small business matters to attend to. Will you not wait and +breakfast?" + +"No, I must go. My heart is lightened already since I have +confided my trouble to you. I shall look forward to seeing you +again this afternoon." She dropped her thick black veil over her +face and glided from the room. + +"And what do you think of it all, Watson?" asked Sherlock Holmes, +leaning back in his chair. + +"It seems to me to be a most dark and sinister business." + +"Dark enough and sinister enough." + +"Yet if the lady is correct in saying that the flooring and walls +are sound, and that the door, window, and chimney are impassable, +then her sister must have been undoubtedly alone when she met her +mysterious end." + +"What becomes, then, of these nocturnal whistles, and what of the +very peculiar words of the dying woman?" + +"I cannot think." + +"When you combine the ideas of whistles at night, the presence of +a band of gipsies who are on intimate terms with this old doctor, +the fact that we have every reason to believe that the doctor has +an interest in preventing his stepdaughter's marriage, the dying +allusion to a band, and, finally, the fact that Miss Helen Stoner +heard a metallic clang, which might have been caused by one of +those metal bars that secured the shutters falling back into its +place, I think that there is good ground to think that the +mystery may be cleared along those lines." + +"But what, then, did the gipsies do?" + +"I cannot imagine." + +"I see many objections to any such theory." + +"And so do I. It is precisely for that reason that we are going +to Stoke Moran this day. I want to see whether the objections are +fatal, or if they may be explained away. But what in the name of +the devil!" + +The ejaculation had been drawn from my companion by the fact that +our door had been suddenly dashed open, and that a huge man had +framed himself in the aperture. His costume was a peculiar +mixture of the professional and of the agricultural, having a +black top-hat, a long frock-coat, and a pair of high gaiters, +with a hunting-crop swinging in his hand. So tall was he that his +hat actually brushed the cross bar of the doorway, and his +breadth seemed to span it across from side to side. A large face, +seared with a thousand wrinkles, burned yellow with the sun, and +marked with every evil passion, was turned from one to the other +of us, while his deep-set, bile-shot eyes, and his high, thin, +fleshless nose, gave him somewhat the resemblance to a fierce old +bird of prey. + +"Which of you is Holmes?" asked this apparition. + +"My name, sir; but you have the advantage of me," said my +companion quietly. + +"I am Dr. Grimesby Roylott, of Stoke Moran." + +"Indeed, Doctor," said Holmes blandly. "Pray take a seat." + +"I will do nothing of the kind. My stepdaughter has been here. I +have traced her. What has she been saying to you?" + +"It is a little cold for the time of the year," said Holmes. + +"What has she been saying to you?" screamed the old man +furiously. + +"But I have heard that the crocuses promise well," continued my +companion imperturbably. + +"Ha! You put me off, do you?" said our new visitor, taking a step +forward and shaking his hunting-crop. "I know you, you scoundrel! +I have heard of you before. You are Holmes, the meddler." + +My friend smiled. + +"Holmes, the busybody!" + +His smile broadened. + +"Holmes, the Scotland Yard Jack-in-office!" + +Holmes chuckled heartily. "Your conversation is most +entertaining," said he. "When you go out close the door, for +there is a decided draught." + +"I will go when I have said my say. Don't you dare to meddle with +my affairs. I know that Miss Stoner has been here. I traced her! +I am a dangerous man to fall foul of! See here." He stepped +swiftly forward, seized the poker, and bent it into a curve with +his huge brown hands. + +"See that you keep yourself out of my grip," he snarled, and +hurling the twisted poker into the fireplace he strode out of the +room. + +"He seems a very amiable person," said Holmes, laughing. "I am +not quite so bulky, but if he had remained I might have shown him +that my grip was not much more feeble than his own." As he spoke +he picked up the steel poker and, with a sudden effort, +straightened it out again. + +"Fancy his having the insolence to confound me with the official +detective force! This incident gives zest to our investigation, +however, and I only trust that our little friend will not suffer +from her imprudence in allowing this brute to trace her. And now, +Watson, we shall order breakfast, and afterwards I shall walk +down to Doctors' Commons, where I hope to get some data which may +help us in this matter." + + +It was nearly one o'clock when Sherlock Holmes returned from his +excursion. He held in his hand a sheet of blue paper, scrawled +over with notes and figures. + +"I have seen the will of the deceased wife," said he. "To +determine its exact meaning I have been obliged to work out the +present prices of the investments with which it is concerned. The +total income, which at the time of the wife's death was little +short of 1100 pounds, is now, through the fall in agricultural +prices, not more than 750 pounds. Each daughter can claim an +income of 250 pounds, in case of marriage. It is evident, +therefore, that if both girls had married, this beauty would have +had a mere pittance, while even one of them would cripple him to +a very serious extent. My morning's work has not been wasted, +since it has proved that he has the very strongest motives for +standing in the way of anything of the sort. And now, Watson, +this is too serious for dawdling, especially as the old man is +aware that we are interesting ourselves in his affairs; so if you +are ready, we shall call a cab and drive to Waterloo. I should be +very much obliged if you would slip your revolver into your +pocket. An Eley's No. 2 is an excellent argument with gentlemen +who can twist steel pokers into knots. That and a tooth-brush +are, I think, all that we need." + +At Waterloo we were fortunate in catching a train for +Leatherhead, where we hired a trap at the station inn and drove +for four or five miles through the lovely Surrey lanes. It was a +perfect day, with a bright sun and a few fleecy clouds in the +heavens. The trees and wayside hedges were just throwing out +their first green shoots, and the air was full of the pleasant +smell of the moist earth. To me at least there was a strange +contrast between the sweet promise of the spring and this +sinister quest upon which we were engaged. My companion sat in +the front of the trap, his arms folded, his hat pulled down over +his eyes, and his chin sunk upon his breast, buried in the +deepest thought. Suddenly, however, he started, tapped me on the +shoulder, and pointed over the meadows. + +"Look there!" said he. + +A heavily timbered park stretched up in a gentle slope, +thickening into a grove at the highest point. From amid the +branches there jutted out the grey gables and high roof-tree of a +very old mansion. + +"Stoke Moran?" said he. + +"Yes, sir, that be the house of Dr. Grimesby Roylott," remarked +the driver. + +"There is some building going on there," said Holmes; "that is +where we are going." + +"There's the village," said the driver, pointing to a cluster of +roofs some distance to the left; "but if you want to get to the +house, you'll find it shorter to get over this stile, and so by +the foot-path over the fields. There it is, where the lady is +walking." + +"And the lady, I fancy, is Miss Stoner," observed Holmes, shading +his eyes. "Yes, I think we had better do as you suggest." + +We got off, paid our fare, and the trap rattled back on its way +to Leatherhead. + +"I thought it as well," said Holmes as we climbed the stile, +"that this fellow should think we had come here as architects, or +on some definite business. It may stop his gossip. +Good-afternoon, Miss Stoner. You see that we have been as good as +our word." + +Our client of the morning had hurried forward to meet us with a +face which spoke her joy. "I have been waiting so eagerly for +you," she cried, shaking hands with us warmly. "All has turned +out splendidly. Dr. Roylott has gone to town, and it is unlikely +that he will be back before evening." + +"We have had the pleasure of making the doctor's acquaintance," +said Holmes, and in a few words he sketched out what had +occurred. Miss Stoner turned white to the lips as she listened. + +"Good heavens!" she cried, "he has followed me, then." + +"So it appears." + +"He is so cunning that I never know when I am safe from him. What +will he say when he returns?" + +"He must guard himself, for he may find that there is someone +more cunning than himself upon his track. You must lock yourself +up from him to-night. If he is violent, we shall take you away to +your aunt's at Harrow. Now, we must make the best use of our +time, so kindly take us at once to the rooms which we are to +examine." + +The building was of grey, lichen-blotched stone, with a high +central portion and two curving wings, like the claws of a crab, +thrown out on each side. In one of these wings the windows were +broken and blocked with wooden boards, while the roof was partly +caved in, a picture of ruin. The central portion was in little +better repair, but the right-hand block was comparatively modern, +and the blinds in the windows, with the blue smoke curling up +from the chimneys, showed that this was where the family resided. +Some scaffolding had been erected against the end wall, and the +stone-work had been broken into, but there were no signs of any +workmen at the moment of our visit. Holmes walked slowly up and +down the ill-trimmed lawn and examined with deep attention the +outsides of the windows. + +"This, I take it, belongs to the room in which you used to sleep, +the centre one to your sister's, and the one next to the main +building to Dr. Roylott's chamber?" + +"Exactly so. But I am now sleeping in the middle one." + +"Pending the alterations, as I understand. By the way, there does +not seem to be any very pressing need for repairs at that end +wall." + +"There were none. I believe that it was an excuse to move me from +my room." + +"Ah! that is suggestive. Now, on the other side of this narrow +wing runs the corridor from which these three rooms open. There +are windows in it, of course?" + +"Yes, but very small ones. Too narrow for anyone to pass +through." + +"As you both locked your doors at night, your rooms were +unapproachable from that side. Now, would you have the kindness +to go into your room and bar your shutters?" + +Miss Stoner did so, and Holmes, after a careful examination +through the open window, endeavoured in every way to force the +shutter open, but without success. There was no slit through +which a knife could be passed to raise the bar. Then with his +lens he tested the hinges, but they were of solid iron, built +firmly into the massive masonry. "Hum!" said he, scratching his +chin in some perplexity, "my theory certainly presents some +difficulties. No one could pass these shutters if they were +bolted. Well, we shall see if the inside throws any light upon +the matter." + +A small side door led into the whitewashed corridor from which +the three bedrooms opened. Holmes refused to examine the third +chamber, so we passed at once to the second, that in which Miss +Stoner was now sleeping, and in which her sister had met with her +fate. It was a homely little room, with a low ceiling and a +gaping fireplace, after the fashion of old country-houses. A +brown chest of drawers stood in one corner, a narrow +white-counterpaned bed in another, and a dressing-table on the +left-hand side of the window. These articles, with two small +wicker-work chairs, made up all the furniture in the room save +for a square of Wilton carpet in the centre. The boards round and +the panelling of the walls were of brown, worm-eaten oak, so old +and discoloured that it may have dated from the original building +of the house. Holmes drew one of the chairs into a corner and sat +silent, while his eyes travelled round and round and up and down, +taking in every detail of the apartment. + +"Where does that bell communicate with?" he asked at last +pointing to a thick bell-rope which hung down beside the bed, the +tassel actually lying upon the pillow. + +"It goes to the housekeeper's room." + +"It looks newer than the other things?" + +"Yes, it was only put there a couple of years ago." + +"Your sister asked for it, I suppose?" + +"No, I never heard of her using it. We used always to get what we +wanted for ourselves." + +"Indeed, it seemed unnecessary to put so nice a bell-pull there. +You will excuse me for a few minutes while I satisfy myself as to +this floor." He threw himself down upon his face with his lens in +his hand and crawled swiftly backward and forward, examining +minutely the cracks between the boards. Then he did the same with +the wood-work with which the chamber was panelled. Finally he +walked over to the bed and spent some time in staring at it and +in running his eye up and down the wall. Finally he took the +bell-rope in his hand and gave it a brisk tug. + +"Why, it's a dummy," said he. + +"Won't it ring?" + +"No, it is not even attached to a wire. This is very interesting. +You can see now that it is fastened to a hook just above where +the little opening for the ventilator is." + +"How very absurd! I never noticed that before." + +"Very strange!" muttered Holmes, pulling at the rope. "There are +one or two very singular points about this room. For example, +what a fool a builder must be to open a ventilator into another +room, when, with the same trouble, he might have communicated +with the outside air!" + +"That is also quite modern," said the lady. + +"Done about the same time as the bell-rope?" remarked Holmes. + +"Yes, there were several little changes carried out about that +time." + +"They seem to have been of a most interesting character--dummy +bell-ropes, and ventilators which do not ventilate. With your +permission, Miss Stoner, we shall now carry our researches into +the inner apartment." + +Dr. Grimesby Roylott's chamber was larger than that of his +step-daughter, but was as plainly furnished. A camp-bed, a small +wooden shelf full of books, mostly of a technical character, an +armchair beside the bed, a plain wooden chair against the wall, a +round table, and a large iron safe were the principal things +which met the eye. Holmes walked slowly round and examined each +and all of them with the keenest interest. + +"What's in here?" he asked, tapping the safe. + +"My stepfather's business papers." + +"Oh! you have seen inside, then?" + +"Only once, some years ago. I remember that it was full of +papers." + +"There isn't a cat in it, for example?" + +"No. What a strange idea!" + +"Well, look at this!" He took up a small saucer of milk which +stood on the top of it. + +"No; we don't keep a cat. But there is a cheetah and a baboon." + +"Ah, yes, of course! Well, a cheetah is just a big cat, and yet a +saucer of milk does not go very far in satisfying its wants, I +daresay. There is one point which I should wish to determine." He +squatted down in front of the wooden chair and examined the seat +of it with the greatest attention. + +"Thank you. That is quite settled," said he, rising and putting +his lens in his pocket. "Hullo! Here is something interesting!" + +The object which had caught his eye was a small dog lash hung on +one corner of the bed. The lash, however, was curled upon itself +and tied so as to make a loop of whipcord. + +"What do you make of that, Watson?" + +"It's a common enough lash. But I don't know why it should be +tied." + +"That is not quite so common, is it? Ah, me! it's a wicked world, +and when a clever man turns his brains to crime it is the worst +of all. I think that I have seen enough now, Miss Stoner, and +with your permission we shall walk out upon the lawn." + +I had never seen my friend's face so grim or his brow so dark as +it was when we turned from the scene of this investigation. We +had walked several times up and down the lawn, neither Miss +Stoner nor myself liking to break in upon his thoughts before he +roused himself from his reverie. + +"It is very essential, Miss Stoner," said he, "that you should +absolutely follow my advice in every respect." + +"I shall most certainly do so." + +"The matter is too serious for any hesitation. Your life may +depend upon your compliance." + +"I assure you that I am in your hands." + +"In the first place, both my friend and I must spend the night in +your room." + +Both Miss Stoner and I gazed at him in astonishment. + +"Yes, it must be so. Let me explain. I believe that that is the +village inn over there?" + +"Yes, that is the Crown." + +"Very good. Your windows would be visible from there?" + +"Certainly." + +"You must confine yourself to your room, on pretence of a +headache, when your stepfather comes back. Then when you hear him +retire for the night, you must open the shutters of your window, +undo the hasp, put your lamp there as a signal to us, and then +withdraw quietly with everything which you are likely to want +into the room which you used to occupy. I have no doubt that, in +spite of the repairs, you could manage there for one night." + +"Oh, yes, easily." + +"The rest you will leave in our hands." + +"But what will you do?" + +"We shall spend the night in your room, and we shall investigate +the cause of this noise which has disturbed you." + +"I believe, Mr. Holmes, that you have already made up your mind," +said Miss Stoner, laying her hand upon my companion's sleeve. + +"Perhaps I have." + +"Then, for pity's sake, tell me what was the cause of my sister's +death." + +"I should prefer to have clearer proofs before I speak." + +"You can at least tell me whether my own thought is correct, and +if she died from some sudden fright." + +"No, I do not think so. I think that there was probably some more +tangible cause. And now, Miss Stoner, we must leave you for if +Dr. Roylott returned and saw us our journey would be in vain. +Good-bye, and be brave, for if you will do what I have told you, +you may rest assured that we shall soon drive away the dangers +that threaten you." + +Sherlock Holmes and I had no difficulty in engaging a bedroom and +sitting-room at the Crown Inn. They were on the upper floor, and +from our window we could command a view of the avenue gate, and +of the inhabited wing of Stoke Moran Manor House. At dusk we saw +Dr. Grimesby Roylott drive past, his huge form looming up beside +the little figure of the lad who drove him. The boy had some +slight difficulty in undoing the heavy iron gates, and we heard +the hoarse roar of the doctor's voice and saw the fury with which +he shook his clinched fists at him. The trap drove on, and a few +minutes later we saw a sudden light spring up among the trees as +the lamp was lit in one of the sitting-rooms. + +"Do you know, Watson," said Holmes as we sat together in the +gathering darkness, "I have really some scruples as to taking you +to-night. There is a distinct element of danger." + +"Can I be of assistance?" + +"Your presence might be invaluable." + +"Then I shall certainly come." + +"It is very kind of you." + +"You speak of danger. You have evidently seen more in these rooms +than was visible to me." + +"No, but I fancy that I may have deduced a little more. I imagine +that you saw all that I did." + +"I saw nothing remarkable save the bell-rope, and what purpose +that could answer I confess is more than I can imagine." + +"You saw the ventilator, too?" + +"Yes, but I do not think that it is such a very unusual thing to +have a small opening between two rooms. It was so small that a +rat could hardly pass through." + +"I knew that we should find a ventilator before ever we came to +Stoke Moran." + +"My dear Holmes!" + +"Oh, yes, I did. You remember in her statement she said that her +sister could smell Dr. Roylott's cigar. Now, of course that +suggested at once that there must be a communication between the +two rooms. It could only be a small one, or it would have been +remarked upon at the coroner's inquiry. I deduced a ventilator." + +"But what harm can there be in that?" + +"Well, there is at least a curious coincidence of dates. A +ventilator is made, a cord is hung, and a lady who sleeps in the +bed dies. Does not that strike you?" + +"I cannot as yet see any connection." + +"Did you observe anything very peculiar about that bed?" + +"No." + +"It was clamped to the floor. Did you ever see a bed fastened +like that before?" + +"I cannot say that I have." + +"The lady could not move her bed. It must always be in the same +relative position to the ventilator and to the rope--or so we may +call it, since it was clearly never meant for a bell-pull." + +"Holmes," I cried, "I seem to see dimly what you are hinting at. +We are only just in time to prevent some subtle and horrible +crime." + +"Subtle enough and horrible enough. When a doctor does go wrong +he is the first of criminals. He has nerve and he has knowledge. +Palmer and Pritchard were among the heads of their profession. +This man strikes even deeper, but I think, Watson, that we shall +be able to strike deeper still. But we shall have horrors enough +before the night is over; for goodness' sake let us have a quiet +pipe and turn our minds for a few hours to something more +cheerful." + + +About nine o'clock the light among the trees was extinguished, +and all was dark in the direction of the Manor House. Two hours +passed slowly away, and then, suddenly, just at the stroke of +eleven, a single bright light shone out right in front of us. + +"That is our signal," said Holmes, springing to his feet; "it +comes from the middle window." + +As we passed out he exchanged a few words with the landlord, +explaining that we were going on a late visit to an acquaintance, +and that it was possible that we might spend the night there. A +moment later we were out on the dark road, a chill wind blowing +in our faces, and one yellow light twinkling in front of us +through the gloom to guide us on our sombre errand. + +There was little difficulty in entering the grounds, for +unrepaired breaches gaped in the old park wall. Making our way +among the trees, we reached the lawn, crossed it, and were about +to enter through the window when out from a clump of laurel +bushes there darted what seemed to be a hideous and distorted +child, who threw itself upon the grass with writhing limbs and +then ran swiftly across the lawn into the darkness. + +"My God!" I whispered; "did you see it?" + +Holmes was for the moment as startled as I. His hand closed like +a vice upon my wrist in his agitation. Then he broke into a low +laugh and put his lips to my ear. + +"It is a nice household," he murmured. "That is the baboon." + +I had forgotten the strange pets which the doctor affected. There +was a cheetah, too; perhaps we might find it upon our shoulders +at any moment. I confess that I felt easier in my mind when, +after following Holmes' example and slipping off my shoes, I +found myself inside the bedroom. My companion noiselessly closed +the shutters, moved the lamp onto the table, and cast his eyes +round the room. All was as we had seen it in the daytime. Then +creeping up to me and making a trumpet of his hand, he whispered +into my ear again so gently that it was all that I could do to +distinguish the words: + +"The least sound would be fatal to our plans." + +I nodded to show that I had heard. + +"We must sit without light. He would see it through the +ventilator." + +I nodded again. + +"Do not go asleep; your very life may depend upon it. Have your +pistol ready in case we should need it. I will sit on the side of +the bed, and you in that chair." + +I took out my revolver and laid it on the corner of the table. + +Holmes had brought up a long thin cane, and this he placed upon +the bed beside him. By it he laid the box of matches and the +stump of a candle. Then he turned down the lamp, and we were left +in darkness. + +How shall I ever forget that dreadful vigil? I could not hear a +sound, not even the drawing of a breath, and yet I knew that my +companion sat open-eyed, within a few feet of me, in the same +state of nervous tension in which I was myself. The shutters cut +off the least ray of light, and we waited in absolute darkness. + +From outside came the occasional cry of a night-bird, and once at +our very window a long drawn catlike whine, which told us that +the cheetah was indeed at liberty. Far away we could hear the +deep tones of the parish clock, which boomed out every quarter of +an hour. How long they seemed, those quarters! Twelve struck, and +one and two and three, and still we sat waiting silently for +whatever might befall. + +Suddenly there was the momentary gleam of a light up in the +direction of the ventilator, which vanished immediately, but was +succeeded by a strong smell of burning oil and heated metal. +Someone in the next room had lit a dark-lantern. I heard a gentle +sound of movement, and then all was silent once more, though the +smell grew stronger. For half an hour I sat with straining ears. +Then suddenly another sound became audible--a very gentle, +soothing sound, like that of a small jet of steam escaping +continually from a kettle. The instant that we heard it, Holmes +sprang from the bed, struck a match, and lashed furiously with +his cane at the bell-pull. + +"You see it, Watson?" he yelled. "You see it?" + +But I saw nothing. At the moment when Holmes struck the light I +heard a low, clear whistle, but the sudden glare flashing into my +weary eyes made it impossible for me to tell what it was at which +my friend lashed so savagely. I could, however, see that his face +was deadly pale and filled with horror and loathing. He had +ceased to strike and was gazing up at the ventilator when +suddenly there broke from the silence of the night the most +horrible cry to which I have ever listened. It swelled up louder +and louder, a hoarse yell of pain and fear and anger all mingled +in the one dreadful shriek. They say that away down in the +village, and even in the distant parsonage, that cry raised the +sleepers from their beds. It struck cold to our hearts, and I +stood gazing at Holmes, and he at me, until the last echoes of it +had died away into the silence from which it rose. + +"What can it mean?" I gasped. + +"It means that it is all over," Holmes answered. "And perhaps, +after all, it is for the best. Take your pistol, and we will +enter Dr. Roylott's room." + +With a grave face he lit the lamp and led the way down the +corridor. Twice he struck at the chamber door without any reply +from within. Then he turned the handle and entered, I at his +heels, with the cocked pistol in my hand. + +It was a singular sight which met our eyes. On the table stood a +dark-lantern with the shutter half open, throwing a brilliant +beam of light upon the iron safe, the door of which was ajar. +Beside this table, on the wooden chair, sat Dr. Grimesby Roylott +clad in a long grey dressing-gown, his bare ankles protruding +beneath, and his feet thrust into red heelless Turkish slippers. +Across his lap lay the short stock with the long lash which we +had noticed during the day. His chin was cocked upward and his +eyes were fixed in a dreadful, rigid stare at the corner of the +ceiling. Round his brow he had a peculiar yellow band, with +brownish speckles, which seemed to be bound tightly round his +head. As we entered he made neither sound nor motion. + +"The band! the speckled band!" whispered Holmes. + +I took a step forward. In an instant his strange headgear began +to move, and there reared itself from among his hair the squat +diamond-shaped head and puffed neck of a loathsome serpent. + +"It is a swamp adder!" cried Holmes; "the deadliest snake in +India. He has died within ten seconds of being bitten. Violence +does, in truth, recoil upon the violent, and the schemer falls +into the pit which he digs for another. Let us thrust this +creature back into its den, and we can then remove Miss Stoner to +some place of shelter and let the county police know what has +happened." + +As he spoke he drew the dog-whip swiftly from the dead man's lap, +and throwing the noose round the reptile's neck he drew it from +its horrid perch and, carrying it at arm's length, threw it into +the iron safe, which he closed upon it. + +Such are the true facts of the death of Dr. Grimesby Roylott, of +Stoke Moran. It is not necessary that I should prolong a +narrative which has already run to too great a length by telling +how we broke the sad news to the terrified girl, how we conveyed +her by the morning train to the care of her good aunt at Harrow, +of how the slow process of official inquiry came to the +conclusion that the doctor met his fate while indiscreetly +playing with a dangerous pet. The little which I had yet to learn +of the case was told me by Sherlock Holmes as we travelled back +next day. + +"I had," said he, "come to an entirely erroneous conclusion which +shows, my dear Watson, how dangerous it always is to reason from +insufficient data. The presence of the gipsies, and the use of +the word 'band,' which was used by the poor girl, no doubt, to +explain the appearance which she had caught a hurried glimpse of +by the light of her match, were sufficient to put me upon an +entirely wrong scent. I can only claim the merit that I instantly +reconsidered my position when, however, it became clear to me +that whatever danger threatened an occupant of the room could not +come either from the window or the door. My attention was +speedily drawn, as I have already remarked to you, to this +ventilator, and to the bell-rope which hung down to the bed. The +discovery that this was a dummy, and that the bed was clamped to +the floor, instantly gave rise to the suspicion that the rope was +there as a bridge for something passing through the hole and +coming to the bed. The idea of a snake instantly occurred to me, +and when I coupled it with my knowledge that the doctor was +furnished with a supply of creatures from India, I felt that I +was probably on the right track. The idea of using a form of +poison which could not possibly be discovered by any chemical +test was just such a one as would occur to a clever and ruthless +man who had had an Eastern training. The rapidity with which such +a poison would take effect would also, from his point of view, be +an advantage. It would be a sharp-eyed coroner, indeed, who could +distinguish the two little dark punctures which would show where +the poison fangs had done their work. Then I thought of the +whistle. Of course he must recall the snake before the morning +light revealed it to the victim. He had trained it, probably by +the use of the milk which we saw, to return to him when summoned. +He would put it through this ventilator at the hour that he +thought best, with the certainty that it would crawl down the +rope and land on the bed. It might or might not bite the +occupant, perhaps she might escape every night for a week, but +sooner or later she must fall a victim. + +"I had come to these conclusions before ever I had entered his +room. An inspection of his chair showed me that he had been in +the habit of standing on it, which of course would be necessary +in order that he should reach the ventilator. The sight of the +safe, the saucer of milk, and the loop of whipcord were enough to +finally dispel any doubts which may have remained. The metallic +clang heard by Miss Stoner was obviously caused by her stepfather +hastily closing the door of his safe upon its terrible occupant. +Having once made up my mind, you know the steps which I took in +order to put the matter to the proof. I heard the creature hiss +as I have no doubt that you did also, and I instantly lit the +light and attacked it." + +"With the result of driving it through the ventilator." + +"And also with the result of causing it to turn upon its master +at the other side. Some of the blows of my cane came home and +roused its snakish temper, so that it flew upon the first person +it saw. In this way I am no doubt indirectly responsible for Dr. +Grimesby Roylott's death, and I cannot say that it is likely to +weigh very heavily upon my conscience." + + + +IX. THE ADVENTURE OF THE ENGINEER'S THUMB + +Of all the problems which have been submitted to my friend, Mr. +Sherlock Holmes, for solution during the years of our intimacy, +there were only two which I was the means of introducing to his +notice--that of Mr. Hatherley's thumb, and that of Colonel +Warburton's madness. Of these the latter may have afforded a +finer field for an acute and original observer, but the other was +so strange in its inception and so dramatic in its details that +it may be the more worthy of being placed upon record, even if it +gave my friend fewer openings for those deductive methods of +reasoning by which he achieved such remarkable results. The story +has, I believe, been told more than once in the newspapers, but, +like all such narratives, its effect is much less striking when +set forth en bloc in a single half-column of print than when the +facts slowly evolve before your own eyes, and the mystery clears +gradually away as each new discovery furnishes a step which leads +on to the complete truth. At the time the circumstances made a +deep impression upon me, and the lapse of two years has hardly +served to weaken the effect. + +It was in the summer of '89, not long after my marriage, that the +events occurred which I am now about to summarise. I had returned +to civil practice and had finally abandoned Holmes in his Baker +Street rooms, although I continually visited him and occasionally +even persuaded him to forgo his Bohemian habits so far as to come +and visit us. My practice had steadily increased, and as I +happened to live at no very great distance from Paddington +Station, I got a few patients from among the officials. One of +these, whom I had cured of a painful and lingering disease, was +never weary of advertising my virtues and of endeavouring to send +me on every sufferer over whom he might have any influence. + +One morning, at a little before seven o'clock, I was awakened by +the maid tapping at the door to announce that two men had come +from Paddington and were waiting in the consulting-room. I +dressed hurriedly, for I knew by experience that railway cases +were seldom trivial, and hastened downstairs. As I descended, my +old ally, the guard, came out of the room and closed the door +tightly behind him. + +"I've got him here," he whispered, jerking his thumb over his +shoulder; "he's all right." + +"What is it, then?" I asked, for his manner suggested that it was +some strange creature which he had caged up in my room. + +"It's a new patient," he whispered. "I thought I'd bring him +round myself; then he couldn't slip away. There he is, all safe +and sound. I must go now, Doctor; I have my dooties, just the +same as you." And off he went, this trusty tout, without even +giving me time to thank him. + +I entered my consulting-room and found a gentleman seated by the +table. He was quietly dressed in a suit of heather tweed with a +soft cloth cap which he had laid down upon my books. Round one of +his hands he had a handkerchief wrapped, which was mottled all +over with bloodstains. He was young, not more than +five-and-twenty, I should say, with a strong, masculine face; but +he was exceedingly pale and gave me the impression of a man who +was suffering from some strong agitation, which it took all his +strength of mind to control. + +"I am sorry to knock you up so early, Doctor," said he, "but I +have had a very serious accident during the night. I came in by +train this morning, and on inquiring at Paddington as to where I +might find a doctor, a worthy fellow very kindly escorted me +here. I gave the maid a card, but I see that she has left it upon +the side-table." + +I took it up and glanced at it. "Mr. Victor Hatherley, hydraulic +engineer, 16A, Victoria Street (3rd floor)." That was the name, +style, and abode of my morning visitor. "I regret that I have +kept you waiting," said I, sitting down in my library-chair. "You +are fresh from a night journey, I understand, which is in itself +a monotonous occupation." + +"Oh, my night could not be called monotonous," said he, and +laughed. He laughed very heartily, with a high, ringing note, +leaning back in his chair and shaking his sides. All my medical +instincts rose up against that laugh. + +"Stop it!" I cried; "pull yourself together!" and I poured out +some water from a caraffe. + +It was useless, however. He was off in one of those hysterical +outbursts which come upon a strong nature when some great crisis +is over and gone. Presently he came to himself once more, very +weary and pale-looking. + +"I have been making a fool of myself," he gasped. + +"Not at all. Drink this." I dashed some brandy into the water, +and the colour began to come back to his bloodless cheeks. + +"That's better!" said he. "And now, Doctor, perhaps you would +kindly attend to my thumb, or rather to the place where my thumb +used to be." + +He unwound the handkerchief and held out his hand. It gave even +my hardened nerves a shudder to look at it. There were four +protruding fingers and a horrid red, spongy surface where the +thumb should have been. It had been hacked or torn right out from +the roots. + +"Good heavens!" I cried, "this is a terrible injury. It must have +bled considerably." + +"Yes, it did. I fainted when it was done, and I think that I must +have been senseless for a long time. When I came to I found that +it was still bleeding, so I tied one end of my handkerchief very +tightly round the wrist and braced it up with a twig." + +"Excellent! You should have been a surgeon." + +"It is a question of hydraulics, you see, and came within my own +province." + +"This has been done," said I, examining the wound, "by a very +heavy and sharp instrument." + +"A thing like a cleaver," said he. + +"An accident, I presume?" + +"By no means." + +"What! a murderous attack?" + +"Very murderous indeed." + +"You horrify me." + +I sponged the wound, cleaned it, dressed it, and finally covered +it over with cotton wadding and carbolised bandages. He lay back +without wincing, though he bit his lip from time to time. + +"How is that?" I asked when I had finished. + +"Capital! Between your brandy and your bandage, I feel a new man. +I was very weak, but I have had a good deal to go through." + +"Perhaps you had better not speak of the matter. It is evidently +trying to your nerves." + +"Oh, no, not now. I shall have to tell my tale to the police; +but, between ourselves, if it were not for the convincing +evidence of this wound of mine, I should be surprised if they +believed my statement, for it is a very extraordinary one, and I +have not much in the way of proof with which to back it up; and, +even if they believe me, the clues which I can give them are so +vague that it is a question whether justice will be done." + +"Ha!" cried I, "if it is anything in the nature of a problem +which you desire to see solved, I should strongly recommend you +to come to my friend, Mr. Sherlock Holmes, before you go to the +official police." + +"Oh, I have heard of that fellow," answered my visitor, "and I +should be very glad if he would take the matter up, though of +course I must use the official police as well. Would you give me +an introduction to him?" + +"I'll do better. I'll take you round to him myself." + +"I should be immensely obliged to you." + +"We'll call a cab and go together. We shall just be in time to +have a little breakfast with him. Do you feel equal to it?" + +"Yes; I shall not feel easy until I have told my story." + +"Then my servant will call a cab, and I shall be with you in an +instant." I rushed upstairs, explained the matter shortly to my +wife, and in five minutes was inside a hansom, driving with my +new acquaintance to Baker Street. + +Sherlock Holmes was, as I expected, lounging about his +sitting-room in his dressing-gown, reading the agony column of The +Times and smoking his before-breakfast pipe, which was composed +of all the plugs and dottles left from his smokes of the day +before, all carefully dried and collected on the corner of the +mantelpiece. He received us in his quietly genial fashion, +ordered fresh rashers and eggs, and joined us in a hearty meal. +When it was concluded he settled our new acquaintance upon the +sofa, placed a pillow beneath his head, and laid a glass of +brandy and water within his reach. + +"It is easy to see that your experience has been no common one, +Mr. Hatherley," said he. "Pray, lie down there and make yourself +absolutely at home. Tell us what you can, but stop when you are +tired and keep up your strength with a little stimulant." + +"Thank you," said my patient, "but I have felt another man since +the doctor bandaged me, and I think that your breakfast has +completed the cure. I shall take up as little of your valuable +time as possible, so I shall start at once upon my peculiar +experiences." + +Holmes sat in his big armchair with the weary, heavy-lidded +expression which veiled his keen and eager nature, while I sat +opposite to him, and we listened in silence to the strange story +which our visitor detailed to us. + +"You must know," said he, "that I am an orphan and a bachelor, +residing alone in lodgings in London. By profession I am a +hydraulic engineer, and I have had considerable experience of my +work during the seven years that I was apprenticed to Venner & +Matheson, the well-known firm, of Greenwich. Two years ago, +having served my time, and having also come into a fair sum of +money through my poor father's death, I determined to start in +business for myself and took professional chambers in Victoria +Street. + +"I suppose that everyone finds his first independent start in +business a dreary experience. To me it has been exceptionally so. +During two years I have had three consultations and one small +job, and that is absolutely all that my profession has brought +me. My gross takings amount to 27 pounds 10s. Every day, from +nine in the morning until four in the afternoon, I waited in my +little den, until at last my heart began to sink, and I came to +believe that I should never have any practice at all. + +"Yesterday, however, just as I was thinking of leaving the +office, my clerk entered to say there was a gentleman waiting who +wished to see me upon business. He brought up a card, too, with +the name of 'Colonel Lysander Stark' engraved upon it. Close at +his heels came the colonel himself, a man rather over the middle +size, but of an exceeding thinness. I do not think that I have +ever seen so thin a man. His whole face sharpened away into nose +and chin, and the skin of his cheeks was drawn quite tense over +his outstanding bones. Yet this emaciation seemed to be his +natural habit, and due to no disease, for his eye was bright, his +step brisk, and his bearing assured. He was plainly but neatly +dressed, and his age, I should judge, would be nearer forty than +thirty. + +"'Mr. Hatherley?' said he, with something of a German accent. +'You have been recommended to me, Mr. Hatherley, as being a man +who is not only proficient in his profession but is also discreet +and capable of preserving a secret.' + +"I bowed, feeling as flattered as any young man would at such an +address. 'May I ask who it was who gave me so good a character?' + +"'Well, perhaps it is better that I should not tell you that just +at this moment. I have it from the same source that you are both +an orphan and a bachelor and are residing alone in London.' + +"'That is quite correct,' I answered; 'but you will excuse me if +I say that I cannot see how all this bears upon my professional +qualifications. I understand that it was on a professional matter +that you wished to speak to me?' + +"'Undoubtedly so. But you will find that all I say is really to +the point. I have a professional commission for you, but absolute +secrecy is quite essential--absolute secrecy, you understand, and +of course we may expect that more from a man who is alone than +from one who lives in the bosom of his family.' + +"'If I promise to keep a secret,' said I, 'you may absolutely +depend upon my doing so.' + +"He looked very hard at me as I spoke, and it seemed to me that I +had never seen so suspicious and questioning an eye. + +"'Do you promise, then?' said he at last. + +"'Yes, I promise.' + +"'Absolute and complete silence before, during, and after? No +reference to the matter at all, either in word or writing?' + +"'I have already given you my word.' + +"'Very good.' He suddenly sprang up, and darting like lightning +across the room he flung open the door. The passage outside was +empty. + +"'That's all right,' said he, coming back. 'I know that clerks are +sometimes curious as to their master's affairs. Now we can talk +in safety.' He drew up his chair very close to mine and began to +stare at me again with the same questioning and thoughtful look. + +"A feeling of repulsion, and of something akin to fear had begun +to rise within me at the strange antics of this fleshless man. +Even my dread of losing a client could not restrain me from +showing my impatience. + +"'I beg that you will state your business, sir,' said I; 'my time +is of value.' Heaven forgive me for that last sentence, but the +words came to my lips. + +"'How would fifty guineas for a night's work suit you?' he asked. + +"'Most admirably.' + +"'I say a night's work, but an hour's would be nearer the mark. I +simply want your opinion about a hydraulic stamping machine which +has got out of gear. If you show us what is wrong we shall soon +set it right ourselves. What do you think of such a commission as +that?' + +"'The work appears to be light and the pay munificent.' + +"'Precisely so. We shall want you to come to-night by the last +train.' + +"'Where to?' + +"'To Eyford, in Berkshire. It is a little place near the borders +of Oxfordshire, and within seven miles of Reading. There is a +train from Paddington which would bring you there at about +11:15.' + +"'Very good.' + +"'I shall come down in a carriage to meet you.' + +"'There is a drive, then?' + +"'Yes, our little place is quite out in the country. It is a good +seven miles from Eyford Station.' + +"'Then we can hardly get there before midnight. I suppose there +would be no chance of a train back. I should be compelled to stop +the night.' + +"'Yes, we could easily give you a shake-down.' + +"'That is very awkward. Could I not come at some more convenient +hour?' + +"'We have judged it best that you should come late. It is to +recompense you for any inconvenience that we are paying to you, a +young and unknown man, a fee which would buy an opinion from the +very heads of your profession. Still, of course, if you would +like to draw out of the business, there is plenty of time to do +so.' + +"I thought of the fifty guineas, and of how very useful they +would be to me. 'Not at all,' said I, 'I shall be very happy to +accommodate myself to your wishes. I should like, however, to +understand a little more clearly what it is that you wish me to +do.' + +"'Quite so. It is very natural that the pledge of secrecy which +we have exacted from you should have aroused your curiosity. I +have no wish to commit you to anything without your having it all +laid before you. I suppose that we are absolutely safe from +eavesdroppers?' + +"'Entirely.' + +"'Then the matter stands thus. You are probably aware that +fuller's-earth is a valuable product, and that it is only found +in one or two places in England?' + +"'I have heard so.' + +"'Some little time ago I bought a small place--a very small +place--within ten miles of Reading. I was fortunate enough to +discover that there was a deposit of fuller's-earth in one of my +fields. On examining it, however, I found that this deposit was a +comparatively small one, and that it formed a link between two +very much larger ones upon the right and left--both of them, +however, in the grounds of my neighbours. These good people were +absolutely ignorant that their land contained that which was +quite as valuable as a gold-mine. Naturally, it was to my +interest to buy their land before they discovered its true value, +but unfortunately I had no capital by which I could do this. I +took a few of my friends into the secret, however, and they +suggested that we should quietly and secretly work our own little +deposit and that in this way we should earn the money which would +enable us to buy the neighbouring fields. This we have now been +doing for some time, and in order to help us in our operations we +erected a hydraulic press. This press, as I have already +explained, has got out of order, and we wish your advice upon the +subject. We guard our secret very jealously, however, and if it +once became known that we had hydraulic engineers coming to our +little house, it would soon rouse inquiry, and then, if the facts +came out, it would be good-bye to any chance of getting these +fields and carrying out our plans. That is why I have made you +promise me that you will not tell a human being that you are +going to Eyford to-night. I hope that I make it all plain?' + +"'I quite follow you,' said I. 'The only point which I could not +quite understand was what use you could make of a hydraulic press +in excavating fuller's-earth, which, as I understand, is dug out +like gravel from a pit.' + +"'Ah!' said he carelessly, 'we have our own process. We compress +the earth into bricks, so as to remove them without revealing +what they are. But that is a mere detail. I have taken you fully +into my confidence now, Mr. Hatherley, and I have shown you how I +trust you.' He rose as he spoke. 'I shall expect you, then, at +Eyford at 11:15.' + +"'I shall certainly be there.' + +"'And not a word to a soul.' He looked at me with a last long, +questioning gaze, and then, pressing my hand in a cold, dank +grasp, he hurried from the room. + +"Well, when I came to think it all over in cool blood I was very +much astonished, as you may both think, at this sudden commission +which had been intrusted to me. On the one hand, of course, I was +glad, for the fee was at least tenfold what I should have asked +had I set a price upon my own services, and it was possible that +this order might lead to other ones. On the other hand, the face +and manner of my patron had made an unpleasant impression upon +me, and I could not think that his explanation of the +fuller's-earth was sufficient to explain the necessity for my +coming at midnight, and his extreme anxiety lest I should tell +anyone of my errand. However, I threw all fears to the winds, ate +a hearty supper, drove to Paddington, and started off, having +obeyed to the letter the injunction as to holding my tongue. + +"At Reading I had to change not only my carriage but my station. +However, I was in time for the last train to Eyford, and I +reached the little dim-lit station after eleven o'clock. I was the +only passenger who got out there, and there was no one upon the +platform save a single sleepy porter with a lantern. As I passed +out through the wicket gate, however, I found my acquaintance of +the morning waiting in the shadow upon the other side. Without a +word he grasped my arm and hurried me into a carriage, the door +of which was standing open. He drew up the windows on either +side, tapped on the wood-work, and away we went as fast as the +horse could go." + +"One horse?" interjected Holmes. + +"Yes, only one." + +"Did you observe the colour?" + +"Yes, I saw it by the side-lights when I was stepping into the +carriage. It was a chestnut." + +"Tired-looking or fresh?" + +"Oh, fresh and glossy." + +"Thank you. I am sorry to have interrupted you. Pray continue +your most interesting statement." + +"Away we went then, and we drove for at least an hour. Colonel +Lysander Stark had said that it was only seven miles, but I +should think, from the rate that we seemed to go, and from the +time that we took, that it must have been nearer twelve. He sat +at my side in silence all the time, and I was aware, more than +once when I glanced in his direction, that he was looking at me +with great intensity. The country roads seem to be not very good +in that part of the world, for we lurched and jolted terribly. I +tried to look out of the windows to see something of where we +were, but they were made of frosted glass, and I could make out +nothing save the occasional bright blur of a passing light. Now +and then I hazarded some remark to break the monotony of the +journey, but the colonel answered only in monosyllables, and the +conversation soon flagged. At last, however, the bumping of the +road was exchanged for the crisp smoothness of a gravel-drive, +and the carriage came to a stand. Colonel Lysander Stark sprang +out, and, as I followed after him, pulled me swiftly into a porch +which gaped in front of us. We stepped, as it were, right out of +the carriage and into the hall, so that I failed to catch the +most fleeting glance of the front of the house. The instant that +I had crossed the threshold the door slammed heavily behind us, +and I heard faintly the rattle of the wheels as the carriage +drove away. + +"It was pitch dark inside the house, and the colonel fumbled +about looking for matches and muttering under his breath. +Suddenly a door opened at the other end of the passage, and a +long, golden bar of light shot out in our direction. It grew +broader, and a woman appeared with a lamp in her hand, which she +held above her head, pushing her face forward and peering at us. +I could see that she was pretty, and from the gloss with which +the light shone upon her dark dress I knew that it was a rich +material. She spoke a few words in a foreign tongue in a tone as +though asking a question, and when my companion answered in a +gruff monosyllable she gave such a start that the lamp nearly +fell from her hand. Colonel Stark went up to her, whispered +something in her ear, and then, pushing her back into the room +from whence she had come, he walked towards me again with the +lamp in his hand. + +"'Perhaps you will have the kindness to wait in this room for a +few minutes,' said he, throwing open another door. It was a +quiet, little, plainly furnished room, with a round table in the +centre, on which several German books were scattered. Colonel +Stark laid down the lamp on the top of a harmonium beside the +door. 'I shall not keep you waiting an instant,' said he, and +vanished into the darkness. + +"I glanced at the books upon the table, and in spite of my +ignorance of German I could see that two of them were treatises +on science, the others being volumes of poetry. Then I walked +across to the window, hoping that I might catch some glimpse of +the country-side, but an oak shutter, heavily barred, was folded +across it. It was a wonderfully silent house. There was an old +clock ticking loudly somewhere in the passage, but otherwise +everything was deadly still. A vague feeling of uneasiness began +to steal over me. Who were these German people, and what were +they doing living in this strange, out-of-the-way place? And +where was the place? I was ten miles or so from Eyford, that was +all I knew, but whether north, south, east, or west I had no +idea. For that matter, Reading, and possibly other large towns, +were within that radius, so the place might not be so secluded, +after all. Yet it was quite certain, from the absolute stillness, +that we were in the country. I paced up and down the room, +humming a tune under my breath to keep up my spirits and feeling +that I was thoroughly earning my fifty-guinea fee. + +"Suddenly, without any preliminary sound in the midst of the +utter stillness, the door of my room swung slowly open. The woman +was standing in the aperture, the darkness of the hall behind +her, the yellow light from my lamp beating upon her eager and +beautiful face. I could see at a glance that she was sick with +fear, and the sight sent a chill to my own heart. She held up one +shaking finger to warn me to be silent, and she shot a few +whispered words of broken English at me, her eyes glancing back, +like those of a frightened horse, into the gloom behind her. + +"'I would go,' said she, trying hard, as it seemed to me, to +speak calmly; 'I would go. I should not stay here. There is no +good for you to do.' + +"'But, madam,' said I, 'I have not yet done what I came for. I +cannot possibly leave until I have seen the machine.' + +"'It is not worth your while to wait,' she went on. 'You can pass +through the door; no one hinders.' And then, seeing that I smiled +and shook my head, she suddenly threw aside her constraint and +made a step forward, with her hands wrung together. 'For the love +of Heaven!' she whispered, 'get away from here before it is too +late!' + +"But I am somewhat headstrong by nature, and the more ready to +engage in an affair when there is some obstacle in the way. I +thought of my fifty-guinea fee, of my wearisome journey, and of +the unpleasant night which seemed to be before me. Was it all to +go for nothing? Why should I slink away without having carried +out my commission, and without the payment which was my due? This +woman might, for all I knew, be a monomaniac. With a stout +bearing, therefore, though her manner had shaken me more than I +cared to confess, I still shook my head and declared my intention +of remaining where I was. She was about to renew her entreaties +when a door slammed overhead, and the sound of several footsteps +was heard upon the stairs. She listened for an instant, threw up +her hands with a despairing gesture, and vanished as suddenly and +as noiselessly as she had come. + +"The newcomers were Colonel Lysander Stark and a short thick man +with a chinchilla beard growing out of the creases of his double +chin, who was introduced to me as Mr. Ferguson. + +"'This is my secretary and manager,' said the colonel. 'By the +way, I was under the impression that I left this door shut just +now. I fear that you have felt the draught.' + +"'On the contrary,' said I, 'I opened the door myself because I +felt the room to be a little close.' + +"He shot one of his suspicious looks at me. 'Perhaps we had +better proceed to business, then,' said he. 'Mr. Ferguson and I +will take you up to see the machine.' + +"'I had better put my hat on, I suppose.' + +"'Oh, no, it is in the house.' + +"'What, you dig fuller's-earth in the house?' + +"'No, no. This is only where we compress it. But never mind that. +All we wish you to do is to examine the machine and to let us +know what is wrong with it.' + +"We went upstairs together, the colonel first with the lamp, the +fat manager and I behind him. It was a labyrinth of an old house, +with corridors, passages, narrow winding staircases, and little +low doors, the thresholds of which were hollowed out by the +generations who had crossed them. There were no carpets and no +signs of any furniture above the ground floor, while the plaster +was peeling off the walls, and the damp was breaking through in +green, unhealthy blotches. I tried to put on as unconcerned an +air as possible, but I had not forgotten the warnings of the +lady, even though I disregarded them, and I kept a keen eye upon +my two companions. Ferguson appeared to be a morose and silent +man, but I could see from the little that he said that he was at +least a fellow-countryman. + +"Colonel Lysander Stark stopped at last before a low door, which +he unlocked. Within was a small, square room, in which the three +of us could hardly get at one time. Ferguson remained outside, +and the colonel ushered me in. + +"'We are now,' said he, 'actually within the hydraulic press, and +it would be a particularly unpleasant thing for us if anyone were +to turn it on. The ceiling of this small chamber is really the +end of the descending piston, and it comes down with the force of +many tons upon this metal floor. There are small lateral columns +of water outside which receive the force, and which transmit and +multiply it in the manner which is familiar to you. The machine +goes readily enough, but there is some stiffness in the working +of it, and it has lost a little of its force. Perhaps you will +have the goodness to look it over and to show us how we can set +it right.' + +"I took the lamp from him, and I examined the machine very +thoroughly. It was indeed a gigantic one, and capable of +exercising enormous pressure. When I passed outside, however, and +pressed down the levers which controlled it, I knew at once by +the whishing sound that there was a slight leakage, which allowed +a regurgitation of water through one of the side cylinders. An +examination showed that one of the india-rubber bands which was +round the head of a driving-rod had shrunk so as not quite to +fill the socket along which it worked. This was clearly the cause +of the loss of power, and I pointed it out to my companions, who +followed my remarks very carefully and asked several practical +questions as to how they should proceed to set it right. When I +had made it clear to them, I returned to the main chamber of the +machine and took a good look at it to satisfy my own curiosity. +It was obvious at a glance that the story of the fuller's-earth +was the merest fabrication, for it would be absurd to suppose +that so powerful an engine could be designed for so inadequate a +purpose. The walls were of wood, but the floor consisted of a +large iron trough, and when I came to examine it I could see a +crust of metallic deposit all over it. I had stooped and was +scraping at this to see exactly what it was when I heard a +muttered exclamation in German and saw the cadaverous face of the +colonel looking down at me. + +"'What are you doing there?' he asked. + +"I felt angry at having been tricked by so elaborate a story as +that which he had told me. 'I was admiring your fuller's-earth,' +said I; 'I think that I should be better able to advise you as to +your machine if I knew what the exact purpose was for which it +was used.' + +"The instant that I uttered the words I regretted the rashness of +my speech. His face set hard, and a baleful light sprang up in +his grey eyes. + +"'Very well,' said he, 'you shall know all about the machine.' He +took a step backward, slammed the little door, and turned the key +in the lock. I rushed towards it and pulled at the handle, but it +was quite secure, and did not give in the least to my kicks and +shoves. 'Hullo!' I yelled. 'Hullo! Colonel! Let me out!' + +"And then suddenly in the silence I heard a sound which sent my +heart into my mouth. It was the clank of the levers and the swish +of the leaking cylinder. He had set the engine at work. The lamp +still stood upon the floor where I had placed it when examining +the trough. By its light I saw that the black ceiling was coming +down upon me, slowly, jerkily, but, as none knew better than +myself, with a force which must within a minute grind me to a +shapeless pulp. I threw myself, screaming, against the door, and +dragged with my nails at the lock. I implored the colonel to let +me out, but the remorseless clanking of the levers drowned my +cries. The ceiling was only a foot or two above my head, and with +my hand upraised I could feel its hard, rough surface. Then it +flashed through my mind that the pain of my death would depend +very much upon the position in which I met it. If I lay on my +face the weight would come upon my spine, and I shuddered to +think of that dreadful snap. Easier the other way, perhaps; and +yet, had I the nerve to lie and look up at that deadly black +shadow wavering down upon me? Already I was unable to stand +erect, when my eye caught something which brought a gush of hope +back to my heart. + +"I have said that though the floor and ceiling were of iron, the +walls were of wood. As I gave a last hurried glance around, I saw +a thin line of yellow light between two of the boards, which +broadened and broadened as a small panel was pushed backward. For +an instant I could hardly believe that here was indeed a door +which led away from death. The next instant I threw myself +through, and lay half-fainting upon the other side. The panel had +closed again behind me, but the crash of the lamp, and a few +moments afterwards the clang of the two slabs of metal, told me +how narrow had been my escape. + +"I was recalled to myself by a frantic plucking at my wrist, and +I found myself lying upon the stone floor of a narrow corridor, +while a woman bent over me and tugged at me with her left hand, +while she held a candle in her right. It was the same good friend +whose warning I had so foolishly rejected. + +"'Come! come!' she cried breathlessly. 'They will be here in a +moment. They will see that you are not there. Oh, do not waste +the so-precious time, but come!' + +"This time, at least, I did not scorn her advice. I staggered to +my feet and ran with her along the corridor and down a winding +stair. The latter led to another broad passage, and just as we +reached it we heard the sound of running feet and the shouting of +two voices, one answering the other from the floor on which we +were and from the one beneath. My guide stopped and looked about +her like one who is at her wit's end. Then she threw open a door +which led into a bedroom, through the window of which the moon +was shining brightly. + +"'It is your only chance,' said she. 'It is high, but it may be +that you can jump it.' + +"As she spoke a light sprang into view at the further end of the +passage, and I saw the lean figure of Colonel Lysander Stark +rushing forward with a lantern in one hand and a weapon like a +butcher's cleaver in the other. I rushed across the bedroom, +flung open the window, and looked out. How quiet and sweet and +wholesome the garden looked in the moonlight, and it could not be +more than thirty feet down. I clambered out upon the sill, but I +hesitated to jump until I should have heard what passed between +my saviour and the ruffian who pursued me. If she were ill-used, +then at any risks I was determined to go back to her assistance. +The thought had hardly flashed through my mind before he was at +the door, pushing his way past her; but she threw her arms round +him and tried to hold him back. + +"'Fritz! Fritz!' she cried in English, 'remember your promise +after the last time. You said it should not be again. He will be +silent! Oh, he will be silent!' + +"'You are mad, Elise!' he shouted, struggling to break away from +her. 'You will be the ruin of us. He has seen too much. Let me +pass, I say!' He dashed her to one side, and, rushing to the +window, cut at me with his heavy weapon. I had let myself go, and +was hanging by the hands to the sill, when his blow fell. I was +conscious of a dull pain, my grip loosened, and I fell into the +garden below. + +"I was shaken but not hurt by the fall; so I picked myself up and +rushed off among the bushes as hard as I could run, for I +understood that I was far from being out of danger yet. Suddenly, +however, as I ran, a deadly dizziness and sickness came over me. +I glanced down at my hand, which was throbbing painfully, and +then, for the first time, saw that my thumb had been cut off and +that the blood was pouring from my wound. I endeavoured to tie my +handkerchief round it, but there came a sudden buzzing in my +ears, and next moment I fell in a dead faint among the +rose-bushes. + +"How long I remained unconscious I cannot tell. It must have been +a very long time, for the moon had sunk, and a bright morning was +breaking when I came to myself. My clothes were all sodden with +dew, and my coat-sleeve was drenched with blood from my wounded +thumb. The smarting of it recalled in an instant all the +particulars of my night's adventure, and I sprang to my feet with +the feeling that I might hardly yet be safe from my pursuers. But +to my astonishment, when I came to look round me, neither house +nor garden were to be seen. I had been lying in an angle of the +hedge close by the highroad, and just a little lower down was a +long building, which proved, upon my approaching it, to be the +very station at which I had arrived upon the previous night. Were +it not for the ugly wound upon my hand, all that had passed +during those dreadful hours might have been an evil dream. + +"Half dazed, I went into the station and asked about the morning +train. There would be one to Reading in less than an hour. The +same porter was on duty, I found, as had been there when I +arrived. I inquired of him whether he had ever heard of Colonel +Lysander Stark. The name was strange to him. Had he observed a +carriage the night before waiting for me? No, he had not. Was +there a police-station anywhere near? There was one about three +miles off. + +"It was too far for me to go, weak and ill as I was. I determined +to wait until I got back to town before telling my story to the +police. It was a little past six when I arrived, so I went first +to have my wound dressed, and then the doctor was kind enough to +bring me along here. I put the case into your hands and shall do +exactly what you advise." + +We both sat in silence for some little time after listening to +this extraordinary narrative. Then Sherlock Holmes pulled down +from the shelf one of the ponderous commonplace books in which he +placed his cuttings. + +"Here is an advertisement which will interest you," said he. "It +appeared in all the papers about a year ago. Listen to this: +'Lost, on the 9th inst., Mr. Jeremiah Hayling, aged +twenty-six, a hydraulic engineer. Left his lodgings at ten +o'clock at night, and has not been heard of since. Was +dressed in,' etc., etc. Ha! That represents the last time that +the colonel needed to have his machine overhauled, I fancy." + +"Good heavens!" cried my patient. "Then that explains what the +girl said." + +"Undoubtedly. It is quite clear that the colonel was a cool and +desperate man, who was absolutely determined that nothing should +stand in the way of his little game, like those out-and-out +pirates who will leave no survivor from a captured ship. Well, +every moment now is precious, so if you feel equal to it we shall +go down to Scotland Yard at once as a preliminary to starting for +Eyford." + +Some three hours or so afterwards we were all in the train +together, bound from Reading to the little Berkshire village. +There were Sherlock Holmes, the hydraulic engineer, Inspector +Bradstreet, of Scotland Yard, a plain-clothes man, and myself. +Bradstreet had spread an ordnance map of the county out upon the +seat and was busy with his compasses drawing a circle with Eyford +for its centre. + +"There you are," said he. "That circle is drawn at a radius of +ten miles from the village. The place we want must be somewhere +near that line. You said ten miles, I think, sir." + +"It was an hour's good drive." + +"And you think that they brought you back all that way when you +were unconscious?" + +"They must have done so. I have a confused memory, too, of having +been lifted and conveyed somewhere." + +"What I cannot understand," said I, "is why they should have +spared you when they found you lying fainting in the garden. +Perhaps the villain was softened by the woman's entreaties." + +"I hardly think that likely. I never saw a more inexorable face +in my life." + +"Oh, we shall soon clear up all that," said Bradstreet. "Well, I +have drawn my circle, and I only wish I knew at what point upon +it the folk that we are in search of are to be found." + +"I think I could lay my finger on it," said Holmes quietly. + +"Really, now!" cried the inspector, "you have formed your +opinion! Come, now, we shall see who agrees with you. I say it is +south, for the country is more deserted there." + +"And I say east," said my patient. + +"I am for west," remarked the plain-clothes man. "There are +several quiet little villages up there." + +"And I am for north," said I, "because there are no hills there, +and our friend says that he did not notice the carriage go up +any." + +"Come," cried the inspector, laughing; "it's a very pretty +diversity of opinion. We have boxed the compass among us. Who do +you give your casting vote to?" + +"You are all wrong." + +"But we can't all be." + +"Oh, yes, you can. This is my point." He placed his finger in the +centre of the circle. "This is where we shall find them." + +"But the twelve-mile drive?" gasped Hatherley. + +"Six out and six back. Nothing simpler. You say yourself that the +horse was fresh and glossy when you got in. How could it be that +if it had gone twelve miles over heavy roads?" + +"Indeed, it is a likely ruse enough," observed Bradstreet +thoughtfully. "Of course there can be no doubt as to the nature +of this gang." + +"None at all," said Holmes. "They are coiners on a large scale, +and have used the machine to form the amalgam which has taken the +place of silver." + +"We have known for some time that a clever gang was at work," +said the inspector. "They have been turning out half-crowns by +the thousand. We even traced them as far as Reading, but could +get no farther, for they had covered their traces in a way that +showed that they were very old hands. But now, thanks to this +lucky chance, I think that we have got them right enough." + +But the inspector was mistaken, for those criminals were not +destined to fall into the hands of justice. As we rolled into +Eyford Station we saw a gigantic column of smoke which streamed +up from behind a small clump of trees in the neighbourhood and +hung like an immense ostrich feather over the landscape. + +"A house on fire?" asked Bradstreet as the train steamed off +again on its way. + +"Yes, sir!" said the station-master. + +"When did it break out?" + +"I hear that it was during the night, sir, but it has got worse, +and the whole place is in a blaze." + +"Whose house is it?" + +"Dr. Becher's." + +"Tell me," broke in the engineer, "is Dr. Becher a German, very +thin, with a long, sharp nose?" + +The station-master laughed heartily. "No, sir, Dr. Becher is an +Englishman, and there isn't a man in the parish who has a +better-lined waistcoat. But he has a gentleman staying with him, +a patient, as I understand, who is a foreigner, and he looks as +if a little good Berkshire beef would do him no harm." + +The station-master had not finished his speech before we were all +hastening in the direction of the fire. The road topped a low +hill, and there was a great widespread whitewashed building in +front of us, spouting fire at every chink and window, while in +the garden in front three fire-engines were vainly striving to +keep the flames under. + +"That's it!" cried Hatherley, in intense excitement. "There is +the gravel-drive, and there are the rose-bushes where I lay. That +second window is the one that I jumped from." + +"Well, at least," said Holmes, "you have had your revenge upon +them. There can be no question that it was your oil-lamp which, +when it was crushed in the press, set fire to the wooden walls, +though no doubt they were too excited in the chase after you to +observe it at the time. Now keep your eyes open in this crowd for +your friends of last night, though I very much fear that they are +a good hundred miles off by now." + +And Holmes' fears came to be realised, for from that day to this +no word has ever been heard either of the beautiful woman, the +sinister German, or the morose Englishman. Early that morning a +peasant had met a cart containing several people and some very +bulky boxes driving rapidly in the direction of Reading, but +there all traces of the fugitives disappeared, and even Holmes' +ingenuity failed ever to discover the least clue as to their +whereabouts. + +The firemen had been much perturbed at the strange arrangements +which they had found within, and still more so by discovering a +newly severed human thumb upon a window-sill of the second floor. +About sunset, however, their efforts were at last successful, and +they subdued the flames, but not before the roof had fallen in, +and the whole place been reduced to such absolute ruin that, save +some twisted cylinders and iron piping, not a trace remained of +the machinery which had cost our unfortunate acquaintance so +dearly. Large masses of nickel and of tin were discovered stored +in an out-house, but no coins were to be found, which may have +explained the presence of those bulky boxes which have been +already referred to. + +How our hydraulic engineer had been conveyed from the garden to +the spot where he recovered his senses might have remained +forever a mystery were it not for the soft mould, which told us a +very plain tale. He had evidently been carried down by two +persons, one of whom had remarkably small feet and the other +unusually large ones. On the whole, it was most probable that the +silent Englishman, being less bold or less murderous than his +companion, had assisted the woman to bear the unconscious man out +of the way of danger. + +"Well," said our engineer ruefully as we took our seats to return +once more to London, "it has been a pretty business for me! I +have lost my thumb and I have lost a fifty-guinea fee, and what +have I gained?" + +"Experience," said Holmes, laughing. "Indirectly it may be of +value, you know; you have only to put it into words to gain the +reputation of being excellent company for the remainder of your +existence." + + + +X. THE ADVENTURE OF THE NOBLE BACHELOR + +The Lord St. Simon marriage, and its curious termination, have +long ceased to be a subject of interest in those exalted circles +in which the unfortunate bridegroom moves. Fresh scandals have +eclipsed it, and their more piquant details have drawn the +gossips away from this four-year-old drama. As I have reason to +believe, however, that the full facts have never been revealed to +the general public, and as my friend Sherlock Holmes had a +considerable share in clearing the matter up, I feel that no +memoir of him would be complete without some little sketch of +this remarkable episode. + +It was a few weeks before my own marriage, during the days when I +was still sharing rooms with Holmes in Baker Street, that he came +home from an afternoon stroll to find a letter on the table +waiting for him. I had remained indoors all day, for the weather +had taken a sudden turn to rain, with high autumnal winds, and +the Jezail bullet which I had brought back in one of my limbs as +a relic of my Afghan campaign throbbed with dull persistence. +With my body in one easy-chair and my legs upon another, I had +surrounded myself with a cloud of newspapers until at last, +saturated with the news of the day, I tossed them all aside and +lay listless, watching the huge crest and monogram upon the +envelope upon the table and wondering lazily who my friend's +noble correspondent could be. + +"Here is a very fashionable epistle," I remarked as he entered. +"Your morning letters, if I remember right, were from a +fish-monger and a tide-waiter." + +"Yes, my correspondence has certainly the charm of variety," he +answered, smiling, "and the humbler are usually the more +interesting. This looks like one of those unwelcome social +summonses which call upon a man either to be bored or to lie." + +He broke the seal and glanced over the contents. + +"Oh, come, it may prove to be something of interest, after all." + +"Not social, then?" + +"No, distinctly professional." + +"And from a noble client?" + +"One of the highest in England." + +"My dear fellow, I congratulate you." + +"I assure you, Watson, without affectation, that the status of my +client is a matter of less moment to me than the interest of his +case. It is just possible, however, that that also may not be +wanting in this new investigation. You have been reading the +papers diligently of late, have you not?" + +"It looks like it," said I ruefully, pointing to a huge bundle in +the corner. "I have had nothing else to do." + +"It is fortunate, for you will perhaps be able to post me up. I +read nothing except the criminal news and the agony column. The +latter is always instructive. But if you have followed recent +events so closely you must have read about Lord St. Simon and his +wedding?" + +"Oh, yes, with the deepest interest." + +"That is well. The letter which I hold in my hand is from Lord +St. Simon. I will read it to you, and in return you must turn +over these papers and let me have whatever bears upon the matter. +This is what he says: + +"'MY DEAR MR. SHERLOCK HOLMES:--Lord Backwater tells me that I +may place implicit reliance upon your judgment and discretion. I +have determined, therefore, to call upon you and to consult you +in reference to the very painful event which has occurred in +connection with my wedding. Mr. Lestrade, of Scotland Yard, is +acting already in the matter, but he assures me that he sees no +objection to your co-operation, and that he even thinks that +it might be of some assistance. I will call at four o'clock in +the afternoon, and, should you have any other engagement at that +time, I hope that you will postpone it, as this matter is of +paramount importance. Yours faithfully, ST. SIMON.' + +"It is dated from Grosvenor Mansions, written with a quill pen, +and the noble lord has had the misfortune to get a smear of ink +upon the outer side of his right little finger," remarked Holmes +as he folded up the epistle. + +"He says four o'clock. It is three now. He will be here in an +hour." + +"Then I have just time, with your assistance, to get clear upon +the subject. Turn over those papers and arrange the extracts in +their order of time, while I take a glance as to who our client +is." He picked a red-covered volume from a line of books of +reference beside the mantelpiece. "Here he is," said he, sitting +down and flattening it out upon his knee. "'Lord Robert Walsingham +de Vere St. Simon, second son of the Duke of Balmoral.' Hum! 'Arms: +Azure, three caltrops in chief over a fess sable. Born in 1846.' +He's forty-one years of age, which is mature for marriage. Was +Under-Secretary for the colonies in a late administration. The +Duke, his father, was at one time Secretary for Foreign Affairs. +They inherit Plantagenet blood by direct descent, and Tudor on +the distaff side. Ha! Well, there is nothing very instructive in +all this. I think that I must turn to you Watson, for something +more solid." + +"I have very little difficulty in finding what I want," said I, +"for the facts are quite recent, and the matter struck me as +remarkable. I feared to refer them to you, however, as I knew +that you had an inquiry on hand and that you disliked the +intrusion of other matters." + +"Oh, you mean the little problem of the Grosvenor Square +furniture van. That is quite cleared up now--though, indeed, it +was obvious from the first. Pray give me the results of your +newspaper selections." + +"Here is the first notice which I can find. It is in the personal +column of the Morning Post, and dates, as you see, some weeks +back: 'A marriage has been arranged,' it says, 'and will, if +rumour is correct, very shortly take place, between Lord Robert +St. Simon, second son of the Duke of Balmoral, and Miss Hatty +Doran, the only daughter of Aloysius Doran. Esq., of San +Francisco, Cal., U.S.A.' That is all." + +"Terse and to the point," remarked Holmes, stretching his long, +thin legs towards the fire. + +"There was a paragraph amplifying this in one of the society +papers of the same week. Ah, here it is: 'There will soon be a +call for protection in the marriage market, for the present +free-trade principle appears to tell heavily against our home +product. One by one the management of the noble houses of Great +Britain is passing into the hands of our fair cousins from across +the Atlantic. An important addition has been made during the last +week to the list of the prizes which have been borne away by +these charming invaders. Lord St. Simon, who has shown himself +for over twenty years proof against the little god's arrows, has +now definitely announced his approaching marriage with Miss Hatty +Doran, the fascinating daughter of a California millionaire. Miss +Doran, whose graceful figure and striking face attracted much +attention at the Westbury House festivities, is an only child, +and it is currently reported that her dowry will run to +considerably over the six figures, with expectancies for the +future. As it is an open secret that the Duke of Balmoral has +been compelled to sell his pictures within the last few years, +and as Lord St. Simon has no property of his own save the small +estate of Birchmoor, it is obvious that the Californian heiress +is not the only gainer by an alliance which will enable her to +make the easy and common transition from a Republican lady to a +British peeress.'" + +"Anything else?" asked Holmes, yawning. + +"Oh, yes; plenty. Then there is another note in the Morning Post +to say that the marriage would be an absolutely quiet one, that it +would be at St. George's, Hanover Square, that only half a dozen +intimate friends would be invited, and that the party would +return to the furnished house at Lancaster Gate which has been +taken by Mr. Aloysius Doran. Two days later--that is, on +Wednesday last--there is a curt announcement that the wedding had +taken place, and that the honeymoon would be passed at Lord +Backwater's place, near Petersfield. Those are all the notices +which appeared before the disappearance of the bride." + +"Before the what?" asked Holmes with a start. + +"The vanishing of the lady." + +"When did she vanish, then?" + +"At the wedding breakfast." + +"Indeed. This is more interesting than it promised to be; quite +dramatic, in fact." + +"Yes; it struck me as being a little out of the common." + +"They often vanish before the ceremony, and occasionally during +the honeymoon; but I cannot call to mind anything quite so prompt +as this. Pray let me have the details." + +"I warn you that they are very incomplete." + +"Perhaps we may make them less so." + +"Such as they are, they are set forth in a single article of a +morning paper of yesterday, which I will read to you. It is +headed, 'Singular Occurrence at a Fashionable Wedding': + +"'The family of Lord Robert St. Simon has been thrown into the +greatest consternation by the strange and painful episodes which +have taken place in connection with his wedding. The ceremony, as +shortly announced in the papers of yesterday, occurred on the +previous morning; but it is only now that it has been possible to +confirm the strange rumours which have been so persistently +floating about. In spite of the attempts of the friends to hush +the matter up, so much public attention has now been drawn to it +that no good purpose can be served by affecting to disregard what +is a common subject for conversation. + +"'The ceremony, which was performed at St. George's, Hanover +Square, was a very quiet one, no one being present save the +father of the bride, Mr. Aloysius Doran, the Duchess of Balmoral, +Lord Backwater, Lord Eustace and Lady Clara St. Simon (the +younger brother and sister of the bridegroom), and Lady Alicia +Whittington. The whole party proceeded afterwards to the house of +Mr. Aloysius Doran, at Lancaster Gate, where breakfast had been +prepared. It appears that some little trouble was caused by a +woman, whose name has not been ascertained, who endeavoured to +force her way into the house after the bridal party, alleging +that she had some claim upon Lord St. Simon. It was only after a +painful and prolonged scene that she was ejected by the butler +and the footman. The bride, who had fortunately entered the house +before this unpleasant interruption, had sat down to breakfast +with the rest, when she complained of a sudden indisposition and +retired to her room. Her prolonged absence having caused some +comment, her father followed her, but learned from her maid that +she had only come up to her chamber for an instant, caught up an +ulster and bonnet, and hurried down to the passage. One of the +footmen declared that he had seen a lady leave the house thus +apparelled, but had refused to credit that it was his mistress, +believing her to be with the company. On ascertaining that his +daughter had disappeared, Mr. Aloysius Doran, in conjunction with +the bridegroom, instantly put themselves in communication with +the police, and very energetic inquiries are being made, which +will probably result in a speedy clearing up of this very +singular business. Up to a late hour last night, however, nothing +had transpired as to the whereabouts of the missing lady. There +are rumours of foul play in the matter, and it is said that the +police have caused the arrest of the woman who had caused the +original disturbance, in the belief that, from jealousy or some +other motive, she may have been concerned in the strange +disappearance of the bride.'" + +"And is that all?" + +"Only one little item in another of the morning papers, but it is +a suggestive one." + +"And it is--" + +"That Miss Flora Millar, the lady who had caused the disturbance, +has actually been arrested. It appears that she was formerly a +danseuse at the Allegro, and that she has known the bridegroom +for some years. There are no further particulars, and the whole +case is in your hands now--so far as it has been set forth in the +public press." + +"And an exceedingly interesting case it appears to be. I would +not have missed it for worlds. But there is a ring at the bell, +Watson, and as the clock makes it a few minutes after four, I +have no doubt that this will prove to be our noble client. Do not +dream of going, Watson, for I very much prefer having a witness, +if only as a check to my own memory." + +"Lord Robert St. Simon," announced our page-boy, throwing open +the door. A gentleman entered, with a pleasant, cultured face, +high-nosed and pale, with something perhaps of petulance about +the mouth, and with the steady, well-opened eye of a man whose +pleasant lot it had ever been to command and to be obeyed. His +manner was brisk, and yet his general appearance gave an undue +impression of age, for he had a slight forward stoop and a little +bend of the knees as he walked. His hair, too, as he swept off +his very curly-brimmed hat, was grizzled round the edges and thin +upon the top. As to his dress, it was careful to the verge of +foppishness, with high collar, black frock-coat, white waistcoat, +yellow gloves, patent-leather shoes, and light-coloured gaiters. +He advanced slowly into the room, turning his head from left to +right, and swinging in his right hand the cord which held his +golden eyeglasses. + +"Good-day, Lord St. Simon," said Holmes, rising and bowing. "Pray +take the basket-chair. This is my friend and colleague, Dr. +Watson. Draw up a little to the fire, and we will talk this +matter over." + +"A most painful matter to me, as you can most readily imagine, +Mr. Holmes. I have been cut to the quick. I understand that you +have already managed several delicate cases of this sort, sir, +though I presume that they were hardly from the same class of +society." + +"No, I am descending." + +"I beg pardon." + +"My last client of the sort was a king." + +"Oh, really! I had no idea. And which king?" + +"The King of Scandinavia." + +"What! Had he lost his wife?" + +"You can understand," said Holmes suavely, "that I extend to the +affairs of my other clients the same secrecy which I promise to +you in yours." + +"Of course! Very right! very right! I'm sure I beg pardon. As to +my own case, I am ready to give you any information which may +assist you in forming an opinion." + +"Thank you. I have already learned all that is in the public +prints, nothing more. I presume that I may take it as correct--this +article, for example, as to the disappearance of the bride." + +Lord St. Simon glanced over it. "Yes, it is correct, as far as it +goes." + +"But it needs a great deal of supplementing before anyone could +offer an opinion. I think that I may arrive at my facts most +directly by questioning you." + +"Pray do so." + +"When did you first meet Miss Hatty Doran?" + +"In San Francisco, a year ago." + +"You were travelling in the States?" + +"Yes." + +"Did you become engaged then?" + +"No." + +"But you were on a friendly footing?" + +"I was amused by her society, and she could see that I was +amused." + +"Her father is very rich?" + +"He is said to be the richest man on the Pacific slope." + +"And how did he make his money?" + +"In mining. He had nothing a few years ago. Then he struck gold, +invested it, and came up by leaps and bounds." + +"Now, what is your own impression as to the young lady's--your +wife's character?" + +The nobleman swung his glasses a little faster and stared down +into the fire. "You see, Mr. Holmes," said he, "my wife was +twenty before her father became a rich man. During that time she +ran free in a mining camp and wandered through woods or +mountains, so that her education has come from Nature rather than +from the schoolmaster. She is what we call in England a tomboy, +with a strong nature, wild and free, unfettered by any sort of +traditions. She is impetuous--volcanic, I was about to say. She +is swift in making up her mind and fearless in carrying out her +resolutions. On the other hand, I would not have given her the +name which I have the honour to bear"--he gave a little stately +cough--"had not I thought her to be at bottom a noble woman. I +believe that she is capable of heroic self-sacrifice and that +anything dishonourable would be repugnant to her." + +"Have you her photograph?" + +"I brought this with me." He opened a locket and showed us the +full face of a very lovely woman. It was not a photograph but an +ivory miniature, and the artist had brought out the full effect +of the lustrous black hair, the large dark eyes, and the +exquisite mouth. Holmes gazed long and earnestly at it. Then he +closed the locket and handed it back to Lord St. Simon. + +"The young lady came to London, then, and you renewed your +acquaintance?" + +"Yes, her father brought her over for this last London season. I +met her several times, became engaged to her, and have now +married her." + +"She brought, I understand, a considerable dowry?" + +"A fair dowry. Not more than is usual in my family." + +"And this, of course, remains to you, since the marriage is a +fait accompli?" + +"I really have made no inquiries on the subject." + +"Very naturally not. Did you see Miss Doran on the day before the +wedding?" + +"Yes." + +"Was she in good spirits?" + +"Never better. She kept talking of what we should do in our +future lives." + +"Indeed! That is very interesting. And on the morning of the +wedding?" + +"She was as bright as possible--at least until after the +ceremony." + +"And did you observe any change in her then?" + +"Well, to tell the truth, I saw then the first signs that I had +ever seen that her temper was just a little sharp. The incident +however, was too trivial to relate and can have no possible +bearing upon the case." + +"Pray let us have it, for all that." + +"Oh, it is childish. She dropped her bouquet as we went towards +the vestry. She was passing the front pew at the time, and it +fell over into the pew. There was a moment's delay, but the +gentleman in the pew handed it up to her again, and it did not +appear to be the worse for the fall. Yet when I spoke to her of +the matter, she answered me abruptly; and in the carriage, on our +way home, she seemed absurdly agitated over this trifling cause." + +"Indeed! You say that there was a gentleman in the pew. Some of +the general public were present, then?" + +"Oh, yes. It is impossible to exclude them when the church is +open." + +"This gentleman was not one of your wife's friends?" + +"No, no; I call him a gentleman by courtesy, but he was quite a +common-looking person. I hardly noticed his appearance. But +really I think that we are wandering rather far from the point." + +"Lady St. Simon, then, returned from the wedding in a less +cheerful frame of mind than she had gone to it. What did she do +on re-entering her father's house?" + +"I saw her in conversation with her maid." + +"And who is her maid?" + +"Alice is her name. She is an American and came from California +with her." + +"A confidential servant?" + +"A little too much so. It seemed to me that her mistress allowed +her to take great liberties. Still, of course, in America they +look upon these things in a different way." + +"How long did she speak to this Alice?" + +"Oh, a few minutes. I had something else to think of." + +"You did not overhear what they said?" + +"Lady St. Simon said something about 'jumping a claim.' She was +accustomed to use slang of the kind. I have no idea what she +meant." + +"American slang is very expressive sometimes. And what did your +wife do when she finished speaking to her maid?" + +"She walked into the breakfast-room." + +"On your arm?" + +"No, alone. She was very independent in little matters like that. +Then, after we had sat down for ten minutes or so, she rose +hurriedly, muttered some words of apology, and left the room. She +never came back." + +"But this maid, Alice, as I understand, deposes that she went to +her room, covered her bride's dress with a long ulster, put on a +bonnet, and went out." + +"Quite so. And she was afterwards seen walking into Hyde Park in +company with Flora Millar, a woman who is now in custody, and who +had already made a disturbance at Mr. Doran's house that +morning." + +"Ah, yes. I should like a few particulars as to this young lady, +and your relations to her." + +Lord St. Simon shrugged his shoulders and raised his eyebrows. +"We have been on a friendly footing for some years--I may say on +a very friendly footing. She used to be at the Allegro. I have +not treated her ungenerously, and she had no just cause of +complaint against me, but you know what women are, Mr. Holmes. +Flora was a dear little thing, but exceedingly hot-headed and +devotedly attached to me. She wrote me dreadful letters when she +heard that I was about to be married, and, to tell the truth, the +reason why I had the marriage celebrated so quietly was that I +feared lest there might be a scandal in the church. She came to +Mr. Doran's door just after we returned, and she endeavoured to +push her way in, uttering very abusive expressions towards my +wife, and even threatening her, but I had foreseen the +possibility of something of the sort, and I had two police +fellows there in private clothes, who soon pushed her out again. +She was quiet when she saw that there was no good in making a +row." + +"Did your wife hear all this?" + +"No, thank goodness, she did not." + +"And she was seen walking with this very woman afterwards?" + +"Yes. That is what Mr. Lestrade, of Scotland Yard, looks upon as +so serious. It is thought that Flora decoyed my wife out and laid +some terrible trap for her." + +"Well, it is a possible supposition." + +"You think so, too?" + +"I did not say a probable one. But you do not yourself look upon +this as likely?" + +"I do not think Flora would hurt a fly." + +"Still, jealousy is a strange transformer of characters. Pray +what is your own theory as to what took place?" + +"Well, really, I came to seek a theory, not to propound one. I +have given you all the facts. Since you ask me, however, I may +say that it has occurred to me as possible that the excitement of +this affair, the consciousness that she had made so immense a +social stride, had the effect of causing some little nervous +disturbance in my wife." + +"In short, that she had become suddenly deranged?" + +"Well, really, when I consider that she has turned her back--I +will not say upon me, but upon so much that many have aspired to +without success--I can hardly explain it in any other fashion." + +"Well, certainly that is also a conceivable hypothesis," said +Holmes, smiling. "And now, Lord St. Simon, I think that I have +nearly all my data. May I ask whether you were seated at the +breakfast-table so that you could see out of the window?" + +"We could see the other side of the road and the Park." + +"Quite so. Then I do not think that I need to detain you longer. +I shall communicate with you." + +"Should you be fortunate enough to solve this problem," said our +client, rising. + +"I have solved it." + +"Eh? What was that?" + +"I say that I have solved it." + +"Where, then, is my wife?" + +"That is a detail which I shall speedily supply." + +Lord St. Simon shook his head. "I am afraid that it will take +wiser heads than yours or mine," he remarked, and bowing in a +stately, old-fashioned manner he departed. + +"It is very good of Lord St. Simon to honour my head by putting +it on a level with his own," said Sherlock Holmes, laughing. "I +think that I shall have a whisky and soda and a cigar after all +this cross-questioning. I had formed my conclusions as to the +case before our client came into the room." + +"My dear Holmes!" + +"I have notes of several similar cases, though none, as I +remarked before, which were quite as prompt. My whole examination +served to turn my conjecture into a certainty. Circumstantial +evidence is occasionally very convincing, as when you find a +trout in the milk, to quote Thoreau's example." + +"But I have heard all that you have heard." + +"Without, however, the knowledge of pre-existing cases which +serves me so well. There was a parallel instance in Aberdeen some +years back, and something on very much the same lines at Munich +the year after the Franco-Prussian War. It is one of these +cases--but, hullo, here is Lestrade! Good-afternoon, Lestrade! +You will find an extra tumbler upon the sideboard, and there are +cigars in the box." + +The official detective was attired in a pea-jacket and cravat, +which gave him a decidedly nautical appearance, and he carried a +black canvas bag in his hand. With a short greeting he seated +himself and lit the cigar which had been offered to him. + +"What's up, then?" asked Holmes with a twinkle in his eye. "You +look dissatisfied." + +"And I feel dissatisfied. It is this infernal St. Simon marriage +case. I can make neither head nor tail of the business." + +"Really! You surprise me." + +"Who ever heard of such a mixed affair? Every clue seems to slip +through my fingers. I have been at work upon it all day." + +"And very wet it seems to have made you," said Holmes laying his +hand upon the arm of the pea-jacket. + +"Yes, I have been dragging the Serpentine." + +"In heaven's name, what for?" + +"In search of the body of Lady St. Simon." + +Sherlock Holmes leaned back in his chair and laughed heartily. + +"Have you dragged the basin of Trafalgar Square fountain?" he +asked. + +"Why? What do you mean?" + +"Because you have just as good a chance of finding this lady in +the one as in the other." + +Lestrade shot an angry glance at my companion. "I suppose you +know all about it," he snarled. + +"Well, I have only just heard the facts, but my mind is made up." + +"Oh, indeed! Then you think that the Serpentine plays no part in +the matter?" + +"I think it very unlikely." + +"Then perhaps you will kindly explain how it is that we found +this in it?" He opened his bag as he spoke, and tumbled onto the +floor a wedding-dress of watered silk, a pair of white satin +shoes and a bride's wreath and veil, all discoloured and soaked +in water. "There," said he, putting a new wedding-ring upon the +top of the pile. "There is a little nut for you to crack, Master +Holmes." + +"Oh, indeed!" said my friend, blowing blue rings into the air. +"You dragged them from the Serpentine?" + +"No. They were found floating near the margin by a park-keeper. +They have been identified as her clothes, and it seemed to me +that if the clothes were there the body would not be far off." + +"By the same brilliant reasoning, every man's body is to be found +in the neighbourhood of his wardrobe. And pray what did you hope +to arrive at through this?" + +"At some evidence implicating Flora Millar in the disappearance." + +"I am afraid that you will find it difficult." + +"Are you, indeed, now?" cried Lestrade with some bitterness. "I +am afraid, Holmes, that you are not very practical with your +deductions and your inferences. You have made two blunders in as +many minutes. This dress does implicate Miss Flora Millar." + +"And how?" + +"In the dress is a pocket. In the pocket is a card-case. In the +card-case is a note. And here is the very note." He slapped it +down upon the table in front of him. "Listen to this: 'You will +see me when all is ready. Come at once. F.H.M.' Now my theory all +along has been that Lady St. Simon was decoyed away by Flora +Millar, and that she, with confederates, no doubt, was +responsible for her disappearance. Here, signed with her +initials, is the very note which was no doubt quietly slipped +into her hand at the door and which lured her within their +reach." + +"Very good, Lestrade," said Holmes, laughing. "You really are +very fine indeed. Let me see it." He took up the paper in a +listless way, but his attention instantly became riveted, and he +gave a little cry of satisfaction. "This is indeed important," +said he. + +"Ha! you find it so?" + +"Extremely so. I congratulate you warmly." + +Lestrade rose in his triumph and bent his head to look. "Why," he +shrieked, "you're looking at the wrong side!" + +"On the contrary, this is the right side." + +"The right side? You're mad! Here is the note written in pencil +over here." + +"And over here is what appears to be the fragment of a hotel +bill, which interests me deeply." + +"There's nothing in it. I looked at it before," said Lestrade. +"'Oct. 4th, rooms 8s., breakfast 2s. 6d., cocktail 1s., lunch 2s. +6d., glass sherry, 8d.' I see nothing in that." + +"Very likely not. It is most important, all the same. As to the +note, it is important also, or at least the initials are, so I +congratulate you again." + +"I've wasted time enough," said Lestrade, rising. "I believe in +hard work and not in sitting by the fire spinning fine theories. +Good-day, Mr. Holmes, and we shall see which gets to the bottom +of the matter first." He gathered up the garments, thrust them +into the bag, and made for the door. + +"Just one hint to you, Lestrade," drawled Holmes before his rival +vanished; "I will tell you the true solution of the matter. Lady +St. Simon is a myth. There is not, and there never has been, any +such person." + +Lestrade looked sadly at my companion. Then he turned to me, +tapped his forehead three times, shook his head solemnly, and +hurried away. + +He had hardly shut the door behind him when Holmes rose to put on +his overcoat. "There is something in what the fellow says about +outdoor work," he remarked, "so I think, Watson, that I must +leave you to your papers for a little." + +It was after five o'clock when Sherlock Holmes left me, but I had +no time to be lonely, for within an hour there arrived a +confectioner's man with a very large flat box. This he unpacked +with the help of a youth whom he had brought with him, and +presently, to my very great astonishment, a quite epicurean +little cold supper began to be laid out upon our humble +lodging-house mahogany. There were a couple of brace of cold +woodcock, a pheasant, a pt de foie gras pie with a group of +ancient and cobwebby bottles. Having laid out all these luxuries, +my two visitors vanished away, like the genii of the Arabian +Nights, with no explanation save that the things had been paid +for and were ordered to this address. + +Just before nine o'clock Sherlock Holmes stepped briskly into the +room. His features were gravely set, but there was a light in his +eye which made me think that he had not been disappointed in his +conclusions. + +"They have laid the supper, then," he said, rubbing his hands. + +"You seem to expect company. They have laid for five." + +"Yes, I fancy we may have some company dropping in," said he. "I +am surprised that Lord St. Simon has not already arrived. Ha! I +fancy that I hear his step now upon the stairs." + +It was indeed our visitor of the afternoon who came bustling in, +dangling his glasses more vigorously than ever, and with a very +perturbed expression upon his aristocratic features. + +"My messenger reached you, then?" asked Holmes. + +"Yes, and I confess that the contents startled me beyond measure. +Have you good authority for what you say?" + +"The best possible." + +Lord St. Simon sank into a chair and passed his hand over his +forehead. + +"What will the Duke say," he murmured, "when he hears that one of +the family has been subjected to such humiliation?" + +"It is the purest accident. I cannot allow that there is any +humiliation." + +"Ah, you look on these things from another standpoint." + +"I fail to see that anyone is to blame. I can hardly see how the +lady could have acted otherwise, though her abrupt method of +doing it was undoubtedly to be regretted. Having no mother, she +had no one to advise her at such a crisis." + +"It was a slight, sir, a public slight," said Lord St. Simon, +tapping his fingers upon the table. + +"You must make allowance for this poor girl, placed in so +unprecedented a position." + +"I will make no allowance. I am very angry indeed, and I have +been shamefully used." + +"I think that I heard a ring," said Holmes. "Yes, there are steps +on the landing. If I cannot persuade you to take a lenient view +of the matter, Lord St. Simon, I have brought an advocate here +who may be more successful." He opened the door and ushered in a +lady and gentleman. "Lord St. Simon," said he "allow me to +introduce you to Mr. and Mrs. Francis Hay Moulton. The lady, I +think, you have already met." + +At the sight of these newcomers our client had sprung from his +seat and stood very erect, with his eyes cast down and his hand +thrust into the breast of his frock-coat, a picture of offended +dignity. The lady had taken a quick step forward and had held out +her hand to him, but he still refused to raise his eyes. It was +as well for his resolution, perhaps, for her pleading face was +one which it was hard to resist. + +"You're angry, Robert," said she. "Well, I guess you have every +cause to be." + +"Pray make no apology to me," said Lord St. Simon bitterly. + +"Oh, yes, I know that I have treated you real bad and that I +should have spoken to you before I went; but I was kind of +rattled, and from the time when I saw Frank here again I just +didn't know what I was doing or saying. I only wonder I didn't +fall down and do a faint right there before the altar." + +"Perhaps, Mrs. Moulton, you would like my friend and me to leave +the room while you explain this matter?" + +"If I may give an opinion," remarked the strange gentleman, +"we've had just a little too much secrecy over this business +already. For my part, I should like all Europe and America to +hear the rights of it." He was a small, wiry, sunburnt man, +clean-shaven, with a sharp face and alert manner. + +"Then I'll tell our story right away," said the lady. "Frank here +and I met in '84, in McQuire's camp, near the Rockies, where pa +was working a claim. We were engaged to each other, Frank and I; +but then one day father struck a rich pocket and made a pile, +while poor Frank here had a claim that petered out and came to +nothing. The richer pa grew the poorer was Frank; so at last pa +wouldn't hear of our engagement lasting any longer, and he took +me away to 'Frisco. Frank wouldn't throw up his hand, though; so +he followed me there, and he saw me without pa knowing anything +about it. It would only have made him mad to know, so we just +fixed it all up for ourselves. Frank said that he would go and +make his pile, too, and never come back to claim me until he had +as much as pa. So then I promised to wait for him to the end of +time and pledged myself not to marry anyone else while he lived. +'Why shouldn't we be married right away, then,' said he, 'and +then I will feel sure of you; and I won't claim to be your +husband until I come back?' Well, we talked it over, and he had +fixed it all up so nicely, with a clergyman all ready in waiting, +that we just did it right there; and then Frank went off to seek +his fortune, and I went back to pa. + +"The next I heard of Frank was that he was in Montana, and then +he went prospecting in Arizona, and then I heard of him from New +Mexico. After that came a long newspaper story about how a +miners' camp had been attacked by Apache Indians, and there was +my Frank's name among the killed. I fainted dead away, and I was +very sick for months after. Pa thought I had a decline and took +me to half the doctors in 'Frisco. Not a word of news came for a +year and more, so that I never doubted that Frank was really +dead. Then Lord St. Simon came to 'Frisco, and we came to London, +and a marriage was arranged, and pa was very pleased, but I felt +all the time that no man on this earth would ever take the place +in my heart that had been given to my poor Frank. + +"Still, if I had married Lord St. Simon, of course I'd have done +my duty by him. We can't command our love, but we can our +actions. I went to the altar with him with the intention to make +him just as good a wife as it was in me to be. But you may +imagine what I felt when, just as I came to the altar rails, I +glanced back and saw Frank standing and looking at me out of the +first pew. I thought it was his ghost at first; but when I looked +again there he was still, with a kind of question in his eyes, as +if to ask me whether I were glad or sorry to see him. I wonder I +didn't drop. I know that everything was turning round, and the +words of the clergyman were just like the buzz of a bee in my +ear. I didn't know what to do. Should I stop the service and make +a scene in the church? I glanced at him again, and he seemed to +know what I was thinking, for he raised his finger to his lips to +tell me to be still. Then I saw him scribble on a piece of paper, +and I knew that he was writing me a note. As I passed his pew on +the way out I dropped my bouquet over to him, and he slipped the +note into my hand when he returned me the flowers. It was only a +line asking me to join him when he made the sign to me to do so. +Of course I never doubted for a moment that my first duty was now +to him, and I determined to do just whatever he might direct. + +"When I got back I told my maid, who had known him in California, +and had always been his friend. I ordered her to say nothing, but +to get a few things packed and my ulster ready. I know I ought to +have spoken to Lord St. Simon, but it was dreadful hard before +his mother and all those great people. I just made up my mind to +run away and explain afterwards. I hadn't been at the table ten +minutes before I saw Frank out of the window at the other side of +the road. He beckoned to me and then began walking into the Park. +I slipped out, put on my things, and followed him. Some woman +came talking something or other about Lord St. Simon to +me--seemed to me from the little I heard as if he had a little +secret of his own before marriage also--but I managed to get away +from her and soon overtook Frank. We got into a cab together, and +away we drove to some lodgings he had taken in Gordon Square, and +that was my true wedding after all those years of waiting. Frank +had been a prisoner among the Apaches, had escaped, came on to +'Frisco, found that I had given him up for dead and had gone to +England, followed me there, and had come upon me at last on the +very morning of my second wedding." + +"I saw it in a paper," explained the American. "It gave the name +and the church but not where the lady lived." + +"Then we had a talk as to what we should do, and Frank was all +for openness, but I was so ashamed of it all that I felt as if I +should like to vanish away and never see any of them again--just +sending a line to pa, perhaps, to show him that I was alive. It +was awful to me to think of all those lords and ladies sitting +round that breakfast-table and waiting for me to come back. So +Frank took my wedding-clothes and things and made a bundle of +them, so that I should not be traced, and dropped them away +somewhere where no one could find them. It is likely that we +should have gone on to Paris to-morrow, only that this good +gentleman, Mr. Holmes, came round to us this evening, though how +he found us is more than I can think, and he showed us very +clearly and kindly that I was wrong and that Frank was right, and +that we should be putting ourselves in the wrong if we were so +secret. Then he offered to give us a chance of talking to Lord +St. Simon alone, and so we came right away round to his rooms at +once. Now, Robert, you have heard it all, and I am very sorry if +I have given you pain, and I hope that you do not think very +meanly of me." + +Lord St. Simon had by no means relaxed his rigid attitude, but +had listened with a frowning brow and a compressed lip to this +long narrative. + +"Excuse me," he said, "but it is not my custom to discuss my most +intimate personal affairs in this public manner." + +"Then you won't forgive me? You won't shake hands before I go?" + +"Oh, certainly, if it would give you any pleasure." He put out +his hand and coldly grasped that which she extended to him. + +"I had hoped," suggested Holmes, "that you would have joined us +in a friendly supper." + +"I think that there you ask a little too much," responded his +Lordship. "I may be forced to acquiesce in these recent +developments, but I can hardly be expected to make merry over +them. I think that with your permission I will now wish you all a +very good-night." He included us all in a sweeping bow and +stalked out of the room. + +"Then I trust that you at least will honour me with your +company," said Sherlock Holmes. "It is always a joy to meet an +American, Mr. Moulton, for I am one of those who believe that the +folly of a monarch and the blundering of a minister in far-gone +years will not prevent our children from being some day citizens +of the same world-wide country under a flag which shall be a +quartering of the Union Jack with the Stars and Stripes." + +"The case has been an interesting one," remarked Holmes when our +visitors had left us, "because it serves to show very clearly how +simple the explanation may be of an affair which at first sight +seems to be almost inexplicable. Nothing could be more natural +than the sequence of events as narrated by this lady, and nothing +stranger than the result when viewed, for instance, by Mr. +Lestrade of Scotland Yard." + +"You were not yourself at fault at all, then?" + +"From the first, two facts were very obvious to me, the one that +the lady had been quite willing to undergo the wedding ceremony, +the other that she had repented of it within a few minutes of +returning home. Obviously something had occurred during the +morning, then, to cause her to change her mind. What could that +something be? She could not have spoken to anyone when she was +out, for she had been in the company of the bridegroom. Had she +seen someone, then? If she had, it must be someone from America +because she had spent so short a time in this country that she +could hardly have allowed anyone to acquire so deep an influence +over her that the mere sight of him would induce her to change +her plans so completely. You see we have already arrived, by a +process of exclusion, at the idea that she might have seen an +American. Then who could this American be, and why should he +possess so much influence over her? It might be a lover; it might +be a husband. Her young womanhood had, I knew, been spent in +rough scenes and under strange conditions. So far I had got +before I ever heard Lord St. Simon's narrative. When he told us +of a man in a pew, of the change in the bride's manner, of so +transparent a device for obtaining a note as the dropping of a +bouquet, of her resort to her confidential maid, and of her very +significant allusion to claim-jumping--which in miners' parlance +means taking possession of that which another person has a prior +claim to--the whole situation became absolutely clear. She had +gone off with a man, and the man was either a lover or was a +previous husband--the chances being in favour of the latter." + +"And how in the world did you find them?" + +"It might have been difficult, but friend Lestrade held +information in his hands the value of which he did not himself +know. The initials were, of course, of the highest importance, +but more valuable still was it to know that within a week he had +settled his bill at one of the most select London hotels." + +"How did you deduce the select?" + +"By the select prices. Eight shillings for a bed and eightpence +for a glass of sherry pointed to one of the most expensive +hotels. There are not many in London which charge at that rate. +In the second one which I visited in Northumberland Avenue, I +learned by an inspection of the book that Francis H. Moulton, an +American gentleman, had left only the day before, and on looking +over the entries against him, I came upon the very items which I +had seen in the duplicate bill. His letters were to be forwarded +to 226 Gordon Square; so thither I travelled, and being fortunate +enough to find the loving couple at home, I ventured to give them +some paternal advice and to point out to them that it would be +better in every way that they should make their position a little +clearer both to the general public and to Lord St. Simon in +particular. I invited them to meet him here, and, as you see, I +made him keep the appointment." + +"But with no very good result," I remarked. "His conduct was +certainly not very gracious." + +"Ah, Watson," said Holmes, smiling, "perhaps you would not be +very gracious either, if, after all the trouble of wooing and +wedding, you found yourself deprived in an instant of wife and of +fortune. I think that we may judge Lord St. Simon very mercifully +and thank our stars that we are never likely to find ourselves in +the same position. Draw your chair up and hand me my violin, for +the only problem we have still to solve is how to while away +these bleak autumnal evenings." + + + +XI. THE ADVENTURE OF THE BERYL CORONET + +"Holmes," said I as I stood one morning in our bow-window looking +down the street, "here is a madman coming along. It seems rather +sad that his relatives should allow him to come out alone." + +My friend rose lazily from his armchair and stood with his hands +in the pockets of his dressing-gown, looking over my shoulder. It +was a bright, crisp February morning, and the snow of the day +before still lay deep upon the ground, shimmering brightly in the +wintry sun. Down the centre of Baker Street it had been ploughed +into a brown crumbly band by the traffic, but at either side and +on the heaped-up edges of the foot-paths it still lay as white as +when it fell. The grey pavement had been cleaned and scraped, but +was still dangerously slippery, so that there were fewer +passengers than usual. Indeed, from the direction of the +Metropolitan Station no one was coming save the single gentleman +whose eccentric conduct had drawn my attention. + +He was a man of about fifty, tall, portly, and imposing, with a +massive, strongly marked face and a commanding figure. He was +dressed in a sombre yet rich style, in black frock-coat, shining +hat, neat brown gaiters, and well-cut pearl-grey trousers. Yet +his actions were in absurd contrast to the dignity of his dress +and features, for he was running hard, with occasional little +springs, such as a weary man gives who is little accustomed to +set any tax upon his legs. As he ran he jerked his hands up and +down, waggled his head, and writhed his face into the most +extraordinary contortions. + +"What on earth can be the matter with him?" I asked. "He is +looking up at the numbers of the houses." + +"I believe that he is coming here," said Holmes, rubbing his +hands. + +"Here?" + +"Yes; I rather think he is coming to consult me professionally. I +think that I recognise the symptoms. Ha! did I not tell you?" As +he spoke, the man, puffing and blowing, rushed at our door and +pulled at our bell until the whole house resounded with the +clanging. + +A few moments later he was in our room, still puffing, still +gesticulating, but with so fixed a look of grief and despair in +his eyes that our smiles were turned in an instant to horror and +pity. For a while he could not get his words out, but swayed his +body and plucked at his hair like one who has been driven to the +extreme limits of his reason. Then, suddenly springing to his +feet, he beat his head against the wall with such force that we +both rushed upon him and tore him away to the centre of the room. +Sherlock Holmes pushed him down into the easy-chair and, sitting +beside him, patted his hand and chatted with him in the easy, +soothing tones which he knew so well how to employ. + +"You have come to me to tell your story, have you not?" said he. +"You are fatigued with your haste. Pray wait until you have +recovered yourself, and then I shall be most happy to look into +any little problem which you may submit to me." + +The man sat for a minute or more with a heaving chest, fighting +against his emotion. Then he passed his handkerchief over his +brow, set his lips tight, and turned his face towards us. + +"No doubt you think me mad?" said he. + +"I see that you have had some great trouble," responded Holmes. + +"God knows I have!--a trouble which is enough to unseat my +reason, so sudden and so terrible is it. Public disgrace I might +have faced, although I am a man whose character has never yet +borne a stain. Private affliction also is the lot of every man; +but the two coming together, and in so frightful a form, have +been enough to shake my very soul. Besides, it is not I alone. +The very noblest in the land may suffer unless some way be found +out of this horrible affair." + +"Pray compose yourself, sir," said Holmes, "and let me have a +clear account of who you are and what it is that has befallen +you." + +"My name," answered our visitor, "is probably familiar to your +ears. I am Alexander Holder, of the banking firm of Holder & +Stevenson, of Threadneedle Street." + +The name was indeed well known to us as belonging to the senior +partner in the second largest private banking concern in the City +of London. What could have happened, then, to bring one of the +foremost citizens of London to this most pitiable pass? We +waited, all curiosity, until with another effort he braced +himself to tell his story. + +"I feel that time is of value," said he; "that is why I hastened +here when the police inspector suggested that I should secure +your co-operation. I came to Baker Street by the Underground and +hurried from there on foot, for the cabs go slowly through this +snow. That is why I was so out of breath, for I am a man who +takes very little exercise. I feel better now, and I will put the +facts before you as shortly and yet as clearly as I can. + +"It is, of course, well known to you that in a successful banking +business as much depends upon our being able to find remunerative +investments for our funds as upon our increasing our connection +and the number of our depositors. One of our most lucrative means +of laying out money is in the shape of loans, where the security +is unimpeachable. We have done a good deal in this direction +during the last few years, and there are many noble families to +whom we have advanced large sums upon the security of their +pictures, libraries, or plate. + +"Yesterday morning I was seated in my office at the bank when a +card was brought in to me by one of the clerks. I started when I +saw the name, for it was that of none other than--well, perhaps +even to you I had better say no more than that it was a name +which is a household word all over the earth--one of the highest, +noblest, most exalted names in England. I was overwhelmed by the +honour and attempted, when he entered, to say so, but he plunged +at once into business with the air of a man who wishes to hurry +quickly through a disagreeable task. + +"'Mr. Holder,' said he, 'I have been informed that you are in the +habit of advancing money.' + +"'The firm does so when the security is good.' I answered. + +"'It is absolutely essential to me,' said he, 'that I should have +50,000 pounds at once. I could, of course, borrow so trifling a +sum ten times over from my friends, but I much prefer to make it +a matter of business and to carry out that business myself. In my +position you can readily understand that it is unwise to place +one's self under obligations.' + +"'For how long, may I ask, do you want this sum?' I asked. + +"'Next Monday I have a large sum due to me, and I shall then most +certainly repay what you advance, with whatever interest you +think it right to charge. But it is very essential to me that the +money should be paid at once.' + +"'I should be happy to advance it without further parley from my +own private purse,' said I, 'were it not that the strain would be +rather more than it could bear. If, on the other hand, I am to do +it in the name of the firm, then in justice to my partner I must +insist that, even in your case, every businesslike precaution +should be taken.' + +"'I should much prefer to have it so,' said he, raising up a +square, black morocco case which he had laid beside his chair. +'You have doubtless heard of the Beryl Coronet?' + +"'One of the most precious public possessions of the empire,' +said I. + +"'Precisely.' He opened the case, and there, imbedded in soft, +flesh-coloured velvet, lay the magnificent piece of jewellery +which he had named. 'There are thirty-nine enormous beryls,' said +he, 'and the price of the gold chasing is incalculable. The +lowest estimate would put the worth of the coronet at double the +sum which I have asked. I am prepared to leave it with you as my +security.' + +"I took the precious case into my hands and looked in some +perplexity from it to my illustrious client. + +"'You doubt its value?' he asked. + +"'Not at all. I only doubt--' + +"'The propriety of my leaving it. You may set your mind at rest +about that. I should not dream of doing so were it not absolutely +certain that I should be able in four days to reclaim it. It is a +pure matter of form. Is the security sufficient?' + +"'Ample.' + +"'You understand, Mr. Holder, that I am giving you a strong proof +of the confidence which I have in you, founded upon all that I +have heard of you. I rely upon you not only to be discreet and to +refrain from all gossip upon the matter but, above all, to +preserve this coronet with every possible precaution because I +need not say that a great public scandal would be caused if any +harm were to befall it. Any injury to it would be almost as +serious as its complete loss, for there are no beryls in the +world to match these, and it would be impossible to replace them. +I leave it with you, however, with every confidence, and I shall +call for it in person on Monday morning.' + +"Seeing that my client was anxious to leave, I said no more but, +calling for my cashier, I ordered him to pay over fifty 1000 +pound notes. When I was alone once more, however, with the +precious case lying upon the table in front of me, I could not +but think with some misgivings of the immense responsibility +which it entailed upon me. There could be no doubt that, as it +was a national possession, a horrible scandal would ensue if any +misfortune should occur to it. I already regretted having ever +consented to take charge of it. However, it was too late to alter +the matter now, so I locked it up in my private safe and turned +once more to my work. + +"When evening came I felt that it would be an imprudence to leave +so precious a thing in the office behind me. Bankers' safes had +been forced before now, and why should not mine be? If so, how +terrible would be the position in which I should find myself! I +determined, therefore, that for the next few days I would always +carry the case backward and forward with me, so that it might +never be really out of my reach. With this intention, I called a +cab and drove out to my house at Streatham, carrying the jewel +with me. I did not breathe freely until I had taken it upstairs +and locked it in the bureau of my dressing-room. + +"And now a word as to my household, Mr. Holmes, for I wish you to +thoroughly understand the situation. My groom and my page sleep +out of the house, and may be set aside altogether. I have three +maid-servants who have been with me a number of years and whose +absolute reliability is quite above suspicion. Another, Lucy +Parr, the second waiting-maid, has only been in my service a few +months. She came with an excellent character, however, and has +always given me satisfaction. She is a very pretty girl and has +attracted admirers who have occasionally hung about the place. +That is the only drawback which we have found to her, but we +believe her to be a thoroughly good girl in every way. + +"So much for the servants. My family itself is so small that it +will not take me long to describe it. I am a widower and have an +only son, Arthur. He has been a disappointment to me, Mr. +Holmes--a grievous disappointment. I have no doubt that I am +myself to blame. People tell me that I have spoiled him. Very +likely I have. When my dear wife died I felt that he was all I +had to love. I could not bear to see the smile fade even for a +moment from his face. I have never denied him a wish. Perhaps it +would have been better for both of us had I been sterner, but I +meant it for the best. + +"It was naturally my intention that he should succeed me in my +business, but he was not of a business turn. He was wild, +wayward, and, to speak the truth, I could not trust him in the +handling of large sums of money. When he was young he became a +member of an aristocratic club, and there, having charming +manners, he was soon the intimate of a number of men with long +purses and expensive habits. He learned to play heavily at cards +and to squander money on the turf, until he had again and again +to come to me and implore me to give him an advance upon his +allowance, that he might settle his debts of honour. He tried +more than once to break away from the dangerous company which he +was keeping, but each time the influence of his friend, Sir +George Burnwell, was enough to draw him back again. + +"And, indeed, I could not wonder that such a man as Sir George +Burnwell should gain an influence over him, for he has frequently +brought him to my house, and I have found myself that I could +hardly resist the fascination of his manner. He is older than +Arthur, a man of the world to his finger-tips, one who had been +everywhere, seen everything, a brilliant talker, and a man of +great personal beauty. Yet when I think of him in cold blood, far +away from the glamour of his presence, I am convinced from his +cynical speech and the look which I have caught in his eyes that +he is one who should be deeply distrusted. So I think, and so, +too, thinks my little Mary, who has a woman's quick insight into +character. + +"And now there is only she to be described. She is my niece; but +when my brother died five years ago and left her alone in the +world I adopted her, and have looked upon her ever since as my +daughter. She is a sunbeam in my house--sweet, loving, beautiful, +a wonderful manager and housekeeper, yet as tender and quiet and +gentle as a woman could be. She is my right hand. I do not know +what I could do without her. In only one matter has she ever gone +against my wishes. Twice my boy has asked her to marry him, for +he loves her devotedly, but each time she has refused him. I +think that if anyone could have drawn him into the right path it +would have been she, and that his marriage might have changed his +whole life; but now, alas! it is too late--forever too late! + +"Now, Mr. Holmes, you know the people who live under my roof, and +I shall continue with my miserable story. + +"When we were taking coffee in the drawing-room that night after +dinner, I told Arthur and Mary my experience, and of the precious +treasure which we had under our roof, suppressing only the name +of my client. Lucy Parr, who had brought in the coffee, had, I am +sure, left the room; but I cannot swear that the door was closed. +Mary and Arthur were much interested and wished to see the famous +coronet, but I thought it better not to disturb it. + +"'Where have you put it?' asked Arthur. + +"'In my own bureau.' + +"'Well, I hope to goodness the house won't be burgled during the +night.' said he. + +"'It is locked up,' I answered. + +"'Oh, any old key will fit that bureau. When I was a youngster I +have opened it myself with the key of the box-room cupboard.' + +"He often had a wild way of talking, so that I thought little of +what he said. He followed me to my room, however, that night with +a very grave face. + +"'Look here, dad,' said he with his eyes cast down, 'can you let +me have 200 pounds?' + +"'No, I cannot!' I answered sharply. 'I have been far too +generous with you in money matters.' + +"'You have been very kind,' said he, 'but I must have this money, +or else I can never show my face inside the club again.' + +"'And a very good thing, too!' I cried. + +"'Yes, but you would not have me leave it a dishonoured man,' +said he. 'I could not bear the disgrace. I must raise the money +in some way, and if you will not let me have it, then I must try +other means.' + +"I was very angry, for this was the third demand during the +month. 'You shall not have a farthing from me,' I cried, on which +he bowed and left the room without another word. + +"When he was gone I unlocked my bureau, made sure that my +treasure was safe, and locked it again. Then I started to go +round the house to see that all was secure--a duty which I +usually leave to Mary but which I thought it well to perform +myself that night. As I came down the stairs I saw Mary herself +at the side window of the hall, which she closed and fastened as +I approached. + +"'Tell me, dad,' said she, looking, I thought, a little +disturbed, 'did you give Lucy, the maid, leave to go out +to-night?' + +"'Certainly not.' + +"'She came in just now by the back door. I have no doubt that she +has only been to the side gate to see someone, but I think that +it is hardly safe and should be stopped.' + +"'You must speak to her in the morning, or I will if you prefer +it. Are you sure that everything is fastened?' + +"'Quite sure, dad.' + +"'Then, good-night.' I kissed her and went up to my bedroom +again, where I was soon asleep. + +"I am endeavouring to tell you everything, Mr. Holmes, which may +have any bearing upon the case, but I beg that you will question +me upon any point which I do not make clear." + +"On the contrary, your statement is singularly lucid." + +"I come to a part of my story now in which I should wish to be +particularly so. I am not a very heavy sleeper, and the anxiety +in my mind tended, no doubt, to make me even less so than usual. +About two in the morning, then, I was awakened by some sound in +the house. It had ceased ere I was wide awake, but it had left an +impression behind it as though a window had gently closed +somewhere. I lay listening with all my ears. Suddenly, to my +horror, there was a distinct sound of footsteps moving softly in +the next room. I slipped out of bed, all palpitating with fear, +and peeped round the corner of my dressing-room door. + +"'Arthur!' I screamed, 'you villain! you thief! How dare you +touch that coronet?' + +"The gas was half up, as I had left it, and my unhappy boy, +dressed only in his shirt and trousers, was standing beside the +light, holding the coronet in his hands. He appeared to be +wrenching at it, or bending it with all his strength. At my cry +he dropped it from his grasp and turned as pale as death. I +snatched it up and examined it. One of the gold corners, with +three of the beryls in it, was missing. + +"'You blackguard!' I shouted, beside myself with rage. 'You have +destroyed it! You have dishonoured me forever! Where are the +jewels which you have stolen?' + +"'Stolen!' he cried. + +"'Yes, thief!' I roared, shaking him by the shoulder. + +"'There are none missing. There cannot be any missing,' said he. + +"'There are three missing. And you know where they are. Must I +call you a liar as well as a thief? Did I not see you trying to +tear off another piece?' + +"'You have called me names enough,' said he, 'I will not stand it +any longer. I shall not say another word about this business, +since you have chosen to insult me. I will leave your house in +the morning and make my own way in the world.' + +"'You shall leave it in the hands of the police!' I cried +half-mad with grief and rage. 'I shall have this matter probed to +the bottom.' + +"'You shall learn nothing from me,' said he with a passion such +as I should not have thought was in his nature. 'If you choose to +call the police, let the police find what they can.' + +"By this time the whole house was astir, for I had raised my +voice in my anger. Mary was the first to rush into my room, and, +at the sight of the coronet and of Arthur's face, she read the +whole story and, with a scream, fell down senseless on the +ground. I sent the house-maid for the police and put the +investigation into their hands at once. When the inspector and a +constable entered the house, Arthur, who had stood sullenly with +his arms folded, asked me whether it was my intention to charge +him with theft. I answered that it had ceased to be a private +matter, but had become a public one, since the ruined coronet was +national property. I was determined that the law should have its +way in everything. + +"'At least,' said he, 'you will not have me arrested at once. It +would be to your advantage as well as mine if I might leave the +house for five minutes.' + +"'That you may get away, or perhaps that you may conceal what you +have stolen,' said I. And then, realising the dreadful position +in which I was placed, I implored him to remember that not only +my honour but that of one who was far greater than I was at +stake; and that he threatened to raise a scandal which would +convulse the nation. He might avert it all if he would but tell +me what he had done with the three missing stones. + +"'You may as well face the matter,' said I; 'you have been caught +in the act, and no confession could make your guilt more heinous. +If you but make such reparation as is in your power, by telling +us where the beryls are, all shall be forgiven and forgotten.' + +"'Keep your forgiveness for those who ask for it,' he answered, +turning away from me with a sneer. I saw that he was too hardened +for any words of mine to influence him. There was but one way for +it. I called in the inspector and gave him into custody. A search +was made at once not only of his person but of his room and of +every portion of the house where he could possibly have concealed +the gems; but no trace of them could be found, nor would the +wretched boy open his mouth for all our persuasions and our +threats. This morning he was removed to a cell, and I, after +going through all the police formalities, have hurried round to +you to implore you to use your skill in unravelling the matter. +The police have openly confessed that they can at present make +nothing of it. You may go to any expense which you think +necessary. I have already offered a reward of 1000 pounds. My +God, what shall I do! I have lost my honour, my gems, and my son +in one night. Oh, what shall I do!" + +He put a hand on either side of his head and rocked himself to +and fro, droning to himself like a child whose grief has got +beyond words. + +Sherlock Holmes sat silent for some few minutes, with his brows +knitted and his eyes fixed upon the fire. + +"Do you receive much company?" he asked. + +"None save my partner with his family and an occasional friend of +Arthur's. Sir George Burnwell has been several times lately. No +one else, I think." + +"Do you go out much in society?" + +"Arthur does. Mary and I stay at home. We neither of us care for +it." + +"That is unusual in a young girl." + +"She is of a quiet nature. Besides, she is not so very young. She +is four-and-twenty." + +"This matter, from what you say, seems to have been a shock to +her also." + +"Terrible! She is even more affected than I." + +"You have neither of you any doubt as to your son's guilt?" + +"How can we have when I saw him with my own eyes with the coronet +in his hands." + +"I hardly consider that a conclusive proof. Was the remainder of +the coronet at all injured?" + +"Yes, it was twisted." + +"Do you not think, then, that he might have been trying to +straighten it?" + +"God bless you! You are doing what you can for him and for me. +But it is too heavy a task. What was he doing there at all? If +his purpose were innocent, why did he not say so?" + +"Precisely. And if it were guilty, why did he not invent a lie? +His silence appears to me to cut both ways. There are several +singular points about the case. What did the police think of the +noise which awoke you from your sleep?" + +"They considered that it might be caused by Arthur's closing his +bedroom door." + +"A likely story! As if a man bent on felony would slam his door +so as to wake a household. What did they say, then, of the +disappearance of these gems?" + +"They are still sounding the planking and probing the furniture +in the hope of finding them." + +"Have they thought of looking outside the house?" + +"Yes, they have shown extraordinary energy. The whole garden has +already been minutely examined." + +"Now, my dear sir," said Holmes, "is it not obvious to you now +that this matter really strikes very much deeper than either you +or the police were at first inclined to think? It appeared to you +to be a simple case; to me it seems exceedingly complex. Consider +what is involved by your theory. You suppose that your son came +down from his bed, went, at great risk, to your dressing-room, +opened your bureau, took out your coronet, broke off by main +force a small portion of it, went off to some other place, +concealed three gems out of the thirty-nine, with such skill that +nobody can find them, and then returned with the other thirty-six +into the room in which he exposed himself to the greatest danger +of being discovered. I ask you now, is such a theory tenable?" + +"But what other is there?" cried the banker with a gesture of +despair. "If his motives were innocent, why does he not explain +them?" + +"It is our task to find that out," replied Holmes; "so now, if +you please, Mr. Holder, we will set off for Streatham together, +and devote an hour to glancing a little more closely into +details." + +My friend insisted upon my accompanying them in their expedition, +which I was eager enough to do, for my curiosity and sympathy +were deeply stirred by the story to which we had listened. I +confess that the guilt of the banker's son appeared to me to be +as obvious as it did to his unhappy father, but still I had such +faith in Holmes' judgment that I felt that there must be some +grounds for hope as long as he was dissatisfied with the accepted +explanation. He hardly spoke a word the whole way out to the +southern suburb, but sat with his chin upon his breast and his +hat drawn over his eyes, sunk in the deepest thought. Our client +appeared to have taken fresh heart at the little glimpse of hope +which had been presented to him, and he even broke into a +desultory chat with me over his business affairs. A short railway +journey and a shorter walk brought us to Fairbank, the modest +residence of the great financier. + +Fairbank was a good-sized square house of white stone, standing +back a little from the road. A double carriage-sweep, with a +snow-clad lawn, stretched down in front to two large iron gates +which closed the entrance. On the right side was a small wooden +thicket, which led into a narrow path between two neat hedges +stretching from the road to the kitchen door, and forming the +tradesmen's entrance. On the left ran a lane which led to the +stables, and was not itself within the grounds at all, being a +public, though little used, thoroughfare. Holmes left us standing +at the door and walked slowly all round the house, across the +front, down the tradesmen's path, and so round by the garden +behind into the stable lane. So long was he that Mr. Holder and I +went into the dining-room and waited by the fire until he should +return. We were sitting there in silence when the door opened and +a young lady came in. She was rather above the middle height, +slim, with dark hair and eyes, which seemed the darker against +the absolute pallor of her skin. I do not think that I have ever +seen such deadly paleness in a woman's face. Her lips, too, were +bloodless, but her eyes were flushed with crying. As she swept +silently into the room she impressed me with a greater sense of +grief than the banker had done in the morning, and it was the +more striking in her as she was evidently a woman of strong +character, with immense capacity for self-restraint. Disregarding +my presence, she went straight to her uncle and passed her hand +over his head with a sweet womanly caress. + +"You have given orders that Arthur should be liberated, have you +not, dad?" she asked. + +"No, no, my girl, the matter must be probed to the bottom." + +"But I am so sure that he is innocent. You know what woman's +instincts are. I know that he has done no harm and that you will +be sorry for having acted so harshly." + +"Why is he silent, then, if he is innocent?" + +"Who knows? Perhaps because he was so angry that you should +suspect him." + +"How could I help suspecting him, when I actually saw him with +the coronet in his hand?" + +"Oh, but he had only picked it up to look at it. Oh, do, do take +my word for it that he is innocent. Let the matter drop and say +no more. It is so dreadful to think of our dear Arthur in +prison!" + +"I shall never let it drop until the gems are found--never, Mary! +Your affection for Arthur blinds you as to the awful consequences +to me. Far from hushing the thing up, I have brought a gentleman +down from London to inquire more deeply into it." + +"This gentleman?" she asked, facing round to me. + +"No, his friend. He wished us to leave him alone. He is round in +the stable lane now." + +"The stable lane?" She raised her dark eyebrows. "What can he +hope to find there? Ah! this, I suppose, is he. I trust, sir, +that you will succeed in proving, what I feel sure is the truth, +that my cousin Arthur is innocent of this crime." + +"I fully share your opinion, and I trust, with you, that we may +prove it," returned Holmes, going back to the mat to knock the +snow from his shoes. "I believe I have the honour of addressing +Miss Mary Holder. Might I ask you a question or two?" + +"Pray do, sir, if it may help to clear this horrible affair up." + +"You heard nothing yourself last night?" + +"Nothing, until my uncle here began to speak loudly. I heard +that, and I came down." + +"You shut up the windows and doors the night before. Did you +fasten all the windows?" + +"Yes." + +"Were they all fastened this morning?" + +"Yes." + +"You have a maid who has a sweetheart? I think that you remarked +to your uncle last night that she had been out to see him?" + +"Yes, and she was the girl who waited in the drawing-room, and +who may have heard uncle's remarks about the coronet." + +"I see. You infer that she may have gone out to tell her +sweetheart, and that the two may have planned the robbery." + +"But what is the good of all these vague theories," cried the +banker impatiently, "when I have told you that I saw Arthur with +the coronet in his hands?" + +"Wait a little, Mr. Holder. We must come back to that. About this +girl, Miss Holder. You saw her return by the kitchen door, I +presume?" + +"Yes; when I went to see if the door was fastened for the night I +met her slipping in. I saw the man, too, in the gloom." + +"Do you know him?" + +"Oh, yes! he is the green-grocer who brings our vegetables round. +His name is Francis Prosper." + +"He stood," said Holmes, "to the left of the door--that is to +say, farther up the path than is necessary to reach the door?" + +"Yes, he did." + +"And he is a man with a wooden leg?" + +Something like fear sprang up in the young lady's expressive +black eyes. "Why, you are like a magician," said she. "How do you +know that?" She smiled, but there was no answering smile in +Holmes' thin, eager face. + +"I should be very glad now to go upstairs," said he. "I shall +probably wish to go over the outside of the house again. Perhaps +I had better take a look at the lower windows before I go up." + +He walked swiftly round from one to the other, pausing only at +the large one which looked from the hall onto the stable lane. +This he opened and made a very careful examination of the sill +with his powerful magnifying lens. "Now we shall go upstairs," +said he at last. + +The banker's dressing-room was a plainly furnished little +chamber, with a grey carpet, a large bureau, and a long mirror. +Holmes went to the bureau first and looked hard at the lock. + +"Which key was used to open it?" he asked. + +"That which my son himself indicated--that of the cupboard of the +lumber-room." + +"Have you it here?" + +"That is it on the dressing-table." + +Sherlock Holmes took it up and opened the bureau. + +"It is a noiseless lock," said he. "It is no wonder that it did +not wake you. This case, I presume, contains the coronet. We must +have a look at it." He opened the case, and taking out the diadem +he laid it upon the table. It was a magnificent specimen of the +jeweller's art, and the thirty-six stones were the finest that I +have ever seen. At one side of the coronet was a cracked edge, +where a corner holding three gems had been torn away. + +"Now, Mr. Holder," said Holmes, "here is the corner which +corresponds to that which has been so unfortunately lost. Might I +beg that you will break it off." + +The banker recoiled in horror. "I should not dream of trying," +said he. + +"Then I will." Holmes suddenly bent his strength upon it, but +without result. "I feel it give a little," said he; "but, though +I am exceptionally strong in the fingers, it would take me all my +time to break it. An ordinary man could not do it. Now, what do +you think would happen if I did break it, Mr. Holder? There would +be a noise like a pistol shot. Do you tell me that all this +happened within a few yards of your bed and that you heard +nothing of it?" + +"I do not know what to think. It is all dark to me." + +"But perhaps it may grow lighter as we go. What do you think, +Miss Holder?" + +"I confess that I still share my uncle's perplexity." + +"Your son had no shoes or slippers on when you saw him?" + +"He had nothing on save only his trousers and shirt." + +"Thank you. We have certainly been favoured with extraordinary +luck during this inquiry, and it will be entirely our own fault +if we do not succeed in clearing the matter up. With your +permission, Mr. Holder, I shall now continue my investigations +outside." + +He went alone, at his own request, for he explained that any +unnecessary footmarks might make his task more difficult. For an +hour or more he was at work, returning at last with his feet +heavy with snow and his features as inscrutable as ever. + +"I think that I have seen now all that there is to see, Mr. +Holder," said he; "I can serve you best by returning to my +rooms." + +"But the gems, Mr. Holmes. Where are they?" + +"I cannot tell." + +The banker wrung his hands. "I shall never see them again!" he +cried. "And my son? You give me hopes?" + +"My opinion is in no way altered." + +"Then, for God's sake, what was this dark business which was +acted in my house last night?" + +"If you can call upon me at my Baker Street rooms to-morrow +morning between nine and ten I shall be happy to do what I can to +make it clearer. I understand that you give me carte blanche to +act for you, provided only that I get back the gems, and that you +place no limit on the sum I may draw." + +"I would give my fortune to have them back." + +"Very good. I shall look into the matter between this and then. +Good-bye; it is just possible that I may have to come over here +again before evening." + +It was obvious to me that my companion's mind was now made up +about the case, although what his conclusions were was more than +I could even dimly imagine. Several times during our homeward +journey I endeavoured to sound him upon the point, but he always +glided away to some other topic, until at last I gave it over in +despair. It was not yet three when we found ourselves in our +rooms once more. He hurried to his chamber and was down again in +a few minutes dressed as a common loafer. With his collar turned +up, his shiny, seedy coat, his red cravat, and his worn boots, he +was a perfect sample of the class. + +"I think that this should do," said he, glancing into the glass +above the fireplace. "I only wish that you could come with me, +Watson, but I fear that it won't do. I may be on the trail in +this matter, or I may be following a will-o'-the-wisp, but I +shall soon know which it is. I hope that I may be back in a few +hours." He cut a slice of beef from the joint upon the sideboard, +sandwiched it between two rounds of bread, and thrusting this +rude meal into his pocket he started off upon his expedition. + +I had just finished my tea when he returned, evidently in +excellent spirits, swinging an old elastic-sided boot in his +hand. He chucked it down into a corner and helped himself to a +cup of tea. + +"I only looked in as I passed," said he. "I am going right on." + +"Where to?" + +"Oh, to the other side of the West End. It may be some time +before I get back. Don't wait up for me in case I should be +late." + +"How are you getting on?" + +"Oh, so so. Nothing to complain of. I have been out to Streatham +since I saw you last, but I did not call at the house. It is a +very sweet little problem, and I would not have missed it for a +good deal. However, I must not sit gossiping here, but must get +these disreputable clothes off and return to my highly +respectable self." + +I could see by his manner that he had stronger reasons for +satisfaction than his words alone would imply. His eyes twinkled, +and there was even a touch of colour upon his sallow cheeks. He +hastened upstairs, and a few minutes later I heard the slam of +the hall door, which told me that he was off once more upon his +congenial hunt. + +I waited until midnight, but there was no sign of his return, so +I retired to my room. It was no uncommon thing for him to be away +for days and nights on end when he was hot upon a scent, so that +his lateness caused me no surprise. I do not know at what hour he +came in, but when I came down to breakfast in the morning there +he was with a cup of coffee in one hand and the paper in the +other, as fresh and trim as possible. + +"You will excuse my beginning without you, Watson," said he, "but +you remember that our client has rather an early appointment this +morning." + +"Why, it is after nine now," I answered. "I should not be +surprised if that were he. I thought I heard a ring." + +It was, indeed, our friend the financier. I was shocked by the +change which had come over him, for his face which was naturally +of a broad and massive mould, was now pinched and fallen in, +while his hair seemed to me at least a shade whiter. He entered +with a weariness and lethargy which was even more painful than +his violence of the morning before, and he dropped heavily into +the armchair which I pushed forward for him. + +"I do not know what I have done to be so severely tried," said +he. "Only two days ago I was a happy and prosperous man, without +a care in the world. Now I am left to a lonely and dishonoured +age. One sorrow comes close upon the heels of another. My niece, +Mary, has deserted me." + +"Deserted you?" + +"Yes. Her bed this morning had not been slept in, her room was +empty, and a note for me lay upon the hall table. I had said to +her last night, in sorrow and not in anger, that if she had +married my boy all might have been well with him. Perhaps it was +thoughtless of me to say so. It is to that remark that she refers +in this note: + +"'MY DEAREST UNCLE:--I feel that I have brought trouble upon you, +and that if I had acted differently this terrible misfortune +might never have occurred. I cannot, with this thought in my +mind, ever again be happy under your roof, and I feel that I must +leave you forever. Do not worry about my future, for that is +provided for; and, above all, do not search for me, for it will +be fruitless labour and an ill-service to me. In life or in +death, I am ever your loving,--MARY.' + +"What could she mean by that note, Mr. Holmes? Do you think it +points to suicide?" + +"No, no, nothing of the kind. It is perhaps the best possible +solution. I trust, Mr. Holder, that you are nearing the end of +your troubles." + +"Ha! You say so! You have heard something, Mr. Holmes; you have +learned something! Where are the gems?" + +"You would not think 1000 pounds apiece an excessive sum for +them?" + +"I would pay ten." + +"That would be unnecessary. Three thousand will cover the matter. +And there is a little reward, I fancy. Have you your check-book? +Here is a pen. Better make it out for 4000 pounds." + +With a dazed face the banker made out the required check. Holmes +walked over to his desk, took out a little triangular piece of +gold with three gems in it, and threw it down upon the table. + +With a shriek of joy our client clutched it up. + +"You have it!" he gasped. "I am saved! I am saved!" + +The reaction of joy was as passionate as his grief had been, and +he hugged his recovered gems to his bosom. + +"There is one other thing you owe, Mr. Holder," said Sherlock +Holmes rather sternly. + +"Owe!" He caught up a pen. "Name the sum, and I will pay it." + +"No, the debt is not to me. You owe a very humble apology to that +noble lad, your son, who has carried himself in this matter as I +should be proud to see my own son do, should I ever chance to +have one." + +"Then it was not Arthur who took them?" + +"I told you yesterday, and I repeat to-day, that it was not." + +"You are sure of it! Then let us hurry to him at once to let him +know that the truth is known." + +"He knows it already. When I had cleared it all up I had an +interview with him, and finding that he would not tell me the +story, I told it to him, on which he had to confess that I was +right and to add the very few details which were not yet quite +clear to me. Your news of this morning, however, may open his +lips." + +"For heaven's sake, tell me, then, what is this extraordinary +mystery!" + +"I will do so, and I will show you the steps by which I reached +it. And let me say to you, first, that which it is hardest for me +to say and for you to hear: there has been an understanding +between Sir George Burnwell and your niece Mary. They have now +fled together." + +"My Mary? Impossible!" + +"It is unfortunately more than possible; it is certain. Neither +you nor your son knew the true character of this man when you +admitted him into your family circle. He is one of the most +dangerous men in England--a ruined gambler, an absolutely +desperate villain, a man without heart or conscience. Your niece +knew nothing of such men. When he breathed his vows to her, as he +had done to a hundred before her, she flattered herself that she +alone had touched his heart. The devil knows best what he said, +but at least she became his tool and was in the habit of seeing +him nearly every evening." + +"I cannot, and I will not, believe it!" cried the banker with an +ashen face. + +"I will tell you, then, what occurred in your house last night. +Your niece, when you had, as she thought, gone to your room, +slipped down and talked to her lover through the window which +leads into the stable lane. His footmarks had pressed right +through the snow, so long had he stood there. She told him of the +coronet. His wicked lust for gold kindled at the news, and he +bent her to his will. I have no doubt that she loved you, but +there are women in whom the love of a lover extinguishes all +other loves, and I think that she must have been one. She had +hardly listened to his instructions when she saw you coming +downstairs, on which she closed the window rapidly and told you +about one of the servants' escapade with her wooden-legged lover, +which was all perfectly true. + +"Your boy, Arthur, went to bed after his interview with you but +he slept badly on account of his uneasiness about his club debts. +In the middle of the night he heard a soft tread pass his door, +so he rose and, looking out, was surprised to see his cousin +walking very stealthily along the passage until she disappeared +into your dressing-room. Petrified with astonishment, the lad +slipped on some clothes and waited there in the dark to see what +would come of this strange affair. Presently she emerged from the +room again, and in the light of the passage-lamp your son saw +that she carried the precious coronet in her hands. She passed +down the stairs, and he, thrilling with horror, ran along and +slipped behind the curtain near your door, whence he could see +what passed in the hall beneath. He saw her stealthily open the +window, hand out the coronet to someone in the gloom, and then +closing it once more hurry back to her room, passing quite close +to where he stood hid behind the curtain. + +"As long as she was on the scene he could not take any action +without a horrible exposure of the woman whom he loved. But the +instant that she was gone he realised how crushing a misfortune +this would be for you, and how all-important it was to set it +right. He rushed down, just as he was, in his bare feet, opened +the window, sprang out into the snow, and ran down the lane, +where he could see a dark figure in the moonlight. Sir George +Burnwell tried to get away, but Arthur caught him, and there was +a struggle between them, your lad tugging at one side of the +coronet, and his opponent at the other. In the scuffle, your son +struck Sir George and cut him over the eye. Then something +suddenly snapped, and your son, finding that he had the coronet +in his hands, rushed back, closed the window, ascended to your +room, and had just observed that the coronet had been twisted in +the struggle and was endeavouring to straighten it when you +appeared upon the scene." + +"Is it possible?" gasped the banker. + +"You then roused his anger by calling him names at a moment when +he felt that he had deserved your warmest thanks. He could not +explain the true state of affairs without betraying one who +certainly deserved little enough consideration at his hands. He +took the more chivalrous view, however, and preserved her +secret." + +"And that was why she shrieked and fainted when she saw the +coronet," cried Mr. Holder. "Oh, my God! what a blind fool I have +been! And his asking to be allowed to go out for five minutes! +The dear fellow wanted to see if the missing piece were at the +scene of the struggle. How cruelly I have misjudged him!" + +"When I arrived at the house," continued Holmes, "I at once went +very carefully round it to observe if there were any traces in +the snow which might help me. I knew that none had fallen since +the evening before, and also that there had been a strong frost +to preserve impressions. I passed along the tradesmen's path, but +found it all trampled down and indistinguishable. Just beyond it, +however, at the far side of the kitchen door, a woman had stood +and talked with a man, whose round impressions on one side showed +that he had a wooden leg. I could even tell that they had been +disturbed, for the woman had run back swiftly to the door, as was +shown by the deep toe and light heel marks, while Wooden-leg had +waited a little, and then had gone away. I thought at the time +that this might be the maid and her sweetheart, of whom you had +already spoken to me, and inquiry showed it was so. I passed +round the garden without seeing anything more than random tracks, +which I took to be the police; but when I got into the stable +lane a very long and complex story was written in the snow in +front of me. + +"There was a double line of tracks of a booted man, and a second +double line which I saw with delight belonged to a man with naked +feet. I was at once convinced from what you had told me that the +latter was your son. The first had walked both ways, but the +other had run swiftly, and as his tread was marked in places over +the depression of the boot, it was obvious that he had passed +after the other. I followed them up and found they led to the +hall window, where Boots had worn all the snow away while +waiting. Then I walked to the other end, which was a hundred +yards or more down the lane. I saw where Boots had faced round, +where the snow was cut up as though there had been a struggle, +and, finally, where a few drops of blood had fallen, to show me +that I was not mistaken. Boots had then run down the lane, and +another little smudge of blood showed that it was he who had been +hurt. When he came to the highroad at the other end, I found that +the pavement had been cleared, so there was an end to that clue. + +"On entering the house, however, I examined, as you remember, the +sill and framework of the hall window with my lens, and I could +at once see that someone had passed out. I could distinguish the +outline of an instep where the wet foot had been placed in coming +in. I was then beginning to be able to form an opinion as to what +had occurred. A man had waited outside the window; someone had +brought the gems; the deed had been overseen by your son; he had +pursued the thief; had struggled with him; they had each tugged +at the coronet, their united strength causing injuries which +neither alone could have effected. He had returned with the +prize, but had left a fragment in the grasp of his opponent. So +far I was clear. The question now was, who was the man and who +was it brought him the coronet? + +"It is an old maxim of mine that when you have excluded the +impossible, whatever remains, however improbable, must be the +truth. Now, I knew that it was not you who had brought it down, +so there only remained your niece and the maids. But if it were +the maids, why should your son allow himself to be accused in +their place? There could be no possible reason. As he loved his +cousin, however, there was an excellent explanation why he should +retain her secret--the more so as the secret was a disgraceful +one. When I remembered that you had seen her at that window, and +how she had fainted on seeing the coronet again, my conjecture +became a certainty. + +"And who could it be who was her confederate? A lover evidently, +for who else could outweigh the love and gratitude which she must +feel to you? I knew that you went out little, and that your +circle of friends was a very limited one. But among them was Sir +George Burnwell. I had heard of him before as being a man of evil +reputation among women. It must have been he who wore those boots +and retained the missing gems. Even though he knew that Arthur +had discovered him, he might still flatter himself that he was +safe, for the lad could not say a word without compromising his +own family. + +"Well, your own good sense will suggest what measures I took +next. I went in the shape of a loafer to Sir George's house, +managed to pick up an acquaintance with his valet, learned that +his master had cut his head the night before, and, finally, at +the expense of six shillings, made all sure by buying a pair of +his cast-off shoes. With these I journeyed down to Streatham and +saw that they exactly fitted the tracks." + +"I saw an ill-dressed vagabond in the lane yesterday evening," +said Mr. Holder. + +"Precisely. It was I. I found that I had my man, so I came home +and changed my clothes. It was a delicate part which I had to +play then, for I saw that a prosecution must be avoided to avert +scandal, and I knew that so astute a villain would see that our +hands were tied in the matter. I went and saw him. At first, of +course, he denied everything. But when I gave him every +particular that had occurred, he tried to bluster and took down a +life-preserver from the wall. I knew my man, however, and I +clapped a pistol to his head before he could strike. Then he +became a little more reasonable. I told him that we would give +him a price for the stones he held--1000 pounds apiece. That +brought out the first signs of grief that he had shown. 'Why, +dash it all!' said he, 'I've let them go at six hundred for the +three!' I soon managed to get the address of the receiver who had +them, on promising him that there would be no prosecution. Off I +set to him, and after much chaffering I got our stones at 1000 +pounds apiece. Then I looked in upon your son, told him that all +was right, and eventually got to my bed about two o'clock, after +what I may call a really hard day's work." + +"A day which has saved England from a great public scandal," said +the banker, rising. "Sir, I cannot find words to thank you, but +you shall not find me ungrateful for what you have done. Your +skill has indeed exceeded all that I have heard of it. And now I +must fly to my dear boy to apologise to him for the wrong which I +have done him. As to what you tell me of poor Mary, it goes to my +very heart. Not even your skill can inform me where she is now." + +"I think that we may safely say," returned Holmes, "that she is +wherever Sir George Burnwell is. It is equally certain, too, that +whatever her sins are, they will soon receive a more than +sufficient punishment." + + + +XII. THE ADVENTURE OF THE COPPER BEECHES + +"To the man who loves art for its own sake," remarked Sherlock +Holmes, tossing aside the advertisement sheet of the Daily +Telegraph, "it is frequently in its least important and lowliest +manifestations that the keenest pleasure is to be derived. It is +pleasant to me to observe, Watson, that you have so far grasped +this truth that in these little records of our cases which you +have been good enough to draw up, and, I am bound to say, +occasionally to embellish, you have given prominence not so much +to the many causes clbres and sensational trials in which I +have figured but rather to those incidents which may have been +trivial in themselves, but which have given room for those +faculties of deduction and of logical synthesis which I have made +my special province." + +"And yet," said I, smiling, "I cannot quite hold myself absolved +from the charge of sensationalism which has been urged against my +records." + +"You have erred, perhaps," he observed, taking up a glowing +cinder with the tongs and lighting with it the long cherry-wood +pipe which was wont to replace his clay when he was in a +disputatious rather than a meditative mood--"you have erred +perhaps in attempting to put colour and life into each of your +statements instead of confining yourself to the task of placing +upon record that severe reasoning from cause to effect which is +really the only notable feature about the thing." + +"It seems to me that I have done you full justice in the matter," +I remarked with some coldness, for I was repelled by the egotism +which I had more than once observed to be a strong factor in my +friend's singular character. + +"No, it is not selfishness or conceit," said he, answering, as +was his wont, my thoughts rather than my words. "If I claim full +justice for my art, it is because it is an impersonal thing--a +thing beyond myself. Crime is common. Logic is rare. Therefore it +is upon the logic rather than upon the crime that you should +dwell. You have degraded what should have been a course of +lectures into a series of tales." + +It was a cold morning of the early spring, and we sat after +breakfast on either side of a cheery fire in the old room at +Baker Street. A thick fog rolled down between the lines of +dun-coloured houses, and the opposing windows loomed like dark, +shapeless blurs through the heavy yellow wreaths. Our gas was lit +and shone on the white cloth and glimmer of china and metal, for +the table had not been cleared yet. Sherlock Holmes had been +silent all the morning, dipping continuously into the +advertisement columns of a succession of papers until at last, +having apparently given up his search, he had emerged in no very +sweet temper to lecture me upon my literary shortcomings. + +"At the same time," he remarked after a pause, during which he +had sat puffing at his long pipe and gazing down into the fire, +"you can hardly be open to a charge of sensationalism, for out of +these cases which you have been so kind as to interest yourself +in, a fair proportion do not treat of crime, in its legal sense, +at all. The small matter in which I endeavoured to help the King +of Bohemia, the singular experience of Miss Mary Sutherland, the +problem connected with the man with the twisted lip, and the +incident of the noble bachelor, were all matters which are +outside the pale of the law. But in avoiding the sensational, I +fear that you may have bordered on the trivial." + +"The end may have been so," I answered, "but the methods I hold +to have been novel and of interest." + +"Pshaw, my dear fellow, what do the public, the great unobservant +public, who could hardly tell a weaver by his tooth or a +compositor by his left thumb, care about the finer shades of +analysis and deduction! But, indeed, if you are trivial, I cannot +blame you, for the days of the great cases are past. Man, or at +least criminal man, has lost all enterprise and originality. As +to my own little practice, it seems to be degenerating into an +agency for recovering lost lead pencils and giving advice to +young ladies from boarding-schools. I think that I have touched +bottom at last, however. This note I had this morning marks my +zero-point, I fancy. Read it!" He tossed a crumpled letter across +to me. + +It was dated from Montague Place upon the preceding evening, and +ran thus: + +"DEAR MR. HOLMES:--I am very anxious to consult you as to whether +I should or should not accept a situation which has been offered +to me as governess. I shall call at half-past ten to-morrow if I +do not inconvenience you. Yours faithfully, + "VIOLET HUNTER." + +"Do you know the young lady?" I asked. + +"Not I." + +"It is half-past ten now." + +"Yes, and I have no doubt that is her ring." + +"It may turn out to be of more interest than you think. You +remember that the affair of the blue carbuncle, which appeared to +be a mere whim at first, developed into a serious investigation. +It may be so in this case, also." + +"Well, let us hope so. But our doubts will very soon be solved, +for here, unless I am much mistaken, is the person in question." + +As he spoke the door opened and a young lady entered the room. +She was plainly but neatly dressed, with a bright, quick face, +freckled like a plover's egg, and with the brisk manner of a +woman who has had her own way to make in the world. + +"You will excuse my troubling you, I am sure," said she, as my +companion rose to greet her, "but I have had a very strange +experience, and as I have no parents or relations of any sort +from whom I could ask advice, I thought that perhaps you would be +kind enough to tell me what I should do." + +"Pray take a seat, Miss Hunter. I shall be happy to do anything +that I can to serve you." + +I could see that Holmes was favourably impressed by the manner +and speech of his new client. He looked her over in his searching +fashion, and then composed himself, with his lids drooping and +his finger-tips together, to listen to her story. + +"I have been a governess for five years," said she, "in the +family of Colonel Spence Munro, but two months ago the colonel +received an appointment at Halifax, in Nova Scotia, and took his +children over to America with him, so that I found myself without +a situation. I advertised, and I answered advertisements, but +without success. At last the little money which I had saved began +to run short, and I was at my wit's end as to what I should do. + +"There is a well-known agency for governesses in the West End +called Westaway's, and there I used to call about once a week in +order to see whether anything had turned up which might suit me. +Westaway was the name of the founder of the business, but it is +really managed by Miss Stoper. She sits in her own little office, +and the ladies who are seeking employment wait in an anteroom, +and are then shown in one by one, when she consults her ledgers +and sees whether she has anything which would suit them. + +"Well, when I called last week I was shown into the little office +as usual, but I found that Miss Stoper was not alone. A +prodigiously stout man with a very smiling face and a great heavy +chin which rolled down in fold upon fold over his throat sat at +her elbow with a pair of glasses on his nose, looking very +earnestly at the ladies who entered. As I came in he gave quite a +jump in his chair and turned quickly to Miss Stoper. + +"'That will do,' said he; 'I could not ask for anything better. +Capital! capital!' He seemed quite enthusiastic and rubbed his +hands together in the most genial fashion. He was such a +comfortable-looking man that it was quite a pleasure to look at +him. + +"'You are looking for a situation, miss?' he asked. + +"'Yes, sir.' + +"'As governess?' + +"'Yes, sir.' + +"'And what salary do you ask?' + +"'I had 4 pounds a month in my last place with Colonel Spence +Munro.' + +"'Oh, tut, tut! sweating--rank sweating!' he cried, throwing his +fat hands out into the air like a man who is in a boiling +passion. 'How could anyone offer so pitiful a sum to a lady with +such attractions and accomplishments?' + +"'My accomplishments, sir, may be less than you imagine,' said I. +'A little French, a little German, music, and drawing--' + +"'Tut, tut!' he cried. 'This is all quite beside the question. +The point is, have you or have you not the bearing and deportment +of a lady? There it is in a nutshell. If you have not, you are +not fitted for the rearing of a child who may some day play a +considerable part in the history of the country. But if you have +why, then, how could any gentleman ask you to condescend to +accept anything under the three figures? Your salary with me, +madam, would commence at 100 pounds a year.' + +"You may imagine, Mr. Holmes, that to me, destitute as I was, +such an offer seemed almost too good to be true. The gentleman, +however, seeing perhaps the look of incredulity upon my face, +opened a pocket-book and took out a note. + +"'It is also my custom,' said he, smiling in the most pleasant +fashion until his eyes were just two little shining slits amid +the white creases of his face, 'to advance to my young ladies +half their salary beforehand, so that they may meet any little +expenses of their journey and their wardrobe.' + +"It seemed to me that I had never met so fascinating and so +thoughtful a man. As I was already in debt to my tradesmen, the +advance was a great convenience, and yet there was something +unnatural about the whole transaction which made me wish to know +a little more before I quite committed myself. + +"'May I ask where you live, sir?' said I. + +"'Hampshire. Charming rural place. The Copper Beeches, five miles +on the far side of Winchester. It is the most lovely country, my +dear young lady, and the dearest old country-house.' + +"'And my duties, sir? I should be glad to know what they would +be.' + +"'One child--one dear little romper just six years old. Oh, if +you could see him killing cockroaches with a slipper! Smack! +smack! smack! Three gone before you could wink!' He leaned back +in his chair and laughed his eyes into his head again. + +"I was a little startled at the nature of the child's amusement, +but the father's laughter made me think that perhaps he was +joking. + +"'My sole duties, then,' I asked, 'are to take charge of a single +child?' + +"'No, no, not the sole, not the sole, my dear young lady,' he +cried. 'Your duty would be, as I am sure your good sense would +suggest, to obey any little commands my wife might give, provided +always that they were such commands as a lady might with +propriety obey. You see no difficulty, heh?' + +"'I should be happy to make myself useful.' + +"'Quite so. In dress now, for example. We are faddy people, you +know--faddy but kind-hearted. If you were asked to wear any dress +which we might give you, you would not object to our little whim. +Heh?' + +"'No,' said I, considerably astonished at his words. + +"'Or to sit here, or sit there, that would not be offensive to +you?' + +"'Oh, no.' + +"'Or to cut your hair quite short before you come to us?' + +"I could hardly believe my ears. As you may observe, Mr. Holmes, +my hair is somewhat luxuriant, and of a rather peculiar tint of +chestnut. It has been considered artistic. I could not dream of +sacrificing it in this offhand fashion. + +"'I am afraid that that is quite impossible,' said I. He had been +watching me eagerly out of his small eyes, and I could see a +shadow pass over his face as I spoke. + +"'I am afraid that it is quite essential,' said he. 'It is a +little fancy of my wife's, and ladies' fancies, you know, madam, +ladies' fancies must be consulted. And so you won't cut your +hair?' + +"'No, sir, I really could not,' I answered firmly. + +"'Ah, very well; then that quite settles the matter. It is a +pity, because in other respects you would really have done very +nicely. In that case, Miss Stoper, I had best inspect a few more +of your young ladies.' + +"The manageress had sat all this while busy with her papers +without a word to either of us, but she glanced at me now with so +much annoyance upon her face that I could not help suspecting +that she had lost a handsome commission through my refusal. + +"'Do you desire your name to be kept upon the books?' she asked. + +"'If you please, Miss Stoper.' + +"'Well, really, it seems rather useless, since you refuse the +most excellent offers in this fashion,' said she sharply. 'You +can hardly expect us to exert ourselves to find another such +opening for you. Good-day to you, Miss Hunter.' She struck a gong +upon the table, and I was shown out by the page. + +"Well, Mr. Holmes, when I got back to my lodgings and found +little enough in the cupboard, and two or three bills upon the +table, I began to ask myself whether I had not done a very +foolish thing. After all, if these people had strange fads and +expected obedience on the most extraordinary matters, they were +at least ready to pay for their eccentricity. Very few +governesses in England are getting 100 pounds a year. Besides, +what use was my hair to me? Many people are improved by wearing +it short and perhaps I should be among the number. Next day I was +inclined to think that I had made a mistake, and by the day after +I was sure of it. I had almost overcome my pride so far as to go +back to the agency and inquire whether the place was still open +when I received this letter from the gentleman himself. I have it +here and I will read it to you: + + "'The Copper Beeches, near Winchester. +"'DEAR MISS HUNTER:--Miss Stoper has very kindly given me your +address, and I write from here to ask you whether you have +reconsidered your decision. My wife is very anxious that you +should come, for she has been much attracted by my description of +you. We are willing to give 30 pounds a quarter, or 120 pounds a +year, so as to recompense you for any little inconvenience which +our fads may cause you. They are not very exacting, after all. My +wife is fond of a particular shade of electric blue and would +like you to wear such a dress indoors in the morning. You need +not, however, go to the expense of purchasing one, as we have one +belonging to my dear daughter Alice (now in Philadelphia), which +would, I should think, fit you very well. Then, as to sitting +here or there, or amusing yourself in any manner indicated, that +need cause you no inconvenience. As regards your hair, it is no +doubt a pity, especially as I could not help remarking its beauty +during our short interview, but I am afraid that I must remain +firm upon this point, and I only hope that the increased salary +may recompense you for the loss. Your duties, as far as the child +is concerned, are very light. Now do try to come, and I shall +meet you with the dog-cart at Winchester. Let me know your train. +Yours faithfully, JEPHRO RUCASTLE.' + +"That is the letter which I have just received, Mr. Holmes, and +my mind is made up that I will accept it. I thought, however, +that before taking the final step I should like to submit the +whole matter to your consideration." + +"Well, Miss Hunter, if your mind is made up, that settles the +question," said Holmes, smiling. + +"But you would not advise me to refuse?" + +"I confess that it is not the situation which I should like to +see a sister of mine apply for." + +"What is the meaning of it all, Mr. Holmes?" + +"Ah, I have no data. I cannot tell. Perhaps you have yourself +formed some opinion?" + +"Well, there seems to me to be only one possible solution. Mr. +Rucastle seemed to be a very kind, good-natured man. Is it not +possible that his wife is a lunatic, that he desires to keep the +matter quiet for fear she should be taken to an asylum, and that +he humours her fancies in every way in order to prevent an +outbreak?" + +"That is a possible solution--in fact, as matters stand, it is +the most probable one. But in any case it does not seem to be a +nice household for a young lady." + +"But the money, Mr. Holmes, the money!" + +"Well, yes, of course the pay is good--too good. That is what +makes me uneasy. Why should they give you 120 pounds a year, when +they could have their pick for 40 pounds? There must be some +strong reason behind." + +"I thought that if I told you the circumstances you would +understand afterwards if I wanted your help. I should feel so +much stronger if I felt that you were at the back of me." + +"Oh, you may carry that feeling away with you. I assure you that +your little problem promises to be the most interesting which has +come my way for some months. There is something distinctly novel +about some of the features. If you should find yourself in doubt +or in danger--" + +"Danger! What danger do you foresee?" + +Holmes shook his head gravely. "It would cease to be a danger if +we could define it," said he. "But at any time, day or night, a +telegram would bring me down to your help." + +"That is enough." She rose briskly from her chair with the +anxiety all swept from her face. "I shall go down to Hampshire +quite easy in my mind now. I shall write to Mr. Rucastle at once, +sacrifice my poor hair to-night, and start for Winchester +to-morrow." With a few grateful words to Holmes she bade us both +good-night and bustled off upon her way. + +"At least," said I as we heard her quick, firm steps descending +the stairs, "she seems to be a young lady who is very well able +to take care of herself." + +"And she would need to be," said Holmes gravely. "I am much +mistaken if we do not hear from her before many days are past." + +It was not very long before my friend's prediction was fulfilled. +A fortnight went by, during which I frequently found my thoughts +turning in her direction and wondering what strange side-alley of +human experience this lonely woman had strayed into. The unusual +salary, the curious conditions, the light duties, all pointed to +something abnormal, though whether a fad or a plot, or whether +the man were a philanthropist or a villain, it was quite beyond +my powers to determine. As to Holmes, I observed that he sat +frequently for half an hour on end, with knitted brows and an +abstracted air, but he swept the matter away with a wave of his +hand when I mentioned it. "Data! data! data!" he cried +impatiently. "I can't make bricks without clay." And yet he would +always wind up by muttering that no sister of his should ever +have accepted such a situation. + +The telegram which we eventually received came late one night +just as I was thinking of turning in and Holmes was settling down +to one of those all-night chemical researches which he frequently +indulged in, when I would leave him stooping over a retort and a +test-tube at night and find him in the same position when I came +down to breakfast in the morning. He opened the yellow envelope, +and then, glancing at the message, threw it across to me. + +"Just look up the trains in Bradshaw," said he, and turned back +to his chemical studies. + +The summons was a brief and urgent one. + +"Please be at the Black Swan Hotel at Winchester at midday +to-morrow," it said. "Do come! I am at my wit's end. HUNTER." + +"Will you come with me?" asked Holmes, glancing up. + +"I should wish to." + +"Just look it up, then." + +"There is a train at half-past nine," said I, glancing over my +Bradshaw. "It is due at Winchester at 11:30." + +"That will do very nicely. Then perhaps I had better postpone my +analysis of the acetones, as we may need to be at our best in the +morning." + +By eleven o'clock the next day we were well upon our way to the +old English capital. Holmes had been buried in the morning papers +all the way down, but after we had passed the Hampshire border he +threw them down and began to admire the scenery. It was an ideal +spring day, a light blue sky, flecked with little fleecy white +clouds drifting across from west to east. The sun was shining +very brightly, and yet there was an exhilarating nip in the air, +which set an edge to a man's energy. All over the countryside, +away to the rolling hills around Aldershot, the little red and +grey roofs of the farm-steadings peeped out from amid the light +green of the new foliage. + +"Are they not fresh and beautiful?" I cried with all the +enthusiasm of a man fresh from the fogs of Baker Street. + +But Holmes shook his head gravely. + +"Do you know, Watson," said he, "that it is one of the curses of +a mind with a turn like mine that I must look at everything with +reference to my own special subject. You look at these scattered +houses, and you are impressed by their beauty. I look at them, +and the only thought which comes to me is a feeling of their +isolation and of the impunity with which crime may be committed +there." + +"Good heavens!" I cried. "Who would associate crime with these +dear old homesteads?" + +"They always fill me with a certain horror. It is my belief, +Watson, founded upon my experience, that the lowest and vilest +alleys in London do not present a more dreadful record of sin +than does the smiling and beautiful countryside." + +"You horrify me!" + +"But the reason is very obvious. The pressure of public opinion +can do in the town what the law cannot accomplish. There is no +lane so vile that the scream of a tortured child, or the thud of +a drunkard's blow, does not beget sympathy and indignation among +the neighbours, and then the whole machinery of justice is ever +so close that a word of complaint can set it going, and there is +but a step between the crime and the dock. But look at these +lonely houses, each in its own fields, filled for the most part +with poor ignorant folk who know little of the law. Think of the +deeds of hellish cruelty, the hidden wickedness which may go on, +year in, year out, in such places, and none the wiser. Had this +lady who appeals to us for help gone to live in Winchester, I +should never have had a fear for her. It is the five miles of +country which makes the danger. Still, it is clear that she is +not personally threatened." + +"No. If she can come to Winchester to meet us she can get away." + +"Quite so. She has her freedom." + +"What CAN be the matter, then? Can you suggest no explanation?" + +"I have devised seven separate explanations, each of which would +cover the facts as far as we know them. But which of these is +correct can only be determined by the fresh information which we +shall no doubt find waiting for us. Well, there is the tower of +the cathedral, and we shall soon learn all that Miss Hunter has +to tell." + +The Black Swan is an inn of repute in the High Street, at no +distance from the station, and there we found the young lady +waiting for us. She had engaged a sitting-room, and our lunch +awaited us upon the table. + +"I am so delighted that you have come," she said earnestly. "It +is so very kind of you both; but indeed I do not know what I +should do. Your advice will be altogether invaluable to me." + +"Pray tell us what has happened to you." + +"I will do so, and I must be quick, for I have promised Mr. +Rucastle to be back before three. I got his leave to come into +town this morning, though he little knew for what purpose." + +"Let us have everything in its due order." Holmes thrust his long +thin legs out towards the fire and composed himself to listen. + +"In the first place, I may say that I have met, on the whole, +with no actual ill-treatment from Mr. and Mrs. Rucastle. It is +only fair to them to say that. But I cannot understand them, and +I am not easy in my mind about them." + +"What can you not understand?" + +"Their reasons for their conduct. But you shall have it all just +as it occurred. When I came down, Mr. Rucastle met me here and +drove me in his dog-cart to the Copper Beeches. It is, as he +said, beautifully situated, but it is not beautiful in itself, +for it is a large square block of a house, whitewashed, but all +stained and streaked with damp and bad weather. There are grounds +round it, woods on three sides, and on the fourth a field which +slopes down to the Southampton highroad, which curves past about +a hundred yards from the front door. This ground in front belongs +to the house, but the woods all round are part of Lord +Southerton's preserves. A clump of copper beeches immediately in +front of the hall door has given its name to the place. + +"I was driven over by my employer, who was as amiable as ever, +and was introduced by him that evening to his wife and the child. +There was no truth, Mr. Holmes, in the conjecture which seemed to +us to be probable in your rooms at Baker Street. Mrs. Rucastle is +not mad. I found her to be a silent, pale-faced woman, much +younger than her husband, not more than thirty, I should think, +while he can hardly be less than forty-five. From their +conversation I have gathered that they have been married about +seven years, that he was a widower, and that his only child by +the first wife was the daughter who has gone to Philadelphia. Mr. +Rucastle told me in private that the reason why she had left them +was that she had an unreasoning aversion to her stepmother. As +the daughter could not have been less than twenty, I can quite +imagine that her position must have been uncomfortable with her +father's young wife. + +"Mrs. Rucastle seemed to me to be colourless in mind as well as +in feature. She impressed me neither favourably nor the reverse. +She was a nonentity. It was easy to see that she was passionately +devoted both to her husband and to her little son. Her light grey +eyes wandered continually from one to the other, noting every +little want and forestalling it if possible. He was kind to her +also in his bluff, boisterous fashion, and on the whole they +seemed to be a happy couple. And yet she had some secret sorrow, +this woman. She would often be lost in deep thought, with the +saddest look upon her face. More than once I have surprised her +in tears. I have thought sometimes that it was the disposition of +her child which weighed upon her mind, for I have never met so +utterly spoiled and so ill-natured a little creature. He is small +for his age, with a head which is quite disproportionately large. +His whole life appears to be spent in an alternation between +savage fits of passion and gloomy intervals of sulking. Giving +pain to any creature weaker than himself seems to be his one idea +of amusement, and he shows quite remarkable talent in planning +the capture of mice, little birds, and insects. But I would +rather not talk about the creature, Mr. Holmes, and, indeed, he +has little to do with my story." + +"I am glad of all details," remarked my friend, "whether they +seem to you to be relevant or not." + +"I shall try not to miss anything of importance. The one +unpleasant thing about the house, which struck me at once, was +the appearance and conduct of the servants. There are only two, a +man and his wife. Toller, for that is his name, is a rough, +uncouth man, with grizzled hair and whiskers, and a perpetual +smell of drink. Twice since I have been with them he has been +quite drunk, and yet Mr. Rucastle seemed to take no notice of it. +His wife is a very tall and strong woman with a sour face, as +silent as Mrs. Rucastle and much less amiable. They are a most +unpleasant couple, but fortunately I spend most of my time in the +nursery and my own room, which are next to each other in one +corner of the building. + +"For two days after my arrival at the Copper Beeches my life was +very quiet; on the third, Mrs. Rucastle came down just after +breakfast and whispered something to her husband. + +"'Oh, yes,' said he, turning to me, 'we are very much obliged to +you, Miss Hunter, for falling in with our whims so far as to cut +your hair. I assure you that it has not detracted in the tiniest +iota from your appearance. We shall now see how the electric-blue +dress will become you. You will find it laid out upon the bed in +your room, and if you would be so good as to put it on we should +both be extremely obliged.' + +"The dress which I found waiting for me was of a peculiar shade +of blue. It was of excellent material, a sort of beige, but it +bore unmistakable signs of having been worn before. It could not +have been a better fit if I had been measured for it. Both Mr. +and Mrs. Rucastle expressed a delight at the look of it, which +seemed quite exaggerated in its vehemence. They were waiting for +me in the drawing-room, which is a very large room, stretching +along the entire front of the house, with three long windows +reaching down to the floor. A chair had been placed close to the +central window, with its back turned towards it. In this I was +asked to sit, and then Mr. Rucastle, walking up and down on the +other side of the room, began to tell me a series of the funniest +stories that I have ever listened to. You cannot imagine how +comical he was, and I laughed until I was quite weary. Mrs. +Rucastle, however, who has evidently no sense of humour, never so +much as smiled, but sat with her hands in her lap, and a sad, +anxious look upon her face. After an hour or so, Mr. Rucastle +suddenly remarked that it was time to commence the duties of the +day, and that I might change my dress and go to little Edward in +the nursery. + +"Two days later this same performance was gone through under +exactly similar circumstances. Again I changed my dress, again I +sat in the window, and again I laughed very heartily at the funny +stories of which my employer had an immense rpertoire, and which +he told inimitably. Then he handed me a yellow-backed novel, and +moving my chair a little sideways, that my own shadow might not +fall upon the page, he begged me to read aloud to him. I read for +about ten minutes, beginning in the heart of a chapter, and then +suddenly, in the middle of a sentence, he ordered me to cease and +to change my dress. + +"You can easily imagine, Mr. Holmes, how curious I became as to +what the meaning of this extraordinary performance could possibly +be. They were always very careful, I observed, to turn my face +away from the window, so that I became consumed with the desire +to see what was going on behind my back. At first it seemed to be +impossible, but I soon devised a means. My hand-mirror had been +broken, so a happy thought seized me, and I concealed a piece of +the glass in my handkerchief. On the next occasion, in the midst +of my laughter, I put my handkerchief up to my eyes, and was able +with a little management to see all that there was behind me. I +confess that I was disappointed. There was nothing. At least that +was my first impression. At the second glance, however, I +perceived that there was a man standing in the Southampton Road, +a small bearded man in a grey suit, who seemed to be looking in +my direction. The road is an important highway, and there are +usually people there. This man, however, was leaning against the +railings which bordered our field and was looking earnestly up. I +lowered my handkerchief and glanced at Mrs. Rucastle to find her +eyes fixed upon me with a most searching gaze. She said nothing, +but I am convinced that she had divined that I had a mirror in my +hand and had seen what was behind me. She rose at once. + +"'Jephro,' said she, 'there is an impertinent fellow upon the +road there who stares up at Miss Hunter.' + +"'No friend of yours, Miss Hunter?' he asked. + +"'No, I know no one in these parts.' + +"'Dear me! How very impertinent! Kindly turn round and motion to +him to go away.' + +"'Surely it would be better to take no notice.' + +"'No, no, we should have him loitering here always. Kindly turn +round and wave him away like that.' + +"I did as I was told, and at the same instant Mrs. Rucastle drew +down the blind. That was a week ago, and from that time I have +not sat again in the window, nor have I worn the blue dress, nor +seen the man in the road." + +"Pray continue," said Holmes. "Your narrative promises to be a +most interesting one." + +"You will find it rather disconnected, I fear, and there may +prove to be little relation between the different incidents of +which I speak. On the very first day that I was at the Copper +Beeches, Mr. Rucastle took me to a small outhouse which stands +near the kitchen door. As we approached it I heard the sharp +rattling of a chain, and the sound as of a large animal moving +about. + +"'Look in here!' said Mr. Rucastle, showing me a slit between two +planks. 'Is he not a beauty?' + +"I looked through and was conscious of two glowing eyes, and of a +vague figure huddled up in the darkness. + +"'Don't be frightened,' said my employer, laughing at the start +which I had given. 'It's only Carlo, my mastiff. I call him mine, +but really old Toller, my groom, is the only man who can do +anything with him. We feed him once a day, and not too much then, +so that he is always as keen as mustard. Toller lets him loose +every night, and God help the trespasser whom he lays his fangs +upon. For goodness' sake don't you ever on any pretext set your +foot over the threshold at night, for it's as much as your life +is worth.' + +"The warning was no idle one, for two nights later I happened to +look out of my bedroom window about two o'clock in the morning. +It was a beautiful moonlight night, and the lawn in front of the +house was silvered over and almost as bright as day. I was +standing, rapt in the peaceful beauty of the scene, when I was +aware that something was moving under the shadow of the copper +beeches. As it emerged into the moonshine I saw what it was. It +was a giant dog, as large as a calf, tawny tinted, with hanging +jowl, black muzzle, and huge projecting bones. It walked slowly +across the lawn and vanished into the shadow upon the other side. +That dreadful sentinel sent a chill to my heart which I do not +think that any burglar could have done. + +"And now I have a very strange experience to tell you. I had, as +you know, cut off my hair in London, and I had placed it in a +great coil at the bottom of my trunk. One evening, after the +child was in bed, I began to amuse myself by examining the +furniture of my room and by rearranging my own little things. +There was an old chest of drawers in the room, the two upper ones +empty and open, the lower one locked. I had filled the first two +with my linen, and as I had still much to pack away I was +naturally annoyed at not having the use of the third drawer. It +struck me that it might have been fastened by a mere oversight, +so I took out my bunch of keys and tried to open it. The very +first key fitted to perfection, and I drew the drawer open. There +was only one thing in it, but I am sure that you would never +guess what it was. It was my coil of hair. + +"I took it up and examined it. It was of the same peculiar tint, +and the same thickness. But then the impossibility of the thing +obtruded itself upon me. How could my hair have been locked in +the drawer? With trembling hands I undid my trunk, turned out the +contents, and drew from the bottom my own hair. I laid the two +tresses together, and I assure you that they were identical. Was +it not extraordinary? Puzzle as I would, I could make nothing at +all of what it meant. I returned the strange hair to the drawer, +and I said nothing of the matter to the Rucastles as I felt that +I had put myself in the wrong by opening a drawer which they had +locked. + +"I am naturally observant, as you may have remarked, Mr. Holmes, +and I soon had a pretty good plan of the whole house in my head. +There was one wing, however, which appeared not to be inhabited +at all. A door which faced that which led into the quarters of +the Tollers opened into this suite, but it was invariably locked. +One day, however, as I ascended the stair, I met Mr. Rucastle +coming out through this door, his keys in his hand, and a look on +his face which made him a very different person to the round, +jovial man to whom I was accustomed. His cheeks were red, his +brow was all crinkled with anger, and the veins stood out at his +temples with passion. He locked the door and hurried past me +without a word or a look. + +"This aroused my curiosity, so when I went out for a walk in the +grounds with my charge, I strolled round to the side from which I +could see the windows of this part of the house. There were four +of them in a row, three of which were simply dirty, while the +fourth was shuttered up. They were evidently all deserted. As I +strolled up and down, glancing at them occasionally, Mr. Rucastle +came out to me, looking as merry and jovial as ever. + +"'Ah!' said he, 'you must not think me rude if I passed you +without a word, my dear young lady. I was preoccupied with +business matters.' + +"I assured him that I was not offended. 'By the way,' said I, +'you seem to have quite a suite of spare rooms up there, and one +of them has the shutters up.' + +"He looked surprised and, as it seemed to me, a little startled +at my remark. + +"'Photography is one of my hobbies,' said he. 'I have made my +dark room up there. But, dear me! what an observant young lady we +have come upon. Who would have believed it? Who would have ever +believed it?' He spoke in a jesting tone, but there was no jest +in his eyes as he looked at me. I read suspicion there and +annoyance, but no jest. + +"Well, Mr. Holmes, from the moment that I understood that there +was something about that suite of rooms which I was not to know, +I was all on fire to go over them. It was not mere curiosity, +though I have my share of that. It was more a feeling of duty--a +feeling that some good might come from my penetrating to this +place. They talk of woman's instinct; perhaps it was woman's +instinct which gave me that feeling. At any rate, it was there, +and I was keenly on the lookout for any chance to pass the +forbidden door. + +"It was only yesterday that the chance came. I may tell you that, +besides Mr. Rucastle, both Toller and his wife find something to +do in these deserted rooms, and I once saw him carrying a large +black linen bag with him through the door. Recently he has been +drinking hard, and yesterday evening he was very drunk; and when +I came upstairs there was the key in the door. I have no doubt at +all that he had left it there. Mr. and Mrs. Rucastle were both +downstairs, and the child was with them, so that I had an +admirable opportunity. I turned the key gently in the lock, +opened the door, and slipped through. + +"There was a little passage in front of me, unpapered and +uncarpeted, which turned at a right angle at the farther end. +Round this corner were three doors in a line, the first and third +of which were open. They each led into an empty room, dusty and +cheerless, with two windows in the one and one in the other, so +thick with dirt that the evening light glimmered dimly through +them. The centre door was closed, and across the outside of it +had been fastened one of the broad bars of an iron bed, padlocked +at one end to a ring in the wall, and fastened at the other with +stout cord. The door itself was locked as well, and the key was +not there. This barricaded door corresponded clearly with the +shuttered window outside, and yet I could see by the glimmer from +beneath it that the room was not in darkness. Evidently there was +a skylight which let in light from above. As I stood in the +passage gazing at the sinister door and wondering what secret it +might veil, I suddenly heard the sound of steps within the room +and saw a shadow pass backward and forward against the little +slit of dim light which shone out from under the door. A mad, +unreasoning terror rose up in me at the sight, Mr. Holmes. My +overstrung nerves failed me suddenly, and I turned and ran--ran +as though some dreadful hand were behind me clutching at the +skirt of my dress. I rushed down the passage, through the door, +and straight into the arms of Mr. Rucastle, who was waiting +outside. + +"'So,' said he, smiling, 'it was you, then. I thought that it +must be when I saw the door open.' + +"'Oh, I am so frightened!' I panted. + +"'My dear young lady! my dear young lady!'--you cannot think how +caressing and soothing his manner was--'and what has frightened +you, my dear young lady?' + +"But his voice was just a little too coaxing. He overdid it. I +was keenly on my guard against him. + +"'I was foolish enough to go into the empty wing,' I answered. +'But it is so lonely and eerie in this dim light that I was +frightened and ran out again. Oh, it is so dreadfully still in +there!' + +"'Only that?' said he, looking at me keenly. + +"'Why, what did you think?' I asked. + +"'Why do you think that I lock this door?' + +"'I am sure that I do not know.' + +"'It is to keep people out who have no business there. Do you +see?' He was still smiling in the most amiable manner. + +"'I am sure if I had known--' + +"'Well, then, you know now. And if you ever put your foot over +that threshold again'--here in an instant the smile hardened into +a grin of rage, and he glared down at me with the face of a +demon--'I'll throw you to the mastiff.' + +"I was so terrified that I do not know what I did. I suppose that +I must have rushed past him into my room. I remember nothing +until I found myself lying on my bed trembling all over. Then I +thought of you, Mr. Holmes. I could not live there longer without +some advice. I was frightened of the house, of the man, of the +woman, of the servants, even of the child. They were all horrible +to me. If I could only bring you down all would be well. Of +course I might have fled from the house, but my curiosity was +almost as strong as my fears. My mind was soon made up. I would +send you a wire. I put on my hat and cloak, went down to the +office, which is about half a mile from the house, and then +returned, feeling very much easier. A horrible doubt came into my +mind as I approached the door lest the dog might be loose, but I +remembered that Toller had drunk himself into a state of +insensibility that evening, and I knew that he was the only one +in the household who had any influence with the savage creature, +or who would venture to set him free. I slipped in in safety and +lay awake half the night in my joy at the thought of seeing you. +I had no difficulty in getting leave to come into Winchester this +morning, but I must be back before three o'clock, for Mr. and +Mrs. Rucastle are going on a visit, and will be away all the +evening, so that I must look after the child. Now I have told you +all my adventures, Mr. Holmes, and I should be very glad if you +could tell me what it all means, and, above all, what I should +do." + +Holmes and I had listened spellbound to this extraordinary story. +My friend rose now and paced up and down the room, his hands in +his pockets, and an expression of the most profound gravity upon +his face. + +"Is Toller still drunk?" he asked. + +"Yes. I heard his wife tell Mrs. Rucastle that she could do +nothing with him." + +"That is well. And the Rucastles go out to-night?" + +"Yes." + +"Is there a cellar with a good strong lock?" + +"Yes, the wine-cellar." + +"You seem to me to have acted all through this matter like a very +brave and sensible girl, Miss Hunter. Do you think that you could +perform one more feat? I should not ask it of you if I did not +think you a quite exceptional woman." + +"I will try. What is it?" + +"We shall be at the Copper Beeches by seven o'clock, my friend +and I. The Rucastles will be gone by that time, and Toller will, +we hope, be incapable. There only remains Mrs. Toller, who might +give the alarm. If you could send her into the cellar on some +errand, and then turn the key upon her, you would facilitate +matters immensely." + +"I will do it." + +"Excellent! We shall then look thoroughly into the affair. Of +course there is only one feasible explanation. You have been +brought there to personate someone, and the real person is +imprisoned in this chamber. That is obvious. As to who this +prisoner is, I have no doubt that it is the daughter, Miss Alice +Rucastle, if I remember right, who was said to have gone to +America. You were chosen, doubtless, as resembling her in height, +figure, and the colour of your hair. Hers had been cut off, very +possibly in some illness through which she has passed, and so, of +course, yours had to be sacrificed also. By a curious chance you +came upon her tresses. The man in the road was undoubtedly some +friend of hers--possibly her fianc--and no doubt, as you wore +the girl's dress and were so like her, he was convinced from your +laughter, whenever he saw you, and afterwards from your gesture, +that Miss Rucastle was perfectly happy, and that she no longer +desired his attentions. The dog is let loose at night to prevent +him from endeavouring to communicate with her. So much is fairly +clear. The most serious point in the case is the disposition of +the child." + +"What on earth has that to do with it?" I ejaculated. + +"My dear Watson, you as a medical man are continually gaining +light as to the tendencies of a child by the study of the +parents. Don't you see that the converse is equally valid. I have +frequently gained my first real insight into the character of +parents by studying their children. This child's disposition is +abnormally cruel, merely for cruelty's sake, and whether he +derives this from his smiling father, as I should suspect, or +from his mother, it bodes evil for the poor girl who is in their +power." + +"I am sure that you are right, Mr. Holmes," cried our client. "A +thousand things come back to me which make me certain that you +have hit it. Oh, let us lose not an instant in bringing help to +this poor creature." + +"We must be circumspect, for we are dealing with a very cunning +man. We can do nothing until seven o'clock. At that hour we shall +be with you, and it will not be long before we solve the +mystery." + +We were as good as our word, for it was just seven when we +reached the Copper Beeches, having put up our trap at a wayside +public-house. The group of trees, with their dark leaves shining +like burnished metal in the light of the setting sun, were +sufficient to mark the house even had Miss Hunter not been +standing smiling on the door-step. + +"Have you managed it?" asked Holmes. + +A loud thudding noise came from somewhere downstairs. "That is +Mrs. Toller in the cellar," said she. "Her husband lies snoring +on the kitchen rug. Here are his keys, which are the duplicates +of Mr. Rucastle's." + +"You have done well indeed!" cried Holmes with enthusiasm. "Now +lead the way, and we shall soon see the end of this black +business." + +We passed up the stair, unlocked the door, followed on down a +passage, and found ourselves in front of the barricade which Miss +Hunter had described. Holmes cut the cord and removed the +transverse bar. Then he tried the various keys in the lock, but +without success. No sound came from within, and at the silence +Holmes' face clouded over. + +"I trust that we are not too late," said he. "I think, Miss +Hunter, that we had better go in without you. Now, Watson, put +your shoulder to it, and we shall see whether we cannot make our +way in." + +It was an old rickety door and gave at once before our united +strength. Together we rushed into the room. It was empty. There +was no furniture save a little pallet bed, a small table, and a +basketful of linen. The skylight above was open, and the prisoner +gone. + +"There has been some villainy here," said Holmes; "this beauty +has guessed Miss Hunter's intentions and has carried his victim +off." + +"But how?" + +"Through the skylight. We shall soon see how he managed it." He +swung himself up onto the roof. "Ah, yes," he cried, "here's the +end of a long light ladder against the eaves. That is how he did +it." + +"But it is impossible," said Miss Hunter; "the ladder was not +there when the Rucastles went away." + +"He has come back and done it. I tell you that he is a clever and +dangerous man. I should not be very much surprised if this were +he whose step I hear now upon the stair. I think, Watson, that it +would be as well for you to have your pistol ready." + +The words were hardly out of his mouth before a man appeared at +the door of the room, a very fat and burly man, with a heavy +stick in his hand. Miss Hunter screamed and shrunk against the +wall at the sight of him, but Sherlock Holmes sprang forward and +confronted him. + +"You villain!" said he, "where's your daughter?" + +The fat man cast his eyes round, and then up at the open +skylight. + +"It is for me to ask you that," he shrieked, "you thieves! Spies +and thieves! I have caught you, have I? You are in my power. I'll +serve you!" He turned and clattered down the stairs as hard as he +could go. + +"He's gone for the dog!" cried Miss Hunter. + +"I have my revolver," said I. + +"Better close the front door," cried Holmes, and we all rushed +down the stairs together. We had hardly reached the hall when we +heard the baying of a hound, and then a scream of agony, with a +horrible worrying sound which it was dreadful to listen to. An +elderly man with a red face and shaking limbs came staggering out +at a side door. + +"My God!" he cried. "Someone has loosed the dog. It's not been +fed for two days. Quick, quick, or it'll be too late!" + +Holmes and I rushed out and round the angle of the house, with +Toller hurrying behind us. There was the huge famished brute, its +black muzzle buried in Rucastle's throat, while he writhed and +screamed upon the ground. Running up, I blew its brains out, and +it fell over with its keen white teeth still meeting in the great +creases of his neck. With much labour we separated them and +carried him, living but horribly mangled, into the house. We laid +him upon the drawing-room sofa, and having dispatched the sobered +Toller to bear the news to his wife, I did what I could to +relieve his pain. We were all assembled round him when the door +opened, and a tall, gaunt woman entered the room. + +"Mrs. Toller!" cried Miss Hunter. + +"Yes, miss. Mr. Rucastle let me out when he came back before he +went up to you. Ah, miss, it is a pity you didn't let me know +what you were planning, for I would have told you that your pains +were wasted." + +"Ha!" said Holmes, looking keenly at her. "It is clear that Mrs. +Toller knows more about this matter than anyone else." + +"Yes, sir, I do, and I am ready enough to tell what I know." + +"Then, pray, sit down, and let us hear it for there are several +points on which I must confess that I am still in the dark." + +"I will soon make it clear to you," said she; "and I'd have done +so before now if I could ha' got out from the cellar. If there's +police-court business over this, you'll remember that I was the +one that stood your friend, and that I was Miss Alice's friend +too. + +"She was never happy at home, Miss Alice wasn't, from the time +that her father married again. She was slighted like and had no +say in anything, but it never really became bad for her until +after she met Mr. Fowler at a friend's house. As well as I could +learn, Miss Alice had rights of her own by will, but she was so +quiet and patient, she was, that she never said a word about them +but just left everything in Mr. Rucastle's hands. He knew he was +safe with her; but when there was a chance of a husband coming +forward, who would ask for all that the law would give him, then +her father thought it time to put a stop on it. He wanted her to +sign a paper, so that whether she married or not, he could use +her money. When she wouldn't do it, he kept on worrying her until +she got brain-fever, and for six weeks was at death's door. Then +she got better at last, all worn to a shadow, and with her +beautiful hair cut off; but that didn't make no change in her +young man, and he stuck to her as true as man could be." + +"Ah," said Holmes, "I think that what you have been good enough +to tell us makes the matter fairly clear, and that I can deduce +all that remains. Mr. Rucastle then, I presume, took to this +system of imprisonment?" + +"Yes, sir." + +"And brought Miss Hunter down from London in order to get rid of +the disagreeable persistence of Mr. Fowler." + +"That was it, sir." + +"But Mr. Fowler being a persevering man, as a good seaman should +be, blockaded the house, and having met you succeeded by certain +arguments, metallic or otherwise, in convincing you that your +interests were the same as his." + +"Mr. Fowler was a very kind-spoken, free-handed gentleman," said +Mrs. Toller serenely. + +"And in this way he managed that your good man should have no +want of drink, and that a ladder should be ready at the moment +when your master had gone out." + +"You have it, sir, just as it happened." + +"I am sure we owe you an apology, Mrs. Toller," said Holmes, "for +you have certainly cleared up everything which puzzled us. And +here comes the country surgeon and Mrs. Rucastle, so I think, +Watson, that we had best escort Miss Hunter back to Winchester, +as it seems to me that our locus standi now is rather a +questionable one." + +And thus was solved the mystery of the sinister house with the +copper beeches in front of the door. Mr. Rucastle survived, but +was always a broken man, kept alive solely through the care of +his devoted wife. They still live with their old servants, who +probably know so much of Rucastle's past life that he finds it +difficult to part from them. Mr. Fowler and Miss Rucastle were +married, by special license, in Southampton the day after their +flight, and he is now the holder of a government appointment in +the island of Mauritius. As to Miss Violet Hunter, my friend +Holmes, rather to my disappointment, manifested no further +interest in her when once she had ceased to be the centre of one +of his problems, and she is now the head of a private school at +Walsall, where I believe that she has met with considerable success. + + + + + + + + + +End of the Project Gutenberg EBook of The Adventures of Sherlock Holmes, by +Arthur Conan Doyle + +*** END OF THIS PROJECT GUTENBERG EBOOK THE ADVENTURES OF SHERLOCK HOLMES *** + +***** This file should be named 1661-8.txt or 1661-8.zip ***** +This and all associated files of various formats will be found in: + http://www.gutenberg.org/1/6/6/1661/ + +Produced by an anonymous Project Gutenberg volunteer and Jose Menendez + +Updated editions will replace the previous one--the old editions +will be renamed. + +Creating the works from public domain print editions means that no +one owns a United States copyright in these works, so the Foundation +(and you!) can copy and distribute it in the United States without +permission and without paying copyright royalties. Special rules, +set forth in the General Terms of Use part of this license, apply to +copying and distributing Project Gutenberg-tm electronic works to +protect the PROJECT GUTENBERG-tm concept and trademark. Project +Gutenberg is a registered trademark, and may not be used if you +charge for the eBooks, unless you receive specific permission. If you +do not charge anything for copies of this eBook, complying with the +rules is very easy. You may use this eBook for nearly any purpose +such as creation of derivative works, reports, performances and +research. They may be modified and printed and given away--you may do +practically ANYTHING with public domain eBooks. Redistribution is +subject to the trademark license, especially commercial +redistribution. + + + +*** START: FULL LICENSE *** + +THE FULL PROJECT GUTENBERG LICENSE +PLEASE READ THIS BEFORE YOU DISTRIBUTE OR USE THIS WORK + +To protect the Project Gutenberg-tm mission of promoting the free +distribution of electronic works, by using or distributing this work +(or any other work associated in any way with the phrase "Project +Gutenberg"), you agree to comply with all the terms of the Full Project +Gutenberg-tm License (available with this file or online at +http://gutenberg.net/license). + + +Section 1. General Terms of Use and Redistributing Project Gutenberg-tm +electronic works + +1.A. By reading or using any part of this Project Gutenberg-tm +electronic work, you indicate that you have read, understand, agree to +and accept all the terms of this license and intellectual property +(trademark/copyright) agreement. If you do not agree to abide by all +the terms of this agreement, you must cease using and return or destroy +all copies of Project Gutenberg-tm electronic works in your possession. +If you paid a fee for obtaining a copy of or access to a Project +Gutenberg-tm electronic work and you do not agree to be bound by the +terms of this agreement, you may obtain a refund from the person or +entity to whom you paid the fee as set forth in paragraph 1.E.8. + +1.B. "Project Gutenberg" is a registered trademark. It may only be +used on or associated in any way with an electronic work by people who +agree to be bound by the terms of this agreement. There are a few +things that you can do with most Project Gutenberg-tm electronic works +even without complying with the full terms of this agreement. See +paragraph 1.C below. There are a lot of things you can do with Project +Gutenberg-tm electronic works if you follow the terms of this agreement +and help preserve free future access to Project Gutenberg-tm electronic +works. See paragraph 1.E below. + +1.C. The Project Gutenberg Literary Archive Foundation ("the Foundation" +or PGLAF), owns a compilation copyright in the collection of Project +Gutenberg-tm electronic works. Nearly all the individual works in the +collection are in the public domain in the United States. If an +individual work is in the public domain in the United States and you are +located in the United States, we do not claim a right to prevent you from +copying, distributing, performing, displaying or creating derivative +works based on the work as long as all references to Project Gutenberg +are removed. Of course, we hope that you will support the Project +Gutenberg-tm mission of promoting free access to electronic works by +freely sharing Project Gutenberg-tm works in compliance with the terms of +this agreement for keeping the Project Gutenberg-tm name associated with +the work. You can easily comply with the terms of this agreement by +keeping this work in the same format with its attached full Project +Gutenberg-tm License when you share it without charge with others. + +1.D. The copyright laws of the place where you are located also govern +what you can do with this work. Copyright laws in most countries are in +a constant state of change. If you are outside the United States, check +the laws of your country in addition to the terms of this agreement +before downloading, copying, displaying, performing, distributing or +creating derivative works based on this work or any other Project +Gutenberg-tm work. The Foundation makes no representations concerning +the copyright status of any work in any country outside the United +States. + +1.E. Unless you have removed all references to Project Gutenberg: + +1.E.1. The following sentence, with active links to, or other immediate +access to, the full Project Gutenberg-tm License must appear prominently +whenever any copy of a Project Gutenberg-tm work (any work on which the +phrase "Project Gutenberg" appears, or with which the phrase "Project +Gutenberg" is associated) is accessed, displayed, performed, viewed, +copied or distributed: + +This eBook is for the use of anyone anywhere at no cost and with +almost no restrictions whatsoever. You may copy it, give it away or +re-use it under the terms of the Project Gutenberg License included +with this eBook or online at www.gutenberg.net + +1.E.2. If an individual Project Gutenberg-tm electronic work is derived +from the public domain (does not contain a notice indicating that it is +posted with permission of the copyright holder), the work can be copied +and distributed to anyone in the United States without paying any fees +or charges. If you are redistributing or providing access to a work +with the phrase "Project Gutenberg" associated with or appearing on the +work, you must comply either with the requirements of paragraphs 1.E.1 +through 1.E.7 or obtain permission for the use of the work and the +Project Gutenberg-tm trademark as set forth in paragraphs 1.E.8 or +1.E.9. + +1.E.3. If an individual Project Gutenberg-tm electronic work is posted +with the permission of the copyright holder, your use and distribution +must comply with both paragraphs 1.E.1 through 1.E.7 and any additional +terms imposed by the copyright holder. Additional terms will be linked +to the Project Gutenberg-tm License for all works posted with the +permission of the copyright holder found at the beginning of this work. + +1.E.4. Do not unlink or detach or remove the full Project Gutenberg-tm +License terms from this work, or any files containing a part of this +work or any other work associated with Project Gutenberg-tm. + +1.E.5. Do not copy, display, perform, distribute or redistribute this +electronic work, or any part of this electronic work, without +prominently displaying the sentence set forth in paragraph 1.E.1 with +active links or immediate access to the full terms of the Project +Gutenberg-tm License. + +1.E.6. You may convert to and distribute this work in any binary, +compressed, marked up, nonproprietary or proprietary form, including any +word processing or hypertext form. However, if you provide access to or +distribute copies of a Project Gutenberg-tm work in a format other than +"Plain Vanilla ASCII" or other format used in the official version +posted on the official Project Gutenberg-tm web site (www.gutenberg.net), +you must, at no additional cost, fee or expense to the user, provide a +copy, a means of exporting a copy, or a means of obtaining a copy upon +request, of the work in its original "Plain Vanilla ASCII" or other +form. Any alternate format must include the full Project Gutenberg-tm +License as specified in paragraph 1.E.1. + +1.E.7. Do not charge a fee for access to, viewing, displaying, +performing, copying or distributing any Project Gutenberg-tm works +unless you comply with paragraph 1.E.8 or 1.E.9. + +1.E.8. You may charge a reasonable fee for copies of or providing +access to or distributing Project Gutenberg-tm electronic works provided +that + +- You pay a royalty fee of 20% of the gross profits you derive from + the use of Project Gutenberg-tm works calculated using the method + you already use to calculate your applicable taxes. The fee is + owed to the owner of the Project Gutenberg-tm trademark, but he + has agreed to donate royalties under this paragraph to the + Project Gutenberg Literary Archive Foundation. Royalty payments + must be paid within 60 days following each date on which you + prepare (or are legally required to prepare) your periodic tax + returns. Royalty payments should be clearly marked as such and + sent to the Project Gutenberg Literary Archive Foundation at the + address specified in Section 4, "Information about donations to + the Project Gutenberg Literary Archive Foundation." + +- You provide a full refund of any money paid by a user who notifies + you in writing (or by e-mail) within 30 days of receipt that s/he + does not agree to the terms of the full Project Gutenberg-tm + License. You must require such a user to return or + destroy all copies of the works possessed in a physical medium + and discontinue all use of and all access to other copies of + Project Gutenberg-tm works. + +- You provide, in accordance with paragraph 1.F.3, a full refund of any + money paid for a work or a replacement copy, if a defect in the + electronic work is discovered and reported to you within 90 days + of receipt of the work. + +- You comply with all other terms of this agreement for free + distribution of Project Gutenberg-tm works. + +1.E.9. If you wish to charge a fee or distribute a Project Gutenberg-tm +electronic work or group of works on different terms than are set +forth in this agreement, you must obtain permission in writing from +both the Project Gutenberg Literary Archive Foundation and Michael +Hart, the owner of the Project Gutenberg-tm trademark. Contact the +Foundation as set forth in Section 3 below. + +1.F. + +1.F.1. Project Gutenberg volunteers and employees expend considerable +effort to identify, do copyright research on, transcribe and proofread +public domain works in creating the Project Gutenberg-tm +collection. Despite these efforts, Project Gutenberg-tm electronic +works, and the medium on which they may be stored, may contain +"Defects," such as, but not limited to, incomplete, inaccurate or +corrupt data, transcription errors, a copyright or other intellectual +property infringement, a defective or damaged disk or other medium, a +computer virus, or computer codes that damage or cannot be read by +your equipment. + +1.F.2. LIMITED WARRANTY, DISCLAIMER OF DAMAGES - Except for the "Right +of Replacement or Refund" described in paragraph 1.F.3, the Project +Gutenberg Literary Archive Foundation, the owner of the Project +Gutenberg-tm trademark, and any other party distributing a Project +Gutenberg-tm electronic work under this agreement, disclaim all +liability to you for damages, costs and expenses, including legal +fees. YOU AGREE THAT YOU HAVE NO REMEDIES FOR NEGLIGENCE, STRICT +LIABILITY, BREACH OF WARRANTY OR BREACH OF CONTRACT EXCEPT THOSE +PROVIDED IN PARAGRAPH 1.F.3. YOU AGREE THAT THE FOUNDATION, THE +TRADEMARK OWNER, AND ANY DISTRIBUTOR UNDER THIS AGREEMENT WILL NOT BE +LIABLE TO YOU FOR ACTUAL, DIRECT, INDIRECT, CONSEQUENTIAL, PUNITIVE OR +INCIDENTAL DAMAGES EVEN IF YOU GIVE NOTICE OF THE POSSIBILITY OF SUCH +DAMAGE. + +1.F.3. LIMITED RIGHT OF REPLACEMENT OR REFUND - If you discover a +defect in this electronic work within 90 days of receiving it, you can +receive a refund of the money (if any) you paid for it by sending a +written explanation to the person you received the work from. If you +received the work on a physical medium, you must return the medium with +your written explanation. The person or entity that provided you with +the defective work may elect to provide a replacement copy in lieu of a +refund. If you received the work electronically, the person or entity +providing it to you may choose to give you a second opportunity to +receive the work electronically in lieu of a refund. If the second copy +is also defective, you may demand a refund in writing without further +opportunities to fix the problem. + +1.F.4. Except for the limited right of replacement or refund set forth +in paragraph 1.F.3, this work is provided to you 'AS-IS' WITH NO OTHER +WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +WARRANTIES OF MERCHANTIBILITY OR FITNESS FOR ANY PURPOSE. + +1.F.5. Some states do not allow disclaimers of certain implied +warranties or the exclusion or limitation of certain types of damages. +If any disclaimer or limitation set forth in this agreement violates the +law of the state applicable to this agreement, the agreement shall be +interpreted to make the maximum disclaimer or limitation permitted by +the applicable state law. The invalidity or unenforceability of any +provision of this agreement shall not void the remaining provisions. + +1.F.6. INDEMNITY - You agree to indemnify and hold the Foundation, the +trademark owner, any agent or employee of the Foundation, anyone +providing copies of Project Gutenberg-tm electronic works in accordance +with this agreement, and any volunteers associated with the production, +promotion and distribution of Project Gutenberg-tm electronic works, +harmless from all liability, costs and expenses, including legal fees, +that arise directly or indirectly from any of the following which you do +or cause to occur: (a) distribution of this or any Project Gutenberg-tm +work, (b) alteration, modification, or additions or deletions to any +Project Gutenberg-tm work, and (c) any Defect you cause. + + +Section 2. Information about the Mission of Project Gutenberg-tm + +Project Gutenberg-tm is synonymous with the free distribution of +electronic works in formats readable by the widest variety of computers +including obsolete, old, middle-aged and new computers. It exists +because of the efforts of hundreds of volunteers and donations from +people in all walks of life. + +Volunteers and financial support to provide volunteers with the +assistance they need are critical to reaching Project Gutenberg-tm's +goals and ensuring that the Project Gutenberg-tm collection will +remain freely available for generations to come. In 2001, the Project +Gutenberg Literary Archive Foundation was created to provide a secure +and permanent future for Project Gutenberg-tm and future generations. +To learn more about the Project Gutenberg Literary Archive Foundation +and how your efforts and donations can help, see Sections 3 and 4 +and the Foundation web page at http://www.pglaf.org. + + +Section 3. Information about the Project Gutenberg Literary Archive +Foundation + +The Project Gutenberg Literary Archive Foundation is a non profit +501(c)(3) educational corporation organized under the laws of the +state of Mississippi and granted tax exempt status by the Internal +Revenue Service. The Foundation's EIN or federal tax identification +number is 64-6221541. Its 501(c)(3) letter is posted at +http://pglaf.org/fundraising. Contributions to the Project Gutenberg +Literary Archive Foundation are tax deductible to the full extent +permitted by U.S. federal laws and your state's laws. + +The Foundation's principal office is located at 4557 Melan Dr. S. +Fairbanks, AK, 99712., but its volunteers and employees are scattered +throughout numerous locations. Its business office is located at +809 North 1500 West, Salt Lake City, UT 84116, (801) 596-1887, email +business@pglaf.org. Email contact links and up to date contact +information can be found at the Foundation's web site and official +page at http://pglaf.org + +For additional contact information: + Dr. Gregory B. Newby + Chief Executive and Director + gbnewby@pglaf.org + + +Section 4. Information about Donations to the Project Gutenberg +Literary Archive Foundation + +Project Gutenberg-tm depends upon and cannot survive without wide +spread public support and donations to carry out its mission of +increasing the number of public domain and licensed works that can be +freely distributed in machine readable form accessible by the widest +array of equipment including outdated equipment. Many small donations +($1 to $5,000) are particularly important to maintaining tax exempt +status with the IRS. + +The Foundation is committed to complying with the laws regulating +charities and charitable donations in all 50 states of the United +States. Compliance requirements are not uniform and it takes a +considerable effort, much paperwork and many fees to meet and keep up +with these requirements. We do not solicit donations in locations +where we have not received written confirmation of compliance. To +SEND DONATIONS or determine the status of compliance for any +particular state visit http://pglaf.org + +While we cannot and do not solicit contributions from states where we +have not met the solicitation requirements, we know of no prohibition +against accepting unsolicited donations from donors in such states who +approach us with offers to donate. + +International donations are gratefully accepted, but we cannot make +any statements concerning tax treatment of donations received from +outside the United States. U.S. laws alone swamp our small staff. + +Please check the Project Gutenberg Web pages for current donation +methods and addresses. Donations are accepted in a number of other +ways including including checks, online payments and credit card +donations. To donate, please visit: http://pglaf.org/donate + + +Section 5. General Information About Project Gutenberg-tm electronic +works. + +Professor Michael S. Hart is the originator of the Project Gutenberg-tm +concept of a library of electronic works that could be freely shared +with anyone. For thirty years, he produced and distributed Project +Gutenberg-tm eBooks with only a loose network of volunteer support. + + +Project Gutenberg-tm eBooks are often created from several printed +editions, all of which are confirmed as Public Domain in the U.S. +unless a copyright notice is included. Thus, we do not necessarily +keep eBooks in compliance with any particular paper edition. + + +Most people start at our Web site which has the main PG search facility: + + http://www.gutenberg.net + +This Web site includes information about Project Gutenberg-tm, +including how to make donations to the Project Gutenberg Literary +Archive Foundation, how to help produce our new eBooks, and how to +subscribe to our email newsletter to hear about new eBooks. diff --git a/_downloads/68a40820b64deef88fa892002df63041/text.utf16 b/_downloads/68a40820b64deef88fa892002df63041/text.utf16 new file mode 100644 index 0000000..b80b2ef Binary files /dev/null and b/_downloads/68a40820b64deef88fa892002df63041/text.utf16 differ diff --git a/_downloads/69bd04274d4c93424a5ddfb92930eca7/pytest_fixtures.py b/_downloads/69bd04274d4c93424a5ddfb92930eca7/pytest_fixtures.py new file mode 100644 index 0000000..36d7434 --- /dev/null +++ b/_downloads/69bd04274d4c93424a5ddfb92930eca7/pytest_fixtures.py @@ -0,0 +1,60 @@ +#!usr/bin/env python + +""" +example to show how fixtures work in pytest + +to run this and see the output: + +py.test -s -v pytest_fixtures.py +""" + +import pytest + + +@pytest.fixture +# with module-level scope +#@pytest.fixture(scope="module") +def example_fixture(): + """ + An example fixture that does nothing useful + + But does return an object you can use for testing + """ + print("I am running the fixture now") + return {"this": 3, + "that": 2} + + +# now use the fixture in a couple tests +def test_one(example_fixture): + print("running test_one") + assert example_fixture["this"] == 3 + + +def test_two(example_fixture): + print("running test_two") + assert example_fixture["that"] == 2 + +# with teardown: +@pytest.fixture(scope="module") +def example_fixture2(): + """ + An example fixture that does nothing useful + + But does return an object you can use for testing + """ + print("I am running the fixture now") + yield {"this": 3, + "that": 2} + print("and now I am running the teardown code") + + +# using the fixture with teardown: +def test_three(example_fixture2): + print("running test_three") + assert example_fixture2["this"] == 3 + + +def test_four(example_fixture2): + print("running test_four") + assert example_fixture2["this"] == 3 diff --git a/_downloads/6b2570b221e822929ed51f41075b604c/sample_html.html b/_downloads/6b2570b221e822929ed51f41075b604c/sample_html.html new file mode 100644 index 0000000..9c2e675 --- /dev/null +++ b/_downloads/6b2570b221e822929ed51f41075b604c/sample_html.html @@ -0,0 +1,27 @@ + + + + + Python Class Sample page + + +

Python Class - Html rendering example

+

+ Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+
+ + + \ No newline at end of file diff --git a/_downloads/6c3432a547b9c850475df13d5cde0080/simple_classes.py b/_downloads/6c3432a547b9c850475df13d5cde0080/simple_classes.py new file mode 100644 index 0000000..4f99606 --- /dev/null +++ b/_downloads/6c3432a547b9c850475df13d5cde0080/simple_classes.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +""" +simple_classes.py + +demonstrating the basics of a class +""" + +import math + + +# create a point class +class Point: + def __init__(self, x, y): + self.x = x + self.y = y + +# create an instance of that class +p = Point(3, 4) + +# access the attributes +print("p.x is:", p.x) +print("p.y is:", p.y) + + +class Point2: + size = 4 + color = "red" + + def __init__(self, x, y): + self.x = x + self.y = y + +p2 = Point2(4, 5) +print(p2.size) +print(p2.color) + + +class Point3: + size = 4 + color = "red" + + def __init__(self, x, y): + self.x = x + self.y = y + + def get_color(self): + return self.color + + def get_size(self): + return self.size + +class Rect: + + def __init__(self, w, h): + self.w = w + self.h = h + + def get_size(self): + return self.w * self.h + + +p3 = Point3(4, 5) +print(p3.size) +print(p3.get_color()) + + +class Circle: + color = "red" + styles = ['dashed'] + + def __init__(self, diameter): + self.diameter = diameter + + def grow(self, factor=2): + """ + grows the circle's diameter + + :param factor=2: factor by which to grow the circle + """ + self.diameter = self.diameter * factor + + def add_style(self, style): + self.styles.append(style) + + def get_area(self): + return math.pi * self.diameter / 2.0 + + +class NewCircle(Circle): + color = "blue" + + def grow(self, factor=2): + """grows the area by factor...""" + self.diameter = self.diameter * math.sqrt(2) + +nc = NewCircle +print(nc.color) + + +class CircleR(Circle): + def __init__(self, radius): + diameter = radius*2 + Circle.__init__(self, diameter) + + +class CircleR2(Circle): + def __init__(self, radius): + self.radius = radius + + def get_area(self): + return Circle.get_area(self, self.radius*2) diff --git a/_downloads/6e99594bb741e3843a46a748e9a84806/run_html_render.py b/_downloads/6e99594bb741e3843a46a748e9a84806/run_html_render.py new file mode 100644 index 0000000..9608a65 --- /dev/null +++ b/_downloads/6e99594bb741e3843a46a748e9a84806/run_html_render.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 + +""" +a simple script can run and test your html rendering classes. + +Uncomment the steps as you add to your rendering. + +""" + +from io import StringIO + +# importing the html_rendering code with a short name for easy typing. +import html_render as hr + + +# writing the file out: +def render_page(page, filename, indent=None): + """ + render the tree of elements + + This uses StringIO to render to memory, then dump to console and + write to file -- very handy! + """ + + f = StringIO() + if indent is None: + page.render(f) + else: + page.render(f, indent) + + print(f.getvalue()) + with open(filename, 'w') as outfile: + outfile.write(f.getvalue()) + + +# Step 1 +######### + +page = hr.Element() + +page.append("Here is a paragraph of text -- there could be more of them, " + "but this is enough to show that we can do some text") + +page.append("And here is another piece of text -- you should be able to add any number") + +render_page(page, "test_html_output1.html") + +# The rest of the steps have been commented out. +# Uncomment them as you move along with the assignment. + +# ## Step 2 +# ########## + +# page = hr.Html() + +# body = hr.Body() + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text")) + +# body.append(hr.P("And here is another piece of text -- you should be able to add any number")) + +# page.append(body) + +# render_page(page, "test_html_output2.html") + +# # Step 3 +# ########## + +# page = hr.Html() + +# head = hr.Head() +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text")) +# body.append(hr.P("And here is another piece of text -- you should be able to add any number")) + +# page.append(body) + +# render_page(page, "test_html_output3.html") + +# # Step 4 +# ########## + +# page = hr.Html() + +# head = hr.Head() +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text", +# style="text-align: center; font-style: oblique;")) + +# page.append(body) + +# render_page(page, "test_html_output4.html") + +# # Step 5 +# ######### + +# page = hr.Html() + +# head = hr.Head() +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text", +# style="text-align: center; font-style: oblique;")) + +# body.append(hr.Hr()) + +# page.append(body) + +# render_page(page, "test_html_output5.html") + +# # Step 6 +# ######### + +# page = hr.Html() + +# head = hr.Head() +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text", +# style="text-align: center; font-style: oblique;")) + +# body.append(hr.Hr()) + +# body.append("And this is a ") +# body.append( hr.A("http://google.com", "link") ) +# body.append("to google") + +# page.append(body) + +# render_page(page, "test_html_output6.html") + +# # Step 7 +# ######### + +# page = hr.Html() + +# head = hr.Head() +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append( hr.H(2, "PythonClass - Class 6 example") ) + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text", +# style="text-align: center; font-style: oblique;")) + +# body.append(hr.Hr()) + +# list = hr.Ul(id="TheList", style="line-height:200%") + +# list.append( hr.Li("The first item in a list") ) +# list.append( hr.Li("This is the second item", style="color: red") ) + +# item = hr.Li() +# item.append("And this is a ") +# item.append( hr.A("http://google.com", "link") ) +# item.append("to google") + +# list.append(item) + +# body.append(list) + +# page.append(body) + +# render_page(page, "test_html_output7.html") + +# # Step 8 and 9 +# ############## + +# page = hr.Html() + + +# head = hr.Head() +# head.append( hr.Meta(charset="UTF-8") ) +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append( hr.H(2, "PythonClass - Example") ) + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text", +# style="text-align: center; font-style: oblique;")) + +# body.append(hr.Hr()) + +# list = hr.Ul(id="TheList", style="line-height:200%") + +# list.append( hr.Li("The first item in a list") ) +# list.append( hr.Li("This is the second item", style="color: red") ) + +# item = hr.Li() +# item.append("And this is a ") +# item.append( hr.A("http://google.com", "link") ) +# item.append("to google") + +# list.append(item) + +# body.append(list) + +# page.append(body) + +# render_page(page, "test_html_output8.html") diff --git a/_downloads/6ea732f13a1434c31c44b2e1e1403399/property_ugly.py b/_downloads/6ea732f13a1434c31c44b2e1e1403399/property_ugly.py new file mode 100644 index 0000000..fe65f35 --- /dev/null +++ b/_downloads/6ea732f13a1434c31c44b2e1e1403399/property_ugly.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +class C: + """ + Property defined in about the most ugly way possible + """ + def __init__(self): + self._x = None + def x(self): + return self._x + x = property(x) + def _set_x(self, value): + self._x = value + x = x.setter(_set_x) + def _del_x(self): + del self._x + x = x.deleter(_del_x) + + diff --git a/_downloads/6f19c1a8554518241fa56859fad09ec8/my_for.py b/_downloads/6f19c1a8554518241fa56859fad09ec8/my_for.py new file mode 100644 index 0000000..6023ad9 --- /dev/null +++ b/_downloads/6f19c1a8554518241fa56859fad09ec8/my_for.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python + +""" +hand writing 'for' + +demonstrates how for interacts with an iterable +""" + + +l = [1,2,3,4,5,] + + +def my_for(an_iterable, func): + """ + Emulation of a for loop. + + func() will be called with each item in an_iterable + + :param an_iterable: anything that satisfies the interation protocol + + :param func: a callable -- it will be called, passing in each item + in an_iterable. + + """ + # equiv of "for i in l:" + iterator = iter(an_iterable) + while True: + try: + i = next(iterator) + except StopIteration: + break + func(i) + + +if __name__ == "__main__": + + def print_func(x): + print(x) + + l = [1,2,3,4,5,] + my_for(l, print_func) + + t = ('a','b','c','d') + + my_for(t, print_func) + + + + + diff --git a/_downloads/6f651cedf7ea27db7a574c8f9167eae0/test_lambda.py b/_downloads/6f651cedf7ea27db7a574c8f9167eae0/test_lambda.py new file mode 100644 index 0000000..4992afd --- /dev/null +++ b/_downloads/6f651cedf7ea27db7a574c8f9167eae0/test_lambda.py @@ -0,0 +1,50 @@ +""" +Some simple tests for the "lambda and keyword magic" +function builder + +designed to run with py.test +""" + +from lambda_keyword import function_builder +# uncomment to test second version +# from lambda_keyword import function_builder2 as function_builder + + +def test_length(): + """ + the function should return a list of the length input + """ + assert len(function_builder(0)) == 0 + + assert len(function_builder(3)) == 3 + + assert len(function_builder(5)) == 5 + + +def test_increment(): + """ + the functions in the list should increment the input values + """ + func_list = function_builder(5) + + assert func_list[0](3) == 3 + + assert func_list[1](3) == 4 + + assert func_list[2](3) == 5 + + assert func_list[3](3) == 6 + + +def test_increment2(): + """ + the functions in the list should increment the input values + """ + + func_list = function_builder(10) + + assert func_list[0](12) == 12 + + assert func_list[1](10) == 11 + + assert func_list[9](3) == 12 diff --git a/_downloads/6fc7890d9fea2c802c990aeac9070f85/diamond_super.py b/_downloads/6fc7890d9fea2c802c990aeac9070f85/diamond_super.py new file mode 100644 index 0000000..ca5d929 --- /dev/null +++ b/_downloads/6fc7890d9fea2c802c990aeac9070f85/diamond_super.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 + +""" +Example of solving the classic "diamond problem" using super() + +In this case, class A is at the root of the class hierarchy + +B and C both inherit from A + +D inherits from B and C + +ASCII art that shows this: + + ----- + | A | + ----- + / \ + / \ +----- ----- +| B | | C | +----- ----- + \ / + \ / + ----- + | D | + ----- + +So what's the problem? + +If you call a method on D -- it calls B and C's method -- and each +of them call A's method. So A's method gets called twice! + +But using super() makes sure all the methods get called, but none of them twice. +""" + + +class A(object): + def do_your_stuff(self): + print("doing A's stuff") + + +class Default(A): + def do_your_stuff(self): + print('doing Default stuff') + + +class B(A): + def do_your_stuff(self): + super().do_your_stuff() + print("doing B's stuff") + + +class C(A): + def do_your_stuff(self): + super().do_your_stuff() + print("doing C's stuff") + + +class D(B, C): + def do_your_stuff(self): + super().do_your_stuff() + print("doing D's stuff") + + +if __name__ == '__main__': + a = A() + print("\ncalling A's method") + a.do_your_stuff() + + default = Default() + print("\ncalling Default's method") + default.do_your_stuff() + + print("\ncalling B's method") + b = B() + b.do_your_stuff() + + print("\ncalling C's method") + c = C() + c.do_your_stuff() + + print("\ncalling D's method") + d = D() + d.do_your_stuff() diff --git a/_downloads/7107ff38a2e1445230a2fc2073afcf9c/test_html_output6.html b/_downloads/7107ff38a2e1445230a2fc2073afcf9c/test_html_output6.html new file mode 100644 index 0000000..407a87f --- /dev/null +++ b/_downloads/7107ff38a2e1445230a2fc2073afcf9c/test_html_output6.html @@ -0,0 +1,14 @@ + + +PythonClass = Revision 1087: + + +

+Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+
+And this is a +link +to google + + diff --git a/_downloads/73fcc9debc1b4f1d9b16ccff3cef0964/test_lambda.py b/_downloads/73fcc9debc1b4f1d9b16ccff3cef0964/test_lambda.py new file mode 100644 index 0000000..4992afd --- /dev/null +++ b/_downloads/73fcc9debc1b4f1d9b16ccff3cef0964/test_lambda.py @@ -0,0 +1,50 @@ +""" +Some simple tests for the "lambda and keyword magic" +function builder + +designed to run with py.test +""" + +from lambda_keyword import function_builder +# uncomment to test second version +# from lambda_keyword import function_builder2 as function_builder + + +def test_length(): + """ + the function should return a list of the length input + """ + assert len(function_builder(0)) == 0 + + assert len(function_builder(3)) == 3 + + assert len(function_builder(5)) == 5 + + +def test_increment(): + """ + the functions in the list should increment the input values + """ + func_list = function_builder(5) + + assert func_list[0](3) == 3 + + assert func_list[1](3) == 4 + + assert func_list[2](3) == 5 + + assert func_list[3](3) == 6 + + +def test_increment2(): + """ + the functions in the list should increment the input values + """ + + func_list = function_builder(10) + + assert func_list[0](12) == 12 + + assert func_list[1](10) == 11 + + assert func_list[9](3) == 12 diff --git a/_downloads/755eb41102dde9e2c2ec1db66c41339b/text.utf32 b/_downloads/755eb41102dde9e2c2ec1db66c41339b/text.utf32 new file mode 100644 index 0000000..c529531 Binary files /dev/null and b/_downloads/755eb41102dde9e2c2ec1db66c41339b/text.utf32 differ diff --git a/_downloads/759e77431169ef645ab0af9da6dd5990/async_timer.py b/_downloads/759e77431169ef645ab0af9da6dd5990/async_timer.py new file mode 100644 index 0000000..2cbe756 --- /dev/null +++ b/_downloads/759e77431169ef645ab0af9da6dd5990/async_timer.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +""" +Simple async example derived from python docs. + +Will only work on Python 3.5 and above +""" + +import asyncio +import time +import datetime +import random + + +# using "async" makes this a coroutine: +# its code can be run by the event loop +async def display_date(num): + end_time = time.time() + 10.0 # we want it to run for 10 seconds. + while True: # keep doing this until break + print("instance: {} Time: {}".format(num, datetime.datetime.now())) + if (time.time()) >= end_time: + print("instance: {} is all done".format(num)) + break + # pause for a random amount of time + await asyncio.sleep(random.randint(0, 3)) + + +def shutdown(): + print("shutdown called") + # you can access the event loop this way: + loop = asyncio.get_event_loop() + loop.stop() + + +# You register "futures" on the loop this way: +asyncio.ensure_future(display_date(1)) +asyncio.ensure_future(display_date(2)) + +loop = asyncio.get_event_loop() + +# or add tasks to the loop like this: +loop.create_task(display_date(3)) +loop.create_task(display_date(4)) +for i in range(5, 20): + loop.create_task(display_date(i)) + +# this will shut the event loop down in 15 seconds +loop.call_later(15, shutdown) + +print("about to run loop") +# this is a blocking call +loop.run_forever() +print("loop exited") + diff --git a/_downloads/762d8b297a01879edef7304147b250de/timer_test.py b/_downloads/762d8b297a01879edef7304147b250de/timer_test.py new file mode 100644 index 0000000..e3d1422 --- /dev/null +++ b/_downloads/762d8b297a01879edef7304147b250de/timer_test.py @@ -0,0 +1,26 @@ +import time + + +def timer(func): + def timer(*args, **kwargs): + """a decorator which prints execution time of the decorated + function""" + t1 = time.time() + result = func(*args, **kwargs) + t2 = time.time() + print("-- executed %s in %.4f seconds" % (func.__name__, (t2 - t1))) + return result + return timer + + +@timer +def expensive_function(): + time.sleep(1) + + +@timer +def less_expensive_function(): + time.sleep(.02) + +expensive_function() +less_expensive_function() diff --git a/_downloads/7aed047b56b0f01a32459203ca0ca70f/lock_exercise.zip b/_downloads/7aed047b56b0f01a32459203ca0ca70f/lock_exercise.zip new file mode 100644 index 0000000..d315044 Binary files /dev/null and b/_downloads/7aed047b56b0f01a32459203ca0ca70f/lock_exercise.zip differ diff --git a/_downloads/7b9029310dd0d29fb92c3c68c1bfaf1d/html_render.py b/_downloads/7b9029310dd0d29fb92c3c68c1bfaf1d/html_render.py new file mode 100644 index 0000000..508400f --- /dev/null +++ b/_downloads/7b9029310dd0d29fb92c3c68c1bfaf1d/html_render.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +""" +A class-based system for rendering html. +""" + + +# This is the framework for the base class +class Element(object): + + def __init__(self, content=None): + pass + + def append(self, new_content): + pass + + def render(self, out_file): + out_file.write("just something as a place holder...") diff --git a/_downloads/7b94ecfeddc45b179f0950f24ccd113e/text.utf32 b/_downloads/7b94ecfeddc45b179f0950f24ccd113e/text.utf32 new file mode 100644 index 0000000..c529531 Binary files /dev/null and b/_downloads/7b94ecfeddc45b179f0950f24ccd113e/text.utf32 differ diff --git a/_downloads/7bc4ffd8c2cb4cf7a24a782c930f927e/test_html_output7.html b/_downloads/7bc4ffd8c2cb4cf7a24a782c930f927e/test_html_output7.html new file mode 100644 index 0000000..19cfc41 --- /dev/null +++ b/_downloads/7bc4ffd8c2cb4cf7a24a782c930f927e/test_html_output7.html @@ -0,0 +1,25 @@ + + +PythonClass = Revision 1087: + + +

PythonClass - Class 6 example

+

+Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+
+ + + \ No newline at end of file diff --git a/_downloads/7c8759cbd46652f676d956b235d5f1e6/test_random_unitest.py b/_downloads/7c8759cbd46652f676d956b235d5f1e6/test_random_unitest.py new file mode 100644 index 0000000..b8a1b71 --- /dev/null +++ b/_downloads/7c8759cbd46652f676d956b235d5f1e6/test_random_unitest.py @@ -0,0 +1,32 @@ +import random +import unittest + + +class TestSequenceFunctions(unittest.TestCase): + + def setUp(self): + self.seq = list(range(10)) + + def test_shuffle(self): + """ + make sure the shuffled sequence does not lose any elements + """ + random.shuffle(self.seq) + self.seq.sort() + self.assertEqual(self.seq, list(range(10))) + + # should raise an exception for an immutable sequence + self.assertRaises(TypeError, random.shuffle, (1, 2, 3)) + + def test_choice(self): + element = random.choice(self.seq) + self.assertTrue(element in self.seq) + + def test_sample(self): + with self.assertRaises(ValueError): + random.sample(self.seq, 20) + for element in random.sample(self.seq, 5): + self.assertTrue(element in self.seq) + +if __name__ == '__main__': + unittest.main() diff --git a/_downloads/8015ec2e0a1cc6ebc8514e78227617fc/test_walnut_party.py b/_downloads/8015ec2e0a1cc6ebc8514e78227617fc/test_walnut_party.py new file mode 100644 index 0000000..9789afe --- /dev/null +++ b/_downloads/8015ec2e0a1cc6ebc8514e78227617fc/test_walnut_party.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python + +""" +test code for the walnut party example + +Adapted from the "coding bat" site: https://codingbat.com/python + +When squirrels get together for a party, they like to have walnuts. +A squirrel party is successful when the number of walnuts is between +40 and 60, inclusive. Unless it is the weekend, in which case there +is no upper bound on the number of walnuts. + +Return True if the party with the given values is successful, +or False otherwise. +""" + + +# you can change this import to test different versions +from walnut_party import walnut_party +# from walnut_party import walnut_party2 as walnut_party +# from walnut_party import walnut_party3 as walnut_party + + +def test_too_few(): + assert not walnut_party(30, False) + + +def test_middle(): + assert walnut_party(50, False) + + +def test_too_many(): + assert walnut_party(70, True) + + +def test_middle50(): + assert walnut_party(50, True) + + +def test_upper_bound(): + assert walnut_party(60, False) + + +def test_just_too_big(): + assert not walnut_party(61, False) + + +def test_lower_bound(): + assert walnut_party(40, False) + + +def test_just_too_small(): + assert walnut_party(39, False) is False diff --git a/_downloads/8030e817ab7bd6903c7159bfada9b998/get_set_attr.py b/_downloads/8030e817ab7bd6903c7159bfada9b998/get_set_attr.py new file mode 100644 index 0000000..30beb77 --- /dev/null +++ b/_downloads/8030e817ab7bd6903c7159bfada9b998/get_set_attr.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 + +""" +Manipulating attributes + +Example code for manipulating attributes +""" + + +# A simple class for a person +class Person: + def __init__(self, first_name="", last_name="", phone=""): + self.first_name = first_name + self.last_name = last_name + self.phone = phone + + def __str__(self): + msg = ["Person:"] + for name, val in vars(self).items(): + msg.append("{}: {}".format(name, val)) + return "\n".join(msg) + + +def update_person(person): + while True: + att = input("What would you like to update for:\n" + "{}\n" + '(type "quit" to quit) >> '.format(person) + ) + if att.strip().lower() == "quit": + break + if not hasattr(person, att): + ans = input("This person does not have that attribute.\n" + "Would you like to add it? Y,[N] >> ") + if not ans.lower().startswith('y'): + continue + ans = input("What would you like to set it to? >> ") + setattr(person, att, ans) + + +if __name__ == "__main__": + # a little test code: + + # create a couple people: + p1 = Person("Fred", "Jones", "206-555-1234") + update_person(p1) + + diff --git a/_downloads/80bcf8838a72443c482143fa3a2e1dda/test_html_output4.html b/_downloads/80bcf8838a72443c482143fa3a2e1dda/test_html_output4.html new file mode 100644 index 0000000..5846b22 --- /dev/null +++ b/_downloads/80bcf8838a72443c482143fa3a2e1dda/test_html_output4.html @@ -0,0 +1,10 @@ + + +PythonClass = Revision 1087: + + +

+Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+ + \ No newline at end of file diff --git a/_downloads/81cae2301fa776de9a4af1ad0afc2e86/roman8.py b/_downloads/81cae2301fa776de9a4af1ad0afc2e86/roman8.py new file mode 100644 index 0000000..83a7cc2 --- /dev/null +++ b/_downloads/81cae2301fa776de9a4af1ad0afc2e86/roman8.py @@ -0,0 +1,146 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + +import pytest + +roman_numeral_map = (('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1)) + + +def to_roman(n): + """convert integer to Roman numeral""" + if not (0 < n < 4000): + raise ValueError("number out of range (must be 1..3999)") + + if int(n) != n: + raise ValueError("Only integers can be converted to Roman numerals") + + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + return result + + +## 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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result + + +def test_too_large(): + """ + to_roman should raise an ValueError when passed + values over 3999 + """ + with pytest.raises(ValueError): + to_roman(4000) + + +def test_zero(): + """to_roman should raise an ValueError with 0 input""" + 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) + + +def test_non_integer(): + """to_roman should raise an ValueError with non-integer input""" + with pytest.raises(ValueError): + to_roman(0.5) + + +def test_float_with_integer_value(): + """to_roman should work for floats with integer values""" + assert to_roman(3.0) == "III" + + + diff --git a/_downloads/853dca9f84e1b44a2820836dac5ee1b5/roman7.py b/_downloads/853dca9f84e1b44a2820836dac5ee1b5/roman7.py new file mode 100644 index 0000000..beb34ae --- /dev/null +++ b/_downloads/853dca9f84e1b44a2820836dac5ee1b5/roman7.py @@ -0,0 +1,140 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + +import pytest + +roman_numeral_map = (('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1)) + + +def to_roman(n): + """convert integer to Roman numeral""" + if not (0 < n < 4000): + raise ValueError("number out of range (must be 1..3999)") + + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + return result + + +## 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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result + + +def test_too_large(): + """ + to_roman should raise an ValueError when passed + values over 3999 + """ + with pytest.raises(ValueError): + to_roman(4000) + + +def test_zero(): + """to_roman should raise an ValueError with 0 input""" + 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) + + +def test_non_integer(): + """to_roman should raise an ValueError with non-integer input""" + with pytest.raises(ValueError): + to_roman(0.5) + + +def test_float_with_integer_value(): + """to_roman should work for floats with integer values""" + assert to_roman(3.0) == "III" diff --git a/_downloads/856962444f5328c5d5739a3d7d67384e/iterator_1.py b/_downloads/856962444f5328c5d5739a3d7d67384e/iterator_1.py new file mode 100644 index 0000000..479e5bf --- /dev/null +++ b/_downloads/856962444f5328c5d5739a3d7d67384e/iterator_1.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +""" +Simple iterator examples +""" + + +class IterateMe_1: + """ + About as simple an iterator as you can get: + + returns the sequence of numbers from zero to 4 + ( like range(4) ) + """ + + def __init__(self, stop=5): + self.current = -1 + self.stop = stop + + def __iter__(self): + return self + + def __next__(self): + self.current += 1 + if self.current < self.stop: + return self.current + else: + raise StopIteration + + +if __name__ == "__main__": + + print("Testing the iterator") + for i in IterateMe_1(): + print(i) diff --git a/_downloads/86545e73cde934d15efaf0cf55b47286/calculator_test_suite.py b/_downloads/86545e73cde934d15efaf0cf55b47286/calculator_test_suite.py new file mode 100644 index 0000000..7557084 --- /dev/null +++ b/_downloads/86545e73cde934d15efaf0cf55b47286/calculator_test_suite.py @@ -0,0 +1,6 @@ +import unittest + +from test_calculator import TestCalculatorFunctions + +suite = unittest.TestLoader().loadTestsFromTestCase(TestCalculatorFunctions) +unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/_downloads/8694d56f5b9116053ad82cbe3c01153e/test_html_output5.html b/_downloads/8694d56f5b9116053ad82cbe3c01153e/test_html_output5.html new file mode 100644 index 0000000..e7efbfc --- /dev/null +++ b/_downloads/8694d56f5b9116053ad82cbe3c01153e/test_html_output5.html @@ -0,0 +1,11 @@ + + +PythonClass = Revision 1087: + + +

+Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+
+ + \ No newline at end of file diff --git a/_downloads/8858d7793ed802b63b5316435b01b522/roman8.py b/_downloads/8858d7793ed802b63b5316435b01b522/roman8.py new file mode 100644 index 0000000..83a7cc2 --- /dev/null +++ b/_downloads/8858d7793ed802b63b5316435b01b522/roman8.py @@ -0,0 +1,146 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + +import pytest + +roman_numeral_map = (('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1)) + + +def to_roman(n): + """convert integer to Roman numeral""" + if not (0 < n < 4000): + raise ValueError("number out of range (must be 1..3999)") + + if int(n) != n: + raise ValueError("Only integers can be converted to Roman numerals") + + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + return result + + +## 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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result + + +def test_too_large(): + """ + to_roman should raise an ValueError when passed + values over 3999 + """ + with pytest.raises(ValueError): + to_roman(4000) + + +def test_zero(): + """to_roman should raise an ValueError with 0 input""" + 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) + + +def test_non_integer(): + """to_roman should raise an ValueError with non-integer input""" + with pytest.raises(ValueError): + to_roman(0.5) + + +def test_float_with_integer_value(): + """to_roman should work for floats with integer values""" + assert to_roman(3.0) == "III" + + + diff --git a/_downloads/892493f25fa8e6cf5d0a42dec8d7ac1f/roman3.py b/_downloads/892493f25fa8e6cf5d0a42dec8d7ac1f/roman3.py new file mode 100644 index 0000000..26b3b3b --- /dev/null +++ b/_downloads/892493f25fa8e6cf5d0a42dec8d7ac1f/roman3.py @@ -0,0 +1,117 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + +import pytest + +roman_numeral_map = (('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1)) + + +def to_roman(n): + '''convert integer to Roman numeral''' + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + # print(f'subtracting {integer} from input, adding {numeral} to output') + return result + + +## 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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result + + +def test_too_large(): + """ + to_roman should raise an ValueError when passed + values over 3999 + """ + with pytest.raises(ValueError): + to_roman(4000) + + diff --git a/_downloads/896542ca231dd03ff6a055aa032a9a0b/context_manager.py b/_downloads/896542ca231dd03ff6a055aa032a9a0b/context_manager.py new file mode 100644 index 0000000..7e6f74c --- /dev/null +++ b/_downloads/896542ca231dd03ff6a055aa032a9a0b/context_manager.py @@ -0,0 +1,21 @@ +# Demo of a contextmanager + + +class Context(object): + """ + from Doug Hellmann, PyMOTW + https://pymotw.com/3/contextlib/#module-contextlib + """ + + def __init__(self, handle_error): + print('__init__({})'.format(handle_error)) + self.handle_error = handle_error + + def __enter__(self): + print('__enter__()') + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + print(exc_val.args) + print('__exit__({}, {}, {})'.format(exc_type, exc_val, exc_tb)) + return self.handle_error diff --git a/_downloads/8996f535304b3e2e4a45ee10526d0bed/text.utf8 b/_downloads/8996f535304b3e2e4a45ee10526d0bed/text.utf8 new file mode 100644 index 0000000..9de1889 --- /dev/null +++ b/_downloads/8996f535304b3e2e4a45ee10526d0bed/text.utf8 @@ -0,0 +1,17 @@ +Origin (in native language) Name (in native language) +Հայաստան Արամ Խաչատրյան + Australia Nicole Kidman + Österreich Johann Strauß + Azərbaycan Vaqif Səmədoğlu + Азәрбајҹан Вагиф Сәмәдоғлу + Azərbaycan Heydər Əliyev + Азәрбајҹан Һејдәр Әлијев + België René Magritte + Belgique René Magritte + Belgien René Magritte + বাংলা সুকুমার রায় + འབྲུག་ཡུལ། མགོན་པོ་རྡོ་རྗེ། + ប្រទេស​​​កម្ពុជា ព្រះ​ពុទ្ឋឃោសាចារ‌្យ​ជួន​ណាត +Canada Céline Dion + ᓄᓇᕗᒻᒥᐅᑦ ᓱᓴᓐ ᐊᒡᓗᒃᑲᖅ + \ No newline at end of file diff --git a/_downloads/8a7f06ffa98ce186e8b099d14ff6382b/test_html_render.py b/_downloads/8a7f06ffa98ce186e8b099d14ff6382b/test_html_render.py new file mode 100644 index 0000000..5833d50 --- /dev/null +++ b/_downloads/8a7f06ffa98ce186e8b099d14ff6382b/test_html_render.py @@ -0,0 +1,264 @@ +""" +test code for html_render.py + +This is just a start -- you will need more tests! +""" + +import io +import pytest + +# import * is often bad form, but makes it easier to test everything in a module. +from html_render import * + + +# utility function for testing render methods +# needs to be used in multiple tests, so we write it once here. +def render_result(element, ind=""): + """ + calls the element's render method, and returns what got rendered as a + string + """ + # the StringIO object is a "file-like" object -- something that + # provides the methods of a file, but keeps everything in memory + # so it can be used to test code that writes to a file, without + # having to actually write to disk. + outfile = io.StringIO() + # this so the tests will work before we tackle indentation + if ind: + element.render(outfile, ind) + else: + element.render(outfile) + return outfile.getvalue() + +######## +# Step 1 +######## + +def test_init(): + """ + This only tests that it can be initialized with and without + some content -- but it's a start + """ + e = Element() + + e = Element("this is some text") + + +def test_append(): + """ + This tests that you can append text + + It doesn't test if it works -- + that will be covered by the render test later + """ + e = Element("this is some text") + e.append("some more text") + + +def test_render_element(): + """ + Tests whether the Element can render two pieces of text + So it is also testing that the append method works correctly. + + It is not testing whether indentation or line feeds are correct. + """ + e = Element("this is some text") + e.append("and this is some more text") + + # This uses the render_results utility above + file_contents = render_result(e).strip() + + # making sure the content got in there. + assert("this is some text") in file_contents + assert("and this is some more text") in file_contents + + # make sure it's in the right order + assert file_contents.index("this is") < file_contents.index("and this") + + # making sure the opening and closing tags are right. + assert file_contents.startswith("") + assert file_contents.endswith("") + +# # Uncomment this one after you get the one above to pass +# # Does it pass right away? +# def test_render_element2(): +# """ +# Tests whether the Element can render two pieces of text +# So it is also testing that the append method works correctly. + +# It is not testing whether indentation or line feeds are correct. +# """ +# e = Element() +# e.append("this is some text") +# e.append("and this is some more text") + +# # This uses the render_results utility above +# file_contents = render_result(e).strip() + +# # making sure the content got in there. +# assert("this is some text") in file_contents +# assert("and this is some more text") in file_contents + +# # make sure it's in the right order +# assert file_contents.index("this is") < file_contents.index("and this") + +# # making sure the opening and closing tags are right. +# assert file_contents.startswith("") +# assert file_contents.endswith("") + + + +# # ######## +# # # Step 2 +# # ######## + +# # tests for the new tags +# def test_html(): +# e = Html("this is some text") +# e.append("and this is some more text") + +# file_contents = render_result(e).strip() + +# assert("this is some text") in file_contents +# assert("and this is some more text") in file_contents +# print(file_contents) +# assert file_contents.endswith("") + + +# def test_body(): +# e = Body("this is some text") +# e.append("and this is some more text") + +# file_contents = render_result(e).strip() + +# assert("this is some text") in file_contents +# assert("and this is some more text") in file_contents + +# assert file_contents.startswith("") +# assert file_contents.endswith("") + + +# def test_p(): +# e = P("this is some text") +# e.append("and this is some more text") + +# file_contents = render_result(e).strip() + +# assert("this is some text") in file_contents +# assert("and this is some more text") in file_contents + +# assert file_contents.startswith("

") +# assert file_contents.endswith("

") + + +# def test_sub_element(): +# """ +# tests that you can add another element and still render properly +# """ +# page = Html() +# page.append("some plain text.") +# page.append(P("A simple paragraph of text")) +# page.append("Some more plain text.") + +# file_contents = render_result(page) +# print(file_contents) # so we can see it if the test fails + +# # note: The previous tests should make sure that the tags are getting +# # properly rendered, so we don't need to test that here. +# assert "some plain text" in file_contents +# assert "A simple paragraph of text" in file_contents +# assert "Some more plain text." in file_contents +# assert "some plain text" in file_contents +# # but make sure the embedded element's tags get rendered! +# assert "

" in file_contents +# assert "

" in file_contents + + + + +######## +# Step 3 +######## + +# Add your tests here! + +# ##################### +# # indentation testing +# # Uncomment for Step 9 -- adding indentation +# ##################### + + +# def test_indent(): +# """ +# Tests that the indentation gets passed through to the renderer +# """ +# html = Html("some content") +# file_contents = render_result(html, ind=" ").rstrip() #remove the end newline + +# print(file_contents) +# lines = file_contents.split("\n") +# assert lines[0].startswith(" <") +# print(repr(lines[-1])) +# assert lines[-1].startswith(" <") + + +# def test_indent_contents(): +# """ +# The contents in a element should be indented more than the tag +# by the amount in the indent class attribute +# """ +# html = Element("some content") +# file_contents = render_result(html, ind="") + +# print(file_contents) +# lines = file_contents.split("\n") +# assert lines[1].startswith(Element.indent) + + +# def test_multiple_indent(): +# """ +# make sure multiple levels get indented fully +# """ +# body = Body() +# body.append(P("some text")) +# html = Html(body) + +# file_contents = render_result(html) + +# print(file_contents) +# lines = file_contents.split("\n") +# for i in range(3): # this needed to be adapted to the tag +# assert lines[i + 1].startswith(i * Element.indent + "<") + +# assert lines[4].startswith(3 * Element.indent + "some") + + +# def test_element_indent1(): +# """ +# Tests whether the Element indents at least simple content + +# we are expecting to to look like this: + +# +# this is some text +# <\html> + +# More complex indentation should be tested later. +# """ +# e = Element("this is some text") + +# # This uses the render_results utility above +# file_contents = render_result(e).strip() + +# # making sure the content got in there. +# assert("this is some text") in file_contents + +# # break into lines to check indentation +# lines = file_contents.split('\n') +# # making sure the opening and closing tags are right. +# assert lines[0] == "" +# # this line should be indented by the amount specified +# # by the class attribute: "indent" +# assert lines[1].startswith(Element.indent + "thi") +# assert lines[2] == "" +# assert file_contents.endswith("") diff --git a/_downloads/8c937698ccbd5754c61e94a252163987/series_template.py b/_downloads/8c937698ccbd5754c61e94a252163987/series_template.py new file mode 100644 index 0000000..fb161d7 --- /dev/null +++ b/_downloads/8c937698ccbd5754c61e94a252163987/series_template.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 + +""" +a template for the series assignment +""" + + +def fibonacci(n): + """ compute the nth Fibonacci number """ + pass + + +def lucas(n): + """ compute the nth Lucas number """ + pass + + +def sum_series(n, n0=0, n1=1): + """ + compute the nth value of a summation series. + + :param n0=0: value of zeroth element in the series + :param n1=1: value of first element in the series + + This function should generalize the fibonacci() and the lucas(), + so that this function works for any first two numbers for a sum series. + Once generalized that way, sum_series(n, 0, 1) should be equivalent to fibonacci(n). + And sum_series(n, 2, 1) should be equivalent to lucas(n). + + sum_series(n, 3, 2) should generate antoehr series with no specific name + + The defaults are set to 0, 1, so if you don't pass in any values, you'll + get the fibonacci sercies + """ + pass + +if __name__ == "__main__": + # run some tests + assert fibonacci(0) == 0 + assert fibonacci(1) == 1 + assert fibonacci(2) == 1 + assert fibonacci(3) == 2 + assert fibonacci(4) == 3 + assert fibonacci(5) == 5 + assert fibonacci(6) == 8 + assert fibonacci(7) == 13 + + assert lucas(0) == 2 + assert lucas(1) == 1 + + assert lucas(4) == 7 + + # test that sum_series matches fibonacci + assert sum_series(5) == fibonacci(5) + assert sum_series(7, 0, 1) == fibonacci(7) + + # test if sum_series matched lucas + assert sum_series(5, 2, 1) == lucas(5) + + # test if sum_series works for arbitrary initial values + assert sum_series(0, 3, 2) == 3 + assert sum_series(1, 3, 2) == 2 + assert sum_series(2, 3, 2) == 5 + assert sum_series(3, 3, 2) == 7 + assert sum_series(4, 3, 2) == 12 + assert sum_series(5, 3, 2) == 19 + + print("tests passed") diff --git a/_downloads/8cfc8668d4a488ede3e3281cb5fcd659/ICanEatGlass.utf8.txt b/_downloads/8cfc8668d4a488ede3e3281cb5fcd659/ICanEatGlass.utf8.txt new file mode 100644 index 0000000..9ecba2b --- /dev/null +++ b/_downloads/8cfc8668d4a488ede3e3281cb5fcd659/ICanEatGlass.utf8.txt @@ -0,0 +1,23 @@ +I Can Eat Glass: + +And from the sublime to the ridiculous, here is a certain phrase in an assortment of languages: + +Sanskrit: काचं शक्नोम्यत्तुम् । नोपहिनस्ति माम् ॥ + +Sanskrit (standard transcription): kācaṃ śaknomyattum; nopahinasti mām. + +Classical Greek: ὕαλον ϕαγεῖν δύναμαι· τοῦτο οὔ με βλάπτει. + +Greek (monotonic): Μπορώ να φάω σπασμένα γυαλιά χωρίς να πάθω τίποτα. + +Greek (polytonic): Μπορῶ νὰ φάω σπασμένα γυαλιὰ χωρὶς νὰ πάθω τίποτα. + +Latin: Vitrum edere possum; mihi non nocet. + +Old French: Je puis mangier del voirre. Ne me nuit. + +French: Je peux manger du verre, ça ne me fait pas mal. + +Provençal / Occitan: Pòdi manjar de veire, me nafrariá pas. + +Québécois: J'peux manger d'la vitre, ça m'fa pas mal. \ No newline at end of file diff --git a/_downloads/8e374f8484ebc266a6df2ce6add342bf/test_html_output7.html b/_downloads/8e374f8484ebc266a6df2ce6add342bf/test_html_output7.html new file mode 100644 index 0000000..19cfc41 --- /dev/null +++ b/_downloads/8e374f8484ebc266a6df2ce6add342bf/test_html_output7.html @@ -0,0 +1,25 @@ + + +PythonClass = Revision 1087: + + +

PythonClass - Class 6 example

+

+Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+
+
    +
  • +The first item in a list +
  • +
  • +This is the second item +
  • +
  • +And this is a +link +to google +
  • +
+ + \ No newline at end of file diff --git a/_downloads/923431060a2f16b7b6290be15c61e559/object_canvas.py b/_downloads/923431060a2f16b7b6290be15c61e559/object_canvas.py new file mode 100644 index 0000000..8c28631 --- /dev/null +++ b/_downloads/923431060a2f16b7b6290be15c61e559/object_canvas.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python + +""" +object canvas: an example of multiple inheritance and mix-ins + +This is a simplified version of FloatCanvas -- an extension to the +wxPython desktop GUI library + +FloatCanvas is a system for handling zoomable and scalable graphics in +a object-persistant way. That is, graphic objects like circles and +rectangles and what not can be created ahead of time, and then the Canvas +can render them accoding to the current zoom level and pan position, etc. + +This lets the user think about their graphics object should look like, +and not have to worry about exactly how to draw them -- or their pixel +coordinates, or anything else. + +If you want to see all this in all its full complexity, the FloatCanvas +code in part of the wxPython project, and can be seen here: + +https://github.com/wxWidgets/Phoenix/tree/master/wx/lib/floatcanvas + +This code: object_canvas is a simplified version. It doesn't allow scaling +or zooming, and only renders in pixel coordinates. But it does allow +object-persistance, and is a nice demo of the use of mixins. + +This version requires the Python Imaging Library to do the rendering. + +You can get it by installing the "pillow" package from PyPi: + +python -m pip install pillow + +Its docs are here: + +https://pillow.readthedocs.io/en/4.3.x/index.html + +""" +from math import ceil + +from PIL import Image, ImageDraw + + +class ObjectCanvas(): + """ + An object-oriented canvas for drawing things + """ + + def __init__(self, + size=(500, 500), + background=(255, 255, 255, 0) + ): + self.size = size + self.draw_objects = [] + self.background = background + + def add_object(self, draw_object, position="top"): + #fixme: maybe overload the inplace addition operator? + """ + Add a new object to the canvas. + + :param: draw_object -- DrawObject to add + + :param position="top": Position to add the object. "top" puts + the object on top of teh other objects. + "bottom" puts them on the bottom of the stack. + A integer puts it in that place in the order + -- 0 is the bottom. + """ + if position == "top": + self.draw_objects.append(draw_object) + elif position == "bottom": + self.draw_objects.insert(0, draw_object) + else: + self.draw_objects.insert(position, draw_object) + + def render(self, filename): + """ + render the drawing to a file with the given name + """ + image = Image.new('RGBA', self.size, color=self.background) + drawer = ImageDraw.Draw(image) + + for do in self.draw_objects: + do.draw(drawer) + image.save(filename) + + +class DrawObject: + """ + base class for all draw objects + """ + + def __init__(self, *args, **kwargs): + print("in DrawObject __init__", kwargs) + # do nothing, but to make super happy + super().__init__(*args, **kwargs) + + +class LineObject: + """ + mixin for classes with a line + """ + + def __init__(self, + line_color='black', + line_width=1, + **kwargs, + ): + print("in LineObject __init__", kwargs) + super().__init__(**kwargs) + self.line_color = line_color + self.line_width = line_width + + +class FillObject: + """ + mixin for classes with a fill + """ + + def __init__(self, + fill_color=None, + **kwargs + ): + print("in FillObject __init__", kwargs) + self.fill_color = fill_color + + +class PolyLine(DrawObject, LineObject): + def __init__(self, + vertices, + **kwargs + ): + self.vertices = vertices + print("in PolyLine init", kwargs) + super().__init__(**kwargs) + + def draw(self, drawer): + """ + draw the object + + :param drawer: PIL.ImageDraw object to draw to + """ + drawer.line(self.vertices, fill=self.line_color, width=self.line_width) + + +class Circle(DrawObject, LineObject, FillObject): + def __init__(self, center, diameter, **kwargs): + self.center = center + self.diameter = diameter + super().__init__(**kwargs) + + def draw(self, drawer): + """ + Draw the object + :param drawer: PIL.ImageDraw object to draw to + """ + r = self.diameter // 2 + c = self.center + # PIL doesn't support different line widths for ellipses, + # so we fake it. + lw2 = self.line_width / 2 + bounds = ((c[0] - r, c[1] - r), (c[0] + r, c[1] + r)) + drawer.ellipse(bounds, fill=self.fill_color, outline=None) + for i in range(int(ceil(lw2)), int(-lw2), -1): + r = self.diameter // 2 + i + bounds = ((c[0] - r, c[1] - r), (c[0] + r, c[1] + r)) + drawer.ellipse(bounds, fill=None, outline=self.line_color) + + + + + + diff --git a/_downloads/935147e1e30f8dd1ba3ae3daa7690a35/students.txt b/_downloads/935147e1e30f8dd1ba3ae3daa7690a35/students.txt new file mode 100644 index 0000000..a85853a --- /dev/null +++ b/_downloads/935147e1e30f8dd1ba3ae3daa7690a35/students.txt @@ -0,0 +1,31 @@ +Name: Nickname, languages +Swift, Taylor: python, java, perl +Swift, Samuel: Sam +Brooks, Garth: fortran, java, matlab, bash +Buble, Michael: python, powershell +Stefani, Gwen: c#, javascript, python, typescript +Gaye, Marvin: c++, java +Jagger, Michael: Mick, shell, python +Lennon, John: +Nelson, Willy: python, java +McCartney, Paul: nothing +Dylan, Bob: java, python, ruby +Springsteen, Bruce: c++, python +Jackson, Michael: java, c#, python +Berry, Charles: Chuck c++, matlab, +Wonder, Steven: Stevie, c, perl, java, erlang, python +Nicks, Stevie: java, perl, c#, c++, python +Turner, Tina: bash, python +Plant, Robert: Bob, bash, python, ansible +Townshend, Peter: Pete, c#, powershell, python, sql +Moon, Keith: python, r, visualbasic +Mitchell, Joni: php, mysql, python +Ramone, John: Johnny, rex, db +King, Carol: r +Waters, Muddy: perl, python +Star, Richard: Ringo, shell, python, vb +Smith, Patricia: Patti, python +Morrison, Jim: fortran, perl, sql, python +Marley, Robert: Bob, c, c++, lisp +Simon, Paul: bash, python, sql +Charles, Ray: Chuck, java, python diff --git a/_downloads/970a4ba5b81c58429b325a9b7d0cdb2f/property_ugly.py b/_downloads/970a4ba5b81c58429b325a9b7d0cdb2f/property_ugly.py new file mode 100644 index 0000000..fe65f35 --- /dev/null +++ b/_downloads/970a4ba5b81c58429b325a9b7d0cdb2f/property_ugly.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +class C: + """ + Property defined in about the most ugly way possible + """ + def __init__(self): + self._x = None + def x(self): + return self._x + x = property(x) + def _set_x(self, value): + self._x = value + x = x.setter(_set_x) + def _del_x(self): + del self._x + x = x.deleter(_del_x) + + diff --git a/_downloads/996cd867e92a78028143685b46419034/test_html_output8.html b/_downloads/996cd867e92a78028143685b46419034/test_html_output8.html new file mode 100644 index 0000000..3ae1841 --- /dev/null +++ b/_downloads/996cd867e92a78028143685b46419034/test_html_output8.html @@ -0,0 +1,27 @@ + + + + +PythonClass = Revision 1087: + + +

PythonClass - Class 6 example

+

+Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+
+
    +
  • +The first item in a list +
  • +
  • +This is the second item +
  • +
  • +And this is a +link +to google +
  • +
+ + \ No newline at end of file diff --git a/_downloads/99c7ae599bf6af9f50d2fa2ced3aeaa3/html_render.py b/_downloads/99c7ae599bf6af9f50d2fa2ced3aeaa3/html_render.py new file mode 100644 index 0000000..508400f --- /dev/null +++ b/_downloads/99c7ae599bf6af9f50d2fa2ced3aeaa3/html_render.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +""" +A class-based system for rendering html. +""" + + +# This is the framework for the base class +class Element(object): + + def __init__(self, content=None): + pass + + def append(self, new_content): + pass + + def render(self, out_file): + out_file.write("just something as a place holder...") diff --git a/_downloads/9eea9f00dc947ac0352b6f3ce01753df/listing1.py b/_downloads/9eea9f00dc947ac0352b6f3ce01753df/listing1.py new file mode 100644 index 0000000..e6f1014 --- /dev/null +++ b/_downloads/9eea9f00dc947ac0352b6f3ce01753df/listing1.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# coding: utf-8 +""" +""" + +import string +import string + + +module_variable = 0 + +float = 1.0 + +long = "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong" + + + + + +def functionName(self, int): + local = 5 + 5 + module_variable = 5*5 + return module_variable + +class my_class(object): + + def __init__(self, arg1, string): + self.value = True + return + + def method1(self, str): + self.s = str + return self.value + + def method2(self): + return + print('How did we get here?') + + def method1(self): + return self.value + 1 + method2 = method1 + +class my_subclass(my_class): + + def __init__(self, arg1, string): + self.value = arg1 + return + + + +class Food(object): + pass + +class Pizza(Food): + pass + +# test recommendations from http://legacy.python.org/dev/peps/pep-0008/#programming-recommendations + +# http://legacy.python.org/dev/peps/pep-0008/#constants +food = Food() +pizza = Pizza() + +print(type(food) == type(pizza)) +print(isinstance(food, Food)) +print(isinstance(pizza, Food)) + +# create a larger Cyclomatic complexity, error triggered with +# flake8 --max-complexity=5 +def f(x): + if x is 1: + return x + elif x is 2: + return x + elif x is 3: + return x + elif x is 4: + return x + elif x is 5: + return x + +print(f(5)) diff --git a/_downloads/9f634eee56ccdd2ea8c081c80013fb12/sherlock.txt b/_downloads/9f634eee56ccdd2ea8c081c80013fb12/sherlock.txt new file mode 100644 index 0000000..4dec201 --- /dev/null +++ b/_downloads/9f634eee56ccdd2ea8c081c80013fb12/sherlock.txt @@ -0,0 +1,13052 @@ +Project Gutenberg's The Adventures of Sherlock Holmes, by Arthur Conan Doyle + +This eBook is for the use of anyone anywhere at no cost and with +almost no restrictions whatsoever. You may copy it, give it away or +re-use it under the terms of the Project Gutenberg License included +with this eBook or online at www.gutenberg.net + + +Title: The Adventures of Sherlock Holmes + +Author: Arthur Conan Doyle + +Posting Date: April 18, 2011 [EBook #1661] +First Posted: November 29, 2002 + +Language: English + + +*** START OF THIS PROJECT GUTENBERG EBOOK THE ADVENTURES OF SHERLOCK HOLMES *** + + + + +Produced by an anonymous Project Gutenberg volunteer and Jose Menendez + + + + + + + + + +THE ADVENTURES OF SHERLOCK HOLMES + +by + +SIR ARTHUR CONAN DOYLE + + + + I. A Scandal in Bohemia + II. The Red-headed League + III. A Case of Identity + IV. The Boscombe Valley Mystery + V. The Five Orange Pips + VI. The Man with the Twisted Lip + VII. The Adventure of the Blue Carbuncle +VIII. The Adventure of the Speckled Band + IX. The Adventure of the Engineer's Thumb + X. The Adventure of the Noble Bachelor + XI. The Adventure of the Beryl Coronet + XII. The Adventure of the Copper Beeches + + + + +ADVENTURE I. A SCANDAL IN BOHEMIA + +I. + +To Sherlock Holmes she is always THE woman. I have seldom heard +him mention her under any other name. In his eyes she eclipses +and predominates the whole of her sex. It was not that he felt +any emotion akin to love for Irene Adler. All emotions, and that +one particularly, were abhorrent to his cold, precise but +admirably balanced mind. He was, I take it, the most perfect +reasoning and observing machine that the world has seen, but as a +lover he would have placed himself in a false position. He never +spoke of the softer passions, save with a gibe and a sneer. They +were admirable things for the observer--excellent for drawing the +veil from men's motives and actions. But for the trained reasoner +to admit such intrusions into his own delicate and finely +adjusted temperament was to introduce a distracting factor which +might throw a doubt upon all his mental results. Grit in a +sensitive instrument, or a crack in one of his own high-power +lenses, would not be more disturbing than a strong emotion in a +nature such as his. And yet there was but one woman to him, and +that woman was the late Irene Adler, of dubious and questionable +memory. + +I had seen little of Holmes lately. My marriage had drifted us +away from each other. My own complete happiness, and the +home-centred interests which rise up around the man who first +finds himself master of his own establishment, were sufficient to +absorb all my attention, while Holmes, who loathed every form of +society with his whole Bohemian soul, remained in our lodgings in +Baker Street, buried among his old books, and alternating from +week to week between cocaine and ambition, the drowsiness of the +drug, and the fierce energy of his own keen nature. He was still, +as ever, deeply attracted by the study of crime, and occupied his +immense faculties and extraordinary powers of observation in +following out those clues, and clearing up those mysteries which +had been abandoned as hopeless by the official police. From time +to time I heard some vague account of his doings: of his summons +to Odessa in the case of the Trepoff murder, of his clearing up +of the singular tragedy of the Atkinson brothers at Trincomalee, +and finally of the mission which he had accomplished so +delicately and successfully for the reigning family of Holland. +Beyond these signs of his activity, however, which I merely +shared with all the readers of the daily press, I knew little of +my former friend and companion. + +One night--it was on the twentieth of March, 1888--I was +returning from a journey to a patient (for I had now returned to +civil practice), when my way led me through Baker Street. As I +passed the well-remembered door, which must always be associated +in my mind with my wooing, and with the dark incidents of the +Study in Scarlet, I was seized with a keen desire to see Holmes +again, and to know how he was employing his extraordinary powers. +His rooms were brilliantly lit, and, even as I looked up, I saw +his tall, spare figure pass twice in a dark silhouette against +the blind. He was pacing the room swiftly, eagerly, with his head +sunk upon his chest and his hands clasped behind him. To me, who +knew his every mood and habit, his attitude and manner told their +own story. He was at work again. He had risen out of his +drug-created dreams and was hot upon the scent of some new +problem. I rang the bell and was shown up to the chamber which +had formerly been in part my own. + +His manner was not effusive. It seldom was; but he was glad, I +think, to see me. With hardly a word spoken, but with a kindly +eye, he waved me to an armchair, threw across his case of cigars, +and indicated a spirit case and a gasogene in the corner. Then he +stood before the fire and looked me over in his singular +introspective fashion. + +"Wedlock suits you," he remarked. "I think, Watson, that you have +put on seven and a half pounds since I saw you." + +"Seven!" I answered. + +"Indeed, I should have thought a little more. Just a trifle more, +I fancy, Watson. And in practice again, I observe. You did not +tell me that you intended to go into harness." + +"Then, how do you know?" + +"I see it, I deduce it. How do I know that you have been getting +yourself very wet lately, and that you have a most clumsy and +careless servant girl?" + +"My dear Holmes," said I, "this is too much. You would certainly +have been burned, had you lived a few centuries ago. It is true +that I had a country walk on Thursday and came home in a dreadful +mess, but as I have changed my clothes I can't imagine how you +deduce it. As to Mary Jane, she is incorrigible, and my wife has +given her notice, but there, again, I fail to see how you work it +out." + +He chuckled to himself and rubbed his long, nervous hands +together. + +"It is simplicity itself," said he; "my eyes tell me that on the +inside of your left shoe, just where the firelight strikes it, +the leather is scored by six almost parallel cuts. Obviously they +have been caused by someone who has very carelessly scraped round +the edges of the sole in order to remove crusted mud from it. +Hence, you see, my double deduction that you had been out in vile +weather, and that you had a particularly malignant boot-slitting +specimen of the London slavey. As to your practice, if a +gentleman walks into my rooms smelling of iodoform, with a black +mark of nitrate of silver upon his right forefinger, and a bulge +on the right side of his top-hat to show where he has secreted +his stethoscope, I must be dull, indeed, if I do not pronounce +him to be an active member of the medical profession." + +I could not help laughing at the ease with which he explained his +process of deduction. "When I hear you give your reasons," I +remarked, "the thing always appears to me to be so ridiculously +simple that I could easily do it myself, though at each +successive instance of your reasoning I am baffled until you +explain your process. And yet I believe that my eyes are as good +as yours." + +"Quite so," he answered, lighting a cigarette, and throwing +himself down into an armchair. "You see, but you do not observe. +The distinction is clear. For example, you have frequently seen +the steps which lead up from the hall to this room." + +"Frequently." + +"How often?" + +"Well, some hundreds of times." + +"Then how many are there?" + +"How many? I don't know." + +"Quite so! You have not observed. And yet you have seen. That is +just my point. Now, I know that there are seventeen steps, +because I have both seen and observed. By-the-way, since you are +interested in these little problems, and since you are good +enough to chronicle one or two of my trifling experiences, you +may be interested in this." He threw over a sheet of thick, +pink-tinted note-paper which had been lying open upon the table. +"It came by the last post," said he. "Read it aloud." + +The note was undated, and without either signature or address. + +"There will call upon you to-night, at a quarter to eight +o'clock," it said, "a gentleman who desires to consult you upon a +matter of the very deepest moment. Your recent services to one of +the royal houses of Europe have shown that you are one who may +safely be trusted with matters which are of an importance which +can hardly be exaggerated. This account of you we have from all +quarters received. Be in your chamber then at that hour, and do +not take it amiss if your visitor wear a mask." + +"This is indeed a mystery," I remarked. "What do you imagine that +it means?" + +"I have no data yet. It is a capital mistake to theorize before +one has data. Insensibly one begins to twist facts to suit +theories, instead of theories to suit facts. But the note itself. +What do you deduce from it?" + +I carefully examined the writing, and the paper upon which it was +written. + +"The man who wrote it was presumably well to do," I remarked, +endeavouring to imitate my companion's processes. "Such paper +could not be bought under half a crown a packet. It is peculiarly +strong and stiff." + +"Peculiar--that is the very word," said Holmes. "It is not an +English paper at all. Hold it up to the light." + +I did so, and saw a large "E" with a small "g," a "P," and a +large "G" with a small "t" woven into the texture of the paper. + +"What do you make of that?" asked Holmes. + +"The name of the maker, no doubt; or his monogram, rather." + +"Not at all. The 'G' with the small 't' stands for +'Gesellschaft,' which is the German for 'Company.' It is a +customary contraction like our 'Co.' 'P,' of course, stands for +'Papier.' Now for the 'Eg.' Let us glance at our Continental +Gazetteer." He took down a heavy brown volume from his shelves. +"Eglow, Eglonitz--here we are, Egria. It is in a German-speaking +country--in Bohemia, not far from Carlsbad. 'Remarkable as being +the scene of the death of Wallenstein, and for its numerous +glass-factories and paper-mills.' Ha, ha, my boy, what do you +make of that?" His eyes sparkled, and he sent up a great blue +triumphant cloud from his cigarette. + +"The paper was made in Bohemia," I said. + +"Precisely. And the man who wrote the note is a German. Do you +note the peculiar construction of the sentence--'This account of +you we have from all quarters received.' A Frenchman or Russian +could not have written that. It is the German who is so +uncourteous to his verbs. It only remains, therefore, to discover +what is wanted by this German who writes upon Bohemian paper and +prefers wearing a mask to showing his face. And here he comes, if +I am not mistaken, to resolve all our doubts." + +As he spoke there was the sharp sound of horses' hoofs and +grating wheels against the curb, followed by a sharp pull at the +bell. Holmes whistled. + +"A pair, by the sound," said he. "Yes," he continued, glancing +out of the window. "A nice little brougham and a pair of +beauties. A hundred and fifty guineas apiece. There's money in +this case, Watson, if there is nothing else." + +"I think that I had better go, Holmes." + +"Not a bit, Doctor. Stay where you are. I am lost without my +Boswell. And this promises to be interesting. It would be a pity +to miss it." + +"But your client--" + +"Never mind him. I may want your help, and so may he. Here he +comes. Sit down in that armchair, Doctor, and give us your best +attention." + +A slow and heavy step, which had been heard upon the stairs and +in the passage, paused immediately outside the door. Then there +was a loud and authoritative tap. + +"Come in!" said Holmes. + +A man entered who could hardly have been less than six feet six +inches in height, with the chest and limbs of a Hercules. His +dress was rich with a richness which would, in England, be looked +upon as akin to bad taste. Heavy bands of astrakhan were slashed +across the sleeves and fronts of his double-breasted coat, while +the deep blue cloak which was thrown over his shoulders was lined +with flame-coloured silk and secured at the neck with a brooch +which consisted of a single flaming beryl. Boots which extended +halfway up his calves, and which were trimmed at the tops with +rich brown fur, completed the impression of barbaric opulence +which was suggested by his whole appearance. He carried a +broad-brimmed hat in his hand, while he wore across the upper +part of his face, extending down past the cheekbones, a black +vizard mask, which he had apparently adjusted that very moment, +for his hand was still raised to it as he entered. From the lower +part of the face he appeared to be a man of strong character, +with a thick, hanging lip, and a long, straight chin suggestive +of resolution pushed to the length of obstinacy. + +"You had my note?" he asked with a deep harsh voice and a +strongly marked German accent. "I told you that I would call." He +looked from one to the other of us, as if uncertain which to +address. + +"Pray take a seat," said Holmes. "This is my friend and +colleague, Dr. Watson, who is occasionally good enough to help me +in my cases. Whom have I the honour to address?" + +"You may address me as the Count Von Kramm, a Bohemian nobleman. +I understand that this gentleman, your friend, is a man of honour +and discretion, whom I may trust with a matter of the most +extreme importance. If not, I should much prefer to communicate +with you alone." + +I rose to go, but Holmes caught me by the wrist and pushed me +back into my chair. "It is both, or none," said he. "You may say +before this gentleman anything which you may say to me." + +The Count shrugged his broad shoulders. "Then I must begin," said +he, "by binding you both to absolute secrecy for two years; at +the end of that time the matter will be of no importance. At +present it is not too much to say that it is of such weight it +may have an influence upon European history." + +"I promise," said Holmes. + +"And I." + +"You will excuse this mask," continued our strange visitor. "The +august person who employs me wishes his agent to be unknown to +you, and I may confess at once that the title by which I have +just called myself is not exactly my own." + +"I was aware of it," said Holmes dryly. + +"The circumstances are of great delicacy, and every precaution +has to be taken to quench what might grow to be an immense +scandal and seriously compromise one of the reigning families of +Europe. To speak plainly, the matter implicates the great House +of Ormstein, hereditary kings of Bohemia." + +"I was also aware of that," murmured Holmes, settling himself +down in his armchair and closing his eyes. + +Our visitor glanced with some apparent surprise at the languid, +lounging figure of the man who had been no doubt depicted to him +as the most incisive reasoner and most energetic agent in Europe. +Holmes slowly reopened his eyes and looked impatiently at his +gigantic client. + +"If your Majesty would condescend to state your case," he +remarked, "I should be better able to advise you." + +The man sprang from his chair and paced up and down the room in +uncontrollable agitation. Then, with a gesture of desperation, he +tore the mask from his face and hurled it upon the ground. "You +are right," he cried; "I am the King. Why should I attempt to +conceal it?" + +"Why, indeed?" murmured Holmes. "Your Majesty had not spoken +before I was aware that I was addressing Wilhelm Gottsreich +Sigismond von Ormstein, Grand Duke of Cassel-Felstein, and +hereditary King of Bohemia." + +"But you can understand," said our strange visitor, sitting down +once more and passing his hand over his high white forehead, "you +can understand that I am not accustomed to doing such business in +my own person. Yet the matter was so delicate that I could not +confide it to an agent without putting myself in his power. I +have come incognito from Prague for the purpose of consulting +you." + +"Then, pray consult," said Holmes, shutting his eyes once more. + +"The facts are briefly these: Some five years ago, during a +lengthy visit to Warsaw, I made the acquaintance of the well-known +adventuress, Irene Adler. The name is no doubt familiar to you." + +"Kindly look her up in my index, Doctor," murmured Holmes without +opening his eyes. For many years he had adopted a system of +docketing all paragraphs concerning men and things, so that it +was difficult to name a subject or a person on which he could not +at once furnish information. In this case I found her biography +sandwiched in between that of a Hebrew rabbi and that of a +staff-commander who had written a monograph upon the deep-sea +fishes. + +"Let me see!" said Holmes. "Hum! Born in New Jersey in the year +1858. Contralto--hum! La Scala, hum! Prima donna Imperial Opera +of Warsaw--yes! Retired from operatic stage--ha! Living in +London--quite so! Your Majesty, as I understand, became entangled +with this young person, wrote her some compromising letters, and +is now desirous of getting those letters back." + +"Precisely so. But how--" + +"Was there a secret marriage?" + +"None." + +"No legal papers or certificates?" + +"None." + +"Then I fail to follow your Majesty. If this young person should +produce her letters for blackmailing or other purposes, how is +she to prove their authenticity?" + +"There is the writing." + +"Pooh, pooh! Forgery." + +"My private note-paper." + +"Stolen." + +"My own seal." + +"Imitated." + +"My photograph." + +"Bought." + +"We were both in the photograph." + +"Oh, dear! That is very bad! Your Majesty has indeed committed an +indiscretion." + +"I was mad--insane." + +"You have compromised yourself seriously." + +"I was only Crown Prince then. I was young. I am but thirty now." + +"It must be recovered." + +"We have tried and failed." + +"Your Majesty must pay. It must be bought." + +"She will not sell." + +"Stolen, then." + +"Five attempts have been made. Twice burglars in my pay ransacked +her house. Once we diverted her luggage when she travelled. Twice +she has been waylaid. There has been no result." + +"No sign of it?" + +"Absolutely none." + +Holmes laughed. "It is quite a pretty little problem," said he. + +"But a very serious one to me," returned the King reproachfully. + +"Very, indeed. And what does she propose to do with the +photograph?" + +"To ruin me." + +"But how?" + +"I am about to be married." + +"So I have heard." + +"To Clotilde Lothman von Saxe-Meningen, second daughter of the +King of Scandinavia. You may know the strict principles of her +family. She is herself the very soul of delicacy. A shadow of a +doubt as to my conduct would bring the matter to an end." + +"And Irene Adler?" + +"Threatens to send them the photograph. And she will do it. I +know that she will do it. You do not know her, but she has a soul +of steel. She has the face of the most beautiful of women, and +the mind of the most resolute of men. Rather than I should marry +another woman, there are no lengths to which she would not +go--none." + +"You are sure that she has not sent it yet?" + +"I am sure." + +"And why?" + +"Because she has said that she would send it on the day when the +betrothal was publicly proclaimed. That will be next Monday." + +"Oh, then we have three days yet," said Holmes with a yawn. "That +is very fortunate, as I have one or two matters of importance to +look into just at present. Your Majesty will, of course, stay in +London for the present?" + +"Certainly. You will find me at the Langham under the name of the +Count Von Kramm." + +"Then I shall drop you a line to let you know how we progress." + +"Pray do so. I shall be all anxiety." + +"Then, as to money?" + +"You have carte blanche." + +"Absolutely?" + +"I tell you that I would give one of the provinces of my kingdom +to have that photograph." + +"And for present expenses?" + +The King took a heavy chamois leather bag from under his cloak +and laid it on the table. + +"There are three hundred pounds in gold and seven hundred in +notes," he said. + +Holmes scribbled a receipt upon a sheet of his note-book and +handed it to him. + +"And Mademoiselle's address?" he asked. + +"Is Briony Lodge, Serpentine Avenue, St. John's Wood." + +Holmes took a note of it. "One other question," said he. "Was the +photograph a cabinet?" + +"It was." + +"Then, good-night, your Majesty, and I trust that we shall soon +have some good news for you. And good-night, Watson," he added, +as the wheels of the royal brougham rolled down the street. "If +you will be good enough to call to-morrow afternoon at three +o'clock I should like to chat this little matter over with you." + + +II. + +At three o'clock precisely I was at Baker Street, but Holmes had +not yet returned. The landlady informed me that he had left the +house shortly after eight o'clock in the morning. I sat down +beside the fire, however, with the intention of awaiting him, +however long he might be. I was already deeply interested in his +inquiry, for, though it was surrounded by none of the grim and +strange features which were associated with the two crimes which +I have already recorded, still, the nature of the case and the +exalted station of his client gave it a character of its own. +Indeed, apart from the nature of the investigation which my +friend had on hand, there was something in his masterly grasp of +a situation, and his keen, incisive reasoning, which made it a +pleasure to me to study his system of work, and to follow the +quick, subtle methods by which he disentangled the most +inextricable mysteries. So accustomed was I to his invariable +success that the very possibility of his failing had ceased to +enter into my head. + +It was close upon four before the door opened, and a +drunken-looking groom, ill-kempt and side-whiskered, with an +inflamed face and disreputable clothes, walked into the room. +Accustomed as I was to my friend's amazing powers in the use of +disguises, I had to look three times before I was certain that it +was indeed he. With a nod he vanished into the bedroom, whence he +emerged in five minutes tweed-suited and respectable, as of old. +Putting his hands into his pockets, he stretched out his legs in +front of the fire and laughed heartily for some minutes. + +"Well, really!" he cried, and then he choked and laughed again +until he was obliged to lie back, limp and helpless, in the +chair. + +"What is it?" + +"It's quite too funny. I am sure you could never guess how I +employed my morning, or what I ended by doing." + +"I can't imagine. I suppose that you have been watching the +habits, and perhaps the house, of Miss Irene Adler." + +"Quite so; but the sequel was rather unusual. I will tell you, +however. I left the house a little after eight o'clock this +morning in the character of a groom out of work. There is a +wonderful sympathy and freemasonry among horsey men. Be one of +them, and you will know all that there is to know. I soon found +Briony Lodge. It is a bijou villa, with a garden at the back, but +built out in front right up to the road, two stories. Chubb lock +to the door. Large sitting-room on the right side, well +furnished, with long windows almost to the floor, and those +preposterous English window fasteners which a child could open. +Behind there was nothing remarkable, save that the passage window +could be reached from the top of the coach-house. I walked round +it and examined it closely from every point of view, but without +noting anything else of interest. + +"I then lounged down the street and found, as I expected, that +there was a mews in a lane which runs down by one wall of the +garden. I lent the ostlers a hand in rubbing down their horses, +and received in exchange twopence, a glass of half and half, two +fills of shag tobacco, and as much information as I could desire +about Miss Adler, to say nothing of half a dozen other people in +the neighbourhood in whom I was not in the least interested, but +whose biographies I was compelled to listen to." + +"And what of Irene Adler?" I asked. + +"Oh, she has turned all the men's heads down in that part. She is +the daintiest thing under a bonnet on this planet. So say the +Serpentine-mews, to a man. She lives quietly, sings at concerts, +drives out at five every day, and returns at seven sharp for +dinner. Seldom goes out at other times, except when she sings. +Has only one male visitor, but a good deal of him. He is dark, +handsome, and dashing, never calls less than once a day, and +often twice. He is a Mr. Godfrey Norton, of the Inner Temple. See +the advantages of a cabman as a confidant. They had driven him +home a dozen times from Serpentine-mews, and knew all about him. +When I had listened to all they had to tell, I began to walk up +and down near Briony Lodge once more, and to think over my plan +of campaign. + +"This Godfrey Norton was evidently an important factor in the +matter. He was a lawyer. That sounded ominous. What was the +relation between them, and what the object of his repeated +visits? Was she his client, his friend, or his mistress? If the +former, she had probably transferred the photograph to his +keeping. If the latter, it was less likely. On the issue of this +question depended whether I should continue my work at Briony +Lodge, or turn my attention to the gentleman's chambers in the +Temple. It was a delicate point, and it widened the field of my +inquiry. I fear that I bore you with these details, but I have to +let you see my little difficulties, if you are to understand the +situation." + +"I am following you closely," I answered. + +"I was still balancing the matter in my mind when a hansom cab +drove up to Briony Lodge, and a gentleman sprang out. He was a +remarkably handsome man, dark, aquiline, and moustached--evidently +the man of whom I had heard. He appeared to be in a +great hurry, shouted to the cabman to wait, and brushed past the +maid who opened the door with the air of a man who was thoroughly +at home. + +"He was in the house about half an hour, and I could catch +glimpses of him in the windows of the sitting-room, pacing up and +down, talking excitedly, and waving his arms. Of her I could see +nothing. Presently he emerged, looking even more flurried than +before. As he stepped up to the cab, he pulled a gold watch from +his pocket and looked at it earnestly, 'Drive like the devil,' he +shouted, 'first to Gross & Hankey's in Regent Street, and then to +the Church of St. Monica in the Edgeware Road. Half a guinea if +you do it in twenty minutes!' + +"Away they went, and I was just wondering whether I should not do +well to follow them when up the lane came a neat little landau, +the coachman with his coat only half-buttoned, and his tie under +his ear, while all the tags of his harness were sticking out of +the buckles. It hadn't pulled up before she shot out of the hall +door and into it. I only caught a glimpse of her at the moment, +but she was a lovely woman, with a face that a man might die for. + +"'The Church of St. Monica, John,' she cried, 'and half a +sovereign if you reach it in twenty minutes.' + +"This was quite too good to lose, Watson. I was just balancing +whether I should run for it, or whether I should perch behind her +landau when a cab came through the street. The driver looked +twice at such a shabby fare, but I jumped in before he could +object. 'The Church of St. Monica,' said I, 'and half a sovereign +if you reach it in twenty minutes.' It was twenty-five minutes to +twelve, and of course it was clear enough what was in the wind. + +"My cabby drove fast. I don't think I ever drove faster, but the +others were there before us. The cab and the landau with their +steaming horses were in front of the door when I arrived. I paid +the man and hurried into the church. There was not a soul there +save the two whom I had followed and a surpliced clergyman, who +seemed to be expostulating with them. They were all three +standing in a knot in front of the altar. I lounged up the side +aisle like any other idler who has dropped into a church. +Suddenly, to my surprise, the three at the altar faced round to +me, and Godfrey Norton came running as hard as he could towards +me. + +"'Thank God,' he cried. 'You'll do. Come! Come!' + +"'What then?' I asked. + +"'Come, man, come, only three minutes, or it won't be legal.' + +"I was half-dragged up to the altar, and before I knew where I was +I found myself mumbling responses which were whispered in my ear, +and vouching for things of which I knew nothing, and generally +assisting in the secure tying up of Irene Adler, spinster, to +Godfrey Norton, bachelor. It was all done in an instant, and +there was the gentleman thanking me on the one side and the lady +on the other, while the clergyman beamed on me in front. It was +the most preposterous position in which I ever found myself in my +life, and it was the thought of it that started me laughing just +now. It seems that there had been some informality about their +license, that the clergyman absolutely refused to marry them +without a witness of some sort, and that my lucky appearance +saved the bridegroom from having to sally out into the streets in +search of a best man. The bride gave me a sovereign, and I mean +to wear it on my watch-chain in memory of the occasion." + +"This is a very unexpected turn of affairs," said I; "and what +then?" + +"Well, I found my plans very seriously menaced. It looked as if +the pair might take an immediate departure, and so necessitate +very prompt and energetic measures on my part. At the church +door, however, they separated, he driving back to the Temple, and +she to her own house. 'I shall drive out in the park at five as +usual,' she said as she left him. I heard no more. They drove +away in different directions, and I went off to make my own +arrangements." + +"Which are?" + +"Some cold beef and a glass of beer," he answered, ringing the +bell. "I have been too busy to think of food, and I am likely to +be busier still this evening. By the way, Doctor, I shall want +your co-operation." + +"I shall be delighted." + +"You don't mind breaking the law?" + +"Not in the least." + +"Nor running a chance of arrest?" + +"Not in a good cause." + +"Oh, the cause is excellent!" + +"Then I am your man." + +"I was sure that I might rely on you." + +"But what is it you wish?" + +"When Mrs. Turner has brought in the tray I will make it clear to +you. Now," he said as he turned hungrily on the simple fare that +our landlady had provided, "I must discuss it while I eat, for I +have not much time. It is nearly five now. In two hours we must +be on the scene of action. Miss Irene, or Madame, rather, returns +from her drive at seven. We must be at Briony Lodge to meet her." + +"And what then?" + +"You must leave that to me. I have already arranged what is to +occur. There is only one point on which I must insist. You must +not interfere, come what may. You understand?" + +"I am to be neutral?" + +"To do nothing whatever. There will probably be some small +unpleasantness. Do not join in it. It will end in my being +conveyed into the house. Four or five minutes afterwards the +sitting-room window will open. You are to station yourself close +to that open window." + +"Yes." + +"You are to watch me, for I will be visible to you." + +"Yes." + +"And when I raise my hand--so--you will throw into the room what +I give you to throw, and will, at the same time, raise the cry of +fire. You quite follow me?" + +"Entirely." + +"It is nothing very formidable," he said, taking a long cigar-shaped +roll from his pocket. "It is an ordinary plumber's smoke-rocket, +fitted with a cap at either end to make it self-lighting. +Your task is confined to that. When you raise your cry of fire, +it will be taken up by quite a number of people. You may then +walk to the end of the street, and I will rejoin you in ten +minutes. I hope that I have made myself clear?" + +"I am to remain neutral, to get near the window, to watch you, +and at the signal to throw in this object, then to raise the cry +of fire, and to wait you at the corner of the street." + +"Precisely." + +"Then you may entirely rely on me." + +"That is excellent. I think, perhaps, it is almost time that I +prepare for the new role I have to play." + +He disappeared into his bedroom and returned in a few minutes in +the character of an amiable and simple-minded Nonconformist +clergyman. His broad black hat, his baggy trousers, his white +tie, his sympathetic smile, and general look of peering and +benevolent curiosity were such as Mr. John Hare alone could have +equalled. It was not merely that Holmes changed his costume. His +expression, his manner, his very soul seemed to vary with every +fresh part that he assumed. The stage lost a fine actor, even as +science lost an acute reasoner, when he became a specialist in +crime. + +It was a quarter past six when we left Baker Street, and it still +wanted ten minutes to the hour when we found ourselves in +Serpentine Avenue. It was already dusk, and the lamps were just +being lighted as we paced up and down in front of Briony Lodge, +waiting for the coming of its occupant. The house was just such +as I had pictured it from Sherlock Holmes' succinct description, +but the locality appeared to be less private than I expected. On +the contrary, for a small street in a quiet neighbourhood, it was +remarkably animated. There was a group of shabbily dressed men +smoking and laughing in a corner, a scissors-grinder with his +wheel, two guardsmen who were flirting with a nurse-girl, and +several well-dressed young men who were lounging up and down with +cigars in their mouths. + +"You see," remarked Holmes, as we paced to and fro in front of +the house, "this marriage rather simplifies matters. The +photograph becomes a double-edged weapon now. The chances are +that she would be as averse to its being seen by Mr. Godfrey +Norton, as our client is to its coming to the eyes of his +princess. Now the question is, Where are we to find the +photograph?" + +"Where, indeed?" + +"It is most unlikely that she carries it about with her. It is +cabinet size. Too large for easy concealment about a woman's +dress. She knows that the King is capable of having her waylaid +and searched. Two attempts of the sort have already been made. We +may take it, then, that she does not carry it about with her." + +"Where, then?" + +"Her banker or her lawyer. There is that double possibility. But +I am inclined to think neither. Women are naturally secretive, +and they like to do their own secreting. Why should she hand it +over to anyone else? She could trust her own guardianship, but +she could not tell what indirect or political influence might be +brought to bear upon a business man. Besides, remember that she +had resolved to use it within a few days. It must be where she +can lay her hands upon it. It must be in her own house." + +"But it has twice been burgled." + +"Pshaw! They did not know how to look." + +"But how will you look?" + +"I will not look." + +"What then?" + +"I will get her to show me." + +"But she will refuse." + +"She will not be able to. But I hear the rumble of wheels. It is +her carriage. Now carry out my orders to the letter." + +As he spoke the gleam of the side-lights of a carriage came round +the curve of the avenue. It was a smart little landau which +rattled up to the door of Briony Lodge. As it pulled up, one of +the loafing men at the corner dashed forward to open the door in +the hope of earning a copper, but was elbowed away by another +loafer, who had rushed up with the same intention. A fierce +quarrel broke out, which was increased by the two guardsmen, who +took sides with one of the loungers, and by the scissors-grinder, +who was equally hot upon the other side. A blow was struck, and +in an instant the lady, who had stepped from her carriage, was +the centre of a little knot of flushed and struggling men, who +struck savagely at each other with their fists and sticks. Holmes +dashed into the crowd to protect the lady; but just as he reached +her he gave a cry and dropped to the ground, with the blood +running freely down his face. At his fall the guardsmen took to +their heels in one direction and the loungers in the other, while +a number of better-dressed people, who had watched the scuffle +without taking part in it, crowded in to help the lady and to +attend to the injured man. Irene Adler, as I will still call her, +had hurried up the steps; but she stood at the top with her +superb figure outlined against the lights of the hall, looking +back into the street. + +"Is the poor gentleman much hurt?" she asked. + +"He is dead," cried several voices. + +"No, no, there's life in him!" shouted another. "But he'll be +gone before you can get him to hospital." + +"He's a brave fellow," said a woman. "They would have had the +lady's purse and watch if it hadn't been for him. They were a +gang, and a rough one, too. Ah, he's breathing now." + +"He can't lie in the street. May we bring him in, marm?" + +"Surely. Bring him into the sitting-room. There is a comfortable +sofa. This way, please!" + +Slowly and solemnly he was borne into Briony Lodge and laid out +in the principal room, while I still observed the proceedings +from my post by the window. The lamps had been lit, but the +blinds had not been drawn, so that I could see Holmes as he lay +upon the couch. I do not know whether he was seized with +compunction at that moment for the part he was playing, but I +know that I never felt more heartily ashamed of myself in my life +than when I saw the beautiful creature against whom I was +conspiring, or the grace and kindliness with which she waited +upon the injured man. And yet it would be the blackest treachery +to Holmes to draw back now from the part which he had intrusted +to me. I hardened my heart, and took the smoke-rocket from under +my ulster. After all, I thought, we are not injuring her. We are +but preventing her from injuring another. + +Holmes had sat up upon the couch, and I saw him motion like a man +who is in need of air. A maid rushed across and threw open the +window. At the same instant I saw him raise his hand and at the +signal I tossed my rocket into the room with a cry of "Fire!" The +word was no sooner out of my mouth than the whole crowd of +spectators, well dressed and ill--gentlemen, ostlers, and +servant-maids--joined in a general shriek of "Fire!" Thick clouds +of smoke curled through the room and out at the open window. I +caught a glimpse of rushing figures, and a moment later the voice +of Holmes from within assuring them that it was a false alarm. +Slipping through the shouting crowd I made my way to the corner +of the street, and in ten minutes was rejoiced to find my +friend's arm in mine, and to get away from the scene of uproar. +He walked swiftly and in silence for some few minutes until we +had turned down one of the quiet streets which lead towards the +Edgeware Road. + +"You did it very nicely, Doctor," he remarked. "Nothing could +have been better. It is all right." + +"You have the photograph?" + +"I know where it is." + +"And how did you find out?" + +"She showed me, as I told you she would." + +"I am still in the dark." + +"I do not wish to make a mystery," said he, laughing. "The matter +was perfectly simple. You, of course, saw that everyone in the +street was an accomplice. They were all engaged for the evening." + +"I guessed as much." + +"Then, when the row broke out, I had a little moist red paint in +the palm of my hand. I rushed forward, fell down, clapped my hand +to my face, and became a piteous spectacle. It is an old trick." + +"That also I could fathom." + +"Then they carried me in. She was bound to have me in. What else +could she do? And into her sitting-room, which was the very room +which I suspected. It lay between that and her bedroom, and I was +determined to see which. They laid me on a couch, I motioned for +air, they were compelled to open the window, and you had your +chance." + +"How did that help you?" + +"It was all-important. When a woman thinks that her house is on +fire, her instinct is at once to rush to the thing which she +values most. It is a perfectly overpowering impulse, and I have +more than once taken advantage of it. In the case of the +Darlington substitution scandal it was of use to me, and also in +the Arnsworth Castle business. A married woman grabs at her baby; +an unmarried one reaches for her jewel-box. Now it was clear to +me that our lady of to-day had nothing in the house more precious +to her than what we are in quest of. She would rush to secure it. +The alarm of fire was admirably done. The smoke and shouting were +enough to shake nerves of steel. She responded beautifully. The +photograph is in a recess behind a sliding panel just above the +right bell-pull. She was there in an instant, and I caught a +glimpse of it as she half-drew it out. When I cried out that it +was a false alarm, she replaced it, glanced at the rocket, rushed +from the room, and I have not seen her since. I rose, and, making +my excuses, escaped from the house. I hesitated whether to +attempt to secure the photograph at once; but the coachman had +come in, and as he was watching me narrowly it seemed safer to +wait. A little over-precipitance may ruin all." + +"And now?" I asked. + +"Our quest is practically finished. I shall call with the King +to-morrow, and with you, if you care to come with us. We will be +shown into the sitting-room to wait for the lady, but it is +probable that when she comes she may find neither us nor the +photograph. It might be a satisfaction to his Majesty to regain +it with his own hands." + +"And when will you call?" + +"At eight in the morning. She will not be up, so that we shall +have a clear field. Besides, we must be prompt, for this marriage +may mean a complete change in her life and habits. I must wire to +the King without delay." + +We had reached Baker Street and had stopped at the door. He was +searching his pockets for the key when someone passing said: + +"Good-night, Mister Sherlock Holmes." + +There were several people on the pavement at the time, but the +greeting appeared to come from a slim youth in an ulster who had +hurried by. + +"I've heard that voice before," said Holmes, staring down the +dimly lit street. "Now, I wonder who the deuce that could have +been." + + +III. + +I slept at Baker Street that night, and we were engaged upon our +toast and coffee in the morning when the King of Bohemia rushed +into the room. + +"You have really got it!" he cried, grasping Sherlock Holmes by +either shoulder and looking eagerly into his face. + +"Not yet." + +"But you have hopes?" + +"I have hopes." + +"Then, come. I am all impatience to be gone." + +"We must have a cab." + +"No, my brougham is waiting." + +"Then that will simplify matters." We descended and started off +once more for Briony Lodge. + +"Irene Adler is married," remarked Holmes. + +"Married! When?" + +"Yesterday." + +"But to whom?" + +"To an English lawyer named Norton." + +"But she could not love him." + +"I am in hopes that she does." + +"And why in hopes?" + +"Because it would spare your Majesty all fear of future +annoyance. If the lady loves her husband, she does not love your +Majesty. If she does not love your Majesty, there is no reason +why she should interfere with your Majesty's plan." + +"It is true. And yet--Well! I wish she had been of my own +station! What a queen she would have made!" He relapsed into a +moody silence, which was not broken until we drew up in +Serpentine Avenue. + +The door of Briony Lodge was open, and an elderly woman stood +upon the steps. She watched us with a sardonic eye as we stepped +from the brougham. + +"Mr. Sherlock Holmes, I believe?" said she. + +"I am Mr. Holmes," answered my companion, looking at her with a +questioning and rather startled gaze. + +"Indeed! My mistress told me that you were likely to call. She +left this morning with her husband by the 5:15 train from Charing +Cross for the Continent." + +"What!" Sherlock Holmes staggered back, white with chagrin and +surprise. "Do you mean that she has left England?" + +"Never to return." + +"And the papers?" asked the King hoarsely. "All is lost." + +"We shall see." He pushed past the servant and rushed into the +drawing-room, followed by the King and myself. The furniture was +scattered about in every direction, with dismantled shelves and +open drawers, as if the lady had hurriedly ransacked them before +her flight. Holmes rushed at the bell-pull, tore back a small +sliding shutter, and, plunging in his hand, pulled out a +photograph and a letter. The photograph was of Irene Adler +herself in evening dress, the letter was superscribed to +"Sherlock Holmes, Esq. To be left till called for." My friend +tore it open and we all three read it together. It was dated at +midnight of the preceding night and ran in this way: + +"MY DEAR MR. SHERLOCK HOLMES,--You really did it very well. You +took me in completely. Until after the alarm of fire, I had not a +suspicion. But then, when I found how I had betrayed myself, I +began to think. I had been warned against you months ago. I had +been told that if the King employed an agent it would certainly +be you. And your address had been given me. Yet, with all this, +you made me reveal what you wanted to know. Even after I became +suspicious, I found it hard to think evil of such a dear, kind +old clergyman. But, you know, I have been trained as an actress +myself. Male costume is nothing new to me. I often take advantage +of the freedom which it gives. I sent John, the coachman, to +watch you, ran up stairs, got into my walking-clothes, as I call +them, and came down just as you departed. + +"Well, I followed you to your door, and so made sure that I was +really an object of interest to the celebrated Mr. Sherlock +Holmes. Then I, rather imprudently, wished you good-night, and +started for the Temple to see my husband. + +"We both thought the best resource was flight, when pursued by +so formidable an antagonist; so you will find the nest empty when +you call to-morrow. As to the photograph, your client may rest in +peace. I love and am loved by a better man than he. The King may +do what he will without hindrance from one whom he has cruelly +wronged. I keep it only to safeguard myself, and to preserve a +weapon which will always secure me from any steps which he might +take in the future. I leave a photograph which he might care to +possess; and I remain, dear Mr. Sherlock Holmes, + + "Very truly yours, + "IRENE NORTON, ne ADLER." + +"What a woman--oh, what a woman!" cried the King of Bohemia, when +we had all three read this epistle. "Did I not tell you how quick +and resolute she was? Would she not have made an admirable queen? +Is it not a pity that she was not on my level?" + +"From what I have seen of the lady she seems indeed to be on a +very different level to your Majesty," said Holmes coldly. "I am +sorry that I have not been able to bring your Majesty's business +to a more successful conclusion." + +"On the contrary, my dear sir," cried the King; "nothing could be +more successful. I know that her word is inviolate. The +photograph is now as safe as if it were in the fire." + +"I am glad to hear your Majesty say so." + +"I am immensely indebted to you. Pray tell me in what way I can +reward you. This ring--" He slipped an emerald snake ring from +his finger and held it out upon the palm of his hand. + +"Your Majesty has something which I should value even more +highly," said Holmes. + +"You have but to name it." + +"This photograph!" + +The King stared at him in amazement. + +"Irene's photograph!" he cried. "Certainly, if you wish it." + +"I thank your Majesty. Then there is no more to be done in the +matter. I have the honour to wish you a very good-morning." He +bowed, and, turning away without observing the hand which the +King had stretched out to him, he set off in my company for his +chambers. + +And that was how a great scandal threatened to affect the kingdom +of Bohemia, and how the best plans of Mr. Sherlock Holmes were +beaten by a woman's wit. He used to make merry over the +cleverness of women, but I have not heard him do it of late. And +when he speaks of Irene Adler, or when he refers to her +photograph, it is always under the honourable title of the woman. + + + +ADVENTURE II. THE RED-HEADED LEAGUE + +I had called upon my friend, Mr. Sherlock Holmes, one day in the +autumn of last year and found him in deep conversation with a +very stout, florid-faced, elderly gentleman with fiery red hair. +With an apology for my intrusion, I was about to withdraw when +Holmes pulled me abruptly into the room and closed the door +behind me. + +"You could not possibly have come at a better time, my dear +Watson," he said cordially. + +"I was afraid that you were engaged." + +"So I am. Very much so." + +"Then I can wait in the next room." + +"Not at all. This gentleman, Mr. Wilson, has been my partner and +helper in many of my most successful cases, and I have no +doubt that he will be of the utmost use to me in yours also." + +The stout gentleman half rose from his chair and gave a bob of +greeting, with a quick little questioning glance from his small +fat-encircled eyes. + +"Try the settee," said Holmes, relapsing into his armchair and +putting his fingertips together, as was his custom when in +judicial moods. "I know, my dear Watson, that you share my love +of all that is bizarre and outside the conventions and humdrum +routine of everyday life. You have shown your relish for it by +the enthusiasm which has prompted you to chronicle, and, if you +will excuse my saying so, somewhat to embellish so many of my own +little adventures." + +"Your cases have indeed been of the greatest interest to me," I +observed. + +"You will remember that I remarked the other day, just before we +went into the very simple problem presented by Miss Mary +Sutherland, that for strange effects and extraordinary +combinations we must go to life itself, which is always far more +daring than any effort of the imagination." + +"A proposition which I took the liberty of doubting." + +"You did, Doctor, but none the less you must come round to my +view, for otherwise I shall keep on piling fact upon fact on you +until your reason breaks down under them and acknowledges me to +be right. Now, Mr. Jabez Wilson here has been good enough to call +upon me this morning, and to begin a narrative which promises to +be one of the most singular which I have listened to for some +time. You have heard me remark that the strangest and most unique +things are very often connected not with the larger but with the +smaller crimes, and occasionally, indeed, where there is room for +doubt whether any positive crime has been committed. As far as I +have heard it is impossible for me to say whether the present +case is an instance of crime or not, but the course of events is +certainly among the most singular that I have ever listened to. +Perhaps, Mr. Wilson, you would have the great kindness to +recommence your narrative. I ask you not merely because my friend +Dr. Watson has not heard the opening part but also because the +peculiar nature of the story makes me anxious to have every +possible detail from your lips. As a rule, when I have heard some +slight indication of the course of events, I am able to guide +myself by the thousands of other similar cases which occur to my +memory. In the present instance I am forced to admit that the +facts are, to the best of my belief, unique." + +The portly client puffed out his chest with an appearance of some +little pride and pulled a dirty and wrinkled newspaper from the +inside pocket of his greatcoat. As he glanced down the +advertisement column, with his head thrust forward and the paper +flattened out upon his knee, I took a good look at the man and +endeavoured, after the fashion of my companion, to read the +indications which might be presented by his dress or appearance. + +I did not gain very much, however, by my inspection. Our visitor +bore every mark of being an average commonplace British +tradesman, obese, pompous, and slow. He wore rather baggy grey +shepherd's check trousers, a not over-clean black frock-coat, +unbuttoned in the front, and a drab waistcoat with a heavy brassy +Albert chain, and a square pierced bit of metal dangling down as +an ornament. A frayed top-hat and a faded brown overcoat with a +wrinkled velvet collar lay upon a chair beside him. Altogether, +look as I would, there was nothing remarkable about the man save +his blazing red head, and the expression of extreme chagrin and +discontent upon his features. + +Sherlock Holmes' quick eye took in my occupation, and he shook +his head with a smile as he noticed my questioning glances. +"Beyond the obvious facts that he has at some time done manual +labour, that he takes snuff, that he is a Freemason, that he has +been in China, and that he has done a considerable amount of +writing lately, I can deduce nothing else." + +Mr. Jabez Wilson started up in his chair, with his forefinger +upon the paper, but his eyes upon my companion. + +"How, in the name of good-fortune, did you know all that, Mr. +Holmes?" he asked. "How did you know, for example, that I did +manual labour. It's as true as gospel, for I began as a ship's +carpenter." + +"Your hands, my dear sir. Your right hand is quite a size larger +than your left. You have worked with it, and the muscles are more +developed." + +"Well, the snuff, then, and the Freemasonry?" + +"I won't insult your intelligence by telling you how I read that, +especially as, rather against the strict rules of your order, you +use an arc-and-compass breastpin." + +"Ah, of course, I forgot that. But the writing?" + +"What else can be indicated by that right cuff so very shiny for +five inches, and the left one with the smooth patch near the +elbow where you rest it upon the desk?" + +"Well, but China?" + +"The fish that you have tattooed immediately above your right +wrist could only have been done in China. I have made a small +study of tattoo marks and have even contributed to the literature +of the subject. That trick of staining the fishes' scales of a +delicate pink is quite peculiar to China. When, in addition, I +see a Chinese coin hanging from your watch-chain, the matter +becomes even more simple." + +Mr. Jabez Wilson laughed heavily. "Well, I never!" said he. "I +thought at first that you had done something clever, but I see +that there was nothing in it, after all." + +"I begin to think, Watson," said Holmes, "that I make a mistake +in explaining. 'Omne ignotum pro magnifico,' you know, and my +poor little reputation, such as it is, will suffer shipwreck if I +am so candid. Can you not find the advertisement, Mr. Wilson?" + +"Yes, I have got it now," he answered with his thick red finger +planted halfway down the column. "Here it is. This is what began +it all. You just read it for yourself, sir." + +I took the paper from him and read as follows: + +"TO THE RED-HEADED LEAGUE: On account of the bequest of the late +Ezekiah Hopkins, of Lebanon, Pennsylvania, U. S. A., there is now +another vacancy open which entitles a member of the League to a +salary of 4 pounds a week for purely nominal services. All +red-headed men who are sound in body and mind and above the age +of twenty-one years, are eligible. Apply in person on Monday, at +eleven o'clock, to Duncan Ross, at the offices of the League, 7 +Pope's Court, Fleet Street." + +"What on earth does this mean?" I ejaculated after I had twice +read over the extraordinary announcement. + +Holmes chuckled and wriggled in his chair, as was his habit when +in high spirits. "It is a little off the beaten track, isn't it?" +said he. "And now, Mr. Wilson, off you go at scratch and tell us +all about yourself, your household, and the effect which this +advertisement had upon your fortunes. You will first make a note, +Doctor, of the paper and the date." + +"It is The Morning Chronicle of April 27, 1890. Just two months +ago." + +"Very good. Now, Mr. Wilson?" + +"Well, it is just as I have been telling you, Mr. Sherlock +Holmes," said Jabez Wilson, mopping his forehead; "I have a small +pawnbroker's business at Coburg Square, near the City. It's not a +very large affair, and of late years it has not done more than +just give me a living. I used to be able to keep two assistants, +but now I only keep one; and I would have a job to pay him but +that he is willing to come for half wages so as to learn the +business." + +"What is the name of this obliging youth?" asked Sherlock Holmes. + +"His name is Vincent Spaulding, and he's not such a youth, +either. It's hard to say his age. I should not wish a smarter +assistant, Mr. Holmes; and I know very well that he could better +himself and earn twice what I am able to give him. But, after +all, if he is satisfied, why should I put ideas in his head?" + +"Why, indeed? You seem most fortunate in having an employ who +comes under the full market price. It is not a common experience +among employers in this age. I don't know that your assistant is +not as remarkable as your advertisement." + +"Oh, he has his faults, too," said Mr. Wilson. "Never was such a +fellow for photography. Snapping away with a camera when he ought +to be improving his mind, and then diving down into the cellar +like a rabbit into its hole to develop his pictures. That is his +main fault, but on the whole he's a good worker. There's no vice +in him." + +"He is still with you, I presume?" + +"Yes, sir. He and a girl of fourteen, who does a bit of simple +cooking and keeps the place clean--that's all I have in the +house, for I am a widower and never had any family. We live very +quietly, sir, the three of us; and we keep a roof over our heads +and pay our debts, if we do nothing more. + +"The first thing that put us out was that advertisement. +Spaulding, he came down into the office just this day eight +weeks, with this very paper in his hand, and he says: + +"'I wish to the Lord, Mr. Wilson, that I was a red-headed man.' + +"'Why that?' I asks. + +"'Why,' says he, 'here's another vacancy on the League of the +Red-headed Men. It's worth quite a little fortune to any man who +gets it, and I understand that there are more vacancies than +there are men, so that the trustees are at their wits' end what +to do with the money. If my hair would only change colour, here's +a nice little crib all ready for me to step into.' + +"'Why, what is it, then?' I asked. You see, Mr. Holmes, I am a +very stay-at-home man, and as my business came to me instead of +my having to go to it, I was often weeks on end without putting +my foot over the door-mat. In that way I didn't know much of what +was going on outside, and I was always glad of a bit of news. + +"'Have you never heard of the League of the Red-headed Men?' he +asked with his eyes open. + +"'Never.' + +"'Why, I wonder at that, for you are eligible yourself for one +of the vacancies.' + +"'And what are they worth?' I asked. + +"'Oh, merely a couple of hundred a year, but the work is slight, +and it need not interfere very much with one's other +occupations.' + +"Well, you can easily think that that made me prick up my ears, +for the business has not been over-good for some years, and an +extra couple of hundred would have been very handy. + +"'Tell me all about it,' said I. + +"'Well,' said he, showing me the advertisement, 'you can see for +yourself that the League has a vacancy, and there is the address +where you should apply for particulars. As far as I can make out, +the League was founded by an American millionaire, Ezekiah +Hopkins, who was very peculiar in his ways. He was himself +red-headed, and he had a great sympathy for all red-headed men; +so when he died it was found that he had left his enormous +fortune in the hands of trustees, with instructions to apply the +interest to the providing of easy berths to men whose hair is of +that colour. From all I hear it is splendid pay and very little to +do.' + +"'But,' said I, 'there would be millions of red-headed men who +would apply.' + +"'Not so many as you might think,' he answered. 'You see it is +really confined to Londoners, and to grown men. This American had +started from London when he was young, and he wanted to do the +old town a good turn. Then, again, I have heard it is no use your +applying if your hair is light red, or dark red, or anything but +real bright, blazing, fiery red. Now, if you cared to apply, Mr. +Wilson, you would just walk in; but perhaps it would hardly be +worth your while to put yourself out of the way for the sake of a +few hundred pounds.' + +"Now, it is a fact, gentlemen, as you may see for yourselves, +that my hair is of a very full and rich tint, so that it seemed +to me that if there was to be any competition in the matter I +stood as good a chance as any man that I had ever met. Vincent +Spaulding seemed to know so much about it that I thought he might +prove useful, so I just ordered him to put up the shutters for +the day and to come right away with me. He was very willing to +have a holiday, so we shut the business up and started off for +the address that was given us in the advertisement. + +"I never hope to see such a sight as that again, Mr. Holmes. From +north, south, east, and west every man who had a shade of red in +his hair had tramped into the city to answer the advertisement. +Fleet Street was choked with red-headed folk, and Pope's Court +looked like a coster's orange barrow. I should not have thought +there were so many in the whole country as were brought together +by that single advertisement. Every shade of colour they +were--straw, lemon, orange, brick, Irish-setter, liver, clay; +but, as Spaulding said, there were not many who had the real +vivid flame-coloured tint. When I saw how many were waiting, I +would have given it up in despair; but Spaulding would not hear +of it. How he did it I could not imagine, but he pushed and +pulled and butted until he got me through the crowd, and right up +to the steps which led to the office. There was a double stream +upon the stair, some going up in hope, and some coming back +dejected; but we wedged in as well as we could and soon found +ourselves in the office." + +"Your experience has been a most entertaining one," remarked +Holmes as his client paused and refreshed his memory with a huge +pinch of snuff. "Pray continue your very interesting statement." + +"There was nothing in the office but a couple of wooden chairs +and a deal table, behind which sat a small man with a head that +was even redder than mine. He said a few words to each candidate +as he came up, and then he always managed to find some fault in +them which would disqualify them. Getting a vacancy did not seem +to be such a very easy matter, after all. However, when our turn +came the little man was much more favourable to me than to any of +the others, and he closed the door as we entered, so that he +might have a private word with us. + +"'This is Mr. Jabez Wilson,' said my assistant, 'and he is +willing to fill a vacancy in the League.' + +"'And he is admirably suited for it,' the other answered. 'He has +every requirement. I cannot recall when I have seen anything so +fine.' He took a step backward, cocked his head on one side, and +gazed at my hair until I felt quite bashful. Then suddenly he +plunged forward, wrung my hand, and congratulated me warmly on my +success. + +"'It would be injustice to hesitate,' said he. 'You will, +however, I am sure, excuse me for taking an obvious precaution.' +With that he seized my hair in both his hands, and tugged until I +yelled with the pain. 'There is water in your eyes,' said he as +he released me. 'I perceive that all is as it should be. But we +have to be careful, for we have twice been deceived by wigs and +once by paint. I could tell you tales of cobbler's wax which +would disgust you with human nature.' He stepped over to the +window and shouted through it at the top of his voice that the +vacancy was filled. A groan of disappointment came up from below, +and the folk all trooped away in different directions until there +was not a red-head to be seen except my own and that of the +manager. + +"'My name,' said he, 'is Mr. Duncan Ross, and I am myself one of +the pensioners upon the fund left by our noble benefactor. Are +you a married man, Mr. Wilson? Have you a family?' + +"I answered that I had not. + +"His face fell immediately. + +"'Dear me!' he said gravely, 'that is very serious indeed! I am +sorry to hear you say that. The fund was, of course, for the +propagation and spread of the red-heads as well as for their +maintenance. It is exceedingly unfortunate that you should be a +bachelor.' + +"My face lengthened at this, Mr. Holmes, for I thought that I was +not to have the vacancy after all; but after thinking it over for +a few minutes he said that it would be all right. + +"'In the case of another,' said he, 'the objection might be +fatal, but we must stretch a point in favour of a man with such a +head of hair as yours. When shall you be able to enter upon your +new duties?' + +"'Well, it is a little awkward, for I have a business already,' +said I. + +"'Oh, never mind about that, Mr. Wilson!' said Vincent Spaulding. +'I should be able to look after that for you.' + +"'What would be the hours?' I asked. + +"'Ten to two.' + +"Now a pawnbroker's business is mostly done of an evening, Mr. +Holmes, especially Thursday and Friday evening, which is just +before pay-day; so it would suit me very well to earn a little in +the mornings. Besides, I knew that my assistant was a good man, +and that he would see to anything that turned up. + +"'That would suit me very well,' said I. 'And the pay?' + +"'Is 4 pounds a week.' + +"'And the work?' + +"'Is purely nominal.' + +"'What do you call purely nominal?' + +"'Well, you have to be in the office, or at least in the +building, the whole time. If you leave, you forfeit your whole +position forever. The will is very clear upon that point. You +don't comply with the conditions if you budge from the office +during that time.' + +"'It's only four hours a day, and I should not think of leaving,' +said I. + +"'No excuse will avail,' said Mr. Duncan Ross; 'neither sickness +nor business nor anything else. There you must stay, or you lose +your billet.' + +"'And the work?' + +"'Is to copy out the "Encyclopaedia Britannica." There is the first +volume of it in that press. You must find your own ink, pens, and +blotting-paper, but we provide this table and chair. Will you be +ready to-morrow?' + +"'Certainly,' I answered. + +"'Then, good-bye, Mr. Jabez Wilson, and let me congratulate you +once more on the important position which you have been fortunate +enough to gain.' He bowed me out of the room and I went home with +my assistant, hardly knowing what to say or do, I was so pleased +at my own good fortune. + +"Well, I thought over the matter all day, and by evening I was in +low spirits again; for I had quite persuaded myself that the +whole affair must be some great hoax or fraud, though what its +object might be I could not imagine. It seemed altogether past +belief that anyone could make such a will, or that they would pay +such a sum for doing anything so simple as copying out the +'Encyclopaedia Britannica.' Vincent Spaulding did what he could to +cheer me up, but by bedtime I had reasoned myself out of the +whole thing. However, in the morning I determined to have a look +at it anyhow, so I bought a penny bottle of ink, and with a +quill-pen, and seven sheets of foolscap paper, I started off for +Pope's Court. + +"Well, to my surprise and delight, everything was as right as +possible. The table was set out ready for me, and Mr. Duncan Ross +was there to see that I got fairly to work. He started me off +upon the letter A, and then he left me; but he would drop in from +time to time to see that all was right with me. At two o'clock he +bade me good-day, complimented me upon the amount that I had +written, and locked the door of the office after me. + +"This went on day after day, Mr. Holmes, and on Saturday the +manager came in and planked down four golden sovereigns for my +week's work. It was the same next week, and the same the week +after. Every morning I was there at ten, and every afternoon I +left at two. By degrees Mr. Duncan Ross took to coming in only +once of a morning, and then, after a time, he did not come in at +all. Still, of course, I never dared to leave the room for an +instant, for I was not sure when he might come, and the billet +was such a good one, and suited me so well, that I would not risk +the loss of it. + +"Eight weeks passed away like this, and I had written about +Abbots and Archery and Armour and Architecture and Attica, and +hoped with diligence that I might get on to the B's before very +long. It cost me something in foolscap, and I had pretty nearly +filled a shelf with my writings. And then suddenly the whole +business came to an end." + +"To an end?" + +"Yes, sir. And no later than this morning. I went to my work as +usual at ten o'clock, but the door was shut and locked, with a +little square of cardboard hammered on to the middle of the +panel with a tack. Here it is, and you can read for yourself." + +He held up a piece of white cardboard about the size of a sheet +of note-paper. It read in this fashion: + + THE RED-HEADED LEAGUE + + IS + + DISSOLVED. + + October 9, 1890. + +Sherlock Holmes and I surveyed this curt announcement and the +rueful face behind it, until the comical side of the affair so +completely overtopped every other consideration that we both +burst out into a roar of laughter. + +"I cannot see that there is anything very funny," cried our +client, flushing up to the roots of his flaming head. "If you can +do nothing better than laugh at me, I can go elsewhere." + +"No, no," cried Holmes, shoving him back into the chair from +which he had half risen. "I really wouldn't miss your case for +the world. It is most refreshingly unusual. But there is, if you +will excuse my saying so, something just a little funny about it. +Pray what steps did you take when you found the card upon the +door?" + +"I was staggered, sir. I did not know what to do. Then I called +at the offices round, but none of them seemed to know anything +about it. Finally, I went to the landlord, who is an accountant +living on the ground-floor, and I asked him if he could tell me +what had become of the Red-headed League. He said that he had +never heard of any such body. Then I asked him who Mr. Duncan +Ross was. He answered that the name was new to him. + +"'Well,' said I, 'the gentleman at No. 4.' + +"'What, the red-headed man?' + +"'Yes.' + +"'Oh,' said he, 'his name was William Morris. He was a solicitor +and was using my room as a temporary convenience until his new +premises were ready. He moved out yesterday.' + +"'Where could I find him?' + +"'Oh, at his new offices. He did tell me the address. Yes, 17 +King Edward Street, near St. Paul's.' + +"I started off, Mr. Holmes, but when I got to that address it was +a manufactory of artificial knee-caps, and no one in it had ever +heard of either Mr. William Morris or Mr. Duncan Ross." + +"And what did you do then?" asked Holmes. + +"I went home to Saxe-Coburg Square, and I took the advice of my +assistant. But he could not help me in any way. He could only say +that if I waited I should hear by post. But that was not quite +good enough, Mr. Holmes. I did not wish to lose such a place +without a struggle, so, as I had heard that you were good enough +to give advice to poor folk who were in need of it, I came right +away to you." + +"And you did very wisely," said Holmes. "Your case is an +exceedingly remarkable one, and I shall be happy to look into it. +From what you have told me I think that it is possible that +graver issues hang from it than might at first sight appear." + +"Grave enough!" said Mr. Jabez Wilson. "Why, I have lost four +pound a week." + +"As far as you are personally concerned," remarked Holmes, "I do +not see that you have any grievance against this extraordinary +league. On the contrary, you are, as I understand, richer by some +30 pounds, to say nothing of the minute knowledge which you have +gained on every subject which comes under the letter A. You have +lost nothing by them." + +"No, sir. But I want to find out about them, and who they are, +and what their object was in playing this prank--if it was a +prank--upon me. It was a pretty expensive joke for them, for it +cost them two and thirty pounds." + +"We shall endeavour to clear up these points for you. And, first, +one or two questions, Mr. Wilson. This assistant of yours who +first called your attention to the advertisement--how long had he +been with you?" + +"About a month then." + +"How did he come?" + +"In answer to an advertisement." + +"Was he the only applicant?" + +"No, I had a dozen." + +"Why did you pick him?" + +"Because he was handy and would come cheap." + +"At half-wages, in fact." + +"Yes." + +"What is he like, this Vincent Spaulding?" + +"Small, stout-built, very quick in his ways, no hair on his face, +though he's not short of thirty. Has a white splash of acid upon +his forehead." + +Holmes sat up in his chair in considerable excitement. "I thought +as much," said he. "Have you ever observed that his ears are +pierced for earrings?" + +"Yes, sir. He told me that a gipsy had done it for him when he +was a lad." + +"Hum!" said Holmes, sinking back in deep thought. "He is still +with you?" + +"Oh, yes, sir; I have only just left him." + +"And has your business been attended to in your absence?" + +"Nothing to complain of, sir. There's never very much to do of a +morning." + +"That will do, Mr. Wilson. I shall be happy to give you an +opinion upon the subject in the course of a day or two. To-day is +Saturday, and I hope that by Monday we may come to a conclusion." + +"Well, Watson," said Holmes when our visitor had left us, "what +do you make of it all?" + +"I make nothing of it," I answered frankly. "It is a most +mysterious business." + +"As a rule," said Holmes, "the more bizarre a thing is the less +mysterious it proves to be. It is your commonplace, featureless +crimes which are really puzzling, just as a commonplace face is +the most difficult to identify. But I must be prompt over this +matter." + +"What are you going to do, then?" I asked. + +"To smoke," he answered. "It is quite a three pipe problem, and I +beg that you won't speak to me for fifty minutes." He curled +himself up in his chair, with his thin knees drawn up to his +hawk-like nose, and there he sat with his eyes closed and his +black clay pipe thrusting out like the bill of some strange bird. +I had come to the conclusion that he had dropped asleep, and +indeed was nodding myself, when he suddenly sprang out of his +chair with the gesture of a man who has made up his mind and put +his pipe down upon the mantelpiece. + +"Sarasate plays at the St. James's Hall this afternoon," he +remarked. "What do you think, Watson? Could your patients spare +you for a few hours?" + +"I have nothing to do to-day. My practice is never very +absorbing." + +"Then put on your hat and come. I am going through the City +first, and we can have some lunch on the way. I observe that +there is a good deal of German music on the programme, which is +rather more to my taste than Italian or French. It is +introspective, and I want to introspect. Come along!" + +We travelled by the Underground as far as Aldersgate; and a short +walk took us to Saxe-Coburg Square, the scene of the singular +story which we had listened to in the morning. It was a poky, +little, shabby-genteel place, where four lines of dingy +two-storied brick houses looked out into a small railed-in +enclosure, where a lawn of weedy grass and a few clumps of faded +laurel-bushes made a hard fight against a smoke-laden and +uncongenial atmosphere. Three gilt balls and a brown board with +"JABEZ WILSON" in white letters, upon a corner house, announced +the place where our red-headed client carried on his business. +Sherlock Holmes stopped in front of it with his head on one side +and looked it all over, with his eyes shining brightly between +puckered lids. Then he walked slowly up the street, and then down +again to the corner, still looking keenly at the houses. Finally +he returned to the pawnbroker's, and, having thumped vigorously +upon the pavement with his stick two or three times, he went up +to the door and knocked. It was instantly opened by a +bright-looking, clean-shaven young fellow, who asked him to step +in. + +"Thank you," said Holmes, "I only wished to ask you how you would +go from here to the Strand." + +"Third right, fourth left," answered the assistant promptly, +closing the door. + +"Smart fellow, that," observed Holmes as we walked away. "He is, +in my judgment, the fourth smartest man in London, and for daring +I am not sure that he has not a claim to be third. I have known +something of him before." + +"Evidently," said I, "Mr. Wilson's assistant counts for a good +deal in this mystery of the Red-headed League. I am sure that you +inquired your way merely in order that you might see him." + +"Not him." + +"What then?" + +"The knees of his trousers." + +"And what did you see?" + +"What I expected to see." + +"Why did you beat the pavement?" + +"My dear doctor, this is a time for observation, not for talk. We +are spies in an enemy's country. We know something of Saxe-Coburg +Square. Let us now explore the parts which lie behind it." + +The road in which we found ourselves as we turned round the +corner from the retired Saxe-Coburg Square presented as great a +contrast to it as the front of a picture does to the back. It was +one of the main arteries which conveyed the traffic of the City +to the north and west. The roadway was blocked with the immense +stream of commerce flowing in a double tide inward and outward, +while the footpaths were black with the hurrying swarm of +pedestrians. It was difficult to realise as we looked at the line +of fine shops and stately business premises that they really +abutted on the other side upon the faded and stagnant square +which we had just quitted. + +"Let me see," said Holmes, standing at the corner and glancing +along the line, "I should like just to remember the order of the +houses here. It is a hobby of mine to have an exact knowledge of +London. There is Mortimer's, the tobacconist, the little +newspaper shop, the Coburg branch of the City and Suburban Bank, +the Vegetarian Restaurant, and McFarlane's carriage-building +depot. That carries us right on to the other block. And now, +Doctor, we've done our work, so it's time we had some play. A +sandwich and a cup of coffee, and then off to violin-land, where +all is sweetness and delicacy and harmony, and there are no +red-headed clients to vex us with their conundrums." + +My friend was an enthusiastic musician, being himself not only a +very capable performer but a composer of no ordinary merit. All +the afternoon he sat in the stalls wrapped in the most perfect +happiness, gently waving his long, thin fingers in time to the +music, while his gently smiling face and his languid, dreamy eyes +were as unlike those of Holmes the sleuth-hound, Holmes the +relentless, keen-witted, ready-handed criminal agent, as it was +possible to conceive. In his singular character the dual nature +alternately asserted itself, and his extreme exactness and +astuteness represented, as I have often thought, the reaction +against the poetic and contemplative mood which occasionally +predominated in him. The swing of his nature took him from +extreme languor to devouring energy; and, as I knew well, he was +never so truly formidable as when, for days on end, he had been +lounging in his armchair amid his improvisations and his +black-letter editions. Then it was that the lust of the chase +would suddenly come upon him, and that his brilliant reasoning +power would rise to the level of intuition, until those who were +unacquainted with his methods would look askance at him as on a +man whose knowledge was not that of other mortals. When I saw him +that afternoon so enwrapped in the music at St. James's Hall I +felt that an evil time might be coming upon those whom he had set +himself to hunt down. + +"You want to go home, no doubt, Doctor," he remarked as we +emerged. + +"Yes, it would be as well." + +"And I have some business to do which will take some hours. This +business at Coburg Square is serious." + +"Why serious?" + +"A considerable crime is in contemplation. I have every reason to +believe that we shall be in time to stop it. But to-day being +Saturday rather complicates matters. I shall want your help +to-night." + +"At what time?" + +"Ten will be early enough." + +"I shall be at Baker Street at ten." + +"Very well. And, I say, Doctor, there may be some little danger, +so kindly put your army revolver in your pocket." He waved his +hand, turned on his heel, and disappeared in an instant among the +crowd. + +I trust that I am not more dense than my neighbours, but I was +always oppressed with a sense of my own stupidity in my dealings +with Sherlock Holmes. Here I had heard what he had heard, I had +seen what he had seen, and yet from his words it was evident that +he saw clearly not only what had happened but what was about to +happen, while to me the whole business was still confused and +grotesque. As I drove home to my house in Kensington I thought +over it all, from the extraordinary story of the red-headed +copier of the "Encyclopaedia" down to the visit to Saxe-Coburg +Square, and the ominous words with which he had parted from me. +What was this nocturnal expedition, and why should I go armed? +Where were we going, and what were we to do? I had the hint from +Holmes that this smooth-faced pawnbroker's assistant was a +formidable man--a man who might play a deep game. I tried to +puzzle it out, but gave it up in despair and set the matter aside +until night should bring an explanation. + +It was a quarter-past nine when I started from home and made my +way across the Park, and so through Oxford Street to Baker +Street. Two hansoms were standing at the door, and as I entered +the passage I heard the sound of voices from above. On entering +his room I found Holmes in animated conversation with two men, +one of whom I recognised as Peter Jones, the official police +agent, while the other was a long, thin, sad-faced man, with a +very shiny hat and oppressively respectable frock-coat. + +"Ha! Our party is complete," said Holmes, buttoning up his +pea-jacket and taking his heavy hunting crop from the rack. +"Watson, I think you know Mr. Jones, of Scotland Yard? Let me +introduce you to Mr. Merryweather, who is to be our companion in +to-night's adventure." + +"We're hunting in couples again, Doctor, you see," said Jones in +his consequential way. "Our friend here is a wonderful man for +starting a chase. All he wants is an old dog to help him to do +the running down." + +"I hope a wild goose may not prove to be the end of our chase," +observed Mr. Merryweather gloomily. + +"You may place considerable confidence in Mr. Holmes, sir," said +the police agent loftily. "He has his own little methods, which +are, if he won't mind my saying so, just a little too theoretical +and fantastic, but he has the makings of a detective in him. It +is not too much to say that once or twice, as in that business of +the Sholto murder and the Agra treasure, he has been more nearly +correct than the official force." + +"Oh, if you say so, Mr. Jones, it is all right," said the +stranger with deference. "Still, I confess that I miss my rubber. +It is the first Saturday night for seven-and-twenty years that I +have not had my rubber." + +"I think you will find," said Sherlock Holmes, "that you will +play for a higher stake to-night than you have ever done yet, and +that the play will be more exciting. For you, Mr. Merryweather, +the stake will be some 30,000 pounds; and for you, Jones, it will +be the man upon whom you wish to lay your hands." + +"John Clay, the murderer, thief, smasher, and forger. He's a +young man, Mr. Merryweather, but he is at the head of his +profession, and I would rather have my bracelets on him than on +any criminal in London. He's a remarkable man, is young John +Clay. His grandfather was a royal duke, and he himself has been +to Eton and Oxford. His brain is as cunning as his fingers, and +though we meet signs of him at every turn, we never know where to +find the man himself. He'll crack a crib in Scotland one week, +and be raising money to build an orphanage in Cornwall the next. +I've been on his track for years and have never set eyes on him +yet." + +"I hope that I may have the pleasure of introducing you to-night. +I've had one or two little turns also with Mr. John Clay, and I +agree with you that he is at the head of his profession. It is +past ten, however, and quite time that we started. If you two +will take the first hansom, Watson and I will follow in the +second." + +Sherlock Holmes was not very communicative during the long drive +and lay back in the cab humming the tunes which he had heard in +the afternoon. We rattled through an endless labyrinth of gas-lit +streets until we emerged into Farrington Street. + +"We are close there now," my friend remarked. "This fellow +Merryweather is a bank director, and personally interested in the +matter. I thought it as well to have Jones with us also. He is +not a bad fellow, though an absolute imbecile in his profession. +He has one positive virtue. He is as brave as a bulldog and as +tenacious as a lobster if he gets his claws upon anyone. Here we +are, and they are waiting for us." + +We had reached the same crowded thoroughfare in which we had +found ourselves in the morning. Our cabs were dismissed, and, +following the guidance of Mr. Merryweather, we passed down a +narrow passage and through a side door, which he opened for us. +Within there was a small corridor, which ended in a very massive +iron gate. This also was opened, and led down a flight of winding +stone steps, which terminated at another formidable gate. Mr. +Merryweather stopped to light a lantern, and then conducted us +down a dark, earth-smelling passage, and so, after opening a +third door, into a huge vault or cellar, which was piled all +round with crates and massive boxes. + +"You are not very vulnerable from above," Holmes remarked as he +held up the lantern and gazed about him. + +"Nor from below," said Mr. Merryweather, striking his stick upon +the flags which lined the floor. "Why, dear me, it sounds quite +hollow!" he remarked, looking up in surprise. + +"I must really ask you to be a little more quiet!" said Holmes +severely. "You have already imperilled the whole success of our +expedition. Might I beg that you would have the goodness to sit +down upon one of those boxes, and not to interfere?" + +The solemn Mr. Merryweather perched himself upon a crate, with a +very injured expression upon his face, while Holmes fell upon his +knees upon the floor and, with the lantern and a magnifying lens, +began to examine minutely the cracks between the stones. A few +seconds sufficed to satisfy him, for he sprang to his feet again +and put his glass in his pocket. + +"We have at least an hour before us," he remarked, "for they can +hardly take any steps until the good pawnbroker is safely in bed. +Then they will not lose a minute, for the sooner they do their +work the longer time they will have for their escape. We are at +present, Doctor--as no doubt you have divined--in the cellar of +the City branch of one of the principal London banks. Mr. +Merryweather is the chairman of directors, and he will explain to +you that there are reasons why the more daring criminals of +London should take a considerable interest in this cellar at +present." + +"It is our French gold," whispered the director. "We have had +several warnings that an attempt might be made upon it." + +"Your French gold?" + +"Yes. We had occasion some months ago to strengthen our resources +and borrowed for that purpose 30,000 napoleons from the Bank of +France. It has become known that we have never had occasion to +unpack the money, and that it is still lying in our cellar. The +crate upon which I sit contains 2,000 napoleons packed between +layers of lead foil. Our reserve of bullion is much larger at +present than is usually kept in a single branch office, and the +directors have had misgivings upon the subject." + +"Which were very well justified," observed Holmes. "And now it is +time that we arranged our little plans. I expect that within an +hour matters will come to a head. In the meantime Mr. +Merryweather, we must put the screen over that dark lantern." + +"And sit in the dark?" + +"I am afraid so. I had brought a pack of cards in my pocket, and +I thought that, as we were a partie carre, you might have your +rubber after all. But I see that the enemy's preparations have +gone so far that we cannot risk the presence of a light. And, +first of all, we must choose our positions. These are daring men, +and though we shall take them at a disadvantage, they may do us +some harm unless we are careful. I shall stand behind this crate, +and do you conceal yourselves behind those. Then, when I flash a +light upon them, close in swiftly. If they fire, Watson, have no +compunction about shooting them down." + +I placed my revolver, cocked, upon the top of the wooden case +behind which I crouched. Holmes shot the slide across the front +of his lantern and left us in pitch darkness--such an absolute +darkness as I have never before experienced. The smell of hot +metal remained to assure us that the light was still there, ready +to flash out at a moment's notice. To me, with my nerves worked +up to a pitch of expectancy, there was something depressing and +subduing in the sudden gloom, and in the cold dank air of the +vault. + +"They have but one retreat," whispered Holmes. "That is back +through the house into Saxe-Coburg Square. I hope that you have +done what I asked you, Jones?" + +"I have an inspector and two officers waiting at the front door." + +"Then we have stopped all the holes. And now we must be silent +and wait." + +What a time it seemed! From comparing notes afterwards it was but +an hour and a quarter, yet it appeared to me that the night must +have almost gone and the dawn be breaking above us. My limbs +were weary and stiff, for I feared to change my position; yet my +nerves were worked up to the highest pitch of tension, and my +hearing was so acute that I could not only hear the gentle +breathing of my companions, but I could distinguish the deeper, +heavier in-breath of the bulky Jones from the thin, sighing note +of the bank director. From my position I could look over the case +in the direction of the floor. Suddenly my eyes caught the glint +of a light. + +At first it was but a lurid spark upon the stone pavement. Then +it lengthened out until it became a yellow line, and then, +without any warning or sound, a gash seemed to open and a hand +appeared, a white, almost womanly hand, which felt about in the +centre of the little area of light. For a minute or more the +hand, with its writhing fingers, protruded out of the floor. Then +it was withdrawn as suddenly as it appeared, and all was dark +again save the single lurid spark which marked a chink between +the stones. + +Its disappearance, however, was but momentary. With a rending, +tearing sound, one of the broad, white stones turned over upon +its side and left a square, gaping hole, through which streamed +the light of a lantern. Over the edge there peeped a clean-cut, +boyish face, which looked keenly about it, and then, with a hand +on either side of the aperture, drew itself shoulder-high and +waist-high, until one knee rested upon the edge. In another +instant he stood at the side of the hole and was hauling after +him a companion, lithe and small like himself, with a pale face +and a shock of very red hair. + +"It's all clear," he whispered. "Have you the chisel and the +bags? Great Scott! Jump, Archie, jump, and I'll swing for it!" + +Sherlock Holmes had sprung out and seized the intruder by the +collar. The other dived down the hole, and I heard the sound of +rending cloth as Jones clutched at his skirts. The light flashed +upon the barrel of a revolver, but Holmes' hunting crop came +down on the man's wrist, and the pistol clinked upon the stone +floor. + +"It's no use, John Clay," said Holmes blandly. "You have no +chance at all." + +"So I see," the other answered with the utmost coolness. "I fancy +that my pal is all right, though I see you have got his +coat-tails." + +"There are three men waiting for him at the door," said Holmes. + +"Oh, indeed! You seem to have done the thing very completely. I +must compliment you." + +"And I you," Holmes answered. "Your red-headed idea was very new +and effective." + +"You'll see your pal again presently," said Jones. "He's quicker +at climbing down holes than I am. Just hold out while I fix the +derbies." + +"I beg that you will not touch me with your filthy hands," +remarked our prisoner as the handcuffs clattered upon his wrists. +"You may not be aware that I have royal blood in my veins. Have +the goodness, also, when you address me always to say 'sir' and +'please.'" + +"All right," said Jones with a stare and a snigger. "Well, would +you please, sir, march upstairs, where we can get a cab to carry +your Highness to the police-station?" + +"That is better," said John Clay serenely. He made a sweeping bow +to the three of us and walked quietly off in the custody of the +detective. + +"Really, Mr. Holmes," said Mr. Merryweather as we followed them +from the cellar, "I do not know how the bank can thank you or +repay you. There is no doubt that you have detected and defeated +in the most complete manner one of the most determined attempts +at bank robbery that have ever come within my experience." + +"I have had one or two little scores of my own to settle with Mr. +John Clay," said Holmes. "I have been at some small expense over +this matter, which I shall expect the bank to refund, but beyond +that I am amply repaid by having had an experience which is in +many ways unique, and by hearing the very remarkable narrative of +the Red-headed League." + + +"You see, Watson," he explained in the early hours of the morning +as we sat over a glass of whisky and soda in Baker Street, "it +was perfectly obvious from the first that the only possible +object of this rather fantastic business of the advertisement of +the League, and the copying of the 'Encyclopaedia,' must be to get +this not over-bright pawnbroker out of the way for a number of +hours every day. It was a curious way of managing it, but, +really, it would be difficult to suggest a better. The method was +no doubt suggested to Clay's ingenious mind by the colour of his +accomplice's hair. The 4 pounds a week was a lure which must draw +him, and what was it to them, who were playing for thousands? +They put in the advertisement, one rogue has the temporary +office, the other rogue incites the man to apply for it, and +together they manage to secure his absence every morning in the +week. From the time that I heard of the assistant having come for +half wages, it was obvious to me that he had some strong motive +for securing the situation." + +"But how could you guess what the motive was?" + +"Had there been women in the house, I should have suspected a +mere vulgar intrigue. That, however, was out of the question. The +man's business was a small one, and there was nothing in his +house which could account for such elaborate preparations, and +such an expenditure as they were at. It must, then, be something +out of the house. What could it be? I thought of the assistant's +fondness for photography, and his trick of vanishing into the +cellar. The cellar! There was the end of this tangled clue. Then +I made inquiries as to this mysterious assistant and found that I +had to deal with one of the coolest and most daring criminals in +London. He was doing something in the cellar--something which +took many hours a day for months on end. What could it be, once +more? I could think of nothing save that he was running a tunnel +to some other building. + +"So far I had got when we went to visit the scene of action. I +surprised you by beating upon the pavement with my stick. I was +ascertaining whether the cellar stretched out in front or behind. +It was not in front. Then I rang the bell, and, as I hoped, the +assistant answered it. We have had some skirmishes, but we had +never set eyes upon each other before. I hardly looked at his +face. His knees were what I wished to see. You must yourself have +remarked how worn, wrinkled, and stained they were. They spoke of +those hours of burrowing. The only remaining point was what they +were burrowing for. I walked round the corner, saw the City and +Suburban Bank abutted on our friend's premises, and felt that I +had solved my problem. When you drove home after the concert I +called upon Scotland Yard and upon the chairman of the bank +directors, with the result that you have seen." + +"And how could you tell that they would make their attempt +to-night?" I asked. + +"Well, when they closed their League offices that was a sign that +they cared no longer about Mr. Jabez Wilson's presence--in other +words, that they had completed their tunnel. But it was essential +that they should use it soon, as it might be discovered, or the +bullion might be removed. Saturday would suit them better than +any other day, as it would give them two days for their escape. +For all these reasons I expected them to come to-night." + +"You reasoned it out beautifully," I exclaimed in unfeigned +admiration. "It is so long a chain, and yet every link rings +true." + +"It saved me from ennui," he answered, yawning. "Alas! I already +feel it closing in upon me. My life is spent in one long effort +to escape from the commonplaces of existence. These little +problems help me to do so." + +"And you are a benefactor of the race," said I. + +He shrugged his shoulders. "Well, perhaps, after all, it is of +some little use," he remarked. "'L'homme c'est rien--l'oeuvre +c'est tout,' as Gustave Flaubert wrote to George Sand." + + + +ADVENTURE III. A CASE OF IDENTITY + +"My dear fellow," said Sherlock Holmes as we sat on either side +of the fire in his lodgings at Baker Street, "life is infinitely +stranger than anything which the mind of man could invent. We +would not dare to conceive the things which are really mere +commonplaces of existence. If we could fly out of that window +hand in hand, hover over this great city, gently remove the +roofs, and peep in at the queer things which are going on, the +strange coincidences, the plannings, the cross-purposes, the +wonderful chains of events, working through generations, and +leading to the most outr results, it would make all fiction with +its conventionalities and foreseen conclusions most stale and +unprofitable." + +"And yet I am not convinced of it," I answered. "The cases which +come to light in the papers are, as a rule, bald enough, and +vulgar enough. We have in our police reports realism pushed to +its extreme limits, and yet the result is, it must be confessed, +neither fascinating nor artistic." + +"A certain selection and discretion must be used in producing a +realistic effect," remarked Holmes. "This is wanting in the +police report, where more stress is laid, perhaps, upon the +platitudes of the magistrate than upon the details, which to an +observer contain the vital essence of the whole matter. Depend +upon it, there is nothing so unnatural as the commonplace." + +I smiled and shook my head. "I can quite understand your thinking +so," I said. "Of course, in your position of unofficial adviser +and helper to everybody who is absolutely puzzled, throughout +three continents, you are brought in contact with all that is +strange and bizarre. But here"--I picked up the morning paper +from the ground--"let us put it to a practical test. Here is the +first heading upon which I come. 'A husband's cruelty to his +wife.' There is half a column of print, but I know without +reading it that it is all perfectly familiar to me. There is, of +course, the other woman, the drink, the push, the blow, the +bruise, the sympathetic sister or landlady. The crudest of +writers could invent nothing more crude." + +"Indeed, your example is an unfortunate one for your argument," +said Holmes, taking the paper and glancing his eye down it. "This +is the Dundas separation case, and, as it happens, I was engaged +in clearing up some small points in connection with it. The +husband was a teetotaler, there was no other woman, and the +conduct complained of was that he had drifted into the habit of +winding up every meal by taking out his false teeth and hurling +them at his wife, which, you will allow, is not an action likely +to occur to the imagination of the average story-teller. Take a +pinch of snuff, Doctor, and acknowledge that I have scored over +you in your example." + +He held out his snuffbox of old gold, with a great amethyst in +the centre of the lid. Its splendour was in such contrast to his +homely ways and simple life that I could not help commenting upon +it. + +"Ah," said he, "I forgot that I had not seen you for some weeks. +It is a little souvenir from the King of Bohemia in return for my +assistance in the case of the Irene Adler papers." + +"And the ring?" I asked, glancing at a remarkable brilliant which +sparkled upon his finger. + +"It was from the reigning family of Holland, though the matter in +which I served them was of such delicacy that I cannot confide it +even to you, who have been good enough to chronicle one or two of +my little problems." + +"And have you any on hand just now?" I asked with interest. + +"Some ten or twelve, but none which present any feature of +interest. They are important, you understand, without being +interesting. Indeed, I have found that it is usually in +unimportant matters that there is a field for the observation, +and for the quick analysis of cause and effect which gives the +charm to an investigation. The larger crimes are apt to be the +simpler, for the bigger the crime the more obvious, as a rule, is +the motive. In these cases, save for one rather intricate matter +which has been referred to me from Marseilles, there is nothing +which presents any features of interest. It is possible, however, +that I may have something better before very many minutes are +over, for this is one of my clients, or I am much mistaken." + +He had risen from his chair and was standing between the parted +blinds gazing down into the dull neutral-tinted London street. +Looking over his shoulder, I saw that on the pavement opposite +there stood a large woman with a heavy fur boa round her neck, +and a large curling red feather in a broad-brimmed hat which was +tilted in a coquettish Duchess of Devonshire fashion over her +ear. From under this great panoply she peeped up in a nervous, +hesitating fashion at our windows, while her body oscillated +backward and forward, and her fingers fidgeted with her glove +buttons. Suddenly, with a plunge, as of the swimmer who leaves +the bank, she hurried across the road, and we heard the sharp +clang of the bell. + +"I have seen those symptoms before," said Holmes, throwing his +cigarette into the fire. "Oscillation upon the pavement always +means an affaire de coeur. She would like advice, but is not sure +that the matter is not too delicate for communication. And yet +even here we may discriminate. When a woman has been seriously +wronged by a man she no longer oscillates, and the usual symptom +is a broken bell wire. Here we may take it that there is a love +matter, but that the maiden is not so much angry as perplexed, or +grieved. But here she comes in person to resolve our doubts." + +As he spoke there was a tap at the door, and the boy in buttons +entered to announce Miss Mary Sutherland, while the lady herself +loomed behind his small black figure like a full-sailed +merchant-man behind a tiny pilot boat. Sherlock Holmes welcomed +her with the easy courtesy for which he was remarkable, and, +having closed the door and bowed her into an armchair, he looked +her over in the minute and yet abstracted fashion which was +peculiar to him. + +"Do you not find," he said, "that with your short sight it is a +little trying to do so much typewriting?" + +"I did at first," she answered, "but now I know where the letters +are without looking." Then, suddenly realising the full purport +of his words, she gave a violent start and looked up, with fear +and astonishment upon her broad, good-humoured face. "You've +heard about me, Mr. Holmes," she cried, "else how could you know +all that?" + +"Never mind," said Holmes, laughing; "it is my business to know +things. Perhaps I have trained myself to see what others +overlook. If not, why should you come to consult me?" + +"I came to you, sir, because I heard of you from Mrs. Etherege, +whose husband you found so easy when the police and everyone had +given him up for dead. Oh, Mr. Holmes, I wish you would do as +much for me. I'm not rich, but still I have a hundred a year in +my own right, besides the little that I make by the machine, and +I would give it all to know what has become of Mr. Hosmer Angel." + +"Why did you come away to consult me in such a hurry?" asked +Sherlock Holmes, with his finger-tips together and his eyes to +the ceiling. + +Again a startled look came over the somewhat vacuous face of Miss +Mary Sutherland. "Yes, I did bang out of the house," she said, +"for it made me angry to see the easy way in which Mr. +Windibank--that is, my father--took it all. He would not go to +the police, and he would not go to you, and so at last, as he +would do nothing and kept on saying that there was no harm done, +it made me mad, and I just on with my things and came right away +to you." + +"Your father," said Holmes, "your stepfather, surely, since the +name is different." + +"Yes, my stepfather. I call him father, though it sounds funny, +too, for he is only five years and two months older than myself." + +"And your mother is alive?" + +"Oh, yes, mother is alive and well. I wasn't best pleased, Mr. +Holmes, when she married again so soon after father's death, and +a man who was nearly fifteen years younger than herself. Father +was a plumber in the Tottenham Court Road, and he left a tidy +business behind him, which mother carried on with Mr. Hardy, the +foreman; but when Mr. Windibank came he made her sell the +business, for he was very superior, being a traveller in wines. +They got 4700 pounds for the goodwill and interest, which wasn't +near as much as father could have got if he had been alive." + +I had expected to see Sherlock Holmes impatient under this +rambling and inconsequential narrative, but, on the contrary, he +had listened with the greatest concentration of attention. + +"Your own little income," he asked, "does it come out of the +business?" + +"Oh, no, sir. It is quite separate and was left me by my uncle +Ned in Auckland. It is in New Zealand stock, paying 4 1/2 per +cent. Two thousand five hundred pounds was the amount, but I can +only touch the interest." + +"You interest me extremely," said Holmes. "And since you draw so +large a sum as a hundred a year, with what you earn into the +bargain, you no doubt travel a little and indulge yourself in +every way. I believe that a single lady can get on very nicely +upon an income of about 60 pounds." + +"I could do with much less than that, Mr. Holmes, but you +understand that as long as I live at home I don't wish to be a +burden to them, and so they have the use of the money just while +I am staying with them. Of course, that is only just for the +time. Mr. Windibank draws my interest every quarter and pays it +over to mother, and I find that I can do pretty well with what I +earn at typewriting. It brings me twopence a sheet, and I can +often do from fifteen to twenty sheets in a day." + +"You have made your position very clear to me," said Holmes. +"This is my friend, Dr. Watson, before whom you can speak as +freely as before myself. Kindly tell us now all about your +connection with Mr. Hosmer Angel." + +A flush stole over Miss Sutherland's face, and she picked +nervously at the fringe of her jacket. "I met him first at the +gasfitters' ball," she said. "They used to send father tickets +when he was alive, and then afterwards they remembered us, and +sent them to mother. Mr. Windibank did not wish us to go. He +never did wish us to go anywhere. He would get quite mad if I +wanted so much as to join a Sunday-school treat. But this time I +was set on going, and I would go; for what right had he to +prevent? He said the folk were not fit for us to know, when all +father's friends were to be there. And he said that I had nothing +fit to wear, when I had my purple plush that I had never so much +as taken out of the drawer. At last, when nothing else would do, +he went off to France upon the business of the firm, but we went, +mother and I, with Mr. Hardy, who used to be our foreman, and it +was there I met Mr. Hosmer Angel." + +"I suppose," said Holmes, "that when Mr. Windibank came back from +France he was very annoyed at your having gone to the ball." + +"Oh, well, he was very good about it. He laughed, I remember, and +shrugged his shoulders, and said there was no use denying +anything to a woman, for she would have her way." + +"I see. Then at the gasfitters' ball you met, as I understand, a +gentleman called Mr. Hosmer Angel." + +"Yes, sir. I met him that night, and he called next day to ask if +we had got home all safe, and after that we met him--that is to +say, Mr. Holmes, I met him twice for walks, but after that father +came back again, and Mr. Hosmer Angel could not come to the house +any more." + +"No?" + +"Well, you know father didn't like anything of the sort. He +wouldn't have any visitors if he could help it, and he used to +say that a woman should be happy in her own family circle. But +then, as I used to say to mother, a woman wants her own circle to +begin with, and I had not got mine yet." + +"But how about Mr. Hosmer Angel? Did he make no attempt to see +you?" + +"Well, father was going off to France again in a week, and Hosmer +wrote and said that it would be safer and better not to see each +other until he had gone. We could write in the meantime, and he +used to write every day. I took the letters in in the morning, so +there was no need for father to know." + +"Were you engaged to the gentleman at this time?" + +"Oh, yes, Mr. Holmes. We were engaged after the first walk that +we took. Hosmer--Mr. Angel--was a cashier in an office in +Leadenhall Street--and--" + +"What office?" + +"That's the worst of it, Mr. Holmes, I don't know." + +"Where did he live, then?" + +"He slept on the premises." + +"And you don't know his address?" + +"No--except that it was Leadenhall Street." + +"Where did you address your letters, then?" + +"To the Leadenhall Street Post Office, to be left till called +for. He said that if they were sent to the office he would be +chaffed by all the other clerks about having letters from a lady, +so I offered to typewrite them, like he did his, but he wouldn't +have that, for he said that when I wrote them they seemed to come +from me, but when they were typewritten he always felt that the +machine had come between us. That will just show you how fond he +was of me, Mr. Holmes, and the little things that he would think +of." + +"It was most suggestive," said Holmes. "It has long been an axiom +of mine that the little things are infinitely the most important. +Can you remember any other little things about Mr. Hosmer Angel?" + +"He was a very shy man, Mr. Holmes. He would rather walk with me +in the evening than in the daylight, for he said that he hated to +be conspicuous. Very retiring and gentlemanly he was. Even his +voice was gentle. He'd had the quinsy and swollen glands when he +was young, he told me, and it had left him with a weak throat, +and a hesitating, whispering fashion of speech. He was always +well dressed, very neat and plain, but his eyes were weak, just +as mine are, and he wore tinted glasses against the glare." + +"Well, and what happened when Mr. Windibank, your stepfather, +returned to France?" + +"Mr. Hosmer Angel came to the house again and proposed that we +should marry before father came back. He was in dreadful earnest +and made me swear, with my hands on the Testament, that whatever +happened I would always be true to him. Mother said he was quite +right to make me swear, and that it was a sign of his passion. +Mother was all in his favour from the first and was even fonder +of him than I was. Then, when they talked of marrying within the +week, I began to ask about father; but they both said never to +mind about father, but just to tell him afterwards, and mother +said she would make it all right with him. I didn't quite like +that, Mr. Holmes. It seemed funny that I should ask his leave, as +he was only a few years older than me; but I didn't want to do +anything on the sly, so I wrote to father at Bordeaux, where the +company has its French offices, but the letter came back to me on +the very morning of the wedding." + +"It missed him, then?" + +"Yes, sir; for he had started to England just before it arrived." + +"Ha! that was unfortunate. Your wedding was arranged, then, for +the Friday. Was it to be in church?" + +"Yes, sir, but very quietly. It was to be at St. Saviour's, near +King's Cross, and we were to have breakfast afterwards at the St. +Pancras Hotel. Hosmer came for us in a hansom, but as there were +two of us he put us both into it and stepped himself into a +four-wheeler, which happened to be the only other cab in the +street. We got to the church first, and when the four-wheeler +drove up we waited for him to step out, but he never did, and +when the cabman got down from the box and looked there was no one +there! The cabman said that he could not imagine what had become +of him, for he had seen him get in with his own eyes. That was +last Friday, Mr. Holmes, and I have never seen or heard anything +since then to throw any light upon what became of him." + +"It seems to me that you have been very shamefully treated," said +Holmes. + +"Oh, no, sir! He was too good and kind to leave me so. Why, all +the morning he was saying to me that, whatever happened, I was to +be true; and that even if something quite unforeseen occurred to +separate us, I was always to remember that I was pledged to him, +and that he would claim his pledge sooner or later. It seemed +strange talk for a wedding-morning, but what has happened since +gives a meaning to it." + +"Most certainly it does. Your own opinion is, then, that some +unforeseen catastrophe has occurred to him?" + +"Yes, sir. I believe that he foresaw some danger, or else he +would not have talked so. And then I think that what he foresaw +happened." + +"But you have no notion as to what it could have been?" + +"None." + +"One more question. How did your mother take the matter?" + +"She was angry, and said that I was never to speak of the matter +again." + +"And your father? Did you tell him?" + +"Yes; and he seemed to think, with me, that something had +happened, and that I should hear of Hosmer again. As he said, +what interest could anyone have in bringing me to the doors of +the church, and then leaving me? Now, if he had borrowed my +money, or if he had married me and got my money settled on him, +there might be some reason, but Hosmer was very independent about +money and never would look at a shilling of mine. And yet, what +could have happened? And why could he not write? Oh, it drives me +half-mad to think of it, and I can't sleep a wink at night." She +pulled a little handkerchief out of her muff and began to sob +heavily into it. + +"I shall glance into the case for you," said Holmes, rising, "and +I have no doubt that we shall reach some definite result. Let the +weight of the matter rest upon me now, and do not let your mind +dwell upon it further. Above all, try to let Mr. Hosmer Angel +vanish from your memory, as he has done from your life." + +"Then you don't think I'll see him again?" + +"I fear not." + +"Then what has happened to him?" + +"You will leave that question in my hands. I should like an +accurate description of him and any letters of his which you can +spare." + +"I advertised for him in last Saturday's Chronicle," said she. +"Here is the slip and here are four letters from him." + +"Thank you. And your address?" + +"No. 31 Lyon Place, Camberwell." + +"Mr. Angel's address you never had, I understand. Where is your +father's place of business?" + +"He travels for Westhouse & Marbank, the great claret importers +of Fenchurch Street." + +"Thank you. You have made your statement very clearly. You will +leave the papers here, and remember the advice which I have given +you. Let the whole incident be a sealed book, and do not allow it +to affect your life." + +"You are very kind, Mr. Holmes, but I cannot do that. I shall be +true to Hosmer. He shall find me ready when he comes back." + +For all the preposterous hat and the vacuous face, there was +something noble in the simple faith of our visitor which +compelled our respect. She laid her little bundle of papers upon +the table and went her way, with a promise to come again whenever +she might be summoned. + +Sherlock Holmes sat silent for a few minutes with his fingertips +still pressed together, his legs stretched out in front of him, +and his gaze directed upward to the ceiling. Then he took down +from the rack the old and oily clay pipe, which was to him as a +counsellor, and, having lit it, he leaned back in his chair, with +the thick blue cloud-wreaths spinning up from him, and a look of +infinite languor in his face. + +"Quite an interesting study, that maiden," he observed. "I found +her more interesting than her little problem, which, by the way, +is rather a trite one. You will find parallel cases, if you +consult my index, in Andover in '77, and there was something of +the sort at The Hague last year. Old as is the idea, however, +there were one or two details which were new to me. But the +maiden herself was most instructive." + +"You appeared to read a good deal upon her which was quite +invisible to me," I remarked. + +"Not invisible but unnoticed, Watson. You did not know where to +look, and so you missed all that was important. I can never bring +you to realise the importance of sleeves, the suggestiveness of +thumb-nails, or the great issues that may hang from a boot-lace. +Now, what did you gather from that woman's appearance? Describe +it." + +"Well, she had a slate-coloured, broad-brimmed straw hat, with a +feather of a brickish red. Her jacket was black, with black beads +sewn upon it, and a fringe of little black jet ornaments. Her +dress was brown, rather darker than coffee colour, with a little +purple plush at the neck and sleeves. Her gloves were greyish and +were worn through at the right forefinger. Her boots I didn't +observe. She had small round, hanging gold earrings, and a +general air of being fairly well-to-do in a vulgar, comfortable, +easy-going way." + +Sherlock Holmes clapped his hands softly together and chuckled. + +"'Pon my word, Watson, you are coming along wonderfully. You have +really done very well indeed. It is true that you have missed +everything of importance, but you have hit upon the method, and +you have a quick eye for colour. Never trust to general +impressions, my boy, but concentrate yourself upon details. My +first glance is always at a woman's sleeve. In a man it is +perhaps better first to take the knee of the trouser. As you +observe, this woman had plush upon her sleeves, which is a most +useful material for showing traces. The double line a little +above the wrist, where the typewritist presses against the table, +was beautifully defined. The sewing-machine, of the hand type, +leaves a similar mark, but only on the left arm, and on the side +of it farthest from the thumb, instead of being right across the +broadest part, as this was. I then glanced at her face, and, +observing the dint of a pince-nez at either side of her nose, I +ventured a remark upon short sight and typewriting, which seemed +to surprise her." + +"It surprised me." + +"But, surely, it was obvious. I was then much surprised and +interested on glancing down to observe that, though the boots +which she was wearing were not unlike each other, they were +really odd ones; the one having a slightly decorated toe-cap, and +the other a plain one. One was buttoned only in the two lower +buttons out of five, and the other at the first, third, and +fifth. Now, when you see that a young lady, otherwise neatly +dressed, has come away from home with odd boots, half-buttoned, +it is no great deduction to say that she came away in a hurry." + +"And what else?" I asked, keenly interested, as I always was, by +my friend's incisive reasoning. + +"I noted, in passing, that she had written a note before leaving +home but after being fully dressed. You observed that her right +glove was torn at the forefinger, but you did not apparently see +that both glove and finger were stained with violet ink. She had +written in a hurry and dipped her pen too deep. It must have been +this morning, or the mark would not remain clear upon the finger. +All this is amusing, though rather elementary, but I must go back +to business, Watson. Would you mind reading me the advertised +description of Mr. Hosmer Angel?" + +I held the little printed slip to the light. + +"Missing," it said, "on the morning of the fourteenth, a gentleman +named Hosmer Angel. About five ft. seven in. in height; +strongly built, sallow complexion, black hair, a little bald in +the centre, bushy, black side-whiskers and moustache; tinted +glasses, slight infirmity of speech. Was dressed, when last seen, +in black frock-coat faced with silk, black waistcoat, gold Albert +chain, and grey Harris tweed trousers, with brown gaiters over +elastic-sided boots. Known to have been employed in an office in +Leadenhall Street. Anybody bringing--" + +"That will do," said Holmes. "As to the letters," he continued, +glancing over them, "they are very commonplace. Absolutely no +clue in them to Mr. Angel, save that he quotes Balzac once. There +is one remarkable point, however, which will no doubt strike +you." + +"They are typewritten," I remarked. + +"Not only that, but the signature is typewritten. Look at the +neat little 'Hosmer Angel' at the bottom. There is a date, you +see, but no superscription except Leadenhall Street, which is +rather vague. The point about the signature is very suggestive--in +fact, we may call it conclusive." + +"Of what?" + +"My dear fellow, is it possible you do not see how strongly it +bears upon the case?" + +"I cannot say that I do unless it were that he wished to be able +to deny his signature if an action for breach of promise were +instituted." + +"No, that was not the point. However, I shall write two letters, +which should settle the matter. One is to a firm in the City, the +other is to the young lady's stepfather, Mr. Windibank, asking +him whether he could meet us here at six o'clock tomorrow +evening. It is just as well that we should do business with the +male relatives. And now, Doctor, we can do nothing until the +answers to those letters come, so we may put our little problem +upon the shelf for the interim." + +I had had so many reasons to believe in my friend's subtle powers +of reasoning and extraordinary energy in action that I felt that +he must have some solid grounds for the assured and easy +demeanour with which he treated the singular mystery which he had +been called upon to fathom. Once only had I known him to fail, in +the case of the King of Bohemia and of the Irene Adler +photograph; but when I looked back to the weird business of the +Sign of Four, and the extraordinary circumstances connected with +the Study in Scarlet, I felt that it would be a strange tangle +indeed which he could not unravel. + +I left him then, still puffing at his black clay pipe, with the +conviction that when I came again on the next evening I would +find that he held in his hands all the clues which would lead up +to the identity of the disappearing bridegroom of Miss Mary +Sutherland. + +A professional case of great gravity was engaging my own +attention at the time, and the whole of next day I was busy at +the bedside of the sufferer. It was not until close upon six +o'clock that I found myself free and was able to spring into a +hansom and drive to Baker Street, half afraid that I might be too +late to assist at the dnouement of the little mystery. I found +Sherlock Holmes alone, however, half asleep, with his long, thin +form curled up in the recesses of his armchair. A formidable +array of bottles and test-tubes, with the pungent cleanly smell +of hydrochloric acid, told me that he had spent his day in the +chemical work which was so dear to him. + +"Well, have you solved it?" I asked as I entered. + +"Yes. It was the bisulphate of baryta." + +"No, no, the mystery!" I cried. + +"Oh, that! I thought of the salt that I have been working upon. +There was never any mystery in the matter, though, as I said +yesterday, some of the details are of interest. The only drawback +is that there is no law, I fear, that can touch the scoundrel." + +"Who was he, then, and what was his object in deserting Miss +Sutherland?" + +The question was hardly out of my mouth, and Holmes had not yet +opened his lips to reply, when we heard a heavy footfall in the +passage and a tap at the door. + +"This is the girl's stepfather, Mr. James Windibank," said +Holmes. "He has written to me to say that he would be here at +six. Come in!" + +The man who entered was a sturdy, middle-sized fellow, some +thirty years of age, clean-shaven, and sallow-skinned, with a +bland, insinuating manner, and a pair of wonderfully sharp and +penetrating grey eyes. He shot a questioning glance at each of +us, placed his shiny top-hat upon the sideboard, and with a +slight bow sidled down into the nearest chair. + +"Good-evening, Mr. James Windibank," said Holmes. "I think that +this typewritten letter is from you, in which you made an +appointment with me for six o'clock?" + +"Yes, sir. I am afraid that I am a little late, but I am not +quite my own master, you know. I am sorry that Miss Sutherland +has troubled you about this little matter, for I think it is far +better not to wash linen of the sort in public. It was quite +against my wishes that she came, but she is a very excitable, +impulsive girl, as you may have noticed, and she is not easily +controlled when she has made up her mind on a point. Of course, I +did not mind you so much, as you are not connected with the +official police, but it is not pleasant to have a family +misfortune like this noised abroad. Besides, it is a useless +expense, for how could you possibly find this Hosmer Angel?" + +"On the contrary," said Holmes quietly; "I have every reason to +believe that I will succeed in discovering Mr. Hosmer Angel." + +Mr. Windibank gave a violent start and dropped his gloves. "I am +delighted to hear it," he said. + +"It is a curious thing," remarked Holmes, "that a typewriter has +really quite as much individuality as a man's handwriting. Unless +they are quite new, no two of them write exactly alike. Some +letters get more worn than others, and some wear only on one +side. Now, you remark in this note of yours, Mr. Windibank, that +in every case there is some little slurring over of the 'e,' and +a slight defect in the tail of the 'r.' There are fourteen other +characteristics, but those are the more obvious." + +"We do all our correspondence with this machine at the office, +and no doubt it is a little worn," our visitor answered, glancing +keenly at Holmes with his bright little eyes. + +"And now I will show you what is really a very interesting study, +Mr. Windibank," Holmes continued. "I think of writing another +little monograph some of these days on the typewriter and its +relation to crime. It is a subject to which I have devoted some +little attention. I have here four letters which purport to come +from the missing man. They are all typewritten. In each case, not +only are the 'e's' slurred and the 'r's' tailless, but you will +observe, if you care to use my magnifying lens, that the fourteen +other characteristics to which I have alluded are there as well." + +Mr. Windibank sprang out of his chair and picked up his hat. "I +cannot waste time over this sort of fantastic talk, Mr. Holmes," +he said. "If you can catch the man, catch him, and let me know +when you have done it." + +"Certainly," said Holmes, stepping over and turning the key in +the door. "I let you know, then, that I have caught him!" + +"What! where?" shouted Mr. Windibank, turning white to his lips +and glancing about him like a rat in a trap. + +"Oh, it won't do--really it won't," said Holmes suavely. "There +is no possible getting out of it, Mr. Windibank. It is quite too +transparent, and it was a very bad compliment when you said that +it was impossible for me to solve so simple a question. That's +right! Sit down and let us talk it over." + +Our visitor collapsed into a chair, with a ghastly face and a +glitter of moisture on his brow. "It--it's not actionable," he +stammered. + +"I am very much afraid that it is not. But between ourselves, +Windibank, it was as cruel and selfish and heartless a trick in a +petty way as ever came before me. Now, let me just run over the +course of events, and you will contradict me if I go wrong." + +The man sat huddled up in his chair, with his head sunk upon his +breast, like one who is utterly crushed. Holmes stuck his feet up +on the corner of the mantelpiece and, leaning back with his hands +in his pockets, began talking, rather to himself, as it seemed, +than to us. + +"The man married a woman very much older than himself for her +money," said he, "and he enjoyed the use of the money of the +daughter as long as she lived with them. It was a considerable +sum, for people in their position, and the loss of it would have +made a serious difference. It was worth an effort to preserve it. +The daughter was of a good, amiable disposition, but affectionate +and warm-hearted in her ways, so that it was evident that with +her fair personal advantages, and her little income, she would +not be allowed to remain single long. Now her marriage would +mean, of course, the loss of a hundred a year, so what does her +stepfather do to prevent it? He takes the obvious course of +keeping her at home and forbidding her to seek the company of +people of her own age. But soon he found that that would not +answer forever. She became restive, insisted upon her rights, and +finally announced her positive intention of going to a certain +ball. What does her clever stepfather do then? He conceives an +idea more creditable to his head than to his heart. With the +connivance and assistance of his wife he disguised himself, +covered those keen eyes with tinted glasses, masked the face with +a moustache and a pair of bushy whiskers, sunk that clear voice +into an insinuating whisper, and doubly secure on account of the +girl's short sight, he appears as Mr. Hosmer Angel, and keeps off +other lovers by making love himself." + +"It was only a joke at first," groaned our visitor. "We never +thought that she would have been so carried away." + +"Very likely not. However that may be, the young lady was very +decidedly carried away, and, having quite made up her mind that +her stepfather was in France, the suspicion of treachery never +for an instant entered her mind. She was flattered by the +gentleman's attentions, and the effect was increased by the +loudly expressed admiration of her mother. Then Mr. Angel began +to call, for it was obvious that the matter should be pushed as +far as it would go if a real effect were to be produced. There +were meetings, and an engagement, which would finally secure the +girl's affections from turning towards anyone else. But the +deception could not be kept up forever. These pretended journeys +to France were rather cumbrous. The thing to do was clearly to +bring the business to an end in such a dramatic manner that it +would leave a permanent impression upon the young lady's mind and +prevent her from looking upon any other suitor for some time to +come. Hence those vows of fidelity exacted upon a Testament, and +hence also the allusions to a possibility of something happening +on the very morning of the wedding. James Windibank wished Miss +Sutherland to be so bound to Hosmer Angel, and so uncertain as to +his fate, that for ten years to come, at any rate, she would not +listen to another man. As far as the church door he brought her, +and then, as he could go no farther, he conveniently vanished +away by the old trick of stepping in at one door of a +four-wheeler and out at the other. I think that was the chain of +events, Mr. Windibank!" + +Our visitor had recovered something of his assurance while Holmes +had been talking, and he rose from his chair now with a cold +sneer upon his pale face. + +"It may be so, or it may not, Mr. Holmes," said he, "but if you +are so very sharp you ought to be sharp enough to know that it is +you who are breaking the law now, and not me. I have done nothing +actionable from the first, but as long as you keep that door +locked you lay yourself open to an action for assault and illegal +constraint." + +"The law cannot, as you say, touch you," said Holmes, unlocking +and throwing open the door, "yet there never was a man who +deserved punishment more. If the young lady has a brother or a +friend, he ought to lay a whip across your shoulders. By Jove!" +he continued, flushing up at the sight of the bitter sneer upon +the man's face, "it is not part of my duties to my client, but +here's a hunting crop handy, and I think I shall just treat +myself to--" He took two swift steps to the whip, but before he +could grasp it there was a wild clatter of steps upon the stairs, +the heavy hall door banged, and from the window we could see Mr. +James Windibank running at the top of his speed down the road. + +"There's a cold-blooded scoundrel!" said Holmes, laughing, as he +threw himself down into his chair once more. "That fellow will +rise from crime to crime until he does something very bad, and +ends on a gallows. The case has, in some respects, been not +entirely devoid of interest." + +"I cannot now entirely see all the steps of your reasoning," I +remarked. + +"Well, of course it was obvious from the first that this Mr. +Hosmer Angel must have some strong object for his curious +conduct, and it was equally clear that the only man who really +profited by the incident, as far as we could see, was the +stepfather. Then the fact that the two men were never together, +but that the one always appeared when the other was away, was +suggestive. So were the tinted spectacles and the curious voice, +which both hinted at a disguise, as did the bushy whiskers. My +suspicions were all confirmed by his peculiar action in +typewriting his signature, which, of course, inferred that his +handwriting was so familiar to her that she would recognise even +the smallest sample of it. You see all these isolated facts, +together with many minor ones, all pointed in the same +direction." + +"And how did you verify them?" + +"Having once spotted my man, it was easy to get corroboration. I +knew the firm for which this man worked. Having taken the printed +description. I eliminated everything from it which could be the +result of a disguise--the whiskers, the glasses, the voice, and I +sent it to the firm, with a request that they would inform me +whether it answered to the description of any of their +travellers. I had already noticed the peculiarities of the +typewriter, and I wrote to the man himself at his business +address asking him if he would come here. As I expected, his +reply was typewritten and revealed the same trivial but +characteristic defects. The same post brought me a letter from +Westhouse & Marbank, of Fenchurch Street, to say that the +description tallied in every respect with that of their employ, +James Windibank. Voil tout!" + +"And Miss Sutherland?" + +"If I tell her she will not believe me. You may remember the old +Persian saying, 'There is danger for him who taketh the tiger +cub, and danger also for whoso snatches a delusion from a woman.' +There is as much sense in Hafiz as in Horace, and as much +knowledge of the world." + + + +ADVENTURE IV. THE BOSCOMBE VALLEY MYSTERY + +We were seated at breakfast one morning, my wife and I, when the +maid brought in a telegram. It was from Sherlock Holmes and ran +in this way: + +"Have you a couple of days to spare? Have just been wired for from +the west of England in connection with Boscombe Valley tragedy. +Shall be glad if you will come with me. Air and scenery perfect. +Leave Paddington by the 11:15." + +"What do you say, dear?" said my wife, looking across at me. +"Will you go?" + +"I really don't know what to say. I have a fairly long list at +present." + +"Oh, Anstruther would do your work for you. You have been looking +a little pale lately. I think that the change would do you good, +and you are always so interested in Mr. Sherlock Holmes' cases." + +"I should be ungrateful if I were not, seeing what I gained +through one of them," I answered. "But if I am to go, I must pack +at once, for I have only half an hour." + +My experience of camp life in Afghanistan had at least had the +effect of making me a prompt and ready traveller. My wants were +few and simple, so that in less than the time stated I was in a +cab with my valise, rattling away to Paddington Station. Sherlock +Holmes was pacing up and down the platform, his tall, gaunt +figure made even gaunter and taller by his long grey +travelling-cloak and close-fitting cloth cap. + +"It is really very good of you to come, Watson," said he. "It +makes a considerable difference to me, having someone with me on +whom I can thoroughly rely. Local aid is always either worthless +or else biassed. If you will keep the two corner seats I shall +get the tickets." + +We had the carriage to ourselves save for an immense litter of +papers which Holmes had brought with him. Among these he rummaged +and read, with intervals of note-taking and of meditation, until +we were past Reading. Then he suddenly rolled them all into a +gigantic ball and tossed them up onto the rack. + +"Have you heard anything of the case?" he asked. + +"Not a word. I have not seen a paper for some days." + +"The London press has not had very full accounts. I have just +been looking through all the recent papers in order to master the +particulars. It seems, from what I gather, to be one of those +simple cases which are so extremely difficult." + +"That sounds a little paradoxical." + +"But it is profoundly true. Singularity is almost invariably a +clue. The more featureless and commonplace a crime is, the more +difficult it is to bring it home. In this case, however, they +have established a very serious case against the son of the +murdered man." + +"It is a murder, then?" + +"Well, it is conjectured to be so. I shall take nothing for +granted until I have the opportunity of looking personally into +it. I will explain the state of things to you, as far as I have +been able to understand it, in a very few words. + +"Boscombe Valley is a country district not very far from Ross, in +Herefordshire. The largest landed proprietor in that part is a +Mr. John Turner, who made his money in Australia and returned +some years ago to the old country. One of the farms which he +held, that of Hatherley, was let to Mr. Charles McCarthy, who was +also an ex-Australian. The men had known each other in the +colonies, so that it was not unnatural that when they came to +settle down they should do so as near each other as possible. +Turner was apparently the richer man, so McCarthy became his +tenant but still remained, it seems, upon terms of perfect +equality, as they were frequently together. McCarthy had one son, +a lad of eighteen, and Turner had an only daughter of the same +age, but neither of them had wives living. They appear to have +avoided the society of the neighbouring English families and to +have led retired lives, though both the McCarthys were fond of +sport and were frequently seen at the race-meetings of the +neighbourhood. McCarthy kept two servants--a man and a girl. +Turner had a considerable household, some half-dozen at the +least. That is as much as I have been able to gather about the +families. Now for the facts. + +"On June 3rd, that is, on Monday last, McCarthy left his house at +Hatherley about three in the afternoon and walked down to the +Boscombe Pool, which is a small lake formed by the spreading out +of the stream which runs down the Boscombe Valley. He had been +out with his serving-man in the morning at Ross, and he had told +the man that he must hurry, as he had an appointment of +importance to keep at three. From that appointment he never came +back alive. + +"From Hatherley Farm-house to the Boscombe Pool is a quarter of a +mile, and two people saw him as he passed over this ground. One +was an old woman, whose name is not mentioned, and the other was +William Crowder, a game-keeper in the employ of Mr. Turner. Both +these witnesses depose that Mr. McCarthy was walking alone. The +game-keeper adds that within a few minutes of his seeing Mr. +McCarthy pass he had seen his son, Mr. James McCarthy, going the +same way with a gun under his arm. To the best of his belief, the +father was actually in sight at the time, and the son was +following him. He thought no more of the matter until he heard in +the evening of the tragedy that had occurred. + +"The two McCarthys were seen after the time when William Crowder, +the game-keeper, lost sight of them. The Boscombe Pool is thickly +wooded round, with just a fringe of grass and of reeds round the +edge. A girl of fourteen, Patience Moran, who is the daughter of +the lodge-keeper of the Boscombe Valley estate, was in one of the +woods picking flowers. She states that while she was there she +saw, at the border of the wood and close by the lake, Mr. +McCarthy and his son, and that they appeared to be having a +violent quarrel. She heard Mr. McCarthy the elder using very +strong language to his son, and she saw the latter raise up his +hand as if to strike his father. She was so frightened by their +violence that she ran away and told her mother when she reached +home that she had left the two McCarthys quarrelling near +Boscombe Pool, and that she was afraid that they were going to +fight. She had hardly said the words when young Mr. McCarthy came +running up to the lodge to say that he had found his father dead +in the wood, and to ask for the help of the lodge-keeper. He was +much excited, without either his gun or his hat, and his right +hand and sleeve were observed to be stained with fresh blood. On +following him they found the dead body stretched out upon the +grass beside the pool. The head had been beaten in by repeated +blows of some heavy and blunt weapon. The injuries were such as +might very well have been inflicted by the butt-end of his son's +gun, which was found lying on the grass within a few paces of the +body. Under these circumstances the young man was instantly +arrested, and a verdict of 'wilful murder' having been returned +at the inquest on Tuesday, he was on Wednesday brought before the +magistrates at Ross, who have referred the case to the next +Assizes. Those are the main facts of the case as they came out +before the coroner and the police-court." + +"I could hardly imagine a more damning case," I remarked. "If +ever circumstantial evidence pointed to a criminal it does so +here." + +"Circumstantial evidence is a very tricky thing," answered Holmes +thoughtfully. "It may seem to point very straight to one thing, +but if you shift your own point of view a little, you may find it +pointing in an equally uncompromising manner to something +entirely different. It must be confessed, however, that the case +looks exceedingly grave against the young man, and it is very +possible that he is indeed the culprit. There are several people +in the neighbourhood, however, and among them Miss Turner, the +daughter of the neighbouring landowner, who believe in his +innocence, and who have retained Lestrade, whom you may recollect +in connection with the Study in Scarlet, to work out the case in +his interest. Lestrade, being rather puzzled, has referred the +case to me, and hence it is that two middle-aged gentlemen are +flying westward at fifty miles an hour instead of quietly +digesting their breakfasts at home." + +"I am afraid," said I, "that the facts are so obvious that you +will find little credit to be gained out of this case." + +"There is nothing more deceptive than an obvious fact," he +answered, laughing. "Besides, we may chance to hit upon some +other obvious facts which may have been by no means obvious to +Mr. Lestrade. You know me too well to think that I am boasting +when I say that I shall either confirm or destroy his theory by +means which he is quite incapable of employing, or even of +understanding. To take the first example to hand, I very clearly +perceive that in your bedroom the window is upon the right-hand +side, and yet I question whether Mr. Lestrade would have noted +even so self-evident a thing as that." + +"How on earth--" + +"My dear fellow, I know you well. I know the military neatness +which characterises you. You shave every morning, and in this +season you shave by the sunlight; but since your shaving is less +and less complete as we get farther back on the left side, until +it becomes positively slovenly as we get round the angle of the +jaw, it is surely very clear that that side is less illuminated +than the other. I could not imagine a man of your habits looking +at himself in an equal light and being satisfied with such a +result. I only quote this as a trivial example of observation and +inference. Therein lies my mtier, and it is just possible that +it may be of some service in the investigation which lies before +us. There are one or two minor points which were brought out in +the inquest, and which are worth considering." + +"What are they?" + +"It appears that his arrest did not take place at once, but after +the return to Hatherley Farm. On the inspector of constabulary +informing him that he was a prisoner, he remarked that he was not +surprised to hear it, and that it was no more than his deserts. +This observation of his had the natural effect of removing any +traces of doubt which might have remained in the minds of the +coroner's jury." + +"It was a confession," I ejaculated. + +"No, for it was followed by a protestation of innocence." + +"Coming on the top of such a damning series of events, it was at +least a most suspicious remark." + +"On the contrary," said Holmes, "it is the brightest rift which I +can at present see in the clouds. However innocent he might be, +he could not be such an absolute imbecile as not to see that the +circumstances were very black against him. Had he appeared +surprised at his own arrest, or feigned indignation at it, I +should have looked upon it as highly suspicious, because such +surprise or anger would not be natural under the circumstances, +and yet might appear to be the best policy to a scheming man. His +frank acceptance of the situation marks him as either an innocent +man, or else as a man of considerable self-restraint and +firmness. As to his remark about his deserts, it was also not +unnatural if you consider that he stood beside the dead body of +his father, and that there is no doubt that he had that very day +so far forgotten his filial duty as to bandy words with him, and +even, according to the little girl whose evidence is so +important, to raise his hand as if to strike him. The +self-reproach and contrition which are displayed in his remark +appear to me to be the signs of a healthy mind rather than of a +guilty one." + +I shook my head. "Many men have been hanged on far slighter +evidence," I remarked. + +"So they have. And many men have been wrongfully hanged." + +"What is the young man's own account of the matter?" + +"It is, I am afraid, not very encouraging to his supporters, +though there are one or two points in it which are suggestive. +You will find it here, and may read it for yourself." + +He picked out from his bundle a copy of the local Herefordshire +paper, and having turned down the sheet he pointed out the +paragraph in which the unfortunate young man had given his own +statement of what had occurred. I settled myself down in the +corner of the carriage and read it very carefully. It ran in this +way: + +"Mr. James McCarthy, the only son of the deceased, was then called +and gave evidence as follows: 'I had been away from home for +three days at Bristol, and had only just returned upon the +morning of last Monday, the 3rd. My father was absent from home at +the time of my arrival, and I was informed by the maid that he +had driven over to Ross with John Cobb, the groom. Shortly after +my return I heard the wheels of his trap in the yard, and, +looking out of my window, I saw him get out and walk rapidly out +of the yard, though I was not aware in which direction he was +going. I then took my gun and strolled out in the direction of +the Boscombe Pool, with the intention of visiting the rabbit +warren which is upon the other side. On my way I saw William +Crowder, the game-keeper, as he had stated in his evidence; but +he is mistaken in thinking that I was following my father. I had +no idea that he was in front of me. When about a hundred yards +from the pool I heard a cry of "Cooee!" which was a usual signal +between my father and myself. I then hurried forward, and found +him standing by the pool. He appeared to be much surprised at +seeing me and asked me rather roughly what I was doing there. A +conversation ensued which led to high words and almost to blows, +for my father was a man of a very violent temper. Seeing that his +passion was becoming ungovernable, I left him and returned +towards Hatherley Farm. I had not gone more than 150 yards, +however, when I heard a hideous outcry behind me, which caused me +to run back again. I found my father expiring upon the ground, +with his head terribly injured. I dropped my gun and held him in +my arms, but he almost instantly expired. I knelt beside him for +some minutes, and then made my way to Mr. Turner's lodge-keeper, +his house being the nearest, to ask for assistance. I saw no one +near my father when I returned, and I have no idea how he came by +his injuries. He was not a popular man, being somewhat cold and +forbidding in his manners, but he had, as far as I know, no +active enemies. I know nothing further of the matter.' + +"The Coroner: Did your father make any statement to you before +he died? + +"Witness: He mumbled a few words, but I could only catch some +allusion to a rat. + +"The Coroner: What did you understand by that? + +"Witness: It conveyed no meaning to me. I thought that he was +delirious. + +"The Coroner: What was the point upon which you and your father +had this final quarrel? + +"Witness: I should prefer not to answer. + +"The Coroner: I am afraid that I must press it. + +"Witness: It is really impossible for me to tell you. I can +assure you that it has nothing to do with the sad tragedy which +followed. + +"The Coroner: That is for the court to decide. I need not point +out to you that your refusal to answer will prejudice your case +considerably in any future proceedings which may arise. + +"Witness: I must still refuse. + +"The Coroner: I understand that the cry of 'Cooee' was a common +signal between you and your father? + +"Witness: It was. + +"The Coroner: How was it, then, that he uttered it before he saw +you, and before he even knew that you had returned from Bristol? + +"Witness (with considerable confusion): I do not know. + +"A Juryman: Did you see nothing which aroused your suspicions +when you returned on hearing the cry and found your father +fatally injured? + +"Witness: Nothing definite. + +"The Coroner: What do you mean? + +"Witness: I was so disturbed and excited as I rushed out into +the open, that I could think of nothing except of my father. Yet +I have a vague impression that as I ran forward something lay +upon the ground to the left of me. It seemed to me to be +something grey in colour, a coat of some sort, or a plaid perhaps. +When I rose from my father I looked round for it, but it was +gone. + +"'Do you mean that it disappeared before you went for help?' + +"'Yes, it was gone.' + +"'You cannot say what it was?' + +"'No, I had a feeling something was there.' + +"'How far from the body?' + +"'A dozen yards or so.' + +"'And how far from the edge of the wood?' + +"'About the same.' + +"'Then if it was removed it was while you were within a dozen +yards of it?' + +"'Yes, but with my back towards it.' + +"This concluded the examination of the witness." + +"I see," said I as I glanced down the column, "that the coroner +in his concluding remarks was rather severe upon young McCarthy. +He calls attention, and with reason, to the discrepancy about his +father having signalled to him before seeing him, also to his +refusal to give details of his conversation with his father, and +his singular account of his father's dying words. They are all, +as he remarks, very much against the son." + +Holmes laughed softly to himself and stretched himself out upon +the cushioned seat. "Both you and the coroner have been at some +pains," said he, "to single out the very strongest points in the +young man's favour. Don't you see that you alternately give him +credit for having too much imagination and too little? Too +little, if he could not invent a cause of quarrel which would +give him the sympathy of the jury; too much, if he evolved from +his own inner consciousness anything so outr as a dying +reference to a rat, and the incident of the vanishing cloth. No, +sir, I shall approach this case from the point of view that what +this young man says is true, and we shall see whither that +hypothesis will lead us. And now here is my pocket Petrarch, and +not another word shall I say of this case until we are on the +scene of action. We lunch at Swindon, and I see that we shall be +there in twenty minutes." + +It was nearly four o'clock when we at last, after passing through +the beautiful Stroud Valley, and over the broad gleaming Severn, +found ourselves at the pretty little country-town of Ross. A +lean, ferret-like man, furtive and sly-looking, was waiting for +us upon the platform. In spite of the light brown dustcoat and +leather-leggings which he wore in deference to his rustic +surroundings, I had no difficulty in recognising Lestrade, of +Scotland Yard. With him we drove to the Hereford Arms where a +room had already been engaged for us. + +"I have ordered a carriage," said Lestrade as we sat over a cup +of tea. "I knew your energetic nature, and that you would not be +happy until you had been on the scene of the crime." + +"It was very nice and complimentary of you," Holmes answered. "It +is entirely a question of barometric pressure." + +Lestrade looked startled. "I do not quite follow," he said. + +"How is the glass? Twenty-nine, I see. No wind, and not a cloud +in the sky. I have a caseful of cigarettes here which need +smoking, and the sofa is very much superior to the usual country +hotel abomination. I do not think that it is probable that I +shall use the carriage to-night." + +Lestrade laughed indulgently. "You have, no doubt, already formed +your conclusions from the newspapers," he said. "The case is as +plain as a pikestaff, and the more one goes into it the plainer +it becomes. Still, of course, one can't refuse a lady, and such a +very positive one, too. She has heard of you, and would have your +opinion, though I repeatedly told her that there was nothing +which you could do which I had not already done. Why, bless my +soul! here is her carriage at the door." + +He had hardly spoken before there rushed into the room one of the +most lovely young women that I have ever seen in my life. Her +violet eyes shining, her lips parted, a pink flush upon her +cheeks, all thought of her natural reserve lost in her +overpowering excitement and concern. + +"Oh, Mr. Sherlock Holmes!" she cried, glancing from one to the +other of us, and finally, with a woman's quick intuition, +fastening upon my companion, "I am so glad that you have come. I +have driven down to tell you so. I know that James didn't do it. +I know it, and I want you to start upon your work knowing it, +too. Never let yourself doubt upon that point. We have known each +other since we were little children, and I know his faults as no +one else does; but he is too tender-hearted to hurt a fly. Such a +charge is absurd to anyone who really knows him." + +"I hope we may clear him, Miss Turner," said Sherlock Holmes. +"You may rely upon my doing all that I can." + +"But you have read the evidence. You have formed some conclusion? +Do you not see some loophole, some flaw? Do you not yourself +think that he is innocent?" + +"I think that it is very probable." + +"There, now!" she cried, throwing back her head and looking +defiantly at Lestrade. "You hear! He gives me hopes." + +Lestrade shrugged his shoulders. "I am afraid that my colleague +has been a little quick in forming his conclusions," he said. + +"But he is right. Oh! I know that he is right. James never did +it. And about his quarrel with his father, I am sure that the +reason why he would not speak about it to the coroner was because +I was concerned in it." + +"In what way?" asked Holmes. + +"It is no time for me to hide anything. James and his father had +many disagreements about me. Mr. McCarthy was very anxious that +there should be a marriage between us. James and I have always +loved each other as brother and sister; but of course he is young +and has seen very little of life yet, and--and--well, he +naturally did not wish to do anything like that yet. So there +were quarrels, and this, I am sure, was one of them." + +"And your father?" asked Holmes. "Was he in favour of such a +union?" + +"No, he was averse to it also. No one but Mr. McCarthy was in +favour of it." A quick blush passed over her fresh young face as +Holmes shot one of his keen, questioning glances at her. + +"Thank you for this information," said he. "May I see your father +if I call to-morrow?" + +"I am afraid the doctor won't allow it." + +"The doctor?" + +"Yes, have you not heard? Poor father has never been strong for +years back, but this has broken him down completely. He has taken +to his bed, and Dr. Willows says that he is a wreck and that his +nervous system is shattered. Mr. McCarthy was the only man alive +who had known dad in the old days in Victoria." + +"Ha! In Victoria! That is important." + +"Yes, at the mines." + +"Quite so; at the gold-mines, where, as I understand, Mr. Turner +made his money." + +"Yes, certainly." + +"Thank you, Miss Turner. You have been of material assistance to +me." + +"You will tell me if you have any news to-morrow. No doubt you +will go to the prison to see James. Oh, if you do, Mr. Holmes, do +tell him that I know him to be innocent." + +"I will, Miss Turner." + +"I must go home now, for dad is very ill, and he misses me so if +I leave him. Good-bye, and God help you in your undertaking." She +hurried from the room as impulsively as she had entered, and we +heard the wheels of her carriage rattle off down the street. + +"I am ashamed of you, Holmes," said Lestrade with dignity after a +few minutes' silence. "Why should you raise up hopes which you +are bound to disappoint? I am not over-tender of heart, but I +call it cruel." + +"I think that I see my way to clearing James McCarthy," said +Holmes. "Have you an order to see him in prison?" + +"Yes, but only for you and me." + +"Then I shall reconsider my resolution about going out. We have +still time to take a train to Hereford and see him to-night?" + +"Ample." + +"Then let us do so. Watson, I fear that you will find it very +slow, but I shall only be away a couple of hours." + +I walked down to the station with them, and then wandered through +the streets of the little town, finally returning to the hotel, +where I lay upon the sofa and tried to interest myself in a +yellow-backed novel. The puny plot of the story was so thin, +however, when compared to the deep mystery through which we were +groping, and I found my attention wander so continually from the +action to the fact, that I at last flung it across the room and +gave myself up entirely to a consideration of the events of the +day. Supposing that this unhappy young man's story were +absolutely true, then what hellish thing, what absolutely +unforeseen and extraordinary calamity could have occurred between +the time when he parted from his father, and the moment when, +drawn back by his screams, he rushed into the glade? It was +something terrible and deadly. What could it be? Might not the +nature of the injuries reveal something to my medical instincts? +I rang the bell and called for the weekly county paper, which +contained a verbatim account of the inquest. In the surgeon's +deposition it was stated that the posterior third of the left +parietal bone and the left half of the occipital bone had been +shattered by a heavy blow from a blunt weapon. I marked the spot +upon my own head. Clearly such a blow must have been struck from +behind. That was to some extent in favour of the accused, as when +seen quarrelling he was face to face with his father. Still, it +did not go for very much, for the older man might have turned his +back before the blow fell. Still, it might be worth while to call +Holmes' attention to it. Then there was the peculiar dying +reference to a rat. What could that mean? It could not be +delirium. A man dying from a sudden blow does not commonly become +delirious. No, it was more likely to be an attempt to explain how +he met his fate. But what could it indicate? I cudgelled my +brains to find some possible explanation. And then the incident +of the grey cloth seen by young McCarthy. If that were true the +murderer must have dropped some part of his dress, presumably his +overcoat, in his flight, and must have had the hardihood to +return and to carry it away at the instant when the son was +kneeling with his back turned not a dozen paces off. What a +tissue of mysteries and improbabilities the whole thing was! I +did not wonder at Lestrade's opinion, and yet I had so much faith +in Sherlock Holmes' insight that I could not lose hope as long +as every fresh fact seemed to strengthen his conviction of young +McCarthy's innocence. + +It was late before Sherlock Holmes returned. He came back alone, +for Lestrade was staying in lodgings in the town. + +"The glass still keeps very high," he remarked as he sat down. +"It is of importance that it should not rain before we are able +to go over the ground. On the other hand, a man should be at his +very best and keenest for such nice work as that, and I did not +wish to do it when fagged by a long journey. I have seen young +McCarthy." + +"And what did you learn from him?" + +"Nothing." + +"Could he throw no light?" + +"None at all. I was inclined to think at one time that he knew +who had done it and was screening him or her, but I am convinced +now that he is as puzzled as everyone else. He is not a very +quick-witted youth, though comely to look at and, I should think, +sound at heart." + +"I cannot admire his taste," I remarked, "if it is indeed a fact +that he was averse to a marriage with so charming a young lady as +this Miss Turner." + +"Ah, thereby hangs a rather painful tale. This fellow is madly, +insanely, in love with her, but some two years ago, when he was +only a lad, and before he really knew her, for she had been away +five years at a boarding-school, what does the idiot do but get +into the clutches of a barmaid in Bristol and marry her at a +registry office? No one knows a word of the matter, but you can +imagine how maddening it must be to him to be upbraided for not +doing what he would give his very eyes to do, but what he knows +to be absolutely impossible. It was sheer frenzy of this sort +which made him throw his hands up into the air when his father, +at their last interview, was goading him on to propose to Miss +Turner. On the other hand, he had no means of supporting himself, +and his father, who was by all accounts a very hard man, would +have thrown him over utterly had he known the truth. It was with +his barmaid wife that he had spent the last three days in +Bristol, and his father did not know where he was. Mark that +point. It is of importance. Good has come out of evil, however, +for the barmaid, finding from the papers that he is in serious +trouble and likely to be hanged, has thrown him over utterly and +has written to him to say that she has a husband already in the +Bermuda Dockyard, so that there is really no tie between them. I +think that that bit of news has consoled young McCarthy for all +that he has suffered." + +"But if he is innocent, who has done it?" + +"Ah! who? I would call your attention very particularly to two +points. One is that the murdered man had an appointment with +someone at the pool, and that the someone could not have been his +son, for his son was away, and he did not know when he would +return. The second is that the murdered man was heard to cry +'Cooee!' before he knew that his son had returned. Those are the +crucial points upon which the case depends. And now let us talk +about George Meredith, if you please, and we shall leave all +minor matters until to-morrow." + +There was no rain, as Holmes had foretold, and the morning broke +bright and cloudless. At nine o'clock Lestrade called for us with +the carriage, and we set off for Hatherley Farm and the Boscombe +Pool. + +"There is serious news this morning," Lestrade observed. "It is +said that Mr. Turner, of the Hall, is so ill that his life is +despaired of." + +"An elderly man, I presume?" said Holmes. + +"About sixty; but his constitution has been shattered by his life +abroad, and he has been in failing health for some time. This +business has had a very bad effect upon him. He was an old friend +of McCarthy's, and, I may add, a great benefactor to him, for I +have learned that he gave him Hatherley Farm rent free." + +"Indeed! That is interesting," said Holmes. + +"Oh, yes! In a hundred other ways he has helped him. Everybody +about here speaks of his kindness to him." + +"Really! Does it not strike you as a little singular that this +McCarthy, who appears to have had little of his own, and to have +been under such obligations to Turner, should still talk of +marrying his son to Turner's daughter, who is, presumably, +heiress to the estate, and that in such a very cocksure manner, +as if it were merely a case of a proposal and all else would +follow? It is the more strange, since we know that Turner himself +was averse to the idea. The daughter told us as much. Do you not +deduce something from that?" + +"We have got to the deductions and the inferences," said +Lestrade, winking at me. "I find it hard enough to tackle facts, +Holmes, without flying away after theories and fancies." + +"You are right," said Holmes demurely; "you do find it very hard +to tackle the facts." + +"Anyhow, I have grasped one fact which you seem to find it +difficult to get hold of," replied Lestrade with some warmth. + +"And that is--" + +"That McCarthy senior met his death from McCarthy junior and that +all theories to the contrary are the merest moonshine." + +"Well, moonshine is a brighter thing than fog," said Holmes, +laughing. "But I am very much mistaken if this is not Hatherley +Farm upon the left." + +"Yes, that is it." It was a widespread, comfortable-looking +building, two-storied, slate-roofed, with great yellow blotches +of lichen upon the grey walls. The drawn blinds and the smokeless +chimneys, however, gave it a stricken look, as though the weight +of this horror still lay heavy upon it. We called at the door, +when the maid, at Holmes' request, showed us the boots which her +master wore at the time of his death, and also a pair of the +son's, though not the pair which he had then had. Having measured +these very carefully from seven or eight different points, Holmes +desired to be led to the court-yard, from which we all followed +the winding track which led to Boscombe Pool. + +Sherlock Holmes was transformed when he was hot upon such a scent +as this. Men who had only known the quiet thinker and logician of +Baker Street would have failed to recognise him. His face flushed +and darkened. His brows were drawn into two hard black lines, +while his eyes shone out from beneath them with a steely glitter. +His face was bent downward, his shoulders bowed, his lips +compressed, and the veins stood out like whipcord in his long, +sinewy neck. His nostrils seemed to dilate with a purely animal +lust for the chase, and his mind was so absolutely concentrated +upon the matter before him that a question or remark fell +unheeded upon his ears, or, at the most, only provoked a quick, +impatient snarl in reply. Swiftly and silently he made his way +along the track which ran through the meadows, and so by way of +the woods to the Boscombe Pool. It was damp, marshy ground, as is +all that district, and there were marks of many feet, both upon +the path and amid the short grass which bounded it on either +side. Sometimes Holmes would hurry on, sometimes stop dead, and +once he made quite a little detour into the meadow. Lestrade and +I walked behind him, the detective indifferent and contemptuous, +while I watched my friend with the interest which sprang from the +conviction that every one of his actions was directed towards a +definite end. + +The Boscombe Pool, which is a little reed-girt sheet of water +some fifty yards across, is situated at the boundary between the +Hatherley Farm and the private park of the wealthy Mr. Turner. +Above the woods which lined it upon the farther side we could see +the red, jutting pinnacles which marked the site of the rich +landowner's dwelling. On the Hatherley side of the pool the woods +grew very thick, and there was a narrow belt of sodden grass +twenty paces across between the edge of the trees and the reeds +which lined the lake. Lestrade showed us the exact spot at which +the body had been found, and, indeed, so moist was the ground, +that I could plainly see the traces which had been left by the +fall of the stricken man. To Holmes, as I could see by his eager +face and peering eyes, very many other things were to be read +upon the trampled grass. He ran round, like a dog who is picking +up a scent, and then turned upon my companion. + +"What did you go into the pool for?" he asked. + +"I fished about with a rake. I thought there might be some weapon +or other trace. But how on earth--" + +"Oh, tut, tut! I have no time! That left foot of yours with its +inward twist is all over the place. A mole could trace it, and +there it vanishes among the reeds. Oh, how simple it would all +have been had I been here before they came like a herd of buffalo +and wallowed all over it. Here is where the party with the +lodge-keeper came, and they have covered all tracks for six or +eight feet round the body. But here are three separate tracks of +the same feet." He drew out a lens and lay down upon his +waterproof to have a better view, talking all the time rather to +himself than to us. "These are young McCarthy's feet. Twice he +was walking, and once he ran swiftly, so that the soles are +deeply marked and the heels hardly visible. That bears out his +story. He ran when he saw his father on the ground. Then here are +the father's feet as he paced up and down. What is this, then? It +is the butt-end of the gun as the son stood listening. And this? +Ha, ha! What have we here? Tiptoes! tiptoes! Square, too, quite +unusual boots! They come, they go, they come again--of course +that was for the cloak. Now where did they come from?" He ran up +and down, sometimes losing, sometimes finding the track until we +were well within the edge of the wood and under the shadow of a +great beech, the largest tree in the neighbourhood. Holmes traced +his way to the farther side of this and lay down once more upon +his face with a little cry of satisfaction. For a long time he +remained there, turning over the leaves and dried sticks, +gathering up what seemed to me to be dust into an envelope and +examining with his lens not only the ground but even the bark of +the tree as far as he could reach. A jagged stone was lying among +the moss, and this also he carefully examined and retained. Then +he followed a pathway through the wood until he came to the +highroad, where all traces were lost. + +"It has been a case of considerable interest," he remarked, +returning to his natural manner. "I fancy that this grey house on +the right must be the lodge. I think that I will go in and have a +word with Moran, and perhaps write a little note. Having done +that, we may drive back to our luncheon. You may walk to the cab, +and I shall be with you presently." + +It was about ten minutes before we regained our cab and drove +back into Ross, Holmes still carrying with him the stone which he +had picked up in the wood. + +"This may interest you, Lestrade," he remarked, holding it out. +"The murder was done with it." + +"I see no marks." + +"There are none." + +"How do you know, then?" + +"The grass was growing under it. It had only lain there a few +days. There was no sign of a place whence it had been taken. It +corresponds with the injuries. There is no sign of any other +weapon." + +"And the murderer?" + +"Is a tall man, left-handed, limps with the right leg, wears +thick-soled shooting-boots and a grey cloak, smokes Indian +cigars, uses a cigar-holder, and carries a blunt pen-knife in his +pocket. There are several other indications, but these may be +enough to aid us in our search." + +Lestrade laughed. "I am afraid that I am still a sceptic," he +said. "Theories are all very well, but we have to deal with a +hard-headed British jury." + +"Nous verrons," answered Holmes calmly. "You work your own +method, and I shall work mine. I shall be busy this afternoon, +and shall probably return to London by the evening train." + +"And leave your case unfinished?" + +"No, finished." + +"But the mystery?" + +"It is solved." + +"Who was the criminal, then?" + +"The gentleman I describe." + +"But who is he?" + +"Surely it would not be difficult to find out. This is not such a +populous neighbourhood." + +Lestrade shrugged his shoulders. "I am a practical man," he said, +"and I really cannot undertake to go about the country looking +for a left-handed gentleman with a game leg. I should become the +laughing-stock of Scotland Yard." + +"All right," said Holmes quietly. "I have given you the chance. +Here are your lodgings. Good-bye. I shall drop you a line before +I leave." + +Having left Lestrade at his rooms, we drove to our hotel, where +we found lunch upon the table. Holmes was silent and buried in +thought with a pained expression upon his face, as one who finds +himself in a perplexing position. + +"Look here, Watson," he said when the cloth was cleared "just sit +down in this chair and let me preach to you for a little. I don't +know quite what to do, and I should value your advice. Light a +cigar and let me expound." + + "Pray do so." + +"Well, now, in considering this case there are two points about +young McCarthy's narrative which struck us both instantly, +although they impressed me in his favour and you against him. One +was the fact that his father should, according to his account, +cry 'Cooee!' before seeing him. The other was his singular dying +reference to a rat. He mumbled several words, you understand, but +that was all that caught the son's ear. Now from this double +point our research must commence, and we will begin it by +presuming that what the lad says is absolutely true." + +"What of this 'Cooee!' then?" + +"Well, obviously it could not have been meant for the son. The +son, as far as he knew, was in Bristol. It was mere chance that +he was within earshot. The 'Cooee!' was meant to attract the +attention of whoever it was that he had the appointment with. But +'Cooee' is a distinctly Australian cry, and one which is used +between Australians. There is a strong presumption that the +person whom McCarthy expected to meet him at Boscombe Pool was +someone who had been in Australia." + +"What of the rat, then?" + +Sherlock Holmes took a folded paper from his pocket and flattened +it out on the table. "This is a map of the Colony of Victoria," +he said. "I wired to Bristol for it last night." He put his hand +over part of the map. "What do you read?" + +"ARAT," I read. + +"And now?" He raised his hand. + +"BALLARAT." + +"Quite so. That was the word the man uttered, and of which his +son only caught the last two syllables. He was trying to utter +the name of his murderer. So and so, of Ballarat." + +"It is wonderful!" I exclaimed. + +"It is obvious. And now, you see, I had narrowed the field down +considerably. The possession of a grey garment was a third point +which, granting the son's statement to be correct, was a +certainty. We have come now out of mere vagueness to the definite +conception of an Australian from Ballarat with a grey cloak." + +"Certainly." + +"And one who was at home in the district, for the pool can only +be approached by the farm or by the estate, where strangers could +hardly wander." + +"Quite so." + +"Then comes our expedition of to-day. By an examination of the +ground I gained the trifling details which I gave to that +imbecile Lestrade, as to the personality of the criminal." + +"But how did you gain them?" + +"You know my method. It is founded upon the observation of +trifles." + +"His height I know that you might roughly judge from the length +of his stride. His boots, too, might be told from their traces." + +"Yes, they were peculiar boots." + +"But his lameness?" + +"The impression of his right foot was always less distinct than +his left. He put less weight upon it. Why? Because he limped--he +was lame." + +"But his left-handedness." + +"You were yourself struck by the nature of the injury as recorded +by the surgeon at the inquest. The blow was struck from +immediately behind, and yet was upon the left side. Now, how can +that be unless it were by a left-handed man? He had stood behind +that tree during the interview between the father and son. He had +even smoked there. I found the ash of a cigar, which my special +knowledge of tobacco ashes enables me to pronounce as an Indian +cigar. I have, as you know, devoted some attention to this, and +written a little monograph on the ashes of 140 different +varieties of pipe, cigar, and cigarette tobacco. Having found the +ash, I then looked round and discovered the stump among the moss +where he had tossed it. It was an Indian cigar, of the variety +which are rolled in Rotterdam." + +"And the cigar-holder?" + +"I could see that the end had not been in his mouth. Therefore he +used a holder. The tip had been cut off, not bitten off, but the +cut was not a clean one, so I deduced a blunt pen-knife." + +"Holmes," I said, "you have drawn a net round this man from which +he cannot escape, and you have saved an innocent human life as +truly as if you had cut the cord which was hanging him. I see the +direction in which all this points. The culprit is--" + +"Mr. John Turner," cried the hotel waiter, opening the door of +our sitting-room, and ushering in a visitor. + +The man who entered was a strange and impressive figure. His +slow, limping step and bowed shoulders gave the appearance of +decrepitude, and yet his hard, deep-lined, craggy features, and +his enormous limbs showed that he was possessed of unusual +strength of body and of character. His tangled beard, grizzled +hair, and outstanding, drooping eyebrows combined to give an air +of dignity and power to his appearance, but his face was of an +ashen white, while his lips and the corners of his nostrils were +tinged with a shade of blue. It was clear to me at a glance that +he was in the grip of some deadly and chronic disease. + +"Pray sit down on the sofa," said Holmes gently. "You had my +note?" + +"Yes, the lodge-keeper brought it up. You said that you wished to +see me here to avoid scandal." + +"I thought people would talk if I went to the Hall." + +"And why did you wish to see me?" He looked across at my +companion with despair in his weary eyes, as though his question +was already answered. + +"Yes," said Holmes, answering the look rather than the words. "It +is so. I know all about McCarthy." + +The old man sank his face in his hands. "God help me!" he cried. +"But I would not have let the young man come to harm. I give you +my word that I would have spoken out if it went against him at +the Assizes." + +"I am glad to hear you say so," said Holmes gravely. + +"I would have spoken now had it not been for my dear girl. It +would break her heart--it will break her heart when she hears +that I am arrested." + +"It may not come to that," said Holmes. + +"What?" + +"I am no official agent. I understand that it was your daughter +who required my presence here, and I am acting in her interests. +Young McCarthy must be got off, however." + +"I am a dying man," said old Turner. "I have had diabetes for +years. My doctor says it is a question whether I shall live a +month. Yet I would rather die under my own roof than in a gaol." + +Holmes rose and sat down at the table with his pen in his hand +and a bundle of paper before him. "Just tell us the truth," he +said. "I shall jot down the facts. You will sign it, and Watson +here can witness it. Then I could produce your confession at the +last extremity to save young McCarthy. I promise you that I shall +not use it unless it is absolutely needed." + +"It's as well," said the old man; "it's a question whether I +shall live to the Assizes, so it matters little to me, but I +should wish to spare Alice the shock. And now I will make the +thing clear to you; it has been a long time in the acting, but +will not take me long to tell. + +"You didn't know this dead man, McCarthy. He was a devil +incarnate. I tell you that. God keep you out of the clutches of +such a man as he. His grip has been upon me these twenty years, +and he has blasted my life. I'll tell you first how I came to be +in his power. + +"It was in the early '60's at the diggings. I was a young chap +then, hot-blooded and reckless, ready to turn my hand at +anything; I got among bad companions, took to drink, had no luck +with my claim, took to the bush, and in a word became what you +would call over here a highway robber. There were six of us, and +we had a wild, free life of it, sticking up a station from time +to time, or stopping the wagons on the road to the diggings. +Black Jack of Ballarat was the name I went under, and our party +is still remembered in the colony as the Ballarat Gang. + +"One day a gold convoy came down from Ballarat to Melbourne, and +we lay in wait for it and attacked it. There were six troopers +and six of us, so it was a close thing, but we emptied four of +their saddles at the first volley. Three of our boys were killed, +however, before we got the swag. I put my pistol to the head of +the wagon-driver, who was this very man McCarthy. I wish to the +Lord that I had shot him then, but I spared him, though I saw his +wicked little eyes fixed on my face, as though to remember every +feature. We got away with the gold, became wealthy men, and made +our way over to England without being suspected. There I parted +from my old pals and determined to settle down to a quiet and +respectable life. I bought this estate, which chanced to be in +the market, and I set myself to do a little good with my money, +to make up for the way in which I had earned it. I married, too, +and though my wife died young she left me my dear little Alice. +Even when she was just a baby her wee hand seemed to lead me down +the right path as nothing else had ever done. In a word, I turned +over a new leaf and did my best to make up for the past. All was +going well when McCarthy laid his grip upon me. + +"I had gone up to town about an investment, and I met him in +Regent Street with hardly a coat to his back or a boot to his +foot. + +"'Here we are, Jack,' says he, touching me on the arm; 'we'll be +as good as a family to you. There's two of us, me and my son, and +you can have the keeping of us. If you don't--it's a fine, +law-abiding country is England, and there's always a policeman +within hail.' + +"Well, down they came to the west country, there was no shaking +them off, and there they have lived rent free on my best land +ever since. There was no rest for me, no peace, no forgetfulness; +turn where I would, there was his cunning, grinning face at my +elbow. It grew worse as Alice grew up, for he soon saw I was more +afraid of her knowing my past than of the police. Whatever he +wanted he must have, and whatever it was I gave him without +question, land, money, houses, until at last he asked a thing +which I could not give. He asked for Alice. + +"His son, you see, had grown up, and so had my girl, and as I was +known to be in weak health, it seemed a fine stroke to him that +his lad should step into the whole property. But there I was +firm. I would not have his cursed stock mixed with mine; not that +I had any dislike to the lad, but his blood was in him, and that +was enough. I stood firm. McCarthy threatened. I braved him to do +his worst. We were to meet at the pool midway between our houses +to talk it over. + +"When I went down there I found him talking with his son, so I +smoked a cigar and waited behind a tree until he should be alone. +But as I listened to his talk all that was black and bitter in +me seemed to come uppermost. He was urging his son to marry my +daughter with as little regard for what she might think as if she +were a slut from off the streets. It drove me mad to think that I +and all that I held most dear should be in the power of such a +man as this. Could I not snap the bond? I was already a dying and +a desperate man. Though clear of mind and fairly strong of limb, +I knew that my own fate was sealed. But my memory and my girl! +Both could be saved if I could but silence that foul tongue. I +did it, Mr. Holmes. I would do it again. Deeply as I have sinned, +I have led a life of martyrdom to atone for it. But that my girl +should be entangled in the same meshes which held me was more +than I could suffer. I struck him down with no more compunction +than if he had been some foul and venomous beast. His cry brought +back his son; but I had gained the cover of the wood, though I +was forced to go back to fetch the cloak which I had dropped in +my flight. That is the true story, gentlemen, of all that +occurred." + +"Well, it is not for me to judge you," said Holmes as the old man +signed the statement which had been drawn out. "I pray that we +may never be exposed to such a temptation." + +"I pray not, sir. And what do you intend to do?" + +"In view of your health, nothing. You are yourself aware that you +will soon have to answer for your deed at a higher court than the +Assizes. I will keep your confession, and if McCarthy is +condemned I shall be forced to use it. If not, it shall never be +seen by mortal eye; and your secret, whether you be alive or +dead, shall be safe with us." + +"Farewell, then," said the old man solemnly. "Your own deathbeds, +when they come, will be the easier for the thought of the peace +which you have given to mine." Tottering and shaking in all his +giant frame, he stumbled slowly from the room. + +"God help us!" said Holmes after a long silence. "Why does fate +play such tricks with poor, helpless worms? I never hear of such +a case as this that I do not think of Baxter's words, and say, +'There, but for the grace of God, goes Sherlock Holmes.'" + +James McCarthy was acquitted at the Assizes on the strength of a +number of objections which had been drawn out by Holmes and +submitted to the defending counsel. Old Turner lived for seven +months after our interview, but he is now dead; and there is +every prospect that the son and daughter may come to live happily +together in ignorance of the black cloud which rests upon their +past. + + + +ADVENTURE V. THE FIVE ORANGE PIPS + +When I glance over my notes and records of the Sherlock Holmes +cases between the years '82 and '90, I am faced by so many which +present strange and interesting features that it is no easy +matter to know which to choose and which to leave. Some, however, +have already gained publicity through the papers, and others have +not offered a field for those peculiar qualities which my friend +possessed in so high a degree, and which it is the object of +these papers to illustrate. Some, too, have baffled his +analytical skill, and would be, as narratives, beginnings without +an ending, while others have been but partially cleared up, and +have their explanations founded rather upon conjecture and +surmise than on that absolute logical proof which was so dear to +him. There is, however, one of these last which was so remarkable +in its details and so startling in its results that I am tempted +to give some account of it in spite of the fact that there are +points in connection with it which never have been, and probably +never will be, entirely cleared up. + +The year '87 furnished us with a long series of cases of greater +or less interest, of which I retain the records. Among my +headings under this one twelve months I find an account of the +adventure of the Paradol Chamber, of the Amateur Mendicant +Society, who held a luxurious club in the lower vault of a +furniture warehouse, of the facts connected with the loss of the +British barque "Sophy Anderson", of the singular adventures of the +Grice Patersons in the island of Uffa, and finally of the +Camberwell poisoning case. In the latter, as may be remembered, +Sherlock Holmes was able, by winding up the dead man's watch, to +prove that it had been wound up two hours before, and that +therefore the deceased had gone to bed within that time--a +deduction which was of the greatest importance in clearing up the +case. All these I may sketch out at some future date, but none of +them present such singular features as the strange train of +circumstances which I have now taken up my pen to describe. + +It was in the latter days of September, and the equinoctial gales +had set in with exceptional violence. All day the wind had +screamed and the rain had beaten against the windows, so that +even here in the heart of great, hand-made London we were forced +to raise our minds for the instant from the routine of life and +to recognise the presence of those great elemental forces which +shriek at mankind through the bars of his civilisation, like +untamed beasts in a cage. As evening drew in, the storm grew +higher and louder, and the wind cried and sobbed like a child in +the chimney. Sherlock Holmes sat moodily at one side of the +fireplace cross-indexing his records of crime, while I at the +other was deep in one of Clark Russell's fine sea-stories until +the howl of the gale from without seemed to blend with the text, +and the splash of the rain to lengthen out into the long swash of +the sea waves. My wife was on a visit to her mother's, and for a +few days I was a dweller once more in my old quarters at Baker +Street. + +"Why," said I, glancing up at my companion, "that was surely the +bell. Who could come to-night? Some friend of yours, perhaps?" + +"Except yourself I have none," he answered. "I do not encourage +visitors." + +"A client, then?" + +"If so, it is a serious case. Nothing less would bring a man out +on such a day and at such an hour. But I take it that it is more +likely to be some crony of the landlady's." + +Sherlock Holmes was wrong in his conjecture, however, for there +came a step in the passage and a tapping at the door. He +stretched out his long arm to turn the lamp away from himself and +towards the vacant chair upon which a newcomer must sit. + +"Come in!" said he. + +The man who entered was young, some two-and-twenty at the +outside, well-groomed and trimly clad, with something of +refinement and delicacy in his bearing. The streaming umbrella +which he held in his hand, and his long shining waterproof told +of the fierce weather through which he had come. He looked about +him anxiously in the glare of the lamp, and I could see that his +face was pale and his eyes heavy, like those of a man who is +weighed down with some great anxiety. + +"I owe you an apology," he said, raising his golden pince-nez to +his eyes. "I trust that I am not intruding. I fear that I have +brought some traces of the storm and rain into your snug +chamber." + +"Give me your coat and umbrella," said Holmes. "They may rest +here on the hook and will be dry presently. You have come up from +the south-west, I see." + +"Yes, from Horsham." + +"That clay and chalk mixture which I see upon your toe caps is +quite distinctive." + +"I have come for advice." + +"That is easily got." + +"And help." + +"That is not always so easy." + +"I have heard of you, Mr. Holmes. I heard from Major Prendergast +how you saved him in the Tankerville Club scandal." + +"Ah, of course. He was wrongfully accused of cheating at cards." + +"He said that you could solve anything." + +"He said too much." + +"That you are never beaten." + +"I have been beaten four times--three times by men, and once by a +woman." + +"But what is that compared with the number of your successes?" + +"It is true that I have been generally successful." + +"Then you may be so with me." + +"I beg that you will draw your chair up to the fire and favour me +with some details as to your case." + +"It is no ordinary one." + +"None of those which come to me are. I am the last court of +appeal." + +"And yet I question, sir, whether, in all your experience, you +have ever listened to a more mysterious and inexplicable chain of +events than those which have happened in my own family." + +"You fill me with interest," said Holmes. "Pray give us the +essential facts from the commencement, and I can afterwards +question you as to those details which seem to me to be most +important." + +The young man pulled his chair up and pushed his wet feet out +towards the blaze. + +"My name," said he, "is John Openshaw, but my own affairs have, +as far as I can understand, little to do with this awful +business. It is a hereditary matter; so in order to give you an +idea of the facts, I must go back to the commencement of the +affair. + +"You must know that my grandfather had two sons--my uncle Elias +and my father Joseph. My father had a small factory at Coventry, +which he enlarged at the time of the invention of bicycling. He +was a patentee of the Openshaw unbreakable tire, and his business +met with such success that he was able to sell it and to retire +upon a handsome competence. + +"My uncle Elias emigrated to America when he was a young man and +became a planter in Florida, where he was reported to have done +very well. At the time of the war he fought in Jackson's army, +and afterwards under Hood, where he rose to be a colonel. When +Lee laid down his arms my uncle returned to his plantation, where +he remained for three or four years. About 1869 or 1870 he came +back to Europe and took a small estate in Sussex, near Horsham. +He had made a very considerable fortune in the States, and his +reason for leaving them was his aversion to the negroes, and his +dislike of the Republican policy in extending the franchise to +them. He was a singular man, fierce and quick-tempered, very +foul-mouthed when he was angry, and of a most retiring +disposition. During all the years that he lived at Horsham, I +doubt if ever he set foot in the town. He had a garden and two or +three fields round his house, and there he would take his +exercise, though very often for weeks on end he would never leave +his room. He drank a great deal of brandy and smoked very +heavily, but he would see no society and did not want any +friends, not even his own brother. + +"He didn't mind me; in fact, he took a fancy to me, for at the +time when he saw me first I was a youngster of twelve or so. This +would be in the year 1878, after he had been eight or nine years +in England. He begged my father to let me live with him and he +was very kind to me in his way. When he was sober he used to be +fond of playing backgammon and draughts with me, and he would +make me his representative both with the servants and with the +tradespeople, so that by the time that I was sixteen I was quite +master of the house. I kept all the keys and could go where I +liked and do what I liked, so long as I did not disturb him in +his privacy. There was one singular exception, however, for he +had a single room, a lumber-room up among the attics, which was +invariably locked, and which he would never permit either me or +anyone else to enter. With a boy's curiosity I have peeped +through the keyhole, but I was never able to see more than such a +collection of old trunks and bundles as would be expected in such +a room. + +"One day--it was in March, 1883--a letter with a foreign stamp +lay upon the table in front of the colonel's plate. It was not a +common thing for him to receive letters, for his bills were all +paid in ready money, and he had no friends of any sort. 'From +India!' said he as he took it up, 'Pondicherry postmark! What can +this be?' Opening it hurriedly, out there jumped five little +dried orange pips, which pattered down upon his plate. I began to +laugh at this, but the laugh was struck from my lips at the sight +of his face. His lip had fallen, his eyes were protruding, his +skin the colour of putty, and he glared at the envelope which he +still held in his trembling hand, 'K. K. K.!' he shrieked, and +then, 'My God, my God, my sins have overtaken me!' + +"'What is it, uncle?' I cried. + +"'Death,' said he, and rising from the table he retired to his +room, leaving me palpitating with horror. I took up the envelope +and saw scrawled in red ink upon the inner flap, just above the +gum, the letter K three times repeated. There was nothing else +save the five dried pips. What could be the reason of his +overpowering terror? I left the breakfast-table, and as I +ascended the stair I met him coming down with an old rusty key, +which must have belonged to the attic, in one hand, and a small +brass box, like a cashbox, in the other. + +"'They may do what they like, but I'll checkmate them still,' +said he with an oath. 'Tell Mary that I shall want a fire in my +room to-day, and send down to Fordham, the Horsham lawyer.' + +"I did as he ordered, and when the lawyer arrived I was asked to +step up to the room. The fire was burning brightly, and in the +grate there was a mass of black, fluffy ashes, as of burned +paper, while the brass box stood open and empty beside it. As I +glanced at the box I noticed, with a start, that upon the lid was +printed the treble K which I had read in the morning upon the +envelope. + +"'I wish you, John,' said my uncle, 'to witness my will. I leave +my estate, with all its advantages and all its disadvantages, to +my brother, your father, whence it will, no doubt, descend to +you. If you can enjoy it in peace, well and good! If you find you +cannot, take my advice, my boy, and leave it to your deadliest +enemy. I am sorry to give you such a two-edged thing, but I can't +say what turn things are going to take. Kindly sign the paper +where Mr. Fordham shows you.' + +"I signed the paper as directed, and the lawyer took it away with +him. The singular incident made, as you may think, the deepest +impression upon me, and I pondered over it and turned it every +way in my mind without being able to make anything of it. Yet I +could not shake off the vague feeling of dread which it left +behind, though the sensation grew less keen as the weeks passed +and nothing happened to disturb the usual routine of our lives. I +could see a change in my uncle, however. He drank more than ever, +and he was less inclined for any sort of society. Most of his +time he would spend in his room, with the door locked upon the +inside, but sometimes he would emerge in a sort of drunken frenzy +and would burst out of the house and tear about the garden with a +revolver in his hand, screaming out that he was afraid of no man, +and that he was not to be cooped up, like a sheep in a pen, by +man or devil. When these hot fits were over, however, he would +rush tumultuously in at the door and lock and bar it behind him, +like a man who can brazen it out no longer against the terror +which lies at the roots of his soul. At such times I have seen +his face, even on a cold day, glisten with moisture, as though it +were new raised from a basin. + +"Well, to come to an end of the matter, Mr. Holmes, and not to +abuse your patience, there came a night when he made one of those +drunken sallies from which he never came back. We found him, when +we went to search for him, face downward in a little +green-scummed pool, which lay at the foot of the garden. There +was no sign of any violence, and the water was but two feet deep, +so that the jury, having regard to his known eccentricity, +brought in a verdict of 'suicide.' But I, who knew how he winced +from the very thought of death, had much ado to persuade myself +that he had gone out of his way to meet it. The matter passed, +however, and my father entered into possession of the estate, and +of some 14,000 pounds, which lay to his credit at the bank." + +"One moment," Holmes interposed, "your statement is, I foresee, +one of the most remarkable to which I have ever listened. Let me +have the date of the reception by your uncle of the letter, and +the date of his supposed suicide." + +"The letter arrived on March 10, 1883. His death was seven weeks +later, upon the night of May 2nd." + +"Thank you. Pray proceed." + +"When my father took over the Horsham property, he, at my +request, made a careful examination of the attic, which had been +always locked up. We found the brass box there, although its +contents had been destroyed. On the inside of the cover was a +paper label, with the initials of K. K. K. repeated upon it, and +'Letters, memoranda, receipts, and a register' written beneath. +These, we presume, indicated the nature of the papers which had +been destroyed by Colonel Openshaw. For the rest, there was +nothing of much importance in the attic save a great many +scattered papers and note-books bearing upon my uncle's life in +America. Some of them were of the war time and showed that he had +done his duty well and had borne the repute of a brave soldier. +Others were of a date during the reconstruction of the Southern +states, and were mostly concerned with politics, for he had +evidently taken a strong part in opposing the carpet-bag +politicians who had been sent down from the North. + +"Well, it was the beginning of '84 when my father came to live at +Horsham, and all went as well as possible with us until the +January of '85. On the fourth day after the new year I heard my +father give a sharp cry of surprise as we sat together at the +breakfast-table. There he was, sitting with a newly opened +envelope in one hand and five dried orange pips in the +outstretched palm of the other one. He had always laughed at what +he called my cock-and-bull story about the colonel, but he looked +very scared and puzzled now that the same thing had come upon +himself. + +"'Why, what on earth does this mean, John?' he stammered. + +"My heart had turned to lead. 'It is K. K. K.,' said I. + +"He looked inside the envelope. 'So it is,' he cried. 'Here are +the very letters. But what is this written above them?' + +"'Put the papers on the sundial,' I read, peeping over his +shoulder. + +"'What papers? What sundial?' he asked. + +"'The sundial in the garden. There is no other,' said I; 'but the +papers must be those that are destroyed.' + +"'Pooh!' said he, gripping hard at his courage. 'We are in a +civilised land here, and we can't have tomfoolery of this kind. +Where does the thing come from?' + +"'From Dundee,' I answered, glancing at the postmark. + +"'Some preposterous practical joke,' said he. 'What have I to do +with sundials and papers? I shall take no notice of such +nonsense.' + +"'I should certainly speak to the police,' I said. + +"'And be laughed at for my pains. Nothing of the sort.' + +"'Then let me do so?' + +"'No, I forbid you. I won't have a fuss made about such +nonsense.' + +"It was in vain to argue with him, for he was a very obstinate +man. I went about, however, with a heart which was full of +forebodings. + +"On the third day after the coming of the letter my father went +from home to visit an old friend of his, Major Freebody, who is +in command of one of the forts upon Portsdown Hill. I was glad +that he should go, for it seemed to me that he was farther from +danger when he was away from home. In that, however, I was in +error. Upon the second day of his absence I received a telegram +from the major, imploring me to come at once. My father had +fallen over one of the deep chalk-pits which abound in the +neighbourhood, and was lying senseless, with a shattered skull. I +hurried to him, but he passed away without having ever recovered +his consciousness. He had, as it appears, been returning from +Fareham in the twilight, and as the country was unknown to him, +and the chalk-pit unfenced, the jury had no hesitation in +bringing in a verdict of 'death from accidental causes.' +Carefully as I examined every fact connected with his death, I +was unable to find anything which could suggest the idea of +murder. There were no signs of violence, no footmarks, no +robbery, no record of strangers having been seen upon the roads. +And yet I need not tell you that my mind was far from at ease, +and that I was well-nigh certain that some foul plot had been +woven round him. + +"In this sinister way I came into my inheritance. You will ask me +why I did not dispose of it? I answer, because I was well +convinced that our troubles were in some way dependent upon an +incident in my uncle's life, and that the danger would be as +pressing in one house as in another. + +"It was in January, '85, that my poor father met his end, and two +years and eight months have elapsed since then. During that time +I have lived happily at Horsham, and I had begun to hope that +this curse had passed away from the family, and that it had ended +with the last generation. I had begun to take comfort too soon, +however; yesterday morning the blow fell in the very shape in +which it had come upon my father." + +The young man took from his waistcoat a crumpled envelope, and +turning to the table he shook out upon it five little dried +orange pips. + +"This is the envelope," he continued. "The postmark is +London--eastern division. Within are the very words which were +upon my father's last message: 'K. K. K.'; and then 'Put the +papers on the sundial.'" + +"What have you done?" asked Holmes. + +"Nothing." + +"Nothing?" + +"To tell the truth"--he sank his face into his thin, white +hands--"I have felt helpless. I have felt like one of those poor +rabbits when the snake is writhing towards it. I seem to be in +the grasp of some resistless, inexorable evil, which no foresight +and no precautions can guard against." + +"Tut! tut!" cried Sherlock Holmes. "You must act, man, or you are +lost. Nothing but energy can save you. This is no time for +despair." + +"I have seen the police." + +"Ah!" + +"But they listened to my story with a smile. I am convinced that +the inspector has formed the opinion that the letters are all +practical jokes, and that the deaths of my relations were really +accidents, as the jury stated, and were not to be connected with +the warnings." + +Holmes shook his clenched hands in the air. "Incredible +imbecility!" he cried. + +"They have, however, allowed me a policeman, who may remain in +the house with me." + +"Has he come with you to-night?" + +"No. His orders were to stay in the house." + +Again Holmes raved in the air. + +"Why did you come to me," he cried, "and, above all, why did you +not come at once?" + +"I did not know. It was only to-day that I spoke to Major +Prendergast about my troubles and was advised by him to come to +you." + +"It is really two days since you had the letter. We should have +acted before this. You have no further evidence, I suppose, than +that which you have placed before us--no suggestive detail which +might help us?" + +"There is one thing," said John Openshaw. He rummaged in his coat +pocket, and, drawing out a piece of discoloured, blue-tinted +paper, he laid it out upon the table. "I have some remembrance," +said he, "that on the day when my uncle burned the papers I +observed that the small, unburned margins which lay amid the +ashes were of this particular colour. I found this single sheet +upon the floor of his room, and I am inclined to think that it +may be one of the papers which has, perhaps, fluttered out from +among the others, and in that way has escaped destruction. Beyond +the mention of pips, I do not see that it helps us much. I think +myself that it is a page from some private diary. The writing is +undoubtedly my uncle's." + +Holmes moved the lamp, and we both bent over the sheet of paper, +which showed by its ragged edge that it had indeed been torn from +a book. It was headed, "March, 1869," and beneath were the +following enigmatical notices: + +"4th. Hudson came. Same old platform. + +"7th. Set the pips on McCauley, Paramore, and + John Swain, of St. Augustine. + +"9th. McCauley cleared. + +"10th. John Swain cleared. + +"12th. Visited Paramore. All well." + +"Thank you!" said Holmes, folding up the paper and returning it +to our visitor. "And now you must on no account lose another +instant. We cannot spare time even to discuss what you have told +me. You must get home instantly and act." + +"What shall I do?" + +"There is but one thing to do. It must be done at once. You must +put this piece of paper which you have shown us into the brass +box which you have described. You must also put in a note to say +that all the other papers were burned by your uncle, and that +this is the only one which remains. You must assert that in such +words as will carry conviction with them. Having done this, you +must at once put the box out upon the sundial, as directed. Do +you understand?" + +"Entirely." + +"Do not think of revenge, or anything of the sort, at present. I +think that we may gain that by means of the law; but we have our +web to weave, while theirs is already woven. The first +consideration is to remove the pressing danger which threatens +you. The second is to clear up the mystery and to punish the +guilty parties." + +"I thank you," said the young man, rising and pulling on his +overcoat. "You have given me fresh life and hope. I shall +certainly do as you advise." + +"Do not lose an instant. And, above all, take care of yourself in +the meanwhile, for I do not think that there can be a doubt that +you are threatened by a very real and imminent danger. How do you +go back?" + +"By train from Waterloo." + +"It is not yet nine. The streets will be crowded, so I trust that +you may be in safety. And yet you cannot guard yourself too +closely." + +"I am armed." + +"That is well. To-morrow I shall set to work upon your case." + +"I shall see you at Horsham, then?" + +"No, your secret lies in London. It is there that I shall seek +it." + +"Then I shall call upon you in a day, or in two days, with news +as to the box and the papers. I shall take your advice in every +particular." He shook hands with us and took his leave. Outside +the wind still screamed and the rain splashed and pattered +against the windows. This strange, wild story seemed to have come +to us from amid the mad elements--blown in upon us like a sheet +of sea-weed in a gale--and now to have been reabsorbed by them +once more. + +Sherlock Holmes sat for some time in silence, with his head sunk +forward and his eyes bent upon the red glow of the fire. Then he +lit his pipe, and leaning back in his chair he watched the blue +smoke-rings as they chased each other up to the ceiling. + +"I think, Watson," he remarked at last, "that of all our cases we +have had none more fantastic than this." + +"Save, perhaps, the Sign of Four." + +"Well, yes. Save, perhaps, that. And yet this John Openshaw seems +to me to be walking amid even greater perils than did the +Sholtos." + +"But have you," I asked, "formed any definite conception as to +what these perils are?" + +"There can be no question as to their nature," he answered. + +"Then what are they? Who is this K. K. K., and why does he pursue +this unhappy family?" + +Sherlock Holmes closed his eyes and placed his elbows upon the +arms of his chair, with his finger-tips together. "The ideal +reasoner," he remarked, "would, when he had once been shown a +single fact in all its bearings, deduce from it not only all the +chain of events which led up to it but also all the results which +would follow from it. As Cuvier could correctly describe a whole +animal by the contemplation of a single bone, so the observer who +has thoroughly understood one link in a series of incidents +should be able to accurately state all the other ones, both +before and after. We have not yet grasped the results which the +reason alone can attain to. Problems may be solved in the study +which have baffled all those who have sought a solution by the +aid of their senses. To carry the art, however, to its highest +pitch, it is necessary that the reasoner should be able to +utilise all the facts which have come to his knowledge; and this +in itself implies, as you will readily see, a possession of all +knowledge, which, even in these days of free education and +encyclopaedias, is a somewhat rare accomplishment. It is not so +impossible, however, that a man should possess all knowledge +which is likely to be useful to him in his work, and this I have +endeavoured in my case to do. If I remember rightly, you on one +occasion, in the early days of our friendship, defined my limits +in a very precise fashion." + +"Yes," I answered, laughing. "It was a singular document. +Philosophy, astronomy, and politics were marked at zero, I +remember. Botany variable, geology profound as regards the +mud-stains from any region within fifty miles of town, chemistry +eccentric, anatomy unsystematic, sensational literature and crime +records unique, violin-player, boxer, swordsman, lawyer, and +self-poisoner by cocaine and tobacco. Those, I think, were the +main points of my analysis." + +Holmes grinned at the last item. "Well," he said, "I say now, as +I said then, that a man should keep his little brain-attic +stocked with all the furniture that he is likely to use, and the +rest he can put away in the lumber-room of his library, where he +can get it if he wants it. Now, for such a case as the one which +has been submitted to us to-night, we need certainly to muster +all our resources. Kindly hand me down the letter K of the +'American Encyclopaedia' which stands upon the shelf beside you. +Thank you. Now let us consider the situation and see what may be +deduced from it. In the first place, we may start with a strong +presumption that Colonel Openshaw had some very strong reason for +leaving America. Men at his time of life do not change all their +habits and exchange willingly the charming climate of Florida for +the lonely life of an English provincial town. His extreme love +of solitude in England suggests the idea that he was in fear of +someone or something, so we may assume as a working hypothesis +that it was fear of someone or something which drove him from +America. As to what it was he feared, we can only deduce that by +considering the formidable letters which were received by himself +and his successors. Did you remark the postmarks of those +letters?" + +"The first was from Pondicherry, the second from Dundee, and the +third from London." + +"From East London. What do you deduce from that?" + +"They are all seaports. That the writer was on board of a ship." + +"Excellent. We have already a clue. There can be no doubt that +the probability--the strong probability--is that the writer was +on board of a ship. And now let us consider another point. In the +case of Pondicherry, seven weeks elapsed between the threat and +its fulfilment, in Dundee it was only some three or four days. +Does that suggest anything?" + +"A greater distance to travel." + +"But the letter had also a greater distance to come." + +"Then I do not see the point." + +"There is at least a presumption that the vessel in which the man +or men are is a sailing-ship. It looks as if they always send +their singular warning or token before them when starting upon +their mission. You see how quickly the deed followed the sign +when it came from Dundee. If they had come from Pondicherry in a +steamer they would have arrived almost as soon as their letter. +But, as a matter of fact, seven weeks elapsed. I think that those +seven weeks represented the difference between the mail-boat which +brought the letter and the sailing vessel which brought the +writer." + +"It is possible." + +"More than that. It is probable. And now you see the deadly +urgency of this new case, and why I urged young Openshaw to +caution. The blow has always fallen at the end of the time which +it would take the senders to travel the distance. But this one +comes from London, and therefore we cannot count upon delay." + +"Good God!" I cried. "What can it mean, this relentless +persecution?" + +"The papers which Openshaw carried are obviously of vital +importance to the person or persons in the sailing-ship. I think +that it is quite clear that there must be more than one of them. +A single man could not have carried out two deaths in such a way +as to deceive a coroner's jury. There must have been several in +it, and they must have been men of resource and determination. +Their papers they mean to have, be the holder of them who it may. +In this way you see K. K. K. ceases to be the initials of an +individual and becomes the badge of a society." + +"But of what society?" + +"Have you never--" said Sherlock Holmes, bending forward and +sinking his voice--"have you never heard of the Ku Klux Klan?" + +"I never have." + +Holmes turned over the leaves of the book upon his knee. "Here it +is," said he presently: + +"'Ku Klux Klan. A name derived from the fanciful resemblance to +the sound produced by cocking a rifle. This terrible secret +society was formed by some ex-Confederate soldiers in the +Southern states after the Civil War, and it rapidly formed local +branches in different parts of the country, notably in Tennessee, +Louisiana, the Carolinas, Georgia, and Florida. Its power was +used for political purposes, principally for the terrorising of +the negro voters and the murdering and driving from the country +of those who were opposed to its views. Its outrages were usually +preceded by a warning sent to the marked man in some fantastic +but generally recognised shape--a sprig of oak-leaves in some +parts, melon seeds or orange pips in others. On receiving this +the victim might either openly abjure his former ways, or might +fly from the country. If he braved the matter out, death would +unfailingly come upon him, and usually in some strange and +unforeseen manner. So perfect was the organisation of the +society, and so systematic its methods, that there is hardly a +case upon record where any man succeeded in braving it with +impunity, or in which any of its outrages were traced home to the +perpetrators. For some years the organisation flourished in spite +of the efforts of the United States government and of the better +classes of the community in the South. Eventually, in the year +1869, the movement rather suddenly collapsed, although there have +been sporadic outbreaks of the same sort since that date.' + +"You will observe," said Holmes, laying down the volume, "that +the sudden breaking up of the society was coincident with the +disappearance of Openshaw from America with their papers. It may +well have been cause and effect. It is no wonder that he and his +family have some of the more implacable spirits upon their track. +You can understand that this register and diary may implicate +some of the first men in the South, and that there may be many +who will not sleep easy at night until it is recovered." + +"Then the page we have seen--" + +"Is such as we might expect. It ran, if I remember right, 'sent +the pips to A, B, and C'--that is, sent the society's warning to +them. Then there are successive entries that A and B cleared, or +left the country, and finally that C was visited, with, I fear, a +sinister result for C. Well, I think, Doctor, that we may let +some light into this dark place, and I believe that the only +chance young Openshaw has in the meantime is to do what I have +told him. There is nothing more to be said or to be done +to-night, so hand me over my violin and let us try to forget for +half an hour the miserable weather and the still more miserable +ways of our fellow-men." + + +It had cleared in the morning, and the sun was shining with a +subdued brightness through the dim veil which hangs over the +great city. Sherlock Holmes was already at breakfast when I came +down. + +"You will excuse me for not waiting for you," said he; "I have, I +foresee, a very busy day before me in looking into this case of +young Openshaw's." + +"What steps will you take?" I asked. + +"It will very much depend upon the results of my first inquiries. +I may have to go down to Horsham, after all." + +"You will not go there first?" + +"No, I shall commence with the City. Just ring the bell and the +maid will bring up your coffee." + +As I waited, I lifted the unopened newspaper from the table and +glanced my eye over it. It rested upon a heading which sent a +chill to my heart. + +"Holmes," I cried, "you are too late." + +"Ah!" said he, laying down his cup, "I feared as much. How was it +done?" He spoke calmly, but I could see that he was deeply moved. + +"My eye caught the name of Openshaw, and the heading 'Tragedy +Near Waterloo Bridge.' Here is the account: + +"Between nine and ten last night Police-Constable Cook, of the H +Division, on duty near Waterloo Bridge, heard a cry for help and +a splash in the water. The night, however, was extremely dark and +stormy, so that, in spite of the help of several passers-by, it +was quite impossible to effect a rescue. The alarm, however, was +given, and, by the aid of the water-police, the body was +eventually recovered. It proved to be that of a young gentleman +whose name, as it appears from an envelope which was found in his +pocket, was John Openshaw, and whose residence is near Horsham. +It is conjectured that he may have been hurrying down to catch +the last train from Waterloo Station, and that in his haste and +the extreme darkness he missed his path and walked over the edge +of one of the small landing-places for river steamboats. The body +exhibited no traces of violence, and there can be no doubt that +the deceased had been the victim of an unfortunate accident, +which should have the effect of calling the attention of the +authorities to the condition of the riverside landing-stages." + +We sat in silence for some minutes, Holmes more depressed and +shaken than I had ever seen him. + +"That hurts my pride, Watson," he said at last. "It is a petty +feeling, no doubt, but it hurts my pride. It becomes a personal +matter with me now, and, if God sends me health, I shall set my +hand upon this gang. That he should come to me for help, and that +I should send him away to his death--!" He sprang from his chair +and paced about the room in uncontrollable agitation, with a +flush upon his sallow cheeks and a nervous clasping and +unclasping of his long thin hands. + +"They must be cunning devils," he exclaimed at last. "How could +they have decoyed him down there? The Embankment is not on the +direct line to the station. The bridge, no doubt, was too +crowded, even on such a night, for their purpose. Well, Watson, +we shall see who will win in the long run. I am going out now!" + +"To the police?" + +"No; I shall be my own police. When I have spun the web they may +take the flies, but not before." + +All day I was engaged in my professional work, and it was late in +the evening before I returned to Baker Street. Sherlock Holmes +had not come back yet. It was nearly ten o'clock before he +entered, looking pale and worn. He walked up to the sideboard, +and tearing a piece from the loaf he devoured it voraciously, +washing it down with a long draught of water. + +"You are hungry," I remarked. + +"Starving. It had escaped my memory. I have had nothing since +breakfast." + +"Nothing?" + +"Not a bite. I had no time to think of it." + +"And how have you succeeded?" + +"Well." + +"You have a clue?" + +"I have them in the hollow of my hand. Young Openshaw shall not +long remain unavenged. Why, Watson, let us put their own devilish +trade-mark upon them. It is well thought of!" + +"What do you mean?" + +He took an orange from the cupboard, and tearing it to pieces he +squeezed out the pips upon the table. Of these he took five and +thrust them into an envelope. On the inside of the flap he wrote +"S. H. for J. O." Then he sealed it and addressed it to "Captain +James Calhoun, Barque 'Lone Star,' Savannah, Georgia." + +"That will await him when he enters port," said he, chuckling. +"It may give him a sleepless night. He will find it as sure a +precursor of his fate as Openshaw did before him." + +"And who is this Captain Calhoun?" + +"The leader of the gang. I shall have the others, but he first." + +"How did you trace it, then?" + +He took a large sheet of paper from his pocket, all covered with +dates and names. + +"I have spent the whole day," said he, "over Lloyd's registers +and files of the old papers, following the future career of every +vessel which touched at Pondicherry in January and February in +'83. There were thirty-six ships of fair tonnage which were +reported there during those months. Of these, one, the 'Lone Star,' +instantly attracted my attention, since, although it was reported +as having cleared from London, the name is that which is given to +one of the states of the Union." + +"Texas, I think." + +"I was not and am not sure which; but I knew that the ship must +have an American origin." + +"What then?" + +"I searched the Dundee records, and when I found that the barque +'Lone Star' was there in January, '85, my suspicion became a +certainty. I then inquired as to the vessels which lay at present +in the port of London." + +"Yes?" + +"The 'Lone Star' had arrived here last week. I went down to the +Albert Dock and found that she had been taken down the river by +the early tide this morning, homeward bound to Savannah. I wired +to Gravesend and learned that she had passed some time ago, and +as the wind is easterly I have no doubt that she is now past the +Goodwins and not very far from the Isle of Wight." + +"What will you do, then?" + +"Oh, I have my hand upon him. He and the two mates, are as I +learn, the only native-born Americans in the ship. The others are +Finns and Germans. I know, also, that they were all three away +from the ship last night. I had it from the stevedore who has +been loading their cargo. By the time that their sailing-ship +reaches Savannah the mail-boat will have carried this letter, and +the cable will have informed the police of Savannah that these +three gentlemen are badly wanted here upon a charge of murder." + +There is ever a flaw, however, in the best laid of human plans, +and the murderers of John Openshaw were never to receive the +orange pips which would show them that another, as cunning and as +resolute as themselves, was upon their track. Very long and very +severe were the equinoctial gales that year. We waited long for +news of the "Lone Star" of Savannah, but none ever reached us. We +did at last hear that somewhere far out in the Atlantic a +shattered stern-post of a boat was seen swinging in the trough +of a wave, with the letters "L. S." carved upon it, and that is +all which we shall ever know of the fate of the "Lone Star." + + + +ADVENTURE VI. THE MAN WITH THE TWISTED LIP + +Isa Whitney, brother of the late Elias Whitney, D.D., Principal +of the Theological College of St. George's, was much addicted to +opium. The habit grew upon him, as I understand, from some +foolish freak when he was at college; for having read De +Quincey's description of his dreams and sensations, he had +drenched his tobacco with laudanum in an attempt to produce the +same effects. He found, as so many more have done, that the +practice is easier to attain than to get rid of, and for many +years he continued to be a slave to the drug, an object of +mingled horror and pity to his friends and relatives. I can see +him now, with yellow, pasty face, drooping lids, and pin-point +pupils, all huddled in a chair, the wreck and ruin of a noble +man. + +One night--it was in June, '89--there came a ring to my bell, +about the hour when a man gives his first yawn and glances at the +clock. I sat up in my chair, and my wife laid her needle-work +down in her lap and made a little face of disappointment. + +"A patient!" said she. "You'll have to go out." + +I groaned, for I was newly come back from a weary day. + +We heard the door open, a few hurried words, and then quick steps +upon the linoleum. Our own door flew open, and a lady, clad in +some dark-coloured stuff, with a black veil, entered the room. + +"You will excuse my calling so late," she began, and then, +suddenly losing her self-control, she ran forward, threw her arms +about my wife's neck, and sobbed upon her shoulder. "Oh, I'm in +such trouble!" she cried; "I do so want a little help." + +"Why," said my wife, pulling up her veil, "it is Kate Whitney. +How you startled me, Kate! I had not an idea who you were when +you came in." + +"I didn't know what to do, so I came straight to you." That was +always the way. Folk who were in grief came to my wife like birds +to a light-house. + +"It was very sweet of you to come. Now, you must have some wine +and water, and sit here comfortably and tell us all about it. Or +should you rather that I sent James off to bed?" + +"Oh, no, no! I want the doctor's advice and help, too. It's about +Isa. He has not been home for two days. I am so frightened about +him!" + +It was not the first time that she had spoken to us of her +husband's trouble, to me as a doctor, to my wife as an old friend +and school companion. We soothed and comforted her by such words +as we could find. Did she know where her husband was? Was it +possible that we could bring him back to her? + +It seems that it was. She had the surest information that of late +he had, when the fit was on him, made use of an opium den in the +farthest east of the City. Hitherto his orgies had always been +confined to one day, and he had come back, twitching and +shattered, in the evening. But now the spell had been upon him +eight-and-forty hours, and he lay there, doubtless among the +dregs of the docks, breathing in the poison or sleeping off the +effects. There he was to be found, she was sure of it, at the Bar +of Gold, in Upper Swandam Lane. But what was she to do? How could +she, a young and timid woman, make her way into such a place and +pluck her husband out from among the ruffians who surrounded him? + +There was the case, and of course there was but one way out of +it. Might I not escort her to this place? And then, as a second +thought, why should she come at all? I was Isa Whitney's medical +adviser, and as such I had influence over him. I could manage it +better if I were alone. I promised her on my word that I would +send him home in a cab within two hours if he were indeed at the +address which she had given me. And so in ten minutes I had left +my armchair and cheery sitting-room behind me, and was speeding +eastward in a hansom on a strange errand, as it seemed to me at +the time, though the future only could show how strange it was to +be. + +But there was no great difficulty in the first stage of my +adventure. Upper Swandam Lane is a vile alley lurking behind the +high wharves which line the north side of the river to the east +of London Bridge. Between a slop-shop and a gin-shop, approached +by a steep flight of steps leading down to a black gap like the +mouth of a cave, I found the den of which I was in search. +Ordering my cab to wait, I passed down the steps, worn hollow in +the centre by the ceaseless tread of drunken feet; and by the +light of a flickering oil-lamp above the door I found the latch +and made my way into a long, low room, thick and heavy with the +brown opium smoke, and terraced with wooden berths, like the +forecastle of an emigrant ship. + +Through the gloom one could dimly catch a glimpse of bodies lying +in strange fantastic poses, bowed shoulders, bent knees, heads +thrown back, and chins pointing upward, with here and there a +dark, lack-lustre eye turned upon the newcomer. Out of the black +shadows there glimmered little red circles of light, now bright, +now faint, as the burning poison waxed or waned in the bowls of +the metal pipes. The most lay silent, but some muttered to +themselves, and others talked together in a strange, low, +monotonous voice, their conversation coming in gushes, and then +suddenly tailing off into silence, each mumbling out his own +thoughts and paying little heed to the words of his neighbour. At +the farther end was a small brazier of burning charcoal, beside +which on a three-legged wooden stool there sat a tall, thin old +man, with his jaw resting upon his two fists, and his elbows upon +his knees, staring into the fire. + +As I entered, a sallow Malay attendant had hurried up with a pipe +for me and a supply of the drug, beckoning me to an empty berth. + +"Thank you. I have not come to stay," said I. "There is a friend +of mine here, Mr. Isa Whitney, and I wish to speak with him." + +There was a movement and an exclamation from my right, and +peering through the gloom, I saw Whitney, pale, haggard, and +unkempt, staring out at me. + +"My God! It's Watson," said he. He was in a pitiable state of +reaction, with every nerve in a twitter. "I say, Watson, what +o'clock is it?" + +"Nearly eleven." + +"Of what day?" + +"Of Friday, June 19th." + +"Good heavens! I thought it was Wednesday. It is Wednesday. What +d'you want to frighten a chap for?" He sank his face onto his +arms and began to sob in a high treble key. + +"I tell you that it is Friday, man. Your wife has been waiting +this two days for you. You should be ashamed of yourself!" + +"So I am. But you've got mixed, Watson, for I have only been here +a few hours, three pipes, four pipes--I forget how many. But I'll +go home with you. I wouldn't frighten Kate--poor little Kate. +Give me your hand! Have you a cab?" + +"Yes, I have one waiting." + +"Then I shall go in it. But I must owe something. Find what I +owe, Watson. I am all off colour. I can do nothing for myself." + +I walked down the narrow passage between the double row of +sleepers, holding my breath to keep out the vile, stupefying +fumes of the drug, and looking about for the manager. As I passed +the tall man who sat by the brazier I felt a sudden pluck at my +skirt, and a low voice whispered, "Walk past me, and then look +back at me." The words fell quite distinctly upon my ear. I +glanced down. They could only have come from the old man at my +side, and yet he sat now as absorbed as ever, very thin, very +wrinkled, bent with age, an opium pipe dangling down from between +his knees, as though it had dropped in sheer lassitude from his +fingers. I took two steps forward and looked back. It took all my +self-control to prevent me from breaking out into a cry of +astonishment. He had turned his back so that none could see him +but I. His form had filled out, his wrinkles were gone, the dull +eyes had regained their fire, and there, sitting by the fire and +grinning at my surprise, was none other than Sherlock Holmes. He +made a slight motion to me to approach him, and instantly, as he +turned his face half round to the company once more, subsided +into a doddering, loose-lipped senility. + +"Holmes!" I whispered, "what on earth are you doing in this den?" + +"As low as you can," he answered; "I have excellent ears. If you +would have the great kindness to get rid of that sottish friend +of yours I should be exceedingly glad to have a little talk with +you." + +"I have a cab outside." + +"Then pray send him home in it. You may safely trust him, for he +appears to be too limp to get into any mischief. I should +recommend you also to send a note by the cabman to your wife to +say that you have thrown in your lot with me. If you will wait +outside, I shall be with you in five minutes." + +It was difficult to refuse any of Sherlock Holmes' requests, for +they were always so exceedingly definite, and put forward with +such a quiet air of mastery. I felt, however, that when Whitney +was once confined in the cab my mission was practically +accomplished; and for the rest, I could not wish anything better +than to be associated with my friend in one of those singular +adventures which were the normal condition of his existence. In a +few minutes I had written my note, paid Whitney's bill, led him +out to the cab, and seen him driven through the darkness. In a +very short time a decrepit figure had emerged from the opium den, +and I was walking down the street with Sherlock Holmes. For two +streets he shuffled along with a bent back and an uncertain foot. +Then, glancing quickly round, he straightened himself out and +burst into a hearty fit of laughter. + +"I suppose, Watson," said he, "that you imagine that I have added +opium-smoking to cocaine injections, and all the other little +weaknesses on which you have favoured me with your medical +views." + +"I was certainly surprised to find you there." + +"But not more so than I to find you." + +"I came to find a friend." + +"And I to find an enemy." + +"An enemy?" + +"Yes; one of my natural enemies, or, shall I say, my natural +prey. Briefly, Watson, I am in the midst of a very remarkable +inquiry, and I have hoped to find a clue in the incoherent +ramblings of these sots, as I have done before now. Had I been +recognised in that den my life would not have been worth an +hour's purchase; for I have used it before now for my own +purposes, and the rascally Lascar who runs it has sworn to have +vengeance upon me. There is a trap-door at the back of that +building, near the corner of Paul's Wharf, which could tell some +strange tales of what has passed through it upon the moonless +nights." + +"What! You do not mean bodies?" + +"Ay, bodies, Watson. We should be rich men if we had 1000 pounds +for every poor devil who has been done to death in that den. It +is the vilest murder-trap on the whole riverside, and I fear that +Neville St. Clair has entered it never to leave it more. But our +trap should be here." He put his two forefingers between his +teeth and whistled shrilly--a signal which was answered by a +similar whistle from the distance, followed shortly by the rattle +of wheels and the clink of horses' hoofs. + +"Now, Watson," said Holmes, as a tall dog-cart dashed up through +the gloom, throwing out two golden tunnels of yellow light from +its side lanterns. "You'll come with me, won't you?" + +"If I can be of use." + +"Oh, a trusty comrade is always of use; and a chronicler still +more so. My room at The Cedars is a double-bedded one." + +"The Cedars?" + +"Yes; that is Mr. St. Clair's house. I am staying there while I +conduct the inquiry." + +"Where is it, then?" + +"Near Lee, in Kent. We have a seven-mile drive before us." + +"But I am all in the dark." + +"Of course you are. You'll know all about it presently. Jump up +here. All right, John; we shall not need you. Here's half a +crown. Look out for me to-morrow, about eleven. Give her her +head. So long, then!" + +He flicked the horse with his whip, and we dashed away through +the endless succession of sombre and deserted streets, which +widened gradually, until we were flying across a broad +balustraded bridge, with the murky river flowing sluggishly +beneath us. Beyond lay another dull wilderness of bricks and +mortar, its silence broken only by the heavy, regular footfall of +the policeman, or the songs and shouts of some belated party of +revellers. A dull wrack was drifting slowly across the sky, and a +star or two twinkled dimly here and there through the rifts of +the clouds. Holmes drove in silence, with his head sunk upon his +breast, and the air of a man who is lost in thought, while I sat +beside him, curious to learn what this new quest might be which +seemed to tax his powers so sorely, and yet afraid to break in +upon the current of his thoughts. We had driven several miles, +and were beginning to get to the fringe of the belt of suburban +villas, when he shook himself, shrugged his shoulders, and lit up +his pipe with the air of a man who has satisfied himself that he +is acting for the best. + +"You have a grand gift of silence, Watson," said he. "It makes +you quite invaluable as a companion. 'Pon my word, it is a great +thing for me to have someone to talk to, for my own thoughts are +not over-pleasant. I was wondering what I should say to this dear +little woman to-night when she meets me at the door." + +"You forget that I know nothing about it." + +"I shall just have time to tell you the facts of the case before +we get to Lee. It seems absurdly simple, and yet, somehow I can +get nothing to go upon. There's plenty of thread, no doubt, but I +can't get the end of it into my hand. Now, I'll state the case +clearly and concisely to you, Watson, and maybe you can see a +spark where all is dark to me." + +"Proceed, then." + +"Some years ago--to be definite, in May, 1884--there came to Lee +a gentleman, Neville St. Clair by name, who appeared to have +plenty of money. He took a large villa, laid out the grounds very +nicely, and lived generally in good style. By degrees he made +friends in the neighbourhood, and in 1887 he married the daughter +of a local brewer, by whom he now has two children. He had no +occupation, but was interested in several companies and went into +town as a rule in the morning, returning by the 5:14 from Cannon +Street every night. Mr. St. Clair is now thirty-seven years of +age, is a man of temperate habits, a good husband, a very +affectionate father, and a man who is popular with all who know +him. I may add that his whole debts at the present moment, as far +as we have been able to ascertain, amount to 88 pounds 10s., while +he has 220 pounds standing to his credit in the Capital and +Counties Bank. There is no reason, therefore, to think that money +troubles have been weighing upon his mind. + +"Last Monday Mr. Neville St. Clair went into town rather earlier +than usual, remarking before he started that he had two important +commissions to perform, and that he would bring his little boy +home a box of bricks. Now, by the merest chance, his wife +received a telegram upon this same Monday, very shortly after his +departure, to the effect that a small parcel of considerable +value which she had been expecting was waiting for her at the +offices of the Aberdeen Shipping Company. Now, if you are well up +in your London, you will know that the office of the company is +in Fresno Street, which branches out of Upper Swandam Lane, where +you found me to-night. Mrs. St. Clair had her lunch, started for +the City, did some shopping, proceeded to the company's office, +got her packet, and found herself at exactly 4:35 walking through +Swandam Lane on her way back to the station. Have you followed me +so far?" + +"It is very clear." + +"If you remember, Monday was an exceedingly hot day, and Mrs. St. +Clair walked slowly, glancing about in the hope of seeing a cab, +as she did not like the neighbourhood in which she found herself. +While she was walking in this way down Swandam Lane, she suddenly +heard an ejaculation or cry, and was struck cold to see her +husband looking down at her and, as it seemed to her, beckoning +to her from a second-floor window. The window was open, and she +distinctly saw his face, which she describes as being terribly +agitated. He waved his hands frantically to her, and then +vanished from the window so suddenly that it seemed to her that +he had been plucked back by some irresistible force from behind. +One singular point which struck her quick feminine eye was that +although he wore some dark coat, such as he had started to town +in, he had on neither collar nor necktie. + +"Convinced that something was amiss with him, she rushed down the +steps--for the house was none other than the opium den in which +you found me to-night--and running through the front room she +attempted to ascend the stairs which led to the first floor. At +the foot of the stairs, however, she met this Lascar scoundrel of +whom I have spoken, who thrust her back and, aided by a Dane, who +acts as assistant there, pushed her out into the street. Filled +with the most maddening doubts and fears, she rushed down the +lane and, by rare good-fortune, met in Fresno Street a number of +constables with an inspector, all on their way to their beat. The +inspector and two men accompanied her back, and in spite of the +continued resistance of the proprietor, they made their way to +the room in which Mr. St. Clair had last been seen. There was no +sign of him there. In fact, in the whole of that floor there was +no one to be found save a crippled wretch of hideous aspect, who, +it seems, made his home there. Both he and the Lascar stoutly +swore that no one else had been in the front room during the +afternoon. So determined was their denial that the inspector was +staggered, and had almost come to believe that Mrs. St. Clair had +been deluded when, with a cry, she sprang at a small deal box +which lay upon the table and tore the lid from it. Out there fell +a cascade of children's bricks. It was the toy which he had +promised to bring home. + +"This discovery, and the evident confusion which the cripple +showed, made the inspector realise that the matter was serious. +The rooms were carefully examined, and results all pointed to an +abominable crime. The front room was plainly furnished as a +sitting-room and led into a small bedroom, which looked out upon +the back of one of the wharves. Between the wharf and the bedroom +window is a narrow strip, which is dry at low tide but is covered +at high tide with at least four and a half feet of water. The +bedroom window was a broad one and opened from below. On +examination traces of blood were to be seen upon the windowsill, +and several scattered drops were visible upon the wooden floor of +the bedroom. Thrust away behind a curtain in the front room were +all the clothes of Mr. Neville St. Clair, with the exception of +his coat. His boots, his socks, his hat, and his watch--all were +there. There were no signs of violence upon any of these +garments, and there were no other traces of Mr. Neville St. +Clair. Out of the window he must apparently have gone for no +other exit could be discovered, and the ominous bloodstains upon +the sill gave little promise that he could save himself by +swimming, for the tide was at its very highest at the moment of +the tragedy. + +"And now as to the villains who seemed to be immediately +implicated in the matter. The Lascar was known to be a man of the +vilest antecedents, but as, by Mrs. St. Clair's story, he was +known to have been at the foot of the stair within a very few +seconds of her husband's appearance at the window, he could +hardly have been more than an accessory to the crime. His defence +was one of absolute ignorance, and he protested that he had no +knowledge as to the doings of Hugh Boone, his lodger, and that he +could not account in any way for the presence of the missing +gentleman's clothes. + +"So much for the Lascar manager. Now for the sinister cripple who +lives upon the second floor of the opium den, and who was +certainly the last human being whose eyes rested upon Neville St. +Clair. His name is Hugh Boone, and his hideous face is one which +is familiar to every man who goes much to the City. He is a +professional beggar, though in order to avoid the police +regulations he pretends to a small trade in wax vestas. Some +little distance down Threadneedle Street, upon the left-hand +side, there is, as you may have remarked, a small angle in the +wall. Here it is that this creature takes his daily seat, +cross-legged with his tiny stock of matches on his lap, and as he +is a piteous spectacle a small rain of charity descends into the +greasy leather cap which lies upon the pavement beside him. I +have watched the fellow more than once before ever I thought of +making his professional acquaintance, and I have been surprised +at the harvest which he has reaped in a short time. His +appearance, you see, is so remarkable that no one can pass him +without observing him. A shock of orange hair, a pale face +disfigured by a horrible scar, which, by its contraction, has +turned up the outer edge of his upper lip, a bulldog chin, and a +pair of very penetrating dark eyes, which present a singular +contrast to the colour of his hair, all mark him out from amid +the common crowd of mendicants and so, too, does his wit, for he +is ever ready with a reply to any piece of chaff which may be +thrown at him by the passers-by. This is the man whom we now +learn to have been the lodger at the opium den, and to have been +the last man to see the gentleman of whom we are in quest." + +"But a cripple!" said I. "What could he have done single-handed +against a man in the prime of life?" + +"He is a cripple in the sense that he walks with a limp; but in +other respects he appears to be a powerful and well-nurtured man. +Surely your medical experience would tell you, Watson, that +weakness in one limb is often compensated for by exceptional +strength in the others." + +"Pray continue your narrative." + +"Mrs. St. Clair had fainted at the sight of the blood upon the +window, and she was escorted home in a cab by the police, as her +presence could be of no help to them in their investigations. +Inspector Barton, who had charge of the case, made a very careful +examination of the premises, but without finding anything which +threw any light upon the matter. One mistake had been made in not +arresting Boone instantly, as he was allowed some few minutes +during which he might have communicated with his friend the +Lascar, but this fault was soon remedied, and he was seized and +searched, without anything being found which could incriminate +him. There were, it is true, some blood-stains upon his right +shirt-sleeve, but he pointed to his ring-finger, which had been +cut near the nail, and explained that the bleeding came from +there, adding that he had been to the window not long before, and +that the stains which had been observed there came doubtless from +the same source. He denied strenuously having ever seen Mr. +Neville St. Clair and swore that the presence of the clothes in +his room was as much a mystery to him as to the police. As to +Mrs. St. Clair's assertion that she had actually seen her husband +at the window, he declared that she must have been either mad or +dreaming. He was removed, loudly protesting, to the +police-station, while the inspector remained upon the premises in +the hope that the ebbing tide might afford some fresh clue. + +"And it did, though they hardly found upon the mud-bank what they +had feared to find. It was Neville St. Clair's coat, and not +Neville St. Clair, which lay uncovered as the tide receded. And +what do you think they found in the pockets?" + +"I cannot imagine." + +"No, I don't think you would guess. Every pocket stuffed with +pennies and half-pennies--421 pennies and 270 half-pennies. It +was no wonder that it had not been swept away by the tide. But a +human body is a different matter. There is a fierce eddy between +the wharf and the house. It seemed likely enough that the +weighted coat had remained when the stripped body had been sucked +away into the river." + +"But I understand that all the other clothes were found in the +room. Would the body be dressed in a coat alone?" + +"No, sir, but the facts might be met speciously enough. Suppose +that this man Boone had thrust Neville St. Clair through the +window, there is no human eye which could have seen the deed. +What would he do then? It would of course instantly strike him +that he must get rid of the tell-tale garments. He would seize +the coat, then, and be in the act of throwing it out, when it +would occur to him that it would swim and not sink. He has little +time, for he has heard the scuffle downstairs when the wife tried +to force her way up, and perhaps he has already heard from his +Lascar confederate that the police are hurrying up the street. +There is not an instant to be lost. He rushes to some secret +hoard, where he has accumulated the fruits of his beggary, and he +stuffs all the coins upon which he can lay his hands into the +pockets to make sure of the coat's sinking. He throws it out, and +would have done the same with the other garments had not he heard +the rush of steps below, and only just had time to close the +window when the police appeared." + +"It certainly sounds feasible." + +"Well, we will take it as a working hypothesis for want of a +better. Boone, as I have told you, was arrested and taken to the +station, but it could not be shown that there had ever before +been anything against him. He had for years been known as a +professional beggar, but his life appeared to have been a very +quiet and innocent one. There the matter stands at present, and +the questions which have to be solved--what Neville St. Clair was +doing in the opium den, what happened to him when there, where is +he now, and what Hugh Boone had to do with his disappearance--are +all as far from a solution as ever. I confess that I cannot +recall any case within my experience which looked at the first +glance so simple and yet which presented such difficulties." + +While Sherlock Holmes had been detailing this singular series of +events, we had been whirling through the outskirts of the great +town until the last straggling houses had been left behind, and +we rattled along with a country hedge upon either side of us. +Just as he finished, however, we drove through two scattered +villages, where a few lights still glimmered in the windows. + +"We are on the outskirts of Lee," said my companion. "We have +touched on three English counties in our short drive, starting in +Middlesex, passing over an angle of Surrey, and ending in Kent. +See that light among the trees? That is The Cedars, and beside +that lamp sits a woman whose anxious ears have already, I have +little doubt, caught the clink of our horse's feet." + +"But why are you not conducting the case from Baker Street?" I +asked. + +"Because there are many inquiries which must be made out here. +Mrs. St. Clair has most kindly put two rooms at my disposal, and +you may rest assured that she will have nothing but a welcome for +my friend and colleague. I hate to meet her, Watson, when I have +no news of her husband. Here we are. Whoa, there, whoa!" + +We had pulled up in front of a large villa which stood within its +own grounds. A stable-boy had run out to the horse's head, and +springing down, I followed Holmes up the small, winding +gravel-drive which led to the house. As we approached, the door +flew open, and a little blonde woman stood in the opening, clad +in some sort of light mousseline de soie, with a touch of fluffy +pink chiffon at her neck and wrists. She stood with her figure +outlined against the flood of light, one hand upon the door, one +half-raised in her eagerness, her body slightly bent, her head +and face protruded, with eager eyes and parted lips, a standing +question. + +"Well?" she cried, "well?" And then, seeing that there were two +of us, she gave a cry of hope which sank into a groan as she saw +that my companion shook his head and shrugged his shoulders. + +"No good news?" + +"None." + +"No bad?" + +"No." + +"Thank God for that. But come in. You must be weary, for you have +had a long day." + +"This is my friend, Dr. Watson. He has been of most vital use to +me in several of my cases, and a lucky chance has made it +possible for me to bring him out and associate him with this +investigation." + +"I am delighted to see you," said she, pressing my hand warmly. +"You will, I am sure, forgive anything that may be wanting in our +arrangements, when you consider the blow which has come so +suddenly upon us." + +"My dear madam," said I, "I am an old campaigner, and if I were +not I can very well see that no apology is needed. If I can be of +any assistance, either to you or to my friend here, I shall be +indeed happy." + +"Now, Mr. Sherlock Holmes," said the lady as we entered a +well-lit dining-room, upon the table of which a cold supper had +been laid out, "I should very much like to ask you one or two +plain questions, to which I beg that you will give a plain +answer." + +"Certainly, madam." + +"Do not trouble about my feelings. I am not hysterical, nor given +to fainting. I simply wish to hear your real, real opinion." + +"Upon what point?" + +"In your heart of hearts, do you think that Neville is alive?" + +Sherlock Holmes seemed to be embarrassed by the question. +"Frankly, now!" she repeated, standing upon the rug and looking +keenly down at him as he leaned back in a basket-chair. + +"Frankly, then, madam, I do not." + +"You think that he is dead?" + +"I do." + +"Murdered?" + +"I don't say that. Perhaps." + +"And on what day did he meet his death?" + +"On Monday." + +"Then perhaps, Mr. Holmes, you will be good enough to explain how +it is that I have received a letter from him to-day." + +Sherlock Holmes sprang out of his chair as if he had been +galvanised. + +"What!" he roared. + +"Yes, to-day." She stood smiling, holding up a little slip of +paper in the air. + +"May I see it?" + +"Certainly." + +He snatched it from her in his eagerness, and smoothing it out +upon the table he drew over the lamp and examined it intently. I +had left my chair and was gazing at it over his shoulder. The +envelope was a very coarse one and was stamped with the Gravesend +postmark and with the date of that very day, or rather of the day +before, for it was considerably after midnight. + +"Coarse writing," murmured Holmes. "Surely this is not your +husband's writing, madam." + +"No, but the enclosure is." + +"I perceive also that whoever addressed the envelope had to go +and inquire as to the address." + +"How can you tell that?" + +"The name, you see, is in perfectly black ink, which has dried +itself. The rest is of the greyish colour, which shows that +blotting-paper has been used. If it had been written straight +off, and then blotted, none would be of a deep black shade. This +man has written the name, and there has then been a pause before +he wrote the address, which can only mean that he was not +familiar with it. It is, of course, a trifle, but there is +nothing so important as trifles. Let us now see the letter. Ha! +there has been an enclosure here!" + +"Yes, there was a ring. His signet-ring." + +"And you are sure that this is your husband's hand?" + +"One of his hands." + +"One?" + +"His hand when he wrote hurriedly. It is very unlike his usual +writing, and yet I know it well." + +"'Dearest do not be frightened. All will come well. There is a +huge error which it may take some little time to rectify. +Wait in patience.--NEVILLE.' Written in pencil upon the fly-leaf +of a book, octavo size, no water-mark. Hum! Posted to-day in +Gravesend by a man with a dirty thumb. Ha! And the flap has been +gummed, if I am not very much in error, by a person who had been +chewing tobacco. And you have no doubt that it is your husband's +hand, madam?" + +"None. Neville wrote those words." + +"And they were posted to-day at Gravesend. Well, Mrs. St. Clair, +the clouds lighten, though I should not venture to say that the +danger is over." + +"But he must be alive, Mr. Holmes." + +"Unless this is a clever forgery to put us on the wrong scent. +The ring, after all, proves nothing. It may have been taken from +him." + +"No, no; it is, it is his very own writing!" + +"Very well. It may, however, have been written on Monday and only +posted to-day." + +"That is possible." + +"If so, much may have happened between." + +"Oh, you must not discourage me, Mr. Holmes. I know that all is +well with him. There is so keen a sympathy between us that I +should know if evil came upon him. On the very day that I saw him +last he cut himself in the bedroom, and yet I in the dining-room +rushed upstairs instantly with the utmost certainty that +something had happened. Do you think that I would respond to such +a trifle and yet be ignorant of his death?" + +"I have seen too much not to know that the impression of a woman +may be more valuable than the conclusion of an analytical +reasoner. And in this letter you certainly have a very strong +piece of evidence to corroborate your view. But if your husband +is alive and able to write letters, why should he remain away +from you?" + +"I cannot imagine. It is unthinkable." + +"And on Monday he made no remarks before leaving you?" + +"No." + +"And you were surprised to see him in Swandam Lane?" + +"Very much so." + +"Was the window open?" + +"Yes." + +"Then he might have called to you?" + +"He might." + +"He only, as I understand, gave an inarticulate cry?" + +"Yes." + +"A call for help, you thought?" + +"Yes. He waved his hands." + +"But it might have been a cry of surprise. Astonishment at the +unexpected sight of you might cause him to throw up his hands?" + +"It is possible." + +"And you thought he was pulled back?" + +"He disappeared so suddenly." + +"He might have leaped back. You did not see anyone else in the +room?" + +"No, but this horrible man confessed to having been there, and +the Lascar was at the foot of the stairs." + +"Quite so. Your husband, as far as you could see, had his +ordinary clothes on?" + +"But without his collar or tie. I distinctly saw his bare +throat." + +"Had he ever spoken of Swandam Lane?" + +"Never." + +"Had he ever showed any signs of having taken opium?" + +"Never." + +"Thank you, Mrs. St. Clair. Those are the principal points about +which I wished to be absolutely clear. We shall now have a little +supper and then retire, for we may have a very busy day +to-morrow." + +A large and comfortable double-bedded room had been placed at our +disposal, and I was quickly between the sheets, for I was weary +after my night of adventure. Sherlock Holmes was a man, however, +who, when he had an unsolved problem upon his mind, would go for +days, and even for a week, without rest, turning it over, +rearranging his facts, looking at it from every point of view +until he had either fathomed it or convinced himself that his +data were insufficient. It was soon evident to me that he was now +preparing for an all-night sitting. He took off his coat and +waistcoat, put on a large blue dressing-gown, and then wandered +about the room collecting pillows from his bed and cushions from +the sofa and armchairs. With these he constructed a sort of +Eastern divan, upon which he perched himself cross-legged, with +an ounce of shag tobacco and a box of matches laid out in front +of him. In the dim light of the lamp I saw him sitting there, an +old briar pipe between his lips, his eyes fixed vacantly upon the +corner of the ceiling, the blue smoke curling up from him, +silent, motionless, with the light shining upon his strong-set +aquiline features. So he sat as I dropped off to sleep, and so he +sat when a sudden ejaculation caused me to wake up, and I found +the summer sun shining into the apartment. The pipe was still +between his lips, the smoke still curled upward, and the room was +full of a dense tobacco haze, but nothing remained of the heap of +shag which I had seen upon the previous night. + +"Awake, Watson?" he asked. + +"Yes." + +"Game for a morning drive?" + +"Certainly." + +"Then dress. No one is stirring yet, but I know where the +stable-boy sleeps, and we shall soon have the trap out." He +chuckled to himself as he spoke, his eyes twinkled, and he seemed +a different man to the sombre thinker of the previous night. + +As I dressed I glanced at my watch. It was no wonder that no one +was stirring. It was twenty-five minutes past four. I had hardly +finished when Holmes returned with the news that the boy was +putting in the horse. + +"I want to test a little theory of mine," said he, pulling on his +boots. "I think, Watson, that you are now standing in the +presence of one of the most absolute fools in Europe. I deserve +to be kicked from here to Charing Cross. But I think I have the +key of the affair now." + +"And where is it?" I asked, smiling. + +"In the bathroom," he answered. "Oh, yes, I am not joking," he +continued, seeing my look of incredulity. "I have just been +there, and I have taken it out, and I have got it in this +Gladstone bag. Come on, my boy, and we shall see whether it will +not fit the lock." + +We made our way downstairs as quietly as possible, and out into +the bright morning sunshine. In the road stood our horse and +trap, with the half-clad stable-boy waiting at the head. We both +sprang in, and away we dashed down the London Road. A few country +carts were stirring, bearing in vegetables to the metropolis, but +the lines of villas on either side were as silent and lifeless as +some city in a dream. + +"It has been in some points a singular case," said Holmes, +flicking the horse on into a gallop. "I confess that I have been +as blind as a mole, but it is better to learn wisdom late than +never to learn it at all." + +In town the earliest risers were just beginning to look sleepily +from their windows as we drove through the streets of the Surrey +side. Passing down the Waterloo Bridge Road we crossed over the +river, and dashing up Wellington Street wheeled sharply to the +right and found ourselves in Bow Street. Sherlock Holmes was well +known to the force, and the two constables at the door saluted +him. One of them held the horse's head while the other led us in. + +"Who is on duty?" asked Holmes. + +"Inspector Bradstreet, sir." + +"Ah, Bradstreet, how are you?" A tall, stout official had come +down the stone-flagged passage, in a peaked cap and frogged +jacket. "I wish to have a quiet word with you, Bradstreet." +"Certainly, Mr. Holmes. Step into my room here." It was a small, +office-like room, with a huge ledger upon the table, and a +telephone projecting from the wall. The inspector sat down at his +desk. + +"What can I do for you, Mr. Holmes?" + +"I called about that beggarman, Boone--the one who was charged +with being concerned in the disappearance of Mr. Neville St. +Clair, of Lee." + +"Yes. He was brought up and remanded for further inquiries." + +"So I heard. You have him here?" + +"In the cells." + +"Is he quiet?" + +"Oh, he gives no trouble. But he is a dirty scoundrel." + +"Dirty?" + +"Yes, it is all we can do to make him wash his hands, and his +face is as black as a tinker's. Well, when once his case has been +settled, he will have a regular prison bath; and I think, if you +saw him, you would agree with me that he needed it." + +"I should like to see him very much." + +"Would you? That is easily done. Come this way. You can leave +your bag." + +"No, I think that I'll take it." + +"Very good. Come this way, if you please." He led us down a +passage, opened a barred door, passed down a winding stair, and +brought us to a whitewashed corridor with a line of doors on each +side. + +"The third on the right is his," said the inspector. "Here it +is!" He quietly shot back a panel in the upper part of the door +and glanced through. + +"He is asleep," said he. "You can see him very well." + +We both put our eyes to the grating. The prisoner lay with his +face towards us, in a very deep sleep, breathing slowly and +heavily. He was a middle-sized man, coarsely clad as became his +calling, with a coloured shirt protruding through the rent in his +tattered coat. He was, as the inspector had said, extremely +dirty, but the grime which covered his face could not conceal its +repulsive ugliness. A broad wheal from an old scar ran right +across it from eye to chin, and by its contraction had turned up +one side of the upper lip, so that three teeth were exposed in a +perpetual snarl. A shock of very bright red hair grew low over +his eyes and forehead. + +"He's a beauty, isn't he?" said the inspector. + +"He certainly needs a wash," remarked Holmes. "I had an idea that +he might, and I took the liberty of bringing the tools with me." +He opened the Gladstone bag as he spoke, and took out, to my +astonishment, a very large bath-sponge. + +"He! he! You are a funny one," chuckled the inspector. + +"Now, if you will have the great goodness to open that door very +quietly, we will soon make him cut a much more respectable +figure." + +"Well, I don't know why not," said the inspector. "He doesn't +look a credit to the Bow Street cells, does he?" He slipped his +key into the lock, and we all very quietly entered the cell. The +sleeper half turned, and then settled down once more into a deep +slumber. Holmes stooped to the water-jug, moistened his sponge, +and then rubbed it twice vigorously across and down the +prisoner's face. + +"Let me introduce you," he shouted, "to Mr. Neville St. Clair, of +Lee, in the county of Kent." + +Never in my life have I seen such a sight. The man's face peeled +off under the sponge like the bark from a tree. Gone was the +coarse brown tint! Gone, too, was the horrid scar which had +seamed it across, and the twisted lip which had given the +repulsive sneer to the face! A twitch brought away the tangled +red hair, and there, sitting up in his bed, was a pale, +sad-faced, refined-looking man, black-haired and smooth-skinned, +rubbing his eyes and staring about him with sleepy bewilderment. +Then suddenly realising the exposure, he broke into a scream and +threw himself down with his face to the pillow. + +"Great heavens!" cried the inspector, "it is, indeed, the missing +man. I know him from the photograph." + +The prisoner turned with the reckless air of a man who abandons +himself to his destiny. "Be it so," said he. "And pray what am I +charged with?" + +"With making away with Mr. Neville St.-- Oh, come, you can't be +charged with that unless they make a case of attempted suicide of +it," said the inspector with a grin. "Well, I have been +twenty-seven years in the force, but this really takes the cake." + +"If I am Mr. Neville St. Clair, then it is obvious that no crime +has been committed, and that, therefore, I am illegally +detained." + +"No crime, but a very great error has been committed," said +Holmes. "You would have done better to have trusted your wife." + +"It was not the wife; it was the children," groaned the prisoner. +"God help me, I would not have them ashamed of their father. My +God! What an exposure! What can I do?" + +Sherlock Holmes sat down beside him on the couch and patted him +kindly on the shoulder. + +"If you leave it to a court of law to clear the matter up," said +he, "of course you can hardly avoid publicity. On the other hand, +if you convince the police authorities that there is no possible +case against you, I do not know that there is any reason that the +details should find their way into the papers. Inspector +Bradstreet would, I am sure, make notes upon anything which you +might tell us and submit it to the proper authorities. The case +would then never go into court at all." + +"God bless you!" cried the prisoner passionately. "I would have +endured imprisonment, ay, even execution, rather than have left +my miserable secret as a family blot to my children. + +"You are the first who have ever heard my story. My father was a +schoolmaster in Chesterfield, where I received an excellent +education. I travelled in my youth, took to the stage, and +finally became a reporter on an evening paper in London. One day +my editor wished to have a series of articles upon begging in the +metropolis, and I volunteered to supply them. There was the point +from which all my adventures started. It was only by trying +begging as an amateur that I could get the facts upon which to +base my articles. When an actor I had, of course, learned all the +secrets of making up, and had been famous in the green-room for +my skill. I took advantage now of my attainments. I painted my +face, and to make myself as pitiable as possible I made a good +scar and fixed one side of my lip in a twist by the aid of a +small slip of flesh-coloured plaster. Then with a red head of +hair, and an appropriate dress, I took my station in the business +part of the city, ostensibly as a match-seller but really as a +beggar. For seven hours I plied my trade, and when I returned +home in the evening I found to my surprise that I had received no +less than 26s. 4d. + +"I wrote my articles and thought little more of the matter until, +some time later, I backed a bill for a friend and had a writ +served upon me for 25 pounds. I was at my wit's end where to get +the money, but a sudden idea came to me. I begged a fortnight's +grace from the creditor, asked for a holiday from my employers, +and spent the time in begging in the City under my disguise. In +ten days I had the money and had paid the debt. + +"Well, you can imagine how hard it was to settle down to arduous +work at 2 pounds a week when I knew that I could earn as much in +a day by smearing my face with a little paint, laying my cap on +the ground, and sitting still. It was a long fight between my +pride and the money, but the dollars won at last, and I threw up +reporting and sat day after day in the corner which I had first +chosen, inspiring pity by my ghastly face and filling my pockets +with coppers. Only one man knew my secret. He was the keeper of a +low den in which I used to lodge in Swandam Lane, where I could +every morning emerge as a squalid beggar and in the evenings +transform myself into a well-dressed man about town. This fellow, +a Lascar, was well paid by me for his rooms, so that I knew that +my secret was safe in his possession. + +"Well, very soon I found that I was saving considerable sums of +money. I do not mean that any beggar in the streets of London +could earn 700 pounds a year--which is less than my average +takings--but I had exceptional advantages in my power of making +up, and also in a facility of repartee, which improved by +practice and made me quite a recognised character in the City. +All day a stream of pennies, varied by silver, poured in upon me, +and it was a very bad day in which I failed to take 2 pounds. + +"As I grew richer I grew more ambitious, took a house in the +country, and eventually married, without anyone having a +suspicion as to my real occupation. My dear wife knew that I had +business in the City. She little knew what. + +"Last Monday I had finished for the day and was dressing in my +room above the opium den when I looked out of my window and saw, +to my horror and astonishment, that my wife was standing in the +street, with her eyes fixed full upon me. I gave a cry of +surprise, threw up my arms to cover my face, and, rushing to my +confidant, the Lascar, entreated him to prevent anyone from +coming up to me. I heard her voice downstairs, but I knew that +she could not ascend. Swiftly I threw off my clothes, pulled on +those of a beggar, and put on my pigments and wig. Even a wife's +eyes could not pierce so complete a disguise. But then it +occurred to me that there might be a search in the room, and that +the clothes might betray me. I threw open the window, reopening +by my violence a small cut which I had inflicted upon myself in +the bedroom that morning. Then I seized my coat, which was +weighted by the coppers which I had just transferred to it from +the leather bag in which I carried my takings. I hurled it out of +the window, and it disappeared into the Thames. The other clothes +would have followed, but at that moment there was a rush of +constables up the stair, and a few minutes after I found, rather, +I confess, to my relief, that instead of being identified as Mr. +Neville St. Clair, I was arrested as his murderer. + +"I do not know that there is anything else for me to explain. I +was determined to preserve my disguise as long as possible, and +hence my preference for a dirty face. Knowing that my wife would +be terribly anxious, I slipped off my ring and confided it to the +Lascar at a moment when no constable was watching me, together +with a hurried scrawl, telling her that she had no cause to +fear." + +"That note only reached her yesterday," said Holmes. + +"Good God! What a week she must have spent!" + +"The police have watched this Lascar," said Inspector Bradstreet, +"and I can quite understand that he might find it difficult to +post a letter unobserved. Probably he handed it to some sailor +customer of his, who forgot all about it for some days." + +"That was it," said Holmes, nodding approvingly; "I have no doubt +of it. But have you never been prosecuted for begging?" + +"Many times; but what was a fine to me?" + +"It must stop here, however," said Bradstreet. "If the police are +to hush this thing up, there must be no more of Hugh Boone." + +"I have sworn it by the most solemn oaths which a man can take." + +"In that case I think that it is probable that no further steps +may be taken. But if you are found again, then all must come out. +I am sure, Mr. Holmes, that we are very much indebted to you for +having cleared the matter up. I wish I knew how you reach your +results." + +"I reached this one," said my friend, "by sitting upon five +pillows and consuming an ounce of shag. I think, Watson, that if +we drive to Baker Street we shall just be in time for breakfast." + + + +VII. THE ADVENTURE OF THE BLUE CARBUNCLE + +I had called upon my friend Sherlock Holmes upon the second +morning after Christmas, with the intention of wishing him the +compliments of the season. He was lounging upon the sofa in a +purple dressing-gown, a pipe-rack within his reach upon the +right, and a pile of crumpled morning papers, evidently newly +studied, near at hand. Beside the couch was a wooden chair, and +on the angle of the back hung a very seedy and disreputable +hard-felt hat, much the worse for wear, and cracked in several +places. A lens and a forceps lying upon the seat of the chair +suggested that the hat had been suspended in this manner for the +purpose of examination. + +"You are engaged," said I; "perhaps I interrupt you." + +"Not at all. I am glad to have a friend with whom I can discuss +my results. The matter is a perfectly trivial one"--he jerked his +thumb in the direction of the old hat--"but there are points in +connection with it which are not entirely devoid of interest and +even of instruction." + +I seated myself in his armchair and warmed my hands before his +crackling fire, for a sharp frost had set in, and the windows +were thick with the ice crystals. "I suppose," I remarked, "that, +homely as it looks, this thing has some deadly story linked on to +it--that it is the clue which will guide you in the solution of +some mystery and the punishment of some crime." + +"No, no. No crime," said Sherlock Holmes, laughing. "Only one of +those whimsical little incidents which will happen when you have +four million human beings all jostling each other within the +space of a few square miles. Amid the action and reaction of so +dense a swarm of humanity, every possible combination of events +may be expected to take place, and many a little problem will be +presented which may be striking and bizarre without being +criminal. We have already had experience of such." + +"So much so," I remarked, "that of the last six cases which I +have added to my notes, three have been entirely free of any +legal crime." + +"Precisely. You allude to my attempt to recover the Irene Adler +papers, to the singular case of Miss Mary Sutherland, and to the +adventure of the man with the twisted lip. Well, I have no doubt +that this small matter will fall into the same innocent category. +You know Peterson, the commissionaire?" + +"Yes." + +"It is to him that this trophy belongs." + +"It is his hat." + +"No, no, he found it. Its owner is unknown. I beg that you will +look upon it not as a battered billycock but as an intellectual +problem. And, first, as to how it came here. It arrived upon +Christmas morning, in company with a good fat goose, which is, I +have no doubt, roasting at this moment in front of Peterson's +fire. The facts are these: about four o'clock on Christmas +morning, Peterson, who, as you know, is a very honest fellow, was +returning from some small jollification and was making his way +homeward down Tottenham Court Road. In front of him he saw, in +the gaslight, a tallish man, walking with a slight stagger, and +carrying a white goose slung over his shoulder. As he reached the +corner of Goodge Street, a row broke out between this stranger +and a little knot of roughs. One of the latter knocked off the +man's hat, on which he raised his stick to defend himself and, +swinging it over his head, smashed the shop window behind him. +Peterson had rushed forward to protect the stranger from his +assailants; but the man, shocked at having broken the window, and +seeing an official-looking person in uniform rushing towards him, +dropped his goose, took to his heels, and vanished amid the +labyrinth of small streets which lie at the back of Tottenham +Court Road. The roughs had also fled at the appearance of +Peterson, so that he was left in possession of the field of +battle, and also of the spoils of victory in the shape of this +battered hat and a most unimpeachable Christmas goose." + +"Which surely he restored to their owner?" + +"My dear fellow, there lies the problem. It is true that 'For +Mrs. Henry Baker' was printed upon a small card which was tied to +the bird's left leg, and it is also true that the initials 'H. +B.' are legible upon the lining of this hat, but as there are +some thousands of Bakers, and some hundreds of Henry Bakers in +this city of ours, it is not easy to restore lost property to any +one of them." + +"What, then, did Peterson do?" + +"He brought round both hat and goose to me on Christmas morning, +knowing that even the smallest problems are of interest to me. +The goose we retained until this morning, when there were signs +that, in spite of the slight frost, it would be well that it +should be eaten without unnecessary delay. Its finder has carried +it off, therefore, to fulfil the ultimate destiny of a goose, +while I continue to retain the hat of the unknown gentleman who +lost his Christmas dinner." + +"Did he not advertise?" + +"No." + +"Then, what clue could you have as to his identity?" + +"Only as much as we can deduce." + +"From his hat?" + +"Precisely." + +"But you are joking. What can you gather from this old battered +felt?" + +"Here is my lens. You know my methods. What can you gather +yourself as to the individuality of the man who has worn this +article?" + +I took the tattered object in my hands and turned it over rather +ruefully. It was a very ordinary black hat of the usual round +shape, hard and much the worse for wear. The lining had been of +red silk, but was a good deal discoloured. There was no maker's +name; but, as Holmes had remarked, the initials "H. B." were +scrawled upon one side. It was pierced in the brim for a +hat-securer, but the elastic was missing. For the rest, it was +cracked, exceedingly dusty, and spotted in several places, +although there seemed to have been some attempt to hide the +discoloured patches by smearing them with ink. + +"I can see nothing," said I, handing it back to my friend. + +"On the contrary, Watson, you can see everything. You fail, +however, to reason from what you see. You are too timid in +drawing your inferences." + +"Then, pray tell me what it is that you can infer from this hat?" + +He picked it up and gazed at it in the peculiar introspective +fashion which was characteristic of him. "It is perhaps less +suggestive than it might have been," he remarked, "and yet there +are a few inferences which are very distinct, and a few others +which represent at least a strong balance of probability. That +the man was highly intellectual is of course obvious upon the +face of it, and also that he was fairly well-to-do within the +last three years, although he has now fallen upon evil days. He +had foresight, but has less now than formerly, pointing to a +moral retrogression, which, when taken with the decline of his +fortunes, seems to indicate some evil influence, probably drink, +at work upon him. This may account also for the obvious fact that +his wife has ceased to love him." + +"My dear Holmes!" + +"He has, however, retained some degree of self-respect," he +continued, disregarding my remonstrance. "He is a man who leads a +sedentary life, goes out little, is out of training entirely, is +middle-aged, has grizzled hair which he has had cut within the +last few days, and which he anoints with lime-cream. These are +the more patent facts which are to be deduced from his hat. Also, +by the way, that it is extremely improbable that he has gas laid +on in his house." + +"You are certainly joking, Holmes." + +"Not in the least. Is it possible that even now, when I give you +these results, you are unable to see how they are attained?" + +"I have no doubt that I am very stupid, but I must confess that I +am unable to follow you. For example, how did you deduce that +this man was intellectual?" + +For answer Holmes clapped the hat upon his head. It came right +over the forehead and settled upon the bridge of his nose. "It is +a question of cubic capacity," said he; "a man with so large a +brain must have something in it." + +"The decline of his fortunes, then?" + +"This hat is three years old. These flat brims curled at the edge +came in then. It is a hat of the very best quality. Look at the +band of ribbed silk and the excellent lining. If this man could +afford to buy so expensive a hat three years ago, and has had no +hat since, then he has assuredly gone down in the world." + +"Well, that is clear enough, certainly. But how about the +foresight and the moral retrogression?" + +Sherlock Holmes laughed. "Here is the foresight," said he putting +his finger upon the little disc and loop of the hat-securer. +"They are never sold upon hats. If this man ordered one, it is a +sign of a certain amount of foresight, since he went out of his +way to take this precaution against the wind. But since we see +that he has broken the elastic and has not troubled to replace +it, it is obvious that he has less foresight now than formerly, +which is a distinct proof of a weakening nature. On the other +hand, he has endeavoured to conceal some of these stains upon the +felt by daubing them with ink, which is a sign that he has not +entirely lost his self-respect." + +"Your reasoning is certainly plausible." + +"The further points, that he is middle-aged, that his hair is +grizzled, that it has been recently cut, and that he uses +lime-cream, are all to be gathered from a close examination of the +lower part of the lining. The lens discloses a large number of +hair-ends, clean cut by the scissors of the barber. They all +appear to be adhesive, and there is a distinct odour of +lime-cream. This dust, you will observe, is not the gritty, grey +dust of the street but the fluffy brown dust of the house, +showing that it has been hung up indoors most of the time, while +the marks of moisture upon the inside are proof positive that the +wearer perspired very freely, and could therefore, hardly be in +the best of training." + +"But his wife--you said that she had ceased to love him." + +"This hat has not been brushed for weeks. When I see you, my dear +Watson, with a week's accumulation of dust upon your hat, and +when your wife allows you to go out in such a state, I shall fear +that you also have been unfortunate enough to lose your wife's +affection." + +"But he might be a bachelor." + +"Nay, he was bringing home the goose as a peace-offering to his +wife. Remember the card upon the bird's leg." + +"You have an answer to everything. But how on earth do you deduce +that the gas is not laid on in his house?" + +"One tallow stain, or even two, might come by chance; but when I +see no less than five, I think that there can be little doubt +that the individual must be brought into frequent contact with +burning tallow--walks upstairs at night probably with his hat in +one hand and a guttering candle in the other. Anyhow, he never +got tallow-stains from a gas-jet. Are you satisfied?" + +"Well, it is very ingenious," said I, laughing; "but since, as +you said just now, there has been no crime committed, and no harm +done save the loss of a goose, all this seems to be rather a +waste of energy." + +Sherlock Holmes had opened his mouth to reply, when the door flew +open, and Peterson, the commissionaire, rushed into the apartment +with flushed cheeks and the face of a man who is dazed with +astonishment. + +"The goose, Mr. Holmes! The goose, sir!" he gasped. + +"Eh? What of it, then? Has it returned to life and flapped off +through the kitchen window?" Holmes twisted himself round upon +the sofa to get a fairer view of the man's excited face. + +"See here, sir! See what my wife found in its crop!" He held out +his hand and displayed upon the centre of the palm a brilliantly +scintillating blue stone, rather smaller than a bean in size, but +of such purity and radiance that it twinkled like an electric +point in the dark hollow of his hand. + +Sherlock Holmes sat up with a whistle. "By Jove, Peterson!" said +he, "this is treasure trove indeed. I suppose you know what you +have got?" + +"A diamond, sir? A precious stone. It cuts into glass as though +it were putty." + +"It's more than a precious stone. It is the precious stone." + +"Not the Countess of Morcar's blue carbuncle!" I ejaculated. + +"Precisely so. I ought to know its size and shape, seeing that I +have read the advertisement about it in The Times every day +lately. It is absolutely unique, and its value can only be +conjectured, but the reward offered of 1000 pounds is certainly +not within a twentieth part of the market price." + +"A thousand pounds! Great Lord of mercy!" The commissionaire +plumped down into a chair and stared from one to the other of us. + +"That is the reward, and I have reason to know that there are +sentimental considerations in the background which would induce +the Countess to part with half her fortune if she could but +recover the gem." + +"It was lost, if I remember aright, at the Hotel Cosmopolitan," I +remarked. + +"Precisely so, on December 22nd, just five days ago. John Horner, +a plumber, was accused of having abstracted it from the lady's +jewel-case. The evidence against him was so strong that the case +has been referred to the Assizes. I have some account of the +matter here, I believe." He rummaged amid his newspapers, +glancing over the dates, until at last he smoothed one out, +doubled it over, and read the following paragraph: + +"Hotel Cosmopolitan Jewel Robbery. John Horner, 26, plumber, was +brought up upon the charge of having upon the 22nd inst., +abstracted from the jewel-case of the Countess of Morcar the +valuable gem known as the blue carbuncle. James Ryder, +upper-attendant at the hotel, gave his evidence to the effect +that he had shown Horner up to the dressing-room of the Countess +of Morcar upon the day of the robbery in order that he might +solder the second bar of the grate, which was loose. He had +remained with Horner some little time, but had finally been +called away. On returning, he found that Horner had disappeared, +that the bureau had been forced open, and that the small morocco +casket in which, as it afterwards transpired, the Countess was +accustomed to keep her jewel, was lying empty upon the +dressing-table. Ryder instantly gave the alarm, and Horner was +arrested the same evening; but the stone could not be found +either upon his person or in his rooms. Catherine Cusack, maid to +the Countess, deposed to having heard Ryder's cry of dismay on +discovering the robbery, and to having rushed into the room, +where she found matters as described by the last witness. +Inspector Bradstreet, B division, gave evidence as to the arrest +of Horner, who struggled frantically, and protested his innocence +in the strongest terms. Evidence of a previous conviction for +robbery having been given against the prisoner, the magistrate +refused to deal summarily with the offence, but referred it to +the Assizes. Horner, who had shown signs of intense emotion +during the proceedings, fainted away at the conclusion and was +carried out of court." + +"Hum! So much for the police-court," said Holmes thoughtfully, +tossing aside the paper. "The question for us now to solve is the +sequence of events leading from a rifled jewel-case at one end to +the crop of a goose in Tottenham Court Road at the other. You +see, Watson, our little deductions have suddenly assumed a much +more important and less innocent aspect. Here is the stone; the +stone came from the goose, and the goose came from Mr. Henry +Baker, the gentleman with the bad hat and all the other +characteristics with which I have bored you. So now we must set +ourselves very seriously to finding this gentleman and +ascertaining what part he has played in this little mystery. To +do this, we must try the simplest means first, and these lie +undoubtedly in an advertisement in all the evening papers. If +this fail, I shall have recourse to other methods." + +"What will you say?" + +"Give me a pencil and that slip of paper. Now, then: 'Found at +the corner of Goodge Street, a goose and a black felt hat. Mr. +Henry Baker can have the same by applying at 6:30 this evening at +221B, Baker Street.' That is clear and concise." + +"Very. But will he see it?" + +"Well, he is sure to keep an eye on the papers, since, to a poor +man, the loss was a heavy one. He was clearly so scared by his +mischance in breaking the window and by the approach of Peterson +that he thought of nothing but flight, but since then he must +have bitterly regretted the impulse which caused him to drop his +bird. Then, again, the introduction of his name will cause him to +see it, for everyone who knows him will direct his attention to +it. Here you are, Peterson, run down to the advertising agency +and have this put in the evening papers." + +"In which, sir?" + +"Oh, in the Globe, Star, Pall Mall, St. James's, Evening News, +Standard, Echo, and any others that occur to you." + +"Very well, sir. And this stone?" + +"Ah, yes, I shall keep the stone. Thank you. And, I say, +Peterson, just buy a goose on your way back and leave it here +with me, for we must have one to give to this gentleman in place +of the one which your family is now devouring." + +When the commissionaire had gone, Holmes took up the stone and +held it against the light. "It's a bonny thing," said he. "Just +see how it glints and sparkles. Of course it is a nucleus and +focus of crime. Every good stone is. They are the devil's pet +baits. In the larger and older jewels every facet may stand for a +bloody deed. This stone is not yet twenty years old. It was found +in the banks of the Amoy River in southern China and is remarkable +in having every characteristic of the carbuncle, save that it is +blue in shade instead of ruby red. In spite of its youth, it has +already a sinister history. There have been two murders, a +vitriol-throwing, a suicide, and several robberies brought about +for the sake of this forty-grain weight of crystallised charcoal. +Who would think that so pretty a toy would be a purveyor to the +gallows and the prison? I'll lock it up in my strong box now and +drop a line to the Countess to say that we have it." + +"Do you think that this man Horner is innocent?" + +"I cannot tell." + +"Well, then, do you imagine that this other one, Henry Baker, had +anything to do with the matter?" + +"It is, I think, much more likely that Henry Baker is an +absolutely innocent man, who had no idea that the bird which he +was carrying was of considerably more value than if it were made +of solid gold. That, however, I shall determine by a very simple +test if we have an answer to our advertisement." + +"And you can do nothing until then?" + +"Nothing." + +"In that case I shall continue my professional round. But I shall +come back in the evening at the hour you have mentioned, for I +should like to see the solution of so tangled a business." + +"Very glad to see you. I dine at seven. There is a woodcock, I +believe. By the way, in view of recent occurrences, perhaps I +ought to ask Mrs. Hudson to examine its crop." + +I had been delayed at a case, and it was a little after half-past +six when I found myself in Baker Street once more. As I +approached the house I saw a tall man in a Scotch bonnet with a +coat which was buttoned up to his chin waiting outside in the +bright semicircle which was thrown from the fanlight. Just as I +arrived the door was opened, and we were shown up together to +Holmes' room. + +"Mr. Henry Baker, I believe," said he, rising from his armchair +and greeting his visitor with the easy air of geniality which he +could so readily assume. "Pray take this chair by the fire, Mr. +Baker. It is a cold night, and I observe that your circulation is +more adapted for summer than for winter. Ah, Watson, you have +just come at the right time. Is that your hat, Mr. Baker?" + +"Yes, sir, that is undoubtedly my hat." + +He was a large man with rounded shoulders, a massive head, and a +broad, intelligent face, sloping down to a pointed beard of +grizzled brown. A touch of red in nose and cheeks, with a slight +tremor of his extended hand, recalled Holmes' surmise as to his +habits. His rusty black frock-coat was buttoned right up in +front, with the collar turned up, and his lank wrists protruded +from his sleeves without a sign of cuff or shirt. He spoke in a +slow staccato fashion, choosing his words with care, and gave the +impression generally of a man of learning and letters who had had +ill-usage at the hands of fortune. + +"We have retained these things for some days," said Holmes, +"because we expected to see an advertisement from you giving your +address. I am at a loss to know now why you did not advertise." + +Our visitor gave a rather shamefaced laugh. "Shillings have not +been so plentiful with me as they once were," he remarked. "I had +no doubt that the gang of roughs who assaulted me had carried off +both my hat and the bird. I did not care to spend more money in a +hopeless attempt at recovering them." + +"Very naturally. By the way, about the bird, we were compelled to +eat it." + +"To eat it!" Our visitor half rose from his chair in his +excitement. + +"Yes, it would have been of no use to anyone had we not done so. +But I presume that this other goose upon the sideboard, which is +about the same weight and perfectly fresh, will answer your +purpose equally well?" + +"Oh, certainly, certainly," answered Mr. Baker with a sigh of +relief. + +"Of course, we still have the feathers, legs, crop, and so on of +your own bird, so if you wish--" + +The man burst into a hearty laugh. "They might be useful to me as +relics of my adventure," said he, "but beyond that I can hardly +see what use the disjecta membra of my late acquaintance are +going to be to me. No, sir, I think that, with your permission, I +will confine my attentions to the excellent bird which I perceive +upon the sideboard." + +Sherlock Holmes glanced sharply across at me with a slight shrug +of his shoulders. + +"There is your hat, then, and there your bird," said he. "By the +way, would it bore you to tell me where you got the other one +from? I am somewhat of a fowl fancier, and I have seldom seen a +better grown goose." + +"Certainly, sir," said Baker, who had risen and tucked his newly +gained property under his arm. "There are a few of us who +frequent the Alpha Inn, near the Museum--we are to be found in +the Museum itself during the day, you understand. This year our +good host, Windigate by name, instituted a goose club, by which, +on consideration of some few pence every week, we were each to +receive a bird at Christmas. My pence were duly paid, and the +rest is familiar to you. I am much indebted to you, sir, for a +Scotch bonnet is fitted neither to my years nor my gravity." With +a comical pomposity of manner he bowed solemnly to both of us and +strode off upon his way. + +"So much for Mr. Henry Baker," said Holmes when he had closed the +door behind him. "It is quite certain that he knows nothing +whatever about the matter. Are you hungry, Watson?" + +"Not particularly." + +"Then I suggest that we turn our dinner into a supper and follow +up this clue while it is still hot." + +"By all means." + +It was a bitter night, so we drew on our ulsters and wrapped +cravats about our throats. Outside, the stars were shining coldly +in a cloudless sky, and the breath of the passers-by blew out +into smoke like so many pistol shots. Our footfalls rang out +crisply and loudly as we swung through the doctors' quarter, +Wimpole Street, Harley Street, and so through Wigmore Street into +Oxford Street. In a quarter of an hour we were in Bloomsbury at +the Alpha Inn, which is a small public-house at the corner of one +of the streets which runs down into Holborn. Holmes pushed open +the door of the private bar and ordered two glasses of beer from +the ruddy-faced, white-aproned landlord. + +"Your beer should be excellent if it is as good as your geese," +said he. + +"My geese!" The man seemed surprised. + +"Yes. I was speaking only half an hour ago to Mr. Henry Baker, +who was a member of your goose club." + +"Ah! yes, I see. But you see, sir, them's not our geese." + +"Indeed! Whose, then?" + +"Well, I got the two dozen from a salesman in Covent Garden." + +"Indeed? I know some of them. Which was it?" + +"Breckinridge is his name." + +"Ah! I don't know him. Well, here's your good health landlord, +and prosperity to your house. Good-night." + +"Now for Mr. Breckinridge," he continued, buttoning up his coat +as we came out into the frosty air. "Remember, Watson that though +we have so homely a thing as a goose at one end of this chain, we +have at the other a man who will certainly get seven years' penal +servitude unless we can establish his innocence. It is possible +that our inquiry may but confirm his guilt; but, in any case, we +have a line of investigation which has been missed by the police, +and which a singular chance has placed in our hands. Let us +follow it out to the bitter end. Faces to the south, then, and +quick march!" + +We passed across Holborn, down Endell Street, and so through a +zigzag of slums to Covent Garden Market. One of the largest +stalls bore the name of Breckinridge upon it, and the proprietor +a horsey-looking man, with a sharp face and trim side-whiskers was +helping a boy to put up the shutters. + +"Good-evening. It's a cold night," said Holmes. + +The salesman nodded and shot a questioning glance at my +companion. + +"Sold out of geese, I see," continued Holmes, pointing at the +bare slabs of marble. + +"Let you have five hundred to-morrow morning." + +"That's no good." + +"Well, there are some on the stall with the gas-flare." + +"Ah, but I was recommended to you." + +"Who by?" + +"The landlord of the Alpha." + +"Oh, yes; I sent him a couple of dozen." + +"Fine birds they were, too. Now where did you get them from?" + +To my surprise the question provoked a burst of anger from the +salesman. + +"Now, then, mister," said he, with his head cocked and his arms +akimbo, "what are you driving at? Let's have it straight, now." + +"It is straight enough. I should like to know who sold you the +geese which you supplied to the Alpha." + +"Well then, I shan't tell you. So now!" + +"Oh, it is a matter of no importance; but I don't know why you +should be so warm over such a trifle." + +"Warm! You'd be as warm, maybe, if you were as pestered as I am. +When I pay good money for a good article there should be an end +of the business; but it's 'Where are the geese?' and 'Who did you +sell the geese to?' and 'What will you take for the geese?' One +would think they were the only geese in the world, to hear the +fuss that is made over them." + +"Well, I have no connection with any other people who have been +making inquiries," said Holmes carelessly. "If you won't tell us +the bet is off, that is all. But I'm always ready to back my +opinion on a matter of fowls, and I have a fiver on it that the +bird I ate is country bred." + +"Well, then, you've lost your fiver, for it's town bred," snapped +the salesman. + +"It's nothing of the kind." + +"I say it is." + +"I don't believe it." + +"D'you think you know more about fowls than I, who have handled +them ever since I was a nipper? I tell you, all those birds that +went to the Alpha were town bred." + +"You'll never persuade me to believe that." + +"Will you bet, then?" + +"It's merely taking your money, for I know that I am right. But +I'll have a sovereign on with you, just to teach you not to be +obstinate." + +The salesman chuckled grimly. "Bring me the books, Bill," said +he. + +The small boy brought round a small thin volume and a great +greasy-backed one, laying them out together beneath the hanging +lamp. + +"Now then, Mr. Cocksure," said the salesman, "I thought that I +was out of geese, but before I finish you'll find that there is +still one left in my shop. You see this little book?" + +"Well?" + +"That's the list of the folk from whom I buy. D'you see? Well, +then, here on this page are the country folk, and the numbers +after their names are where their accounts are in the big ledger. +Now, then! You see this other page in red ink? Well, that is a +list of my town suppliers. Now, look at that third name. Just +read it out to me." + +"Mrs. Oakshott, 117, Brixton Road--249," read Holmes. + +"Quite so. Now turn that up in the ledger." + +Holmes turned to the page indicated. "Here you are, 'Mrs. +Oakshott, 117, Brixton Road, egg and poultry supplier.'" + +"Now, then, what's the last entry?" + +"'December 22nd. Twenty-four geese at 7s. 6d.'" + +"Quite so. There you are. And underneath?" + +"'Sold to Mr. Windigate of the Alpha, at 12s.'" + +"What have you to say now?" + +Sherlock Holmes looked deeply chagrined. He drew a sovereign from +his pocket and threw it down upon the slab, turning away with the +air of a man whose disgust is too deep for words. A few yards off +he stopped under a lamp-post and laughed in the hearty, noiseless +fashion which was peculiar to him. + +"When you see a man with whiskers of that cut and the 'Pink 'un' +protruding out of his pocket, you can always draw him by a bet," +said he. "I daresay that if I had put 100 pounds down in front of +him, that man would not have given me such complete information +as was drawn from him by the idea that he was doing me on a +wager. Well, Watson, we are, I fancy, nearing the end of our +quest, and the only point which remains to be determined is +whether we should go on to this Mrs. Oakshott to-night, or +whether we should reserve it for to-morrow. It is clear from what +that surly fellow said that there are others besides ourselves +who are anxious about the matter, and I should--" + +His remarks were suddenly cut short by a loud hubbub which broke +out from the stall which we had just left. Turning round we saw a +little rat-faced fellow standing in the centre of the circle of +yellow light which was thrown by the swinging lamp, while +Breckinridge, the salesman, framed in the door of his stall, was +shaking his fists fiercely at the cringing figure. + +"I've had enough of you and your geese," he shouted. "I wish you +were all at the devil together. If you come pestering me any more +with your silly talk I'll set the dog at you. You bring Mrs. +Oakshott here and I'll answer her, but what have you to do with +it? Did I buy the geese off you?" + +"No; but one of them was mine all the same," whined the little +man. + +"Well, then, ask Mrs. Oakshott for it." + +"She told me to ask you." + +"Well, you can ask the King of Proosia, for all I care. I've had +enough of it. Get out of this!" He rushed fiercely forward, and +the inquirer flitted away into the darkness. + +"Ha! this may save us a visit to Brixton Road," whispered Holmes. +"Come with me, and we will see what is to be made of this +fellow." Striding through the scattered knots of people who +lounged round the flaring stalls, my companion speedily overtook +the little man and touched him upon the shoulder. He sprang +round, and I could see in the gas-light that every vestige of +colour had been driven from his face. + +"Who are you, then? What do you want?" he asked in a quavering +voice. + +"You will excuse me," said Holmes blandly, "but I could not help +overhearing the questions which you put to the salesman just now. +I think that I could be of assistance to you." + +"You? Who are you? How could you know anything of the matter?" + +"My name is Sherlock Holmes. It is my business to know what other +people don't know." + +"But you can know nothing of this?" + +"Excuse me, I know everything of it. You are endeavouring to +trace some geese which were sold by Mrs. Oakshott, of Brixton +Road, to a salesman named Breckinridge, by him in turn to Mr. +Windigate, of the Alpha, and by him to his club, of which Mr. +Henry Baker is a member." + +"Oh, sir, you are the very man whom I have longed to meet," cried +the little fellow with outstretched hands and quivering fingers. +"I can hardly explain to you how interested I am in this matter." + +Sherlock Holmes hailed a four-wheeler which was passing. "In that +case we had better discuss it in a cosy room rather than in this +wind-swept market-place," said he. "But pray tell me, before we +go farther, who it is that I have the pleasure of assisting." + +The man hesitated for an instant. "My name is John Robinson," he +answered with a sidelong glance. + +"No, no; the real name," said Holmes sweetly. "It is always +awkward doing business with an alias." + +A flush sprang to the white cheeks of the stranger. "Well then," +said he, "my real name is James Ryder." + +"Precisely so. Head attendant at the Hotel Cosmopolitan. Pray +step into the cab, and I shall soon be able to tell you +everything which you would wish to know." + +The little man stood glancing from one to the other of us with +half-frightened, half-hopeful eyes, as one who is not sure +whether he is on the verge of a windfall or of a catastrophe. +Then he stepped into the cab, and in half an hour we were back in +the sitting-room at Baker Street. Nothing had been said during +our drive, but the high, thin breathing of our new companion, and +the claspings and unclaspings of his hands, spoke of the nervous +tension within him. + +"Here we are!" said Holmes cheerily as we filed into the room. +"The fire looks very seasonable in this weather. You look cold, +Mr. Ryder. Pray take the basket-chair. I will just put on my +slippers before we settle this little matter of yours. Now, then! +You want to know what became of those geese?" + +"Yes, sir." + +"Or rather, I fancy, of that goose. It was one bird, I imagine in +which you were interested--white, with a black bar across the +tail." + +Ryder quivered with emotion. "Oh, sir," he cried, "can you tell +me where it went to?" + +"It came here." + +"Here?" + +"Yes, and a most remarkable bird it proved. I don't wonder that +you should take an interest in it. It laid an egg after it was +dead--the bonniest, brightest little blue egg that ever was seen. +I have it here in my museum." + +Our visitor staggered to his feet and clutched the mantelpiece +with his right hand. Holmes unlocked his strong-box and held up +the blue carbuncle, which shone out like a star, with a cold, +brilliant, many-pointed radiance. Ryder stood glaring with a +drawn face, uncertain whether to claim or to disown it. + +"The game's up, Ryder," said Holmes quietly. "Hold up, man, or +you'll be into the fire! Give him an arm back into his chair, +Watson. He's not got blood enough to go in for felony with +impunity. Give him a dash of brandy. So! Now he looks a little +more human. What a shrimp it is, to be sure!" + +For a moment he had staggered and nearly fallen, but the brandy +brought a tinge of colour into his cheeks, and he sat staring +with frightened eyes at his accuser. + +"I have almost every link in my hands, and all the proofs which I +could possibly need, so there is little which you need tell me. +Still, that little may as well be cleared up to make the case +complete. You had heard, Ryder, of this blue stone of the +Countess of Morcar's?" + +"It was Catherine Cusack who told me of it," said he in a +crackling voice. + +"I see--her ladyship's waiting-maid. Well, the temptation of +sudden wealth so easily acquired was too much for you, as it has +been for better men before you; but you were not very scrupulous +in the means you used. It seems to me, Ryder, that there is the +making of a very pretty villain in you. You knew that this man +Horner, the plumber, had been concerned in some such matter +before, and that suspicion would rest the more readily upon him. +What did you do, then? You made some small job in my lady's +room--you and your confederate Cusack--and you managed that he +should be the man sent for. Then, when he had left, you rifled +the jewel-case, raised the alarm, and had this unfortunate man +arrested. You then--" + +Ryder threw himself down suddenly upon the rug and clutched at my +companion's knees. "For God's sake, have mercy!" he shrieked. +"Think of my father! Of my mother! It would break their hearts. I +never went wrong before! I never will again. I swear it. I'll +swear it on a Bible. Oh, don't bring it into court! For Christ's +sake, don't!" + +"Get back into your chair!" said Holmes sternly. "It is very well +to cringe and crawl now, but you thought little enough of this +poor Horner in the dock for a crime of which he knew nothing." + +"I will fly, Mr. Holmes. I will leave the country, sir. Then the +charge against him will break down." + +"Hum! We will talk about that. And now let us hear a true account +of the next act. How came the stone into the goose, and how came +the goose into the open market? Tell us the truth, for there lies +your only hope of safety." + +Ryder passed his tongue over his parched lips. "I will tell you +it just as it happened, sir," said he. "When Horner had been +arrested, it seemed to me that it would be best for me to get +away with the stone at once, for I did not know at what moment +the police might not take it into their heads to search me and my +room. There was no place about the hotel where it would be safe. +I went out, as if on some commission, and I made for my sister's +house. She had married a man named Oakshott, and lived in Brixton +Road, where she fattened fowls for the market. All the way there +every man I met seemed to me to be a policeman or a detective; +and, for all that it was a cold night, the sweat was pouring down +my face before I came to the Brixton Road. My sister asked me +what was the matter, and why I was so pale; but I told her that I +had been upset by the jewel robbery at the hotel. Then I went +into the back yard and smoked a pipe and wondered what it would +be best to do. + +"I had a friend once called Maudsley, who went to the bad, and +has just been serving his time in Pentonville. One day he had met +me, and fell into talk about the ways of thieves, and how they +could get rid of what they stole. I knew that he would be true to +me, for I knew one or two things about him; so I made up my mind +to go right on to Kilburn, where he lived, and take him into my +confidence. He would show me how to turn the stone into money. +But how to get to him in safety? I thought of the agonies I had +gone through in coming from the hotel. I might at any moment be +seized and searched, and there would be the stone in my waistcoat +pocket. I was leaning against the wall at the time and looking at +the geese which were waddling about round my feet, and suddenly +an idea came into my head which showed me how I could beat the +best detective that ever lived. + +"My sister had told me some weeks before that I might have the +pick of her geese for a Christmas present, and I knew that she +was always as good as her word. I would take my goose now, and in +it I would carry my stone to Kilburn. There was a little shed in +the yard, and behind this I drove one of the birds--a fine big +one, white, with a barred tail. I caught it, and prying its bill +open, I thrust the stone down its throat as far as my finger +could reach. The bird gave a gulp, and I felt the stone pass +along its gullet and down into its crop. But the creature flapped +and struggled, and out came my sister to know what was the +matter. As I turned to speak to her the brute broke loose and +fluttered off among the others. + +"'Whatever were you doing with that bird, Jem?' says she. + +"'Well,' said I, 'you said you'd give me one for Christmas, and I +was feeling which was the fattest.' + +"'Oh,' says she, 'we've set yours aside for you--Jem's bird, we +call it. It's the big white one over yonder. There's twenty-six +of them, which makes one for you, and one for us, and two dozen +for the market.' + +"'Thank you, Maggie,' says I; 'but if it is all the same to you, +I'd rather have that one I was handling just now.' + +"'The other is a good three pound heavier,' said she, 'and we +fattened it expressly for you.' + +"'Never mind. I'll have the other, and I'll take it now,' said I. + +"'Oh, just as you like,' said she, a little huffed. 'Which is it +you want, then?' + +"'That white one with the barred tail, right in the middle of the +flock.' + +"'Oh, very well. Kill it and take it with you.' + +"Well, I did what she said, Mr. Holmes, and I carried the bird +all the way to Kilburn. I told my pal what I had done, for he was +a man that it was easy to tell a thing like that to. He laughed +until he choked, and we got a knife and opened the goose. My +heart turned to water, for there was no sign of the stone, and I +knew that some terrible mistake had occurred. I left the bird, +rushed back to my sister's, and hurried into the back yard. There +was not a bird to be seen there. + +"'Where are they all, Maggie?' I cried. + +"'Gone to the dealer's, Jem.' + +"'Which dealer's?' + +"'Breckinridge, of Covent Garden.' + +"'But was there another with a barred tail?' I asked, 'the same +as the one I chose?' + +"'Yes, Jem; there were two barred-tailed ones, and I could never +tell them apart.' + +"Well, then, of course I saw it all, and I ran off as hard as my +feet would carry me to this man Breckinridge; but he had sold the +lot at once, and not one word would he tell me as to where they +had gone. You heard him yourselves to-night. Well, he has always +answered me like that. My sister thinks that I am going mad. +Sometimes I think that I am myself. And now--and now I am myself +a branded thief, without ever having touched the wealth for which +I sold my character. God help me! God help me!" He burst into +convulsive sobbing, with his face buried in his hands. + +There was a long silence, broken only by his heavy breathing and +by the measured tapping of Sherlock Holmes' finger-tips upon the +edge of the table. Then my friend rose and threw open the door. + +"Get out!" said he. + +"What, sir! Oh, Heaven bless you!" + +"No more words. Get out!" + +And no more words were needed. There was a rush, a clatter upon +the stairs, the bang of a door, and the crisp rattle of running +footfalls from the street. + +"After all, Watson," said Holmes, reaching up his hand for his +clay pipe, "I am not retained by the police to supply their +deficiencies. If Horner were in danger it would be another thing; +but this fellow will not appear against him, and the case must +collapse. I suppose that I am commuting a felony, but it is just +possible that I am saving a soul. This fellow will not go wrong +again; he is too terribly frightened. Send him to gaol now, and +you make him a gaol-bird for life. Besides, it is the season of +forgiveness. Chance has put in our way a most singular and +whimsical problem, and its solution is its own reward. If you +will have the goodness to touch the bell, Doctor, we will begin +another investigation, in which, also a bird will be the chief +feature." + + + +VIII. THE ADVENTURE OF THE SPECKLED BAND + +On glancing over my notes of the seventy odd cases in which I +have during the last eight years studied the methods of my friend +Sherlock Holmes, I find many tragic, some comic, a large number +merely strange, but none commonplace; for, working as he did +rather for the love of his art than for the acquirement of +wealth, he refused to associate himself with any investigation +which did not tend towards the unusual, and even the fantastic. +Of all these varied cases, however, I cannot recall any which +presented more singular features than that which was associated +with the well-known Surrey family of the Roylotts of Stoke Moran. +The events in question occurred in the early days of my +association with Holmes, when we were sharing rooms as bachelors +in Baker Street. It is possible that I might have placed them +upon record before, but a promise of secrecy was made at the +time, from which I have only been freed during the last month by +the untimely death of the lady to whom the pledge was given. It +is perhaps as well that the facts should now come to light, for I +have reasons to know that there are widespread rumours as to the +death of Dr. Grimesby Roylott which tend to make the matter even +more terrible than the truth. + +It was early in April in the year '83 that I woke one morning to +find Sherlock Holmes standing, fully dressed, by the side of my +bed. He was a late riser, as a rule, and as the clock on the +mantelpiece showed me that it was only a quarter-past seven, I +blinked up at him in some surprise, and perhaps just a little +resentment, for I was myself regular in my habits. + +"Very sorry to knock you up, Watson," said he, "but it's the +common lot this morning. Mrs. Hudson has been knocked up, she +retorted upon me, and I on you." + +"What is it, then--a fire?" + +"No; a client. It seems that a young lady has arrived in a +considerable state of excitement, who insists upon seeing me. She +is waiting now in the sitting-room. Now, when young ladies wander +about the metropolis at this hour of the morning, and knock +sleepy people up out of their beds, I presume that it is +something very pressing which they have to communicate. Should it +prove to be an interesting case, you would, I am sure, wish to +follow it from the outset. I thought, at any rate, that I should +call you and give you the chance." + +"My dear fellow, I would not miss it for anything." + +I had no keener pleasure than in following Holmes in his +professional investigations, and in admiring the rapid +deductions, as swift as intuitions, and yet always founded on a +logical basis with which he unravelled the problems which were +submitted to him. I rapidly threw on my clothes and was ready in +a few minutes to accompany my friend down to the sitting-room. A +lady dressed in black and heavily veiled, who had been sitting in +the window, rose as we entered. + +"Good-morning, madam," said Holmes cheerily. "My name is Sherlock +Holmes. This is my intimate friend and associate, Dr. Watson, +before whom you can speak as freely as before myself. Ha! I am +glad to see that Mrs. Hudson has had the good sense to light the +fire. Pray draw up to it, and I shall order you a cup of hot +coffee, for I observe that you are shivering." + +"It is not cold which makes me shiver," said the woman in a low +voice, changing her seat as requested. + +"What, then?" + +"It is fear, Mr. Holmes. It is terror." She raised her veil as +she spoke, and we could see that she was indeed in a pitiable +state of agitation, her face all drawn and grey, with restless +frightened eyes, like those of some hunted animal. Her features +and figure were those of a woman of thirty, but her hair was shot +with premature grey, and her expression was weary and haggard. +Sherlock Holmes ran her over with one of his quick, +all-comprehensive glances. + +"You must not fear," said he soothingly, bending forward and +patting her forearm. "We shall soon set matters right, I have no +doubt. You have come in by train this morning, I see." + +"You know me, then?" + +"No, but I observe the second half of a return ticket in the palm +of your left glove. You must have started early, and yet you had +a good drive in a dog-cart, along heavy roads, before you reached +the station." + +The lady gave a violent start and stared in bewilderment at my +companion. + +"There is no mystery, my dear madam," said he, smiling. "The left +arm of your jacket is spattered with mud in no less than seven +places. The marks are perfectly fresh. There is no vehicle save a +dog-cart which throws up mud in that way, and then only when you +sit on the left-hand side of the driver." + +"Whatever your reasons may be, you are perfectly correct," said +she. "I started from home before six, reached Leatherhead at +twenty past, and came in by the first train to Waterloo. Sir, I +can stand this strain no longer; I shall go mad if it continues. +I have no one to turn to--none, save only one, who cares for me, +and he, poor fellow, can be of little aid. I have heard of you, +Mr. Holmes; I have heard of you from Mrs. Farintosh, whom you +helped in the hour of her sore need. It was from her that I had +your address. Oh, sir, do you not think that you could help me, +too, and at least throw a little light through the dense darkness +which surrounds me? At present it is out of my power to reward +you for your services, but in a month or six weeks I shall be +married, with the control of my own income, and then at least you +shall not find me ungrateful." + +Holmes turned to his desk and, unlocking it, drew out a small +case-book, which he consulted. + +"Farintosh," said he. "Ah yes, I recall the case; it was +concerned with an opal tiara. I think it was before your time, +Watson. I can only say, madam, that I shall be happy to devote +the same care to your case as I did to that of your friend. As to +reward, my profession is its own reward; but you are at liberty +to defray whatever expenses I may be put to, at the time which +suits you best. And now I beg that you will lay before us +everything that may help us in forming an opinion upon the +matter." + +"Alas!" replied our visitor, "the very horror of my situation +lies in the fact that my fears are so vague, and my suspicions +depend so entirely upon small points, which might seem trivial to +another, that even he to whom of all others I have a right to +look for help and advice looks upon all that I tell him about it +as the fancies of a nervous woman. He does not say so, but I can +read it from his soothing answers and averted eyes. But I have +heard, Mr. Holmes, that you can see deeply into the manifold +wickedness of the human heart. You may advise me how to walk amid +the dangers which encompass me." + +"I am all attention, madam." + +"My name is Helen Stoner, and I am living with my stepfather, who +is the last survivor of one of the oldest Saxon families in +England, the Roylotts of Stoke Moran, on the western border of +Surrey." + +Holmes nodded his head. "The name is familiar to me," said he. + +"The family was at one time among the richest in England, and the +estates extended over the borders into Berkshire in the north, +and Hampshire in the west. In the last century, however, four +successive heirs were of a dissolute and wasteful disposition, +and the family ruin was eventually completed by a gambler in the +days of the Regency. Nothing was left save a few acres of ground, +and the two-hundred-year-old house, which is itself crushed under +a heavy mortgage. The last squire dragged out his existence +there, living the horrible life of an aristocratic pauper; but +his only son, my stepfather, seeing that he must adapt himself to +the new conditions, obtained an advance from a relative, which +enabled him to take a medical degree and went out to Calcutta, +where, by his professional skill and his force of character, he +established a large practice. In a fit of anger, however, caused +by some robberies which had been perpetrated in the house, he +beat his native butler to death and narrowly escaped a capital +sentence. As it was, he suffered a long term of imprisonment and +afterwards returned to England a morose and disappointed man. + +"When Dr. Roylott was in India he married my mother, Mrs. Stoner, +the young widow of Major-General Stoner, of the Bengal Artillery. +My sister Julia and I were twins, and we were only two years old +at the time of my mother's re-marriage. She had a considerable +sum of money--not less than 1000 pounds a year--and this she +bequeathed to Dr. Roylott entirely while we resided with him, +with a provision that a certain annual sum should be allowed to +each of us in the event of our marriage. Shortly after our return +to England my mother died--she was killed eight years ago in a +railway accident near Crewe. Dr. Roylott then abandoned his +attempts to establish himself in practice in London and took us +to live with him in the old ancestral house at Stoke Moran. The +money which my mother had left was enough for all our wants, and +there seemed to be no obstacle to our happiness. + +"But a terrible change came over our stepfather about this time. +Instead of making friends and exchanging visits with our +neighbours, who had at first been overjoyed to see a Roylott of +Stoke Moran back in the old family seat, he shut himself up in +his house and seldom came out save to indulge in ferocious +quarrels with whoever might cross his path. Violence of temper +approaching to mania has been hereditary in the men of the +family, and in my stepfather's case it had, I believe, been +intensified by his long residence in the tropics. A series of +disgraceful brawls took place, two of which ended in the +police-court, until at last he became the terror of the village, +and the folks would fly at his approach, for he is a man of +immense strength, and absolutely uncontrollable in his anger. + +"Last week he hurled the local blacksmith over a parapet into a +stream, and it was only by paying over all the money which I +could gather together that I was able to avert another public +exposure. He had no friends at all save the wandering gipsies, +and he would give these vagabonds leave to encamp upon the few +acres of bramble-covered land which represent the family estate, +and would accept in return the hospitality of their tents, +wandering away with them sometimes for weeks on end. He has a +passion also for Indian animals, which are sent over to him by a +correspondent, and he has at this moment a cheetah and a baboon, +which wander freely over his grounds and are feared by the +villagers almost as much as their master. + +"You can imagine from what I say that my poor sister Julia and I +had no great pleasure in our lives. No servant would stay with +us, and for a long time we did all the work of the house. She was +but thirty at the time of her death, and yet her hair had already +begun to whiten, even as mine has." + +"Your sister is dead, then?" + +"She died just two years ago, and it is of her death that I wish +to speak to you. You can understand that, living the life which I +have described, we were little likely to see anyone of our own +age and position. We had, however, an aunt, my mother's maiden +sister, Miss Honoria Westphail, who lives near Harrow, and we +were occasionally allowed to pay short visits at this lady's +house. Julia went there at Christmas two years ago, and met there +a half-pay major of marines, to whom she became engaged. My +stepfather learned of the engagement when my sister returned and +offered no objection to the marriage; but within a fortnight of +the day which had been fixed for the wedding, the terrible event +occurred which has deprived me of my only companion." + +Sherlock Holmes had been leaning back in his chair with his eyes +closed and his head sunk in a cushion, but he half opened his +lids now and glanced across at his visitor. + +"Pray be precise as to details," said he. + +"It is easy for me to be so, for every event of that dreadful +time is seared into my memory. The manor-house is, as I have +already said, very old, and only one wing is now inhabited. The +bedrooms in this wing are on the ground floor, the sitting-rooms +being in the central block of the buildings. Of these bedrooms +the first is Dr. Roylott's, the second my sister's, and the third +my own. There is no communication between them, but they all open +out into the same corridor. Do I make myself plain?" + +"Perfectly so." + +"The windows of the three rooms open out upon the lawn. That +fatal night Dr. Roylott had gone to his room early, though we +knew that he had not retired to rest, for my sister was troubled +by the smell of the strong Indian cigars which it was his custom +to smoke. She left her room, therefore, and came into mine, where +she sat for some time, chatting about her approaching wedding. At +eleven o'clock she rose to leave me, but she paused at the door +and looked back. + +"'Tell me, Helen,' said she, 'have you ever heard anyone whistle +in the dead of the night?' + +"'Never,' said I. + +"'I suppose that you could not possibly whistle, yourself, in +your sleep?' + +"'Certainly not. But why?' + +"'Because during the last few nights I have always, about three +in the morning, heard a low, clear whistle. I am a light sleeper, +and it has awakened me. I cannot tell where it came from--perhaps +from the next room, perhaps from the lawn. I thought that I would +just ask you whether you had heard it.' + +"'No, I have not. It must be those wretched gipsies in the +plantation.' + +"'Very likely. And yet if it were on the lawn, I wonder that you +did not hear it also.' + +"'Ah, but I sleep more heavily than you.' + +"'Well, it is of no great consequence, at any rate.' She smiled +back at me, closed my door, and a few moments later I heard her +key turn in the lock." + +"Indeed," said Holmes. "Was it your custom always to lock +yourselves in at night?" + +"Always." + +"And why?" + +"I think that I mentioned to you that the doctor kept a cheetah +and a baboon. We had no feeling of security unless our doors were +locked." + +"Quite so. Pray proceed with your statement." + +"I could not sleep that night. A vague feeling of impending +misfortune impressed me. My sister and I, you will recollect, +were twins, and you know how subtle are the links which bind two +souls which are so closely allied. It was a wild night. The wind +was howling outside, and the rain was beating and splashing +against the windows. Suddenly, amid all the hubbub of the gale, +there burst forth the wild scream of a terrified woman. I knew +that it was my sister's voice. I sprang from my bed, wrapped a +shawl round me, and rushed into the corridor. As I opened my door +I seemed to hear a low whistle, such as my sister described, and +a few moments later a clanging sound, as if a mass of metal had +fallen. As I ran down the passage, my sister's door was unlocked, +and revolved slowly upon its hinges. I stared at it +horror-stricken, not knowing what was about to issue from it. By +the light of the corridor-lamp I saw my sister appear at the +opening, her face blanched with terror, her hands groping for +help, her whole figure swaying to and fro like that of a +drunkard. I ran to her and threw my arms round her, but at that +moment her knees seemed to give way and she fell to the ground. +She writhed as one who is in terrible pain, and her limbs were +dreadfully convulsed. At first I thought that she had not +recognised me, but as I bent over her she suddenly shrieked out +in a voice which I shall never forget, 'Oh, my God! Helen! It was +the band! The speckled band!' There was something else which she +would fain have said, and she stabbed with her finger into the +air in the direction of the doctor's room, but a fresh convulsion +seized her and choked her words. I rushed out, calling loudly for +my stepfather, and I met him hastening from his room in his +dressing-gown. When he reached my sister's side she was +unconscious, and though he poured brandy down her throat and sent +for medical aid from the village, all efforts were in vain, for +she slowly sank and died without having recovered her +consciousness. Such was the dreadful end of my beloved sister." + +"One moment," said Holmes, "are you sure about this whistle and +metallic sound? Could you swear to it?" + +"That was what the county coroner asked me at the inquiry. It is +my strong impression that I heard it, and yet, among the crash of +the gale and the creaking of an old house, I may possibly have +been deceived." + +"Was your sister dressed?" + +"No, she was in her night-dress. In her right hand was found the +charred stump of a match, and in her left a match-box." + +"Showing that she had struck a light and looked about her when +the alarm took place. That is important. And what conclusions did +the coroner come to?" + +"He investigated the case with great care, for Dr. Roylott's +conduct had long been notorious in the county, but he was unable +to find any satisfactory cause of death. My evidence showed that +the door had been fastened upon the inner side, and the windows +were blocked by old-fashioned shutters with broad iron bars, +which were secured every night. The walls were carefully sounded, +and were shown to be quite solid all round, and the flooring was +also thoroughly examined, with the same result. The chimney is +wide, but is barred up by four large staples. It is certain, +therefore, that my sister was quite alone when she met her end. +Besides, there were no marks of any violence upon her." + +"How about poison?" + +"The doctors examined her for it, but without success." + +"What do you think that this unfortunate lady died of, then?" + +"It is my belief that she died of pure fear and nervous shock, +though what it was that frightened her I cannot imagine." + +"Were there gipsies in the plantation at the time?" + +"Yes, there are nearly always some there." + +"Ah, and what did you gather from this allusion to a band--a +speckled band?" + +"Sometimes I have thought that it was merely the wild talk of +delirium, sometimes that it may have referred to some band of +people, perhaps to these very gipsies in the plantation. I do not +know whether the spotted handkerchiefs which so many of them wear +over their heads might have suggested the strange adjective which +she used." + +Holmes shook his head like a man who is far from being satisfied. + +"These are very deep waters," said he; "pray go on with your +narrative." + +"Two years have passed since then, and my life has been until +lately lonelier than ever. A month ago, however, a dear friend, +whom I have known for many years, has done me the honour to ask +my hand in marriage. His name is Armitage--Percy Armitage--the +second son of Mr. Armitage, of Crane Water, near Reading. My +stepfather has offered no opposition to the match, and we are to +be married in the course of the spring. Two days ago some repairs +were started in the west wing of the building, and my bedroom +wall has been pierced, so that I have had to move into the +chamber in which my sister died, and to sleep in the very bed in +which she slept. Imagine, then, my thrill of terror when last +night, as I lay awake, thinking over her terrible fate, I +suddenly heard in the silence of the night the low whistle which +had been the herald of her own death. I sprang up and lit the +lamp, but nothing was to be seen in the room. I was too shaken to +go to bed again, however, so I dressed, and as soon as it was +daylight I slipped down, got a dog-cart at the Crown Inn, which +is opposite, and drove to Leatherhead, from whence I have come on +this morning with the one object of seeing you and asking your +advice." + +"You have done wisely," said my friend. "But have you told me +all?" + +"Yes, all." + +"Miss Roylott, you have not. You are screening your stepfather." + +"Why, what do you mean?" + +For answer Holmes pushed back the frill of black lace which +fringed the hand that lay upon our visitor's knee. Five little +livid spots, the marks of four fingers and a thumb, were printed +upon the white wrist. + +"You have been cruelly used," said Holmes. + +The lady coloured deeply and covered over her injured wrist. "He +is a hard man," she said, "and perhaps he hardly knows his own +strength." + +There was a long silence, during which Holmes leaned his chin +upon his hands and stared into the crackling fire. + +"This is a very deep business," he said at last. "There are a +thousand details which I should desire to know before I decide +upon our course of action. Yet we have not a moment to lose. If +we were to come to Stoke Moran to-day, would it be possible for +us to see over these rooms without the knowledge of your +stepfather?" + +"As it happens, he spoke of coming into town to-day upon some +most important business. It is probable that he will be away all +day, and that there would be nothing to disturb you. We have a +housekeeper now, but she is old and foolish, and I could easily +get her out of the way." + +"Excellent. You are not averse to this trip, Watson?" + +"By no means." + +"Then we shall both come. What are you going to do yourself?" + +"I have one or two things which I would wish to do now that I am +in town. But I shall return by the twelve o'clock train, so as to +be there in time for your coming." + +"And you may expect us early in the afternoon. I have myself some +small business matters to attend to. Will you not wait and +breakfast?" + +"No, I must go. My heart is lightened already since I have +confided my trouble to you. I shall look forward to seeing you +again this afternoon." She dropped her thick black veil over her +face and glided from the room. + +"And what do you think of it all, Watson?" asked Sherlock Holmes, +leaning back in his chair. + +"It seems to me to be a most dark and sinister business." + +"Dark enough and sinister enough." + +"Yet if the lady is correct in saying that the flooring and walls +are sound, and that the door, window, and chimney are impassable, +then her sister must have been undoubtedly alone when she met her +mysterious end." + +"What becomes, then, of these nocturnal whistles, and what of the +very peculiar words of the dying woman?" + +"I cannot think." + +"When you combine the ideas of whistles at night, the presence of +a band of gipsies who are on intimate terms with this old doctor, +the fact that we have every reason to believe that the doctor has +an interest in preventing his stepdaughter's marriage, the dying +allusion to a band, and, finally, the fact that Miss Helen Stoner +heard a metallic clang, which might have been caused by one of +those metal bars that secured the shutters falling back into its +place, I think that there is good ground to think that the +mystery may be cleared along those lines." + +"But what, then, did the gipsies do?" + +"I cannot imagine." + +"I see many objections to any such theory." + +"And so do I. It is precisely for that reason that we are going +to Stoke Moran this day. I want to see whether the objections are +fatal, or if they may be explained away. But what in the name of +the devil!" + +The ejaculation had been drawn from my companion by the fact that +our door had been suddenly dashed open, and that a huge man had +framed himself in the aperture. His costume was a peculiar +mixture of the professional and of the agricultural, having a +black top-hat, a long frock-coat, and a pair of high gaiters, +with a hunting-crop swinging in his hand. So tall was he that his +hat actually brushed the cross bar of the doorway, and his +breadth seemed to span it across from side to side. A large face, +seared with a thousand wrinkles, burned yellow with the sun, and +marked with every evil passion, was turned from one to the other +of us, while his deep-set, bile-shot eyes, and his high, thin, +fleshless nose, gave him somewhat the resemblance to a fierce old +bird of prey. + +"Which of you is Holmes?" asked this apparition. + +"My name, sir; but you have the advantage of me," said my +companion quietly. + +"I am Dr. Grimesby Roylott, of Stoke Moran." + +"Indeed, Doctor," said Holmes blandly. "Pray take a seat." + +"I will do nothing of the kind. My stepdaughter has been here. I +have traced her. What has she been saying to you?" + +"It is a little cold for the time of the year," said Holmes. + +"What has she been saying to you?" screamed the old man +furiously. + +"But I have heard that the crocuses promise well," continued my +companion imperturbably. + +"Ha! You put me off, do you?" said our new visitor, taking a step +forward and shaking his hunting-crop. "I know you, you scoundrel! +I have heard of you before. You are Holmes, the meddler." + +My friend smiled. + +"Holmes, the busybody!" + +His smile broadened. + +"Holmes, the Scotland Yard Jack-in-office!" + +Holmes chuckled heartily. "Your conversation is most +entertaining," said he. "When you go out close the door, for +there is a decided draught." + +"I will go when I have said my say. Don't you dare to meddle with +my affairs. I know that Miss Stoner has been here. I traced her! +I am a dangerous man to fall foul of! See here." He stepped +swiftly forward, seized the poker, and bent it into a curve with +his huge brown hands. + +"See that you keep yourself out of my grip," he snarled, and +hurling the twisted poker into the fireplace he strode out of the +room. + +"He seems a very amiable person," said Holmes, laughing. "I am +not quite so bulky, but if he had remained I might have shown him +that my grip was not much more feeble than his own." As he spoke +he picked up the steel poker and, with a sudden effort, +straightened it out again. + +"Fancy his having the insolence to confound me with the official +detective force! This incident gives zest to our investigation, +however, and I only trust that our little friend will not suffer +from her imprudence in allowing this brute to trace her. And now, +Watson, we shall order breakfast, and afterwards I shall walk +down to Doctors' Commons, where I hope to get some data which may +help us in this matter." + + +It was nearly one o'clock when Sherlock Holmes returned from his +excursion. He held in his hand a sheet of blue paper, scrawled +over with notes and figures. + +"I have seen the will of the deceased wife," said he. "To +determine its exact meaning I have been obliged to work out the +present prices of the investments with which it is concerned. The +total income, which at the time of the wife's death was little +short of 1100 pounds, is now, through the fall in agricultural +prices, not more than 750 pounds. Each daughter can claim an +income of 250 pounds, in case of marriage. It is evident, +therefore, that if both girls had married, this beauty would have +had a mere pittance, while even one of them would cripple him to +a very serious extent. My morning's work has not been wasted, +since it has proved that he has the very strongest motives for +standing in the way of anything of the sort. And now, Watson, +this is too serious for dawdling, especially as the old man is +aware that we are interesting ourselves in his affairs; so if you +are ready, we shall call a cab and drive to Waterloo. I should be +very much obliged if you would slip your revolver into your +pocket. An Eley's No. 2 is an excellent argument with gentlemen +who can twist steel pokers into knots. That and a tooth-brush +are, I think, all that we need." + +At Waterloo we were fortunate in catching a train for +Leatherhead, where we hired a trap at the station inn and drove +for four or five miles through the lovely Surrey lanes. It was a +perfect day, with a bright sun and a few fleecy clouds in the +heavens. The trees and wayside hedges were just throwing out +their first green shoots, and the air was full of the pleasant +smell of the moist earth. To me at least there was a strange +contrast between the sweet promise of the spring and this +sinister quest upon which we were engaged. My companion sat in +the front of the trap, his arms folded, his hat pulled down over +his eyes, and his chin sunk upon his breast, buried in the +deepest thought. Suddenly, however, he started, tapped me on the +shoulder, and pointed over the meadows. + +"Look there!" said he. + +A heavily timbered park stretched up in a gentle slope, +thickening into a grove at the highest point. From amid the +branches there jutted out the grey gables and high roof-tree of a +very old mansion. + +"Stoke Moran?" said he. + +"Yes, sir, that be the house of Dr. Grimesby Roylott," remarked +the driver. + +"There is some building going on there," said Holmes; "that is +where we are going." + +"There's the village," said the driver, pointing to a cluster of +roofs some distance to the left; "but if you want to get to the +house, you'll find it shorter to get over this stile, and so by +the foot-path over the fields. There it is, where the lady is +walking." + +"And the lady, I fancy, is Miss Stoner," observed Holmes, shading +his eyes. "Yes, I think we had better do as you suggest." + +We got off, paid our fare, and the trap rattled back on its way +to Leatherhead. + +"I thought it as well," said Holmes as we climbed the stile, +"that this fellow should think we had come here as architects, or +on some definite business. It may stop his gossip. +Good-afternoon, Miss Stoner. You see that we have been as good as +our word." + +Our client of the morning had hurried forward to meet us with a +face which spoke her joy. "I have been waiting so eagerly for +you," she cried, shaking hands with us warmly. "All has turned +out splendidly. Dr. Roylott has gone to town, and it is unlikely +that he will be back before evening." + +"We have had the pleasure of making the doctor's acquaintance," +said Holmes, and in a few words he sketched out what had +occurred. Miss Stoner turned white to the lips as she listened. + +"Good heavens!" she cried, "he has followed me, then." + +"So it appears." + +"He is so cunning that I never know when I am safe from him. What +will he say when he returns?" + +"He must guard himself, for he may find that there is someone +more cunning than himself upon his track. You must lock yourself +up from him to-night. If he is violent, we shall take you away to +your aunt's at Harrow. Now, we must make the best use of our +time, so kindly take us at once to the rooms which we are to +examine." + +The building was of grey, lichen-blotched stone, with a high +central portion and two curving wings, like the claws of a crab, +thrown out on each side. In one of these wings the windows were +broken and blocked with wooden boards, while the roof was partly +caved in, a picture of ruin. The central portion was in little +better repair, but the right-hand block was comparatively modern, +and the blinds in the windows, with the blue smoke curling up +from the chimneys, showed that this was where the family resided. +Some scaffolding had been erected against the end wall, and the +stone-work had been broken into, but there were no signs of any +workmen at the moment of our visit. Holmes walked slowly up and +down the ill-trimmed lawn and examined with deep attention the +outsides of the windows. + +"This, I take it, belongs to the room in which you used to sleep, +the centre one to your sister's, and the one next to the main +building to Dr. Roylott's chamber?" + +"Exactly so. But I am now sleeping in the middle one." + +"Pending the alterations, as I understand. By the way, there does +not seem to be any very pressing need for repairs at that end +wall." + +"There were none. I believe that it was an excuse to move me from +my room." + +"Ah! that is suggestive. Now, on the other side of this narrow +wing runs the corridor from which these three rooms open. There +are windows in it, of course?" + +"Yes, but very small ones. Too narrow for anyone to pass +through." + +"As you both locked your doors at night, your rooms were +unapproachable from that side. Now, would you have the kindness +to go into your room and bar your shutters?" + +Miss Stoner did so, and Holmes, after a careful examination +through the open window, endeavoured in every way to force the +shutter open, but without success. There was no slit through +which a knife could be passed to raise the bar. Then with his +lens he tested the hinges, but they were of solid iron, built +firmly into the massive masonry. "Hum!" said he, scratching his +chin in some perplexity, "my theory certainly presents some +difficulties. No one could pass these shutters if they were +bolted. Well, we shall see if the inside throws any light upon +the matter." + +A small side door led into the whitewashed corridor from which +the three bedrooms opened. Holmes refused to examine the third +chamber, so we passed at once to the second, that in which Miss +Stoner was now sleeping, and in which her sister had met with her +fate. It was a homely little room, with a low ceiling and a +gaping fireplace, after the fashion of old country-houses. A +brown chest of drawers stood in one corner, a narrow +white-counterpaned bed in another, and a dressing-table on the +left-hand side of the window. These articles, with two small +wicker-work chairs, made up all the furniture in the room save +for a square of Wilton carpet in the centre. The boards round and +the panelling of the walls were of brown, worm-eaten oak, so old +and discoloured that it may have dated from the original building +of the house. Holmes drew one of the chairs into a corner and sat +silent, while his eyes travelled round and round and up and down, +taking in every detail of the apartment. + +"Where does that bell communicate with?" he asked at last +pointing to a thick bell-rope which hung down beside the bed, the +tassel actually lying upon the pillow. + +"It goes to the housekeeper's room." + +"It looks newer than the other things?" + +"Yes, it was only put there a couple of years ago." + +"Your sister asked for it, I suppose?" + +"No, I never heard of her using it. We used always to get what we +wanted for ourselves." + +"Indeed, it seemed unnecessary to put so nice a bell-pull there. +You will excuse me for a few minutes while I satisfy myself as to +this floor." He threw himself down upon his face with his lens in +his hand and crawled swiftly backward and forward, examining +minutely the cracks between the boards. Then he did the same with +the wood-work with which the chamber was panelled. Finally he +walked over to the bed and spent some time in staring at it and +in running his eye up and down the wall. Finally he took the +bell-rope in his hand and gave it a brisk tug. + +"Why, it's a dummy," said he. + +"Won't it ring?" + +"No, it is not even attached to a wire. This is very interesting. +You can see now that it is fastened to a hook just above where +the little opening for the ventilator is." + +"How very absurd! I never noticed that before." + +"Very strange!" muttered Holmes, pulling at the rope. "There are +one or two very singular points about this room. For example, +what a fool a builder must be to open a ventilator into another +room, when, with the same trouble, he might have communicated +with the outside air!" + +"That is also quite modern," said the lady. + +"Done about the same time as the bell-rope?" remarked Holmes. + +"Yes, there were several little changes carried out about that +time." + +"They seem to have been of a most interesting character--dummy +bell-ropes, and ventilators which do not ventilate. With your +permission, Miss Stoner, we shall now carry our researches into +the inner apartment." + +Dr. Grimesby Roylott's chamber was larger than that of his +step-daughter, but was as plainly furnished. A camp-bed, a small +wooden shelf full of books, mostly of a technical character, an +armchair beside the bed, a plain wooden chair against the wall, a +round table, and a large iron safe were the principal things +which met the eye. Holmes walked slowly round and examined each +and all of them with the keenest interest. + +"What's in here?" he asked, tapping the safe. + +"My stepfather's business papers." + +"Oh! you have seen inside, then?" + +"Only once, some years ago. I remember that it was full of +papers." + +"There isn't a cat in it, for example?" + +"No. What a strange idea!" + +"Well, look at this!" He took up a small saucer of milk which +stood on the top of it. + +"No; we don't keep a cat. But there is a cheetah and a baboon." + +"Ah, yes, of course! Well, a cheetah is just a big cat, and yet a +saucer of milk does not go very far in satisfying its wants, I +daresay. There is one point which I should wish to determine." He +squatted down in front of the wooden chair and examined the seat +of it with the greatest attention. + +"Thank you. That is quite settled," said he, rising and putting +his lens in his pocket. "Hullo! Here is something interesting!" + +The object which had caught his eye was a small dog lash hung on +one corner of the bed. The lash, however, was curled upon itself +and tied so as to make a loop of whipcord. + +"What do you make of that, Watson?" + +"It's a common enough lash. But I don't know why it should be +tied." + +"That is not quite so common, is it? Ah, me! it's a wicked world, +and when a clever man turns his brains to crime it is the worst +of all. I think that I have seen enough now, Miss Stoner, and +with your permission we shall walk out upon the lawn." + +I had never seen my friend's face so grim or his brow so dark as +it was when we turned from the scene of this investigation. We +had walked several times up and down the lawn, neither Miss +Stoner nor myself liking to break in upon his thoughts before he +roused himself from his reverie. + +"It is very essential, Miss Stoner," said he, "that you should +absolutely follow my advice in every respect." + +"I shall most certainly do so." + +"The matter is too serious for any hesitation. Your life may +depend upon your compliance." + +"I assure you that I am in your hands." + +"In the first place, both my friend and I must spend the night in +your room." + +Both Miss Stoner and I gazed at him in astonishment. + +"Yes, it must be so. Let me explain. I believe that that is the +village inn over there?" + +"Yes, that is the Crown." + +"Very good. Your windows would be visible from there?" + +"Certainly." + +"You must confine yourself to your room, on pretence of a +headache, when your stepfather comes back. Then when you hear him +retire for the night, you must open the shutters of your window, +undo the hasp, put your lamp there as a signal to us, and then +withdraw quietly with everything which you are likely to want +into the room which you used to occupy. I have no doubt that, in +spite of the repairs, you could manage there for one night." + +"Oh, yes, easily." + +"The rest you will leave in our hands." + +"But what will you do?" + +"We shall spend the night in your room, and we shall investigate +the cause of this noise which has disturbed you." + +"I believe, Mr. Holmes, that you have already made up your mind," +said Miss Stoner, laying her hand upon my companion's sleeve. + +"Perhaps I have." + +"Then, for pity's sake, tell me what was the cause of my sister's +death." + +"I should prefer to have clearer proofs before I speak." + +"You can at least tell me whether my own thought is correct, and +if she died from some sudden fright." + +"No, I do not think so. I think that there was probably some more +tangible cause. And now, Miss Stoner, we must leave you for if +Dr. Roylott returned and saw us our journey would be in vain. +Good-bye, and be brave, for if you will do what I have told you, +you may rest assured that we shall soon drive away the dangers +that threaten you." + +Sherlock Holmes and I had no difficulty in engaging a bedroom and +sitting-room at the Crown Inn. They were on the upper floor, and +from our window we could command a view of the avenue gate, and +of the inhabited wing of Stoke Moran Manor House. At dusk we saw +Dr. Grimesby Roylott drive past, his huge form looming up beside +the little figure of the lad who drove him. The boy had some +slight difficulty in undoing the heavy iron gates, and we heard +the hoarse roar of the doctor's voice and saw the fury with which +he shook his clinched fists at him. The trap drove on, and a few +minutes later we saw a sudden light spring up among the trees as +the lamp was lit in one of the sitting-rooms. + +"Do you know, Watson," said Holmes as we sat together in the +gathering darkness, "I have really some scruples as to taking you +to-night. There is a distinct element of danger." + +"Can I be of assistance?" + +"Your presence might be invaluable." + +"Then I shall certainly come." + +"It is very kind of you." + +"You speak of danger. You have evidently seen more in these rooms +than was visible to me." + +"No, but I fancy that I may have deduced a little more. I imagine +that you saw all that I did." + +"I saw nothing remarkable save the bell-rope, and what purpose +that could answer I confess is more than I can imagine." + +"You saw the ventilator, too?" + +"Yes, but I do not think that it is such a very unusual thing to +have a small opening between two rooms. It was so small that a +rat could hardly pass through." + +"I knew that we should find a ventilator before ever we came to +Stoke Moran." + +"My dear Holmes!" + +"Oh, yes, I did. You remember in her statement she said that her +sister could smell Dr. Roylott's cigar. Now, of course that +suggested at once that there must be a communication between the +two rooms. It could only be a small one, or it would have been +remarked upon at the coroner's inquiry. I deduced a ventilator." + +"But what harm can there be in that?" + +"Well, there is at least a curious coincidence of dates. A +ventilator is made, a cord is hung, and a lady who sleeps in the +bed dies. Does not that strike you?" + +"I cannot as yet see any connection." + +"Did you observe anything very peculiar about that bed?" + +"No." + +"It was clamped to the floor. Did you ever see a bed fastened +like that before?" + +"I cannot say that I have." + +"The lady could not move her bed. It must always be in the same +relative position to the ventilator and to the rope--or so we may +call it, since it was clearly never meant for a bell-pull." + +"Holmes," I cried, "I seem to see dimly what you are hinting at. +We are only just in time to prevent some subtle and horrible +crime." + +"Subtle enough and horrible enough. When a doctor does go wrong +he is the first of criminals. He has nerve and he has knowledge. +Palmer and Pritchard were among the heads of their profession. +This man strikes even deeper, but I think, Watson, that we shall +be able to strike deeper still. But we shall have horrors enough +before the night is over; for goodness' sake let us have a quiet +pipe and turn our minds for a few hours to something more +cheerful." + + +About nine o'clock the light among the trees was extinguished, +and all was dark in the direction of the Manor House. Two hours +passed slowly away, and then, suddenly, just at the stroke of +eleven, a single bright light shone out right in front of us. + +"That is our signal," said Holmes, springing to his feet; "it +comes from the middle window." + +As we passed out he exchanged a few words with the landlord, +explaining that we were going on a late visit to an acquaintance, +and that it was possible that we might spend the night there. A +moment later we were out on the dark road, a chill wind blowing +in our faces, and one yellow light twinkling in front of us +through the gloom to guide us on our sombre errand. + +There was little difficulty in entering the grounds, for +unrepaired breaches gaped in the old park wall. Making our way +among the trees, we reached the lawn, crossed it, and were about +to enter through the window when out from a clump of laurel +bushes there darted what seemed to be a hideous and distorted +child, who threw itself upon the grass with writhing limbs and +then ran swiftly across the lawn into the darkness. + +"My God!" I whispered; "did you see it?" + +Holmes was for the moment as startled as I. His hand closed like +a vice upon my wrist in his agitation. Then he broke into a low +laugh and put his lips to my ear. + +"It is a nice household," he murmured. "That is the baboon." + +I had forgotten the strange pets which the doctor affected. There +was a cheetah, too; perhaps we might find it upon our shoulders +at any moment. I confess that I felt easier in my mind when, +after following Holmes' example and slipping off my shoes, I +found myself inside the bedroom. My companion noiselessly closed +the shutters, moved the lamp onto the table, and cast his eyes +round the room. All was as we had seen it in the daytime. Then +creeping up to me and making a trumpet of his hand, he whispered +into my ear again so gently that it was all that I could do to +distinguish the words: + +"The least sound would be fatal to our plans." + +I nodded to show that I had heard. + +"We must sit without light. He would see it through the +ventilator." + +I nodded again. + +"Do not go asleep; your very life may depend upon it. Have your +pistol ready in case we should need it. I will sit on the side of +the bed, and you in that chair." + +I took out my revolver and laid it on the corner of the table. + +Holmes had brought up a long thin cane, and this he placed upon +the bed beside him. By it he laid the box of matches and the +stump of a candle. Then he turned down the lamp, and we were left +in darkness. + +How shall I ever forget that dreadful vigil? I could not hear a +sound, not even the drawing of a breath, and yet I knew that my +companion sat open-eyed, within a few feet of me, in the same +state of nervous tension in which I was myself. The shutters cut +off the least ray of light, and we waited in absolute darkness. + +From outside came the occasional cry of a night-bird, and once at +our very window a long drawn catlike whine, which told us that +the cheetah was indeed at liberty. Far away we could hear the +deep tones of the parish clock, which boomed out every quarter of +an hour. How long they seemed, those quarters! Twelve struck, and +one and two and three, and still we sat waiting silently for +whatever might befall. + +Suddenly there was the momentary gleam of a light up in the +direction of the ventilator, which vanished immediately, but was +succeeded by a strong smell of burning oil and heated metal. +Someone in the next room had lit a dark-lantern. I heard a gentle +sound of movement, and then all was silent once more, though the +smell grew stronger. For half an hour I sat with straining ears. +Then suddenly another sound became audible--a very gentle, +soothing sound, like that of a small jet of steam escaping +continually from a kettle. The instant that we heard it, Holmes +sprang from the bed, struck a match, and lashed furiously with +his cane at the bell-pull. + +"You see it, Watson?" he yelled. "You see it?" + +But I saw nothing. At the moment when Holmes struck the light I +heard a low, clear whistle, but the sudden glare flashing into my +weary eyes made it impossible for me to tell what it was at which +my friend lashed so savagely. I could, however, see that his face +was deadly pale and filled with horror and loathing. He had +ceased to strike and was gazing up at the ventilator when +suddenly there broke from the silence of the night the most +horrible cry to which I have ever listened. It swelled up louder +and louder, a hoarse yell of pain and fear and anger all mingled +in the one dreadful shriek. They say that away down in the +village, and even in the distant parsonage, that cry raised the +sleepers from their beds. It struck cold to our hearts, and I +stood gazing at Holmes, and he at me, until the last echoes of it +had died away into the silence from which it rose. + +"What can it mean?" I gasped. + +"It means that it is all over," Holmes answered. "And perhaps, +after all, it is for the best. Take your pistol, and we will +enter Dr. Roylott's room." + +With a grave face he lit the lamp and led the way down the +corridor. Twice he struck at the chamber door without any reply +from within. Then he turned the handle and entered, I at his +heels, with the cocked pistol in my hand. + +It was a singular sight which met our eyes. On the table stood a +dark-lantern with the shutter half open, throwing a brilliant +beam of light upon the iron safe, the door of which was ajar. +Beside this table, on the wooden chair, sat Dr. Grimesby Roylott +clad in a long grey dressing-gown, his bare ankles protruding +beneath, and his feet thrust into red heelless Turkish slippers. +Across his lap lay the short stock with the long lash which we +had noticed during the day. His chin was cocked upward and his +eyes were fixed in a dreadful, rigid stare at the corner of the +ceiling. Round his brow he had a peculiar yellow band, with +brownish speckles, which seemed to be bound tightly round his +head. As we entered he made neither sound nor motion. + +"The band! the speckled band!" whispered Holmes. + +I took a step forward. In an instant his strange headgear began +to move, and there reared itself from among his hair the squat +diamond-shaped head and puffed neck of a loathsome serpent. + +"It is a swamp adder!" cried Holmes; "the deadliest snake in +India. He has died within ten seconds of being bitten. Violence +does, in truth, recoil upon the violent, and the schemer falls +into the pit which he digs for another. Let us thrust this +creature back into its den, and we can then remove Miss Stoner to +some place of shelter and let the county police know what has +happened." + +As he spoke he drew the dog-whip swiftly from the dead man's lap, +and throwing the noose round the reptile's neck he drew it from +its horrid perch and, carrying it at arm's length, threw it into +the iron safe, which he closed upon it. + +Such are the true facts of the death of Dr. Grimesby Roylott, of +Stoke Moran. It is not necessary that I should prolong a +narrative which has already run to too great a length by telling +how we broke the sad news to the terrified girl, how we conveyed +her by the morning train to the care of her good aunt at Harrow, +of how the slow process of official inquiry came to the +conclusion that the doctor met his fate while indiscreetly +playing with a dangerous pet. The little which I had yet to learn +of the case was told me by Sherlock Holmes as we travelled back +next day. + +"I had," said he, "come to an entirely erroneous conclusion which +shows, my dear Watson, how dangerous it always is to reason from +insufficient data. The presence of the gipsies, and the use of +the word 'band,' which was used by the poor girl, no doubt, to +explain the appearance which she had caught a hurried glimpse of +by the light of her match, were sufficient to put me upon an +entirely wrong scent. I can only claim the merit that I instantly +reconsidered my position when, however, it became clear to me +that whatever danger threatened an occupant of the room could not +come either from the window or the door. My attention was +speedily drawn, as I have already remarked to you, to this +ventilator, and to the bell-rope which hung down to the bed. The +discovery that this was a dummy, and that the bed was clamped to +the floor, instantly gave rise to the suspicion that the rope was +there as a bridge for something passing through the hole and +coming to the bed. The idea of a snake instantly occurred to me, +and when I coupled it with my knowledge that the doctor was +furnished with a supply of creatures from India, I felt that I +was probably on the right track. The idea of using a form of +poison which could not possibly be discovered by any chemical +test was just such a one as would occur to a clever and ruthless +man who had had an Eastern training. The rapidity with which such +a poison would take effect would also, from his point of view, be +an advantage. It would be a sharp-eyed coroner, indeed, who could +distinguish the two little dark punctures which would show where +the poison fangs had done their work. Then I thought of the +whistle. Of course he must recall the snake before the morning +light revealed it to the victim. He had trained it, probably by +the use of the milk which we saw, to return to him when summoned. +He would put it through this ventilator at the hour that he +thought best, with the certainty that it would crawl down the +rope and land on the bed. It might or might not bite the +occupant, perhaps she might escape every night for a week, but +sooner or later she must fall a victim. + +"I had come to these conclusions before ever I had entered his +room. An inspection of his chair showed me that he had been in +the habit of standing on it, which of course would be necessary +in order that he should reach the ventilator. The sight of the +safe, the saucer of milk, and the loop of whipcord were enough to +finally dispel any doubts which may have remained. The metallic +clang heard by Miss Stoner was obviously caused by her stepfather +hastily closing the door of his safe upon its terrible occupant. +Having once made up my mind, you know the steps which I took in +order to put the matter to the proof. I heard the creature hiss +as I have no doubt that you did also, and I instantly lit the +light and attacked it." + +"With the result of driving it through the ventilator." + +"And also with the result of causing it to turn upon its master +at the other side. Some of the blows of my cane came home and +roused its snakish temper, so that it flew upon the first person +it saw. In this way I am no doubt indirectly responsible for Dr. +Grimesby Roylott's death, and I cannot say that it is likely to +weigh very heavily upon my conscience." + + + +IX. THE ADVENTURE OF THE ENGINEER'S THUMB + +Of all the problems which have been submitted to my friend, Mr. +Sherlock Holmes, for solution during the years of our intimacy, +there were only two which I was the means of introducing to his +notice--that of Mr. Hatherley's thumb, and that of Colonel +Warburton's madness. Of these the latter may have afforded a +finer field for an acute and original observer, but the other was +so strange in its inception and so dramatic in its details that +it may be the more worthy of being placed upon record, even if it +gave my friend fewer openings for those deductive methods of +reasoning by which he achieved such remarkable results. The story +has, I believe, been told more than once in the newspapers, but, +like all such narratives, its effect is much less striking when +set forth en bloc in a single half-column of print than when the +facts slowly evolve before your own eyes, and the mystery clears +gradually away as each new discovery furnishes a step which leads +on to the complete truth. At the time the circumstances made a +deep impression upon me, and the lapse of two years has hardly +served to weaken the effect. + +It was in the summer of '89, not long after my marriage, that the +events occurred which I am now about to summarise. I had returned +to civil practice and had finally abandoned Holmes in his Baker +Street rooms, although I continually visited him and occasionally +even persuaded him to forgo his Bohemian habits so far as to come +and visit us. My practice had steadily increased, and as I +happened to live at no very great distance from Paddington +Station, I got a few patients from among the officials. One of +these, whom I had cured of a painful and lingering disease, was +never weary of advertising my virtues and of endeavouring to send +me on every sufferer over whom he might have any influence. + +One morning, at a little before seven o'clock, I was awakened by +the maid tapping at the door to announce that two men had come +from Paddington and were waiting in the consulting-room. I +dressed hurriedly, for I knew by experience that railway cases +were seldom trivial, and hastened downstairs. As I descended, my +old ally, the guard, came out of the room and closed the door +tightly behind him. + +"I've got him here," he whispered, jerking his thumb over his +shoulder; "he's all right." + +"What is it, then?" I asked, for his manner suggested that it was +some strange creature which he had caged up in my room. + +"It's a new patient," he whispered. "I thought I'd bring him +round myself; then he couldn't slip away. There he is, all safe +and sound. I must go now, Doctor; I have my dooties, just the +same as you." And off he went, this trusty tout, without even +giving me time to thank him. + +I entered my consulting-room and found a gentleman seated by the +table. He was quietly dressed in a suit of heather tweed with a +soft cloth cap which he had laid down upon my books. Round one of +his hands he had a handkerchief wrapped, which was mottled all +over with bloodstains. He was young, not more than +five-and-twenty, I should say, with a strong, masculine face; but +he was exceedingly pale and gave me the impression of a man who +was suffering from some strong agitation, which it took all his +strength of mind to control. + +"I am sorry to knock you up so early, Doctor," said he, "but I +have had a very serious accident during the night. I came in by +train this morning, and on inquiring at Paddington as to where I +might find a doctor, a worthy fellow very kindly escorted me +here. I gave the maid a card, but I see that she has left it upon +the side-table." + +I took it up and glanced at it. "Mr. Victor Hatherley, hydraulic +engineer, 16A, Victoria Street (3rd floor)." That was the name, +style, and abode of my morning visitor. "I regret that I have +kept you waiting," said I, sitting down in my library-chair. "You +are fresh from a night journey, I understand, which is in itself +a monotonous occupation." + +"Oh, my night could not be called monotonous," said he, and +laughed. He laughed very heartily, with a high, ringing note, +leaning back in his chair and shaking his sides. All my medical +instincts rose up against that laugh. + +"Stop it!" I cried; "pull yourself together!" and I poured out +some water from a caraffe. + +It was useless, however. He was off in one of those hysterical +outbursts which come upon a strong nature when some great crisis +is over and gone. Presently he came to himself once more, very +weary and pale-looking. + +"I have been making a fool of myself," he gasped. + +"Not at all. Drink this." I dashed some brandy into the water, +and the colour began to come back to his bloodless cheeks. + +"That's better!" said he. "And now, Doctor, perhaps you would +kindly attend to my thumb, or rather to the place where my thumb +used to be." + +He unwound the handkerchief and held out his hand. It gave even +my hardened nerves a shudder to look at it. There were four +protruding fingers and a horrid red, spongy surface where the +thumb should have been. It had been hacked or torn right out from +the roots. + +"Good heavens!" I cried, "this is a terrible injury. It must have +bled considerably." + +"Yes, it did. I fainted when it was done, and I think that I must +have been senseless for a long time. When I came to I found that +it was still bleeding, so I tied one end of my handkerchief very +tightly round the wrist and braced it up with a twig." + +"Excellent! You should have been a surgeon." + +"It is a question of hydraulics, you see, and came within my own +province." + +"This has been done," said I, examining the wound, "by a very +heavy and sharp instrument." + +"A thing like a cleaver," said he. + +"An accident, I presume?" + +"By no means." + +"What! a murderous attack?" + +"Very murderous indeed." + +"You horrify me." + +I sponged the wound, cleaned it, dressed it, and finally covered +it over with cotton wadding and carbolised bandages. He lay back +without wincing, though he bit his lip from time to time. + +"How is that?" I asked when I had finished. + +"Capital! Between your brandy and your bandage, I feel a new man. +I was very weak, but I have had a good deal to go through." + +"Perhaps you had better not speak of the matter. It is evidently +trying to your nerves." + +"Oh, no, not now. I shall have to tell my tale to the police; +but, between ourselves, if it were not for the convincing +evidence of this wound of mine, I should be surprised if they +believed my statement, for it is a very extraordinary one, and I +have not much in the way of proof with which to back it up; and, +even if they believe me, the clues which I can give them are so +vague that it is a question whether justice will be done." + +"Ha!" cried I, "if it is anything in the nature of a problem +which you desire to see solved, I should strongly recommend you +to come to my friend, Mr. Sherlock Holmes, before you go to the +official police." + +"Oh, I have heard of that fellow," answered my visitor, "and I +should be very glad if he would take the matter up, though of +course I must use the official police as well. Would you give me +an introduction to him?" + +"I'll do better. I'll take you round to him myself." + +"I should be immensely obliged to you." + +"We'll call a cab and go together. We shall just be in time to +have a little breakfast with him. Do you feel equal to it?" + +"Yes; I shall not feel easy until I have told my story." + +"Then my servant will call a cab, and I shall be with you in an +instant." I rushed upstairs, explained the matter shortly to my +wife, and in five minutes was inside a hansom, driving with my +new acquaintance to Baker Street. + +Sherlock Holmes was, as I expected, lounging about his +sitting-room in his dressing-gown, reading the agony column of The +Times and smoking his before-breakfast pipe, which was composed +of all the plugs and dottles left from his smokes of the day +before, all carefully dried and collected on the corner of the +mantelpiece. He received us in his quietly genial fashion, +ordered fresh rashers and eggs, and joined us in a hearty meal. +When it was concluded he settled our new acquaintance upon the +sofa, placed a pillow beneath his head, and laid a glass of +brandy and water within his reach. + +"It is easy to see that your experience has been no common one, +Mr. Hatherley," said he. "Pray, lie down there and make yourself +absolutely at home. Tell us what you can, but stop when you are +tired and keep up your strength with a little stimulant." + +"Thank you," said my patient, "but I have felt another man since +the doctor bandaged me, and I think that your breakfast has +completed the cure. I shall take up as little of your valuable +time as possible, so I shall start at once upon my peculiar +experiences." + +Holmes sat in his big armchair with the weary, heavy-lidded +expression which veiled his keen and eager nature, while I sat +opposite to him, and we listened in silence to the strange story +which our visitor detailed to us. + +"You must know," said he, "that I am an orphan and a bachelor, +residing alone in lodgings in London. By profession I am a +hydraulic engineer, and I have had considerable experience of my +work during the seven years that I was apprenticed to Venner & +Matheson, the well-known firm, of Greenwich. Two years ago, +having served my time, and having also come into a fair sum of +money through my poor father's death, I determined to start in +business for myself and took professional chambers in Victoria +Street. + +"I suppose that everyone finds his first independent start in +business a dreary experience. To me it has been exceptionally so. +During two years I have had three consultations and one small +job, and that is absolutely all that my profession has brought +me. My gross takings amount to 27 pounds 10s. Every day, from +nine in the morning until four in the afternoon, I waited in my +little den, until at last my heart began to sink, and I came to +believe that I should never have any practice at all. + +"Yesterday, however, just as I was thinking of leaving the +office, my clerk entered to say there was a gentleman waiting who +wished to see me upon business. He brought up a card, too, with +the name of 'Colonel Lysander Stark' engraved upon it. Close at +his heels came the colonel himself, a man rather over the middle +size, but of an exceeding thinness. I do not think that I have +ever seen so thin a man. His whole face sharpened away into nose +and chin, and the skin of his cheeks was drawn quite tense over +his outstanding bones. Yet this emaciation seemed to be his +natural habit, and due to no disease, for his eye was bright, his +step brisk, and his bearing assured. He was plainly but neatly +dressed, and his age, I should judge, would be nearer forty than +thirty. + +"'Mr. Hatherley?' said he, with something of a German accent. +'You have been recommended to me, Mr. Hatherley, as being a man +who is not only proficient in his profession but is also discreet +and capable of preserving a secret.' + +"I bowed, feeling as flattered as any young man would at such an +address. 'May I ask who it was who gave me so good a character?' + +"'Well, perhaps it is better that I should not tell you that just +at this moment. I have it from the same source that you are both +an orphan and a bachelor and are residing alone in London.' + +"'That is quite correct,' I answered; 'but you will excuse me if +I say that I cannot see how all this bears upon my professional +qualifications. I understand that it was on a professional matter +that you wished to speak to me?' + +"'Undoubtedly so. But you will find that all I say is really to +the point. I have a professional commission for you, but absolute +secrecy is quite essential--absolute secrecy, you understand, and +of course we may expect that more from a man who is alone than +from one who lives in the bosom of his family.' + +"'If I promise to keep a secret,' said I, 'you may absolutely +depend upon my doing so.' + +"He looked very hard at me as I spoke, and it seemed to me that I +had never seen so suspicious and questioning an eye. + +"'Do you promise, then?' said he at last. + +"'Yes, I promise.' + +"'Absolute and complete silence before, during, and after? No +reference to the matter at all, either in word or writing?' + +"'I have already given you my word.' + +"'Very good.' He suddenly sprang up, and darting like lightning +across the room he flung open the door. The passage outside was +empty. + +"'That's all right,' said he, coming back. 'I know that clerks are +sometimes curious as to their master's affairs. Now we can talk +in safety.' He drew up his chair very close to mine and began to +stare at me again with the same questioning and thoughtful look. + +"A feeling of repulsion, and of something akin to fear had begun +to rise within me at the strange antics of this fleshless man. +Even my dread of losing a client could not restrain me from +showing my impatience. + +"'I beg that you will state your business, sir,' said I; 'my time +is of value.' Heaven forgive me for that last sentence, but the +words came to my lips. + +"'How would fifty guineas for a night's work suit you?' he asked. + +"'Most admirably.' + +"'I say a night's work, but an hour's would be nearer the mark. I +simply want your opinion about a hydraulic stamping machine which +has got out of gear. If you show us what is wrong we shall soon +set it right ourselves. What do you think of such a commission as +that?' + +"'The work appears to be light and the pay munificent.' + +"'Precisely so. We shall want you to come to-night by the last +train.' + +"'Where to?' + +"'To Eyford, in Berkshire. It is a little place near the borders +of Oxfordshire, and within seven miles of Reading. There is a +train from Paddington which would bring you there at about +11:15.' + +"'Very good.' + +"'I shall come down in a carriage to meet you.' + +"'There is a drive, then?' + +"'Yes, our little place is quite out in the country. It is a good +seven miles from Eyford Station.' + +"'Then we can hardly get there before midnight. I suppose there +would be no chance of a train back. I should be compelled to stop +the night.' + +"'Yes, we could easily give you a shake-down.' + +"'That is very awkward. Could I not come at some more convenient +hour?' + +"'We have judged it best that you should come late. It is to +recompense you for any inconvenience that we are paying to you, a +young and unknown man, a fee which would buy an opinion from the +very heads of your profession. Still, of course, if you would +like to draw out of the business, there is plenty of time to do +so.' + +"I thought of the fifty guineas, and of how very useful they +would be to me. 'Not at all,' said I, 'I shall be very happy to +accommodate myself to your wishes. I should like, however, to +understand a little more clearly what it is that you wish me to +do.' + +"'Quite so. It is very natural that the pledge of secrecy which +we have exacted from you should have aroused your curiosity. I +have no wish to commit you to anything without your having it all +laid before you. I suppose that we are absolutely safe from +eavesdroppers?' + +"'Entirely.' + +"'Then the matter stands thus. You are probably aware that +fuller's-earth is a valuable product, and that it is only found +in one or two places in England?' + +"'I have heard so.' + +"'Some little time ago I bought a small place--a very small +place--within ten miles of Reading. I was fortunate enough to +discover that there was a deposit of fuller's-earth in one of my +fields. On examining it, however, I found that this deposit was a +comparatively small one, and that it formed a link between two +very much larger ones upon the right and left--both of them, +however, in the grounds of my neighbours. These good people were +absolutely ignorant that their land contained that which was +quite as valuable as a gold-mine. Naturally, it was to my +interest to buy their land before they discovered its true value, +but unfortunately I had no capital by which I could do this. I +took a few of my friends into the secret, however, and they +suggested that we should quietly and secretly work our own little +deposit and that in this way we should earn the money which would +enable us to buy the neighbouring fields. This we have now been +doing for some time, and in order to help us in our operations we +erected a hydraulic press. This press, as I have already +explained, has got out of order, and we wish your advice upon the +subject. We guard our secret very jealously, however, and if it +once became known that we had hydraulic engineers coming to our +little house, it would soon rouse inquiry, and then, if the facts +came out, it would be good-bye to any chance of getting these +fields and carrying out our plans. That is why I have made you +promise me that you will not tell a human being that you are +going to Eyford to-night. I hope that I make it all plain?' + +"'I quite follow you,' said I. 'The only point which I could not +quite understand was what use you could make of a hydraulic press +in excavating fuller's-earth, which, as I understand, is dug out +like gravel from a pit.' + +"'Ah!' said he carelessly, 'we have our own process. We compress +the earth into bricks, so as to remove them without revealing +what they are. But that is a mere detail. I have taken you fully +into my confidence now, Mr. Hatherley, and I have shown you how I +trust you.' He rose as he spoke. 'I shall expect you, then, at +Eyford at 11:15.' + +"'I shall certainly be there.' + +"'And not a word to a soul.' He looked at me with a last long, +questioning gaze, and then, pressing my hand in a cold, dank +grasp, he hurried from the room. + +"Well, when I came to think it all over in cool blood I was very +much astonished, as you may both think, at this sudden commission +which had been intrusted to me. On the one hand, of course, I was +glad, for the fee was at least tenfold what I should have asked +had I set a price upon my own services, and it was possible that +this order might lead to other ones. On the other hand, the face +and manner of my patron had made an unpleasant impression upon +me, and I could not think that his explanation of the +fuller's-earth was sufficient to explain the necessity for my +coming at midnight, and his extreme anxiety lest I should tell +anyone of my errand. However, I threw all fears to the winds, ate +a hearty supper, drove to Paddington, and started off, having +obeyed to the letter the injunction as to holding my tongue. + +"At Reading I had to change not only my carriage but my station. +However, I was in time for the last train to Eyford, and I +reached the little dim-lit station after eleven o'clock. I was the +only passenger who got out there, and there was no one upon the +platform save a single sleepy porter with a lantern. As I passed +out through the wicket gate, however, I found my acquaintance of +the morning waiting in the shadow upon the other side. Without a +word he grasped my arm and hurried me into a carriage, the door +of which was standing open. He drew up the windows on either +side, tapped on the wood-work, and away we went as fast as the +horse could go." + +"One horse?" interjected Holmes. + +"Yes, only one." + +"Did you observe the colour?" + +"Yes, I saw it by the side-lights when I was stepping into the +carriage. It was a chestnut." + +"Tired-looking or fresh?" + +"Oh, fresh and glossy." + +"Thank you. I am sorry to have interrupted you. Pray continue +your most interesting statement." + +"Away we went then, and we drove for at least an hour. Colonel +Lysander Stark had said that it was only seven miles, but I +should think, from the rate that we seemed to go, and from the +time that we took, that it must have been nearer twelve. He sat +at my side in silence all the time, and I was aware, more than +once when I glanced in his direction, that he was looking at me +with great intensity. The country roads seem to be not very good +in that part of the world, for we lurched and jolted terribly. I +tried to look out of the windows to see something of where we +were, but they were made of frosted glass, and I could make out +nothing save the occasional bright blur of a passing light. Now +and then I hazarded some remark to break the monotony of the +journey, but the colonel answered only in monosyllables, and the +conversation soon flagged. At last, however, the bumping of the +road was exchanged for the crisp smoothness of a gravel-drive, +and the carriage came to a stand. Colonel Lysander Stark sprang +out, and, as I followed after him, pulled me swiftly into a porch +which gaped in front of us. We stepped, as it were, right out of +the carriage and into the hall, so that I failed to catch the +most fleeting glance of the front of the house. The instant that +I had crossed the threshold the door slammed heavily behind us, +and I heard faintly the rattle of the wheels as the carriage +drove away. + +"It was pitch dark inside the house, and the colonel fumbled +about looking for matches and muttering under his breath. +Suddenly a door opened at the other end of the passage, and a +long, golden bar of light shot out in our direction. It grew +broader, and a woman appeared with a lamp in her hand, which she +held above her head, pushing her face forward and peering at us. +I could see that she was pretty, and from the gloss with which +the light shone upon her dark dress I knew that it was a rich +material. She spoke a few words in a foreign tongue in a tone as +though asking a question, and when my companion answered in a +gruff monosyllable she gave such a start that the lamp nearly +fell from her hand. Colonel Stark went up to her, whispered +something in her ear, and then, pushing her back into the room +from whence she had come, he walked towards me again with the +lamp in his hand. + +"'Perhaps you will have the kindness to wait in this room for a +few minutes,' said he, throwing open another door. It was a +quiet, little, plainly furnished room, with a round table in the +centre, on which several German books were scattered. Colonel +Stark laid down the lamp on the top of a harmonium beside the +door. 'I shall not keep you waiting an instant,' said he, and +vanished into the darkness. + +"I glanced at the books upon the table, and in spite of my +ignorance of German I could see that two of them were treatises +on science, the others being volumes of poetry. Then I walked +across to the window, hoping that I might catch some glimpse of +the country-side, but an oak shutter, heavily barred, was folded +across it. It was a wonderfully silent house. There was an old +clock ticking loudly somewhere in the passage, but otherwise +everything was deadly still. A vague feeling of uneasiness began +to steal over me. Who were these German people, and what were +they doing living in this strange, out-of-the-way place? And +where was the place? I was ten miles or so from Eyford, that was +all I knew, but whether north, south, east, or west I had no +idea. For that matter, Reading, and possibly other large towns, +were within that radius, so the place might not be so secluded, +after all. Yet it was quite certain, from the absolute stillness, +that we were in the country. I paced up and down the room, +humming a tune under my breath to keep up my spirits and feeling +that I was thoroughly earning my fifty-guinea fee. + +"Suddenly, without any preliminary sound in the midst of the +utter stillness, the door of my room swung slowly open. The woman +was standing in the aperture, the darkness of the hall behind +her, the yellow light from my lamp beating upon her eager and +beautiful face. I could see at a glance that she was sick with +fear, and the sight sent a chill to my own heart. She held up one +shaking finger to warn me to be silent, and she shot a few +whispered words of broken English at me, her eyes glancing back, +like those of a frightened horse, into the gloom behind her. + +"'I would go,' said she, trying hard, as it seemed to me, to +speak calmly; 'I would go. I should not stay here. There is no +good for you to do.' + +"'But, madam,' said I, 'I have not yet done what I came for. I +cannot possibly leave until I have seen the machine.' + +"'It is not worth your while to wait,' she went on. 'You can pass +through the door; no one hinders.' And then, seeing that I smiled +and shook my head, she suddenly threw aside her constraint and +made a step forward, with her hands wrung together. 'For the love +of Heaven!' she whispered, 'get away from here before it is too +late!' + +"But I am somewhat headstrong by nature, and the more ready to +engage in an affair when there is some obstacle in the way. I +thought of my fifty-guinea fee, of my wearisome journey, and of +the unpleasant night which seemed to be before me. Was it all to +go for nothing? Why should I slink away without having carried +out my commission, and without the payment which was my due? This +woman might, for all I knew, be a monomaniac. With a stout +bearing, therefore, though her manner had shaken me more than I +cared to confess, I still shook my head and declared my intention +of remaining where I was. She was about to renew her entreaties +when a door slammed overhead, and the sound of several footsteps +was heard upon the stairs. She listened for an instant, threw up +her hands with a despairing gesture, and vanished as suddenly and +as noiselessly as she had come. + +"The newcomers were Colonel Lysander Stark and a short thick man +with a chinchilla beard growing out of the creases of his double +chin, who was introduced to me as Mr. Ferguson. + +"'This is my secretary and manager,' said the colonel. 'By the +way, I was under the impression that I left this door shut just +now. I fear that you have felt the draught.' + +"'On the contrary,' said I, 'I opened the door myself because I +felt the room to be a little close.' + +"He shot one of his suspicious looks at me. 'Perhaps we had +better proceed to business, then,' said he. 'Mr. Ferguson and I +will take you up to see the machine.' + +"'I had better put my hat on, I suppose.' + +"'Oh, no, it is in the house.' + +"'What, you dig fuller's-earth in the house?' + +"'No, no. This is only where we compress it. But never mind that. +All we wish you to do is to examine the machine and to let us +know what is wrong with it.' + +"We went upstairs together, the colonel first with the lamp, the +fat manager and I behind him. It was a labyrinth of an old house, +with corridors, passages, narrow winding staircases, and little +low doors, the thresholds of which were hollowed out by the +generations who had crossed them. There were no carpets and no +signs of any furniture above the ground floor, while the plaster +was peeling off the walls, and the damp was breaking through in +green, unhealthy blotches. I tried to put on as unconcerned an +air as possible, but I had not forgotten the warnings of the +lady, even though I disregarded them, and I kept a keen eye upon +my two companions. Ferguson appeared to be a morose and silent +man, but I could see from the little that he said that he was at +least a fellow-countryman. + +"Colonel Lysander Stark stopped at last before a low door, which +he unlocked. Within was a small, square room, in which the three +of us could hardly get at one time. Ferguson remained outside, +and the colonel ushered me in. + +"'We are now,' said he, 'actually within the hydraulic press, and +it would be a particularly unpleasant thing for us if anyone were +to turn it on. The ceiling of this small chamber is really the +end of the descending piston, and it comes down with the force of +many tons upon this metal floor. There are small lateral columns +of water outside which receive the force, and which transmit and +multiply it in the manner which is familiar to you. The machine +goes readily enough, but there is some stiffness in the working +of it, and it has lost a little of its force. Perhaps you will +have the goodness to look it over and to show us how we can set +it right.' + +"I took the lamp from him, and I examined the machine very +thoroughly. It was indeed a gigantic one, and capable of +exercising enormous pressure. When I passed outside, however, and +pressed down the levers which controlled it, I knew at once by +the whishing sound that there was a slight leakage, which allowed +a regurgitation of water through one of the side cylinders. An +examination showed that one of the india-rubber bands which was +round the head of a driving-rod had shrunk so as not quite to +fill the socket along which it worked. This was clearly the cause +of the loss of power, and I pointed it out to my companions, who +followed my remarks very carefully and asked several practical +questions as to how they should proceed to set it right. When I +had made it clear to them, I returned to the main chamber of the +machine and took a good look at it to satisfy my own curiosity. +It was obvious at a glance that the story of the fuller's-earth +was the merest fabrication, for it would be absurd to suppose +that so powerful an engine could be designed for so inadequate a +purpose. The walls were of wood, but the floor consisted of a +large iron trough, and when I came to examine it I could see a +crust of metallic deposit all over it. I had stooped and was +scraping at this to see exactly what it was when I heard a +muttered exclamation in German and saw the cadaverous face of the +colonel looking down at me. + +"'What are you doing there?' he asked. + +"I felt angry at having been tricked by so elaborate a story as +that which he had told me. 'I was admiring your fuller's-earth,' +said I; 'I think that I should be better able to advise you as to +your machine if I knew what the exact purpose was for which it +was used.' + +"The instant that I uttered the words I regretted the rashness of +my speech. His face set hard, and a baleful light sprang up in +his grey eyes. + +"'Very well,' said he, 'you shall know all about the machine.' He +took a step backward, slammed the little door, and turned the key +in the lock. I rushed towards it and pulled at the handle, but it +was quite secure, and did not give in the least to my kicks and +shoves. 'Hullo!' I yelled. 'Hullo! Colonel! Let me out!' + +"And then suddenly in the silence I heard a sound which sent my +heart into my mouth. It was the clank of the levers and the swish +of the leaking cylinder. He had set the engine at work. The lamp +still stood upon the floor where I had placed it when examining +the trough. By its light I saw that the black ceiling was coming +down upon me, slowly, jerkily, but, as none knew better than +myself, with a force which must within a minute grind me to a +shapeless pulp. I threw myself, screaming, against the door, and +dragged with my nails at the lock. I implored the colonel to let +me out, but the remorseless clanking of the levers drowned my +cries. The ceiling was only a foot or two above my head, and with +my hand upraised I could feel its hard, rough surface. Then it +flashed through my mind that the pain of my death would depend +very much upon the position in which I met it. If I lay on my +face the weight would come upon my spine, and I shuddered to +think of that dreadful snap. Easier the other way, perhaps; and +yet, had I the nerve to lie and look up at that deadly black +shadow wavering down upon me? Already I was unable to stand +erect, when my eye caught something which brought a gush of hope +back to my heart. + +"I have said that though the floor and ceiling were of iron, the +walls were of wood. As I gave a last hurried glance around, I saw +a thin line of yellow light between two of the boards, which +broadened and broadened as a small panel was pushed backward. For +an instant I could hardly believe that here was indeed a door +which led away from death. The next instant I threw myself +through, and lay half-fainting upon the other side. The panel had +closed again behind me, but the crash of the lamp, and a few +moments afterwards the clang of the two slabs of metal, told me +how narrow had been my escape. + +"I was recalled to myself by a frantic plucking at my wrist, and +I found myself lying upon the stone floor of a narrow corridor, +while a woman bent over me and tugged at me with her left hand, +while she held a candle in her right. It was the same good friend +whose warning I had so foolishly rejected. + +"'Come! come!' she cried breathlessly. 'They will be here in a +moment. They will see that you are not there. Oh, do not waste +the so-precious time, but come!' + +"This time, at least, I did not scorn her advice. I staggered to +my feet and ran with her along the corridor and down a winding +stair. The latter led to another broad passage, and just as we +reached it we heard the sound of running feet and the shouting of +two voices, one answering the other from the floor on which we +were and from the one beneath. My guide stopped and looked about +her like one who is at her wit's end. Then she threw open a door +which led into a bedroom, through the window of which the moon +was shining brightly. + +"'It is your only chance,' said she. 'It is high, but it may be +that you can jump it.' + +"As she spoke a light sprang into view at the further end of the +passage, and I saw the lean figure of Colonel Lysander Stark +rushing forward with a lantern in one hand and a weapon like a +butcher's cleaver in the other. I rushed across the bedroom, +flung open the window, and looked out. How quiet and sweet and +wholesome the garden looked in the moonlight, and it could not be +more than thirty feet down. I clambered out upon the sill, but I +hesitated to jump until I should have heard what passed between +my saviour and the ruffian who pursued me. If she were ill-used, +then at any risks I was determined to go back to her assistance. +The thought had hardly flashed through my mind before he was at +the door, pushing his way past her; but she threw her arms round +him and tried to hold him back. + +"'Fritz! Fritz!' she cried in English, 'remember your promise +after the last time. You said it should not be again. He will be +silent! Oh, he will be silent!' + +"'You are mad, Elise!' he shouted, struggling to break away from +her. 'You will be the ruin of us. He has seen too much. Let me +pass, I say!' He dashed her to one side, and, rushing to the +window, cut at me with his heavy weapon. I had let myself go, and +was hanging by the hands to the sill, when his blow fell. I was +conscious of a dull pain, my grip loosened, and I fell into the +garden below. + +"I was shaken but not hurt by the fall; so I picked myself up and +rushed off among the bushes as hard as I could run, for I +understood that I was far from being out of danger yet. Suddenly, +however, as I ran, a deadly dizziness and sickness came over me. +I glanced down at my hand, which was throbbing painfully, and +then, for the first time, saw that my thumb had been cut off and +that the blood was pouring from my wound. I endeavoured to tie my +handkerchief round it, but there came a sudden buzzing in my +ears, and next moment I fell in a dead faint among the +rose-bushes. + +"How long I remained unconscious I cannot tell. It must have been +a very long time, for the moon had sunk, and a bright morning was +breaking when I came to myself. My clothes were all sodden with +dew, and my coat-sleeve was drenched with blood from my wounded +thumb. The smarting of it recalled in an instant all the +particulars of my night's adventure, and I sprang to my feet with +the feeling that I might hardly yet be safe from my pursuers. But +to my astonishment, when I came to look round me, neither house +nor garden were to be seen. I had been lying in an angle of the +hedge close by the highroad, and just a little lower down was a +long building, which proved, upon my approaching it, to be the +very station at which I had arrived upon the previous night. Were +it not for the ugly wound upon my hand, all that had passed +during those dreadful hours might have been an evil dream. + +"Half dazed, I went into the station and asked about the morning +train. There would be one to Reading in less than an hour. The +same porter was on duty, I found, as had been there when I +arrived. I inquired of him whether he had ever heard of Colonel +Lysander Stark. The name was strange to him. Had he observed a +carriage the night before waiting for me? No, he had not. Was +there a police-station anywhere near? There was one about three +miles off. + +"It was too far for me to go, weak and ill as I was. I determined +to wait until I got back to town before telling my story to the +police. It was a little past six when I arrived, so I went first +to have my wound dressed, and then the doctor was kind enough to +bring me along here. I put the case into your hands and shall do +exactly what you advise." + +We both sat in silence for some little time after listening to +this extraordinary narrative. Then Sherlock Holmes pulled down +from the shelf one of the ponderous commonplace books in which he +placed his cuttings. + +"Here is an advertisement which will interest you," said he. "It +appeared in all the papers about a year ago. Listen to this: +'Lost, on the 9th inst., Mr. Jeremiah Hayling, aged +twenty-six, a hydraulic engineer. Left his lodgings at ten +o'clock at night, and has not been heard of since. Was +dressed in,' etc., etc. Ha! That represents the last time that +the colonel needed to have his machine overhauled, I fancy." + +"Good heavens!" cried my patient. "Then that explains what the +girl said." + +"Undoubtedly. It is quite clear that the colonel was a cool and +desperate man, who was absolutely determined that nothing should +stand in the way of his little game, like those out-and-out +pirates who will leave no survivor from a captured ship. Well, +every moment now is precious, so if you feel equal to it we shall +go down to Scotland Yard at once as a preliminary to starting for +Eyford." + +Some three hours or so afterwards we were all in the train +together, bound from Reading to the little Berkshire village. +There were Sherlock Holmes, the hydraulic engineer, Inspector +Bradstreet, of Scotland Yard, a plain-clothes man, and myself. +Bradstreet had spread an ordnance map of the county out upon the +seat and was busy with his compasses drawing a circle with Eyford +for its centre. + +"There you are," said he. "That circle is drawn at a radius of +ten miles from the village. The place we want must be somewhere +near that line. You said ten miles, I think, sir." + +"It was an hour's good drive." + +"And you think that they brought you back all that way when you +were unconscious?" + +"They must have done so. I have a confused memory, too, of having +been lifted and conveyed somewhere." + +"What I cannot understand," said I, "is why they should have +spared you when they found you lying fainting in the garden. +Perhaps the villain was softened by the woman's entreaties." + +"I hardly think that likely. I never saw a more inexorable face +in my life." + +"Oh, we shall soon clear up all that," said Bradstreet. "Well, I +have drawn my circle, and I only wish I knew at what point upon +it the folk that we are in search of are to be found." + +"I think I could lay my finger on it," said Holmes quietly. + +"Really, now!" cried the inspector, "you have formed your +opinion! Come, now, we shall see who agrees with you. I say it is +south, for the country is more deserted there." + +"And I say east," said my patient. + +"I am for west," remarked the plain-clothes man. "There are +several quiet little villages up there." + +"And I am for north," said I, "because there are no hills there, +and our friend says that he did not notice the carriage go up +any." + +"Come," cried the inspector, laughing; "it's a very pretty +diversity of opinion. We have boxed the compass among us. Who do +you give your casting vote to?" + +"You are all wrong." + +"But we can't all be." + +"Oh, yes, you can. This is my point." He placed his finger in the +centre of the circle. "This is where we shall find them." + +"But the twelve-mile drive?" gasped Hatherley. + +"Six out and six back. Nothing simpler. You say yourself that the +horse was fresh and glossy when you got in. How could it be that +if it had gone twelve miles over heavy roads?" + +"Indeed, it is a likely ruse enough," observed Bradstreet +thoughtfully. "Of course there can be no doubt as to the nature +of this gang." + +"None at all," said Holmes. "They are coiners on a large scale, +and have used the machine to form the amalgam which has taken the +place of silver." + +"We have known for some time that a clever gang was at work," +said the inspector. "They have been turning out half-crowns by +the thousand. We even traced them as far as Reading, but could +get no farther, for they had covered their traces in a way that +showed that they were very old hands. But now, thanks to this +lucky chance, I think that we have got them right enough." + +But the inspector was mistaken, for those criminals were not +destined to fall into the hands of justice. As we rolled into +Eyford Station we saw a gigantic column of smoke which streamed +up from behind a small clump of trees in the neighbourhood and +hung like an immense ostrich feather over the landscape. + +"A house on fire?" asked Bradstreet as the train steamed off +again on its way. + +"Yes, sir!" said the station-master. + +"When did it break out?" + +"I hear that it was during the night, sir, but it has got worse, +and the whole place is in a blaze." + +"Whose house is it?" + +"Dr. Becher's." + +"Tell me," broke in the engineer, "is Dr. Becher a German, very +thin, with a long, sharp nose?" + +The station-master laughed heartily. "No, sir, Dr. Becher is an +Englishman, and there isn't a man in the parish who has a +better-lined waistcoat. But he has a gentleman staying with him, +a patient, as I understand, who is a foreigner, and he looks as +if a little good Berkshire beef would do him no harm." + +The station-master had not finished his speech before we were all +hastening in the direction of the fire. The road topped a low +hill, and there was a great widespread whitewashed building in +front of us, spouting fire at every chink and window, while in +the garden in front three fire-engines were vainly striving to +keep the flames under. + +"That's it!" cried Hatherley, in intense excitement. "There is +the gravel-drive, and there are the rose-bushes where I lay. That +second window is the one that I jumped from." + +"Well, at least," said Holmes, "you have had your revenge upon +them. There can be no question that it was your oil-lamp which, +when it was crushed in the press, set fire to the wooden walls, +though no doubt they were too excited in the chase after you to +observe it at the time. Now keep your eyes open in this crowd for +your friends of last night, though I very much fear that they are +a good hundred miles off by now." + +And Holmes' fears came to be realised, for from that day to this +no word has ever been heard either of the beautiful woman, the +sinister German, or the morose Englishman. Early that morning a +peasant had met a cart containing several people and some very +bulky boxes driving rapidly in the direction of Reading, but +there all traces of the fugitives disappeared, and even Holmes' +ingenuity failed ever to discover the least clue as to their +whereabouts. + +The firemen had been much perturbed at the strange arrangements +which they had found within, and still more so by discovering a +newly severed human thumb upon a window-sill of the second floor. +About sunset, however, their efforts were at last successful, and +they subdued the flames, but not before the roof had fallen in, +and the whole place been reduced to such absolute ruin that, save +some twisted cylinders and iron piping, not a trace remained of +the machinery which had cost our unfortunate acquaintance so +dearly. Large masses of nickel and of tin were discovered stored +in an out-house, but no coins were to be found, which may have +explained the presence of those bulky boxes which have been +already referred to. + +How our hydraulic engineer had been conveyed from the garden to +the spot where he recovered his senses might have remained +forever a mystery were it not for the soft mould, which told us a +very plain tale. He had evidently been carried down by two +persons, one of whom had remarkably small feet and the other +unusually large ones. On the whole, it was most probable that the +silent Englishman, being less bold or less murderous than his +companion, had assisted the woman to bear the unconscious man out +of the way of danger. + +"Well," said our engineer ruefully as we took our seats to return +once more to London, "it has been a pretty business for me! I +have lost my thumb and I have lost a fifty-guinea fee, and what +have I gained?" + +"Experience," said Holmes, laughing. "Indirectly it may be of +value, you know; you have only to put it into words to gain the +reputation of being excellent company for the remainder of your +existence." + + + +X. THE ADVENTURE OF THE NOBLE BACHELOR + +The Lord St. Simon marriage, and its curious termination, have +long ceased to be a subject of interest in those exalted circles +in which the unfortunate bridegroom moves. Fresh scandals have +eclipsed it, and their more piquant details have drawn the +gossips away from this four-year-old drama. As I have reason to +believe, however, that the full facts have never been revealed to +the general public, and as my friend Sherlock Holmes had a +considerable share in clearing the matter up, I feel that no +memoir of him would be complete without some little sketch of +this remarkable episode. + +It was a few weeks before my own marriage, during the days when I +was still sharing rooms with Holmes in Baker Street, that he came +home from an afternoon stroll to find a letter on the table +waiting for him. I had remained indoors all day, for the weather +had taken a sudden turn to rain, with high autumnal winds, and +the Jezail bullet which I had brought back in one of my limbs as +a relic of my Afghan campaign throbbed with dull persistence. +With my body in one easy-chair and my legs upon another, I had +surrounded myself with a cloud of newspapers until at last, +saturated with the news of the day, I tossed them all aside and +lay listless, watching the huge crest and monogram upon the +envelope upon the table and wondering lazily who my friend's +noble correspondent could be. + +"Here is a very fashionable epistle," I remarked as he entered. +"Your morning letters, if I remember right, were from a +fish-monger and a tide-waiter." + +"Yes, my correspondence has certainly the charm of variety," he +answered, smiling, "and the humbler are usually the more +interesting. This looks like one of those unwelcome social +summonses which call upon a man either to be bored or to lie." + +He broke the seal and glanced over the contents. + +"Oh, come, it may prove to be something of interest, after all." + +"Not social, then?" + +"No, distinctly professional." + +"And from a noble client?" + +"One of the highest in England." + +"My dear fellow, I congratulate you." + +"I assure you, Watson, without affectation, that the status of my +client is a matter of less moment to me than the interest of his +case. It is just possible, however, that that also may not be +wanting in this new investigation. You have been reading the +papers diligently of late, have you not?" + +"It looks like it," said I ruefully, pointing to a huge bundle in +the corner. "I have had nothing else to do." + +"It is fortunate, for you will perhaps be able to post me up. I +read nothing except the criminal news and the agony column. The +latter is always instructive. But if you have followed recent +events so closely you must have read about Lord St. Simon and his +wedding?" + +"Oh, yes, with the deepest interest." + +"That is well. The letter which I hold in my hand is from Lord +St. Simon. I will read it to you, and in return you must turn +over these papers and let me have whatever bears upon the matter. +This is what he says: + +"'MY DEAR MR. SHERLOCK HOLMES:--Lord Backwater tells me that I +may place implicit reliance upon your judgment and discretion. I +have determined, therefore, to call upon you and to consult you +in reference to the very painful event which has occurred in +connection with my wedding. Mr. Lestrade, of Scotland Yard, is +acting already in the matter, but he assures me that he sees no +objection to your co-operation, and that he even thinks that +it might be of some assistance. I will call at four o'clock in +the afternoon, and, should you have any other engagement at that +time, I hope that you will postpone it, as this matter is of +paramount importance. Yours faithfully, ST. SIMON.' + +"It is dated from Grosvenor Mansions, written with a quill pen, +and the noble lord has had the misfortune to get a smear of ink +upon the outer side of his right little finger," remarked Holmes +as he folded up the epistle. + +"He says four o'clock. It is three now. He will be here in an +hour." + +"Then I have just time, with your assistance, to get clear upon +the subject. Turn over those papers and arrange the extracts in +their order of time, while I take a glance as to who our client +is." He picked a red-covered volume from a line of books of +reference beside the mantelpiece. "Here he is," said he, sitting +down and flattening it out upon his knee. "'Lord Robert Walsingham +de Vere St. Simon, second son of the Duke of Balmoral.' Hum! 'Arms: +Azure, three caltrops in chief over a fess sable. Born in 1846.' +He's forty-one years of age, which is mature for marriage. Was +Under-Secretary for the colonies in a late administration. The +Duke, his father, was at one time Secretary for Foreign Affairs. +They inherit Plantagenet blood by direct descent, and Tudor on +the distaff side. Ha! Well, there is nothing very instructive in +all this. I think that I must turn to you Watson, for something +more solid." + +"I have very little difficulty in finding what I want," said I, +"for the facts are quite recent, and the matter struck me as +remarkable. I feared to refer them to you, however, as I knew +that you had an inquiry on hand and that you disliked the +intrusion of other matters." + +"Oh, you mean the little problem of the Grosvenor Square +furniture van. That is quite cleared up now--though, indeed, it +was obvious from the first. Pray give me the results of your +newspaper selections." + +"Here is the first notice which I can find. It is in the personal +column of the Morning Post, and dates, as you see, some weeks +back: 'A marriage has been arranged,' it says, 'and will, if +rumour is correct, very shortly take place, between Lord Robert +St. Simon, second son of the Duke of Balmoral, and Miss Hatty +Doran, the only daughter of Aloysius Doran. Esq., of San +Francisco, Cal., U.S.A.' That is all." + +"Terse and to the point," remarked Holmes, stretching his long, +thin legs towards the fire. + +"There was a paragraph amplifying this in one of the society +papers of the same week. Ah, here it is: 'There will soon be a +call for protection in the marriage market, for the present +free-trade principle appears to tell heavily against our home +product. One by one the management of the noble houses of Great +Britain is passing into the hands of our fair cousins from across +the Atlantic. An important addition has been made during the last +week to the list of the prizes which have been borne away by +these charming invaders. Lord St. Simon, who has shown himself +for over twenty years proof against the little god's arrows, has +now definitely announced his approaching marriage with Miss Hatty +Doran, the fascinating daughter of a California millionaire. Miss +Doran, whose graceful figure and striking face attracted much +attention at the Westbury House festivities, is an only child, +and it is currently reported that her dowry will run to +considerably over the six figures, with expectancies for the +future. As it is an open secret that the Duke of Balmoral has +been compelled to sell his pictures within the last few years, +and as Lord St. Simon has no property of his own save the small +estate of Birchmoor, it is obvious that the Californian heiress +is not the only gainer by an alliance which will enable her to +make the easy and common transition from a Republican lady to a +British peeress.'" + +"Anything else?" asked Holmes, yawning. + +"Oh, yes; plenty. Then there is another note in the Morning Post +to say that the marriage would be an absolutely quiet one, that it +would be at St. George's, Hanover Square, that only half a dozen +intimate friends would be invited, and that the party would +return to the furnished house at Lancaster Gate which has been +taken by Mr. Aloysius Doran. Two days later--that is, on +Wednesday last--there is a curt announcement that the wedding had +taken place, and that the honeymoon would be passed at Lord +Backwater's place, near Petersfield. Those are all the notices +which appeared before the disappearance of the bride." + +"Before the what?" asked Holmes with a start. + +"The vanishing of the lady." + +"When did she vanish, then?" + +"At the wedding breakfast." + +"Indeed. This is more interesting than it promised to be; quite +dramatic, in fact." + +"Yes; it struck me as being a little out of the common." + +"They often vanish before the ceremony, and occasionally during +the honeymoon; but I cannot call to mind anything quite so prompt +as this. Pray let me have the details." + +"I warn you that they are very incomplete." + +"Perhaps we may make them less so." + +"Such as they are, they are set forth in a single article of a +morning paper of yesterday, which I will read to you. It is +headed, 'Singular Occurrence at a Fashionable Wedding': + +"'The family of Lord Robert St. Simon has been thrown into the +greatest consternation by the strange and painful episodes which +have taken place in connection with his wedding. The ceremony, as +shortly announced in the papers of yesterday, occurred on the +previous morning; but it is only now that it has been possible to +confirm the strange rumours which have been so persistently +floating about. In spite of the attempts of the friends to hush +the matter up, so much public attention has now been drawn to it +that no good purpose can be served by affecting to disregard what +is a common subject for conversation. + +"'The ceremony, which was performed at St. George's, Hanover +Square, was a very quiet one, no one being present save the +father of the bride, Mr. Aloysius Doran, the Duchess of Balmoral, +Lord Backwater, Lord Eustace and Lady Clara St. Simon (the +younger brother and sister of the bridegroom), and Lady Alicia +Whittington. The whole party proceeded afterwards to the house of +Mr. Aloysius Doran, at Lancaster Gate, where breakfast had been +prepared. It appears that some little trouble was caused by a +woman, whose name has not been ascertained, who endeavoured to +force her way into the house after the bridal party, alleging +that she had some claim upon Lord St. Simon. It was only after a +painful and prolonged scene that she was ejected by the butler +and the footman. The bride, who had fortunately entered the house +before this unpleasant interruption, had sat down to breakfast +with the rest, when she complained of a sudden indisposition and +retired to her room. Her prolonged absence having caused some +comment, her father followed her, but learned from her maid that +she had only come up to her chamber for an instant, caught up an +ulster and bonnet, and hurried down to the passage. One of the +footmen declared that he had seen a lady leave the house thus +apparelled, but had refused to credit that it was his mistress, +believing her to be with the company. On ascertaining that his +daughter had disappeared, Mr. Aloysius Doran, in conjunction with +the bridegroom, instantly put themselves in communication with +the police, and very energetic inquiries are being made, which +will probably result in a speedy clearing up of this very +singular business. Up to a late hour last night, however, nothing +had transpired as to the whereabouts of the missing lady. There +are rumours of foul play in the matter, and it is said that the +police have caused the arrest of the woman who had caused the +original disturbance, in the belief that, from jealousy or some +other motive, she may have been concerned in the strange +disappearance of the bride.'" + +"And is that all?" + +"Only one little item in another of the morning papers, but it is +a suggestive one." + +"And it is--" + +"That Miss Flora Millar, the lady who had caused the disturbance, +has actually been arrested. It appears that she was formerly a +danseuse at the Allegro, and that she has known the bridegroom +for some years. There are no further particulars, and the whole +case is in your hands now--so far as it has been set forth in the +public press." + +"And an exceedingly interesting case it appears to be. I would +not have missed it for worlds. But there is a ring at the bell, +Watson, and as the clock makes it a few minutes after four, I +have no doubt that this will prove to be our noble client. Do not +dream of going, Watson, for I very much prefer having a witness, +if only as a check to my own memory." + +"Lord Robert St. Simon," announced our page-boy, throwing open +the door. A gentleman entered, with a pleasant, cultured face, +high-nosed and pale, with something perhaps of petulance about +the mouth, and with the steady, well-opened eye of a man whose +pleasant lot it had ever been to command and to be obeyed. His +manner was brisk, and yet his general appearance gave an undue +impression of age, for he had a slight forward stoop and a little +bend of the knees as he walked. His hair, too, as he swept off +his very curly-brimmed hat, was grizzled round the edges and thin +upon the top. As to his dress, it was careful to the verge of +foppishness, with high collar, black frock-coat, white waistcoat, +yellow gloves, patent-leather shoes, and light-coloured gaiters. +He advanced slowly into the room, turning his head from left to +right, and swinging in his right hand the cord which held his +golden eyeglasses. + +"Good-day, Lord St. Simon," said Holmes, rising and bowing. "Pray +take the basket-chair. This is my friend and colleague, Dr. +Watson. Draw up a little to the fire, and we will talk this +matter over." + +"A most painful matter to me, as you can most readily imagine, +Mr. Holmes. I have been cut to the quick. I understand that you +have already managed several delicate cases of this sort, sir, +though I presume that they were hardly from the same class of +society." + +"No, I am descending." + +"I beg pardon." + +"My last client of the sort was a king." + +"Oh, really! I had no idea. And which king?" + +"The King of Scandinavia." + +"What! Had he lost his wife?" + +"You can understand," said Holmes suavely, "that I extend to the +affairs of my other clients the same secrecy which I promise to +you in yours." + +"Of course! Very right! very right! I'm sure I beg pardon. As to +my own case, I am ready to give you any information which may +assist you in forming an opinion." + +"Thank you. I have already learned all that is in the public +prints, nothing more. I presume that I may take it as correct--this +article, for example, as to the disappearance of the bride." + +Lord St. Simon glanced over it. "Yes, it is correct, as far as it +goes." + +"But it needs a great deal of supplementing before anyone could +offer an opinion. I think that I may arrive at my facts most +directly by questioning you." + +"Pray do so." + +"When did you first meet Miss Hatty Doran?" + +"In San Francisco, a year ago." + +"You were travelling in the States?" + +"Yes." + +"Did you become engaged then?" + +"No." + +"But you were on a friendly footing?" + +"I was amused by her society, and she could see that I was +amused." + +"Her father is very rich?" + +"He is said to be the richest man on the Pacific slope." + +"And how did he make his money?" + +"In mining. He had nothing a few years ago. Then he struck gold, +invested it, and came up by leaps and bounds." + +"Now, what is your own impression as to the young lady's--your +wife's character?" + +The nobleman swung his glasses a little faster and stared down +into the fire. "You see, Mr. Holmes," said he, "my wife was +twenty before her father became a rich man. During that time she +ran free in a mining camp and wandered through woods or +mountains, so that her education has come from Nature rather than +from the schoolmaster. She is what we call in England a tomboy, +with a strong nature, wild and free, unfettered by any sort of +traditions. She is impetuous--volcanic, I was about to say. She +is swift in making up her mind and fearless in carrying out her +resolutions. On the other hand, I would not have given her the +name which I have the honour to bear"--he gave a little stately +cough--"had not I thought her to be at bottom a noble woman. I +believe that she is capable of heroic self-sacrifice and that +anything dishonourable would be repugnant to her." + +"Have you her photograph?" + +"I brought this with me." He opened a locket and showed us the +full face of a very lovely woman. It was not a photograph but an +ivory miniature, and the artist had brought out the full effect +of the lustrous black hair, the large dark eyes, and the +exquisite mouth. Holmes gazed long and earnestly at it. Then he +closed the locket and handed it back to Lord St. Simon. + +"The young lady came to London, then, and you renewed your +acquaintance?" + +"Yes, her father brought her over for this last London season. I +met her several times, became engaged to her, and have now +married her." + +"She brought, I understand, a considerable dowry?" + +"A fair dowry. Not more than is usual in my family." + +"And this, of course, remains to you, since the marriage is a +fait accompli?" + +"I really have made no inquiries on the subject." + +"Very naturally not. Did you see Miss Doran on the day before the +wedding?" + +"Yes." + +"Was she in good spirits?" + +"Never better. She kept talking of what we should do in our +future lives." + +"Indeed! That is very interesting. And on the morning of the +wedding?" + +"She was as bright as possible--at least until after the +ceremony." + +"And did you observe any change in her then?" + +"Well, to tell the truth, I saw then the first signs that I had +ever seen that her temper was just a little sharp. The incident +however, was too trivial to relate and can have no possible +bearing upon the case." + +"Pray let us have it, for all that." + +"Oh, it is childish. She dropped her bouquet as we went towards +the vestry. She was passing the front pew at the time, and it +fell over into the pew. There was a moment's delay, but the +gentleman in the pew handed it up to her again, and it did not +appear to be the worse for the fall. Yet when I spoke to her of +the matter, she answered me abruptly; and in the carriage, on our +way home, she seemed absurdly agitated over this trifling cause." + +"Indeed! You say that there was a gentleman in the pew. Some of +the general public were present, then?" + +"Oh, yes. It is impossible to exclude them when the church is +open." + +"This gentleman was not one of your wife's friends?" + +"No, no; I call him a gentleman by courtesy, but he was quite a +common-looking person. I hardly noticed his appearance. But +really I think that we are wandering rather far from the point." + +"Lady St. Simon, then, returned from the wedding in a less +cheerful frame of mind than she had gone to it. What did she do +on re-entering her father's house?" + +"I saw her in conversation with her maid." + +"And who is her maid?" + +"Alice is her name. She is an American and came from California +with her." + +"A confidential servant?" + +"A little too much so. It seemed to me that her mistress allowed +her to take great liberties. Still, of course, in America they +look upon these things in a different way." + +"How long did she speak to this Alice?" + +"Oh, a few minutes. I had something else to think of." + +"You did not overhear what they said?" + +"Lady St. Simon said something about 'jumping a claim.' She was +accustomed to use slang of the kind. I have no idea what she +meant." + +"American slang is very expressive sometimes. And what did your +wife do when she finished speaking to her maid?" + +"She walked into the breakfast-room." + +"On your arm?" + +"No, alone. She was very independent in little matters like that. +Then, after we had sat down for ten minutes or so, she rose +hurriedly, muttered some words of apology, and left the room. She +never came back." + +"But this maid, Alice, as I understand, deposes that she went to +her room, covered her bride's dress with a long ulster, put on a +bonnet, and went out." + +"Quite so. And she was afterwards seen walking into Hyde Park in +company with Flora Millar, a woman who is now in custody, and who +had already made a disturbance at Mr. Doran's house that +morning." + +"Ah, yes. I should like a few particulars as to this young lady, +and your relations to her." + +Lord St. Simon shrugged his shoulders and raised his eyebrows. +"We have been on a friendly footing for some years--I may say on +a very friendly footing. She used to be at the Allegro. I have +not treated her ungenerously, and she had no just cause of +complaint against me, but you know what women are, Mr. Holmes. +Flora was a dear little thing, but exceedingly hot-headed and +devotedly attached to me. She wrote me dreadful letters when she +heard that I was about to be married, and, to tell the truth, the +reason why I had the marriage celebrated so quietly was that I +feared lest there might be a scandal in the church. She came to +Mr. Doran's door just after we returned, and she endeavoured to +push her way in, uttering very abusive expressions towards my +wife, and even threatening her, but I had foreseen the +possibility of something of the sort, and I had two police +fellows there in private clothes, who soon pushed her out again. +She was quiet when she saw that there was no good in making a +row." + +"Did your wife hear all this?" + +"No, thank goodness, she did not." + +"And she was seen walking with this very woman afterwards?" + +"Yes. That is what Mr. Lestrade, of Scotland Yard, looks upon as +so serious. It is thought that Flora decoyed my wife out and laid +some terrible trap for her." + +"Well, it is a possible supposition." + +"You think so, too?" + +"I did not say a probable one. But you do not yourself look upon +this as likely?" + +"I do not think Flora would hurt a fly." + +"Still, jealousy is a strange transformer of characters. Pray +what is your own theory as to what took place?" + +"Well, really, I came to seek a theory, not to propound one. I +have given you all the facts. Since you ask me, however, I may +say that it has occurred to me as possible that the excitement of +this affair, the consciousness that she had made so immense a +social stride, had the effect of causing some little nervous +disturbance in my wife." + +"In short, that she had become suddenly deranged?" + +"Well, really, when I consider that she has turned her back--I +will not say upon me, but upon so much that many have aspired to +without success--I can hardly explain it in any other fashion." + +"Well, certainly that is also a conceivable hypothesis," said +Holmes, smiling. "And now, Lord St. Simon, I think that I have +nearly all my data. May I ask whether you were seated at the +breakfast-table so that you could see out of the window?" + +"We could see the other side of the road and the Park." + +"Quite so. Then I do not think that I need to detain you longer. +I shall communicate with you." + +"Should you be fortunate enough to solve this problem," said our +client, rising. + +"I have solved it." + +"Eh? What was that?" + +"I say that I have solved it." + +"Where, then, is my wife?" + +"That is a detail which I shall speedily supply." + +Lord St. Simon shook his head. "I am afraid that it will take +wiser heads than yours or mine," he remarked, and bowing in a +stately, old-fashioned manner he departed. + +"It is very good of Lord St. Simon to honour my head by putting +it on a level with his own," said Sherlock Holmes, laughing. "I +think that I shall have a whisky and soda and a cigar after all +this cross-questioning. I had formed my conclusions as to the +case before our client came into the room." + +"My dear Holmes!" + +"I have notes of several similar cases, though none, as I +remarked before, which were quite as prompt. My whole examination +served to turn my conjecture into a certainty. Circumstantial +evidence is occasionally very convincing, as when you find a +trout in the milk, to quote Thoreau's example." + +"But I have heard all that you have heard." + +"Without, however, the knowledge of pre-existing cases which +serves me so well. There was a parallel instance in Aberdeen some +years back, and something on very much the same lines at Munich +the year after the Franco-Prussian War. It is one of these +cases--but, hullo, here is Lestrade! Good-afternoon, Lestrade! +You will find an extra tumbler upon the sideboard, and there are +cigars in the box." + +The official detective was attired in a pea-jacket and cravat, +which gave him a decidedly nautical appearance, and he carried a +black canvas bag in his hand. With a short greeting he seated +himself and lit the cigar which had been offered to him. + +"What's up, then?" asked Holmes with a twinkle in his eye. "You +look dissatisfied." + +"And I feel dissatisfied. It is this infernal St. Simon marriage +case. I can make neither head nor tail of the business." + +"Really! You surprise me." + +"Who ever heard of such a mixed affair? Every clue seems to slip +through my fingers. I have been at work upon it all day." + +"And very wet it seems to have made you," said Holmes laying his +hand upon the arm of the pea-jacket. + +"Yes, I have been dragging the Serpentine." + +"In heaven's name, what for?" + +"In search of the body of Lady St. Simon." + +Sherlock Holmes leaned back in his chair and laughed heartily. + +"Have you dragged the basin of Trafalgar Square fountain?" he +asked. + +"Why? What do you mean?" + +"Because you have just as good a chance of finding this lady in +the one as in the other." + +Lestrade shot an angry glance at my companion. "I suppose you +know all about it," he snarled. + +"Well, I have only just heard the facts, but my mind is made up." + +"Oh, indeed! Then you think that the Serpentine plays no part in +the matter?" + +"I think it very unlikely." + +"Then perhaps you will kindly explain how it is that we found +this in it?" He opened his bag as he spoke, and tumbled onto the +floor a wedding-dress of watered silk, a pair of white satin +shoes and a bride's wreath and veil, all discoloured and soaked +in water. "There," said he, putting a new wedding-ring upon the +top of the pile. "There is a little nut for you to crack, Master +Holmes." + +"Oh, indeed!" said my friend, blowing blue rings into the air. +"You dragged them from the Serpentine?" + +"No. They were found floating near the margin by a park-keeper. +They have been identified as her clothes, and it seemed to me +that if the clothes were there the body would not be far off." + +"By the same brilliant reasoning, every man's body is to be found +in the neighbourhood of his wardrobe. And pray what did you hope +to arrive at through this?" + +"At some evidence implicating Flora Millar in the disappearance." + +"I am afraid that you will find it difficult." + +"Are you, indeed, now?" cried Lestrade with some bitterness. "I +am afraid, Holmes, that you are not very practical with your +deductions and your inferences. You have made two blunders in as +many minutes. This dress does implicate Miss Flora Millar." + +"And how?" + +"In the dress is a pocket. In the pocket is a card-case. In the +card-case is a note. And here is the very note." He slapped it +down upon the table in front of him. "Listen to this: 'You will +see me when all is ready. Come at once. F.H.M.' Now my theory all +along has been that Lady St. Simon was decoyed away by Flora +Millar, and that she, with confederates, no doubt, was +responsible for her disappearance. Here, signed with her +initials, is the very note which was no doubt quietly slipped +into her hand at the door and which lured her within their +reach." + +"Very good, Lestrade," said Holmes, laughing. "You really are +very fine indeed. Let me see it." He took up the paper in a +listless way, but his attention instantly became riveted, and he +gave a little cry of satisfaction. "This is indeed important," +said he. + +"Ha! you find it so?" + +"Extremely so. I congratulate you warmly." + +Lestrade rose in his triumph and bent his head to look. "Why," he +shrieked, "you're looking at the wrong side!" + +"On the contrary, this is the right side." + +"The right side? You're mad! Here is the note written in pencil +over here." + +"And over here is what appears to be the fragment of a hotel +bill, which interests me deeply." + +"There's nothing in it. I looked at it before," said Lestrade. +"'Oct. 4th, rooms 8s., breakfast 2s. 6d., cocktail 1s., lunch 2s. +6d., glass sherry, 8d.' I see nothing in that." + +"Very likely not. It is most important, all the same. As to the +note, it is important also, or at least the initials are, so I +congratulate you again." + +"I've wasted time enough," said Lestrade, rising. "I believe in +hard work and not in sitting by the fire spinning fine theories. +Good-day, Mr. Holmes, and we shall see which gets to the bottom +of the matter first." He gathered up the garments, thrust them +into the bag, and made for the door. + +"Just one hint to you, Lestrade," drawled Holmes before his rival +vanished; "I will tell you the true solution of the matter. Lady +St. Simon is a myth. There is not, and there never has been, any +such person." + +Lestrade looked sadly at my companion. Then he turned to me, +tapped his forehead three times, shook his head solemnly, and +hurried away. + +He had hardly shut the door behind him when Holmes rose to put on +his overcoat. "There is something in what the fellow says about +outdoor work," he remarked, "so I think, Watson, that I must +leave you to your papers for a little." + +It was after five o'clock when Sherlock Holmes left me, but I had +no time to be lonely, for within an hour there arrived a +confectioner's man with a very large flat box. This he unpacked +with the help of a youth whom he had brought with him, and +presently, to my very great astonishment, a quite epicurean +little cold supper began to be laid out upon our humble +lodging-house mahogany. There were a couple of brace of cold +woodcock, a pheasant, a pt de foie gras pie with a group of +ancient and cobwebby bottles. Having laid out all these luxuries, +my two visitors vanished away, like the genii of the Arabian +Nights, with no explanation save that the things had been paid +for and were ordered to this address. + +Just before nine o'clock Sherlock Holmes stepped briskly into the +room. His features were gravely set, but there was a light in his +eye which made me think that he had not been disappointed in his +conclusions. + +"They have laid the supper, then," he said, rubbing his hands. + +"You seem to expect company. They have laid for five." + +"Yes, I fancy we may have some company dropping in," said he. "I +am surprised that Lord St. Simon has not already arrived. Ha! I +fancy that I hear his step now upon the stairs." + +It was indeed our visitor of the afternoon who came bustling in, +dangling his glasses more vigorously than ever, and with a very +perturbed expression upon his aristocratic features. + +"My messenger reached you, then?" asked Holmes. + +"Yes, and I confess that the contents startled me beyond measure. +Have you good authority for what you say?" + +"The best possible." + +Lord St. Simon sank into a chair and passed his hand over his +forehead. + +"What will the Duke say," he murmured, "when he hears that one of +the family has been subjected to such humiliation?" + +"It is the purest accident. I cannot allow that there is any +humiliation." + +"Ah, you look on these things from another standpoint." + +"I fail to see that anyone is to blame. I can hardly see how the +lady could have acted otherwise, though her abrupt method of +doing it was undoubtedly to be regretted. Having no mother, she +had no one to advise her at such a crisis." + +"It was a slight, sir, a public slight," said Lord St. Simon, +tapping his fingers upon the table. + +"You must make allowance for this poor girl, placed in so +unprecedented a position." + +"I will make no allowance. I am very angry indeed, and I have +been shamefully used." + +"I think that I heard a ring," said Holmes. "Yes, there are steps +on the landing. If I cannot persuade you to take a lenient view +of the matter, Lord St. Simon, I have brought an advocate here +who may be more successful." He opened the door and ushered in a +lady and gentleman. "Lord St. Simon," said he "allow me to +introduce you to Mr. and Mrs. Francis Hay Moulton. The lady, I +think, you have already met." + +At the sight of these newcomers our client had sprung from his +seat and stood very erect, with his eyes cast down and his hand +thrust into the breast of his frock-coat, a picture of offended +dignity. The lady had taken a quick step forward and had held out +her hand to him, but he still refused to raise his eyes. It was +as well for his resolution, perhaps, for her pleading face was +one which it was hard to resist. + +"You're angry, Robert," said she. "Well, I guess you have every +cause to be." + +"Pray make no apology to me," said Lord St. Simon bitterly. + +"Oh, yes, I know that I have treated you real bad and that I +should have spoken to you before I went; but I was kind of +rattled, and from the time when I saw Frank here again I just +didn't know what I was doing or saying. I only wonder I didn't +fall down and do a faint right there before the altar." + +"Perhaps, Mrs. Moulton, you would like my friend and me to leave +the room while you explain this matter?" + +"If I may give an opinion," remarked the strange gentleman, +"we've had just a little too much secrecy over this business +already. For my part, I should like all Europe and America to +hear the rights of it." He was a small, wiry, sunburnt man, +clean-shaven, with a sharp face and alert manner. + +"Then I'll tell our story right away," said the lady. "Frank here +and I met in '84, in McQuire's camp, near the Rockies, where pa +was working a claim. We were engaged to each other, Frank and I; +but then one day father struck a rich pocket and made a pile, +while poor Frank here had a claim that petered out and came to +nothing. The richer pa grew the poorer was Frank; so at last pa +wouldn't hear of our engagement lasting any longer, and he took +me away to 'Frisco. Frank wouldn't throw up his hand, though; so +he followed me there, and he saw me without pa knowing anything +about it. It would only have made him mad to know, so we just +fixed it all up for ourselves. Frank said that he would go and +make his pile, too, and never come back to claim me until he had +as much as pa. So then I promised to wait for him to the end of +time and pledged myself not to marry anyone else while he lived. +'Why shouldn't we be married right away, then,' said he, 'and +then I will feel sure of you; and I won't claim to be your +husband until I come back?' Well, we talked it over, and he had +fixed it all up so nicely, with a clergyman all ready in waiting, +that we just did it right there; and then Frank went off to seek +his fortune, and I went back to pa. + +"The next I heard of Frank was that he was in Montana, and then +he went prospecting in Arizona, and then I heard of him from New +Mexico. After that came a long newspaper story about how a +miners' camp had been attacked by Apache Indians, and there was +my Frank's name among the killed. I fainted dead away, and I was +very sick for months after. Pa thought I had a decline and took +me to half the doctors in 'Frisco. Not a word of news came for a +year and more, so that I never doubted that Frank was really +dead. Then Lord St. Simon came to 'Frisco, and we came to London, +and a marriage was arranged, and pa was very pleased, but I felt +all the time that no man on this earth would ever take the place +in my heart that had been given to my poor Frank. + +"Still, if I had married Lord St. Simon, of course I'd have done +my duty by him. We can't command our love, but we can our +actions. I went to the altar with him with the intention to make +him just as good a wife as it was in me to be. But you may +imagine what I felt when, just as I came to the altar rails, I +glanced back and saw Frank standing and looking at me out of the +first pew. I thought it was his ghost at first; but when I looked +again there he was still, with a kind of question in his eyes, as +if to ask me whether I were glad or sorry to see him. I wonder I +didn't drop. I know that everything was turning round, and the +words of the clergyman were just like the buzz of a bee in my +ear. I didn't know what to do. Should I stop the service and make +a scene in the church? I glanced at him again, and he seemed to +know what I was thinking, for he raised his finger to his lips to +tell me to be still. Then I saw him scribble on a piece of paper, +and I knew that he was writing me a note. As I passed his pew on +the way out I dropped my bouquet over to him, and he slipped the +note into my hand when he returned me the flowers. It was only a +line asking me to join him when he made the sign to me to do so. +Of course I never doubted for a moment that my first duty was now +to him, and I determined to do just whatever he might direct. + +"When I got back I told my maid, who had known him in California, +and had always been his friend. I ordered her to say nothing, but +to get a few things packed and my ulster ready. I know I ought to +have spoken to Lord St. Simon, but it was dreadful hard before +his mother and all those great people. I just made up my mind to +run away and explain afterwards. I hadn't been at the table ten +minutes before I saw Frank out of the window at the other side of +the road. He beckoned to me and then began walking into the Park. +I slipped out, put on my things, and followed him. Some woman +came talking something or other about Lord St. Simon to +me--seemed to me from the little I heard as if he had a little +secret of his own before marriage also--but I managed to get away +from her and soon overtook Frank. We got into a cab together, and +away we drove to some lodgings he had taken in Gordon Square, and +that was my true wedding after all those years of waiting. Frank +had been a prisoner among the Apaches, had escaped, came on to +'Frisco, found that I had given him up for dead and had gone to +England, followed me there, and had come upon me at last on the +very morning of my second wedding." + +"I saw it in a paper," explained the American. "It gave the name +and the church but not where the lady lived." + +"Then we had a talk as to what we should do, and Frank was all +for openness, but I was so ashamed of it all that I felt as if I +should like to vanish away and never see any of them again--just +sending a line to pa, perhaps, to show him that I was alive. It +was awful to me to think of all those lords and ladies sitting +round that breakfast-table and waiting for me to come back. So +Frank took my wedding-clothes and things and made a bundle of +them, so that I should not be traced, and dropped them away +somewhere where no one could find them. It is likely that we +should have gone on to Paris to-morrow, only that this good +gentleman, Mr. Holmes, came round to us this evening, though how +he found us is more than I can think, and he showed us very +clearly and kindly that I was wrong and that Frank was right, and +that we should be putting ourselves in the wrong if we were so +secret. Then he offered to give us a chance of talking to Lord +St. Simon alone, and so we came right away round to his rooms at +once. Now, Robert, you have heard it all, and I am very sorry if +I have given you pain, and I hope that you do not think very +meanly of me." + +Lord St. Simon had by no means relaxed his rigid attitude, but +had listened with a frowning brow and a compressed lip to this +long narrative. + +"Excuse me," he said, "but it is not my custom to discuss my most +intimate personal affairs in this public manner." + +"Then you won't forgive me? You won't shake hands before I go?" + +"Oh, certainly, if it would give you any pleasure." He put out +his hand and coldly grasped that which she extended to him. + +"I had hoped," suggested Holmes, "that you would have joined us +in a friendly supper." + +"I think that there you ask a little too much," responded his +Lordship. "I may be forced to acquiesce in these recent +developments, but I can hardly be expected to make merry over +them. I think that with your permission I will now wish you all a +very good-night." He included us all in a sweeping bow and +stalked out of the room. + +"Then I trust that you at least will honour me with your +company," said Sherlock Holmes. "It is always a joy to meet an +American, Mr. Moulton, for I am one of those who believe that the +folly of a monarch and the blundering of a minister in far-gone +years will not prevent our children from being some day citizens +of the same world-wide country under a flag which shall be a +quartering of the Union Jack with the Stars and Stripes." + +"The case has been an interesting one," remarked Holmes when our +visitors had left us, "because it serves to show very clearly how +simple the explanation may be of an affair which at first sight +seems to be almost inexplicable. Nothing could be more natural +than the sequence of events as narrated by this lady, and nothing +stranger than the result when viewed, for instance, by Mr. +Lestrade of Scotland Yard." + +"You were not yourself at fault at all, then?" + +"From the first, two facts were very obvious to me, the one that +the lady had been quite willing to undergo the wedding ceremony, +the other that she had repented of it within a few minutes of +returning home. Obviously something had occurred during the +morning, then, to cause her to change her mind. What could that +something be? She could not have spoken to anyone when she was +out, for she had been in the company of the bridegroom. Had she +seen someone, then? If she had, it must be someone from America +because she had spent so short a time in this country that she +could hardly have allowed anyone to acquire so deep an influence +over her that the mere sight of him would induce her to change +her plans so completely. You see we have already arrived, by a +process of exclusion, at the idea that she might have seen an +American. Then who could this American be, and why should he +possess so much influence over her? It might be a lover; it might +be a husband. Her young womanhood had, I knew, been spent in +rough scenes and under strange conditions. So far I had got +before I ever heard Lord St. Simon's narrative. When he told us +of a man in a pew, of the change in the bride's manner, of so +transparent a device for obtaining a note as the dropping of a +bouquet, of her resort to her confidential maid, and of her very +significant allusion to claim-jumping--which in miners' parlance +means taking possession of that which another person has a prior +claim to--the whole situation became absolutely clear. She had +gone off with a man, and the man was either a lover or was a +previous husband--the chances being in favour of the latter." + +"And how in the world did you find them?" + +"It might have been difficult, but friend Lestrade held +information in his hands the value of which he did not himself +know. The initials were, of course, of the highest importance, +but more valuable still was it to know that within a week he had +settled his bill at one of the most select London hotels." + +"How did you deduce the select?" + +"By the select prices. Eight shillings for a bed and eightpence +for a glass of sherry pointed to one of the most expensive +hotels. There are not many in London which charge at that rate. +In the second one which I visited in Northumberland Avenue, I +learned by an inspection of the book that Francis H. Moulton, an +American gentleman, had left only the day before, and on looking +over the entries against him, I came upon the very items which I +had seen in the duplicate bill. His letters were to be forwarded +to 226 Gordon Square; so thither I travelled, and being fortunate +enough to find the loving couple at home, I ventured to give them +some paternal advice and to point out to them that it would be +better in every way that they should make their position a little +clearer both to the general public and to Lord St. Simon in +particular. I invited them to meet him here, and, as you see, I +made him keep the appointment." + +"But with no very good result," I remarked. "His conduct was +certainly not very gracious." + +"Ah, Watson," said Holmes, smiling, "perhaps you would not be +very gracious either, if, after all the trouble of wooing and +wedding, you found yourself deprived in an instant of wife and of +fortune. I think that we may judge Lord St. Simon very mercifully +and thank our stars that we are never likely to find ourselves in +the same position. Draw your chair up and hand me my violin, for +the only problem we have still to solve is how to while away +these bleak autumnal evenings." + + + +XI. THE ADVENTURE OF THE BERYL CORONET + +"Holmes," said I as I stood one morning in our bow-window looking +down the street, "here is a madman coming along. It seems rather +sad that his relatives should allow him to come out alone." + +My friend rose lazily from his armchair and stood with his hands +in the pockets of his dressing-gown, looking over my shoulder. It +was a bright, crisp February morning, and the snow of the day +before still lay deep upon the ground, shimmering brightly in the +wintry sun. Down the centre of Baker Street it had been ploughed +into a brown crumbly band by the traffic, but at either side and +on the heaped-up edges of the foot-paths it still lay as white as +when it fell. The grey pavement had been cleaned and scraped, but +was still dangerously slippery, so that there were fewer +passengers than usual. Indeed, from the direction of the +Metropolitan Station no one was coming save the single gentleman +whose eccentric conduct had drawn my attention. + +He was a man of about fifty, tall, portly, and imposing, with a +massive, strongly marked face and a commanding figure. He was +dressed in a sombre yet rich style, in black frock-coat, shining +hat, neat brown gaiters, and well-cut pearl-grey trousers. Yet +his actions were in absurd contrast to the dignity of his dress +and features, for he was running hard, with occasional little +springs, such as a weary man gives who is little accustomed to +set any tax upon his legs. As he ran he jerked his hands up and +down, waggled his head, and writhed his face into the most +extraordinary contortions. + +"What on earth can be the matter with him?" I asked. "He is +looking up at the numbers of the houses." + +"I believe that he is coming here," said Holmes, rubbing his +hands. + +"Here?" + +"Yes; I rather think he is coming to consult me professionally. I +think that I recognise the symptoms. Ha! did I not tell you?" As +he spoke, the man, puffing and blowing, rushed at our door and +pulled at our bell until the whole house resounded with the +clanging. + +A few moments later he was in our room, still puffing, still +gesticulating, but with so fixed a look of grief and despair in +his eyes that our smiles were turned in an instant to horror and +pity. For a while he could not get his words out, but swayed his +body and plucked at his hair like one who has been driven to the +extreme limits of his reason. Then, suddenly springing to his +feet, he beat his head against the wall with such force that we +both rushed upon him and tore him away to the centre of the room. +Sherlock Holmes pushed him down into the easy-chair and, sitting +beside him, patted his hand and chatted with him in the easy, +soothing tones which he knew so well how to employ. + +"You have come to me to tell your story, have you not?" said he. +"You are fatigued with your haste. Pray wait until you have +recovered yourself, and then I shall be most happy to look into +any little problem which you may submit to me." + +The man sat for a minute or more with a heaving chest, fighting +against his emotion. Then he passed his handkerchief over his +brow, set his lips tight, and turned his face towards us. + +"No doubt you think me mad?" said he. + +"I see that you have had some great trouble," responded Holmes. + +"God knows I have!--a trouble which is enough to unseat my +reason, so sudden and so terrible is it. Public disgrace I might +have faced, although I am a man whose character has never yet +borne a stain. Private affliction also is the lot of every man; +but the two coming together, and in so frightful a form, have +been enough to shake my very soul. Besides, it is not I alone. +The very noblest in the land may suffer unless some way be found +out of this horrible affair." + +"Pray compose yourself, sir," said Holmes, "and let me have a +clear account of who you are and what it is that has befallen +you." + +"My name," answered our visitor, "is probably familiar to your +ears. I am Alexander Holder, of the banking firm of Holder & +Stevenson, of Threadneedle Street." + +The name was indeed well known to us as belonging to the senior +partner in the second largest private banking concern in the City +of London. What could have happened, then, to bring one of the +foremost citizens of London to this most pitiable pass? We +waited, all curiosity, until with another effort he braced +himself to tell his story. + +"I feel that time is of value," said he; "that is why I hastened +here when the police inspector suggested that I should secure +your co-operation. I came to Baker Street by the Underground and +hurried from there on foot, for the cabs go slowly through this +snow. That is why I was so out of breath, for I am a man who +takes very little exercise. I feel better now, and I will put the +facts before you as shortly and yet as clearly as I can. + +"It is, of course, well known to you that in a successful banking +business as much depends upon our being able to find remunerative +investments for our funds as upon our increasing our connection +and the number of our depositors. One of our most lucrative means +of laying out money is in the shape of loans, where the security +is unimpeachable. We have done a good deal in this direction +during the last few years, and there are many noble families to +whom we have advanced large sums upon the security of their +pictures, libraries, or plate. + +"Yesterday morning I was seated in my office at the bank when a +card was brought in to me by one of the clerks. I started when I +saw the name, for it was that of none other than--well, perhaps +even to you I had better say no more than that it was a name +which is a household word all over the earth--one of the highest, +noblest, most exalted names in England. I was overwhelmed by the +honour and attempted, when he entered, to say so, but he plunged +at once into business with the air of a man who wishes to hurry +quickly through a disagreeable task. + +"'Mr. Holder,' said he, 'I have been informed that you are in the +habit of advancing money.' + +"'The firm does so when the security is good.' I answered. + +"'It is absolutely essential to me,' said he, 'that I should have +50,000 pounds at once. I could, of course, borrow so trifling a +sum ten times over from my friends, but I much prefer to make it +a matter of business and to carry out that business myself. In my +position you can readily understand that it is unwise to place +one's self under obligations.' + +"'For how long, may I ask, do you want this sum?' I asked. + +"'Next Monday I have a large sum due to me, and I shall then most +certainly repay what you advance, with whatever interest you +think it right to charge. But it is very essential to me that the +money should be paid at once.' + +"'I should be happy to advance it without further parley from my +own private purse,' said I, 'were it not that the strain would be +rather more than it could bear. If, on the other hand, I am to do +it in the name of the firm, then in justice to my partner I must +insist that, even in your case, every businesslike precaution +should be taken.' + +"'I should much prefer to have it so,' said he, raising up a +square, black morocco case which he had laid beside his chair. +'You have doubtless heard of the Beryl Coronet?' + +"'One of the most precious public possessions of the empire,' +said I. + +"'Precisely.' He opened the case, and there, imbedded in soft, +flesh-coloured velvet, lay the magnificent piece of jewellery +which he had named. 'There are thirty-nine enormous beryls,' said +he, 'and the price of the gold chasing is incalculable. The +lowest estimate would put the worth of the coronet at double the +sum which I have asked. I am prepared to leave it with you as my +security.' + +"I took the precious case into my hands and looked in some +perplexity from it to my illustrious client. + +"'You doubt its value?' he asked. + +"'Not at all. I only doubt--' + +"'The propriety of my leaving it. You may set your mind at rest +about that. I should not dream of doing so were it not absolutely +certain that I should be able in four days to reclaim it. It is a +pure matter of form. Is the security sufficient?' + +"'Ample.' + +"'You understand, Mr. Holder, that I am giving you a strong proof +of the confidence which I have in you, founded upon all that I +have heard of you. I rely upon you not only to be discreet and to +refrain from all gossip upon the matter but, above all, to +preserve this coronet with every possible precaution because I +need not say that a great public scandal would be caused if any +harm were to befall it. Any injury to it would be almost as +serious as its complete loss, for there are no beryls in the +world to match these, and it would be impossible to replace them. +I leave it with you, however, with every confidence, and I shall +call for it in person on Monday morning.' + +"Seeing that my client was anxious to leave, I said no more but, +calling for my cashier, I ordered him to pay over fifty 1000 +pound notes. When I was alone once more, however, with the +precious case lying upon the table in front of me, I could not +but think with some misgivings of the immense responsibility +which it entailed upon me. There could be no doubt that, as it +was a national possession, a horrible scandal would ensue if any +misfortune should occur to it. I already regretted having ever +consented to take charge of it. However, it was too late to alter +the matter now, so I locked it up in my private safe and turned +once more to my work. + +"When evening came I felt that it would be an imprudence to leave +so precious a thing in the office behind me. Bankers' safes had +been forced before now, and why should not mine be? If so, how +terrible would be the position in which I should find myself! I +determined, therefore, that for the next few days I would always +carry the case backward and forward with me, so that it might +never be really out of my reach. With this intention, I called a +cab and drove out to my house at Streatham, carrying the jewel +with me. I did not breathe freely until I had taken it upstairs +and locked it in the bureau of my dressing-room. + +"And now a word as to my household, Mr. Holmes, for I wish you to +thoroughly understand the situation. My groom and my page sleep +out of the house, and may be set aside altogether. I have three +maid-servants who have been with me a number of years and whose +absolute reliability is quite above suspicion. Another, Lucy +Parr, the second waiting-maid, has only been in my service a few +months. She came with an excellent character, however, and has +always given me satisfaction. She is a very pretty girl and has +attracted admirers who have occasionally hung about the place. +That is the only drawback which we have found to her, but we +believe her to be a thoroughly good girl in every way. + +"So much for the servants. My family itself is so small that it +will not take me long to describe it. I am a widower and have an +only son, Arthur. He has been a disappointment to me, Mr. +Holmes--a grievous disappointment. I have no doubt that I am +myself to blame. People tell me that I have spoiled him. Very +likely I have. When my dear wife died I felt that he was all I +had to love. I could not bear to see the smile fade even for a +moment from his face. I have never denied him a wish. Perhaps it +would have been better for both of us had I been sterner, but I +meant it for the best. + +"It was naturally my intention that he should succeed me in my +business, but he was not of a business turn. He was wild, +wayward, and, to speak the truth, I could not trust him in the +handling of large sums of money. When he was young he became a +member of an aristocratic club, and there, having charming +manners, he was soon the intimate of a number of men with long +purses and expensive habits. He learned to play heavily at cards +and to squander money on the turf, until he had again and again +to come to me and implore me to give him an advance upon his +allowance, that he might settle his debts of honour. He tried +more than once to break away from the dangerous company which he +was keeping, but each time the influence of his friend, Sir +George Burnwell, was enough to draw him back again. + +"And, indeed, I could not wonder that such a man as Sir George +Burnwell should gain an influence over him, for he has frequently +brought him to my house, and I have found myself that I could +hardly resist the fascination of his manner. He is older than +Arthur, a man of the world to his finger-tips, one who had been +everywhere, seen everything, a brilliant talker, and a man of +great personal beauty. Yet when I think of him in cold blood, far +away from the glamour of his presence, I am convinced from his +cynical speech and the look which I have caught in his eyes that +he is one who should be deeply distrusted. So I think, and so, +too, thinks my little Mary, who has a woman's quick insight into +character. + +"And now there is only she to be described. She is my niece; but +when my brother died five years ago and left her alone in the +world I adopted her, and have looked upon her ever since as my +daughter. She is a sunbeam in my house--sweet, loving, beautiful, +a wonderful manager and housekeeper, yet as tender and quiet and +gentle as a woman could be. She is my right hand. I do not know +what I could do without her. In only one matter has she ever gone +against my wishes. Twice my boy has asked her to marry him, for +he loves her devotedly, but each time she has refused him. I +think that if anyone could have drawn him into the right path it +would have been she, and that his marriage might have changed his +whole life; but now, alas! it is too late--forever too late! + +"Now, Mr. Holmes, you know the people who live under my roof, and +I shall continue with my miserable story. + +"When we were taking coffee in the drawing-room that night after +dinner, I told Arthur and Mary my experience, and of the precious +treasure which we had under our roof, suppressing only the name +of my client. Lucy Parr, who had brought in the coffee, had, I am +sure, left the room; but I cannot swear that the door was closed. +Mary and Arthur were much interested and wished to see the famous +coronet, but I thought it better not to disturb it. + +"'Where have you put it?' asked Arthur. + +"'In my own bureau.' + +"'Well, I hope to goodness the house won't be burgled during the +night.' said he. + +"'It is locked up,' I answered. + +"'Oh, any old key will fit that bureau. When I was a youngster I +have opened it myself with the key of the box-room cupboard.' + +"He often had a wild way of talking, so that I thought little of +what he said. He followed me to my room, however, that night with +a very grave face. + +"'Look here, dad,' said he with his eyes cast down, 'can you let +me have 200 pounds?' + +"'No, I cannot!' I answered sharply. 'I have been far too +generous with you in money matters.' + +"'You have been very kind,' said he, 'but I must have this money, +or else I can never show my face inside the club again.' + +"'And a very good thing, too!' I cried. + +"'Yes, but you would not have me leave it a dishonoured man,' +said he. 'I could not bear the disgrace. I must raise the money +in some way, and if you will not let me have it, then I must try +other means.' + +"I was very angry, for this was the third demand during the +month. 'You shall not have a farthing from me,' I cried, on which +he bowed and left the room without another word. + +"When he was gone I unlocked my bureau, made sure that my +treasure was safe, and locked it again. Then I started to go +round the house to see that all was secure--a duty which I +usually leave to Mary but which I thought it well to perform +myself that night. As I came down the stairs I saw Mary herself +at the side window of the hall, which she closed and fastened as +I approached. + +"'Tell me, dad,' said she, looking, I thought, a little +disturbed, 'did you give Lucy, the maid, leave to go out +to-night?' + +"'Certainly not.' + +"'She came in just now by the back door. I have no doubt that she +has only been to the side gate to see someone, but I think that +it is hardly safe and should be stopped.' + +"'You must speak to her in the morning, or I will if you prefer +it. Are you sure that everything is fastened?' + +"'Quite sure, dad.' + +"'Then, good-night.' I kissed her and went up to my bedroom +again, where I was soon asleep. + +"I am endeavouring to tell you everything, Mr. Holmes, which may +have any bearing upon the case, but I beg that you will question +me upon any point which I do not make clear." + +"On the contrary, your statement is singularly lucid." + +"I come to a part of my story now in which I should wish to be +particularly so. I am not a very heavy sleeper, and the anxiety +in my mind tended, no doubt, to make me even less so than usual. +About two in the morning, then, I was awakened by some sound in +the house. It had ceased ere I was wide awake, but it had left an +impression behind it as though a window had gently closed +somewhere. I lay listening with all my ears. Suddenly, to my +horror, there was a distinct sound of footsteps moving softly in +the next room. I slipped out of bed, all palpitating with fear, +and peeped round the corner of my dressing-room door. + +"'Arthur!' I screamed, 'you villain! you thief! How dare you +touch that coronet?' + +"The gas was half up, as I had left it, and my unhappy boy, +dressed only in his shirt and trousers, was standing beside the +light, holding the coronet in his hands. He appeared to be +wrenching at it, or bending it with all his strength. At my cry +he dropped it from his grasp and turned as pale as death. I +snatched it up and examined it. One of the gold corners, with +three of the beryls in it, was missing. + +"'You blackguard!' I shouted, beside myself with rage. 'You have +destroyed it! You have dishonoured me forever! Where are the +jewels which you have stolen?' + +"'Stolen!' he cried. + +"'Yes, thief!' I roared, shaking him by the shoulder. + +"'There are none missing. There cannot be any missing,' said he. + +"'There are three missing. And you know where they are. Must I +call you a liar as well as a thief? Did I not see you trying to +tear off another piece?' + +"'You have called me names enough,' said he, 'I will not stand it +any longer. I shall not say another word about this business, +since you have chosen to insult me. I will leave your house in +the morning and make my own way in the world.' + +"'You shall leave it in the hands of the police!' I cried +half-mad with grief and rage. 'I shall have this matter probed to +the bottom.' + +"'You shall learn nothing from me,' said he with a passion such +as I should not have thought was in his nature. 'If you choose to +call the police, let the police find what they can.' + +"By this time the whole house was astir, for I had raised my +voice in my anger. Mary was the first to rush into my room, and, +at the sight of the coronet and of Arthur's face, she read the +whole story and, with a scream, fell down senseless on the +ground. I sent the house-maid for the police and put the +investigation into their hands at once. When the inspector and a +constable entered the house, Arthur, who had stood sullenly with +his arms folded, asked me whether it was my intention to charge +him with theft. I answered that it had ceased to be a private +matter, but had become a public one, since the ruined coronet was +national property. I was determined that the law should have its +way in everything. + +"'At least,' said he, 'you will not have me arrested at once. It +would be to your advantage as well as mine if I might leave the +house for five minutes.' + +"'That you may get away, or perhaps that you may conceal what you +have stolen,' said I. And then, realising the dreadful position +in which I was placed, I implored him to remember that not only +my honour but that of one who was far greater than I was at +stake; and that he threatened to raise a scandal which would +convulse the nation. He might avert it all if he would but tell +me what he had done with the three missing stones. + +"'You may as well face the matter,' said I; 'you have been caught +in the act, and no confession could make your guilt more heinous. +If you but make such reparation as is in your power, by telling +us where the beryls are, all shall be forgiven and forgotten.' + +"'Keep your forgiveness for those who ask for it,' he answered, +turning away from me with a sneer. I saw that he was too hardened +for any words of mine to influence him. There was but one way for +it. I called in the inspector and gave him into custody. A search +was made at once not only of his person but of his room and of +every portion of the house where he could possibly have concealed +the gems; but no trace of them could be found, nor would the +wretched boy open his mouth for all our persuasions and our +threats. This morning he was removed to a cell, and I, after +going through all the police formalities, have hurried round to +you to implore you to use your skill in unravelling the matter. +The police have openly confessed that they can at present make +nothing of it. You may go to any expense which you think +necessary. I have already offered a reward of 1000 pounds. My +God, what shall I do! I have lost my honour, my gems, and my son +in one night. Oh, what shall I do!" + +He put a hand on either side of his head and rocked himself to +and fro, droning to himself like a child whose grief has got +beyond words. + +Sherlock Holmes sat silent for some few minutes, with his brows +knitted and his eyes fixed upon the fire. + +"Do you receive much company?" he asked. + +"None save my partner with his family and an occasional friend of +Arthur's. Sir George Burnwell has been several times lately. No +one else, I think." + +"Do you go out much in society?" + +"Arthur does. Mary and I stay at home. We neither of us care for +it." + +"That is unusual in a young girl." + +"She is of a quiet nature. Besides, she is not so very young. She +is four-and-twenty." + +"This matter, from what you say, seems to have been a shock to +her also." + +"Terrible! She is even more affected than I." + +"You have neither of you any doubt as to your son's guilt?" + +"How can we have when I saw him with my own eyes with the coronet +in his hands." + +"I hardly consider that a conclusive proof. Was the remainder of +the coronet at all injured?" + +"Yes, it was twisted." + +"Do you not think, then, that he might have been trying to +straighten it?" + +"God bless you! You are doing what you can for him and for me. +But it is too heavy a task. What was he doing there at all? If +his purpose were innocent, why did he not say so?" + +"Precisely. And if it were guilty, why did he not invent a lie? +His silence appears to me to cut both ways. There are several +singular points about the case. What did the police think of the +noise which awoke you from your sleep?" + +"They considered that it might be caused by Arthur's closing his +bedroom door." + +"A likely story! As if a man bent on felony would slam his door +so as to wake a household. What did they say, then, of the +disappearance of these gems?" + +"They are still sounding the planking and probing the furniture +in the hope of finding them." + +"Have they thought of looking outside the house?" + +"Yes, they have shown extraordinary energy. The whole garden has +already been minutely examined." + +"Now, my dear sir," said Holmes, "is it not obvious to you now +that this matter really strikes very much deeper than either you +or the police were at first inclined to think? It appeared to you +to be a simple case; to me it seems exceedingly complex. Consider +what is involved by your theory. You suppose that your son came +down from his bed, went, at great risk, to your dressing-room, +opened your bureau, took out your coronet, broke off by main +force a small portion of it, went off to some other place, +concealed three gems out of the thirty-nine, with such skill that +nobody can find them, and then returned with the other thirty-six +into the room in which he exposed himself to the greatest danger +of being discovered. I ask you now, is such a theory tenable?" + +"But what other is there?" cried the banker with a gesture of +despair. "If his motives were innocent, why does he not explain +them?" + +"It is our task to find that out," replied Holmes; "so now, if +you please, Mr. Holder, we will set off for Streatham together, +and devote an hour to glancing a little more closely into +details." + +My friend insisted upon my accompanying them in their expedition, +which I was eager enough to do, for my curiosity and sympathy +were deeply stirred by the story to which we had listened. I +confess that the guilt of the banker's son appeared to me to be +as obvious as it did to his unhappy father, but still I had such +faith in Holmes' judgment that I felt that there must be some +grounds for hope as long as he was dissatisfied with the accepted +explanation. He hardly spoke a word the whole way out to the +southern suburb, but sat with his chin upon his breast and his +hat drawn over his eyes, sunk in the deepest thought. Our client +appeared to have taken fresh heart at the little glimpse of hope +which had been presented to him, and he even broke into a +desultory chat with me over his business affairs. A short railway +journey and a shorter walk brought us to Fairbank, the modest +residence of the great financier. + +Fairbank was a good-sized square house of white stone, standing +back a little from the road. A double carriage-sweep, with a +snow-clad lawn, stretched down in front to two large iron gates +which closed the entrance. On the right side was a small wooden +thicket, which led into a narrow path between two neat hedges +stretching from the road to the kitchen door, and forming the +tradesmen's entrance. On the left ran a lane which led to the +stables, and was not itself within the grounds at all, being a +public, though little used, thoroughfare. Holmes left us standing +at the door and walked slowly all round the house, across the +front, down the tradesmen's path, and so round by the garden +behind into the stable lane. So long was he that Mr. Holder and I +went into the dining-room and waited by the fire until he should +return. We were sitting there in silence when the door opened and +a young lady came in. She was rather above the middle height, +slim, with dark hair and eyes, which seemed the darker against +the absolute pallor of her skin. I do not think that I have ever +seen such deadly paleness in a woman's face. Her lips, too, were +bloodless, but her eyes were flushed with crying. As she swept +silently into the room she impressed me with a greater sense of +grief than the banker had done in the morning, and it was the +more striking in her as she was evidently a woman of strong +character, with immense capacity for self-restraint. Disregarding +my presence, she went straight to her uncle and passed her hand +over his head with a sweet womanly caress. + +"You have given orders that Arthur should be liberated, have you +not, dad?" she asked. + +"No, no, my girl, the matter must be probed to the bottom." + +"But I am so sure that he is innocent. You know what woman's +instincts are. I know that he has done no harm and that you will +be sorry for having acted so harshly." + +"Why is he silent, then, if he is innocent?" + +"Who knows? Perhaps because he was so angry that you should +suspect him." + +"How could I help suspecting him, when I actually saw him with +the coronet in his hand?" + +"Oh, but he had only picked it up to look at it. Oh, do, do take +my word for it that he is innocent. Let the matter drop and say +no more. It is so dreadful to think of our dear Arthur in +prison!" + +"I shall never let it drop until the gems are found--never, Mary! +Your affection for Arthur blinds you as to the awful consequences +to me. Far from hushing the thing up, I have brought a gentleman +down from London to inquire more deeply into it." + +"This gentleman?" she asked, facing round to me. + +"No, his friend. He wished us to leave him alone. He is round in +the stable lane now." + +"The stable lane?" She raised her dark eyebrows. "What can he +hope to find there? Ah! this, I suppose, is he. I trust, sir, +that you will succeed in proving, what I feel sure is the truth, +that my cousin Arthur is innocent of this crime." + +"I fully share your opinion, and I trust, with you, that we may +prove it," returned Holmes, going back to the mat to knock the +snow from his shoes. "I believe I have the honour of addressing +Miss Mary Holder. Might I ask you a question or two?" + +"Pray do, sir, if it may help to clear this horrible affair up." + +"You heard nothing yourself last night?" + +"Nothing, until my uncle here began to speak loudly. I heard +that, and I came down." + +"You shut up the windows and doors the night before. Did you +fasten all the windows?" + +"Yes." + +"Were they all fastened this morning?" + +"Yes." + +"You have a maid who has a sweetheart? I think that you remarked +to your uncle last night that she had been out to see him?" + +"Yes, and she was the girl who waited in the drawing-room, and +who may have heard uncle's remarks about the coronet." + +"I see. You infer that she may have gone out to tell her +sweetheart, and that the two may have planned the robbery." + +"But what is the good of all these vague theories," cried the +banker impatiently, "when I have told you that I saw Arthur with +the coronet in his hands?" + +"Wait a little, Mr. Holder. We must come back to that. About this +girl, Miss Holder. You saw her return by the kitchen door, I +presume?" + +"Yes; when I went to see if the door was fastened for the night I +met her slipping in. I saw the man, too, in the gloom." + +"Do you know him?" + +"Oh, yes! he is the green-grocer who brings our vegetables round. +His name is Francis Prosper." + +"He stood," said Holmes, "to the left of the door--that is to +say, farther up the path than is necessary to reach the door?" + +"Yes, he did." + +"And he is a man with a wooden leg?" + +Something like fear sprang up in the young lady's expressive +black eyes. "Why, you are like a magician," said she. "How do you +know that?" She smiled, but there was no answering smile in +Holmes' thin, eager face. + +"I should be very glad now to go upstairs," said he. "I shall +probably wish to go over the outside of the house again. Perhaps +I had better take a look at the lower windows before I go up." + +He walked swiftly round from one to the other, pausing only at +the large one which looked from the hall onto the stable lane. +This he opened and made a very careful examination of the sill +with his powerful magnifying lens. "Now we shall go upstairs," +said he at last. + +The banker's dressing-room was a plainly furnished little +chamber, with a grey carpet, a large bureau, and a long mirror. +Holmes went to the bureau first and looked hard at the lock. + +"Which key was used to open it?" he asked. + +"That which my son himself indicated--that of the cupboard of the +lumber-room." + +"Have you it here?" + +"That is it on the dressing-table." + +Sherlock Holmes took it up and opened the bureau. + +"It is a noiseless lock," said he. "It is no wonder that it did +not wake you. This case, I presume, contains the coronet. We must +have a look at it." He opened the case, and taking out the diadem +he laid it upon the table. It was a magnificent specimen of the +jeweller's art, and the thirty-six stones were the finest that I +have ever seen. At one side of the coronet was a cracked edge, +where a corner holding three gems had been torn away. + +"Now, Mr. Holder," said Holmes, "here is the corner which +corresponds to that which has been so unfortunately lost. Might I +beg that you will break it off." + +The banker recoiled in horror. "I should not dream of trying," +said he. + +"Then I will." Holmes suddenly bent his strength upon it, but +without result. "I feel it give a little," said he; "but, though +I am exceptionally strong in the fingers, it would take me all my +time to break it. An ordinary man could not do it. Now, what do +you think would happen if I did break it, Mr. Holder? There would +be a noise like a pistol shot. Do you tell me that all this +happened within a few yards of your bed and that you heard +nothing of it?" + +"I do not know what to think. It is all dark to me." + +"But perhaps it may grow lighter as we go. What do you think, +Miss Holder?" + +"I confess that I still share my uncle's perplexity." + +"Your son had no shoes or slippers on when you saw him?" + +"He had nothing on save only his trousers and shirt." + +"Thank you. We have certainly been favoured with extraordinary +luck during this inquiry, and it will be entirely our own fault +if we do not succeed in clearing the matter up. With your +permission, Mr. Holder, I shall now continue my investigations +outside." + +He went alone, at his own request, for he explained that any +unnecessary footmarks might make his task more difficult. For an +hour or more he was at work, returning at last with his feet +heavy with snow and his features as inscrutable as ever. + +"I think that I have seen now all that there is to see, Mr. +Holder," said he; "I can serve you best by returning to my +rooms." + +"But the gems, Mr. Holmes. Where are they?" + +"I cannot tell." + +The banker wrung his hands. "I shall never see them again!" he +cried. "And my son? You give me hopes?" + +"My opinion is in no way altered." + +"Then, for God's sake, what was this dark business which was +acted in my house last night?" + +"If you can call upon me at my Baker Street rooms to-morrow +morning between nine and ten I shall be happy to do what I can to +make it clearer. I understand that you give me carte blanche to +act for you, provided only that I get back the gems, and that you +place no limit on the sum I may draw." + +"I would give my fortune to have them back." + +"Very good. I shall look into the matter between this and then. +Good-bye; it is just possible that I may have to come over here +again before evening." + +It was obvious to me that my companion's mind was now made up +about the case, although what his conclusions were was more than +I could even dimly imagine. Several times during our homeward +journey I endeavoured to sound him upon the point, but he always +glided away to some other topic, until at last I gave it over in +despair. It was not yet three when we found ourselves in our +rooms once more. He hurried to his chamber and was down again in +a few minutes dressed as a common loafer. With his collar turned +up, his shiny, seedy coat, his red cravat, and his worn boots, he +was a perfect sample of the class. + +"I think that this should do," said he, glancing into the glass +above the fireplace. "I only wish that you could come with me, +Watson, but I fear that it won't do. I may be on the trail in +this matter, or I may be following a will-o'-the-wisp, but I +shall soon know which it is. I hope that I may be back in a few +hours." He cut a slice of beef from the joint upon the sideboard, +sandwiched it between two rounds of bread, and thrusting this +rude meal into his pocket he started off upon his expedition. + +I had just finished my tea when he returned, evidently in +excellent spirits, swinging an old elastic-sided boot in his +hand. He chucked it down into a corner and helped himself to a +cup of tea. + +"I only looked in as I passed," said he. "I am going right on." + +"Where to?" + +"Oh, to the other side of the West End. It may be some time +before I get back. Don't wait up for me in case I should be +late." + +"How are you getting on?" + +"Oh, so so. Nothing to complain of. I have been out to Streatham +since I saw you last, but I did not call at the house. It is a +very sweet little problem, and I would not have missed it for a +good deal. However, I must not sit gossiping here, but must get +these disreputable clothes off and return to my highly +respectable self." + +I could see by his manner that he had stronger reasons for +satisfaction than his words alone would imply. His eyes twinkled, +and there was even a touch of colour upon his sallow cheeks. He +hastened upstairs, and a few minutes later I heard the slam of +the hall door, which told me that he was off once more upon his +congenial hunt. + +I waited until midnight, but there was no sign of his return, so +I retired to my room. It was no uncommon thing for him to be away +for days and nights on end when he was hot upon a scent, so that +his lateness caused me no surprise. I do not know at what hour he +came in, but when I came down to breakfast in the morning there +he was with a cup of coffee in one hand and the paper in the +other, as fresh and trim as possible. + +"You will excuse my beginning without you, Watson," said he, "but +you remember that our client has rather an early appointment this +morning." + +"Why, it is after nine now," I answered. "I should not be +surprised if that were he. I thought I heard a ring." + +It was, indeed, our friend the financier. I was shocked by the +change which had come over him, for his face which was naturally +of a broad and massive mould, was now pinched and fallen in, +while his hair seemed to me at least a shade whiter. He entered +with a weariness and lethargy which was even more painful than +his violence of the morning before, and he dropped heavily into +the armchair which I pushed forward for him. + +"I do not know what I have done to be so severely tried," said +he. "Only two days ago I was a happy and prosperous man, without +a care in the world. Now I am left to a lonely and dishonoured +age. One sorrow comes close upon the heels of another. My niece, +Mary, has deserted me." + +"Deserted you?" + +"Yes. Her bed this morning had not been slept in, her room was +empty, and a note for me lay upon the hall table. I had said to +her last night, in sorrow and not in anger, that if she had +married my boy all might have been well with him. Perhaps it was +thoughtless of me to say so. It is to that remark that she refers +in this note: + +"'MY DEAREST UNCLE:--I feel that I have brought trouble upon you, +and that if I had acted differently this terrible misfortune +might never have occurred. I cannot, with this thought in my +mind, ever again be happy under your roof, and I feel that I must +leave you forever. Do not worry about my future, for that is +provided for; and, above all, do not search for me, for it will +be fruitless labour and an ill-service to me. In life or in +death, I am ever your loving,--MARY.' + +"What could she mean by that note, Mr. Holmes? Do you think it +points to suicide?" + +"No, no, nothing of the kind. It is perhaps the best possible +solution. I trust, Mr. Holder, that you are nearing the end of +your troubles." + +"Ha! You say so! You have heard something, Mr. Holmes; you have +learned something! Where are the gems?" + +"You would not think 1000 pounds apiece an excessive sum for +them?" + +"I would pay ten." + +"That would be unnecessary. Three thousand will cover the matter. +And there is a little reward, I fancy. Have you your check-book? +Here is a pen. Better make it out for 4000 pounds." + +With a dazed face the banker made out the required check. Holmes +walked over to his desk, took out a little triangular piece of +gold with three gems in it, and threw it down upon the table. + +With a shriek of joy our client clutched it up. + +"You have it!" he gasped. "I am saved! I am saved!" + +The reaction of joy was as passionate as his grief had been, and +he hugged his recovered gems to his bosom. + +"There is one other thing you owe, Mr. Holder," said Sherlock +Holmes rather sternly. + +"Owe!" He caught up a pen. "Name the sum, and I will pay it." + +"No, the debt is not to me. You owe a very humble apology to that +noble lad, your son, who has carried himself in this matter as I +should be proud to see my own son do, should I ever chance to +have one." + +"Then it was not Arthur who took them?" + +"I told you yesterday, and I repeat to-day, that it was not." + +"You are sure of it! Then let us hurry to him at once to let him +know that the truth is known." + +"He knows it already. When I had cleared it all up I had an +interview with him, and finding that he would not tell me the +story, I told it to him, on which he had to confess that I was +right and to add the very few details which were not yet quite +clear to me. Your news of this morning, however, may open his +lips." + +"For heaven's sake, tell me, then, what is this extraordinary +mystery!" + +"I will do so, and I will show you the steps by which I reached +it. And let me say to you, first, that which it is hardest for me +to say and for you to hear: there has been an understanding +between Sir George Burnwell and your niece Mary. They have now +fled together." + +"My Mary? Impossible!" + +"It is unfortunately more than possible; it is certain. Neither +you nor your son knew the true character of this man when you +admitted him into your family circle. He is one of the most +dangerous men in England--a ruined gambler, an absolutely +desperate villain, a man without heart or conscience. Your niece +knew nothing of such men. When he breathed his vows to her, as he +had done to a hundred before her, she flattered herself that she +alone had touched his heart. The devil knows best what he said, +but at least she became his tool and was in the habit of seeing +him nearly every evening." + +"I cannot, and I will not, believe it!" cried the banker with an +ashen face. + +"I will tell you, then, what occurred in your house last night. +Your niece, when you had, as she thought, gone to your room, +slipped down and talked to her lover through the window which +leads into the stable lane. His footmarks had pressed right +through the snow, so long had he stood there. She told him of the +coronet. His wicked lust for gold kindled at the news, and he +bent her to his will. I have no doubt that she loved you, but +there are women in whom the love of a lover extinguishes all +other loves, and I think that she must have been one. She had +hardly listened to his instructions when she saw you coming +downstairs, on which she closed the window rapidly and told you +about one of the servants' escapade with her wooden-legged lover, +which was all perfectly true. + +"Your boy, Arthur, went to bed after his interview with you but +he slept badly on account of his uneasiness about his club debts. +In the middle of the night he heard a soft tread pass his door, +so he rose and, looking out, was surprised to see his cousin +walking very stealthily along the passage until she disappeared +into your dressing-room. Petrified with astonishment, the lad +slipped on some clothes and waited there in the dark to see what +would come of this strange affair. Presently she emerged from the +room again, and in the light of the passage-lamp your son saw +that she carried the precious coronet in her hands. She passed +down the stairs, and he, thrilling with horror, ran along and +slipped behind the curtain near your door, whence he could see +what passed in the hall beneath. He saw her stealthily open the +window, hand out the coronet to someone in the gloom, and then +closing it once more hurry back to her room, passing quite close +to where he stood hid behind the curtain. + +"As long as she was on the scene he could not take any action +without a horrible exposure of the woman whom he loved. But the +instant that she was gone he realised how crushing a misfortune +this would be for you, and how all-important it was to set it +right. He rushed down, just as he was, in his bare feet, opened +the window, sprang out into the snow, and ran down the lane, +where he could see a dark figure in the moonlight. Sir George +Burnwell tried to get away, but Arthur caught him, and there was +a struggle between them, your lad tugging at one side of the +coronet, and his opponent at the other. In the scuffle, your son +struck Sir George and cut him over the eye. Then something +suddenly snapped, and your son, finding that he had the coronet +in his hands, rushed back, closed the window, ascended to your +room, and had just observed that the coronet had been twisted in +the struggle and was endeavouring to straighten it when you +appeared upon the scene." + +"Is it possible?" gasped the banker. + +"You then roused his anger by calling him names at a moment when +he felt that he had deserved your warmest thanks. He could not +explain the true state of affairs without betraying one who +certainly deserved little enough consideration at his hands. He +took the more chivalrous view, however, and preserved her +secret." + +"And that was why she shrieked and fainted when she saw the +coronet," cried Mr. Holder. "Oh, my God! what a blind fool I have +been! And his asking to be allowed to go out for five minutes! +The dear fellow wanted to see if the missing piece were at the +scene of the struggle. How cruelly I have misjudged him!" + +"When I arrived at the house," continued Holmes, "I at once went +very carefully round it to observe if there were any traces in +the snow which might help me. I knew that none had fallen since +the evening before, and also that there had been a strong frost +to preserve impressions. I passed along the tradesmen's path, but +found it all trampled down and indistinguishable. Just beyond it, +however, at the far side of the kitchen door, a woman had stood +and talked with a man, whose round impressions on one side showed +that he had a wooden leg. I could even tell that they had been +disturbed, for the woman had run back swiftly to the door, as was +shown by the deep toe and light heel marks, while Wooden-leg had +waited a little, and then had gone away. I thought at the time +that this might be the maid and her sweetheart, of whom you had +already spoken to me, and inquiry showed it was so. I passed +round the garden without seeing anything more than random tracks, +which I took to be the police; but when I got into the stable +lane a very long and complex story was written in the snow in +front of me. + +"There was a double line of tracks of a booted man, and a second +double line which I saw with delight belonged to a man with naked +feet. I was at once convinced from what you had told me that the +latter was your son. The first had walked both ways, but the +other had run swiftly, and as his tread was marked in places over +the depression of the boot, it was obvious that he had passed +after the other. I followed them up and found they led to the +hall window, where Boots had worn all the snow away while +waiting. Then I walked to the other end, which was a hundred +yards or more down the lane. I saw where Boots had faced round, +where the snow was cut up as though there had been a struggle, +and, finally, where a few drops of blood had fallen, to show me +that I was not mistaken. Boots had then run down the lane, and +another little smudge of blood showed that it was he who had been +hurt. When he came to the highroad at the other end, I found that +the pavement had been cleared, so there was an end to that clue. + +"On entering the house, however, I examined, as you remember, the +sill and framework of the hall window with my lens, and I could +at once see that someone had passed out. I could distinguish the +outline of an instep where the wet foot had been placed in coming +in. I was then beginning to be able to form an opinion as to what +had occurred. A man had waited outside the window; someone had +brought the gems; the deed had been overseen by your son; he had +pursued the thief; had struggled with him; they had each tugged +at the coronet, their united strength causing injuries which +neither alone could have effected. He had returned with the +prize, but had left a fragment in the grasp of his opponent. So +far I was clear. The question now was, who was the man and who +was it brought him the coronet? + +"It is an old maxim of mine that when you have excluded the +impossible, whatever remains, however improbable, must be the +truth. Now, I knew that it was not you who had brought it down, +so there only remained your niece and the maids. But if it were +the maids, why should your son allow himself to be accused in +their place? There could be no possible reason. As he loved his +cousin, however, there was an excellent explanation why he should +retain her secret--the more so as the secret was a disgraceful +one. When I remembered that you had seen her at that window, and +how she had fainted on seeing the coronet again, my conjecture +became a certainty. + +"And who could it be who was her confederate? A lover evidently, +for who else could outweigh the love and gratitude which she must +feel to you? I knew that you went out little, and that your +circle of friends was a very limited one. But among them was Sir +George Burnwell. I had heard of him before as being a man of evil +reputation among women. It must have been he who wore those boots +and retained the missing gems. Even though he knew that Arthur +had discovered him, he might still flatter himself that he was +safe, for the lad could not say a word without compromising his +own family. + +"Well, your own good sense will suggest what measures I took +next. I went in the shape of a loafer to Sir George's house, +managed to pick up an acquaintance with his valet, learned that +his master had cut his head the night before, and, finally, at +the expense of six shillings, made all sure by buying a pair of +his cast-off shoes. With these I journeyed down to Streatham and +saw that they exactly fitted the tracks." + +"I saw an ill-dressed vagabond in the lane yesterday evening," +said Mr. Holder. + +"Precisely. It was I. I found that I had my man, so I came home +and changed my clothes. It was a delicate part which I had to +play then, for I saw that a prosecution must be avoided to avert +scandal, and I knew that so astute a villain would see that our +hands were tied in the matter. I went and saw him. At first, of +course, he denied everything. But when I gave him every +particular that had occurred, he tried to bluster and took down a +life-preserver from the wall. I knew my man, however, and I +clapped a pistol to his head before he could strike. Then he +became a little more reasonable. I told him that we would give +him a price for the stones he held--1000 pounds apiece. That +brought out the first signs of grief that he had shown. 'Why, +dash it all!' said he, 'I've let them go at six hundred for the +three!' I soon managed to get the address of the receiver who had +them, on promising him that there would be no prosecution. Off I +set to him, and after much chaffering I got our stones at 1000 +pounds apiece. Then I looked in upon your son, told him that all +was right, and eventually got to my bed about two o'clock, after +what I may call a really hard day's work." + +"A day which has saved England from a great public scandal," said +the banker, rising. "Sir, I cannot find words to thank you, but +you shall not find me ungrateful for what you have done. Your +skill has indeed exceeded all that I have heard of it. And now I +must fly to my dear boy to apologise to him for the wrong which I +have done him. As to what you tell me of poor Mary, it goes to my +very heart. Not even your skill can inform me where she is now." + +"I think that we may safely say," returned Holmes, "that she is +wherever Sir George Burnwell is. It is equally certain, too, that +whatever her sins are, they will soon receive a more than +sufficient punishment." + + + +XII. THE ADVENTURE OF THE COPPER BEECHES + +"To the man who loves art for its own sake," remarked Sherlock +Holmes, tossing aside the advertisement sheet of the Daily +Telegraph, "it is frequently in its least important and lowliest +manifestations that the keenest pleasure is to be derived. It is +pleasant to me to observe, Watson, that you have so far grasped +this truth that in these little records of our cases which you +have been good enough to draw up, and, I am bound to say, +occasionally to embellish, you have given prominence not so much +to the many causes clbres and sensational trials in which I +have figured but rather to those incidents which may have been +trivial in themselves, but which have given room for those +faculties of deduction and of logical synthesis which I have made +my special province." + +"And yet," said I, smiling, "I cannot quite hold myself absolved +from the charge of sensationalism which has been urged against my +records." + +"You have erred, perhaps," he observed, taking up a glowing +cinder with the tongs and lighting with it the long cherry-wood +pipe which was wont to replace his clay when he was in a +disputatious rather than a meditative mood--"you have erred +perhaps in attempting to put colour and life into each of your +statements instead of confining yourself to the task of placing +upon record that severe reasoning from cause to effect which is +really the only notable feature about the thing." + +"It seems to me that I have done you full justice in the matter," +I remarked with some coldness, for I was repelled by the egotism +which I had more than once observed to be a strong factor in my +friend's singular character. + +"No, it is not selfishness or conceit," said he, answering, as +was his wont, my thoughts rather than my words. "If I claim full +justice for my art, it is because it is an impersonal thing--a +thing beyond myself. Crime is common. Logic is rare. Therefore it +is upon the logic rather than upon the crime that you should +dwell. You have degraded what should have been a course of +lectures into a series of tales." + +It was a cold morning of the early spring, and we sat after +breakfast on either side of a cheery fire in the old room at +Baker Street. A thick fog rolled down between the lines of +dun-coloured houses, and the opposing windows loomed like dark, +shapeless blurs through the heavy yellow wreaths. Our gas was lit +and shone on the white cloth and glimmer of china and metal, for +the table had not been cleared yet. Sherlock Holmes had been +silent all the morning, dipping continuously into the +advertisement columns of a succession of papers until at last, +having apparently given up his search, he had emerged in no very +sweet temper to lecture me upon my literary shortcomings. + +"At the same time," he remarked after a pause, during which he +had sat puffing at his long pipe and gazing down into the fire, +"you can hardly be open to a charge of sensationalism, for out of +these cases which you have been so kind as to interest yourself +in, a fair proportion do not treat of crime, in its legal sense, +at all. The small matter in which I endeavoured to help the King +of Bohemia, the singular experience of Miss Mary Sutherland, the +problem connected with the man with the twisted lip, and the +incident of the noble bachelor, were all matters which are +outside the pale of the law. But in avoiding the sensational, I +fear that you may have bordered on the trivial." + +"The end may have been so," I answered, "but the methods I hold +to have been novel and of interest." + +"Pshaw, my dear fellow, what do the public, the great unobservant +public, who could hardly tell a weaver by his tooth or a +compositor by his left thumb, care about the finer shades of +analysis and deduction! But, indeed, if you are trivial, I cannot +blame you, for the days of the great cases are past. Man, or at +least criminal man, has lost all enterprise and originality. As +to my own little practice, it seems to be degenerating into an +agency for recovering lost lead pencils and giving advice to +young ladies from boarding-schools. I think that I have touched +bottom at last, however. This note I had this morning marks my +zero-point, I fancy. Read it!" He tossed a crumpled letter across +to me. + +It was dated from Montague Place upon the preceding evening, and +ran thus: + +"DEAR MR. HOLMES:--I am very anxious to consult you as to whether +I should or should not accept a situation which has been offered +to me as governess. I shall call at half-past ten to-morrow if I +do not inconvenience you. Yours faithfully, + "VIOLET HUNTER." + +"Do you know the young lady?" I asked. + +"Not I." + +"It is half-past ten now." + +"Yes, and I have no doubt that is her ring." + +"It may turn out to be of more interest than you think. You +remember that the affair of the blue carbuncle, which appeared to +be a mere whim at first, developed into a serious investigation. +It may be so in this case, also." + +"Well, let us hope so. But our doubts will very soon be solved, +for here, unless I am much mistaken, is the person in question." + +As he spoke the door opened and a young lady entered the room. +She was plainly but neatly dressed, with a bright, quick face, +freckled like a plover's egg, and with the brisk manner of a +woman who has had her own way to make in the world. + +"You will excuse my troubling you, I am sure," said she, as my +companion rose to greet her, "but I have had a very strange +experience, and as I have no parents or relations of any sort +from whom I could ask advice, I thought that perhaps you would be +kind enough to tell me what I should do." + +"Pray take a seat, Miss Hunter. I shall be happy to do anything +that I can to serve you." + +I could see that Holmes was favourably impressed by the manner +and speech of his new client. He looked her over in his searching +fashion, and then composed himself, with his lids drooping and +his finger-tips together, to listen to her story. + +"I have been a governess for five years," said she, "in the +family of Colonel Spence Munro, but two months ago the colonel +received an appointment at Halifax, in Nova Scotia, and took his +children over to America with him, so that I found myself without +a situation. I advertised, and I answered advertisements, but +without success. At last the little money which I had saved began +to run short, and I was at my wit's end as to what I should do. + +"There is a well-known agency for governesses in the West End +called Westaway's, and there I used to call about once a week in +order to see whether anything had turned up which might suit me. +Westaway was the name of the founder of the business, but it is +really managed by Miss Stoper. She sits in her own little office, +and the ladies who are seeking employment wait in an anteroom, +and are then shown in one by one, when she consults her ledgers +and sees whether she has anything which would suit them. + +"Well, when I called last week I was shown into the little office +as usual, but I found that Miss Stoper was not alone. A +prodigiously stout man with a very smiling face and a great heavy +chin which rolled down in fold upon fold over his throat sat at +her elbow with a pair of glasses on his nose, looking very +earnestly at the ladies who entered. As I came in he gave quite a +jump in his chair and turned quickly to Miss Stoper. + +"'That will do,' said he; 'I could not ask for anything better. +Capital! capital!' He seemed quite enthusiastic and rubbed his +hands together in the most genial fashion. He was such a +comfortable-looking man that it was quite a pleasure to look at +him. + +"'You are looking for a situation, miss?' he asked. + +"'Yes, sir.' + +"'As governess?' + +"'Yes, sir.' + +"'And what salary do you ask?' + +"'I had 4 pounds a month in my last place with Colonel Spence +Munro.' + +"'Oh, tut, tut! sweating--rank sweating!' he cried, throwing his +fat hands out into the air like a man who is in a boiling +passion. 'How could anyone offer so pitiful a sum to a lady with +such attractions and accomplishments?' + +"'My accomplishments, sir, may be less than you imagine,' said I. +'A little French, a little German, music, and drawing--' + +"'Tut, tut!' he cried. 'This is all quite beside the question. +The point is, have you or have you not the bearing and deportment +of a lady? There it is in a nutshell. If you have not, you are +not fitted for the rearing of a child who may some day play a +considerable part in the history of the country. But if you have +why, then, how could any gentleman ask you to condescend to +accept anything under the three figures? Your salary with me, +madam, would commence at 100 pounds a year.' + +"You may imagine, Mr. Holmes, that to me, destitute as I was, +such an offer seemed almost too good to be true. The gentleman, +however, seeing perhaps the look of incredulity upon my face, +opened a pocket-book and took out a note. + +"'It is also my custom,' said he, smiling in the most pleasant +fashion until his eyes were just two little shining slits amid +the white creases of his face, 'to advance to my young ladies +half their salary beforehand, so that they may meet any little +expenses of their journey and their wardrobe.' + +"It seemed to me that I had never met so fascinating and so +thoughtful a man. As I was already in debt to my tradesmen, the +advance was a great convenience, and yet there was something +unnatural about the whole transaction which made me wish to know +a little more before I quite committed myself. + +"'May I ask where you live, sir?' said I. + +"'Hampshire. Charming rural place. The Copper Beeches, five miles +on the far side of Winchester. It is the most lovely country, my +dear young lady, and the dearest old country-house.' + +"'And my duties, sir? I should be glad to know what they would +be.' + +"'One child--one dear little romper just six years old. Oh, if +you could see him killing cockroaches with a slipper! Smack! +smack! smack! Three gone before you could wink!' He leaned back +in his chair and laughed his eyes into his head again. + +"I was a little startled at the nature of the child's amusement, +but the father's laughter made me think that perhaps he was +joking. + +"'My sole duties, then,' I asked, 'are to take charge of a single +child?' + +"'No, no, not the sole, not the sole, my dear young lady,' he +cried. 'Your duty would be, as I am sure your good sense would +suggest, to obey any little commands my wife might give, provided +always that they were such commands as a lady might with +propriety obey. You see no difficulty, heh?' + +"'I should be happy to make myself useful.' + +"'Quite so. In dress now, for example. We are faddy people, you +know--faddy but kind-hearted. If you were asked to wear any dress +which we might give you, you would not object to our little whim. +Heh?' + +"'No,' said I, considerably astonished at his words. + +"'Or to sit here, or sit there, that would not be offensive to +you?' + +"'Oh, no.' + +"'Or to cut your hair quite short before you come to us?' + +"I could hardly believe my ears. As you may observe, Mr. Holmes, +my hair is somewhat luxuriant, and of a rather peculiar tint of +chestnut. It has been considered artistic. I could not dream of +sacrificing it in this offhand fashion. + +"'I am afraid that that is quite impossible,' said I. He had been +watching me eagerly out of his small eyes, and I could see a +shadow pass over his face as I spoke. + +"'I am afraid that it is quite essential,' said he. 'It is a +little fancy of my wife's, and ladies' fancies, you know, madam, +ladies' fancies must be consulted. And so you won't cut your +hair?' + +"'No, sir, I really could not,' I answered firmly. + +"'Ah, very well; then that quite settles the matter. It is a +pity, because in other respects you would really have done very +nicely. In that case, Miss Stoper, I had best inspect a few more +of your young ladies.' + +"The manageress had sat all this while busy with her papers +without a word to either of us, but she glanced at me now with so +much annoyance upon her face that I could not help suspecting +that she had lost a handsome commission through my refusal. + +"'Do you desire your name to be kept upon the books?' she asked. + +"'If you please, Miss Stoper.' + +"'Well, really, it seems rather useless, since you refuse the +most excellent offers in this fashion,' said she sharply. 'You +can hardly expect us to exert ourselves to find another such +opening for you. Good-day to you, Miss Hunter.' She struck a gong +upon the table, and I was shown out by the page. + +"Well, Mr. Holmes, when I got back to my lodgings and found +little enough in the cupboard, and two or three bills upon the +table, I began to ask myself whether I had not done a very +foolish thing. After all, if these people had strange fads and +expected obedience on the most extraordinary matters, they were +at least ready to pay for their eccentricity. Very few +governesses in England are getting 100 pounds a year. Besides, +what use was my hair to me? Many people are improved by wearing +it short and perhaps I should be among the number. Next day I was +inclined to think that I had made a mistake, and by the day after +I was sure of it. I had almost overcome my pride so far as to go +back to the agency and inquire whether the place was still open +when I received this letter from the gentleman himself. I have it +here and I will read it to you: + + "'The Copper Beeches, near Winchester. +"'DEAR MISS HUNTER:--Miss Stoper has very kindly given me your +address, and I write from here to ask you whether you have +reconsidered your decision. My wife is very anxious that you +should come, for she has been much attracted by my description of +you. We are willing to give 30 pounds a quarter, or 120 pounds a +year, so as to recompense you for any little inconvenience which +our fads may cause you. They are not very exacting, after all. My +wife is fond of a particular shade of electric blue and would +like you to wear such a dress indoors in the morning. You need +not, however, go to the expense of purchasing one, as we have one +belonging to my dear daughter Alice (now in Philadelphia), which +would, I should think, fit you very well. Then, as to sitting +here or there, or amusing yourself in any manner indicated, that +need cause you no inconvenience. As regards your hair, it is no +doubt a pity, especially as I could not help remarking its beauty +during our short interview, but I am afraid that I must remain +firm upon this point, and I only hope that the increased salary +may recompense you for the loss. Your duties, as far as the child +is concerned, are very light. Now do try to come, and I shall +meet you with the dog-cart at Winchester. Let me know your train. +Yours faithfully, JEPHRO RUCASTLE.' + +"That is the letter which I have just received, Mr. Holmes, and +my mind is made up that I will accept it. I thought, however, +that before taking the final step I should like to submit the +whole matter to your consideration." + +"Well, Miss Hunter, if your mind is made up, that settles the +question," said Holmes, smiling. + +"But you would not advise me to refuse?" + +"I confess that it is not the situation which I should like to +see a sister of mine apply for." + +"What is the meaning of it all, Mr. Holmes?" + +"Ah, I have no data. I cannot tell. Perhaps you have yourself +formed some opinion?" + +"Well, there seems to me to be only one possible solution. Mr. +Rucastle seemed to be a very kind, good-natured man. Is it not +possible that his wife is a lunatic, that he desires to keep the +matter quiet for fear she should be taken to an asylum, and that +he humours her fancies in every way in order to prevent an +outbreak?" + +"That is a possible solution--in fact, as matters stand, it is +the most probable one. But in any case it does not seem to be a +nice household for a young lady." + +"But the money, Mr. Holmes, the money!" + +"Well, yes, of course the pay is good--too good. That is what +makes me uneasy. Why should they give you 120 pounds a year, when +they could have their pick for 40 pounds? There must be some +strong reason behind." + +"I thought that if I told you the circumstances you would +understand afterwards if I wanted your help. I should feel so +much stronger if I felt that you were at the back of me." + +"Oh, you may carry that feeling away with you. I assure you that +your little problem promises to be the most interesting which has +come my way for some months. There is something distinctly novel +about some of the features. If you should find yourself in doubt +or in danger--" + +"Danger! What danger do you foresee?" + +Holmes shook his head gravely. "It would cease to be a danger if +we could define it," said he. "But at any time, day or night, a +telegram would bring me down to your help." + +"That is enough." She rose briskly from her chair with the +anxiety all swept from her face. "I shall go down to Hampshire +quite easy in my mind now. I shall write to Mr. Rucastle at once, +sacrifice my poor hair to-night, and start for Winchester +to-morrow." With a few grateful words to Holmes she bade us both +good-night and bustled off upon her way. + +"At least," said I as we heard her quick, firm steps descending +the stairs, "she seems to be a young lady who is very well able +to take care of herself." + +"And she would need to be," said Holmes gravely. "I am much +mistaken if we do not hear from her before many days are past." + +It was not very long before my friend's prediction was fulfilled. +A fortnight went by, during which I frequently found my thoughts +turning in her direction and wondering what strange side-alley of +human experience this lonely woman had strayed into. The unusual +salary, the curious conditions, the light duties, all pointed to +something abnormal, though whether a fad or a plot, or whether +the man were a philanthropist or a villain, it was quite beyond +my powers to determine. As to Holmes, I observed that he sat +frequently for half an hour on end, with knitted brows and an +abstracted air, but he swept the matter away with a wave of his +hand when I mentioned it. "Data! data! data!" he cried +impatiently. "I can't make bricks without clay." And yet he would +always wind up by muttering that no sister of his should ever +have accepted such a situation. + +The telegram which we eventually received came late one night +just as I was thinking of turning in and Holmes was settling down +to one of those all-night chemical researches which he frequently +indulged in, when I would leave him stooping over a retort and a +test-tube at night and find him in the same position when I came +down to breakfast in the morning. He opened the yellow envelope, +and then, glancing at the message, threw it across to me. + +"Just look up the trains in Bradshaw," said he, and turned back +to his chemical studies. + +The summons was a brief and urgent one. + +"Please be at the Black Swan Hotel at Winchester at midday +to-morrow," it said. "Do come! I am at my wit's end. HUNTER." + +"Will you come with me?" asked Holmes, glancing up. + +"I should wish to." + +"Just look it up, then." + +"There is a train at half-past nine," said I, glancing over my +Bradshaw. "It is due at Winchester at 11:30." + +"That will do very nicely. Then perhaps I had better postpone my +analysis of the acetones, as we may need to be at our best in the +morning." + +By eleven o'clock the next day we were well upon our way to the +old English capital. Holmes had been buried in the morning papers +all the way down, but after we had passed the Hampshire border he +threw them down and began to admire the scenery. It was an ideal +spring day, a light blue sky, flecked with little fleecy white +clouds drifting across from west to east. The sun was shining +very brightly, and yet there was an exhilarating nip in the air, +which set an edge to a man's energy. All over the countryside, +away to the rolling hills around Aldershot, the little red and +grey roofs of the farm-steadings peeped out from amid the light +green of the new foliage. + +"Are they not fresh and beautiful?" I cried with all the +enthusiasm of a man fresh from the fogs of Baker Street. + +But Holmes shook his head gravely. + +"Do you know, Watson," said he, "that it is one of the curses of +a mind with a turn like mine that I must look at everything with +reference to my own special subject. You look at these scattered +houses, and you are impressed by their beauty. I look at them, +and the only thought which comes to me is a feeling of their +isolation and of the impunity with which crime may be committed +there." + +"Good heavens!" I cried. "Who would associate crime with these +dear old homesteads?" + +"They always fill me with a certain horror. It is my belief, +Watson, founded upon my experience, that the lowest and vilest +alleys in London do not present a more dreadful record of sin +than does the smiling and beautiful countryside." + +"You horrify me!" + +"But the reason is very obvious. The pressure of public opinion +can do in the town what the law cannot accomplish. There is no +lane so vile that the scream of a tortured child, or the thud of +a drunkard's blow, does not beget sympathy and indignation among +the neighbours, and then the whole machinery of justice is ever +so close that a word of complaint can set it going, and there is +but a step between the crime and the dock. But look at these +lonely houses, each in its own fields, filled for the most part +with poor ignorant folk who know little of the law. Think of the +deeds of hellish cruelty, the hidden wickedness which may go on, +year in, year out, in such places, and none the wiser. Had this +lady who appeals to us for help gone to live in Winchester, I +should never have had a fear for her. It is the five miles of +country which makes the danger. Still, it is clear that she is +not personally threatened." + +"No. If she can come to Winchester to meet us she can get away." + +"Quite so. She has her freedom." + +"What CAN be the matter, then? Can you suggest no explanation?" + +"I have devised seven separate explanations, each of which would +cover the facts as far as we know them. But which of these is +correct can only be determined by the fresh information which we +shall no doubt find waiting for us. Well, there is the tower of +the cathedral, and we shall soon learn all that Miss Hunter has +to tell." + +The Black Swan is an inn of repute in the High Street, at no +distance from the station, and there we found the young lady +waiting for us. She had engaged a sitting-room, and our lunch +awaited us upon the table. + +"I am so delighted that you have come," she said earnestly. "It +is so very kind of you both; but indeed I do not know what I +should do. Your advice will be altogether invaluable to me." + +"Pray tell us what has happened to you." + +"I will do so, and I must be quick, for I have promised Mr. +Rucastle to be back before three. I got his leave to come into +town this morning, though he little knew for what purpose." + +"Let us have everything in its due order." Holmes thrust his long +thin legs out towards the fire and composed himself to listen. + +"In the first place, I may say that I have met, on the whole, +with no actual ill-treatment from Mr. and Mrs. Rucastle. It is +only fair to them to say that. But I cannot understand them, and +I am not easy in my mind about them." + +"What can you not understand?" + +"Their reasons for their conduct. But you shall have it all just +as it occurred. When I came down, Mr. Rucastle met me here and +drove me in his dog-cart to the Copper Beeches. It is, as he +said, beautifully situated, but it is not beautiful in itself, +for it is a large square block of a house, whitewashed, but all +stained and streaked with damp and bad weather. There are grounds +round it, woods on three sides, and on the fourth a field which +slopes down to the Southampton highroad, which curves past about +a hundred yards from the front door. This ground in front belongs +to the house, but the woods all round are part of Lord +Southerton's preserves. A clump of copper beeches immediately in +front of the hall door has given its name to the place. + +"I was driven over by my employer, who was as amiable as ever, +and was introduced by him that evening to his wife and the child. +There was no truth, Mr. Holmes, in the conjecture which seemed to +us to be probable in your rooms at Baker Street. Mrs. Rucastle is +not mad. I found her to be a silent, pale-faced woman, much +younger than her husband, not more than thirty, I should think, +while he can hardly be less than forty-five. From their +conversation I have gathered that they have been married about +seven years, that he was a widower, and that his only child by +the first wife was the daughter who has gone to Philadelphia. Mr. +Rucastle told me in private that the reason why she had left them +was that she had an unreasoning aversion to her stepmother. As +the daughter could not have been less than twenty, I can quite +imagine that her position must have been uncomfortable with her +father's young wife. + +"Mrs. Rucastle seemed to me to be colourless in mind as well as +in feature. She impressed me neither favourably nor the reverse. +She was a nonentity. It was easy to see that she was passionately +devoted both to her husband and to her little son. Her light grey +eyes wandered continually from one to the other, noting every +little want and forestalling it if possible. He was kind to her +also in his bluff, boisterous fashion, and on the whole they +seemed to be a happy couple. And yet she had some secret sorrow, +this woman. She would often be lost in deep thought, with the +saddest look upon her face. More than once I have surprised her +in tears. I have thought sometimes that it was the disposition of +her child which weighed upon her mind, for I have never met so +utterly spoiled and so ill-natured a little creature. He is small +for his age, with a head which is quite disproportionately large. +His whole life appears to be spent in an alternation between +savage fits of passion and gloomy intervals of sulking. Giving +pain to any creature weaker than himself seems to be his one idea +of amusement, and he shows quite remarkable talent in planning +the capture of mice, little birds, and insects. But I would +rather not talk about the creature, Mr. Holmes, and, indeed, he +has little to do with my story." + +"I am glad of all details," remarked my friend, "whether they +seem to you to be relevant or not." + +"I shall try not to miss anything of importance. The one +unpleasant thing about the house, which struck me at once, was +the appearance and conduct of the servants. There are only two, a +man and his wife. Toller, for that is his name, is a rough, +uncouth man, with grizzled hair and whiskers, and a perpetual +smell of drink. Twice since I have been with them he has been +quite drunk, and yet Mr. Rucastle seemed to take no notice of it. +His wife is a very tall and strong woman with a sour face, as +silent as Mrs. Rucastle and much less amiable. They are a most +unpleasant couple, but fortunately I spend most of my time in the +nursery and my own room, which are next to each other in one +corner of the building. + +"For two days after my arrival at the Copper Beeches my life was +very quiet; on the third, Mrs. Rucastle came down just after +breakfast and whispered something to her husband. + +"'Oh, yes,' said he, turning to me, 'we are very much obliged to +you, Miss Hunter, for falling in with our whims so far as to cut +your hair. I assure you that it has not detracted in the tiniest +iota from your appearance. We shall now see how the electric-blue +dress will become you. You will find it laid out upon the bed in +your room, and if you would be so good as to put it on we should +both be extremely obliged.' + +"The dress which I found waiting for me was of a peculiar shade +of blue. It was of excellent material, a sort of beige, but it +bore unmistakable signs of having been worn before. It could not +have been a better fit if I had been measured for it. Both Mr. +and Mrs. Rucastle expressed a delight at the look of it, which +seemed quite exaggerated in its vehemence. They were waiting for +me in the drawing-room, which is a very large room, stretching +along the entire front of the house, with three long windows +reaching down to the floor. A chair had been placed close to the +central window, with its back turned towards it. In this I was +asked to sit, and then Mr. Rucastle, walking up and down on the +other side of the room, began to tell me a series of the funniest +stories that I have ever listened to. You cannot imagine how +comical he was, and I laughed until I was quite weary. Mrs. +Rucastle, however, who has evidently no sense of humour, never so +much as smiled, but sat with her hands in her lap, and a sad, +anxious look upon her face. After an hour or so, Mr. Rucastle +suddenly remarked that it was time to commence the duties of the +day, and that I might change my dress and go to little Edward in +the nursery. + +"Two days later this same performance was gone through under +exactly similar circumstances. Again I changed my dress, again I +sat in the window, and again I laughed very heartily at the funny +stories of which my employer had an immense rpertoire, and which +he told inimitably. Then he handed me a yellow-backed novel, and +moving my chair a little sideways, that my own shadow might not +fall upon the page, he begged me to read aloud to him. I read for +about ten minutes, beginning in the heart of a chapter, and then +suddenly, in the middle of a sentence, he ordered me to cease and +to change my dress. + +"You can easily imagine, Mr. Holmes, how curious I became as to +what the meaning of this extraordinary performance could possibly +be. They were always very careful, I observed, to turn my face +away from the window, so that I became consumed with the desire +to see what was going on behind my back. At first it seemed to be +impossible, but I soon devised a means. My hand-mirror had been +broken, so a happy thought seized me, and I concealed a piece of +the glass in my handkerchief. On the next occasion, in the midst +of my laughter, I put my handkerchief up to my eyes, and was able +with a little management to see all that there was behind me. I +confess that I was disappointed. There was nothing. At least that +was my first impression. At the second glance, however, I +perceived that there was a man standing in the Southampton Road, +a small bearded man in a grey suit, who seemed to be looking in +my direction. The road is an important highway, and there are +usually people there. This man, however, was leaning against the +railings which bordered our field and was looking earnestly up. I +lowered my handkerchief and glanced at Mrs. Rucastle to find her +eyes fixed upon me with a most searching gaze. She said nothing, +but I am convinced that she had divined that I had a mirror in my +hand and had seen what was behind me. She rose at once. + +"'Jephro,' said she, 'there is an impertinent fellow upon the +road there who stares up at Miss Hunter.' + +"'No friend of yours, Miss Hunter?' he asked. + +"'No, I know no one in these parts.' + +"'Dear me! How very impertinent! Kindly turn round and motion to +him to go away.' + +"'Surely it would be better to take no notice.' + +"'No, no, we should have him loitering here always. Kindly turn +round and wave him away like that.' + +"I did as I was told, and at the same instant Mrs. Rucastle drew +down the blind. That was a week ago, and from that time I have +not sat again in the window, nor have I worn the blue dress, nor +seen the man in the road." + +"Pray continue," said Holmes. "Your narrative promises to be a +most interesting one." + +"You will find it rather disconnected, I fear, and there may +prove to be little relation between the different incidents of +which I speak. On the very first day that I was at the Copper +Beeches, Mr. Rucastle took me to a small outhouse which stands +near the kitchen door. As we approached it I heard the sharp +rattling of a chain, and the sound as of a large animal moving +about. + +"'Look in here!' said Mr. Rucastle, showing me a slit between two +planks. 'Is he not a beauty?' + +"I looked through and was conscious of two glowing eyes, and of a +vague figure huddled up in the darkness. + +"'Don't be frightened,' said my employer, laughing at the start +which I had given. 'It's only Carlo, my mastiff. I call him mine, +but really old Toller, my groom, is the only man who can do +anything with him. We feed him once a day, and not too much then, +so that he is always as keen as mustard. Toller lets him loose +every night, and God help the trespasser whom he lays his fangs +upon. For goodness' sake don't you ever on any pretext set your +foot over the threshold at night, for it's as much as your life +is worth.' + +"The warning was no idle one, for two nights later I happened to +look out of my bedroom window about two o'clock in the morning. +It was a beautiful moonlight night, and the lawn in front of the +house was silvered over and almost as bright as day. I was +standing, rapt in the peaceful beauty of the scene, when I was +aware that something was moving under the shadow of the copper +beeches. As it emerged into the moonshine I saw what it was. It +was a giant dog, as large as a calf, tawny tinted, with hanging +jowl, black muzzle, and huge projecting bones. It walked slowly +across the lawn and vanished into the shadow upon the other side. +That dreadful sentinel sent a chill to my heart which I do not +think that any burglar could have done. + +"And now I have a very strange experience to tell you. I had, as +you know, cut off my hair in London, and I had placed it in a +great coil at the bottom of my trunk. One evening, after the +child was in bed, I began to amuse myself by examining the +furniture of my room and by rearranging my own little things. +There was an old chest of drawers in the room, the two upper ones +empty and open, the lower one locked. I had filled the first two +with my linen, and as I had still much to pack away I was +naturally annoyed at not having the use of the third drawer. It +struck me that it might have been fastened by a mere oversight, +so I took out my bunch of keys and tried to open it. The very +first key fitted to perfection, and I drew the drawer open. There +was only one thing in it, but I am sure that you would never +guess what it was. It was my coil of hair. + +"I took it up and examined it. It was of the same peculiar tint, +and the same thickness. But then the impossibility of the thing +obtruded itself upon me. How could my hair have been locked in +the drawer? With trembling hands I undid my trunk, turned out the +contents, and drew from the bottom my own hair. I laid the two +tresses together, and I assure you that they were identical. Was +it not extraordinary? Puzzle as I would, I could make nothing at +all of what it meant. I returned the strange hair to the drawer, +and I said nothing of the matter to the Rucastles as I felt that +I had put myself in the wrong by opening a drawer which they had +locked. + +"I am naturally observant, as you may have remarked, Mr. Holmes, +and I soon had a pretty good plan of the whole house in my head. +There was one wing, however, which appeared not to be inhabited +at all. A door which faced that which led into the quarters of +the Tollers opened into this suite, but it was invariably locked. +One day, however, as I ascended the stair, I met Mr. Rucastle +coming out through this door, his keys in his hand, and a look on +his face which made him a very different person to the round, +jovial man to whom I was accustomed. His cheeks were red, his +brow was all crinkled with anger, and the veins stood out at his +temples with passion. He locked the door and hurried past me +without a word or a look. + +"This aroused my curiosity, so when I went out for a walk in the +grounds with my charge, I strolled round to the side from which I +could see the windows of this part of the house. There were four +of them in a row, three of which were simply dirty, while the +fourth was shuttered up. They were evidently all deserted. As I +strolled up and down, glancing at them occasionally, Mr. Rucastle +came out to me, looking as merry and jovial as ever. + +"'Ah!' said he, 'you must not think me rude if I passed you +without a word, my dear young lady. I was preoccupied with +business matters.' + +"I assured him that I was not offended. 'By the way,' said I, +'you seem to have quite a suite of spare rooms up there, and one +of them has the shutters up.' + +"He looked surprised and, as it seemed to me, a little startled +at my remark. + +"'Photography is one of my hobbies,' said he. 'I have made my +dark room up there. But, dear me! what an observant young lady we +have come upon. Who would have believed it? Who would have ever +believed it?' He spoke in a jesting tone, but there was no jest +in his eyes as he looked at me. I read suspicion there and +annoyance, but no jest. + +"Well, Mr. Holmes, from the moment that I understood that there +was something about that suite of rooms which I was not to know, +I was all on fire to go over them. It was not mere curiosity, +though I have my share of that. It was more a feeling of duty--a +feeling that some good might come from my penetrating to this +place. They talk of woman's instinct; perhaps it was woman's +instinct which gave me that feeling. At any rate, it was there, +and I was keenly on the lookout for any chance to pass the +forbidden door. + +"It was only yesterday that the chance came. I may tell you that, +besides Mr. Rucastle, both Toller and his wife find something to +do in these deserted rooms, and I once saw him carrying a large +black linen bag with him through the door. Recently he has been +drinking hard, and yesterday evening he was very drunk; and when +I came upstairs there was the key in the door. I have no doubt at +all that he had left it there. Mr. and Mrs. Rucastle were both +downstairs, and the child was with them, so that I had an +admirable opportunity. I turned the key gently in the lock, +opened the door, and slipped through. + +"There was a little passage in front of me, unpapered and +uncarpeted, which turned at a right angle at the farther end. +Round this corner were three doors in a line, the first and third +of which were open. They each led into an empty room, dusty and +cheerless, with two windows in the one and one in the other, so +thick with dirt that the evening light glimmered dimly through +them. The centre door was closed, and across the outside of it +had been fastened one of the broad bars of an iron bed, padlocked +at one end to a ring in the wall, and fastened at the other with +stout cord. The door itself was locked as well, and the key was +not there. This barricaded door corresponded clearly with the +shuttered window outside, and yet I could see by the glimmer from +beneath it that the room was not in darkness. Evidently there was +a skylight which let in light from above. As I stood in the +passage gazing at the sinister door and wondering what secret it +might veil, I suddenly heard the sound of steps within the room +and saw a shadow pass backward and forward against the little +slit of dim light which shone out from under the door. A mad, +unreasoning terror rose up in me at the sight, Mr. Holmes. My +overstrung nerves failed me suddenly, and I turned and ran--ran +as though some dreadful hand were behind me clutching at the +skirt of my dress. I rushed down the passage, through the door, +and straight into the arms of Mr. Rucastle, who was waiting +outside. + +"'So,' said he, smiling, 'it was you, then. I thought that it +must be when I saw the door open.' + +"'Oh, I am so frightened!' I panted. + +"'My dear young lady! my dear young lady!'--you cannot think how +caressing and soothing his manner was--'and what has frightened +you, my dear young lady?' + +"But his voice was just a little too coaxing. He overdid it. I +was keenly on my guard against him. + +"'I was foolish enough to go into the empty wing,' I answered. +'But it is so lonely and eerie in this dim light that I was +frightened and ran out again. Oh, it is so dreadfully still in +there!' + +"'Only that?' said he, looking at me keenly. + +"'Why, what did you think?' I asked. + +"'Why do you think that I lock this door?' + +"'I am sure that I do not know.' + +"'It is to keep people out who have no business there. Do you +see?' He was still smiling in the most amiable manner. + +"'I am sure if I had known--' + +"'Well, then, you know now. And if you ever put your foot over +that threshold again'--here in an instant the smile hardened into +a grin of rage, and he glared down at me with the face of a +demon--'I'll throw you to the mastiff.' + +"I was so terrified that I do not know what I did. I suppose that +I must have rushed past him into my room. I remember nothing +until I found myself lying on my bed trembling all over. Then I +thought of you, Mr. Holmes. I could not live there longer without +some advice. I was frightened of the house, of the man, of the +woman, of the servants, even of the child. They were all horrible +to me. If I could only bring you down all would be well. Of +course I might have fled from the house, but my curiosity was +almost as strong as my fears. My mind was soon made up. I would +send you a wire. I put on my hat and cloak, went down to the +office, which is about half a mile from the house, and then +returned, feeling very much easier. A horrible doubt came into my +mind as I approached the door lest the dog might be loose, but I +remembered that Toller had drunk himself into a state of +insensibility that evening, and I knew that he was the only one +in the household who had any influence with the savage creature, +or who would venture to set him free. I slipped in in safety and +lay awake half the night in my joy at the thought of seeing you. +I had no difficulty in getting leave to come into Winchester this +morning, but I must be back before three o'clock, for Mr. and +Mrs. Rucastle are going on a visit, and will be away all the +evening, so that I must look after the child. Now I have told you +all my adventures, Mr. Holmes, and I should be very glad if you +could tell me what it all means, and, above all, what I should +do." + +Holmes and I had listened spellbound to this extraordinary story. +My friend rose now and paced up and down the room, his hands in +his pockets, and an expression of the most profound gravity upon +his face. + +"Is Toller still drunk?" he asked. + +"Yes. I heard his wife tell Mrs. Rucastle that she could do +nothing with him." + +"That is well. And the Rucastles go out to-night?" + +"Yes." + +"Is there a cellar with a good strong lock?" + +"Yes, the wine-cellar." + +"You seem to me to have acted all through this matter like a very +brave and sensible girl, Miss Hunter. Do you think that you could +perform one more feat? I should not ask it of you if I did not +think you a quite exceptional woman." + +"I will try. What is it?" + +"We shall be at the Copper Beeches by seven o'clock, my friend +and I. The Rucastles will be gone by that time, and Toller will, +we hope, be incapable. There only remains Mrs. Toller, who might +give the alarm. If you could send her into the cellar on some +errand, and then turn the key upon her, you would facilitate +matters immensely." + +"I will do it." + +"Excellent! We shall then look thoroughly into the affair. Of +course there is only one feasible explanation. You have been +brought there to personate someone, and the real person is +imprisoned in this chamber. That is obvious. As to who this +prisoner is, I have no doubt that it is the daughter, Miss Alice +Rucastle, if I remember right, who was said to have gone to +America. You were chosen, doubtless, as resembling her in height, +figure, and the colour of your hair. Hers had been cut off, very +possibly in some illness through which she has passed, and so, of +course, yours had to be sacrificed also. By a curious chance you +came upon her tresses. The man in the road was undoubtedly some +friend of hers--possibly her fianc--and no doubt, as you wore +the girl's dress and were so like her, he was convinced from your +laughter, whenever he saw you, and afterwards from your gesture, +that Miss Rucastle was perfectly happy, and that she no longer +desired his attentions. The dog is let loose at night to prevent +him from endeavouring to communicate with her. So much is fairly +clear. The most serious point in the case is the disposition of +the child." + +"What on earth has that to do with it?" I ejaculated. + +"My dear Watson, you as a medical man are continually gaining +light as to the tendencies of a child by the study of the +parents. Don't you see that the converse is equally valid. I have +frequently gained my first real insight into the character of +parents by studying their children. This child's disposition is +abnormally cruel, merely for cruelty's sake, and whether he +derives this from his smiling father, as I should suspect, or +from his mother, it bodes evil for the poor girl who is in their +power." + +"I am sure that you are right, Mr. Holmes," cried our client. "A +thousand things come back to me which make me certain that you +have hit it. Oh, let us lose not an instant in bringing help to +this poor creature." + +"We must be circumspect, for we are dealing with a very cunning +man. We can do nothing until seven o'clock. At that hour we shall +be with you, and it will not be long before we solve the +mystery." + +We were as good as our word, for it was just seven when we +reached the Copper Beeches, having put up our trap at a wayside +public-house. The group of trees, with their dark leaves shining +like burnished metal in the light of the setting sun, were +sufficient to mark the house even had Miss Hunter not been +standing smiling on the door-step. + +"Have you managed it?" asked Holmes. + +A loud thudding noise came from somewhere downstairs. "That is +Mrs. Toller in the cellar," said she. "Her husband lies snoring +on the kitchen rug. Here are his keys, which are the duplicates +of Mr. Rucastle's." + +"You have done well indeed!" cried Holmes with enthusiasm. "Now +lead the way, and we shall soon see the end of this black +business." + +We passed up the stair, unlocked the door, followed on down a +passage, and found ourselves in front of the barricade which Miss +Hunter had described. Holmes cut the cord and removed the +transverse bar. Then he tried the various keys in the lock, but +without success. No sound came from within, and at the silence +Holmes' face clouded over. + +"I trust that we are not too late," said he. "I think, Miss +Hunter, that we had better go in without you. Now, Watson, put +your shoulder to it, and we shall see whether we cannot make our +way in." + +It was an old rickety door and gave at once before our united +strength. Together we rushed into the room. It was empty. There +was no furniture save a little pallet bed, a small table, and a +basketful of linen. The skylight above was open, and the prisoner +gone. + +"There has been some villainy here," said Holmes; "this beauty +has guessed Miss Hunter's intentions and has carried his victim +off." + +"But how?" + +"Through the skylight. We shall soon see how he managed it." He +swung himself up onto the roof. "Ah, yes," he cried, "here's the +end of a long light ladder against the eaves. That is how he did +it." + +"But it is impossible," said Miss Hunter; "the ladder was not +there when the Rucastles went away." + +"He has come back and done it. I tell you that he is a clever and +dangerous man. I should not be very much surprised if this were +he whose step I hear now upon the stair. I think, Watson, that it +would be as well for you to have your pistol ready." + +The words were hardly out of his mouth before a man appeared at +the door of the room, a very fat and burly man, with a heavy +stick in his hand. Miss Hunter screamed and shrunk against the +wall at the sight of him, but Sherlock Holmes sprang forward and +confronted him. + +"You villain!" said he, "where's your daughter?" + +The fat man cast his eyes round, and then up at the open +skylight. + +"It is for me to ask you that," he shrieked, "you thieves! Spies +and thieves! I have caught you, have I? You are in my power. I'll +serve you!" He turned and clattered down the stairs as hard as he +could go. + +"He's gone for the dog!" cried Miss Hunter. + +"I have my revolver," said I. + +"Better close the front door," cried Holmes, and we all rushed +down the stairs together. We had hardly reached the hall when we +heard the baying of a hound, and then a scream of agony, with a +horrible worrying sound which it was dreadful to listen to. An +elderly man with a red face and shaking limbs came staggering out +at a side door. + +"My God!" he cried. "Someone has loosed the dog. It's not been +fed for two days. Quick, quick, or it'll be too late!" + +Holmes and I rushed out and round the angle of the house, with +Toller hurrying behind us. There was the huge famished brute, its +black muzzle buried in Rucastle's throat, while he writhed and +screamed upon the ground. Running up, I blew its brains out, and +it fell over with its keen white teeth still meeting in the great +creases of his neck. With much labour we separated them and +carried him, living but horribly mangled, into the house. We laid +him upon the drawing-room sofa, and having dispatched the sobered +Toller to bear the news to his wife, I did what I could to +relieve his pain. We were all assembled round him when the door +opened, and a tall, gaunt woman entered the room. + +"Mrs. Toller!" cried Miss Hunter. + +"Yes, miss. Mr. Rucastle let me out when he came back before he +went up to you. Ah, miss, it is a pity you didn't let me know +what you were planning, for I would have told you that your pains +were wasted." + +"Ha!" said Holmes, looking keenly at her. "It is clear that Mrs. +Toller knows more about this matter than anyone else." + +"Yes, sir, I do, and I am ready enough to tell what I know." + +"Then, pray, sit down, and let us hear it for there are several +points on which I must confess that I am still in the dark." + +"I will soon make it clear to you," said she; "and I'd have done +so before now if I could ha' got out from the cellar. If there's +police-court business over this, you'll remember that I was the +one that stood your friend, and that I was Miss Alice's friend +too. + +"She was never happy at home, Miss Alice wasn't, from the time +that her father married again. She was slighted like and had no +say in anything, but it never really became bad for her until +after she met Mr. Fowler at a friend's house. As well as I could +learn, Miss Alice had rights of her own by will, but she was so +quiet and patient, she was, that she never said a word about them +but just left everything in Mr. Rucastle's hands. He knew he was +safe with her; but when there was a chance of a husband coming +forward, who would ask for all that the law would give him, then +her father thought it time to put a stop on it. He wanted her to +sign a paper, so that whether she married or not, he could use +her money. When she wouldn't do it, he kept on worrying her until +she got brain-fever, and for six weeks was at death's door. Then +she got better at last, all worn to a shadow, and with her +beautiful hair cut off; but that didn't make no change in her +young man, and he stuck to her as true as man could be." + +"Ah," said Holmes, "I think that what you have been good enough +to tell us makes the matter fairly clear, and that I can deduce +all that remains. Mr. Rucastle then, I presume, took to this +system of imprisonment?" + +"Yes, sir." + +"And brought Miss Hunter down from London in order to get rid of +the disagreeable persistence of Mr. Fowler." + +"That was it, sir." + +"But Mr. Fowler being a persevering man, as a good seaman should +be, blockaded the house, and having met you succeeded by certain +arguments, metallic or otherwise, in convincing you that your +interests were the same as his." + +"Mr. Fowler was a very kind-spoken, free-handed gentleman," said +Mrs. Toller serenely. + +"And in this way he managed that your good man should have no +want of drink, and that a ladder should be ready at the moment +when your master had gone out." + +"You have it, sir, just as it happened." + +"I am sure we owe you an apology, Mrs. Toller," said Holmes, "for +you have certainly cleared up everything which puzzled us. And +here comes the country surgeon and Mrs. Rucastle, so I think, +Watson, that we had best escort Miss Hunter back to Winchester, +as it seems to me that our locus standi now is rather a +questionable one." + +And thus was solved the mystery of the sinister house with the +copper beeches in front of the door. Mr. Rucastle survived, but +was always a broken man, kept alive solely through the care of +his devoted wife. They still live with their old servants, who +probably know so much of Rucastle's past life that he finds it +difficult to part from them. Mr. Fowler and Miss Rucastle were +married, by special license, in Southampton the day after their +flight, and he is now the holder of a government appointment in +the island of Mauritius. As to Miss Violet Hunter, my friend +Holmes, rather to my disappointment, manifested no further +interest in her when once she had ceased to be the centre of one +of his problems, and she is now the head of a private school at +Walsall, where I believe that she has met with considerable success. + + + + + + + + + +End of the Project Gutenberg EBook of The Adventures of Sherlock Holmes, by +Arthur Conan Doyle + +*** END OF THIS PROJECT GUTENBERG EBOOK THE ADVENTURES OF SHERLOCK HOLMES *** + +***** This file should be named 1661-8.txt or 1661-8.zip ***** +This and all associated files of various formats will be found in: + http://www.gutenberg.org/1/6/6/1661/ + +Produced by an anonymous Project Gutenberg volunteer and Jose Menendez + +Updated editions will replace the previous one--the old editions +will be renamed. + +Creating the works from public domain print editions means that no +one owns a United States copyright in these works, so the Foundation +(and you!) can copy and distribute it in the United States without +permission and without paying copyright royalties. Special rules, +set forth in the General Terms of Use part of this license, apply to +copying and distributing Project Gutenberg-tm electronic works to +protect the PROJECT GUTENBERG-tm concept and trademark. Project +Gutenberg is a registered trademark, and may not be used if you +charge for the eBooks, unless you receive specific permission. If you +do not charge anything for copies of this eBook, complying with the +rules is very easy. You may use this eBook for nearly any purpose +such as creation of derivative works, reports, performances and +research. They may be modified and printed and given away--you may do +practically ANYTHING with public domain eBooks. Redistribution is +subject to the trademark license, especially commercial +redistribution. + + + +*** START: FULL LICENSE *** + +THE FULL PROJECT GUTENBERG LICENSE +PLEASE READ THIS BEFORE YOU DISTRIBUTE OR USE THIS WORK + +To protect the Project Gutenberg-tm mission of promoting the free +distribution of electronic works, by using or distributing this work +(or any other work associated in any way with the phrase "Project +Gutenberg"), you agree to comply with all the terms of the Full Project +Gutenberg-tm License (available with this file or online at +http://gutenberg.net/license). + + +Section 1. General Terms of Use and Redistributing Project Gutenberg-tm +electronic works + +1.A. By reading or using any part of this Project Gutenberg-tm +electronic work, you indicate that you have read, understand, agree to +and accept all the terms of this license and intellectual property +(trademark/copyright) agreement. If you do not agree to abide by all +the terms of this agreement, you must cease using and return or destroy +all copies of Project Gutenberg-tm electronic works in your possession. +If you paid a fee for obtaining a copy of or access to a Project +Gutenberg-tm electronic work and you do not agree to be bound by the +terms of this agreement, you may obtain a refund from the person or +entity to whom you paid the fee as set forth in paragraph 1.E.8. + +1.B. "Project Gutenberg" is a registered trademark. It may only be +used on or associated in any way with an electronic work by people who +agree to be bound by the terms of this agreement. There are a few +things that you can do with most Project Gutenberg-tm electronic works +even without complying with the full terms of this agreement. See +paragraph 1.C below. There are a lot of things you can do with Project +Gutenberg-tm electronic works if you follow the terms of this agreement +and help preserve free future access to Project Gutenberg-tm electronic +works. See paragraph 1.E below. + +1.C. The Project Gutenberg Literary Archive Foundation ("the Foundation" +or PGLAF), owns a compilation copyright in the collection of Project +Gutenberg-tm electronic works. Nearly all the individual works in the +collection are in the public domain in the United States. If an +individual work is in the public domain in the United States and you are +located in the United States, we do not claim a right to prevent you from +copying, distributing, performing, displaying or creating derivative +works based on the work as long as all references to Project Gutenberg +are removed. Of course, we hope that you will support the Project +Gutenberg-tm mission of promoting free access to electronic works by +freely sharing Project Gutenberg-tm works in compliance with the terms of +this agreement for keeping the Project Gutenberg-tm name associated with +the work. You can easily comply with the terms of this agreement by +keeping this work in the same format with its attached full Project +Gutenberg-tm License when you share it without charge with others. + +1.D. The copyright laws of the place where you are located also govern +what you can do with this work. Copyright laws in most countries are in +a constant state of change. If you are outside the United States, check +the laws of your country in addition to the terms of this agreement +before downloading, copying, displaying, performing, distributing or +creating derivative works based on this work or any other Project +Gutenberg-tm work. The Foundation makes no representations concerning +the copyright status of any work in any country outside the United +States. + +1.E. Unless you have removed all references to Project Gutenberg: + +1.E.1. The following sentence, with active links to, or other immediate +access to, the full Project Gutenberg-tm License must appear prominently +whenever any copy of a Project Gutenberg-tm work (any work on which the +phrase "Project Gutenberg" appears, or with which the phrase "Project +Gutenberg" is associated) is accessed, displayed, performed, viewed, +copied or distributed: + +This eBook is for the use of anyone anywhere at no cost and with +almost no restrictions whatsoever. You may copy it, give it away or +re-use it under the terms of the Project Gutenberg License included +with this eBook or online at www.gutenberg.net + +1.E.2. If an individual Project Gutenberg-tm electronic work is derived +from the public domain (does not contain a notice indicating that it is +posted with permission of the copyright holder), the work can be copied +and distributed to anyone in the United States without paying any fees +or charges. If you are redistributing or providing access to a work +with the phrase "Project Gutenberg" associated with or appearing on the +work, you must comply either with the requirements of paragraphs 1.E.1 +through 1.E.7 or obtain permission for the use of the work and the +Project Gutenberg-tm trademark as set forth in paragraphs 1.E.8 or +1.E.9. + +1.E.3. If an individual Project Gutenberg-tm electronic work is posted +with the permission of the copyright holder, your use and distribution +must comply with both paragraphs 1.E.1 through 1.E.7 and any additional +terms imposed by the copyright holder. Additional terms will be linked +to the Project Gutenberg-tm License for all works posted with the +permission of the copyright holder found at the beginning of this work. + +1.E.4. Do not unlink or detach or remove the full Project Gutenberg-tm +License terms from this work, or any files containing a part of this +work or any other work associated with Project Gutenberg-tm. + +1.E.5. Do not copy, display, perform, distribute or redistribute this +electronic work, or any part of this electronic work, without +prominently displaying the sentence set forth in paragraph 1.E.1 with +active links or immediate access to the full terms of the Project +Gutenberg-tm License. + +1.E.6. You may convert to and distribute this work in any binary, +compressed, marked up, nonproprietary or proprietary form, including any +word processing or hypertext form. However, if you provide access to or +distribute copies of a Project Gutenberg-tm work in a format other than +"Plain Vanilla ASCII" or other format used in the official version +posted on the official Project Gutenberg-tm web site (www.gutenberg.net), +you must, at no additional cost, fee or expense to the user, provide a +copy, a means of exporting a copy, or a means of obtaining a copy upon +request, of the work in its original "Plain Vanilla ASCII" or other +form. Any alternate format must include the full Project Gutenberg-tm +License as specified in paragraph 1.E.1. + +1.E.7. Do not charge a fee for access to, viewing, displaying, +performing, copying or distributing any Project Gutenberg-tm works +unless you comply with paragraph 1.E.8 or 1.E.9. + +1.E.8. You may charge a reasonable fee for copies of or providing +access to or distributing Project Gutenberg-tm electronic works provided +that + +- You pay a royalty fee of 20% of the gross profits you derive from + the use of Project Gutenberg-tm works calculated using the method + you already use to calculate your applicable taxes. The fee is + owed to the owner of the Project Gutenberg-tm trademark, but he + has agreed to donate royalties under this paragraph to the + Project Gutenberg Literary Archive Foundation. Royalty payments + must be paid within 60 days following each date on which you + prepare (or are legally required to prepare) your periodic tax + returns. Royalty payments should be clearly marked as such and + sent to the Project Gutenberg Literary Archive Foundation at the + address specified in Section 4, "Information about donations to + the Project Gutenberg Literary Archive Foundation." + +- You provide a full refund of any money paid by a user who notifies + you in writing (or by e-mail) within 30 days of receipt that s/he + does not agree to the terms of the full Project Gutenberg-tm + License. You must require such a user to return or + destroy all copies of the works possessed in a physical medium + and discontinue all use of and all access to other copies of + Project Gutenberg-tm works. + +- You provide, in accordance with paragraph 1.F.3, a full refund of any + money paid for a work or a replacement copy, if a defect in the + electronic work is discovered and reported to you within 90 days + of receipt of the work. + +- You comply with all other terms of this agreement for free + distribution of Project Gutenberg-tm works. + +1.E.9. If you wish to charge a fee or distribute a Project Gutenberg-tm +electronic work or group of works on different terms than are set +forth in this agreement, you must obtain permission in writing from +both the Project Gutenberg Literary Archive Foundation and Michael +Hart, the owner of the Project Gutenberg-tm trademark. Contact the +Foundation as set forth in Section 3 below. + +1.F. + +1.F.1. Project Gutenberg volunteers and employees expend considerable +effort to identify, do copyright research on, transcribe and proofread +public domain works in creating the Project Gutenberg-tm +collection. Despite these efforts, Project Gutenberg-tm electronic +works, and the medium on which they may be stored, may contain +"Defects," such as, but not limited to, incomplete, inaccurate or +corrupt data, transcription errors, a copyright or other intellectual +property infringement, a defective or damaged disk or other medium, a +computer virus, or computer codes that damage or cannot be read by +your equipment. + +1.F.2. LIMITED WARRANTY, DISCLAIMER OF DAMAGES - Except for the "Right +of Replacement or Refund" described in paragraph 1.F.3, the Project +Gutenberg Literary Archive Foundation, the owner of the Project +Gutenberg-tm trademark, and any other party distributing a Project +Gutenberg-tm electronic work under this agreement, disclaim all +liability to you for damages, costs and expenses, including legal +fees. YOU AGREE THAT YOU HAVE NO REMEDIES FOR NEGLIGENCE, STRICT +LIABILITY, BREACH OF WARRANTY OR BREACH OF CONTRACT EXCEPT THOSE +PROVIDED IN PARAGRAPH 1.F.3. YOU AGREE THAT THE FOUNDATION, THE +TRADEMARK OWNER, AND ANY DISTRIBUTOR UNDER THIS AGREEMENT WILL NOT BE +LIABLE TO YOU FOR ACTUAL, DIRECT, INDIRECT, CONSEQUENTIAL, PUNITIVE OR +INCIDENTAL DAMAGES EVEN IF YOU GIVE NOTICE OF THE POSSIBILITY OF SUCH +DAMAGE. + +1.F.3. LIMITED RIGHT OF REPLACEMENT OR REFUND - If you discover a +defect in this electronic work within 90 days of receiving it, you can +receive a refund of the money (if any) you paid for it by sending a +written explanation to the person you received the work from. If you +received the work on a physical medium, you must return the medium with +your written explanation. The person or entity that provided you with +the defective work may elect to provide a replacement copy in lieu of a +refund. If you received the work electronically, the person or entity +providing it to you may choose to give you a second opportunity to +receive the work electronically in lieu of a refund. If the second copy +is also defective, you may demand a refund in writing without further +opportunities to fix the problem. + +1.F.4. Except for the limited right of replacement or refund set forth +in paragraph 1.F.3, this work is provided to you 'AS-IS' WITH NO OTHER +WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +WARRANTIES OF MERCHANTIBILITY OR FITNESS FOR ANY PURPOSE. + +1.F.5. Some states do not allow disclaimers of certain implied +warranties or the exclusion or limitation of certain types of damages. +If any disclaimer or limitation set forth in this agreement violates the +law of the state applicable to this agreement, the agreement shall be +interpreted to make the maximum disclaimer or limitation permitted by +the applicable state law. The invalidity or unenforceability of any +provision of this agreement shall not void the remaining provisions. + +1.F.6. INDEMNITY - You agree to indemnify and hold the Foundation, the +trademark owner, any agent or employee of the Foundation, anyone +providing copies of Project Gutenberg-tm electronic works in accordance +with this agreement, and any volunteers associated with the production, +promotion and distribution of Project Gutenberg-tm electronic works, +harmless from all liability, costs and expenses, including legal fees, +that arise directly or indirectly from any of the following which you do +or cause to occur: (a) distribution of this or any Project Gutenberg-tm +work, (b) alteration, modification, or additions or deletions to any +Project Gutenberg-tm work, and (c) any Defect you cause. + + +Section 2. Information about the Mission of Project Gutenberg-tm + +Project Gutenberg-tm is synonymous with the free distribution of +electronic works in formats readable by the widest variety of computers +including obsolete, old, middle-aged and new computers. It exists +because of the efforts of hundreds of volunteers and donations from +people in all walks of life. + +Volunteers and financial support to provide volunteers with the +assistance they need are critical to reaching Project Gutenberg-tm's +goals and ensuring that the Project Gutenberg-tm collection will +remain freely available for generations to come. In 2001, the Project +Gutenberg Literary Archive Foundation was created to provide a secure +and permanent future for Project Gutenberg-tm and future generations. +To learn more about the Project Gutenberg Literary Archive Foundation +and how your efforts and donations can help, see Sections 3 and 4 +and the Foundation web page at http://www.pglaf.org. + + +Section 3. Information about the Project Gutenberg Literary Archive +Foundation + +The Project Gutenberg Literary Archive Foundation is a non profit +501(c)(3) educational corporation organized under the laws of the +state of Mississippi and granted tax exempt status by the Internal +Revenue Service. The Foundation's EIN or federal tax identification +number is 64-6221541. Its 501(c)(3) letter is posted at +http://pglaf.org/fundraising. Contributions to the Project Gutenberg +Literary Archive Foundation are tax deductible to the full extent +permitted by U.S. federal laws and your state's laws. + +The Foundation's principal office is located at 4557 Melan Dr. S. +Fairbanks, AK, 99712., but its volunteers and employees are scattered +throughout numerous locations. Its business office is located at +809 North 1500 West, Salt Lake City, UT 84116, (801) 596-1887, email +business@pglaf.org. Email contact links and up to date contact +information can be found at the Foundation's web site and official +page at http://pglaf.org + +For additional contact information: + Dr. Gregory B. Newby + Chief Executive and Director + gbnewby@pglaf.org + + +Section 4. Information about Donations to the Project Gutenberg +Literary Archive Foundation + +Project Gutenberg-tm depends upon and cannot survive without wide +spread public support and donations to carry out its mission of +increasing the number of public domain and licensed works that can be +freely distributed in machine readable form accessible by the widest +array of equipment including outdated equipment. Many small donations +($1 to $5,000) are particularly important to maintaining tax exempt +status with the IRS. + +The Foundation is committed to complying with the laws regulating +charities and charitable donations in all 50 states of the United +States. Compliance requirements are not uniform and it takes a +considerable effort, much paperwork and many fees to meet and keep up +with these requirements. We do not solicit donations in locations +where we have not received written confirmation of compliance. To +SEND DONATIONS or determine the status of compliance for any +particular state visit http://pglaf.org + +While we cannot and do not solicit contributions from states where we +have not met the solicitation requirements, we know of no prohibition +against accepting unsolicited donations from donors in such states who +approach us with offers to donate. + +International donations are gratefully accepted, but we cannot make +any statements concerning tax treatment of donations received from +outside the United States. U.S. laws alone swamp our small staff. + +Please check the Project Gutenberg Web pages for current donation +methods and addresses. Donations are accepted in a number of other +ways including including checks, online payments and credit card +donations. To donate, please visit: http://pglaf.org/donate + + +Section 5. General Information About Project Gutenberg-tm electronic +works. + +Professor Michael S. Hart is the originator of the Project Gutenberg-tm +concept of a library of electronic works that could be freely shared +with anyone. For thirty years, he produced and distributed Project +Gutenberg-tm eBooks with only a loose network of volunteer support. + + +Project Gutenberg-tm eBooks are often created from several printed +editions, all of which are confirmed as Public Domain in the U.S. +unless a copyright notice is included. Thus, we do not necessarily +keep eBooks in compliance with any particular paper edition. + + +Most people start at our Web site which has the main PG search facility: + + http://www.gutenberg.net + +This Web site includes information about Project Gutenberg-tm, +including how to make donations to the Project Gutenberg Literary +Archive Foundation, how to help produce our new eBooks, and how to +subscribe to our email newsletter to hear about new eBooks. diff --git a/_downloads/a036bf9efd753b24c68d132695a8ded7/walnut_party.py b/_downloads/a036bf9efd753b24c68d132695a8ded7/walnut_party.py new file mode 100644 index 0000000..9b195b2 --- /dev/null +++ b/_downloads/a036bf9efd753b24c68d132695a8ded7/walnut_party.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +""" +When squirrels get together for a party, they like to have walnuts. +A squirrel party is successful when the number of walnuts is between +40 and 60, inclusive. Unless it is the weekend, in which case there +is no upper bound on the number of walnuts. + +Return True if the party with the given values is successful, +or False otherwise. +""" + + +def walnut_party(walnuts, is_weekend): + pass diff --git a/_downloads/a0af394a76895d8a4108e52d618a184d/test_html_output5.html b/_downloads/a0af394a76895d8a4108e52d618a184d/test_html_output5.html new file mode 100644 index 0000000..e7efbfc --- /dev/null +++ b/_downloads/a0af394a76895d8a4108e52d618a184d/test_html_output5.html @@ -0,0 +1,11 @@ + + +PythonClass = Revision 1087: + + +

+Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+
+ + \ No newline at end of file diff --git a/_downloads/a0f956dd7f2527a8bfefbc90278070eb/test_random_unitest.py b/_downloads/a0f956dd7f2527a8bfefbc90278070eb/test_random_unitest.py new file mode 100644 index 0000000..b8a1b71 --- /dev/null +++ b/_downloads/a0f956dd7f2527a8bfefbc90278070eb/test_random_unitest.py @@ -0,0 +1,32 @@ +import random +import unittest + + +class TestSequenceFunctions(unittest.TestCase): + + def setUp(self): + self.seq = list(range(10)) + + def test_shuffle(self): + """ + make sure the shuffled sequence does not lose any elements + """ + random.shuffle(self.seq) + self.seq.sort() + self.assertEqual(self.seq, list(range(10))) + + # should raise an exception for an immutable sequence + self.assertRaises(TypeError, random.shuffle, (1, 2, 3)) + + def test_choice(self): + element = random.choice(self.seq) + self.assertTrue(element in self.seq) + + def test_sample(self): + with self.assertRaises(ValueError): + random.sample(self.seq, 20) + for element in random.sample(self.seq, 5): + self.assertTrue(element in self.seq) + +if __name__ == '__main__': + unittest.main() diff --git a/_downloads/a227f1937bb2aa9586ba90b83f79fc72/test_mock_input.py b/_downloads/a227f1937bb2aa9586ba90b83f79fc72/test_mock_input.py new file mode 100644 index 0000000..7d3b8d1 --- /dev/null +++ b/_downloads/a227f1937bb2aa9586ba90b83f79fc72/test_mock_input.py @@ -0,0 +1,37 @@ +import pytest +from unittest import mock + + +def get_color(): + color = input("what is your favorite color?") + if color == "red": + return "that's a stupid color" + if color == "blue": + return "Hey! that's mine too!" + else: + raise ValueError("nothing to say about that color") + return color + + +@mock.patch('builtins.input') +def test_get_color_red(mocked_input): + mocked_input.return_value = "red" + result = get_color() + assert "stupid" in result + + +@mock.patch('builtins.input') +def test_get_color_blue(mocked_input): + mocked_input.return_value = "blue" + result = get_color() + assert "Hey!" in result + + +@mock.patch('builtins.input') +def test_get_color_purple(mocked_input): + mocked_input.return_value = "purple" + with pytest.raises(ValueError): + result = get_color() + + + diff --git a/_downloads/a585a12ed8c9efcd4751972bce98f5d4/report.py b/_downloads/a585a12ed8c9efcd4751972bce98f5d4/report.py new file mode 100644 index 0000000..2b9596f --- /dev/null +++ b/_downloads/a585a12ed8c9efcd4751972bce98f5d4/report.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python + +""" +A set of classes to facilitate report writing +""" + +import math +import operator +from uuid import uuid4 + + +class Row: + """ + This class represents a single row + with ID, first name, last name and state attributes + """ + def __init__(self, fname, lname, state): + self.id = str(uuid4()) # randomly generated unique ID + self.fname = fname + self.lname = lname + self.state = state + + def __str__(self): + return f"| {self.id} | {self.fname + ' ' + self.lname:<15} | {self.state} |" + + +class Report: + def __init__(self, limit): + self.limit = limit + self.rows = [] + + def add_row(self, row): + """Add a row object to the report""" + pass + + def remove_row(self, row_id): + """Remove a row object by the row's ID""" + pass + + def size(self): + """Return how many total rows the report has""" + pass + + def get_number_of_pages(self): + """ + Get how many pages the report has; this will be based on limit variable. + If your limit=4 and rows list has 6 records then there are two pages: + page1 has 4 records, page2 has 2 records + hint: you'll want to round up + """ + pass + + def get_paged_rows(self, sort_field, page): + """Return a list of rows for a specific page number + :param sort_field: field to sort on, "name" or "-name" (descending) + :param page: specific page for returning data + :return: list of row objects for specific page + + Hints: + 1. You'll want to determine if sort is reversed or not (remember that + sorted() takes in param for that) this is based on if the fields + start with a minus sign for DESCENDING sort + 2. When sorting on passed in field you can use handy `operator` library + with `attrgetter` method (look up official docs) + 3. To actually determine what rows belong on the specific page you'll be + using list slicing (remember the slicing lab?) + + Here is an illustration to help with the code logic: + + The list has 6 rows => [, , , , , ] + for page=2 we expect to get => [, ] + with slicing you'll want to offset your list by 4 in this case + (extra hint: we can define offset as `offset = (page - 1) * self.limit`) + + Remember to write tests first! You'll need a few of them to test all the + functionality. + """ + pass + + +def run_report(sort_field): + print(f"... PAGED REPORT SORTED BY: '{sort_field}'...") + page = 1 + while True: + rows = report.get_paged_rows(sort_field, page=page) + + if not rows: + break + + input(f"Press ENTER to see page {page}") + + print(f"PAGE: {page} of {report.get_number_of_pages()}") + print("---------------------------------------------------------------") + + for row in rows: + print(row) + + print("---------------------------------------------------------------") + + page += 1 + + +if __name__ == "__main__": + + report = Report(4) + + report.add_row(Row("natasha", "smith", "WA")) + report.add_row(Row("devin", "lei", "WA")) + report.add_row(Row("bob", "li", "CA")) + report.add_row(Row("tracy", "jones", "OR")) + report.add_row(Row("johny", "jakes", "WA")) + report.add_row(Row("derek", "wright", "WA")) + + run_report("fname") + + print("\n\nRemoving student: " + f"{report.rows[1].fname} [{report.rows[1].row_id}]... \n\n") + + report.remove_row(report.rows[1].row_id) + + run_report("-fname") diff --git a/_downloads/a66938ee1233498247ecdfb96c52316f/test_html_output3.html b/_downloads/a66938ee1233498247ecdfb96c52316f/test_html_output3.html new file mode 100644 index 0000000..3e19aa1 --- /dev/null +++ b/_downloads/a66938ee1233498247ecdfb96c52316f/test_html_output3.html @@ -0,0 +1,13 @@ + + +PythonClass = Revision 1087: + + +

+Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+

+And here is another piece of text -- you should be able to add any number +

+ + diff --git a/_downloads/a788c1b8e163b053ac6c19b115a5473d/my_for.py b/_downloads/a788c1b8e163b053ac6c19b115a5473d/my_for.py new file mode 100644 index 0000000..6023ad9 --- /dev/null +++ b/_downloads/a788c1b8e163b053ac6c19b115a5473d/my_for.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python + +""" +hand writing 'for' + +demonstrates how for interacts with an iterable +""" + + +l = [1,2,3,4,5,] + + +def my_for(an_iterable, func): + """ + Emulation of a for loop. + + func() will be called with each item in an_iterable + + :param an_iterable: anything that satisfies the interation protocol + + :param func: a callable -- it will be called, passing in each item + in an_iterable. + + """ + # equiv of "for i in l:" + iterator = iter(an_iterable) + while True: + try: + i = next(iterator) + except StopIteration: + break + func(i) + + +if __name__ == "__main__": + + def print_func(x): + print(x) + + l = [1,2,3,4,5,] + my_for(l, print_func) + + t = ('a','b','c','d') + + my_for(t, print_func) + + + + + diff --git a/_downloads/a8258274537e67e034a65e18617ead19/test_html_output9.html b/_downloads/a8258274537e67e034a65e18617ead19/test_html_output9.html new file mode 100644 index 0000000..05dfff2 --- /dev/null +++ b/_downloads/a8258274537e67e034a65e18617ead19/test_html_output9.html @@ -0,0 +1,27 @@ + + + + + PythonClass = Revision 1087: + + +

PythonClass - Class 6 example

+

+ Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+
+
    +
  • + The first item in a list +
  • +
  • + This is the second item +
  • +
  • + And this is a + link + to google +
  • +
+ + \ No newline at end of file diff --git a/_downloads/a9b07f986f03ed4324b7bdbd58933872/test_html_output8.html b/_downloads/a9b07f986f03ed4324b7bdbd58933872/test_html_output8.html new file mode 100644 index 0000000..3ae1841 --- /dev/null +++ b/_downloads/a9b07f986f03ed4324b7bdbd58933872/test_html_output8.html @@ -0,0 +1,27 @@ + + + + +PythonClass = Revision 1087: + + +

PythonClass - Class 6 example

+

+Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+
+
    +
  • +The first item in a list +
  • +
  • +This is the second item +
  • +
  • +And this is a +link +to google +
  • +
+ + \ No newline at end of file diff --git a/_downloads/aa8dbc21673ddee50764b2251c961fe6/test_html_output9.html b/_downloads/aa8dbc21673ddee50764b2251c961fe6/test_html_output9.html new file mode 100644 index 0000000..05dfff2 --- /dev/null +++ b/_downloads/aa8dbc21673ddee50764b2251c961fe6/test_html_output9.html @@ -0,0 +1,27 @@ + + + + + PythonClass = Revision 1087: + + +

PythonClass - Class 6 example

+

+ Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+
+
    +
  • + The first item in a list +
  • +
  • + This is the second item +
  • +
  • + And this is a + link + to google +
  • +
+ + \ No newline at end of file diff --git a/_downloads/b03ea3080d30fc27151bac2341481e59/mangler_dec.py b/_downloads/b03ea3080d30fc27151bac2341481e59/mangler_dec.py new file mode 100644 index 0000000..e8ca493 --- /dev/null +++ b/_downloads/b03ea3080d30fc27151bac2341481e59/mangler_dec.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 + +""" +class decorator that adds both upper and lower case versions of +class attributes. + +Same as the NameMangler metaclass, but with a class decorator instead + +Usage example: + +@name_mangler +class Foo: + x = 1 + +f = Foo() +print(f.x) +print(f.X) +""" + + +def name_mangler(cls): + """ + Class decorator that adds upper and lower case names to the + decorated class + """ + # get the dictionary of class attributes + att_dict = vars(cls) + # create a new dict to hold the attributes + new_attrs = {} + # loop thorough all the class attributes + for name, val in att_dict.items(): + # skip all the "dunder" attributes + if not name.startswith("__"): + # Create both upper and lower case versions of all non-dunder names + # They are stored in the new_attrs dict, as you can't + # update the class namespace while looping through it. + new_attrs[name.upper()] = val + new_attrs[name.lower()] = val + # Add the new names to the cls attributes + # you can't directly update the __dict__ -- class __dict__s are not + # writable. + for name, val in new_attrs.items(): + setattr(cls, name, val) + return cls + + +@name_mangler +class Foo: + x = 1 + Y = 2 + + +# note that it works for methods, too! +@name_mangler +class Bar: + x = 1 + + def a_method(self): + print("in a_method") + + +if __name__ == "__main__": + f = Foo() + print(f.x) + print(f.X) + print(f.y) + print(f.Y) + + b = Bar() + b.A_METHOD() + diff --git a/_downloads/b0a0849e9868025eae97a36708407c70/sherlock_small.txt b/_downloads/b0a0849e9868025eae97a36708407c70/sherlock_small.txt new file mode 100644 index 0000000..dcccaab --- /dev/null +++ b/_downloads/b0a0849e9868025eae97a36708407c70/sherlock_small.txt @@ -0,0 +1,17 @@ +One night--it was on the twentieth of March, 1888--I was +returning from a journey to a patient (for I had now returned to +civil practice), when my way led me through Baker Street. As I +passed the well-remembered door, which must always be associated +in my mind with my wooing, and with the dark incidents of the +Study in Scarlet, I was seized with a keen desire to see Holmes +again, and to know how he was employing his extraordinary powers. +His rooms were brilliantly lit, and, even as I looked up, I saw +his tall, spare figure pass twice in a dark silhouette against +the blind. He was pacing the room swiftly, eagerly, with his head +sunk upon his chest and his hands clasped behind him. To me, who +knew his every mood and habit, his attitude and manner told their +own story. He was at work again. He had risen out of his +drug-created dreams and was hot upon the scent of some new +problem. I rang the bell and was shown up to the chamber which +had formerly been in part my own. + diff --git a/_downloads/b0e2494e43d9fbddc52ca6238be9e08e/test_report.py b/_downloads/b0e2494e43d9fbddc52ca6238be9e08e/test_report.py new file mode 100644 index 0000000..df51fa4 --- /dev/null +++ b/_downloads/b0e2494e43d9fbddc52ca6238be9e08e/test_report.py @@ -0,0 +1,74 @@ +""" +test code for the Report class(es) +""" + +from report import Row, Report + + +def example_report(): + """ + utility function to provide a fresh report to test with + """ + report = Report(limit=4) + + populate_report(report) + return report + + +def populate_report(report): + """ + utility function to populate an existing Report with + some additional data + + :param report: the report object to populate + + The Report will be populated in place + """ + report.add_row(Row("Natasha", "Smith", "WA")) + report.add_row(Row("Devin", "Lei", "WA")) + report.add_row(Row("Bob", "Li", "CA")) + report.add_row(Row("Tracy", "Jones", "OR")) + report.add_row(Row("Johnny", "Jakes", "WA")) + report.add_row(Row("Derek", "Wright", "WA")) + report.add_row(Row("Jordan", "Cooper", "WA")) + report.add_row(Row("Mike", "Wong", "WA")) + + +def test_row_init(): + """ + test that a new row has the proper attributes initialized + """ + row1 = Row("Joe", "Camel", "WA") + + assert row1.fname == "Joe" + assert row1.lname == "Camel" + assert row1.state == "WA" + + +def test_row_id_unique(): + """ two Rows should have unique IDs """ + row1 = Row("Joe", "Camel", "WA") + row2 = Row("Bob", "Camel", "WA") + + assert row1.id != row2.id + + +def test_report_length(): + """ + test report size method + """ + report = example_report() + + # the test data has 8 rows + assert report.size() == 8 + + +def test_number_of_pages(): + """ + check that the number of pages is correct + """ + report = example_report() + + assert report.get_number_of_pages() == 2 + + diff --git a/_downloads/b35b4e5b3f7e45e3318c046aa818ef1e/roman1.py b/_downloads/b35b4e5b3f7e45e3318c046aa818ef1e/roman1.py new file mode 100644 index 0000000..aa01e5c --- /dev/null +++ b/_downloads/b35b4e5b3f7e45e3318c046aa818ef1e/roman1.py @@ -0,0 +1,86 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + + +def to_roman(n): + '''convert an integer to Roman numeral''' + pass + + +## 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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result + + diff --git a/_downloads/b45e97459c4dc0a7c22bd8d8dbff0831/integrate.py b/_downloads/b45e97459c4dc0a7c22bd8d8dbff0831/integrate.py new file mode 100644 index 0000000..f96aadd --- /dev/null +++ b/_downloads/b45e97459c4dc0a7c22bd8d8dbff0831/integrate.py @@ -0,0 +1,44 @@ +def f(x): + return x**2 + + +def integrate(f, a, b, N): + s = 0 + dx = (b - a) / N + for i in range(N): + s += f(a + i * dx) + return s * dx + + +def integrate_f_with_functional_tools(f, a, b, N): + dx = (b - a) / N + return sum(map(f, ((a + y * dx) for y in range(N)))) * dx + + + + + + + + + + + + +# imported here so the rest of the code can run without it +import numpy as np + +def integrate_numpy(f, a, b, N): + """ + numpy can be used to "vectorize" the problem + + f must be "numpy comaptible" + + """ + dx = (b - a) / N + i = np.arange(N) + s = np.sum(f(a + (i * dx))) + return s * dx + + + diff --git a/_downloads/b522fc8f0c034466a69d85fe72bc3a0d/play_with_scope.py b/_downloads/b522fc8f0c034466a69d85fe72bc3a0d/play_with_scope.py new file mode 100644 index 0000000..a71292b --- /dev/null +++ b/_downloads/b522fc8f0c034466a69d85fe72bc3a0d/play_with_scope.py @@ -0,0 +1,47 @@ +""" +some example code to play with scope +""" + +def start_at(x): + def increment_by(y): + return x + y + return increment_by + +closure_1 = start_at(3) +closure_2 = start_at(5) +closure_1(2) +start_at(2)(4) + + +def make_um_counter(): + series = [] + def um_counter(new_word): + series.append(new_word) # free variable + count = 0 + for i in series: + if i == 'um': + count += 1 + return count + return um_counter + + +def make_um_counter2(): + count = 0 + + def um_counter2(new_word): + if new_word == 'um': + count += 1 + return count + return um_counter2 + + +# try using nonlocal: +def make_um_counter3(): + count = 0 + + def um_counter3(new_word): + nonlocal count + if new_word == 'um': + count += 1 + return count + return um_counter3 diff --git a/_downloads/b6f2b0de6e9f2a34d292dff24f62c8e4/add_book_data_flat.py b/_downloads/b6f2b0de6e9f2a34d292dff24f62c8e4/add_book_data_flat.py new file mode 100644 index 0000000..97a0869 --- /dev/null +++ b/_downloads/b6f2b0de6e9f2a34d292dff24f62c8e4/add_book_data_flat.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python + +""" +sample data for persistence/serialization examples +this version is flat for saving in CSV, ini, etc. +""" + +AddressBook = [ {'first_name': "Chris", + 'last_name': "Barker", + 'address_line_1':"835 NE 33rd St", + 'address_line_2' : "", + 'address_city' : "Seattle", + 'address_state': "WA", + 'address_zip': "96543", + 'email' : "PythonCHB@gmail.com", + 'home_phone' : "206-555-1234", + 'office_phone' : "123-456-7890", + 'cell_phone' : "234-567-8901", + }, + + {'first_name': "Fred", + 'last_name': "Jones", + 'address_line_1':"123 SE 13th St", + 'address_line_2' : "Apt. 43", + 'address_city' : "Tacoma", + 'address_state': "WA", + 'address_zip': "93465", + 'email' : "FredJones@some_company.com", + 'home_phone' : "510-555-1234", + 'office_phone' : "564-466-7990", + 'cell_phone' : "403-561-8911", + }, + + {'first_name': "Nancy", + 'last_name': "Wilson", + 'address_line_1':"8654 Walnut St", + 'address_line_2' : "Suite 567", + 'address_city' : "Pasadena", + 'address_state': "CA", + 'address_zip': "12345", + 'email' : "Wilson.Nancy@gmail.com", + 'home_phone' : "423-321-9876", + 'office_phone' : "123-765-9877", + 'cell_phone' : "432-567-8466", + }, + ] + diff --git a/_downloads/b8347debcf8506a6bee19de1c0c83b66/sherlock_small.txt b/_downloads/b8347debcf8506a6bee19de1c0c83b66/sherlock_small.txt new file mode 100644 index 0000000..dcccaab --- /dev/null +++ b/_downloads/b8347debcf8506a6bee19de1c0c83b66/sherlock_small.txt @@ -0,0 +1,17 @@ +One night--it was on the twentieth of March, 1888--I was +returning from a journey to a patient (for I had now returned to +civil practice), when my way led me through Baker Street. As I +passed the well-remembered door, which must always be associated +in my mind with my wooing, and with the dark incidents of the +Study in Scarlet, I was seized with a keen desire to see Holmes +again, and to know how he was employing his extraordinary powers. +His rooms were brilliantly lit, and, even as I looked up, I saw +his tall, spare figure pass twice in a dark silhouette against +the blind. He was pacing the room swiftly, eagerly, with his head +sunk upon his chest and his hands clasped behind him. To me, who +knew his every mood and habit, his attitude and manner told their +own story. He was at work again. He had risen out of his +drug-created dreams and was hot upon the scent of some new +problem. I rang the bell and was shown up to the chamber which +had formerly been in part my own. + diff --git a/_downloads/b85968bcab95562974c4e143f8b7514c/wikidef.zip b/_downloads/b85968bcab95562974c4e143f8b7514c/wikidef.zip new file mode 100644 index 0000000..981fcfe Binary files /dev/null and b/_downloads/b85968bcab95562974c4e143f8b7514c/wikidef.zip differ diff --git a/_downloads/b8e12ca70b948fe59efd60b802563250/oo_intro.py b/_downloads/b8e12ca70b948fe59efd60b802563250/oo_intro.py new file mode 100644 index 0000000..522e62a --- /dev/null +++ b/_downloads/b8e12ca70b948fe59efd60b802563250/oo_intro.py @@ -0,0 +1,103 @@ +import math +import operator +from uuid import uuid4 + + +class Row: + """This class represents a single row with ID, first name, last name and state attributes""" + def __init__(self, fname: str, lname: str, state: str): + self.row_id = str(uuid4()) # randomly generated unique ID + self.fname = fname + self.lname = lname + self.state = state + + def __str__(self): + return f"| {self.row_id} | {self.fname + ' ' + self.lname:<15} | {self.state} |" + + +class Report: + def __init__(self, limit: int): + self.limit = limit + self.rows = [] + + def add_row(self, row: Row) -> None: + """Add a row object to the list""" + pass + + def remove_row(self, row_id: str) -> None: + """Remove a row object by the row ID""" + pass + + def size(self) -> int: + """Return how many total rows the report has""" + pass + + def get_number_of_pages(self) -> int: + """Get how many pages the report has; this will be based on limit variable. + If your limit=4 and rows list has 6 records then there are two pages: page1 has 4 records, page2 has 2 records + hint: you'll want to round up + """ + pass + + def get_paged_rows(self, sort_field: str, page: int) -> list: + """Return a list of rows for a specific page number + :param sort_field: field to sort on, "name" or "-name" (descending) + :param page: specific page for returning data + :return: list of row objects for specific page + + Hints: + 1. you'll want to determine if sort is reversed or not (remember that sorted() takes in param for that) + this is based on if the fields start with a minus sign for DESCENDING sort + 2. when sorting on passed in field you can use handy `operator` library with `attrgetter` method (look up official docs) + 3. to actually determine what rows belong on the specific page you'll be using list slicing (remember lesson 03?) + here is an illustration to help with the code logic: + our list has 6 rows => [, , , , , ] + for page=2 we expect to get => [, ] + with slicing you'll want to offset your list by 4 in this case + (extra hint: we can define offset as `offset = (page - 1) * self.limit`) + + """ + pass + + +if __name__ == "__main__": + + report = Report(4) + + report.add_row(Row("natasha", "smith", "WA")) + report.add_row(Row("devin", "lei", "WA")) + report.add_row(Row("bob", "li", "CA")) + report.add_row(Row("tracy", "jones", "OR")) + report.add_row(Row("johny", "jakes", "WA")) + report.add_row(Row("derek", "wright", "WA")) + + + def run_report(sort_field): + print(f"... PAGED REPORT SORTED BY: '{sort_field}'...") + page = 1 + while True: + rows = report.get_paged_rows(sort_field, page=page) + + if not rows: + break + + input(f"Press ENTER to see page {page}") + + print(f"PAGE: {page} of {report.get_number_of_pages()}") + print("---------------------------------------------------------------") + + for row in rows: + print(row) + + print("---------------------------------------------------------------") + + page += 1 + + + run_report("fname") + + print(f"\n\nRemoving student: {report.rows[1].fname} [{report.rows[1].row_id}]... \n\n") + + report.remove_row(report.rows[1].row_id) + + run_report("-fname") diff --git a/_downloads/b8e9fa621bddde6623aa18c3db828f62/integrate_threads.py b/_downloads/b8e9fa621bddde6623aa18c3db828f62/integrate_threads.py new file mode 100644 index 0000000..16c0d7f --- /dev/null +++ b/_downloads/b8e9fa621bddde6623aa18c3db828f62/integrate_threads.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 + +import threading +import queue + +# from integrate.integrate import integrate, f +from integrate import f, integrate_numpy as integrate +from decorators import timer + + +@timer +def threading_integrate(f, a, b, N, thread_count=2): + """break work into N chunks""" + N_chunk = int(float(N) / thread_count) + dx = float(b - a) / thread_count + + results = queue.Queue() + + def worker(*args): + results.put(integrate(*args)) + + for i in range(thread_count): + x0 = dx * i + x1 = x0 + dx + thread = threading.Thread(target=worker, args=(f, x0, x1, N_chunk)) + thread.start() + print("Thread %s started" % thread.name) + + return sum((results.get() for i in range(thread_count))) + + +if __name__ == "__main__": + + # parameters of the integration + a = 0.0 + b = 10.0 + N = 10**8 + thread_count = 1 + + print("Numerical solution with N=%(N)d : %(x)f" % + {'N': N, 'x': threading_integrate(f, a, b, N, thread_count=thread_count)}) + diff --git a/_downloads/b924bf5f9d157f280183dad684e589a2/quadratic.py b/_downloads/b924bf5f9d157f280183dad684e589a2/quadratic.py new file mode 100644 index 0000000..4b78223 --- /dev/null +++ b/_downloads/b924bf5f9d157f280183dad684e589a2/quadratic.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +""" +A quaratic function evaluator + +used to demonstrate callable classes +""" + +class Quadratic: + """ + Class to evaluate quadratic equations + + Each instance wil have a certain set of coefficients + """ + + def __init__(self, A, B, C): + self.A = A + self.B = B + self.C = C + + def __call__(self, x): + return self.A * x**2 + self.B * x + self.C + \ No newline at end of file diff --git a/_downloads/ba31ee4e6e338fd38619c3cec6019f2d/roman7.py b/_downloads/ba31ee4e6e338fd38619c3cec6019f2d/roman7.py new file mode 100644 index 0000000..beb34ae --- /dev/null +++ b/_downloads/ba31ee4e6e338fd38619c3cec6019f2d/roman7.py @@ -0,0 +1,140 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + +import pytest + +roman_numeral_map = (('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1)) + + +def to_roman(n): + """convert integer to Roman numeral""" + if not (0 < n < 4000): + raise ValueError("number out of range (must be 1..3999)") + + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + return result + + +## 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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result + + +def test_too_large(): + """ + to_roman should raise an ValueError when passed + values over 3999 + """ + with pytest.raises(ValueError): + to_roman(4000) + + +def test_zero(): + """to_roman should raise an ValueError with 0 input""" + 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) + + +def test_non_integer(): + """to_roman should raise an ValueError with non-integer input""" + with pytest.raises(ValueError): + to_roman(0.5) + + +def test_float_with_integer_value(): + """to_roman should work for floats with integer values""" + assert to_roman(3.0) == "III" diff --git a/_downloads/ba3e39ccf5277be8300f07a581cf05f6/oo_intro.py b/_downloads/ba3e39ccf5277be8300f07a581cf05f6/oo_intro.py new file mode 100644 index 0000000..66601e5 --- /dev/null +++ b/_downloads/ba3e39ccf5277be8300f07a581cf05f6/oo_intro.py @@ -0,0 +1,107 @@ +import math +import operator +from uuid import uuid4 + + +class Row: + """ + This class represents a single row + with ID, first name, last name and state attributes + """ + def __init__(self, fname, lname, state): + self.row_id = str(uuid4()) # randomly generated unique ID + self.fname = fname + self.lname = lname + self.state = state + + def __str__(self): + return f"| {self.row_id} | {self.fname + ' ' + self.lname:<15} | {self.state} |" + + +class Report: + def __init__(self, limit: int): + self.limit = limit + self.rows = [] + + def add_row(self, row: Row) -> None: + """Add a row object to the list""" + pass + + def remove_row(self, row_id: str) -> None: + """Remove a row object by the row ID""" + pass + + def size(self) -> int: + """Return how many total rows the report has""" + pass + + def get_number_of_pages(self) -> int: + """ + Get how many pages the report has; this will be based on limit variable. + If your limit=4 and rows list has 6 records then there are two pages: page1 has 4 records, page2 has 2 records + hint: you'll want to round up + """ + pass + + def get_paged_rows(self, sort_field, page): + """Return a list of rows for a specific page number + :param sort_field: field to sort on, "name" or "-name" (descending) + :param page: specific page for returning data + :return: list of row objects for specific page + + Hints: + 1. you'll want to determine if sort is reversed or not (remember that sorted() takes in param for that) + this is based on if the fields start with a minus sign for DESCENDING sort + 2. when sorting on passed in field you can use handy `operator` library with `attrgetter` method (look up official docs) + 3. to actually determine what rows belong on the specific page you'll be using list slicing (remember lesson 03?) + here is an illustration to help with the code logic: + our list has 6 rows => [, , , , , ] + for page=2 we expect to get => [, ] + with slicing you'll want to offset your list by 4 in this case + (extra hint: we can define offset as `offset = (page - 1) * self.limit`) + + """ + pass + + +if __name__ == "__main__": + + report = Report(4) + + report.add_row(Row("natasha", "smith", "WA")) + report.add_row(Row("devin", "lei", "WA")) + report.add_row(Row("bob", "li", "CA")) + report.add_row(Row("tracy", "jones", "OR")) + report.add_row(Row("johny", "jakes", "WA")) + report.add_row(Row("derek", "wright", "WA")) + + + def run_report(sort_field): + print(f"... PAGED REPORT SORTED BY: '{sort_field}'...") + page = 1 + while True: + rows = report.get_paged_rows(sort_field, page=page) + + if not rows: + break + + input(f"Press ENTER to see page {page}") + + print(f"PAGE: {page} of {report.get_number_of_pages()}") + print("---------------------------------------------------------------") + + for row in rows: + print(row) + + print("---------------------------------------------------------------") + + page += 1 + + + run_report("fname") + + print(f"\n\nRemoving student: {report.rows[1].fname} [{report.rows[1].row_id}]... \n\n") + + report.remove_row(report.rows[1].row_id) + + run_report("-fname") diff --git a/_downloads/bb22abb4db949be359ed622d72ba6613/exception_test.py b/_downloads/bb22abb4db949be359ed622d72ba6613/exception_test.py new file mode 100644 index 0000000..975f1df --- /dev/null +++ b/_downloads/bb22abb4db949be359ed622d72ba6613/exception_test.py @@ -0,0 +1,16 @@ +#!/usr/bin/python + +""" +example for what happens when you pass non-ascii unicode to a Exception +""" + +msg = u'This is an ASCII-compatible unicode message' + +#msg = u'This is an non ASCII\N{EM DASH}compatible unicode message' + +print "\nDo you see this message in the Exception report?\n" +print msg +print + +raise ValueError(msg) + diff --git a/_downloads/bd77493fdc2572f5806b0a1dca6d2f5b/sort_key.py b/_downloads/bd77493fdc2572f5806b0a1dca6d2f5b/sort_key.py new file mode 100644 index 0000000..e10df5a --- /dev/null +++ b/_downloads/bd77493fdc2572f5806b0a1dca6d2f5b/sort_key.py @@ -0,0 +1,55 @@ +""" +demonstration of defining a sort_key method for sorting +""" + +import random +import time + + +class Simple: + """ + simple class to demonstrate a simple sorting key method + """ + + def __init__(self, val): + self.val = val + + def sort_key(self): + """ + sorting key function --used to pass in to sort functions + to get faster sorting + + Example:: + + sorted(list_of_simple_objects, key=Simple.sort_key) + + """ + return self.val + + def __lt__(self, other): + """ + less than --required for regular sorting + """ + return self.val < other.val + + def __repr__(self): + return "Simple({})".format(self.val) + + +if __name__ == "__main__": + N = 10000 + a_list = [Simple(random.randint(0, 10000)) for i in range(N)] + # print("Before sorting:", a_list) + + print("Timing for {} items".format(N)) + start = time.clock() + sorted(a_list) + reg_time = time.clock() - start + print("regular sort took: {:.4g}s".format(reg_time)) + + start = time.clock() + sorted(a_list, key=Simple.sort_key) + key_time = time.clock() - start + print("key sort took: {:.4g}s".format(key_time)) + + print("performance improvement factor: {:.4f}".format((reg_time / key_time))) diff --git a/_downloads/bda7b12018915aae35c5aa0ad8d76cc0/play_with_imports.py b/_downloads/bda7b12018915aae35c5aa0ad8d76cc0/play_with_imports.py new file mode 100644 index 0000000..deaaf37 --- /dev/null +++ b/_downloads/bda7b12018915aae35c5aa0ad8d76cc0/play_with_imports.py @@ -0,0 +1,26 @@ + +print('importing') + + +def my_decorator(func): + print('in my_decorator for: ', func.__name__) + def inner(): + print('in inner function') + func() + print('in inner after decorated function') + return inner + + +@my_decorator +def first_func(): + print('running first_func') + +@my_decorator +def other_func(): + print('running other_func') + +print('imports and loading done') + +if __name__ == '__main__': + print('run script') + other_func() diff --git a/_downloads/be3e0f48a8715ea51d96aa45c4451685/roman2.py b/_downloads/be3e0f48a8715ea51d96aa45c4451685/roman2.py new file mode 100644 index 0000000..8503720 --- /dev/null +++ b/_downloads/be3e0f48a8715ea51d96aa45c4451685/roman2.py @@ -0,0 +1,104 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + +roman_numeral_map = (('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1)) + + +def to_roman(n): + '''convert integer to Roman numeral''' + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + # print(f'subtracting {integer} from input, adding {numeral} to output') + return result + + +## 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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result diff --git a/_downloads/be423580dbf23aa061a5ae35b913c491/roman5.py b/_downloads/be423580dbf23aa061a5ae35b913c491/roman5.py new file mode 100644 index 0000000..fba421c --- /dev/null +++ b/_downloads/be423580dbf23aa061a5ae35b913c491/roman5.py @@ -0,0 +1,132 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + +import pytest + +roman_numeral_map = (('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1)) + + +def to_roman(n): + """convert integer to Roman numeral""" + if n > 3999: + raise ValueError("number out of range (must be less than 4000)") + + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + return result + + +## 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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result + + +def test_too_large(): + """ + to_roman should raise an ValueError when passed + values over 3999 + """ + with pytest.raises(ValueError): + to_roman(4000) + + +def test_zero(): + """to_roman should raise an ValueError with 0 input""" + 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) + + + diff --git a/_downloads/be77c514aeb0327315e4a8a69e2fdadd/simple_classes.py b/_downloads/be77c514aeb0327315e4a8a69e2fdadd/simple_classes.py new file mode 100644 index 0000000..4f99606 --- /dev/null +++ b/_downloads/be77c514aeb0327315e4a8a69e2fdadd/simple_classes.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +""" +simple_classes.py + +demonstrating the basics of a class +""" + +import math + + +# create a point class +class Point: + def __init__(self, x, y): + self.x = x + self.y = y + +# create an instance of that class +p = Point(3, 4) + +# access the attributes +print("p.x is:", p.x) +print("p.y is:", p.y) + + +class Point2: + size = 4 + color = "red" + + def __init__(self, x, y): + self.x = x + self.y = y + +p2 = Point2(4, 5) +print(p2.size) +print(p2.color) + + +class Point3: + size = 4 + color = "red" + + def __init__(self, x, y): + self.x = x + self.y = y + + def get_color(self): + return self.color + + def get_size(self): + return self.size + +class Rect: + + def __init__(self, w, h): + self.w = w + self.h = h + + def get_size(self): + return self.w * self.h + + +p3 = Point3(4, 5) +print(p3.size) +print(p3.get_color()) + + +class Circle: + color = "red" + styles = ['dashed'] + + def __init__(self, diameter): + self.diameter = diameter + + def grow(self, factor=2): + """ + grows the circle's diameter + + :param factor=2: factor by which to grow the circle + """ + self.diameter = self.diameter * factor + + def add_style(self, style): + self.styles.append(style) + + def get_area(self): + return math.pi * self.diameter / 2.0 + + +class NewCircle(Circle): + color = "blue" + + def grow(self, factor=2): + """grows the area by factor...""" + self.diameter = self.diameter * math.sqrt(2) + +nc = NewCircle +print(nc.color) + + +class CircleR(Circle): + def __init__(self, radius): + diameter = radius*2 + Circle.__init__(self, diameter) + + +class CircleR2(Circle): + def __init__(self, radius): + self.radius = radius + + def get_area(self): + return Circle.get_area(self, self.radius*2) diff --git a/_downloads/bf4f66243b1821869ff6db4d355b7793/race_condition.py b/_downloads/bf4f66243b1821869ff6db4d355b7793/race_condition.py new file mode 100644 index 0000000..49434eb --- /dev/null +++ b/_downloads/bf4f66243b1821869ff6db4d355b7793/race_condition.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 + +import threading +import time + + +# create a mutable object that is shared among threads +class shared: + val = 1 + + +def func(): + y = shared.val + time.sleep(0.00001) + y += 1 + shared.val = y + + +threads = [] +# with enough threads, there's sufficient overhead to +# cause a race condition +for i in range(100): + thread = threading.Thread(target=func) + threads.append(thread) + thread.start() + +for thread in threads: + thread.join() + +print(shared.val) + diff --git a/_downloads/c072df20f9f73c097607b05c3d32a26b/sample_html.html b/_downloads/c072df20f9f73c097607b05c3d32a26b/sample_html.html new file mode 100644 index 0000000..9c2e675 --- /dev/null +++ b/_downloads/c072df20f9f73c097607b05c3d32a26b/sample_html.html @@ -0,0 +1,27 @@ + + + + + Python Class Sample page + + +

Python Class - Html rendering example

+

+ Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+
+
    +
  • + The first item in a list +
  • +
  • + This is the second item +
  • +
  • + And this is a + link + to google +
  • +
+ + \ No newline at end of file diff --git a/_downloads/c0f1640b9673725156d0b85a27003d5a/super_test.py b/_downloads/c0f1640b9673725156d0b85a27003d5a/super_test.py new file mode 100644 index 0000000..0a73e93 --- /dev/null +++ b/_downloads/c0f1640b9673725156d0b85a27003d5a/super_test.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 + +""" +Some example code demonstrating some super() behaviour +""" + +# Define a multiple inheritance scheme: +class A(): + def __init__(self): + print("in A __init__") + print("self's class is:", self.__class__) + s = super().__init__() + + +class B(): + def __init__(self): + print("in B.__init__") + s = super().__init__() + + +class C(): + def __init__(self): + print("in C.__init__") + s = super().__init__() + + +class D(C, B, A): + def __init__(self): + print("self's class is:", self.__class__) + super().__init__() + +# print our D's method resoluton order +# Is it what you expect? +print("\nD's mro:") +print( D.__mro__) +# see what happens when you create a D object: +print("\ninitializing a D object:") +d = D() + + +# ## super's parameters +# To do its thing, super() needs to know two things: +# +# ``super(type, obj)`` +# +# It needs to know that type (class) that you want the super-classes of, +# AND it needs to know the actual object instance at the time it is called. +# +# python3 fills these in for you at run time, but in python2, you needed to +# specify them: +# +# ``` +# class A(object): +# def __init__(self): +# super(A, self).__init__() +# ``` +# +# But why do you need BOTH `A` and `self`? -- isn't `self` an instance of `A`? +# +# Not neccesarily -- if A's method is being called from a subclass, then +# `self` will be an instance of the subclass. `super()` requires that the object be an instance of the class (or a subclass). +# +# This distiction will come up later.... +# +# Again, py3 takes care of this for you, though you CAN still spell it out. + +# see what you get with super by itself: +s_c = super(C, d) +print("\n the super object of super(C, d) itself") +print(s_c) + + +# This works because `d` is a `D` object, which is a subclass of `C`. + +# create a C instance: +c = C() + +# and try to create a super with the missmatch +print("\n the super object of super(D, c)") +try: + super(D, c) +except TypeError as err: + print(err) +# But this gives a TypeError: `C` is NOT a subclass of `D` + +# it doesn't have to be an exact intance, jsust a subclass: + +print("\n the super object of super(A, d)") +s_a = super(A, d) +print(s_a) + + +print("\n the super object of super(B, d)") +s_b = super(B, d) +print(s_b) + +print("\nD inherits from both A and B, so that worked...") + +print("\nD's MRO:") +print(D.__mro__) + + +# An Example of why you want to use super() everywhere. + +# Classes without super() + +class A(): + def this(self): + print("in A.this") + +class B(): + def this(self): + print("in B.this") + +class C(A,B): + def this(self): + print("in C.this") + A.this(self) + B.this(self) + +print("\nRunning without super()") +print("Creating a C instance without super() -- and calling it's this method:") +c = C() +c.this() + +print("C's `this` explicitly called both A and B's methods -- so they all get called.") + +# Using super in just C: + +print("\n using super() in C, but not everywhere...") + +class A(): + def this(self): + print("in A.this") + +class B(A): + def this(self): + print("in B.this") + +class C(B): + def this(self): + print("in C.this") + super().this() + + +print("\n C's MRO") +print(C.__mro__) + +print("\ncreating a C instance and calling it's this() method:") +c = C() +c.this() + +print("**NOTE: `A.this` did NOT get called!") + +# **Note:** `A.this` did NOT get called! +# +# Even though it is in in the MRO. +# +# Python stopped when it found the method in B. + +# ### Using super everywhere: + +print("using super everywhere:") + +class Base(): + def this(self): + pass # just so there is a base that has the method + +class A(Base): + def this(self): + print("in A.this") + super().this() + +class B(Base): + def this(self): + print("in B.this") + super().this() +class C(A,B): + def this(self): + print("in C.this") + super().this() + + +print("\nnow create a C instance and call its this() method:") +c = C() +c.this() + +print("Now both A and B's methods get called -- probably what you want.") + +print("\nAnd the MRO of the base class") +print(Base.__mro__) + +# But if you don't want both called -- better to just be Explicit, rather than use super(): + +class Base(): + def this(self): + pass # just so there is a base that has the method + +class A(Base): + def this(self): + print("in A.this") + super().this() + +class B(Base): + def this(self): + print("in B.this") + super().this() + +class C(A,B): + def this(self): + print("in C.this") + A.this(self) + +print("\nIf you want total control of what methods get called --don't use super" + "and be explicit") + +c = C() +c.this() + + +print("**Whoa** -- A and B's method DID get called! -- why?") + +print("A's MRO:") +print(A.__mro__) + + +print("B is not there") + +print("\nBut if we print the class of each instance when this() is called") + +class Base(): + def this(self): + pass # just so there is a base that has the method + +class A(Base): + def this(self): + print("in A.this") + print("self's class in A's this method:", self.__class__) + super().this() + +class B(Base): + def this(self): + print("in B.this") + super().this() + +class C(A,B): + def this(self): + print("in C.this") + A.this(self) + + +print("and create a c instance and call this():") +c = C() +c.this() + +print("In A's this method -- self is a C object") + +print("C's MRO:") +print(C.__mro__) + +print("\nRemember, `super()` is dynamic -- what it calls is determined at run time.") +print("That's how it knows to call ``B``'s method too.") +print("Which is why we say that using `super()` is *part* of the interface of" + "the class.") diff --git a/_downloads/c1cc9b924fb183dcd627bdd88e164621/integrate.py b/_downloads/c1cc9b924fb183dcd627bdd88e164621/integrate.py new file mode 100644 index 0000000..f96aadd --- /dev/null +++ b/_downloads/c1cc9b924fb183dcd627bdd88e164621/integrate.py @@ -0,0 +1,44 @@ +def f(x): + return x**2 + + +def integrate(f, a, b, N): + s = 0 + dx = (b - a) / N + for i in range(N): + s += f(a + i * dx) + return s * dx + + +def integrate_f_with_functional_tools(f, a, b, N): + dx = (b - a) / N + return sum(map(f, ((a + y * dx) for y in range(N)))) * dx + + + + + + + + + + + + +# imported here so the rest of the code can run without it +import numpy as np + +def integrate_numpy(f, a, b, N): + """ + numpy can be used to "vectorize" the problem + + f must be "numpy comaptible" + + """ + dx = (b - a) / N + i = np.arange(N) + s = np.sum(f(a + (i * dx))) + return s * dx + + + diff --git a/_downloads/c205fe025d5cbf59e762b22d0523d062/test_html_output5.html b/_downloads/c205fe025d5cbf59e762b22d0523d062/test_html_output5.html new file mode 100644 index 0000000..e7efbfc --- /dev/null +++ b/_downloads/c205fe025d5cbf59e762b22d0523d062/test_html_output5.html @@ -0,0 +1,11 @@ + + +PythonClass = Revision 1087: + + +

+Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+
+ + \ No newline at end of file diff --git a/_downloads/c20ef66feb5e95ea96f5dc621fe06743/test_html_output3.html b/_downloads/c20ef66feb5e95ea96f5dc621fe06743/test_html_output3.html new file mode 100644 index 0000000..3e19aa1 --- /dev/null +++ b/_downloads/c20ef66feb5e95ea96f5dc621fe06743/test_html_output3.html @@ -0,0 +1,13 @@ + + +PythonClass = Revision 1087: + + +

+Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+

+And here is another piece of text -- you should be able to add any number +

+ + diff --git a/_downloads/c4dba83b27b437ce9716ff63c603d5cd/except_test.py b/_downloads/c4dba83b27b437ce9716ff63c603d5cd/except_test.py new file mode 100644 index 0000000..905dd67 --- /dev/null +++ b/_downloads/c4dba83b27b437ce9716ff63c603d5cd/except_test.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +""" +silly little test module that is designed to trigger Exceptions when +run from the except_exercise.py file +""" + +import time + +conclude = "And what leads you to that conclusion?" +district = "Finest in the district, sir." +cheese = "It's certainly uncontaminated by cheese." +clean = "Well, it's so clean." +shop = "Not much of a cheese shop really, is it?" +cust = "Customer: " +clerk = "Shopkeeper: " + + +def fun(reaper): + if reaper == 'spam': + print(s) + elif reaper == 'cheese': + print() + print('Spam, Spam, Spam, Spam, Beautiful Spam') + elif reaper == 'mr death': + print() + return('{}{}\n{}{}'.format(cust, shop, clerk, district)) + + +def more_fun(language): + if language == 'java': + test = [1, 2, 3] + test[5] = language + elif language == 'c': + print('{}{}\n{}{}'.format(cust, conclude, clerk, clean)) + + +def last_fun(): + print(cust, cheese) + time.sleep(1) + import antigravity diff --git a/_downloads/c71f489791c94e7196c17ed340e069b0/latin1_test.py b/_downloads/c71f489791c94e7196c17ed340e069b0/latin1_test.py new file mode 100644 index 0000000..3990078 --- /dev/null +++ b/_downloads/c71f489791c94e7196c17ed340e069b0/latin1_test.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python + +""" +An example of using latin-1 as a universal encoding + +latin-1 is a superset of ASCII that is suitable for western european languages. + +Is the most common, and a good default, if you need a one-byte per char encoding +for European text. + +It also has a nice property: + : every byte value from 0 to 255 is avalid charactor + +Thus you will never get an UnicodeDecodeError if +you try to decode arbitrary bytes with latin-1. + +And it can "round-trip" trhough a unicode object. + +This can be useful is you don't know the encoding -- at least it won't break. +It's also useful if you need to work with cobined text+binary data. + + + +""" + +# all the byte values in a bytes (str) object: +all_bytes = ''.join( [chr(i) for i in range(255)] ) + +print type(all_bytes) +print len(all_bytes) + +print "Example value: 20" +print ord(all_bytes[20]) == 20 +print "Example high value: 245" +print ord(all_bytes[245]) == 245 + +# now decode it to a unicode object: +try: + uni = all_bytes.decode() +except UnicodeDecodeError: + print "OOPS: can't decode with default encoding" + +# latin-1 works: +try: + all_uni = all_bytes.decode('latin-1') + print "Yup -- that worked" + print all_uni + print "note that the ASCII subset is the same..." +except UnicodeDecodeError: + print "OOPS: This should have worked!!" + raise + +## now show that it round-trips: +all_bytes2 = all_uni.encode('latin-1') + +if all_bytes2 == all_bytes: + print "yup -- that worked...the values are preserved on the round trip." +else: + print "Hey, that should have worked" + + + + + + + diff --git a/_downloads/c86a27b37d304372a3f384aed19fcd5c/calculator_test.sh b/_downloads/c86a27b37d304372a3f384aed19fcd5c/calculator_test.sh new file mode 100644 index 0000000..f43dc38 --- /dev/null +++ b/_downloads/c86a27b37d304372a3f384aed19fcd5c/calculator_test.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +test1="2 + 3" +test2="2 - 3" + +echo -n $test1 "= " +./calculator.py $test1 +echo -n $test2 "= " +./calculator.py $test2 diff --git a/_downloads/c92f8e9347b133df651ea72298f5da2b/roman15.py b/_downloads/c92f8e9347b133df651ea72298f5da2b/roman15.py new file mode 100644 index 0000000..360ba0d --- /dev/null +++ b/_downloads/c92f8e9347b133df651ea72298f5da2b/roman15.py @@ -0,0 +1,293 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + +import pytest + +roman_numeral_map = (('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1)) + + +def to_roman(n): + """convert integer to Roman numeral""" + if not (0 < n < 4000): + raise ValueError("number out of range (must be 1..3999)") + + if int(n) != n: + raise ValueError("Only integers can be converted to Roman numerals") + + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + return result + + +def is_valid_roman_numeral(s): + """ + parse a Roman numeral as a human would: left to right, + looking for valid characters and removing them to determine + if this is, indeed, a valid Roman numeral + """ + # first check if uses only valid characters + for c in s: + if c not in "MDCLXVI": + return False + + print("starting to parse") + print("the thousands") + print(f"{s = }") + # first look for the thousands -- up to three Ms + for _ in range(3): + if s[:1] == "M": + s = s[1:] + # then look for the hundreds: + print("the hundreds") + print(f"{s = }") + # there can be only one of CM, CD, or D: + if s[:2] == "CM": # 900 + s = s[2:] + elif s[:2] == "CD": # 400 + s = s[2:] + else: + if s[:1] == "D": # 500 + s = s[1:] + # there can be from 1 to 3 Cs + for _ in range(3): + if s[:1] == "C": + s = s[1:] + # now the tens + print("the tens") + print(f"{s = }") + # There can be one of either XC, XL or L + if s[:2] == "XC": # 90 + s = s[2:] + elif s[:2] == "XL": # 40 + s = s[2:] + else: + if s[:1] == "L": # 50 + s = s[1:] + # there can be up to three Xs + for _ in range(3): + if s[:1] == "X": + s = s[1:] + # and the ones + print("the ones") + print(f"{s = }") + # There can be one of IX, IV or V + if s[:2] == "IX": # 9 + s = s[2:] + elif s[:2] == "IV": # 4 + s = s[2:] + elif s[:1] == "V": # 5 + s = s[1:] + print("looking for the Is") + print(f"{s = }") + # There can be up to three Is + for _ in range(3): + if s[:1] == "I": # 1 + s = s[1:] + # if there is anything left, it's not a valid Roman numeral + print("done") + print(f"{s = }") + if s: + return False + else: + return True + + +def from_roman(s): + """convert Roman numeral to integer""" + if not is_valid_roman_numeral(s): + raise ValueError(f"{s} is not a valid Roman numeral") + result = 0 + index = 0 + for numeral, integer in roman_numeral_map: + while s[index:index + len(numeral)] == numeral: + result += integer + index += len(numeral) + # print(f'found, {numeral} of length, {len(numeral)} adding {integer}') + return result + + +##################################### +# 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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result + + +def test_too_large(): + """ + to_roman should raise an ValueError when passed + values over 3999 + """ + with pytest.raises(ValueError): + to_roman(4000) + + +def test_zero(): + """to_roman should raise an ValueError with 0 input""" + 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) + + +def test_non_integer(): + """to_roman should raise an ValueError with non-integer input""" + with pytest.raises(ValueError): + to_roman(0.5) + + +def test_float_with_integer_value(): + """to_roman should work for floats with integer values""" + assert to_roman(3.0) == "III" + +# #################### +# Tests for from_roman + + +def test_from_roman_known_values(): + """from_roman should give known result with known input""" + for integer, numeral in KNOWN_VALUES: + result = from_roman(numeral) + assert integer == result + + +def test_roundtrip(): + """from_roman(to_roman(n))==n for all n""" + for integer in range(1, 4000): + numeral = to_roman(integer) + result = from_roman(numeral) + assert integer == result + + +# ##################### +# The following to test for valid roman numerals + +def test_invalid_character(): + """ + Roman numerals can only use these characters: + + M, D, C, L, X, V, I + + This tests that other characters will cause a failure + """ + for s in ['Z', 'XXIIIQ', 'QXXIII', 'XXYIII']: + with pytest.raises(ValueError): + from_roman(s) + + +def test_too_many_repeated_numerals(): + '''from_roman should fail with too many repeated numerals''' + for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'): + with pytest.raises(ValueError): + print(f"trying: {s}") + from_roman(s) + + +def test_repeated_pairs(): + '''from_roman should fail with repeated pairs of numerals''' + for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'): + with pytest.raises(ValueError): + print(f"trying: {s}") + from_roman(s) + + +def test_malformed_antecedents(): + '''from_roman should fail with malformed antecedents''' + for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV', + 'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'): + with pytest.raises(ValueError): + print(f"trying: {s}") + from_roman(s) + + diff --git a/_downloads/cd0d110b299e491b9cbd63083e7f9f5e/series_template.py b/_downloads/cd0d110b299e491b9cbd63083e7f9f5e/series_template.py new file mode 100644 index 0000000..fb161d7 --- /dev/null +++ b/_downloads/cd0d110b299e491b9cbd63083e7f9f5e/series_template.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 + +""" +a template for the series assignment +""" + + +def fibonacci(n): + """ compute the nth Fibonacci number """ + pass + + +def lucas(n): + """ compute the nth Lucas number """ + pass + + +def sum_series(n, n0=0, n1=1): + """ + compute the nth value of a summation series. + + :param n0=0: value of zeroth element in the series + :param n1=1: value of first element in the series + + This function should generalize the fibonacci() and the lucas(), + so that this function works for any first two numbers for a sum series. + Once generalized that way, sum_series(n, 0, 1) should be equivalent to fibonacci(n). + And sum_series(n, 2, 1) should be equivalent to lucas(n). + + sum_series(n, 3, 2) should generate antoehr series with no specific name + + The defaults are set to 0, 1, so if you don't pass in any values, you'll + get the fibonacci sercies + """ + pass + +if __name__ == "__main__": + # run some tests + assert fibonacci(0) == 0 + assert fibonacci(1) == 1 + assert fibonacci(2) == 1 + assert fibonacci(3) == 2 + assert fibonacci(4) == 3 + assert fibonacci(5) == 5 + assert fibonacci(6) == 8 + assert fibonacci(7) == 13 + + assert lucas(0) == 2 + assert lucas(1) == 1 + + assert lucas(4) == 7 + + # test that sum_series matches fibonacci + assert sum_series(5) == fibonacci(5) + assert sum_series(7, 0, 1) == fibonacci(7) + + # test if sum_series matched lucas + assert sum_series(5, 2, 1) == lucas(5) + + # test if sum_series works for arbitrary initial values + assert sum_series(0, 3, 2) == 3 + assert sum_series(1, 3, 2) == 2 + assert sum_series(2, 3, 2) == 5 + assert sum_series(3, 3, 2) == 7 + assert sum_series(4, 3, 2) == 12 + assert sum_series(5, 3, 2) == 19 + + print("tests passed") diff --git a/_downloads/cd1f82fcc00d02d1af7aae379d84f37e/pytest_fixtures.py b/_downloads/cd1f82fcc00d02d1af7aae379d84f37e/pytest_fixtures.py new file mode 100644 index 0000000..36d7434 --- /dev/null +++ b/_downloads/cd1f82fcc00d02d1af7aae379d84f37e/pytest_fixtures.py @@ -0,0 +1,60 @@ +#!usr/bin/env python + +""" +example to show how fixtures work in pytest + +to run this and see the output: + +py.test -s -v pytest_fixtures.py +""" + +import pytest + + +@pytest.fixture +# with module-level scope +#@pytest.fixture(scope="module") +def example_fixture(): + """ + An example fixture that does nothing useful + + But does return an object you can use for testing + """ + print("I am running the fixture now") + return {"this": 3, + "that": 2} + + +# now use the fixture in a couple tests +def test_one(example_fixture): + print("running test_one") + assert example_fixture["this"] == 3 + + +def test_two(example_fixture): + print("running test_two") + assert example_fixture["that"] == 2 + +# with teardown: +@pytest.fixture(scope="module") +def example_fixture2(): + """ + An example fixture that does nothing useful + + But does return an object you can use for testing + """ + print("I am running the fixture now") + yield {"this": 3, + "that": 2} + print("and now I am running the teardown code") + + +# using the fixture with teardown: +def test_three(example_fixture2): + print("running test_three") + assert example_fixture2["this"] == 3 + + +def test_four(example_fixture2): + print("running test_four") + assert example_fixture2["this"] == 3 diff --git a/_downloads/cdb476294acbec5e9f1294374ec488e2/test_calculator.py b/_downloads/cdb476294acbec5e9f1294374ec488e2/test_calculator.py new file mode 100644 index 0000000..bbb129c --- /dev/null +++ b/_downloads/cdb476294acbec5e9f1294374ec488e2/test_calculator.py @@ -0,0 +1,39 @@ +import unittest + +import calculator_functions as calc + + +def setUpModule(): + print("running setup module") + + +class TestCalculatorFunctions(unittest.TestCase): + + def setUp(self): + print("running setup") + self.x = 2 + self.y = 3 + + def tearDown(self): + print("running teardown") + + def test_add(self): + print("running test_add") + self.assertEqual(calc.add(self.x, self.y), 5) + + def test_add2(self): + print("running test_add2") + self.assertEqual(calc.add(7, 8), 15) + + +# class TestCalculatorFunctions2(unittest.TestCase): + +# def setUp(self): +# self.x = 2 +# self.y = 3 + +# def test_add(self): +# self.assertEqual(calc.subtract(self.y, self.x), 1) + +if __name__ == "__main__": + unittest.main() diff --git a/_downloads/cfd6afe51bc5be67deffa8b0c595753c/get_news_sync.py b/_downloads/cfd6afe51bc5be67deffa8b0c595753c/get_news_sync.py new file mode 100644 index 0000000..82a7b13 --- /dev/null +++ b/_downloads/cfd6afe51bc5be67deffa8b0c595753c/get_news_sync.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python + +""" +Regular synchronous script to see how much a given word is mentioned in the +news today + +Took about 21 seconds for me. + +Uses data from the NewsAPI: + +https://newsapi.org + +NOTE: you need to register with the web site to get a KEY. +""" +import time +import requests + +WORD = "trump" + +NEWS_API_KEY = "84d0483394c44f288965d7b366e54a74" + +base_url = 'https://newsapi.org/v1/' + + +def get_sources(): + """ + Get all the english language sources of news + + 'https://newsapi.org/v1/sources?language=en' + """ + url = base_url + "sources" + params = {"language": "en"} + resp = requests.get(url, params=params) + data = resp.json() + sources = [src['id'].strip() for src in data['sources']] + print("all the sources") + print(sources) + return sources + + +def get_articles(source): + """ + https://newsapi.org/v1/articles?source=associated-press&sortBy=top&apiKey=1fabc23bb9bc485ca59b3966cbd6ea26 + """ + url = base_url + "articles" + params = {"source": source, + "apiKey": NEWS_API_KEY, + # "sortBy": "latest", # some sources don't support latest + "sortBy": "top", + # "sortBy": "popular", + } + print("requesting:", source) + resp = requests.get(url, params=params) + if resp.status_code != 200: # aiohttpp has "status" + print("something went wrong with {}".format(source)) + print(resp) + print(resp.text) + return [] + data = resp.json() + # the url to the article itself is in data['articles'][i]['url'] + titles = [str(art['title']) + str(art['description']) + for art in data['articles']] + return titles + + +def count_word(word, titles): + word = word.lower() + count = 0 + for title in titles: + if word in title.lower(): + count += 1 + return count + + +start = time.time() +sources = get_sources() + +art_count = 0 +word_count = 0 +for source in sources: + titles = get_articles(source) + art_count += len(titles) + word_count += count_word('trump', titles) + +print(WORD, "found {} times in {} articles".format(word_count, art_count)) +print("Process took {:.0f} seconds".format(time.time() - start)) diff --git a/_downloads/d1dcda5d6afa07687a95d49dd77d3fe0/exception_test.py b/_downloads/d1dcda5d6afa07687a95d49dd77d3fe0/exception_test.py new file mode 100644 index 0000000..975f1df --- /dev/null +++ b/_downloads/d1dcda5d6afa07687a95d49dd77d3fe0/exception_test.py @@ -0,0 +1,16 @@ +#!/usr/bin/python + +""" +example for what happens when you pass non-ascii unicode to a Exception +""" + +msg = u'This is an ASCII-compatible unicode message' + +#msg = u'This is an non ASCII\N{EM DASH}compatible unicode message' + +print "\nDo you see this message in the Exception report?\n" +print msg +print + +raise ValueError(msg) + diff --git a/_downloads/d296af27e123626bc6a13f14a2f2a942/file_yielder.py b/_downloads/d296af27e123626bc6a13f14a2f2a942/file_yielder.py new file mode 100644 index 0000000..d036834 --- /dev/null +++ b/_downloads/d296af27e123626bc6a13f14a2f2a942/file_yielder.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +import pathlib + + +def file_yielder(dir=".", pattern="*"): + """ + iterate over all the files that match the pattern + + pattern us a "glob" pattern, like: *.py + """ + for filename in pathlib.Path(dir).glob(pattern): + with open(filename) as file_obj: + yield file_obj diff --git a/_downloads/d2f4d19315e96ba42fa214da38e3d8b3/simple_timer.py b/_downloads/d2f4d19315e96ba42fa214da38e3d8b3/simple_timer.py new file mode 100644 index 0000000..a7f4a9e --- /dev/null +++ b/_downloads/d2f4d19315e96ba42fa214da38e3d8b3/simple_timer.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python + +""" +Simple example of using a threading.Timer + +NOTE: The docstring is out of sync with the __init__! + I think it was inherited from the threading.Thread docstring. +""" + +import threading +import time +import random + + +def called_once(): + """ + this function is designed to be be called once in the future + """ + print("Hey! I just got called! It's now: {}".format(time.asctime())) + + +def called_later(count): + """ + This function is designed to run, and then set up a timer to call + itself at a random time in the future + + Note that it is limited to 10 invocations + Otherwise, there will always be a background + thread running that can not be easily killed + (at least on *nix -- Windows may let ^C kill it) + + Try hitting ^C early in the run... + """ + + print("Hey! I just got called for {}th time! It's now: {}" + .format(count, time.asctime())) + # this can trigger another invocation + interval = random.randint(1, 3) + count += 1 + if count < 10: + threading.Timer(interval=interval, + function=called_later, + args=(count,)).start() + + +if __name__ == "__main__": + # use the timer... + + # threading.Timer(interval=3, function=called_once).start() + # print("After starting the timer") + # print("it's now: {}".format(time.asctime())) + + called_later(0) + + # do some stuff: + for i in range(100): + print("{}: nothing important...".format(i)) + time.sleep(0.5) + + + + diff --git a/_downloads/d3ffb34a48b3ade73addcdb3de293a0d/properties_example.py b/_downloads/d3ffb34a48b3ade73addcdb3de293a0d/properties_example.py new file mode 100644 index 0000000..f70760e --- /dev/null +++ b/_downloads/d3ffb34a48b3ade73addcdb3de293a0d/properties_example.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +""" +Example code for properties + +NOTE: if your getters and setters are this simple: don't do this! + +""" + + +class C: + def __init__(self): + self._x = None + @property + def x(self): + print("in getter") + return self._x + @x.setter + def x(self, value): + print("in setter", value) + self._x = value + @x.deleter + def x(self): + del self._x + +if __name__ == "__main__": + c = C() + c.x = 5 + print(c.x) + diff --git a/_downloads/d519125b472dd30a1ce63141ae7c30ce/yield_example.py b/_downloads/d519125b472dd30a1ce63141ae7c30ce/yield_example.py new file mode 100644 index 0000000..adf9448 --- /dev/null +++ b/_downloads/d519125b472dd30a1ce63141ae7c30ce/yield_example.py @@ -0,0 +1,37 @@ +def counter(): + print('counter: starting counter') + i = -3 + while i < 3: + i = i + 1 + print('counter: yield', i) + yield i + +def y_range(start, stop, step=1): + print("at the start") + i = start + while i < stop: + print("about to yield") + yield i + print("after yield") + i += step + print("at end of func") + +def test(): + yield 4 + yield 45 + yield 12 + + +# if __name__ == '__main__': +# print "the generator function:" +# print repr(counter) +# print "call generator function" + +# c = counter() +# print " note that nothing printed" +# print "the generator:" +# print repr(c) + +# print 'iterate' +# for item in c: +# print 'received:', item diff --git a/_downloads/d569f91fb5d6e6a0fd37c835012c0804/roman6.py b/_downloads/d569f91fb5d6e6a0fd37c835012c0804/roman6.py new file mode 100644 index 0000000..c14cfdf --- /dev/null +++ b/_downloads/d569f91fb5d6e6a0fd37c835012c0804/roman6.py @@ -0,0 +1,132 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + +import pytest + +roman_numeral_map = (('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1)) + + +def to_roman(n): + """convert integer to Roman numeral""" + if not (0 < n < 4000): + raise ValueError("number out of range (must be 1..3999)") + + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + return result + + +## 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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result + + +def test_too_large(): + """ + to_roman should raise an ValueError when passed + values over 3999 + """ + with pytest.raises(ValueError): + to_roman(4000) + + +def test_zero(): + """to_roman should raise an ValueError with 0 input""" + 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) + + + diff --git a/_downloads/d6efb0b219ebc50e985f6ec2e9138044/test_random_pytest.py b/_downloads/d6efb0b219ebc50e985f6ec2e9138044/test_random_pytest.py new file mode 100644 index 0000000..478aa6c --- /dev/null +++ b/_downloads/d6efb0b219ebc50e985f6ec2e9138044/test_random_pytest.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +""" +port of the random unit tests from the python docs to py.test +""" + +import random +import pytest + + +example_seq = list(range(10)) + + +def test_choice(): + """ + A choice selected should be in the sequence + """ + element = random.choice(example_seq) + assert (element in example_seq) + + +def test_sample(): + """ + All the items in a sample should be in the sequence + """ + for element in random.sample(example_seq, 5): + assert element in example_seq + + +def test_shuffle(): + """ + Make sure a shuffled sequence does not lose any elements + """ + seq = list(range(10)) + random.shuffle(seq) + seq.sort() # If you comment this out, it will fail, so you can see output + print("seq:", seq) # only see output if it fails + assert seq == list(range(10)) + + +def test_shuffle_immutable(): + """ + Trying to shuffle an immutable sequence raises an Exception + """ + with pytest.raises(TypeError): + random.shuffle((1, 2, 3)) + + +def test_sample_too_large(): + """ + Trying to sample more than exist should raise an error + """ + with pytest.raises(ValueError): + random.sample(example_seq, 20) diff --git a/_downloads/d8605fe860ffdccd0b8273cd07127760/roman3.py b/_downloads/d8605fe860ffdccd0b8273cd07127760/roman3.py new file mode 100644 index 0000000..26b3b3b --- /dev/null +++ b/_downloads/d8605fe860ffdccd0b8273cd07127760/roman3.py @@ -0,0 +1,117 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + +import pytest + +roman_numeral_map = (('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1)) + + +def to_roman(n): + '''convert integer to Roman numeral''' + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + # print(f'subtracting {integer} from input, adding {numeral} to output') + return result + + +## 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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result + + +def test_too_large(): + """ + to_roman should raise an ValueError when passed + values over 3999 + """ + with pytest.raises(ValueError): + to_roman(4000) + + diff --git a/_downloads/d8a2901456b14379487d17a1625a4d3b/ICanEatGlass.utf16.txt b/_downloads/d8a2901456b14379487d17a1625a4d3b/ICanEatGlass.utf16.txt new file mode 100644 index 0000000..24a0858 Binary files /dev/null and b/_downloads/d8a2901456b14379487d17a1625a4d3b/ICanEatGlass.utf16.txt differ diff --git a/_downloads/d935f1a96a5b841a5ec3d6d4124e1601/get_news_async.py b/_downloads/d935f1a96a5b841a5ec3d6d4124e1601/get_news_async.py new file mode 100644 index 0000000..8641e0a --- /dev/null +++ b/_downloads/d935f1a96a5b841a5ec3d6d4124e1601/get_news_async.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python + +""" +An Asynchronous version of the script to see how much a given word is +mentioned in the news today + +Uses data from the NewsAPI: + +https://newsapi.org +""" + +import time +import asyncio +import aiohttp + +NEWS_API_KEY = "84d0483394c44f288965d7b366e54a74" + +WORD = "war" +base_url = 'https://newsapi.org/v1/' + + +# This has to run first, so doesn't really need async +# but why use two requests libraries ? +async def get_sources(sources): + """ + Get all the english language sources of news + + 'https://newsapi.org/v1/sources?language=en' + """ + url = base_url + "sources" + params = {"language": "en", "apiKey": NEWS_API_KEY} + async with aiohttp.ClientSession() as session: + async with session.get(url, ssl=False, params=params) as resp: + data = await resp.json() + print("Got the sources") + sources.extend([src['id'].strip() for src in data['sources']]) + + +async def get_articles(source): + """ + download the info for all the articles + """ + url = base_url + "articles" + params = {"source": source, + "apiKey": NEWS_API_KEY, + "sortBy": "top" + } + print("requesting:", source) + async with aiohttp.ClientSession() as session: + async with session.get(url, ssl=False, params=params) as resp: + if resp.status != 200: # aiohttpp has "status" + print(f'something went wrong with: {source}') + await asyncio.sleep(0) # releases control the the mainloop + return + # awaits response rather than waiting on response in the requests version of this + print("got the articles from {}".format(source)) + data = await resp.json() + # the url to the article itself is in data['articles'][i]['url'] + titles.extend([(str(art['title']) + str(art['description'])) + for art in data['articles']]) + + +def count_word(word, titles): + word = word.lower() + count = 0 + for title in titles: + if word in title.lower(): + count += 1 + return count + + +start = time.time() + +# start up a loop: +loop = asyncio.get_event_loop() + +# create the objects to hold the data +sources = [] +titles = [] + +# get the sources -- this is essentially synchronous +loop.run_until_complete(get_sources(sources)) + +# run the loop for the articles +jobs = asyncio.gather(*(get_articles(source) for source in sources)) +loop.run_until_complete(jobs) +loop.close() + +art_count = len(titles) +word_count = count_word(WORD, titles) + +print(f'found {WORD}, {word_count} times in {art_count} articles') +print(f'Process took {(time.time() - start):.0f} sec.') diff --git a/_downloads/d969cdad151d9d616317d2834767eb1f/roman.py b/_downloads/d969cdad151d9d616317d2834767eb1f/roman.py new file mode 100644 index 0000000..8fb46dc --- /dev/null +++ b/_downloads/d969cdad151d9d616317d2834767eb1f/roman.py @@ -0,0 +1,78 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + +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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result + + diff --git a/_downloads/d9849d58494eee24e51aa7f951726589/add_book_data.py b/_downloads/d9849d58494eee24e51aa7f951726589/add_book_data.py new file mode 100644 index 0000000..4ef65cf --- /dev/null +++ b/_downloads/d9849d58494eee24e51aa7f951726589/add_book_data.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +""" +sample data for persistence/serializatiion examples + +This version is nested, with more stucture + - can be saved with pickle, JSON, xml... +""" + +AddressBook = [ {'first_name': "Chris", + 'last_name': "Barker", + 'address' : {'line_1':"835 NE 33rd St", + 'line_2' : "", + 'city' : "Seattle", + 'state': "WA", + 'zip': "96543"}, + 'email' : "PythonCHB@gmail.com", + 'home_phone' : "206-555-1234", + 'office_phone' : "123-456-7890", + 'cell_phone' : "234-567-8901", + }, + + {'first_name': "Fred", + 'last_name': "Jones", + 'address' : {'line_1':"123 SE 13th St", + 'line_2' : "Apt. 43", + 'city' : "Tacoma", + 'state': "WA", + 'zip': "93465"}, + 'email' : "FredJones@some_company.com", + 'home_phone' : "510-555-1234", + 'office_phone' : "564-466-7990", + 'cell_phone' : "403-561-8911", + }, + + {'first_name': "Nancy", + 'last_name': "Wilson", + 'address' : {'line_1':"8654 Walnut St", + 'line_2' : "Suite 567", + 'city' : "Pasadena", + 'state': "CA", + 'zip': "12345"}, + 'email' : "Wilson.Nancy@gmail.com", + 'home_phone' : "423-321-9876", + 'office_phone' : "123-765-9877", + 'cell_phone' : "432-567-8466", + }, + ] + diff --git a/_downloads/d9d3d859a78f4a9849bf2aa49b41c958/run_html_render.py b/_downloads/d9d3d859a78f4a9849bf2aa49b41c958/run_html_render.py new file mode 100644 index 0000000..9608a65 --- /dev/null +++ b/_downloads/d9d3d859a78f4a9849bf2aa49b41c958/run_html_render.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 + +""" +a simple script can run and test your html rendering classes. + +Uncomment the steps as you add to your rendering. + +""" + +from io import StringIO + +# importing the html_rendering code with a short name for easy typing. +import html_render as hr + + +# writing the file out: +def render_page(page, filename, indent=None): + """ + render the tree of elements + + This uses StringIO to render to memory, then dump to console and + write to file -- very handy! + """ + + f = StringIO() + if indent is None: + page.render(f) + else: + page.render(f, indent) + + print(f.getvalue()) + with open(filename, 'w') as outfile: + outfile.write(f.getvalue()) + + +# Step 1 +######### + +page = hr.Element() + +page.append("Here is a paragraph of text -- there could be more of them, " + "but this is enough to show that we can do some text") + +page.append("And here is another piece of text -- you should be able to add any number") + +render_page(page, "test_html_output1.html") + +# The rest of the steps have been commented out. +# Uncomment them as you move along with the assignment. + +# ## Step 2 +# ########## + +# page = hr.Html() + +# body = hr.Body() + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text")) + +# body.append(hr.P("And here is another piece of text -- you should be able to add any number")) + +# page.append(body) + +# render_page(page, "test_html_output2.html") + +# # Step 3 +# ########## + +# page = hr.Html() + +# head = hr.Head() +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text")) +# body.append(hr.P("And here is another piece of text -- you should be able to add any number")) + +# page.append(body) + +# render_page(page, "test_html_output3.html") + +# # Step 4 +# ########## + +# page = hr.Html() + +# head = hr.Head() +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text", +# style="text-align: center; font-style: oblique;")) + +# page.append(body) + +# render_page(page, "test_html_output4.html") + +# # Step 5 +# ######### + +# page = hr.Html() + +# head = hr.Head() +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text", +# style="text-align: center; font-style: oblique;")) + +# body.append(hr.Hr()) + +# page.append(body) + +# render_page(page, "test_html_output5.html") + +# # Step 6 +# ######### + +# page = hr.Html() + +# head = hr.Head() +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text", +# style="text-align: center; font-style: oblique;")) + +# body.append(hr.Hr()) + +# body.append("And this is a ") +# body.append( hr.A("http://google.com", "link") ) +# body.append("to google") + +# page.append(body) + +# render_page(page, "test_html_output6.html") + +# # Step 7 +# ######### + +# page = hr.Html() + +# head = hr.Head() +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append( hr.H(2, "PythonClass - Class 6 example") ) + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text", +# style="text-align: center; font-style: oblique;")) + +# body.append(hr.Hr()) + +# list = hr.Ul(id="TheList", style="line-height:200%") + +# list.append( hr.Li("The first item in a list") ) +# list.append( hr.Li("This is the second item", style="color: red") ) + +# item = hr.Li() +# item.append("And this is a ") +# item.append( hr.A("http://google.com", "link") ) +# item.append("to google") + +# list.append(item) + +# body.append(list) + +# page.append(body) + +# render_page(page, "test_html_output7.html") + +# # Step 8 and 9 +# ############## + +# page = hr.Html() + + +# head = hr.Head() +# head.append( hr.Meta(charset="UTF-8") ) +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append( hr.H(2, "PythonClass - Example") ) + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text", +# style="text-align: center; font-style: oblique;")) + +# body.append(hr.Hr()) + +# list = hr.Ul(id="TheList", style="line-height:200%") + +# list.append( hr.Li("The first item in a list") ) +# list.append( hr.Li("This is the second item", style="color: red") ) + +# item = hr.Li() +# item.append("And this is a ") +# item.append( hr.A("http://google.com", "link") ) +# item.append("to google") + +# list.append(item) + +# body.append(list) + +# page.append(body) + +# render_page(page, "test_html_output8.html") diff --git a/_downloads/da2475249504456dba1a0c7efa6f352d/hello_unicode.py b/_downloads/da2475249504456dba1a0c7efa6f352d/hello_unicode.py new file mode 100644 index 0000000..6bbad1d --- /dev/null +++ b/_downloads/da2475249504456dba1a0c7efa6f352d/hello_unicode.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +hello = 'Hello ' +world = u'世界' + +print hello + world + +print u"It was nice weather today: it reached 80\u00B0" + +print u"Maybe it will reach 90\N{degree sign}" + +print u"It is extremely rare for it ever to reach 100° in Seattle" diff --git a/_downloads/daac433c55123b8c716a46493c8adc88/test_calculator_pytest.py b/_downloads/daac433c55123b8c716a46493c8adc88/test_calculator_pytest.py new file mode 100644 index 0000000..0482978 --- /dev/null +++ b/_downloads/daac433c55123b8c716a46493c8adc88/test_calculator_pytest.py @@ -0,0 +1,39 @@ +#!/use/bin/env python + +""" +tests for the calculator module + +designed to be run with pytest +""" + +import pytest + +import calculator_functions as calc + + +# a very simple test +def test_add(): + assert calc.add(2, 3) == 5 + + +# testing with a variety of parameters: +def test_multiply_ugly(): + """ + the ugly, not very robust way.... + """ + assert calc.multiply(2, 2) == 4 + assert calc.multiply(2, -1) == -2 + assert calc.multiply(-2, -3) == 6 + assert calc.multiply(3, 0) == 0 + assert calc.multiply(0, 3) == 0 + + +param_names = "arg1, arg2, result" +params = [(2, 2, 4), + (2, -1, -2), + (-2, -2, 4), + (3, 0, 0), + ] +@pytest.mark.parametrize(param_names, params) +def test_multiply(arg1, arg2, result): + assert calc.multiply(arg1, arg2) == result diff --git a/_downloads/dca7a114a50f893b75615fe55f00435b/test_trigrams.py b/_downloads/dca7a114a50f893b75615fe55f00435b/test_trigrams.py new file mode 100644 index 0000000..200bce1 --- /dev/null +++ b/_downloads/dca7a114a50f893b75615fe55f00435b/test_trigrams.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python + +""" +test code for the "trigrams" exercise + +NOTE: This is NOT a complete set of tests! You will want to add more of + your own to get complete coverage. + +As you develop your code, if you are about to add a feature, or think +of an edge case: + + Write a new test for that first! + +Also note that you may want to do some different things with processing +the text -- if so, then update the tests (and add more) to reflect how you want your code to work. +""" + +# this is expecting a "trigrams.py" file with your code in it. + +import random +import trigrams + +IWISH = "I wish I may I wish I might".split() + +LONGER_TEXT = """I was seized with a keen desire to see Holmes +again and to know how he was employing his extraordinary powers +His rooms were brilliantly lit and even as I looked up I saw +his tall spare figure pass twice in a dark silhouette against +the blind""".split() + + +def test_trigrams_pairs(): + """ + test that the build_trigram function creates the right pairs of words + """ + tris = trigrams.build_trigram(IWISH) + + pairs = tris.keys() + + # using a set here, as the dict_keys object is a set as well + # And keys are always unique and hashable + # and the order does not matter, so perfect for a set + assert pairs == {("I", "wish"), + ("wish", "I"), + ("may", "I"), + ("I", "may"), + } + + +def test_trigrams_following_words(): + """ + test that the following words are correct + """ + tris = trigrams.build_trigram(IWISH) + + # this will only print if the test fails + # but if if does, you can see what's going on to try to fix it. + print(tris) + + # a separate assert for each pair: + assert tris[("I", "wish")] == ["I", "I"] + assert tris[("wish", "I")] == ["may", "might"] + assert tris[("may", "I")] == ["wish"] + assert tris[("I", "may")] == ["I"] + + +def test_pick_random_pair(): + test_pairs = {("one", "two"): [], + ("two", "three"): [], + ("four", "five"): [], + ("six", "seven"): [], + ("eight", "nine"): [], + } + # set the seed so we'll always get the same one + random.seed(1234) + pair = trigrams.pick_random_pair(test_pairs) + print("the pair is:", pair) + assert pair == ('six', 'seven') + + +def test_get_last_pair(): + words = ["this", "that", "the", "other"] + + assert trigrams.get_last_pair(words) == ("the", "other") + + +def test_get_random_follower(): + """ + test getting a random word from the trigrams dict + """ + # we only need one entry for this test + tri_dict = {("one", "two"): ["four", "five", "six", "seven"]} + + # set the seed so the answer will be consistent + random.seed(1234) + word = trigrams.get_random_follower(tri_dict, ("one", "two")) + print("got word:", word) + assert word == "seven" + + +def test_get_random_follower_not_there(): + """ + test what happens when the word pair is not there + """ + # we only need one entry for this test + tri_dict = {("one", "two"): ["four", "five", "six", "seven"]} + + # here's a word pair that isn't there + # make sure you get something back! + word = trigrams.get_random_follower(tri_dict, ("one", "one")) + print("got word:", word) + assert word # this asserts that you got a non-empty string + + +def test_make_sentence(): + """ + test making a trigrams sentence + + as it is supposed to be random, this tests for things other than + the actual results. + + Which means that it does NOT test everything! This test could pass + with a very broken make_sentence function. So you should probably + add a few more things to this test. + + NOTE that this test relies on the build_trigram() function, so it + will fail if that doesn't work. + """ + + # reset the seed, sop that we won't always get the same answer + random.seed() + + # use the already tested build_trigram function to make the dict + tri_dict = trigrams.build_trigram(LONGER_TEXT) + + + # make a sentence of 6 words + sentence = trigrams.make_sentence(tri_dict, 6) + + print(sentence) + # check that it has 6 words + assert len(sentence.split()) == 6 + # check that the first letter is a capital + assert sentence[0] == sentence[0].upper() + # check that it ends with a period + assert sentence[-1] == "." + # check that there is not a space between the period and the last word. + assert not sentence[-2].isspace() + +# the following tests are for the "make_words" function, +# which takes a text string, and returns a list of words +# It also cleans up the punctuation, while preserving things +# like apostrophes, and capitalized "I". +# you may choose to handle punctuation differently +# feel free to adapt the tests to your choices + + +# TEXT_WITH_PUNC = """ +# One night--it was on the twentieth of March, 1888--I was +# returning from a journey to a patient (for I had now returned to +# civil practice), when my way led me through Baker Street. As I +# passed the well-remembered door, which must always be associated +# in my mind with my wooing, and with the dark incidents of the +# Study in Scarlet, I was seized with a keen desire to see Holmes +# again, and to know how he was employing his extraordinary powers. +# His rooms were brilliantly lit, and, even as I looked up, I saw +# his tall, spare figure pass twice in a dark silhouette against +# the blind. +# """ + + +# def test_make_words_simple(): +# """ +# make sure the basics work! +# """ +# all_words = trigrams.make_words("A really simple sentence.") + +# assert len(all_words) == 4 + + +# def test_make_words_commas(): +# """ +# all commas should be removed +# """ +# # put them all back together for easier checking +# all_words = " ".join(trigrams.make_words(TEXT_WITH_PUNC)) + +# assert "," not in all_words + + +# def test_make_words_parentheses(): +# """ +# all parenthesis should be removed +# """ +# # put them all back together for easier checking +# all_words = " ".join(trigrams.make_words(TEXT_WITH_PUNC)) + +# assert "(" not in all_words +# assert ")" not in all_words + + +# def test_make_words_dashes(): +# """ +# all dashes should be removed +# """ +# # put them all back together for easier checking +# all_words = " ".join(trigrams.make_words(TEXT_WITH_PUNC)) + +# assert "-" not in all_words + + +# def test_make_words_I(): +# """ +# I should be capitalized +# """ +# all_words = trigrams.make_words(TEXT_WITH_PUNC) + +# assert "i" not in all_words +# assert "I" in all_words + + +# def test_make_words_single_quote(): +# """ +# no double quotes +# no single quotes by themselves, but preserved when an apostrophe +# """ +# # put them all back together for easier checking +# text = """ +# "Not at all. The 'G' with the small 't' stands for +# 'Gesellschaft,' which isn't the German for 'Company.' +# """ +# all_words = trigrams.make_words(text) + +# print(all_words) +# # no double quotes +# assert '"' not in " ".join(all_words) +# # apostophe preserved +# assert "isn't" in all_words + +# # none of the words should start or end with a single quote +# for word in all_words: +# assert not word.startswith("'") +# assert not word.endswith("'") + +# assert False + + diff --git a/_downloads/dd35fde1efea7d19ffe17d2f7a3cd0b5/race_condition.py b/_downloads/dd35fde1efea7d19ffe17d2f7a3cd0b5/race_condition.py new file mode 100644 index 0000000..49434eb --- /dev/null +++ b/_downloads/dd35fde1efea7d19ffe17d2f7a3cd0b5/race_condition.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 + +import threading +import time + + +# create a mutable object that is shared among threads +class shared: + val = 1 + + +def func(): + y = shared.val + time.sleep(0.00001) + y += 1 + shared.val = y + + +threads = [] +# with enough threads, there's sufficient overhead to +# cause a race condition +for i in range(100): + thread = threading.Thread(target=func) + threads.append(thread) + thread.start() + +for thread in threads: + thread.join() + +print(shared.val) + diff --git a/_downloads/de9e71595b14e26132917589f5fc90c8/roman17.py b/_downloads/de9e71595b14e26132917589f5fc90c8/roman17.py new file mode 100644 index 0000000..6ad7bf1 --- /dev/null +++ b/_downloads/de9e71595b14e26132917589f5fc90c8/roman17.py @@ -0,0 +1,274 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + +import pytest + +roman_numeral_map = (('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1)) + + +def to_roman(n): + """convert integer to Roman numeral""" + if not (0 < n < 4000): + raise ValueError("number out of range (must be 1..3999)") + + if int(n) != n: + raise ValueError("Only integers can be converted to Roman numerals") + + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + return result + + +def is_valid_roman_numeral(s): + """ + parse a Roman numeral as a human would: left to right, + looking for valid characters and removing them to determine + if this is, indeed, a valid Roman numeral + """ + # first look for the thousands -- up to three Ms + for _ in range(3): + if s[:1] == "M": + s = s[1:] + # then look for the hundreds: + # there can be only one of CM, CD, or D: + if s[:2] == "CM": # 900 + s = s[2:] + elif s[:2] == "CD": # 400 + s = s[2:] + else: + if s[:1] == "D": # 500 + s = s[1:] + # there can be from 1 to 3 Cs + for _ in range(3): + if s[:1] == "C": + s = s[1:] + # now the tens + # There can be one of either XC, XL or L + if s[:2] == "XC": # 90 + s = s[2:] + elif s[:2] == "XL": # 40 + s = s[2:] + else: + if s[:1] == "L": # 50 + s = s[1:] + # there can be up to three Xs + for _ in range(3): + if s[:1] == "X": + s = s[1:] + # and the ones + # There can be one of IX, IV or V + if s[:2] == "IX": # 9 + s = s[2:] + elif s[:2] == "IV": # 4 + s = s[2:] + else: + if s[:1] == "V": # 5 + s = s[1:] + # There can be up to three Is + for _ in range(3): + if s[:1] == "I": # 1 + s = s[1:] + # if there is anything left, it's not a valid Roman numeral + if s: + return False + else: + return True + + +def from_roman(s): + """convert Roman numeral to integer""" + if not is_valid_roman_numeral(s): + raise ValueError(f"{s} is not a valid Roman numeral") + result = 0 + index = 0 + for numeral, integer in roman_numeral_map: + while s[index:index + len(numeral)] == numeral: + result += integer + index += len(numeral) + return result + + +##################################### +# 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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result + + +def test_too_large(): + """ + to_roman should raise a ValueError when passed + values over 3999 + """ + with pytest.raises(ValueError): + to_roman(4000) + + +def test_zero(): + """to_roman should raise a ValueError with 0 input""" + with pytest.raises(ValueError): + to_roman(0) + + +def test_negative(): + """to_roman should raise a ValueError with negative input""" + with pytest.raises(ValueError): + to_roman(-1) + + +def test_non_integer(): + """to_roman should raise a ValueError with non-integer input""" + with pytest.raises(ValueError): + to_roman(0.5) + + +def test_float_with_integer_value(): + """to_roman should work for floats with integer values""" + assert to_roman(3.0) == "III" + + +# #################### +# Tests for from_roman +# #################### + + +def test_from_roman_known_values(): + """from_roman should give known result with known input""" + for integer, numeral in KNOWN_VALUES: + result = from_roman(numeral) + assert integer == result + + +def test_roundtrip(): + """from_roman(to_roman(n))==n for all n""" + for integer in range(1, 4000): + numeral = to_roman(integer) + result = from_roman(numeral) + assert integer == result + + +# The following to test for valid roman numerals + +def test_invalid_character(): + """ + Roman numerals can only use these characters: + + M, D, C, L, X, V, I + + This tests that other characters will cause a failure + """ + for s in ['Z', 'XXIIIQ', 'QXXIII', 'XXYIII']: + with pytest.raises(ValueError): + from_roman(s) + + +def test_too_many_repeated_numerals(): + '''from_roman should fail with too many repeated numerals''' + for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'): + with pytest.raises(ValueError): + print(f"trying: {s}") + from_roman(s) + + +def test_repeated_pairs(): + '''from_roman should fail with repeated pairs of numerals''' + for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'): + with pytest.raises(ValueError): + print(f"trying: {s}") + from_roman(s) + + +def test_malformed_antecedents(): + '''from_roman should fail with malformed antecedents''' + for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV', + 'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'): + with pytest.raises(ValueError): + print(f"trying: {s}") + from_roman(s) diff --git a/_downloads/df18d0e9b634eac4a2652bc951425357/play_with_imports.py b/_downloads/df18d0e9b634eac4a2652bc951425357/play_with_imports.py new file mode 100644 index 0000000..deaaf37 --- /dev/null +++ b/_downloads/df18d0e9b634eac4a2652bc951425357/play_with_imports.py @@ -0,0 +1,26 @@ + +print('importing') + + +def my_decorator(func): + print('in my_decorator for: ', func.__name__) + def inner(): + print('in inner function') + func() + print('in inner after decorated function') + return inner + + +@my_decorator +def first_func(): + print('running first_func') + +@my_decorator +def other_func(): + print('running other_func') + +print('imports and loading done') + +if __name__ == '__main__': + print('run script') + other_func() diff --git a/_downloads/e152a981ef24a559bd1eaf5aeeed017d/roman.py b/_downloads/e152a981ef24a559bd1eaf5aeeed017d/roman.py new file mode 100644 index 0000000..8fb46dc --- /dev/null +++ b/_downloads/e152a981ef24a559bd1eaf5aeeed017d/roman.py @@ -0,0 +1,78 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + +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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result + + diff --git a/_downloads/e1d379f89af956a8f57b8da769442c22/latin1_test.py b/_downloads/e1d379f89af956a8f57b8da769442c22/latin1_test.py new file mode 100644 index 0000000..3990078 --- /dev/null +++ b/_downloads/e1d379f89af956a8f57b8da769442c22/latin1_test.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python + +""" +An example of using latin-1 as a universal encoding + +latin-1 is a superset of ASCII that is suitable for western european languages. + +Is the most common, and a good default, if you need a one-byte per char encoding +for European text. + +It also has a nice property: + : every byte value from 0 to 255 is avalid charactor + +Thus you will never get an UnicodeDecodeError if +you try to decode arbitrary bytes with latin-1. + +And it can "round-trip" trhough a unicode object. + +This can be useful is you don't know the encoding -- at least it won't break. +It's also useful if you need to work with cobined text+binary data. + + + +""" + +# all the byte values in a bytes (str) object: +all_bytes = ''.join( [chr(i) for i in range(255)] ) + +print type(all_bytes) +print len(all_bytes) + +print "Example value: 20" +print ord(all_bytes[20]) == 20 +print "Example high value: 245" +print ord(all_bytes[245]) == 245 + +# now decode it to a unicode object: +try: + uni = all_bytes.decode() +except UnicodeDecodeError: + print "OOPS: can't decode with default encoding" + +# latin-1 works: +try: + all_uni = all_bytes.decode('latin-1') + print "Yup -- that worked" + print all_uni + print "note that the ASCII subset is the same..." +except UnicodeDecodeError: + print "OOPS: This should have worked!!" + raise + +## now show that it round-trips: +all_bytes2 = all_uni.encode('latin-1') + +if all_bytes2 == all_bytes: + print "yup -- that worked...the values are preserved on the round trip." +else: + print "Hey, that should have worked" + + + + + + + diff --git a/_downloads/e213f0bfbb62319706e85b507075c974/vector.py b/_downloads/e213f0bfbb62319706e85b507075c974/vector.py new file mode 100644 index 0000000..7b04a4f --- /dev/null +++ b/_downloads/e213f0bfbb62319706e85b507075c974/vector.py @@ -0,0 +1,50 @@ +""" +Vector type with +, * redefined as Vector addition and dot product +""" + + +class Vector(list): + def __repr__(self): + """ + String representation, uses list (superclass) representation + """ + return 'Vector({})'.format(super().__repr__()) + + def __add__(self, v): + """ + redefine + as element-wise Vector sum + """ + if len(self) != len(v): + raise TypeError("Vector can only be added to a sequence of the same length") + else: + return Vector([x1 + x2 for x1, x2 in zip(self, v)]) + + def __mul__(self, v): + """ + redefine * as Vector dot product + """ + if len(self) != len(v): + raise TypeError("Vector can only be multiplied with a sequence of the same length") + else: + return sum([x1 * x2 for x1, x2 in zip(self, v)]) + + +if __name__ == '__main__': + l1 = [1, 2, 3] + l2 = [4, 5, 6] + v1 = Vector(l1) + v2 = Vector(l2) + + print('l1') + print(l1) + print('l1 + l2') + print(l1 + l2) + # print(l1 * l2) # TypeError + print('zip(l1, l2)') + print(zip(l1, l2)) + print('v1') + print(v1) + print('v1 + v2') + print(v1 + v2) + print('v1 * v2') + print(v1 * v2) diff --git a/_downloads/e6037965b4bb1adfbf99c46c99f6c723/hello_unicode.py b/_downloads/e6037965b4bb1adfbf99c46c99f6c723/hello_unicode.py new file mode 100644 index 0000000..6bbad1d --- /dev/null +++ b/_downloads/e6037965b4bb1adfbf99c46c99f6c723/hello_unicode.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +hello = 'Hello ' +world = u'世界' + +print hello + world + +print u"It was nice weather today: it reached 80\u00B0" + +print u"Maybe it will reach 90\N{degree sign}" + +print u"It is extremely rare for it ever to reach 100° in Seattle" diff --git a/_downloads/e66483fa67d94466bc903abfe56dec1c/properties_example.py b/_downloads/e66483fa67d94466bc903abfe56dec1c/properties_example.py new file mode 100644 index 0000000..f70760e --- /dev/null +++ b/_downloads/e66483fa67d94466bc903abfe56dec1c/properties_example.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +""" +Example code for properties + +NOTE: if your getters and setters are this simple: don't do this! + +""" + + +class C: + def __init__(self): + self._x = None + @property + def x(self): + print("in getter") + return self._x + @x.setter + def x(self, value): + print("in setter", value) + self._x = value + @x.deleter + def x(self): + del self._x + +if __name__ == "__main__": + c = C() + c.x = 5 + print(c.x) + diff --git a/_downloads/e6660e6524c19ba855050981c2fb67a8/roman2.py b/_downloads/e6660e6524c19ba855050981c2fb67a8/roman2.py new file mode 100644 index 0000000..8503720 --- /dev/null +++ b/_downloads/e6660e6524c19ba855050981c2fb67a8/roman2.py @@ -0,0 +1,104 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + +roman_numeral_map = (('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1)) + + +def to_roman(n): + '''convert integer to Roman numeral''' + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + # print(f'subtracting {integer} from input, adding {numeral} to output') + return result + + +## 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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result diff --git a/_downloads/e6726755ec5450041c7567ea8ba037c6/test_html_output7.html b/_downloads/e6726755ec5450041c7567ea8ba037c6/test_html_output7.html new file mode 100644 index 0000000..19cfc41 --- /dev/null +++ b/_downloads/e6726755ec5450041c7567ea8ba037c6/test_html_output7.html @@ -0,0 +1,25 @@ + + +PythonClass = Revision 1087: + + +

PythonClass - Class 6 example

+

+Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+
+
    +
  • +The first item in a list +
  • +
  • +This is the second item +
  • +
  • +And this is a +link +to google +
  • +
+ + \ No newline at end of file diff --git a/_downloads/e70effdc5676ed629a0af6b63608cf8f/raising_an_assert.py b/_downloads/e70effdc5676ed629a0af6b63608cf8f/raising_an_assert.py new file mode 100644 index 0000000..0ea86ea --- /dev/null +++ b/_downloads/e70effdc5676ed629a0af6b63608cf8f/raising_an_assert.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python + +""" +examples of forcing and an AssertionError +""" + + +def test_raise_assertion(): + raise AssertionError("this was done with a direct raise") + + +def test_trasditional_assert(): + assert False, "this was done with a forced assert" diff --git a/_downloads/ea9ff57dd55d2896b2d5e1454ebbda93/index_slicing.py b/_downloads/ea9ff57dd55d2896b2d5e1454ebbda93/index_slicing.py new file mode 100644 index 0000000..dedd464 --- /dev/null +++ b/_downloads/ea9ff57dd55d2896b2d5e1454ebbda93/index_slicing.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +""" +examples / test code for __getindex__ + +Doesn't really do anything, but you can see what happens with different indexing. +""" + +import operator + + +class IndexTest: + + def __getitem__(self, index): + # print("In getindex, indexes is:", index) + if isinstance(index, slice): + print("it's a single slice:", index) + elif isinstance(index, tuple): + print("it's a multi-dimensional slice:", index) + else: + try: + ind = operator.index(index) # this converts arbitrary objects to an int. + print("it's an index: ", ind) + except TypeError: # not a simple index + raise + print("It's a simple index") + + +if __name__ == "__main__": + + it = IndexTest() + + print("calling with simple index") + it[4] + + print("calling with single slice") + it[3:4] + + print("calling with two slices") + it[3:4, 7:8] + + print("calling with an invalid index") + it["this"] + + + + + + + diff --git a/_downloads/edc82bdb8cb562b0b64c990a7e50fd15/diamond.py b/_downloads/edc82bdb8cb562b0b64c990a7e50fd15/diamond.py new file mode 100644 index 0000000..b994c3a --- /dev/null +++ b/_downloads/edc82bdb8cb562b0b64c990a7e50fd15/diamond.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +""" +example of the classic "diamond problem" + +In this case, class A is at the root of the class hierarchy + +B and C both inherit from A + +D inherits from B and C + +ASCII art that shows this: + + ----- + | A | + ----- + / \ + / \ +----- ----- +| B | | C | +----- ----- + \ / + \ / + ----- + | D | + ----- + +So what's the problem? + +If you call a method on D -- it calls B and C's method -- and each +of them call A's method. So A's method gets called twice! + +The reason this is a tricky is that when you write the D class, you may not +know that B and C both inherit from A. And you may indeed *need* to call both +B and C's method. + +""" + + +class A(object): + def do_your_stuff(self): + print("doing A's stuff") + + +class Default(A): + def do_your_stuff(self): + print('doing Default stuff') + + +class B(A): + def do_your_stuff(self): + A.do_your_stuff(self) + print("doing B's stuff") + + +class C(A): + def do_your_stuff(self): + A.do_your_stuff(self) + print("doing C's stuff") + + +class D(B, C): + def do_your_stuff(self): + B.do_your_stuff(self) + C.do_your_stuff(self) + print("doing D's stuff") + + +if __name__ == '__main__': + a = A() + print("\ncalling A's method") + a.do_your_stuff() + + default = Default() + print("\ncalling Default's method") + default.do_your_stuff() + + print("\ncalling B's method") + b = B() + b.do_your_stuff() + + print("\ncalling C's method") + c = C() + c.do_your_stuff() + + print("\ncalling D's method") + d = D() + d.do_your_stuff() diff --git a/_downloads/ef41bf7ef60c6bf0bcaea1137d086b26/eggs.csv b/_downloads/ef41bf7ef60c6bf0bcaea1137d086b26/eggs.csv new file mode 100644 index 0000000..efb5b9e --- /dev/null +++ b/_downloads/ef41bf7ef60c6bf0bcaea1137d086b26/eggs.csv @@ -0,0 +1,3 @@ +Spam, Spam, Spam, Spam, Spam, Baked Beans +Spam, "Lovely, Spam", Wonderful, Spam, and, more + diff --git a/_downloads/ef9be52b61ca4b6efa64c7b43ce8fe40/test_html_output6.html b/_downloads/ef9be52b61ca4b6efa64c7b43ce8fe40/test_html_output6.html new file mode 100644 index 0000000..407a87f --- /dev/null +++ b/_downloads/ef9be52b61ca4b6efa64c7b43ce8fe40/test_html_output6.html @@ -0,0 +1,14 @@ + + +PythonClass = Revision 1087: + + +

+Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+
+And this is a +link +to google + + diff --git a/_downloads/f011030f96079534d2ec0a0739f4a16e/students.txt b/_downloads/f011030f96079534d2ec0a0739f4a16e/students.txt new file mode 100644 index 0000000..a85853a --- /dev/null +++ b/_downloads/f011030f96079534d2ec0a0739f4a16e/students.txt @@ -0,0 +1,31 @@ +Name: Nickname, languages +Swift, Taylor: python, java, perl +Swift, Samuel: Sam +Brooks, Garth: fortran, java, matlab, bash +Buble, Michael: python, powershell +Stefani, Gwen: c#, javascript, python, typescript +Gaye, Marvin: c++, java +Jagger, Michael: Mick, shell, python +Lennon, John: +Nelson, Willy: python, java +McCartney, Paul: nothing +Dylan, Bob: java, python, ruby +Springsteen, Bruce: c++, python +Jackson, Michael: java, c#, python +Berry, Charles: Chuck c++, matlab, +Wonder, Steven: Stevie, c, perl, java, erlang, python +Nicks, Stevie: java, perl, c#, c++, python +Turner, Tina: bash, python +Plant, Robert: Bob, bash, python, ansible +Townshend, Peter: Pete, c#, powershell, python, sql +Moon, Keith: python, r, visualbasic +Mitchell, Joni: php, mysql, python +Ramone, John: Johnny, rex, db +King, Carol: r +Waters, Muddy: perl, python +Star, Richard: Ringo, shell, python, vb +Smith, Patricia: Patti, python +Morrison, Jim: fortran, perl, sql, python +Marley, Robert: Bob, c, c++, lisp +Simon, Paul: bash, python, sql +Charles, Ray: Chuck, java, python diff --git a/_downloads/f08b1501d4508a3350b31cfe284ba554/lock_exercise.zip b/_downloads/f08b1501d4508a3350b31cfe284ba554/lock_exercise.zip new file mode 100644 index 0000000..d315044 Binary files /dev/null and b/_downloads/f08b1501d4508a3350b31cfe284ba554/lock_exercise.zip differ diff --git a/_downloads/f1895d3b0e3189fc1587209a18b4842a/raising_an_assert.py b/_downloads/f1895d3b0e3189fc1587209a18b4842a/raising_an_assert.py new file mode 100644 index 0000000..0ea86ea --- /dev/null +++ b/_downloads/f1895d3b0e3189fc1587209a18b4842a/raising_an_assert.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python + +""" +examples of forcing and an AssertionError +""" + + +def test_raise_assertion(): + raise AssertionError("this was done with a direct raise") + + +def test_trasditional_assert(): + assert False, "this was done with a forced assert" diff --git a/_downloads/f2850d9aec3e084241cd9a60dc398f4f/test_html_output2.html b/_downloads/f2850d9aec3e084241cd9a60dc398f4f/test_html_output2.html new file mode 100644 index 0000000..4908a12 --- /dev/null +++ b/_downloads/f2850d9aec3e084241cd9a60dc398f4f/test_html_output2.html @@ -0,0 +1,10 @@ + + +

+Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+

+And here is another piece of text -- you should be able to add any number +

+ + \ No newline at end of file diff --git a/_downloads/f29a8bf1520fee10243ea5036e514e2b/nba_stats_sync.py b/_downloads/f29a8bf1520fee10243ea5036e514e2b/nba_stats_sync.py new file mode 100644 index 0000000..5413814 --- /dev/null +++ b/_downloads/f29a8bf1520fee10243ea5036e514e2b/nba_stats_sync.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python + +""" +Gathering statistics on NBA players with the regular old +synchronous requests library. + +It took: 214.62 seconds (3.6 minutes) on my machine at home on May 29th + +Borrowed from: + +http://terriblecode.com/blog/asynchronous-http-requests-in-python/ +""" + +import requests +import json +import time + +base_url = 'http://stats.nba.com/stats' +HEADERS = { + 'user-agent': ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/45.0.2454.101 Safari/537.36'), +} + + +def get_players(player_args): + """ + get the names of all the players we are interested in + + This request will get JSON of the players for the 2016-17 season: + + http://stats.nba.com/stats/commonallplayers?LeagueID=00&season=2016-17&isonlycurrentseason=1 + + """ + endpoint = '/commonallplayers' + params = {'leagueid': '00', + 'season': '2016-17', + 'isonlycurrentseason': '1'} + url = base_url + endpoint + print('Getting all players...') + resp = requests.get(url, + headers=HEADERS, + params=params) + data = resp.json() + player_args.extend( + [(item[0], item[2]) for item in data['resultSets'][0]['rowSet']]) + + +def get_player(player_id, player_name): + """ + The request for a player's stats. + + Should be a request like: + + http://stats.nba.com/stats/commonplayerinfo?playerid=203112 + """ + endpoint = '/commonplayerinfo' + params = {'playerid': player_id} + url = base_url + endpoint + print("Getting player", player_name, player_id) + resp = requests.get(url, + headers=HEADERS, + params=params) + print(resp) + data = resp.json() + all_players[player_name] = data + +all_players = {} +players = [] + +start = time.time() +get_players(players) + +print("there are {} players".format(len(players))) +for id, name in players: + get_player(id, name) + +print("Done getting data: it took {:.2F} seconds".format(time.time() - start)) + +# write it out to a file +with open("NBA_stats.json", 'w') as outfile: + json.dump(all_players, outfile, indent=2) + +print("File written out") + diff --git a/_downloads/f3f595e29f0e54088b4822f32d3c5d42/calculator.py b/_downloads/f3f595e29f0e54088b4822f32d3c5d42/calculator.py new file mode 100644 index 0000000..8bd0c75 --- /dev/null +++ b/_downloads/f3f595e29f0e54088b4822f32d3c5d42/calculator.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 + +"""calculator + + Usage: + + calculator.py 1 + 3 + +""" + +import sys + +import calculator_functions as functions + + +# put the real code in a function so we can test it +def main(): + + if len(sys.argv) != 4: + error_message = """ + + Invalid arguments. + + Usage: + + calculator.py 1 + 3 + + or 1 x 3 + or 1 / 3 + or 1 - 3 + """ + raise ValueError(error_message + "\n") + + x = sys.argv[1] + operator = sys.argv[2] + y = sys.argv[3] + + if operator == "+": + return functions.add(x, y) + + elif operator == "-": + return functions.subtract(x, y) + + elif operator == "x": + return functions.multiply(x, y) + + elif operator == "/": + return functions.divide(x, y) + + else: + return "invalid input" + +if __name__ == "__main__": + try: + print(main()) + except ValueError as err: + sys.stderr.write(err.args[0]) + sys.exit(1) diff --git a/_downloads/f6bb161d82f83a97c816d1c1907935a8/test_html_output8.html b/_downloads/f6bb161d82f83a97c816d1c1907935a8/test_html_output8.html new file mode 100644 index 0000000..3ae1841 --- /dev/null +++ b/_downloads/f6bb161d82f83a97c816d1c1907935a8/test_html_output8.html @@ -0,0 +1,27 @@ + + + + +PythonClass = Revision 1087: + + +

PythonClass - Class 6 example

+

+Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+
+
    +
  • +The first item in a list +
  • +
  • +This is the second item +
  • +
  • +And this is a +link +to google +
  • +
+ + \ No newline at end of file diff --git a/_downloads/f75c99b2f6b378ac39cf54257f9ac5fd/roman15.py b/_downloads/f75c99b2f6b378ac39cf54257f9ac5fd/roman15.py new file mode 100644 index 0000000..360ba0d --- /dev/null +++ b/_downloads/f75c99b2f6b378ac39cf54257f9ac5fd/roman15.py @@ -0,0 +1,293 @@ +""" +roman.py + +A Roman numeral to arabic numeral (and back!) converter + +complete with tests + +tests are expected to be able to be run with the pytest system +""" + +import pytest + +roman_numeral_map = (('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1)) + + +def to_roman(n): + """convert integer to Roman numeral""" + if not (0 < n < 4000): + raise ValueError("number out of range (must be 1..3999)") + + if int(n) != n: + raise ValueError("Only integers can be converted to Roman numerals") + + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + return result + + +def is_valid_roman_numeral(s): + """ + parse a Roman numeral as a human would: left to right, + looking for valid characters and removing them to determine + if this is, indeed, a valid Roman numeral + """ + # first check if uses only valid characters + for c in s: + if c not in "MDCLXVI": + return False + + print("starting to parse") + print("the thousands") + print(f"{s = }") + # first look for the thousands -- up to three Ms + for _ in range(3): + if s[:1] == "M": + s = s[1:] + # then look for the hundreds: + print("the hundreds") + print(f"{s = }") + # there can be only one of CM, CD, or D: + if s[:2] == "CM": # 900 + s = s[2:] + elif s[:2] == "CD": # 400 + s = s[2:] + else: + if s[:1] == "D": # 500 + s = s[1:] + # there can be from 1 to 3 Cs + for _ in range(3): + if s[:1] == "C": + s = s[1:] + # now the tens + print("the tens") + print(f"{s = }") + # There can be one of either XC, XL or L + if s[:2] == "XC": # 90 + s = s[2:] + elif s[:2] == "XL": # 40 + s = s[2:] + else: + if s[:1] == "L": # 50 + s = s[1:] + # there can be up to three Xs + for _ in range(3): + if s[:1] == "X": + s = s[1:] + # and the ones + print("the ones") + print(f"{s = }") + # There can be one of IX, IV or V + if s[:2] == "IX": # 9 + s = s[2:] + elif s[:2] == "IV": # 4 + s = s[2:] + elif s[:1] == "V": # 5 + s = s[1:] + print("looking for the Is") + print(f"{s = }") + # There can be up to three Is + for _ in range(3): + if s[:1] == "I": # 1 + s = s[1:] + # if there is anything left, it's not a valid Roman numeral + print("done") + print(f"{s = }") + if s: + return False + else: + return True + + +def from_roman(s): + """convert Roman numeral to integer""" + if not is_valid_roman_numeral(s): + raise ValueError(f"{s} is not a valid Roman numeral") + result = 0 + index = 0 + for numeral, integer in roman_numeral_map: + while s[index:index + len(numeral)] == numeral: + result += integer + index += len(numeral) + # print(f'found, {numeral} of length, {len(numeral)} adding {integer}') + return result + + +##################################### +# 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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + assert numeral == result + + +def test_too_large(): + """ + to_roman should raise an ValueError when passed + values over 3999 + """ + with pytest.raises(ValueError): + to_roman(4000) + + +def test_zero(): + """to_roman should raise an ValueError with 0 input""" + 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) + + +def test_non_integer(): + """to_roman should raise an ValueError with non-integer input""" + with pytest.raises(ValueError): + to_roman(0.5) + + +def test_float_with_integer_value(): + """to_roman should work for floats with integer values""" + assert to_roman(3.0) == "III" + +# #################### +# Tests for from_roman + + +def test_from_roman_known_values(): + """from_roman should give known result with known input""" + for integer, numeral in KNOWN_VALUES: + result = from_roman(numeral) + assert integer == result + + +def test_roundtrip(): + """from_roman(to_roman(n))==n for all n""" + for integer in range(1, 4000): + numeral = to_roman(integer) + result = from_roman(numeral) + assert integer == result + + +# ##################### +# The following to test for valid roman numerals + +def test_invalid_character(): + """ + Roman numerals can only use these characters: + + M, D, C, L, X, V, I + + This tests that other characters will cause a failure + """ + for s in ['Z', 'XXIIIQ', 'QXXIII', 'XXYIII']: + with pytest.raises(ValueError): + from_roman(s) + + +def test_too_many_repeated_numerals(): + '''from_roman should fail with too many repeated numerals''' + for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'): + with pytest.raises(ValueError): + print(f"trying: {s}") + from_roman(s) + + +def test_repeated_pairs(): + '''from_roman should fail with repeated pairs of numerals''' + for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'): + with pytest.raises(ValueError): + print(f"trying: {s}") + from_roman(s) + + +def test_malformed_antecedents(): + '''from_roman should fail with malformed antecedents''' + for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV', + 'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'): + with pytest.raises(ValueError): + print(f"trying: {s}") + from_roman(s) + + diff --git a/_downloads/f79a2960541bd9eecb777c971d7f63fd/eggs.csv b/_downloads/f79a2960541bd9eecb777c971d7f63fd/eggs.csv new file mode 100644 index 0000000..efb5b9e --- /dev/null +++ b/_downloads/f79a2960541bd9eecb777c971d7f63fd/eggs.csv @@ -0,0 +1,3 @@ +Spam, Spam, Spam, Spam, Spam, Baked Beans +Spam, "Lovely, Spam", Wonderful, Spam, and, more + diff --git a/_downloads/f945dd005bb544a3776f773f5b1cee8f/text.utf16 b/_downloads/f945dd005bb544a3776f773f5b1cee8f/text.utf16 new file mode 100644 index 0000000..b80b2ef Binary files /dev/null and b/_downloads/f945dd005bb544a3776f773f5b1cee8f/text.utf16 differ diff --git a/_downloads/fc2042604ef431b3938842700934faf5/run_html_render.py b/_downloads/fc2042604ef431b3938842700934faf5/run_html_render.py new file mode 100644 index 0000000..9608a65 --- /dev/null +++ b/_downloads/fc2042604ef431b3938842700934faf5/run_html_render.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 + +""" +a simple script can run and test your html rendering classes. + +Uncomment the steps as you add to your rendering. + +""" + +from io import StringIO + +# importing the html_rendering code with a short name for easy typing. +import html_render as hr + + +# writing the file out: +def render_page(page, filename, indent=None): + """ + render the tree of elements + + This uses StringIO to render to memory, then dump to console and + write to file -- very handy! + """ + + f = StringIO() + if indent is None: + page.render(f) + else: + page.render(f, indent) + + print(f.getvalue()) + with open(filename, 'w') as outfile: + outfile.write(f.getvalue()) + + +# Step 1 +######### + +page = hr.Element() + +page.append("Here is a paragraph of text -- there could be more of them, " + "but this is enough to show that we can do some text") + +page.append("And here is another piece of text -- you should be able to add any number") + +render_page(page, "test_html_output1.html") + +# The rest of the steps have been commented out. +# Uncomment them as you move along with the assignment. + +# ## Step 2 +# ########## + +# page = hr.Html() + +# body = hr.Body() + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text")) + +# body.append(hr.P("And here is another piece of text -- you should be able to add any number")) + +# page.append(body) + +# render_page(page, "test_html_output2.html") + +# # Step 3 +# ########## + +# page = hr.Html() + +# head = hr.Head() +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text")) +# body.append(hr.P("And here is another piece of text -- you should be able to add any number")) + +# page.append(body) + +# render_page(page, "test_html_output3.html") + +# # Step 4 +# ########## + +# page = hr.Html() + +# head = hr.Head() +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text", +# style="text-align: center; font-style: oblique;")) + +# page.append(body) + +# render_page(page, "test_html_output4.html") + +# # Step 5 +# ######### + +# page = hr.Html() + +# head = hr.Head() +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text", +# style="text-align: center; font-style: oblique;")) + +# body.append(hr.Hr()) + +# page.append(body) + +# render_page(page, "test_html_output5.html") + +# # Step 6 +# ######### + +# page = hr.Html() + +# head = hr.Head() +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text", +# style="text-align: center; font-style: oblique;")) + +# body.append(hr.Hr()) + +# body.append("And this is a ") +# body.append( hr.A("http://google.com", "link") ) +# body.append("to google") + +# page.append(body) + +# render_page(page, "test_html_output6.html") + +# # Step 7 +# ######### + +# page = hr.Html() + +# head = hr.Head() +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append( hr.H(2, "PythonClass - Class 6 example") ) + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text", +# style="text-align: center; font-style: oblique;")) + +# body.append(hr.Hr()) + +# list = hr.Ul(id="TheList", style="line-height:200%") + +# list.append( hr.Li("The first item in a list") ) +# list.append( hr.Li("This is the second item", style="color: red") ) + +# item = hr.Li() +# item.append("And this is a ") +# item.append( hr.A("http://google.com", "link") ) +# item.append("to google") + +# list.append(item) + +# body.append(list) + +# page.append(body) + +# render_page(page, "test_html_output7.html") + +# # Step 8 and 9 +# ############## + +# page = hr.Html() + + +# head = hr.Head() +# head.append( hr.Meta(charset="UTF-8") ) +# head.append(hr.Title("PythonClass = Revision 1087:")) + +# page.append(head) + +# body = hr.Body() + +# body.append( hr.H(2, "PythonClass - Example") ) + +# body.append(hr.P("Here is a paragraph of text -- there could be more of them, " +# "but this is enough to show that we can do some text", +# style="text-align: center; font-style: oblique;")) + +# body.append(hr.Hr()) + +# list = hr.Ul(id="TheList", style="line-height:200%") + +# list.append( hr.Li("The first item in a list") ) +# list.append( hr.Li("This is the second item", style="color: red") ) + +# item = hr.Li() +# item.append("And this is a ") +# item.append( hr.A("http://google.com", "link") ) +# item.append("to google") + +# list.append(item) + +# body.append(list) + +# page.append(body) + +# render_page(page, "test_html_output8.html") diff --git a/_downloads/fe04dbef6fce465a689cbba3e35d4c6b/walnut_party.py b/_downloads/fe04dbef6fce465a689cbba3e35d4c6b/walnut_party.py new file mode 100644 index 0000000..9b195b2 --- /dev/null +++ b/_downloads/fe04dbef6fce465a689cbba3e35d4c6b/walnut_party.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +""" +When squirrels get together for a party, they like to have walnuts. +A squirrel party is successful when the number of walnuts is between +40 and 60, inclusive. Unless it is the weekend, in which case there +is no upper bound on the number of walnuts. + +Return True if the party with the given values is successful, +or False otherwise. +""" + + +def walnut_party(walnuts, is_weekend): + pass diff --git a/_downloads/fe6098b66d5fc92351b0ababe04fde1b/sample_html.html b/_downloads/fe6098b66d5fc92351b0ababe04fde1b/sample_html.html new file mode 100644 index 0000000..9c2e675 --- /dev/null +++ b/_downloads/fe6098b66d5fc92351b0ababe04fde1b/sample_html.html @@ -0,0 +1,27 @@ + + + + + Python Class Sample page + + +

Python Class - Html rendering example

+

+ Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+
+
    +
  • + The first item in a list +
  • +
  • + This is the second item +
  • +
  • + And this is a + link + to google +
  • +
+ + \ No newline at end of file diff --git a/_downloads/fe75a8e5b07d292f8756b94e08aac0db/iterator_1.py b/_downloads/fe75a8e5b07d292f8756b94e08aac0db/iterator_1.py new file mode 100644 index 0000000..479e5bf --- /dev/null +++ b/_downloads/fe75a8e5b07d292f8756b94e08aac0db/iterator_1.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +""" +Simple iterator examples +""" + + +class IterateMe_1: + """ + About as simple an iterator as you can get: + + returns the sequence of numbers from zero to 4 + ( like range(4) ) + """ + + def __init__(self, stop=5): + self.current = -1 + self.stop = stop + + def __iter__(self): + return self + + def __next__(self): + self.current += 1 + if self.current < self.stop: + return self.current + else: + raise StopIteration + + +if __name__ == "__main__": + + print("Testing the iterator") + for i in IterateMe_1(): + print(i) diff --git a/_downloads/ff3124bad490b55dcb8f33c1be33a761/test_html_output6.html b/_downloads/ff3124bad490b55dcb8f33c1be33a761/test_html_output6.html new file mode 100644 index 0000000..407a87f --- /dev/null +++ b/_downloads/ff3124bad490b55dcb8f33c1be33a761/test_html_output6.html @@ -0,0 +1,14 @@ + + +PythonClass = Revision 1087: + + +

+Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+
+And this is a +link +to google + + diff --git a/_downloads/ffc99b456469cd9ced008a995479253d/file_yielder.py b/_downloads/ffc99b456469cd9ced008a995479253d/file_yielder.py new file mode 100644 index 0000000..d036834 --- /dev/null +++ b/_downloads/ffc99b456469cd9ced008a995479253d/file_yielder.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +import pathlib + + +def file_yielder(dir=".", pattern="*"): + """ + iterate over all the files that match the pattern + + pattern us a "glob" pattern, like: *.py + """ + for filename in pathlib.Path(dir).glob(pattern): + with open(filename) as file_obj: + yield file_obj diff --git a/_images/OPP.0108.gif b/_images/OPP.0108.gif new file mode 100644 index 0000000..24e2b23 Binary files /dev/null and b/_images/OPP.0108.gif differ diff --git a/_images/big_o.png b/_images/big_o.png new file mode 100644 index 0000000..ee4e296 Binary files /dev/null and b/_images/big_o.png differ diff --git a/_images/clone_url.png b/_images/clone_url.png new file mode 100644 index 0000000..e476ab5 Binary files /dev/null and b/_images/clone_url.png differ diff --git a/_images/code_button.png b/_images/code_button.png new file mode 100644 index 0000000..7836507 Binary files /dev/null and b/_images/code_button.png differ diff --git a/_images/color_git_prompt.png b/_images/color_git_prompt.png new file mode 100644 index 0000000..b85ab44 Binary files /dev/null and b/_images/color_git_prompt.png differ diff --git a/_images/compare_and_pr.png b/_images/compare_and_pr.png new file mode 100644 index 0000000..ebb016b Binary files /dev/null and b/_images/compare_and_pr.png differ diff --git a/_images/coroutines_plot.png b/_images/coroutines_plot.png new file mode 100644 index 0000000..428b5a5 Binary files /dev/null and b/_images/coroutines_plot.png differ diff --git a/_images/feature_branching_img1.png b/_images/feature_branching_img1.png new file mode 100644 index 0000000..e232024 Binary files /dev/null and b/_images/feature_branching_img1.png differ diff --git a/_images/feature_branching_img11.png b/_images/feature_branching_img11.png new file mode 100644 index 0000000..e232024 Binary files /dev/null and b/_images/feature_branching_img11.png differ diff --git a/_images/feature_branching_img2.png b/_images/feature_branching_img2.png new file mode 100644 index 0000000..3ed9037 Binary files /dev/null and b/_images/feature_branching_img2.png differ diff --git a/_images/feature_branching_img21.png b/_images/feature_branching_img21.png new file mode 100644 index 0000000..3ed9037 Binary files /dev/null and b/_images/feature_branching_img21.png differ diff --git a/_images/feature_branching_img3.png b/_images/feature_branching_img3.png new file mode 100644 index 0000000..0d213a7 Binary files /dev/null and b/_images/feature_branching_img3.png differ diff --git a/_images/feature_branching_img31.png b/_images/feature_branching_img31.png new file mode 100644 index 0000000..0d213a7 Binary files /dev/null and b/_images/feature_branching_img31.png differ diff --git a/_images/feature_branching_img4.png b/_images/feature_branching_img4.png new file mode 100644 index 0000000..7a83856 Binary files /dev/null and b/_images/feature_branching_img4.png differ diff --git a/_images/feature_branching_img41.png b/_images/feature_branching_img41.png new file mode 100644 index 0000000..7a83856 Binary files /dev/null and b/_images/feature_branching_img41.png differ diff --git a/_images/feature_branching_img5.png b/_images/feature_branching_img5.png new file mode 100644 index 0000000..c777c04 Binary files /dev/null and b/_images/feature_branching_img5.png differ diff --git a/_images/feature_branching_img51.png b/_images/feature_branching_img51.png new file mode 100644 index 0000000..c777c04 Binary files /dev/null and b/_images/feature_branching_img51.png differ diff --git a/_images/flags.jpg b/_images/flags.jpg new file mode 100644 index 0000000..061713d Binary files /dev/null and b/_images/flags.jpg differ diff --git a/_images/gil.png b/_images/gil.png new file mode 100644 index 0000000..301a39d Binary files /dev/null and b/_images/gil.png differ diff --git a/_images/git_checkout_branch.png b/_images/git_checkout_branch.png new file mode 100644 index 0000000..dab12bd Binary files /dev/null and b/_images/git_checkout_branch.png differ diff --git a/_images/git_checkout_master.png b/_images/git_checkout_master.png new file mode 100644 index 0000000..dc245a7 Binary files /dev/null and b/_images/git_checkout_master.png differ diff --git a/_images/git_commit_on_branch.png b/_images/git_commit_on_branch.png new file mode 100644 index 0000000..23143d7 Binary files /dev/null and b/_images/git_commit_on_branch.png differ diff --git a/_images/git_head.png b/_images/git_head.png new file mode 100644 index 0000000..c48c40e Binary files /dev/null and b/_images/git_head.png differ diff --git a/_images/git_master_branch.png b/_images/git_master_branch.png new file mode 100644 index 0000000..9c4aeb8 Binary files /dev/null and b/_images/git_master_branch.png differ diff --git a/_images/git_merge_commit.png b/_images/git_merge_commit.png new file mode 100644 index 0000000..2df3d2d Binary files /dev/null and b/_images/git_merge_commit.png differ diff --git a/_images/git_new_branch.png b/_images/git_new_branch.png new file mode 100644 index 0000000..a0a4ef4 Binary files /dev/null and b/_images/git_new_branch.png differ diff --git a/_images/git_new_commit.png b/_images/git_new_commit.png new file mode 100644 index 0000000..012eb84 Binary files /dev/null and b/_images/git_new_commit.png differ diff --git a/_images/git_new_commit_on_master.png b/_images/git_new_commit_on_master.png new file mode 100644 index 0000000..6c25e51 Binary files /dev/null and b/_images/git_new_commit_on_master.png differ diff --git a/_images/git_simple_timeline.png b/_images/git_simple_timeline.png new file mode 100644 index 0000000..465f546 Binary files /dev/null and b/_images/git_simple_timeline.png differ diff --git a/_images/killGIL.jpg b/_images/killGIL.jpg new file mode 100644 index 0000000..08efe7d Binary files /dev/null and b/_images/killGIL.jpg differ diff --git a/_images/make_pr.png b/_images/make_pr.png new file mode 100644 index 0000000..cce1bbd Binary files /dev/null and b/_images/make_pr.png differ diff --git a/_images/pc_menu.png b/_images/pc_menu.png new file mode 100644 index 0000000..1621fb6 Binary files /dev/null and b/_images/pc_menu.png differ diff --git a/_images/plugin_list.png b/_images/plugin_list.png new file mode 100644 index 0000000..5b206bb Binary files /dev/null and b/_images/plugin_list.png differ diff --git a/_images/pr_header.png b/_images/pr_header.png new file mode 100644 index 0000000..4ee761f Binary files /dev/null and b/_images/pr_header.png differ diff --git a/_images/proc_thread_async.png b/_images/proc_thread_async.png new file mode 100644 index 0000000..3e4257b Binary files /dev/null and b/_images/proc_thread_async.png differ diff --git a/_images/program_callstack.png b/_images/program_callstack.png new file mode 100644 index 0000000..821a965 Binary files /dev/null and b/_images/program_callstack.png differ diff --git a/_images/python.png b/_images/python.png new file mode 100644 index 0000000..23a4c6e Binary files /dev/null and b/_images/python.png differ diff --git a/_images/remotes_clone.png b/_images/remotes_clone.png new file mode 100644 index 0000000..8003dce Binary files /dev/null and b/_images/remotes_clone.png differ diff --git a/_images/remotes_fork.png b/_images/remotes_fork.png new file mode 100644 index 0000000..e494696 Binary files /dev/null and b/_images/remotes_fork.png differ diff --git a/_images/remotes_start.png b/_images/remotes_start.png new file mode 100644 index 0000000..aa4839d Binary files /dev/null and b/_images/remotes_start.png differ diff --git a/_images/remotes_upstream.png b/_images/remotes_upstream.png new file mode 100644 index 0000000..de03659 Binary files /dev/null and b/_images/remotes_upstream.png differ diff --git a/_images/simple_prompt.png b/_images/simple_prompt.png new file mode 100644 index 0000000..db8b9d4 Binary files /dev/null and b/_images/simple_prompt.png differ diff --git a/_images/snakeviz.png b/_images/snakeviz.png new file mode 100644 index 0000000..cedea79 Binary files /dev/null and b/_images/snakeviz.png differ diff --git a/_images/test_v_model.png b/_images/test_v_model.png new file mode 100644 index 0000000..c9aae8c Binary files /dev/null and b/_images/test_v_model.png differ diff --git a/_images/transmogrifier.jpg b/_images/transmogrifier.jpg new file mode 100644 index 0000000..73363de Binary files /dev/null and b/_images/transmogrifier.jpg differ diff --git a/_images/two_line_prompt.png b/_images/two_line_prompt.png new file mode 100644 index 0000000..23d9cac Binary files /dev/null and b/_images/two_line_prompt.png differ diff --git a/_images/virtualenv_prompt.png b/_images/virtualenv_prompt.png new file mode 100644 index 0000000..68902a6 Binary files /dev/null and b/_images/virtualenv_prompt.png differ diff --git a/_images/x2.png b/_images/x2.png new file mode 100644 index 0000000..9fdd683 Binary files /dev/null and b/_images/x2.png differ diff --git a/_sources/class_schedule/index.rst.txt b/_sources/class_schedule/index.rst.txt new file mode 100644 index 0000000..764b8b2 --- /dev/null +++ b/_sources/class_schedule/index.rst.txt @@ -0,0 +1,21 @@ +:orphan: + +######################### +Python 310 Class Schedule +######################### + +.. toctree:: + :maxdepth: 2 + + orientation + lesson01 + lesson02 + lesson03 + lesson04 + lesson05 + lesson06 + lesson07 + lesson08 + lesson09 + lesson10 + diff --git a/_sources/class_schedule/lesson01.rst.txt b/_sources/class_schedule/lesson01.rst.txt new file mode 100644 index 0000000..4c4a059 --- /dev/null +++ b/_sources/class_schedule/lesson01.rst.txt @@ -0,0 +1,87 @@ +.. _lesson01: + +##################################### +Lesson 1: Setting up Your Environment +##################################### + +**Intro to the class and Python** + +Introduction +============ + +Learning to program, whether in Python or in any other language, is about more than the fundamentals of the language. Learning to program also involves learning the tools involved. You might liken it to carpentry. Without an adequately provisioned workshop, and without tools appropriate for the job, a carpenter wouldn’t get very far. + +Under ideal circumstances, learning to program also involves learning to collaborate with others on code development. Working with other programmers is one of the best ways to improve as a programmer. The tools involved in collaboration are also a part of a programmer’s workshop. + +In this lesson we are looking to provision your workshop. We are going to set up an environment that will facilitate the collaborative creation of Python code. We are also going to introduce you to Python itself. + +Learning Objectives +=================== + +After completing this lesson, students will be able to: + +1. Install and use your own functional Python development environment appropriate for your operating system of choice + +1. Navigate the command line shell to set the working directory and run Python and git + +1. Have a working installation of git +1. Create and run a simple Python script + +Suggested Workflow +================== + +* Submit Exercise: :ref:`Install Python and Core Tools (graded) ` + +* Do the Readings for this week + +* Watch Required Videos + +* Do the Python practice Activity + +At the End of the Lesson +======================== + +What material in this lesson do you still feel unclear about? Please use ??? to describe any concepts or ideas you've struggled with this week. Your instructor will address concepts that seem problematic for several students. You may also want to address issues raised by other students or ask for their help. + +Pre-class Preparation +--------------------- + +Get your Environment set up! + +If you were able to attend the pre-class orientation, you should be all set. if not, then do this: + +**Install Python** + +:ref:`installing_python` + +**Set Up a Great Dev Environment** + +Make sure you have the basics of command line usage down: + +Work through the supplemental tutorials on setting up your +Command Line (:ref:`shell_customization`) for good development support. + +Make sure you've got your editor set up productively -- +at the very very least, make sure it does Python indentation and syntax coloring well. + +:ref:`setting_up_dev_environment` + + +Exercises: +========== + +.. .. toctree:: +.. :maxdepth: 1 + +.. ../exercises/python_pushups.rst + +Finish Setting up your dev environment +-------------------------------------- + +Make sure you know how to create a python file, save it, edit it, run it, etc. + +Also make sure you are comfortable "playing" with python in the iPython command line. + +If you are uncomfortable with the command line, brush up on that: +`This: `_ is a good tutorial. + diff --git a/_sources/class_schedule/lesson02.rst.txt b/_sources/class_schedule/lesson02.rst.txt new file mode 100644 index 0000000..da39512 --- /dev/null +++ b/_sources/class_schedule/lesson02.rst.txt @@ -0,0 +1,85 @@ +.. _lesson_02: + +#################################### +Lesson 2: Basic Python and Functions +#################################### + + +Readings: +========= + + +.. toctree:: + :maxdepth: 1 + + ../modules/BasicPython + ../modules/Functions + ../modules/Recursion + +Read: :ref:`documentation` + +Exercises: +========== + +.. toctree:: + :maxdepth: 1 + + ../exercises/grid_printer.rst + ../exercises/fizz_buzz.rst + ../exercises/series/fib_and_lucas.rst + + +Python +------ + +Python Tutorial +............... + +Tutorials come in many forms, but I define a python "tutorial" as a quick high-level introduction, designed to kick start your python programming. It should give you enough to get something done, but not provide a lot of detailed explanations. If you have been doing a bit of python coding already, or have taken an introductory programing class with Python, then you may be able to skip this step. + +Every one of you has a different background and learning style. So take a bit of time to figure out which resource works for you. + +:ref:`python_learning_resources` + +Provides some options. Do look it over. + +Python Basics +------------- + +If you are comfortable with the materials in a tutorial, it's time for some more in-depth discussion. Here are a few options to get you started this week: + +Finish Reading: :ref:`basic_python_syntax` + +Read: :ref:`more_on_functions` + +Read: :ref:`documentation` + + +Supplemental Reading +-------------------- + +If that is too fast, then here are some good options for another look: + +*Think Python:* Chapters 1–6 (http://greenteapress.com/thinkpython2/html/index.html) + +*Dive Into Python:* Chapters 1–2 (http://www.diveintopython3.net/) + +*LPTHW:* ex. 1–10, 18-21 (https://learnpythonthehardway.org/python3/) + +(note: LPTHW used to be totally free --now it looks like you may only get a sneak peak -- darn! -- but the first few "chapters" are available) + +**GOAL** + +You should be comfortable with working with variables, numbers, strings, and basic functions before the next class. + +git +--- + +We are only using a small subset of git functionality for this class, +but if you feel lost, and/or want to know more, these are some good resources: + +http://rogerdudler.github.io/git-guide/ + +or + +https://try.github.io/ diff --git a/_sources/class_schedule/lesson03.rst.txt b/_sources/class_schedule/lesson03.rst.txt new file mode 100644 index 0000000..c08a53a --- /dev/null +++ b/_sources/class_schedule/lesson03.rst.txt @@ -0,0 +1,51 @@ +.. _session_1_03: + +###################################################### +Session 3: Booleans, Sequences, Iteration, and Strings +###################################################### + +**Booleans, Sequences, Iteration, and Strings** + + +Readings: +========= + +.. toctree:: + :maxdepth: 1 + + ../modules/Booleans + ../modules/Sequences + ../modules/Iteration + ../modules/Strings + +Exercises: +========== + +.. toctree:: + :maxdepth: 1 + + ../exercises/slicing.rst + ../exercises/list_lab.rst + ../exercises/string_formatting.rst + ../exercises/mailroom/mailroom.rst + ../exercises/mailroom/mailroom_tutorial.rst + +Optional +-------- + +.. toctree:: + :maxdepth: 1 + + ../exercises/rot13.rst + + +Supplemental Reading: +--------------------- + +Think Python, chapters 7 -- 10 (11, 12) + +(http://greenteapress.com/thinkpython/html/thinkpython008.html) + +Dive Into Python: chapters 3, 4 + +(http://www.diveintopython3.net/strings.html) diff --git a/_sources/class_schedule/lesson04.rst.txt b/_sources/class_schedule/lesson04.rst.txt new file mode 100644 index 0000000..73cfe6e --- /dev/null +++ b/_sources/class_schedule/lesson04.rst.txt @@ -0,0 +1,44 @@ +.. _session_1_04: + +################################################# +Session 4: Dictionaries and Sets and Unit Testing +################################################# + + +Readings +======== + +.. toctree:: + :maxdepth: 1 + + ../modules/DictsAndSets + ../modules/DictionaryAsSwitch + ../modules/Exceptions + ../modules/Testing + ../modules/TestDrivenDevelopment + +Recommended Additional Reading +------------------------------ + +* Think Python: Chapters 11, 13, 14 + +* Dive Into Python3: Sections 2.6, 2.7, 4, 11 + +http://www.diveintopython3.net/native-datatypes.html#dictionaries + + + +Exercises: +========== + +.. toctree:: + :maxdepth: 1 + + ../exercises/exceptions/exceptions_lab.rst + ../exercises/exceptions/except_exercise.rst + ../exercises/unit_testing/unit_testing.rst + ../exercises/dict_lab.rst + ../exercises/mailroom/mailroom_with_dicts.rst + ../exercises/trigrams/trigrams.rst + + diff --git a/_sources/class_schedule/lesson05.rst.txt b/_sources/class_schedule/lesson05.rst.txt new file mode 100644 index 0000000..7251393 --- /dev/null +++ b/_sources/class_schedule/lesson05.rst.txt @@ -0,0 +1,34 @@ +.. _session_1_05: + +####################################################### +Lession 5: File Handling, Exceptions and Comprehensions +####################################################### + + +Readings +======== + +.. toctree:: + :maxdepth: 1 + + ../modules/Files + ../modules/NamingThings + ../modules/Documentation + ../modules/Exceptions + ../modules/Comprehensions + ../modules/CollectionsModule + +Exercises: +========== + +.. toctree:: + :maxdepth: 1 + + ../exercises/file_processing/file_lab.rst + ../exercises/exceptions/exceptions_lab.rst + ../exercises/exceptions/except_exercise.rst + ../exercises/comprehensions_lab.rst + ../exercises/mailroom/mailroom_with_exceptions.rst + ../exercises/mailroom/mailroom_with_files.rst + + diff --git a/_sources/class_schedule/lesson06.rst.txt b/_sources/class_schedule/lesson06.rst.txt new file mode 100644 index 0000000..f820945 --- /dev/null +++ b/_sources/class_schedule/lesson06.rst.txt @@ -0,0 +1,37 @@ +.. _session_1_06: + +################################################ +Session 6: Advanced Argument Passing and Modules +################################################ + +Advanced Argument Passing + +Readings +======== + +.. toctree:: + :maxdepth: 1 + + ../modules/AdvancedArgumentPassing + ../modules/MoreOnMutability + ../modules/Modules + +Supplemental Reading +-------------------- + + - http://stupidpythonideas.blogspot.com/2013/08/arguments-and-parameters.html + + - https://pythontips.com/2013/08/04/args-and-kwargs-in-python-explained/ + + +Exercises: +========== + +.. toctree:: + :maxdepth: 1 + + ../exercises/args_kwargs_lab.rst + ../exercises/mailroom/mailroom_with_tests.rst + +**NOTE:** you will find that most introductions to unit testing with Python use the built in ``unittest`` module. However, it is a bit heavyweight, and requires some knowledge of OOP -- classes, etc. So we use pytest in this class: +http://doc.pytest.org/en/latest/. But the principles of testing are the same. diff --git a/_sources/class_schedule/lesson07.rst.txt b/_sources/class_schedule/lesson07.rst.txt new file mode 100644 index 0000000..e431857 --- /dev/null +++ b/_sources/class_schedule/lesson07.rst.txt @@ -0,0 +1,74 @@ +.. _session_1_07: + +##################################### +Session 7: Object Oriented Programing +##################################### + +Object Oriented Programing: classes, instance and class attributes, subclassing and inheritance. + + +Readings +======== + +.. toctree:: + :maxdepth: 1 + + ../modules/ObjectOrientationOverview + ../modules/PythonClasses + ../modules/SubclassingAndInheritance + +Supplemental reading +-------------------- + +* Dive into Python3: 7.2 -- 7.3 + + - http://www.diveintopython3.net/iterators.html#defining-classes + +* Think Python: 15 -- 18 + + - http://www.greenteapress.com/thinkpython/html/thinkpython016.html + +Some Videos to watch: +--------------------- + +Python Class Toolkit by *Raymond Hettinger* + +https://youtu.be/HTLu2DFOdTg + +https://speakerdeck.com/pyconslides/pythons-class-development-toolkit-by-raymond-hettinger + + +The Art of Subclassing by *Raymond Hettinger* + +http://pyvideo.org/video/879/the-art-of-subclassing + +The most salient points from that video are as follows: + +* **Subclassing is not for Specialization** + +* **Classes and subclassing are for code re-use -- not creating taxonomies** + +* **Bear in mind that the subclass is in charge** + +Note that the previous talk and this one were back to back at PyCon -- but despite their contradictory titles -- they have similar messages. + +Stop Writing Classes +.................... + +by *Jack Diederich* + +http://pyvideo.org/video/880/stop-writing-classes + +"If your class has only two methods -- and one of them is ``__init__`` +-- you don't need a class" + + +Exercises: +========== + +.. toctree:: + :maxdepth: 1 + + ../exercises/oo_intro/oo_intro.rst + ../exercises/html_renderer/html_renderer.rst + ../exercises/html_renderer/html_renderer_tutorial.rst diff --git a/_sources/class_schedule/lesson08.rst.txt b/_sources/class_schedule/lesson08.rst.txt new file mode 100644 index 0000000..58b1db7 --- /dev/null +++ b/_sources/class_schedule/lesson08.rst.txt @@ -0,0 +1,51 @@ +.. _session_1_08: + +####################################### +Session 8: Properties and Magic methods +####################################### + +More OO: Properties and Magic methods. + +Readings +======== + +.. toctree:: + :maxdepth: 1 + + ../modules/Properties + ../modules/StaticAndClassMethods + ../modules/SpecialMethodsAndProtocols + +Supplemental Readings +--------------------- + +About Python's "magic methods": + +https://www.python-course.eu/python3_magic_methods.php + +http://minhhh.github.io/posts/a-guide-to-pythons-magic-methods + +A Good Book (dead trees version only): + + Python 3 Object Oriented Programming: *Dusty Phillips* + + (Dusty is a local boy and co-founder of PuPPy) + + +Exercises: +========== + +.. toctree:: + :maxdepth: 1 + + ../exercises/circle/circle_class.rst + +Optional Exercise: +------------------ + +This one is challenging -- if you have got html_render and circle class done, then by all means -- it's a great exercise. but if you are still struggling with html_render -- get that done first. + +.. toctree:: + :maxdepth: 1 + + ../exercises/sparse_array.rst diff --git a/_sources/class_schedule/lesson09.rst.txt b/_sources/class_schedule/lesson09.rst.txt new file mode 100644 index 0000000..71d6b61 --- /dev/null +++ b/_sources/class_schedule/lesson09.rst.txt @@ -0,0 +1,42 @@ +.. _session_1_09: + +######################################################### +Session 9: Static and class methods: multiple inheritance +######################################################### + + +Readings +======== + +.. toctree:: + :maxdepth: 1 + + ../modules/MultipleInheritance + +Supplemental Readings +--------------------- + +Multiple Inheritance and the Diamond Problem +............................................ + +https://en.wikipedia.org/wiki/Multiple_inheritance + +https://en.wikipedia.org/wiki/Multiple_inheritance#The_diamond_problem + +Method Resolution Order +....................... + +If you really want to understand the way it works: + +https://www.python.org/download/releases/2.3/mro/ + +http://python-history.blogspot.com/2010/06/method-resolution-order.html + + +Exercises: +========== + +.. toctree:: + :maxdepth: 1 + + ../exercises/mailroom/mailroom-oo.rst diff --git a/_sources/class_schedule/lesson10.rst.txt b/_sources/class_schedule/lesson10.rst.txt new file mode 100644 index 0000000..039aa28 --- /dev/null +++ b/_sources/class_schedule/lesson10.rst.txt @@ -0,0 +1,27 @@ +.. _session_1_10: + +########################################################################### +Session 10: Intro to Functional Programming: lambda and Map, Filter, Reduce +########################################################################### + +Functional Programming + +Reading: +======== + +.. toctree:: + :maxdepth: 1 + + ../modules/OO_vs_functional + ../modules/Lambda + ../modules/MapFilterReduce + ../modules/IPythonParallel + + +Exercises +========= + +It's now time to catch up on everything you haven't quite finished. +In particular, you should finish an object oriented mailroom that is fully complete and unit tested. + + diff --git a/_sources/class_schedule/orientation.rst.txt b/_sources/class_schedule/orientation.rst.txt new file mode 100644 index 0000000..b9946e7 --- /dev/null +++ b/_sources/class_schedule/orientation.rst.txt @@ -0,0 +1,30 @@ +.. _orientation: + +########### +Orientation +########### + +Getting your system set up for the class. + + +Activities +========== + +Install Python: +--------------- +:ref:`installing_python` + +Setting Up your Environment: +---------------------------- + +:ref:`setting_up_dev_environment` + +Make sure you can run Python code: +---------------------------------- + +:ref:`how_to_run_a_python_file` + +When Done +========= + +Make sure you are ready to go! diff --git a/_sources/exercises/args_kwargs_lab.rst.txt b/_sources/exercises/args_kwargs_lab.rst.txt new file mode 100644 index 0000000..8d98ecd --- /dev/null +++ b/_sources/exercises/args_kwargs_lab.rst.txt @@ -0,0 +1,90 @@ +.. _exercise_args_kwargs_lab: + +``args`` and ``kwargs`` Lab +=========================== + +Goal: +----- + +Develop an understanding of using advanced argument passing and parameter definitons. + +If this is all confusing -- you may want to review this: + +http://stupidpythonideas.blogspot.com/2013/08/arguments-and-parameters.html + +.. note:: + + This is not all that clearly specified -- the goal is for you to + experiment with various ways to define and call functions, so you + can understand what's possible, and what happens with each call. + It is also entirely silly, since the function does not do anything + at all, but it will teach you about using parameters effectively. + + +Test Driven Development? +------------------------ + +Since this code isn't really going to do anything, it doesn't make a lot of sense to test it. However, you need to run the code somehow. So this is a good chance to practice test-driven development -- even if only as a way to run your code as you write it. + +For each step of the exercise, write a test that calls your function in a particular way, and test that it returns what you expect. In this case, what you will be testing is not really the code -- but rather your own expectations of what the results should be. + +You will also be testing Python's argument handling, which you can be pretty sure DOES work correctly. + +So while these won't be useful tests in the usual sense, this is a chance to get used to test driven development. + + +Procedure +--------- + +We are going to do this as test driven development: Your first task for each step below is to write a test that will ensure your code does what you think it should do. + +Keyword Arguments +----------------- + +* Write a function that has four optional parameters (with defaults): + + - `fore_color` + - `back_color` + - `link_color` + - `visited_color` + +* Have it return the colors (use strings for the colors, e.g. "blue", "red", etc.) + +* Call it with a couple different parameters set. That is, write tests that verify that all of the following work as advertised: + + - Using just positional arguments: + + - ``func('red', 'blue', 'yellow', 'chartreuse')`` + + - Using just keyword arguments: + + - ``func(link_color='red', back_color='blue')`` + + - using a combination of positional and keyword + + - ``func('purple', link_color='red', back_color='blue')`` + + - using ``*some_tuple`` and/or ``**some_dict`` + + - ``regular = ('red', 'blue')`` + + - ``links = {'link_color': 'chartreuse'}`` + + - ``func(*regular, **links)`` + + +Generic parameters +------------------ + +* Write a new function with the parameters as: + +``*args`` and ``**kwargs`` + +* Have it return the colors (use strings for the colors again) + +* Call it with the same various combinations of arguments used above. + +* Also have it print ``args`` and ``kwargs`` directly, so you can be sure you understand what's going on. + +* Note that in general, you can't know what will get passed into ``**kwargs`` So maybe adapt your function to be able to do something reasonable with any keywords. + diff --git a/_sources/exercises/circle/circle_class.rst.txt b/_sources/exercises/circle/circle_class.rst.txt new file mode 100644 index 0000000..b2c47af --- /dev/null +++ b/_sources/exercises/circle/circle_class.rst.txt @@ -0,0 +1,262 @@ +.. _exercise_circle_class: + +##################### +Circle Class Exercise +##################### + +Circle Class +============ + +The ultimate circle.... + + +Goal: +----- + +The goal is to create a class that represents a simple circle. + +A Circle can be defined by either specifying the radius or the diameter, +and the user can query the circle for either its radius or diameter. + +Other abilities of a Circle instance: + + * Compute the circle's area. + * Print the circle and get something nice. + * Be able to add two circles together. + * Be able to compare two circles to see which is bigger. + * Be able to compare to see if they are are equal. + * (follows from above) be able to put them in a list and sort them. + + +You will use: + + - properties. + - a bunch of "magic methods". + - a classmethod. + +General Instructions: +--------------------- + +1. For each step, write a couple of unit tests that test the new features. + +2. Run these tests (and they will fail the first time) + +3. Add the code required for your tests to pass. + + +Step 1: +------- + +Create class called ``Circle`` -- it's signature should look like:: + + c = Circle(the_radius) + +The radius is a required parameter (can't have a circle without one!) + +The resulting circle should have an attribute for the radius:: + + c.radius + +So you can do: + +.. code-block:: python + + >> c = Circle(4) + >> print(c.radius) + 4 + +Remember: tests first! + +Step 2: +------- + +Add a "diameter" property, so the user can get the diameter of the circle: + +.. code-block:: python + + >> c = Circle(4) + >> print(c.diameter) + 8 + +Step 3: +------- + +Set up the diameter property so that the user can set the diameter of the circle: + +.. code-block:: python + + >> c = Circle(4) + >> c.diameter = 2 + >> print c.diameter + 2 + >> print c.radius + 1 + +**NOTE** that the radius has changed! + +**Important:** Do not store both the radius and the diameter as attributes! If you do that, they could get out of sync. So store only one (the radius), and have the other calculated "on the fly" by the property. + +Step 4: +-------- + +Add an ``area`` property so the user can get the area of the circle: + +.. code-block:: python + + >> c = Circle(2) + >> print(c.area) + 12.566370 + +(``pi`` can be found in the math module). + +The user should not be able to set the area: + +.. code-block:: python + + >> c = Circle(2) + >> c.area = 42 + AttributeError + +Step 5: +------- + +Add an "alternate constructor" that lets the user create a Circle directly +with the diameter: + +.. code-block:: python + + >> c = Circle.from_diameter(8) + >> print(c.diameter) + 8 + >> print(c.radius) + 4 + +Hint: This is a good use case for a ``classmethod`` + +Step 6: +------- + +Every class should have a nice way to print it out... + +Add ``__str__`` and ``__repr__`` methods to your Circle class. + +Now you can print it: + +.. code-block:: ipython + + In [2]: c = Circle(4) + + In [3]: print(c) + Circle with radius: 4.000000 + + In [4]: repr(c) + Out[4]: 'Circle(4)' + + In [5]: d = eval(repr(c)) + + In [6]: d + Out[6]: Circle(4) + +Step 7: +-------- + +Add some of the numeric protocol to your Circle: + +You should be able to add two circles: + +.. code-block:: ipython + + In [7]: c1 = Circle(2) + + In [8]: c2 = Circle(4) + + In [9]: c1 + c2 + Out[9]: Circle(6) + +and multiply one by a number: + +.. code-block:: ipython + + In [16]: c2 * 3 + Out[16]: Circle(12) + +(what happens with ``3 * c2`` ? -- can you fix that?) + + + +Step 8: +-------- + +Add the ability to compare two circles: + +.. code-block:: ipython + + In [10]: c1 > c2 + Out[10]: False + + In [11]: c1 < c2 + Out[11]: True + + In [12]: c1 == c2 + Out[12]: False + + In [13]: c3 = Circle(4) + + In [14]: c2 == c3 + Out[14]: True + + +Once the comparing is done, you should be able to sort a list of circles: + +.. code-block:: ipython + + In [18]: print circles + [Circle(6), Circle(7), Circle(8), Circle(4), Circle(0), Circle(2), Circle(3), Circle(5), Circle(9), Circle(1)] + + In [19]: circles.sort() + + In [20]: print circles + [Circle(0), Circle(1), Circle(2), Circle(3), Circle(4), Circle(5), Circle(6), Circle(7), Circle(8), Circle(9)] + +**NOTE:** make sure to write unit tests for all of this! Ideally before writing the code. + +Step 8: Optional Features: +-------------------------- + +* See if you can make "reflected" numerics do the right thing: + +.. code-block:: python + + a_circle * 3 == 3 * a_circle + +* What else makes sense: division? others? + +* Add the "augmented assignment" operators, where they make sense: + +.. code-block:: python + + a_circle += another_circle + + a_circle *= 2 + +* Look through all the "magic methods" and see what makes sense for circles. + + +Step 9: Subclassing! +-------------------- + +You've got a circle already -- what if you needed a Sphere? They have a fair bit in common -- both defined by a radius, same relationship of radius to diameter, etc. + +So we can get a pretty useful Sphere class by simply subclassing Circle, and adding and changing a couple things. + +* Create a ``Sphere`` Class that subclasses ``Circle``. + +* Override the ``__str__`` and ``__repr__`` methods to be appropriate for Spheres. + +* Create a ``volume`` property that returns the volume (hint: volume of a sphere is: 4/3 pi r^3). + +* Override the area property so that it either computes the surface area of a sphere (what's the formula for that???), or have it raise an exception: maybe ``NotImplementedError``. + +Make sure to write some tests -- maybe ahead of time! -- that confirm that all this works. And the other things like addition, and sorting... + +Check that the ``Sphere.from_diameter()`` alternate constructor actually creates a Sphere! (you DO NOT have to write a new classmethod for that!) -- pretty cool, eh? diff --git a/_sources/exercises/circle_class.rst.txt b/_sources/exercises/circle_class.rst.txt new file mode 100644 index 0000000..b2c47af --- /dev/null +++ b/_sources/exercises/circle_class.rst.txt @@ -0,0 +1,262 @@ +.. _exercise_circle_class: + +##################### +Circle Class Exercise +##################### + +Circle Class +============ + +The ultimate circle.... + + +Goal: +----- + +The goal is to create a class that represents a simple circle. + +A Circle can be defined by either specifying the radius or the diameter, +and the user can query the circle for either its radius or diameter. + +Other abilities of a Circle instance: + + * Compute the circle's area. + * Print the circle and get something nice. + * Be able to add two circles together. + * Be able to compare two circles to see which is bigger. + * Be able to compare to see if they are are equal. + * (follows from above) be able to put them in a list and sort them. + + +You will use: + + - properties. + - a bunch of "magic methods". + - a classmethod. + +General Instructions: +--------------------- + +1. For each step, write a couple of unit tests that test the new features. + +2. Run these tests (and they will fail the first time) + +3. Add the code required for your tests to pass. + + +Step 1: +------- + +Create class called ``Circle`` -- it's signature should look like:: + + c = Circle(the_radius) + +The radius is a required parameter (can't have a circle without one!) + +The resulting circle should have an attribute for the radius:: + + c.radius + +So you can do: + +.. code-block:: python + + >> c = Circle(4) + >> print(c.radius) + 4 + +Remember: tests first! + +Step 2: +------- + +Add a "diameter" property, so the user can get the diameter of the circle: + +.. code-block:: python + + >> c = Circle(4) + >> print(c.diameter) + 8 + +Step 3: +------- + +Set up the diameter property so that the user can set the diameter of the circle: + +.. code-block:: python + + >> c = Circle(4) + >> c.diameter = 2 + >> print c.diameter + 2 + >> print c.radius + 1 + +**NOTE** that the radius has changed! + +**Important:** Do not store both the radius and the diameter as attributes! If you do that, they could get out of sync. So store only one (the radius), and have the other calculated "on the fly" by the property. + +Step 4: +-------- + +Add an ``area`` property so the user can get the area of the circle: + +.. code-block:: python + + >> c = Circle(2) + >> print(c.area) + 12.566370 + +(``pi`` can be found in the math module). + +The user should not be able to set the area: + +.. code-block:: python + + >> c = Circle(2) + >> c.area = 42 + AttributeError + +Step 5: +------- + +Add an "alternate constructor" that lets the user create a Circle directly +with the diameter: + +.. code-block:: python + + >> c = Circle.from_diameter(8) + >> print(c.diameter) + 8 + >> print(c.radius) + 4 + +Hint: This is a good use case for a ``classmethod`` + +Step 6: +------- + +Every class should have a nice way to print it out... + +Add ``__str__`` and ``__repr__`` methods to your Circle class. + +Now you can print it: + +.. code-block:: ipython + + In [2]: c = Circle(4) + + In [3]: print(c) + Circle with radius: 4.000000 + + In [4]: repr(c) + Out[4]: 'Circle(4)' + + In [5]: d = eval(repr(c)) + + In [6]: d + Out[6]: Circle(4) + +Step 7: +-------- + +Add some of the numeric protocol to your Circle: + +You should be able to add two circles: + +.. code-block:: ipython + + In [7]: c1 = Circle(2) + + In [8]: c2 = Circle(4) + + In [9]: c1 + c2 + Out[9]: Circle(6) + +and multiply one by a number: + +.. code-block:: ipython + + In [16]: c2 * 3 + Out[16]: Circle(12) + +(what happens with ``3 * c2`` ? -- can you fix that?) + + + +Step 8: +-------- + +Add the ability to compare two circles: + +.. code-block:: ipython + + In [10]: c1 > c2 + Out[10]: False + + In [11]: c1 < c2 + Out[11]: True + + In [12]: c1 == c2 + Out[12]: False + + In [13]: c3 = Circle(4) + + In [14]: c2 == c3 + Out[14]: True + + +Once the comparing is done, you should be able to sort a list of circles: + +.. code-block:: ipython + + In [18]: print circles + [Circle(6), Circle(7), Circle(8), Circle(4), Circle(0), Circle(2), Circle(3), Circle(5), Circle(9), Circle(1)] + + In [19]: circles.sort() + + In [20]: print circles + [Circle(0), Circle(1), Circle(2), Circle(3), Circle(4), Circle(5), Circle(6), Circle(7), Circle(8), Circle(9)] + +**NOTE:** make sure to write unit tests for all of this! Ideally before writing the code. + +Step 8: Optional Features: +-------------------------- + +* See if you can make "reflected" numerics do the right thing: + +.. code-block:: python + + a_circle * 3 == 3 * a_circle + +* What else makes sense: division? others? + +* Add the "augmented assignment" operators, where they make sense: + +.. code-block:: python + + a_circle += another_circle + + a_circle *= 2 + +* Look through all the "magic methods" and see what makes sense for circles. + + +Step 9: Subclassing! +-------------------- + +You've got a circle already -- what if you needed a Sphere? They have a fair bit in common -- both defined by a radius, same relationship of radius to diameter, etc. + +So we can get a pretty useful Sphere class by simply subclassing Circle, and adding and changing a couple things. + +* Create a ``Sphere`` Class that subclasses ``Circle``. + +* Override the ``__str__`` and ``__repr__`` methods to be appropriate for Spheres. + +* Create a ``volume`` property that returns the volume (hint: volume of a sphere is: 4/3 pi r^3). + +* Override the area property so that it either computes the surface area of a sphere (what's the formula for that???), or have it raise an exception: maybe ``NotImplementedError``. + +Make sure to write some tests -- maybe ahead of time! -- that confirm that all this works. And the other things like addition, and sorting... + +Check that the ``Sphere.from_diameter()`` alternate constructor actually creates a Sphere! (you DO NOT have to write a new classmethod for that!) -- pretty cool, eh? diff --git a/_sources/exercises/codingbat.rst.txt b/_sources/exercises/codingbat.rst.txt new file mode 100644 index 0000000..a677762 --- /dev/null +++ b/_sources/exercises/codingbat.rst.txt @@ -0,0 +1,21 @@ +.. _codingbat: + +####################### +Practice with CodingBat +####################### + + +Puzzles +======= + +To get a bit of exercise solving some puzzles with Python, work on the Python +exercises at "Coding Bat": http://codingbat.com/python + +There are 8 sets of puzzles. Do as many as you can, but make sure to do at least three -- and all the "Warmups" would be a good goal. + +While the CodingBat site has an interface for submitting your solution and see if it works, we suggest you write your code in your regular text editor and get it to run on your machine first. + +If you are in a formal class, let the instructor know which ones you have done. + +Remember this site -- it's a good idea to come back and play again with these little puzzles as you learn more Python. It's kind of like playing scales to keep fresh when learning a musical instrument. + diff --git a/_sources/exercises/comprehensions_lab.rst.txt b/_sources/exercises/comprehensions_lab.rst.txt new file mode 100644 index 0000000..5cf391f --- /dev/null +++ b/_sources/exercises/comprehensions_lab.rst.txt @@ -0,0 +1,238 @@ +.. _exercise_comprehensions: + +################## +Comprehensions Lab +################## + + +Playing with Comprehensions +============================ + +**Goal:** + +Getting Familiar with list, set and dict comprehensions + +List comprehensions +-------------------- + +Note: this is a bit of a "backwards" exercise -- we show you code, you figure out what it does. + +As a result, not much to submit -- don't worry about it -- you'll have a chance to use these in other exercises. + +.. code-block:: python + + >>> feast = ['lambs', 'sloths', 'orangutans', + 'breakfast cereals', 'fruit bats'] + + >>> comprehension = [delicacy.capitalize() for delicacy in feast] + +What is the output of: + +.. code-block:: python + + >>> comprehension[0] + ??? + + >>> comprehension[2] + ??? + +(figure it out before you try it) + +Filtering lists with list comprehensions +---------------------------------------- + +.. code-block:: python + + >>> feast = ['spam', 'sloths', 'orangutans', 'breakfast cereals', + 'fruit bats'] + + >>> comp = [delicacy for delicacy in feast if len(delicacy) > 6] + +What is the output of: + +.. code-block:: python + + >>> len(feast) + ??? + + >>> len(comp) + ??? + +(figure it out first!) + + +Unpacking tuples in list comprehensions +--------------------------------------- + +.. code-block:: python + + >>> list_of_tuples = [(1, 'lumberjack'), (2, 'inquisition'), (4, 'spam')] + + >>> comprehension = [ skit * number for number, skit in list_of_tuples ] + +What is the output of: + +.. code-block:: python + + >>> comprehension[0] + ??? + + >>> len(comprehension[2]) + ??? + + +Double list comprehensions +--------------------------- +.. code-block:: python + + >>> eggs = ['poached egg', 'fried egg'] + + >>> meats = ['lite spam', 'ham spam', 'fried spam'] + + >>> comprehension = \ + [ '{0} and {1}'.format(egg, meat) for egg in eggs for meat in meats] + + +What is the output of: + +.. code-block:: python + + >>> len(comprehension) + ??? + + >>> comprehension[0] + ??? + +Set comprehensions +------------------ + +.. code-block:: python + + >>> comprehension = { c for c in 'aabbbcccc'} + +What is the output of: + +.. code-block:: python + + >>> comprehension + ??? + + +Dictionary comprehensions +------------------------- + +.. code-block:: python + + >>> dict_of_weapons = {'first': 'fear', + 'second': 'surprise', + 'third':'ruthless efficiency', + 'forth':'fanatical devotion', + 'fifth': None} + >>> dict_comprehension = \ + { k.upper(): weapon for k, weapon in dict_of_weapons.items() if weapon} + +What is the output of: + +.. code-block:: python + + >>> 'first' in dict_comprehension + ??? + >>> 'FIRST' in dict_comprehension + ??? + >>> len(dict_of_weapons) + ??? + >>> len(dict_comprehension) + ??? + +Other resources +--------------- + +See also: + +https://github.com/gregmalcolm/python_koans/blob/master/python3/koans/about_comprehension.py + +From Greg Malcolm's excellent Python Koans series: + +https://github.com/gregmalcolm/python_koans + + +Count Even Numbers +------------------ + +This is from CodingBat "count_evens" (http://codingbat.com/prob/p189616) + +*Using a list comprehension*, return the number of even integers in the given list. + +Note: the % "mod" operator computes the remainder, e.g. ``5 % 2`` is 1. + +.. code-block:: python + + count_evens([2, 1, 2, 3, 4]) == 3 + + count_evens([2, 2, 0]) == 3 + + count_evens([1, 3, 5]) == 0 + + +Can you do this with a single line comprehension? + +.. code-block:: python + + def count_evens(nums): + one_line_comprehension_here + + +``dict`` and ``set`` comprehensions +------------------------------------ + +Revisiting the dict/set lab -- see how much you can do with comprehensions instead. + +(:ref:`exercise_dict_lab`) + +Specifically, look at these: + +First a slightly bigger, more interesting (or at least bigger..) dict: + +.. code-block:: python + + food_prefs = {"name": "Chris", + "city": "Seattle", + "cake": "chocolate", + "fruit": "mango", + "salad": "greek", + "pasta": "lasagna"} + +Working with this dict: +----------------------- + +1. Print the dict by passing it to a string format method, so that you get something like: + + "Chris is from Seattle, and he likes chocolate cake, mango fruit, + greek salad, and lasagna pasta" + +2. Using a list comprehension, build a dictionary of numbers from zero to fifteen and the hexadecimal equivalent (string is fine). (the ``hex()`` function gives you the hexidecimal representation of a number as a string) + +3. Do the previous entirely with a dict comprehension -- should be a one-liner + +4. Using the dictionary from item (1): Make a dictionary using the same keys but with the number of 'a's in each value. You can do this either by editing the dict in place, or making a new one. If you edit in place make a copy first! + +5. Create sets s2, s3 and s4 that contain numbers from zero through twenty, divisible 2, 3 and 4. + + a. Do this with one set comprehension for each set. + + b. What if you had a lot more than 3? -- Don't Repeat Yourself (DRY). + + - create a sequence that holds all the divisors you might want -- + could be 2,3,4, or could be any other arbitrary divisors. + + - loop through that sequence to build the sets up -- so no repeated code. + you will end up with a list of sets -- one set for each divisor in your + sequence. + + - The idea here is that when you see three (Or more!) lines of code that + are almost identical, then you you want to find a way to generalize + that code and have it act on a set of inputs, so the actual code is + only written once. + + c. Extra credit: do it all as a one-liner by nesting a set comprehension + inside a list comprehension. (OK, that may be getting carried away!) diff --git a/_sources/exercises/context-managers-exercise.rst.txt b/_sources/exercises/context-managers-exercise.rst.txt new file mode 100644 index 0000000..67e2b50 --- /dev/null +++ b/_sources/exercises/context-managers-exercise.rst.txt @@ -0,0 +1,124 @@ +.. _exercise_context_manager: + +############################### +A Couple Handy Context Managers +############################### + +Context managers can be used in a number of ways -- the classic is to manage resources - that is, close files and the like. But they are also useful for other handy things when you want to run some code before and after a block of code, or handle exceptions in special way. + +Timing Context Manager +====================== + +This is an example of running some code before and after the enclosed block. + +Create a context manager that will print the elapsed time taken to +run all the code inside the context: + +.. code-block:: ipython + + In [3]: with Timer() as t: + ...: for i in range(100000): + ...: i = i ** 20 + ...: + This code took 0.206805 seconds + +NOTE: the ``time`` module has what you need: + +.. code-block:: python + + import time + + start = time.clock() + # some code here + elapsed = time.clock() - start + +``time.clock()`` returns the number of seconds that this process has been running. You can also use ``time.time()``, which gives the "wall time", rather than the process time. ``time()`` will vary more depending on how busy the system is. But you may want to use it if you want to measure how long it takes to download something, for instance. + +Extra Credit +------------ + +Allow the ``Timer`` context manager to take a file-like +object as an argument (the default should be ``sys.stdout``). The results of the +timing should be printed to the file-like object. You could also pass in a name for this particular context, so the message in the file-like object is labeled -- kind of a poor man's logging system. + +Extra Extra Credit +------------------ + +Implement this as a generator, wrapped by the: + +``contextlib.contextmanager`` + +decorator. + +The pytest error handler +======================== + +pytest come with a nifty context manager for testing for error conditions: + +.. code-block:: python + + with pytest.raises(ZeroDivisionError) + 5 / 0 + +The test should pass + +But: + +.. code-block:: python + + with pytest.raises(ZeroDivisionError) + 5 / 2 + +This test should fail -- no Exception occurred. + +And so should this one: + +.. code-block:: python + + with pytest.raises(ValueError) + 5 / 0 + +the wrong Exception occurred. + +You task is to write a similar context manager (yes, it's already written, but this should help you understand how to handle exceptions in context managers...) + + +Extra Credit +------------ + +The pytest version has a few other features: + +[https://docs.pytest.org/en/latest/assert.html#assertions-about-expected-exceptions] + +See if you can implement a few of them.... + +Hints: +------ + +tests fail when an assert fails: + +.. code-block:: python + + assert some_expression, "a message" + +you get a failure when ``some_expression`` evaluates as false. + +This is more-or-less the same as this code: + +.. code-block:: python + + if some_expression: + raise AssertionError("a message") + +The reason it exists is not so much to save a bit of typing (though that's nice), but that assertions are designed for tests, and thus can be turned off for an entire python process -- and, indeed are turned off when you turn on optimization. + +So in your context manager, you can raise an AssertionError, or force one with an assert: + +.. code-block:: python + + assert False, "a message" + +either will work fine. + +See: +:download:`raising_an_assert.py <../examples/context_managers/raising_an_assert.py>` diff --git a/_sources/exercises/dict_lab.rst.txt b/_sources/exercises/dict_lab.rst.txt new file mode 100644 index 0000000..b64cf2f --- /dev/null +++ b/_sources/exercises/dict_lab.rst.txt @@ -0,0 +1,109 @@ +.. _exercise_dict_lab: + +********************** +Dictionary and Set Lab +********************** + +Learning about dictionaries and sets + + +Goal: +----- + +Learn the basic ins and outs of Python dictionaries and sets. + +Procedure +--------- + +Create a new ``dict_lab.py`` file. + +The file should be an executable Python script. That is to say that one +should be able to run the script directly like so: + +.. code-block:: bash + + $ ./dict_lab.py + +(on OS-X and Linux) or + +.. code-block:: bash + + $ py dict_lab.py + +(on Windows) + +To make this work you, make sure you include the 'shebang' on the first line of your file. + +.. code-block:: bash + + #!/usr/bin/env python3 + + +Finally (on OS-X and linux) you need to make the file executable; do that with this command: + +.. code-block:: bash + + $ chmod +x dict_lab.py + +(The +x means make this executable) + + +Add the file to your clone of the repository and commit changes frequently +while working on the following tasks. + +When the script is run, it should accomplish the following four series of +actions: + +Dictionaries 1 +-------------- + +* Create a dictionary containing "name", "city", and "cake" for "Chris" from "Seattle" who likes "Chocolate" (so the keys should be: "name", etc, and values: "Chris", etc.) + +* Display the dictionary. + +* Delete the entry for "cake". + +* Display the dictionary. + +* Add an entry for "fruit" with "Mango" and display the dictionary. + + - Display the dictionary keys. + - Display the dictionary values. + - Display whether or not "cake" is a key in the dictionary (i.e. False) (now). + - Display whether or not "Mango" is a value in the dictionary (i.e. True). + + +Dictionaries 2 +-------------- + +* Using the dictionary from item 1: Make a dictionary using the same keys but + with the number of 't's in each value as the value (consider upper and lower case?). + + The result should look something like:: + + {"name": 0 + "city": 2 + "cake": 2 + } + +Sets +---- + +* Create sets s2, s3 and s4 that contain numbers from zero through twenty, + divisible by 2, 3 and 4 (figure out a way to compute those -- don't just type them in). + +* Display the sets. + +* Display if s3 is a subset of s2 (False) + +* and if s4 is a subset of s2 (True). + +Sets 2 +------ + +* Create a set with the letters in 'Python' and add 'i' to the set. + +* Create a frozenset with the letters in 'marathon'. + +* Display the union and intersection of the two sets. + diff --git a/_sources/exercises/except_exercise.rst.txt b/_sources/exercises/except_exercise.rst.txt new file mode 100644 index 0000000..c4b0fdc --- /dev/null +++ b/_sources/exercises/except_exercise.rst.txt @@ -0,0 +1,78 @@ +.. _exercise_exceptions: + +################### +Exceptions Exercise +################### + +This is a little exercise that shows you how to handle exceptions in a way that mirrors actual development. + +Procedure +========= + +Here are two files that you should put in your ``lesson05`` directory in the class repo. + +:download:`except_exercise.py` + +:download:`except_test.py` + +Run ``except_exercise.py``:: + + $ python except_exercise.py + +(or ``run except_exercise.py`` in iPython) + +You will find that it crashes with an exception. + +Your job is to write the proper exception handler in the except_exercise.py +code, so that the code can run. + +It will then crash again. + +You will then need to handle the next exception. + +There are instructions in the ``except_exercise.py`` file telling you want you want to achieve. + +This is simulating writing code that is using another library -- your code is ``except_exercise.py`` and ``except_test.py`` is the other library. So you don't want to alter ``except_test.py`` -- only change the ``except_exercise.py`` file. + +**Hint:** the exceptions themselves usually come from the other file, so you will get a traceback like this:: + + --------------------------------------------------------------------------- + NameError Traceback (most recent call last) + ~/PythonStuff/UWPCE/Temp/except_exercise.py in () + 15 first_try = ['spam', 'cheese', 'mr death'] + 16 + ---> 17 joke = fun(first_try[0]) + 18 + 19 + + ~/PythonStuff/UWPCE/Temp/except_test.py in fun(reaper) + 13 def fun(reaper): + 14 if reaper == 'spam': + ---> 15 print(s) + 16 elif reaper == 'cheese': + 17 print() + + NameError: name 's' is not defined + +The ``NameError`` is coming from line 15 of ``except_test.py``. But this is not your code! So you need to look higher up in the traceback to see where in *your* code the exception is triggered. That is where you put your ``try--except`` block. + +In this case, that's line 17 of ``except_exercise.py``. In real life, it can be higher up in a much deeper stack trace -- but keep looking 'till you see your code. + +Results +------- + +When you are done, running except_exercise.py should result in output something like this:: + + Spam, Spam, Spam, Spam, Beautiful Spam + + Customer: Not much of a cheese shop really, is it? + Shopkeeper: Finest in the district, sir. + Customer: And what leads you to that conclusion? + Shopkeeper: Well, it's so clean. + Customer: It's certainly uncontaminated by cheese. + + +Why are you doing this? +----------------------- + +This is a kind of silly exercise, but in real life, this is a common workflow -- you call a library, and find that in certain circumstances it raises an exception. As the code in the library is out of your hands, you need to decide how to handle that exception in your code instead. diff --git a/_sources/exercises/exceptions/except_exercise.rst.txt b/_sources/exercises/exceptions/except_exercise.rst.txt new file mode 100644 index 0000000..2a1ae6e --- /dev/null +++ b/_sources/exercises/exceptions/except_exercise.rst.txt @@ -0,0 +1,79 @@ +.. _exercise_exceptions: + +################### +Exceptions Exercise +################### + +This is a little exercise that shows you how to handle exceptions in a way that mirrors actual development. + +Procedure +========= + +Here are two files that you should be in the github classroom repo: + +:download:`except_exercise.py` + +:download:`except_test.py` + +Run ``except_exercise.py``:: + + $ python except_exercise.py + +(or ``run except_exercise.py`` in iPython) + +You will find that it crashes with an exception. + +Your job is to write the proper exception handler in the +``except_exercise.py`` file, so that the code can run. + +It will then crash again. + +You will then need to handle the next exception. + +There are instructions in the ``except_exercise.py`` file telling you want you want to achieve. + +This is simulating writing code that is using another library -- your code is ``except_exercise.py`` and ``except_test.py`` is the other library. So you don't want to alter ``except_test.py`` -- only change the ``except_exercise.py`` file. + +**Hint:** the exceptions themselves usually come from the other file, so you will get a traceback like this:: + + --------------------------------------------------------------------------- + NameError Traceback (most recent call last) + ~/PythonStuff/UWPCE/Temp/except_exercise.py in () + 15 first_try = ['spam', 'cheese', 'mr death'] + 16 + ---> 17 joke = fun(first_try[0]) + 18 + 19 + + ~/PythonStuff/UWPCE/Temp/except_test.py in fun(reaper) + 13 def fun(reaper): + 14 if reaper == 'spam': + ---> 15 print(s) + 16 elif reaper == 'cheese': + 17 print() + + NameError: name 's' is not defined + +The ``NameError`` is coming from line 15 of ``except_test.py``. But this is not your code! So you need to look higher up in the traceback to see where in *your* code the exception is triggered. That is where you put your ``try--except`` block. + +In this case, that's line 17 of ``except_exercise.py``. In real life, it can be higher up in a much deeper stack trace -- but keep looking 'till you see your code. + +Results +------- + +When you are done, running ``except_exercise.py`` should result in output something like this:: + + Spam, Spam, Spam, Spam, Beautiful Spam + + Customer: Not much of a cheese shop really, is it? + Shopkeeper: Finest in the district, sir. + Customer: And what leads you to that conclusion? + Shopkeeper: Well, it's so clean. + Customer: It's certainly uncontaminated by cheese. + + +Why are you doing this? +----------------------- + +This is a kind of silly exercise, but in real life, this is a common work flow -- you call a library, and find that in certain circumstances it raises an exception. +As the code in the library is out of your hands, you need to decide how to handle that exception in your code instead. diff --git a/_sources/exercises/exceptions/exceptions_lab.rst.txt b/_sources/exercises/exceptions/exceptions_lab.rst.txt new file mode 100644 index 0000000..ffe9d27 --- /dev/null +++ b/_sources/exercises/exceptions/exceptions_lab.rst.txt @@ -0,0 +1,23 @@ +.. _exercise_exceptions_lab: + +************** +Exceptions Lab +************** + +Learning Exceptions +=================== + +Just a little bit for the basics -- this is a bit of an odd example, but should give you the idea if you're still not sure. + +Alternatively, see if you can clean up some of your other code (mailroom, maybe) with Exception handling. + +Exceptions Lab +--------------- + +Improving ``input`` + +* The ``input()`` function can generate two exceptions: ``EOFError`` + or ``KeyboardInterrupt`` on end-of-file(EOF) or canceled input. + +* Create a wrapper function, perhaps ``safe_input()`` that returns ``None`` + rather rather than raising these exceptions, when the user enters ``^C`` for Keyboard Interrupt, or ``^D`` (``^Z`` on Windows) for End Of File. diff --git a/_sources/exercises/exceptions_lab.rst.txt b/_sources/exercises/exceptions_lab.rst.txt new file mode 100644 index 0000000..ffe9d27 --- /dev/null +++ b/_sources/exercises/exceptions_lab.rst.txt @@ -0,0 +1,23 @@ +.. _exercise_exceptions_lab: + +************** +Exceptions Lab +************** + +Learning Exceptions +=================== + +Just a little bit for the basics -- this is a bit of an odd example, but should give you the idea if you're still not sure. + +Alternatively, see if you can clean up some of your other code (mailroom, maybe) with Exception handling. + +Exceptions Lab +--------------- + +Improving ``input`` + +* The ``input()`` function can generate two exceptions: ``EOFError`` + or ``KeyboardInterrupt`` on end-of-file(EOF) or canceled input. + +* Create a wrapper function, perhaps ``safe_input()`` that returns ``None`` + rather rather than raising these exceptions, when the user enters ``^C`` for Keyboard Interrupt, or ``^D`` (``^Z`` on Windows) for End Of File. diff --git a/_sources/exercises/fib_and_lucas.rst.txt b/_sources/exercises/fib_and_lucas.rst.txt new file mode 100644 index 0000000..b024393 --- /dev/null +++ b/_sources/exercises/fib_and_lucas.rst.txt @@ -0,0 +1,117 @@ +.. _exercise_fibonacci: + +************************* +Fibonacci Series Exercise +************************* + +Computing the Fibonacci and Lucas Series +======================================== + +Goal: +----- + +The `Fibonacci Series`_ is a numeric series starting with the integers 0 and 1. + +In this series, the next integer is determined by summing the previous two + + +This gives us:: + + 0, 1, 1, 2, 3, 5, 8, 13, ... + +.. note: 0+1 is 1; 1+1 is 2; 1+2 is 3; 2+3 is 5; 3+5 is 8; and so on forever... + +We will write a function that computes this series -- then generalize it. + +.. _Fibonacci Series: http://en.wikipedia.org/wiki/Fibbonaci_Series + +Step 1 +------ + +* Create a new module ``series.py`` in the ``lesson02`` folder in your student folder. + + - In it, add a function called ``fibonacci``. + + - The function should have one parameter ``n``. + + - The function should return the ``nth`` value in the fibonacci series (starting with zero index). + +* Ensure that your function has a well-formed ``docstring`` + +Note that the fibonacci series is naturally recursive -- the value is +defined by previous values: + +fib(n) = fib(n-2) + fib(n-1) + + +Lucas Numbers +-------------- + +The `Lucas Numbers`_ are a related series of integers that start with the +values 2 and 1 rather than 0 and 1. The resulting series looks like this:: + + 2, 1, 3, 4, 7, 11, 18, 29, ... + +.. _Lucas Numbers: http://en.wikipedia.org/wiki/Lucas_number + + +In your ``series.py`` module, add a new function ``lucas`` that returns the +``nth`` value in the *lucas numbers* series (starting with zero index). + +Ensure that your function has a well-formed ``docstring`` + +YOu should find it's *very* similar to the ``fibonacci()`` function. + +Generalizing +------------ + +Both the *fibonacci series* and the *lucas numbers* are based on an identical formula: + +fib(n) = fib(n-2) + fib(n-1) + +That's why the code is so similar. + +This formula creates a class of series that are all related -- each with a different two starting numbers. + +Add a third function called ``sum_series`` that can compute all of these related series. + +It should have one required parameter and two optional parameters. +The required parameter will determine which element in the +series to print. +The two optional parameters will have default values of 0 and 1 and will determine the first two values for the series to be produced. + +Calling this function with no optional parameters will produce numbers from the *fibonacci series* (because 0 and 1 are the defaults). + +Calling it with the optional arguments 2 and 1 will +produce values from the *lucas numbers*. + +Other values for the optional parameters will produce other series. + +**Note:** While you *could* check the input arguments, and then call one +of the functions you wrote, the idea of this exercise is to make a general +function, rather than one specialized. So you should re-implement the code +in this function. + +In fact, you could go back and re-implement your fibonacci and lucas +functions to call ``sum-series`` with particular arguments. + +Ensure that your function has a well-formed ``docstring`` + +Tests... +-------- + +Add a block of code to the end of your ``series.py`` module. +Use the block to write a series of ``assert`` statements that +demonstrate that your three functions work properly. + +Use comments in this block to inform the observer what your tests do. + +We have created a template for you to use to clarify what we mean by these asserts: + +:download:`series_template.py <../exercises/series_template.py>` + +Add your new module to your personal git repo and commit frequently while working on your implementation. +Include good commit messages that explain concisely both *what* you are doing and *why*. + +When you are finished, push your changes to your fork of the class repository in GitHub and make a pull request. + diff --git a/_sources/exercises/file_lab.rst.txt b/_sources/exercises/file_lab.rst.txt new file mode 100644 index 0000000..9aa4dfd --- /dev/null +++ b/_sources/exercises/file_lab.rst.txt @@ -0,0 +1,68 @@ +.. _exercise_file_lab: + +############# +File Exercise +############# + +A bit of practice with files + +Goal: +===== + +Get a little bit of practice with handling files and parsing simple text. + + +Paths and File Processing +========================= + +* Write a program which prints the full path for all files in the current + directory, one per line. Use either the ``os`` module or ``pathlib``. + +* Write a program which copies a file from a source, to a destination + (without using shutil, or the OS copy command (you are essentially writing a simple version of the OS copy command)). + + - This should work for any kind of file, so you need to open + the files in binary mode: ``open(filename, 'rb')`` (or ``'wb'`` for + writing). Note that for binary files, you can't use ``readline()`` -- + lines don't have any meaning for binary files. + + - Test it with both text and binary files (maybe a jpeg or something of your choosing). + + - Advanced: make it work for any size file: i.e. don't read the entire + contents of the file into memory at once. + + - This should only be a few lines of code :-) + + +File reading and parsing +======================== + +Download this text file: + +:download:`students.txt <../examples/file_exercise/students.txt>` + +In it, you will find a list of names and what programming languages they have used in the past. This may be similar to a list generated at the beginning of this class. + +Write a little script that reads that file and generates a list of all the languages that have been used. + +What might be the best data structure to use to keep track of bunch of values (the languages) without duplication? + +The file format: +---------------- + +The first line of the file is: + +``Name: Nickname, languages`` + +And each line looks something like this: + +``Jagger, Michael: Mick, shell, python`` + +So a colon after the name, then the nickname, and then one or more languages. + +However, like real data files, the file is NOT well-formed. Only some lines have nicknames, and other small differences, so you will need to write some code to make sure you get it all correct. + +How can you tell the difference between a nickname and a language? + +Extra challenge: keep track of how many students specified each language. + diff --git a/_sources/exercises/file_processing/file_lab.rst.txt b/_sources/exercises/file_processing/file_lab.rst.txt new file mode 100644 index 0000000..b771d5e --- /dev/null +++ b/_sources/exercises/file_processing/file_lab.rst.txt @@ -0,0 +1,33 @@ +.. _exercise_file_lab: + +############# +File Exercise +############# + + +Goal: +===== + +Get a little bit of practice with handling files and parsing simple text. + + +Paths and File Processing +========================= + +* Write a program which prints the full path for all files in the current + directory, one per line. Use either the ``os`` module or ``pathlib``. + +* Write a program which copies a file from a source, to a destination + (without using shutil, or the OS copy command (you are essentially writing a simple version of the OS copy command)). + + - This should work for any kind of file, so you need to open + the files in binary mode: ``open(filename, 'rb')`` (or ``'wb'`` for + writing). Note that for binary files, you can't use ``readline()`` -- + lines don't have any meaning for binary files. + + - Test it with both text and binary files (maybe a jpeg or something of your choosing). + + - Advanced: make it work for any size file: i.e. don't read the entire + contents of the file into memory at once. + + - This should only be a few lines of code :-) diff --git a/_sources/exercises/file_processing/file_processing.rst.txt b/_sources/exercises/file_processing/file_processing.rst.txt new file mode 100644 index 0000000..0f2ffd9 --- /dev/null +++ b/_sources/exercises/file_processing/file_processing.rst.txt @@ -0,0 +1,41 @@ +.. _exercise_file_processing: + +############### +File Processing +############### + +A bit of practice with reading and processing files. + + +File reading and parsing +======================== + +Download this text file: + +:download:`students.txt <./students.txt>` + +In it, you will find a list of names and what programming languages they have used in the past. This may be similar to a list generated at the beginning of this class. + +Write a little script that reads that file and generates a list of all the languages that have been used. + +What might be the best data structure to use to keep track of bunch of values (the languages) without duplication? + +The file format: +---------------- + +The first line of the file is: + +``Name: Nickname, languages`` + +And each line looks something like this: + +``Jagger, Michael: Mick, shell, python`` + +So a colon after the name, then the nickname, and then one or more languages. + +However, like real data files, the file is NOT well-formed. Only some lines have nicknames, and other small differences, so you will need to write some code to make sure you get it all correct. + +How can you tell the difference between a nickname and a language? + +Extra challenge: keep track of how many students specified each language. + diff --git a/_sources/exercises/fizz_buzz.rst.txt b/_sources/exercises/fizz_buzz.rst.txt new file mode 100644 index 0000000..31a7f0d --- /dev/null +++ b/_sources/exercises/fizz_buzz.rst.txt @@ -0,0 +1,66 @@ +.. _exercise_fizz_buzz: + +****************** +Fizz Buzz Exercise +****************** + +The Classic Fizz Buzz Problem +============================== + +Fizz Buzz is a classic simple problem in computer science. + +It is often used as an exercise in interviews for programmers. + +Apparently a LOT of people applying for jobs as professional developers can't do this in an interview: + +(http://c2.com/cgi/wiki?FizzBuzzTest) + +Now that we've psyched you out -- it's really pretty straightforward. + +Goal: +----- + +* Write a program that prints the numbers from 1 to 100 inclusive. + +* But for multiples of three print "Fizz" instead of the number. + +* For the multiples of five print "Buzz" instead of the number. + +* For numbers which are multiples of both three and five print "FizzBuzz" instead. + +Hint: +----- + +* Look up the ``%`` operator. What do these do? + + * ``10 % 7`` + * ``14 % 7`` + +(try that in iPython) + +* **Do** try to write a solution *before* looking it up -- there are a million nifty solutions posted on the web, but you'll learn a lot more if you figure it out on your own first. + + +Results: +-------- + +Running your code should result in something like:: + + 1 + 2 + Fizz + 4 + Buzz + Fizz + 7 + 8 + Fizz + Buzz + 11 + Fizz + 13 + 14 + FizzBuzz + 16 + .... + diff --git a/_sources/exercises/grid_printer.rst.txt b/_sources/exercises/grid_printer.rst.txt new file mode 100644 index 0000000..87804fd --- /dev/null +++ b/_sources/exercises/grid_printer.rst.txt @@ -0,0 +1,224 @@ +.. _exercise_grid_printer: + +********************* +Grid Printer Exercise +********************* + +Printing a Grid +================ + +(adapted from Downey, "Think Python", ex. 3.5) + +Goal: +----- + +Write a function that draws a grid like the following:: + + + - - - - + - - - - + + | | | + | | | + | | | + | | | + + - - - - + - - - - + + | | | + | | | + | | | + | | | + + - - - - + - - - - + + +Hints +----- + + +A couple features to get you started... + +printing +-------- + +To print more than one value on a line, you can pass multiple names into the print function: + +.. code-block:: python + + print('+', '-') + +If you don't want a newline after something is printed, you tell Python what you want the print to end with like so: + +.. code-block:: python + + print('+', end=' ') + print('-') + +The output of these statements is ``'+ -'``. + +(that end parameter defaults to a newline...) + +no arguments +------------ + +A print function with no arguments ends the current line and goes to the next line: + +.. code-block:: python + + print() + +simply prints and empty line. + +Simple string manipulation: +--------------------------- + +You can put two strings together with the plus operator: + +.. code-block:: ipython + + In [20]: "this" + "that" + Out[20]: 'thisthat + + +This is called concatenation. + +Concatenation is particularly useful if the strings have been assigned names: + +.. code-block:: ipython + + In [21]: plus = '+' + + In [22]: minus = '-' + + In [23]: plus + minus + plus + Out[23]: '+-+' + +Note that you can link any number of operations together in an expression. + +Multiplication of strings +------------------------- + +You can also multiply strings: + +.. code-block:: ipython + + In [24]: '+' * 10 + Out[24]: '++++++++++' + +And combine that with plus in a complex expression: + +.. code-block:: ipython + + In [29]: first_name = 'Chris' + + In [30]: last_name = 'Barker' + + In [31]: 5 * '*' + first_name +' ' + last_name + 5 * '*' + Out[31]: '*****Chris Barker*****' + +Note that there are better ways to build up complex strings -- we'll get to that later. + +Now you've got what you need to print that grid... + +** give it a try! ** + +Part 2 +======= + +Making it more general. + +Make it a function +------------------ + +One of the points of writing functions is so you can write code that does similar things, but customized by the values of input parameters. So what if we want to be able to print that grid at an arbitrary size? + +Write a function ``print_grid(n)`` that takes one integer argument and prints a grid just like before, *BUT* the size of the grid is given by the argument. + +For example, ``print_grid(9)`` prints the grid at the top of this page. + +``print_grid(3)`` would print a smaller grid:: + + + - + - + + | | | + + - + - + + | | | + + - + - + + + +``print_grid(15)`` prints a larger grid:: + + + - - - - - - - + - - - - - - - + + | | | + | | | + | | | + | | | + | | | + | | | + | | | + + - - - - - - - + - - - - - - - + + | | | + | | | + | | | + | | | + | | | + | | | + | | | + + - - - - - - - + - - - - - - - + + + +This problem is underspecified. Do something reasonable. + + +Part 3: +======= + +Even more general... + +A function with two parameters +------------------------------ + +Write a function that draws a similar grid with a specified number of rows and columns, and with each cell a given size. + +For example, ``print_grid2(3,4)`` results in:: + + + - - - - + - - - - + - - - - + + | | | | + | | | | + | | | | + | | | | + + - - - - + - - - - + - - - - + + | | | | + | | | | + | | | | + | | | | + + - - - - + - - - - + - - - - + + | | | | + | | | | + | | | | + | | | | + + - - - - + - - - - + - - - - + + +(three rows, three columns, and each grid cell four "units" in size) + +What to do about rounding? -- you decide. + +Another example: ``print_grid2(5,3)``:: + + + - - - + - - - + - - - + - - - + - - - + + | | | | | | + | | | | | | + | | | | | | + + - - - + - - - + - - - + - - - + - - - + + | | | | | | + | | | | | | + | | | | | | + + - - - + - - - + - - - + - - - + - - - + + | | | | | | + | | | | | | + | | | | | | + + - - - + - - - + - - - + - - - + - - - + + | | | | | | + | | | | | | + | | | | | | + + - - - + - - - + - - - + - - - + - - - + + | | | | | | + | | | | | | + | | | | | | + + - - - + - - - + - - - + - - - + - - - + + +Have fun! + diff --git a/_sources/exercises/html_renderer.rst.txt b/_sources/exercises/html_renderer.rst.txt new file mode 100644 index 0000000..8761e90 --- /dev/null +++ b/_sources/exercises/html_renderer.rst.txt @@ -0,0 +1,790 @@ +.. _exercise_html_renderer: + +###################### +HTML Renderer Exercise +###################### + +Ever need to write some HTML? Maybe from data? + +And not want to write all those tags yourself? + +OK, maybe not -- but trust me, it's a pain -- let's write some code to do it for us. + +HTML Renderer +============= + +Goal: +----- + +The goal is to create a set of classes to render html pages -- in a "pretty printed" way. + +i.e. nicely indented and human readable. + +We'll try to get to all the features required to render this file: + +:download:`sample_html.html <../examples/html_render/sample_html.html>` + +Take a look at it by opening it in your text editor. And also in a browser to see how it's rendered. + +If you don't know html -- just look at the example and copy that. And you can read the: :ref:`html_primer` at the end of this page for enough to do this exercise. And anyone working with computers these days would benefit from at least a passing familiarity with html. + +The exercise is broken down into a number of steps -- each requiring a few more OO concepts in Python. + +The goal of the code is to render html. The goal of the *exercise* is to build up a simple object hierarchy with: + +* classes +* class attributes +* instance attributes +* methods +* subclassing +* overriding attributes and methods + + +General Instructions: +--------------------- + +You can start with the framework in: + +:download:`html_render.py <../examples/html_render/html_render.py>` + +For each step, add the required functionality. There is example code to run your code for each step in: + +:download:`run_html_render.py <../examples/html_render/run_html_render.py>` + +You should be able to run that code at each step, uncommenting each new step in ``run_html_render.py`` as you go. + +It builds up an html tree, and then calls the ``render()`` method of your element to render the page. + +It uses a ``StringIO`` object (like a file, but in memory) to render to memory, then dumps it to the console, and writes a file. Take a look at the render_page function at the top of the file to make sure you understand it. + +The html generated at each step will be written to the files named: +``test_html_ouput?.html`` + +Unit tests +---------- + +Running the ``run_html_render.py`` script is a (simple) form of integration testing -- it checks how the individual components are working together. But we also want to make sure each individual *unit* (class, method) of code works. So to do that, we'll use: + +**Test Driven Development** + +In addition to checking if the output is what you expect with the running script -- you should also write unit tests as you go. + +Each new line of code should have a test that will run it -- *before* you write that code. + +That is: + + 1. write a test that exercises the next step in your process + 2. run the tests -- the new test will fail + 3. write your code... + 4. run the tests. If it still fails, go back to step 3... + +A start of a test file is provided here: + +:download:`test_html_render.py <../examples/html_render/test_html_render.py>` + +It has a few tests for the first few steps -- uncomment them as you go along. + +But it is NOT comprehensive -- you will need to add more tests at every step! + +You can run ``pytest`` on that test file first thing -- it should pass two tests (that you can create an Element object -- not that it works) and fail one -- one that actually starts to test functionality. + +**NOTE** if you are lost, take a look at the tutorial here: +:ref:`html_renderer_tutorial`. But do try to do it yourself first. + +Step 0: +------- + +Before you can start writing code, you need to get setup. + +* In your directory in the class repo called ``lesson07`` +* In that directory, you can start working on the code. Start by putting the files you just downloaded in that dir: + + - ``html_render.py``, ``run_html_render.py``, + ``sample_html.html``, ``test_html_render.py`` + +* Add those files to your git repo: + + - ``git add *.py sample_html.html`` + + +Step 1: +------- + +.. rubric:: 1a. + +Create an ``Element`` class for rendering an html element (xml element). + +There is a skeleton for one in ``html_render.py`` -- it has just enough so that the first few tests in ``test_html_render.py`` can run -- though that won't pass! + +Do run those tests first -- then add the code to make them pass. + +The ``Element`` class should have a class attribute for the tag name ("html" first). + +The initializer signature should look like: + +.. code-block:: python + + Element(content=None) + +Where ``content`` is expected to be a string -- and defaults to nothing. + +.. rubric:: 1b. + +The class should have an ``append`` method that can add another string to the content. + +(The ``html_render.py`` file you downloaded above should have a skeleton for this class in it.) + +So your class will need a way to store the content in a way that you can keep adding more to it. + +An ``Element`` object has to collect a bunch of sub-elements, in order, and you need to be able to append new ones to it -- sounds like a ``list``, doesn't it? So should it subclass from ``list``? + +Ask yourself -- does this make sense? an "Element *is* a list" -- no. + +But "An Element *uses* a list" makes perfect sense. + +If the *is* phrase makes sense, then subclassing would makes sense. If the *uses* phrase makes sense, *then* you would not want to subclass. + +So no -- you don't want ``Element`` to subclass from list. + +.. rubric:: 1c. + +It should have a ``render(file_out)`` method that renders the tag and the strings in the content. + +``file_out`` could be any open, writable file-like object ( i.e. have a ``write()`` method ). This is what you get from the ``open()`` function -- but there are other kinds of file-like objects. The html will be rendered to this file-like object. + +**NOTE:** html is not sensitive to newlines -- but you don't want all your html on one line. so put a newline in after each tag and each content string. Later on in the assignment, you'll add indentation as well! + +So this ``render()`` method takes a file-like object, and calls its ``write()`` method, writing the html for a tag. + +Once this works, this code: + +.. code-block:: python + + page = Element("Some content") + page.append("Some more contenet") + with open("test.html", 'w') as outfile: + page.render(outfile) + +Will result in a file with something like this in it: + +.. code-block:: html + + + Some content. + Some more content. + + +That is, you should now be able to render an html tag with text in it as content. + +See: step 1. in ``run_html_render.py`` and the test code. + +If you are stuck -- see the tutorial: :ref:`render_tutorial_1` + +Step 2: +------- + +Part A: +....... + +Create a couple subclasses of ``Element``, for each of ````, ````, and ``

`` tags. All you should have to do is override the ``tag`` class attribute (you may need to add a ``tag`` class attribute to the ``Element`` class first, if you haven't already). + +Now you can render a few different types of element. For example: + +.. code-block:: python + + page = Body("Some content") + page.append("Some more contenet") + with open("test.html", 'w') as outfile: + page.render(outfile) + +Will result in a file with something like this in it: + +.. code-block:: html + + + Some content. + Some more content. + + +Note: So why are we subclassing here? Because: "a body element *is* an ``Element``" makes perfect sense -- that's when you want to subclass. Another way to think about it is that you want to subclass to make a specialized version of something. + +You may note that the ``Element`` class really doesn't do anything by itself -- it needs a tag (at least) to be a proper element. This is what's called a "Base Class". It contains functionality required by various subclasses, but may not do anything on its own. In this case, we gave it the tag 'html', so we could run and test the render method. But strictly speaking, as a base class, it could have no tag. + +And of course these subclasses are pretty simple -- only overriding one class attribute. If that's all you need to do to specialize, there are other ways than subclassing to do it. But bear with us -- other element subclasses will require more specialization. + +If you are stuck -- see the tutorial: :ref:`render_tutorial_2_A` + +Part B: +....... + +Now it gets fun! + +Now that you have multiple types of elements, it's worth looking a bit at how html works. A given element can hold text, but it can *also* hold other elements. So we need to update our ``Element`` classes to support that. + +Extend the ``Element.render()`` method so that it can render other elements inside the tag in addition to strings. A recursion-like approach should do it. i.e. it can call the ``render()`` method of the elements it contains. + +You should be able to ``append`` an element to another element -- not just text. + +If this recursion-like idea doesn't make sense to you, take a look at this blog post, which talks about recursive algorithms: + +https://realpython.com/python-thinking-recursively/ + +Figure out a way to deal with the fact that the contained elements could be either simple strings or ``Element`` s with render methods (there are a few ways to handle that...). Think about "Duck Typing" and EAFP. See the section :ref:`notes_on_handling_duck_typing` and the end of the Exercise for more. + +You should now be able to render a basic web page with an ```` tag around the whole thing, a ```` tag inside, and multiple ``

`` tags inside that, with text inside that. + +So code like: + +.. code-block:: python + + page = Html() + body = Body() + body.append(P("a very small paragraph")) + body.append(P("another small paragraph")) + page.append(body) + with open("test.html", 'w') as outfile: + page.render(outfile) + +Should result in something like: + +.. code-block:: html + + + +

+ a very small paragraph +

+

+ another small paragraph +

+ + + +See: :download:`test_html_output2.html <../examples/html_render/test_html_output2.html>` + +NOTE: when you run step 2 in ``run_html_render.py``, you will want to comment out step 1 -- that way you'll only get one set of output. + +If you are stuck -- see the tutorial: :ref:`render_tutorial_2_B` + +Step 3: +------- + +Create a ```` element -- a simple subclass. + +Create a ``OneLineTag`` subclass of ``Element``: + +* It should override the render method, to render everything on one line -- for the simple tags, like:: + + PythonClass - Session 6 example + +Create a ``Title`` subclass of ``OneLineTag`` class for the title. + +You should now be able to render an html doc with a head element, with a +title element in that, and a body element with some ``

`` elements and some text. + +See :download:`test_html_output3.html <../examples/html_render/test_html_output3.html>` + +Step 4: +------- + +Extend the ``Element`` class to accept a set of attributes as keywords to the constructor, e.g. ``run_html_render.py`` + +.. code-block:: python + + Element("some text content", id="TheList", style="line-height:200%") + +html elements can take essentially any attributes -- so you can't hard-code these particular ones (remember ``**kwargs``? ) + +The render method will need to be extended to render the attributes properly. + +Note that you may now have *two* render methods -- the one in the ``Element`` base class, and the one in the ``OneLineTag`` class. They both need to be be able to handle attributes. But **DRY** -- so see if you can factor the code so the code that makes the opening tag, with the attributes is not repeated. + +You can now render some ``

`` tags (and others) with attributes. + +See: :download:`test_html_output4.html <../examples/html_render/test_html_output4.html>` + +NOTE: if you do "proper" CSS+html, then you wouldn't specify style directly in element attributes. + +Rather you would set the "class" attribute:: + +

+ This is my recipe for making curry purely with chocolate. +

+ +However, if you try this as a keyword argument in Python: + +.. code-block:: ipython + + In [1]: P("some content", class="intro") + File "", line 1 + P("some content", class="intro") + ^ + SyntaxError: invalid syntax + +Huh? + +"class" is a reserved work in Python -- for making classes. +So it can't be used as a keyword argument. + +But it's a fine key in a dict, so you can put it in a dict, and pass it in with ``**``: + +.. code-block:: python + + attrs = {'class': 'intro'} + P("some content", **attrs) + +You could also special-case this in your code -- so your users could use "clas" with one s, and you could tranlate it in the generated html. Or even both! + + +Step 5: +-------- + +Create a ``SelfClosingTag`` subclass of Element, to render tags like:: + +
and
(horizontal rule and line break). + +(See: https://www.w3schools.com/tags/tag_hr.asp) + +For example you should be able to use this code:: + + Hr(width=400) + +To get this result:: + +
+ +You will need to override the render method to render just the one tag and attributes, if any. + +Note that self closing tags can't have any content. Make sure that your SelfClosingTag element raises an exception if someone tries to put in any content -- probably a ``TypeError``. + +Create a couple subclasses of ``SelfClosingTag`` for ``
`` and ``
`` + +Note that you now have maybe three render methods -- is there repeated code in them? + +Can you refactor the common parts into a separate method that all the render methods can call? And do all your tests still pass (you do have tests for everything, don't you?) after refactoring? + +See: :download:`test_html_output5.html <../examples/html_render/test_html_output5.html>` + + +Step 6: +------- + +Create an ``A`` class for an anchor (link) element. Its constructor should look like:: + + A(self, link, content) + +where ``link`` is the link, and ``content`` is what you see. It can be called like so:: + + A("http://google.com", "link to google") + +and it should render like:: + + link to google + + +You should be able to subclass from ``Element``, and only override the ``__init__`` --- calling the ``Element`` ``__init__`` from the ``A`` ``__init__`` + +You can now add a link to your web page. + +See: :download:`test_html_output6.html <../examples/html_render/test_html_output6.html>` + +Step 7: +-------- + +Create ``Ul`` class for an unordered list (really simple subclass of ``Element``). + +Create ``Li`` class for an element in a list (also really simple). + +Add a list to your web page. + +Create a ``Header`` class -- this one should take an integer argument for the header level. i.e

,

,

, called like + +.. code-block:: python + + H(2, "The text of the header") + +for an

header. + +It can subclass from ``OneLineTag`` -- overriding the ``__init__``, then calling the superclass ``__init__`` + +See: :download:`test_html_output7.html <../examples/html_render/test_html_output7.html>` + +Step 8: +------- + +Update the ``Html`` element class to render the "" tag at the head of the page, before the html element. + +You can do this by subclassing ``Element``, overriding ``render()``, but then calling the ``Element`` render from the new render. + +Create a subclass of ``SelfClosingTag`` for ```` (like for ``
`` and ``
`` and add the meta element to the beginning of the head element to give your document an encoding. + +The doctype and encoding are HTML 5 and you can check this at: + +http://validator.w3.org/#validate_by_input + +You now have a pretty full-featured html renderer -- play with it, add some new tags, etc.... + +See :download:`test_html_output8.html <../examples/html_render/test_html_output8.html>` + + +Step 9: Adding Indentation +-------------------------- + +Indentation is not strictly required for html -- html ignores most whitespace. + +But it can make it much easier to read for humans, and it's a nice exercise to see how one might make it work in arbitrarily nested html. + +There is also more than one way to indent html -- so you have a bit of flexibility here. + +You will need to enhance your code in a couple ways to add indentation. + +A. Specify the indentation level +................................ + +Add a class attribute to the ``Element`` base class that indicates how much indentation you want -- you can either use a simple string: 2 or four spaces: + +.. code-block:: python + + class Element: + indent = " " + +Or you can use an integer to specify how many spaces you want to use: + +.. code-block:: python + + class Element: + indent = 4 + +Your render method(s) can access this attribute to know how much to indent a element. You want it as a class attribute in the base class, so that all the instances of all the subclasses will share the same value -- to indent all the html consistently. + +Then you need to pass this indentation down the tree as you render the page. + +B. Pass the "current level" of indentation down the tree of elements +.................................................................... + +html elements can be nested arbitrarily deep: + +.. code-block:: html + + + + + PythonClass = Revision 1087: + + +

+ Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

+
    +
  • + The first item in a list +
  • +
  • + This is the second item +
  • +
+ + + +So how does a given element know where it is in the tree? And therefore how deep to indent itself? + +One way: extend your ``render`` method(s) to take another parameter: + +.. code-block:: python + + def render(out_file, cur_ind=""): + + +``cur_ind`` is a string (or number) with the current level of indentation in it: the amount that the entire tag should be indented for pretty printing. + +This is a little tricky: ``cur_ind`` will be the amount that this element should be indented already. It will be from zero (an empty string) to a lot of spaces, depending on how deep it is in the tree. You could use an integer for the number of spaces to indent -- or keep it simple and just use a string with the correct number of spaces in it. + +The amount of each level of indentation should be set by the class attribute: ``indent`` + +So: + +* You probably want ``cur_ind`` to be an optional argument to render -- so it will not indent if nothing is passed in. + +* But if it is passed in, you want your code to USE the ``cur_ind`` parameter -- it is supposed to indicate how much this entire tag is already indented. + +* When a given element gets rendered, you don't know where it is in a potentially deeply nested hierarchy -- it could be at the top level or ten levels deep. passing ``cur_ind`` into the render method is how this is communicated. + +* So when you call ``render`` from *inside* a render method -- you need to tell the nested elements how deep to render themselves -- usually one more level of indentation deep. Probably something like: + + + +``sub_element.render(out_file, cur_ind + self.indent)`` + + +* Remember to keep the amount of spaces per indentation defined as a class attribute of the base class (the ``Element`` class). That way, you could change it in one place, and it would change everywhere and remain consistent. + +* Be sure to test that the indentation of the result changes if you change the class attribute! + +You should have nice pretty indented html now! + +See :download:`test_html_output9.html <../examples/html_render/test_html_output9.html>` + + +.. _notes_on_handling_duck_typing: + +Notes on handling "Duck Typing" +=============================== + +In this exercise, we need to deal with the fact that XML (and thus HTML) allows *either* plain text *or* other tags to be the content of a tag. So our code needs to handle the fact that there are two possible types that we need to be able to render. + +There are two primary ways to address this (and multiple ways to actually write the code for each of these). + +1) Make sure that the content only has renderable objects in it. + +2) Make sure the render() method can handle either type on the fly. + +The difference is where you handle the multiple types -- in the render method itself, or ahead of time, when you append new content to the ``Element``. + +The Ahead of Time Option: +------------------------- + +You can handle it ahead of time by creating a simple object that wraps a string and gives it a render method. As simple as: + +.. code-block:: python + + class TextWrapper: + """ + A simple wrapper that creates a class with a render method + for simple text + """ + def __init__(self, text): + self.text = text + + def render(self, file_out): + file_out.write(self.text) + + +You could require your users to use the wrapper, so instead of just appending a string, they would do: + +.. code-block:: python + + an_element.append(TextWrapper("the string they want to add")) + +But this is not very Pythonic style -- it's OO heavy. Strings for text are so common you want to be able to simply use them: + +.. code-block:: python + + an_element.append("the string they want to add") + +So much easier. + +To accomplish this, you can update the ``append()`` method to put this wrapper around plain strings when something new is added. + + +Checking if it's the Right Type +------------------------------- + +How do you decide if the wrapper is required? + +**Checking it it's an instance of Element:** + +You could check and see if the object being appended is an Element: + +.. code-block:: python + + if isinstance(content, Element): + self.content.append(content) + else: + self.content.append(TextWrapper(content)) + +This would work well, but closes the door to using any other type that may not be a strict subclass of Element, but can render itself. Not too bad in this case, but in general, frowned upon in Python. + + +Alternatively, you could check for the string type: + +.. code-block:: python + + if isinstance(content, str): + self.content.append(TextWrapper(content)) + else: + self.content.append(content) + +I think this is a little better -- strings are a pretty core type in Python, so it's not likely that anyone is going to need to use a "string-like" object. + +Duck Typing +----------- + +The Python model of duck typing is: If quacks like a duck, then treat it like a duck. + +But in this case, we're not actually rendering the object at this stage, so calling the method isn't appropriate. + +**Checking for an attribute** + +Instead of calling the method, see if it's there. You can do that with ``hasattr()`` + +Check if the passed-in object has a ``render`` attribute: + +.. code-block:: python + + if hasattr(content, 'render'): + self.content.append(content) + else: + self.content.append(TextWrapper(str(content)) + + +Note that I added a ``str()`` call too -- so you can pass in anything -- it will get stringified -- this will be ugly for many objects, but will work fine for numbers and other simple objects. + +This is my favorite. + + +Duck Typing on the Fly +---------------------- + +The other option is to simply put both elements and text in the content list, and figure out what to do in the ``render()`` method. + +Again, you could type check -- but I prefer the duck typing approach, and EAFP: + +.. code-block:: python + + try: + content.render(out_file) + except AttributeError: + outfile.write(content) + +If content is a simple string then it won't have a render method, and an ``AttributeError`` will be raised. + +You can catch that, and simply write the content directly instead. + + +You may want to turn it into a string, first:: + + outfile.write(str(content)) + +Then you could write just about anything -- numbers, etc. + + +Where did the Exception come from? +---------------------------------- + +**Caution** + +If the object doesn't have a ``render`` method, then an AttributeError will be raised. But what if it does have a render method, but that method is broken? + +Depending on what's broken, it could raise any number of exceptions. Most will not get caught by the except clause, and will halt the program. + +But if, just by bad luck, it has an bug that raises an ``AttributeError`` -- then this could catch it, and try to simply write it out instead. So you may get something like: ```` in the middle of your html. + +**The beauty of testing** + +If you have a unit test that calls every render method in your code -- then it should catch that error, and in the unit test it will be clear where it is coming from. + + +.. _html_primer: + +HTML Primer +============ + + +The very least you need to know about html to do this assignment. + + +If you are familiar with html, then this will all make sense to you. If you have never seen html before, this might be a bit intimidating, but you really don't need to know much to do this assignment. + +First of all, sample output from each step is provided. So all you really need to do is look at that, and make your code do the same thing. But it does help understand a little bit about what you trying to do. + +HTML +---- + +HTML is "Hyper Text Markup Language". Hypertext, because it can contain links +to other pages, and markup language means that text is "marked up" with +instructions about how to format the text, etc. + +Here is a good basic intro: + +http://www.w3schools.com/html/html_basic.asp + +And there are countless others online. + +As html is XML -- the XML intro is a good source of the XML syntax, too: + +http://www.w3schools.com/xml/default.asp + +But here is a tiny summary of just what you need to know for this project. + +Elements +-------- + +Modern HTML is a particular dialect of XML (eXtensible Markup Language), +which is itself a special case of SGML (Standard Generalized Markup Language) + +It inherits from SGML a basic structure: each piece of the document is an element. Each element is described by a "tag". Each tag has a different meaning, but they all have the same structure:: + + some content + +That is, the tag name is surrounded by < and >, which marks the beginning of +the element, and the end of the element is indicated by the same tag with a slash. + +The real power is that these elements can be nested arbitrarily deep. In order to keep that all readable, we often want to indent the content inside the tags, so it's clear what belongs with what. That is one of the tricky bits of this assignment. + + +Basic tags +---------- + +.. code-block:: html + + is the core tag indicating the entire document + +

is a single paragraph of text

+ + is the tag that indicated the text of the document + + defines the header of the document -- a place for metadata + +Attributes: +------------ + +In addition to the tag name and the content, extra attributes can be attached to a tag. These are added to the "opening tag", with name="something", another_name="something else" format: + +.. code-block:: html + +

+ +There can be all sorts of stuff stored in attributes -- some required for specific tags, some extra, like font sizes and colors. Note that since tags can essentially have any attributes, your code will need to support that -- doesn't it kind of look like a dict? And keyword arguments? + +Special Elements +---------------- + +The general structure is everything in between the opening and closing tag. But some elements don't really have content -- just attributes. So the slash goes at the end of the tag, after the attributes. We can call these self-closing tags: + +.. code-block:: html + + + +To make a link, you use an "anchor" tag: ````. It requires attributes to indicate what the link is: + +.. code-block:: html + + link + +The ``href`` attribute is the link (hyper reference). + +lists +----- + +To make a bulleted list, you use a

    tag (unordered list), and inside that, you put individual list items
  • : + +.. code-block:: html + +
      +
    • + The first item in a list +
    • +
    • + This is the second item +
    • +
    + +Note that the list itself *and* the list items can both take various attributes (all tags can...) + +Section Headers are created with "h" tags:

    is the biggest (highest level), and there is

    ,

    , etc. for sections, sub sections, subsub sections... + +.. code-block:: html + +

    PythonClass -- Example

    + +I think that's all you need to know! diff --git a/_sources/exercises/html_renderer/html_renderer.rst.txt b/_sources/exercises/html_renderer/html_renderer.rst.txt new file mode 100644 index 0000000..cdf688b --- /dev/null +++ b/_sources/exercises/html_renderer/html_renderer.rst.txt @@ -0,0 +1,790 @@ +.. _exercise_html_renderer: + +###################### +HTML Renderer Exercise +###################### + +Ever need to write some HTML? Maybe from data? + +And not want to write all those tags yourself? + +OK, maybe not -- but trust me, it's a pain -- let's write some code to do it for us. + +HTML Renderer +============= + +Goal: +----- + +The goal is to create a set of classes to render html pages -- in a "pretty printed" way. + +i.e. nicely indented and human readable. + +We'll try to get to all the features required to render this file: + +:download:`sample_html.html <./sample_html.html>` + +Take a look at it by opening it in your text editor. And also in a browser to see how it's rendered. + +If you don't know html -- just look at the example and copy that. And you can read the: :ref:`html_primer` at the end of this page for enough to do this exercise. And anyone working with computers these days would benefit from at least a passing familiarity with html. + +The exercise is broken down into a number of steps -- each requiring a few more OO concepts in Python. + +The goal of the code is to render html. The goal of the *exercise* is to build up a simple object hierarchy with: + +* classes +* class attributes +* instance attributes +* methods +* subclassing +* overriding attributes and methods + + +General Instructions: +--------------------- + +You can start with the framework in: + +:download:`html_render.py <./html_render.py>` + +For each step, add the required functionality. There is example code to run your code for each step in: + +:download:`run_html_render.py <./run_html_render.py>` + +You should be able to run that code at each step, uncommenting each new step in ``run_html_render.py`` as you go. + +It builds up an html tree, and then calls the ``render()`` method of your element to render the page. + +It uses a ``StringIO`` object (like a file, but in memory) to render to memory, then dumps it to the console, and writes a file. Take a look at the render_page function at the top of the file to make sure you understand it. + +The html generated at each step will be written to the files named: +``test_html_ouput?.html`` + +Unit tests +---------- + +Running the ``run_html_render.py`` script is a (simple) form of integration testing -- it checks how the individual components are working together. But we also want to make sure each individual *unit* (class, method) of code works. So to do that, we'll use: + +**Test Driven Development** + +In addition to checking if the output is what you expect with the running script -- you should also write unit tests as you go. + +Each new line of code should have a test that will run it -- *before* you write that code. + +That is: + + 1. write a test that exercises the next step in your process + 2. run the tests -- the new test will fail + 3. write your code... + 4. run the tests. If it still fails, go back to step 3... + +A start of a test file is provided here: + +:download:`test_html_render.py <./test_html_render.py>` + +It has a few tests for the first few steps -- uncomment them as you go along. + +But it is NOT comprehensive -- you will need to add more tests at every step! + +You can run ``pytest`` on that test file first thing -- it should pass two tests (that you can create an Element object -- not that it works) and fail one -- one that actually starts to test functionality. + +**NOTE** if you are lost, take a look at the tutorial here: +:ref:`html_renderer_tutorial`. But do try to do it yourself first. + +Step 0: +------- + +Before you can start writing code, you need to get setup. + +* In your directory in the class repo called ``lesson07`` +* In that directory, you can start working on the code. Start by putting the files you just downloaded in that dir: + + - ``html_render.py``, ``run_html_render.py``, + ``sample_html.html``, ``test_html_render.py`` + +* Add those files to your git repo: + + - ``git add *.py sample_html.html`` + + +Step 1: +------- + +.. rubric:: 1a. + +Create an ``Element`` class for rendering an html element (xml element). + +There is a skeleton for one in ``html_render.py`` -- it has just enough so that the first few tests in ``test_html_render.py`` can run -- though that won't pass! + +Do run those tests first -- then add the code to make them pass. + +The ``Element`` class should have a class attribute for the tag name ("html" first). + +The initializer signature should look like: + +.. code-block:: python + + Element(content=None) + +Where ``content`` is expected to be a string -- and defaults to nothing. + +.. rubric:: 1b. + +The class should have an ``append`` method that can add another string to the content. + +(The ``html_render.py`` file you downloaded above should have a skeleton for this class in it.) + +So your class will need a way to store the content in a way that you can keep adding more to it. + +An ``Element`` object has to collect a bunch of sub-elements, in order, and you need to be able to append new ones to it -- sounds like a ``list``, doesn't it? So should it subclass from ``list``? + +Ask yourself -- does this make sense? an "Element *is* a list" -- no. + +But "An Element *uses* a list" makes perfect sense. + +If the *is* phrase makes sense, then subclassing would makes sense. If the *uses* phrase makes sense, *then* you would not want to subclass. + +So no -- you don't want ``Element`` to subclass from list. + +.. rubric:: 1c. + +It should have a ``render(file_out)`` method that renders the tag and the strings in the content. + +``file_out`` could be any open, writable file-like object ( i.e. have a ``write()`` method ). This is what you get from the ``open()`` function -- but there are other kinds of file-like objects. The html will be rendered to this file-like object. + +**NOTE:** html is not sensitive to newlines -- but you don't want all your html on one line. so put a newline in after each tag and each content string. Later on in the assignment, you'll add indentation as well! + +So this ``render()`` method takes a file-like object, and calls its ``write()`` method, writing the html for a tag. + +Once this works, this code: + +.. code-block:: python + + page = Element("Some content") + page.append("Some more contenet") + with open("test.html", 'w') as outfile: + page.render(outfile) + +Will result in a file with something like this in it: + +.. code-block:: html + + + Some content. + Some more content. + + +That is, you should now be able to render an html tag with text in it as content. + +See: step 1. in ``run_html_render.py`` and the test code. + +If you are stuck -- see the tutorial: :ref:`render_tutorial_1` + +Step 2: +------- + +Part A: +....... + +Create a couple subclasses of ``Element``, for each of ````, ````, and ``

    `` tags. All you should have to do is override the ``tag`` class attribute (you may need to add a ``tag`` class attribute to the ``Element`` class first, if you haven't already). + +Now you can render a few different types of element. For example: + +.. code-block:: python + + page = Body("Some content") + page.append("Some more contenet") + with open("test.html", 'w') as outfile: + page.render(outfile) + +Will result in a file with something like this in it: + +.. code-block:: html + + + Some content. + Some more content. + + +Note: So why are we subclassing here? Because: "a body element *is* an ``Element``" makes perfect sense -- that's when you want to subclass. Another way to think about it is that you want to subclass to make a specialized version of something. + +You may note that the ``Element`` class really doesn't do anything by itself -- it needs a tag (at least) to be a proper element. This is what's called a "Base Class". It contains functionality required by various subclasses, but may not do anything on its own. In this case, we gave it the tag 'html', so we could run and test the render method. But strictly speaking, as a base class, it could have no tag. + +And of course these subclasses are pretty simple -- only overriding one class attribute. If that's all you need to do to specialize, there are other ways than subclassing to do it. But bear with us -- other element subclasses will require more specialization. + +If you are stuck -- see the tutorial: :ref:`render_tutorial_2_A` + +Part B: +....... + +Now it gets fun! + +Now that you have multiple types of elements, it's worth looking a bit at how html works. A given element can hold text, but it can *also* hold other elements. So we need to update our ``Element`` classes to support that. + +Extend the ``Element.render()`` method so that it can render other elements inside the tag in addition to strings. A recursion-like approach should do it. i.e. it can call the ``render()`` method of the elements it contains. + +You should be able to ``append`` an element to another element -- not just text. + +If this recursion-like idea doesn't make sense to you, take a look at this blog post, which talks about recursive algorithms: + +https://realpython.com/python-thinking-recursively/ + +Figure out a way to deal with the fact that the contained elements could be either simple strings or ``Element`` s with render methods (there are a few ways to handle that...). Think about "Duck Typing" and EAFP. See the section :ref:`notes_on_handling_duck_typing` and the end of the Exercise for more. + +You should now be able to render a basic web page with an ```` tag around the whole thing, a ```` tag inside, and multiple ``

    `` tags inside that, with text inside that. + +So code like: + +.. code-block:: python + + page = Html() + body = Body() + body.append(P("a very small paragraph")) + body.append(P("another small paragraph")) + page.append(body) + with open("test.html", 'w') as outfile: + page.render(outfile) + +Should result in something like: + +.. code-block:: html + + + +

    + a very small paragraph +

    +

    + another small paragraph +

    + + + +See: :download:`test_html_output2.html <./test_html_output2.html>` + +NOTE: when you run step 2 in ``run_html_render.py``, you will want to comment out step 1 -- that way you'll only get one set of output. + +If you are stuck -- see the tutorial: :ref:`render_tutorial_2_B` + +Step 3: +------- + +Create a ```` element -- a simple subclass. + +Create a ``OneLineTag`` subclass of ``Element``: + +* It should override the render method, to render everything on one line -- for the simple tags, like:: + + PythonClass - Session 6 example + +Create a ``Title`` subclass of ``OneLineTag`` class for the title. + +You should now be able to render an html doc with a head element, with a +title element in that, and a body element with some ``

    `` elements and some text. + +See :download:`test_html_output3.html <./test_html_output3.html>` + +Step 4: +------- + +Extend the ``Element`` class to accept a set of attributes as keywords to the constructor, e.g. ``run_html_render.py`` + +.. code-block:: python + + Element("some text content", id="TheList", style="line-height:200%") + +html elements can take essentially any attributes -- so you can't hard-code these particular ones (remember ``**kwargs``? ) + +The render method will need to be extended to render the attributes properly. + +Note that you may now have *two* render methods -- the one in the ``Element`` base class, and the one in the ``OneLineTag`` class. They both need to be be able to handle attributes. But **DRY** -- so see if you can factor the code so the code that makes the opening tag, with the attributes is not repeated. + +You can now render some ``

    `` tags (and others) with attributes. + +See: :download:`test_html_output4.html <./test_html_output4.html>` + +NOTE: if you do "proper" CSS+html, then you wouldn't specify style directly in element attributes. + +Rather you would set the "class" attribute:: + +

    + This is my recipe for making curry purely with chocolate. +

    + +However, if you try this as a keyword argument in Python: + +.. code-block:: ipython + + In [1]: P("some content", class="intro") + File "", line 1 + P("some content", class="intro") + ^ + SyntaxError: invalid syntax + +Huh? + +"class" is a reserved work in Python -- for making classes. +So it can't be used as a keyword argument. + +But it's a fine key in a dict, so you can put it in a dict, and pass it in with ``**``: + +.. code-block:: python + + attrs = {'class': 'intro'} + P("some content", **attrs) + +You could also special-case this in your code -- so your users could use "clas" with one s, and you could tranlate it in the generated html. Or even both! + + +Step 5: +-------- + +Create a ``SelfClosingTag`` subclass of Element, to render tags like:: + +
    and
    (horizontal rule and line break). + +(See: https://www.w3schools.com/tags/tag_hr.asp) + +For example you should be able to use this code:: + + Hr(width=400) + +To get this result:: + +
    + +You will need to override the render method to render just the one tag and attributes, if any. + +Note that self closing tags can't have any content. Make sure that your SelfClosingTag element raises an exception if someone tries to put in any content -- probably a ``TypeError``. + +Create a couple subclasses of ``SelfClosingTag`` for ``
    `` and ``
    `` + +Note that you now have maybe three render methods -- is there repeated code in them? + +Can you refactor the common parts into a separate method that all the render methods can call? And do all your tests still pass (you do have tests for everything, don't you?) after refactoring? + +See: :download:`test_html_output5.html <./test_html_output5.html>` + + +Step 6: +------- + +Create an ``A`` class for an anchor (link) element. Its constructor should look like:: + + A(self, link, content) + +where ``link`` is the link, and ``content`` is what you see. It can be called like so:: + + A("http://google.com", "link to google") + +and it should render like:: + + link to google + + +You should be able to subclass from ``Element``, and only override the ``__init__`` --- calling the ``Element`` ``__init__`` from the ``A`` ``__init__`` + +You can now add a link to your web page. + +See: :download:`test_html_output6.html <./test_html_output6.html>` + +Step 7: +-------- + +Create ``Ul`` class for an unordered list (really simple subclass of ``Element``). + +Create ``Li`` class for an element in a list (also really simple). + +Add a list to your web page. + +Create a ``Header`` class -- this one should take an integer argument for the header level. i.e

    ,

    ,

    , called like + +.. code-block:: python + + H(2, "The text of the header") + +for an

    header. + +It can subclass from ``OneLineTag`` -- overriding the ``__init__``, then calling the superclass ``__init__`` + +See: :download:`test_html_output7.html <./test_html_output7.html>` + +Step 8: +------- + +Update the ``Html`` element class to render the "" tag at the head of the page, before the html element. + +You can do this by subclassing ``Element``, overriding ``render()``, but then calling the ``Element`` render from the new render. + +Create a subclass of ``SelfClosingTag`` for ```` (like for ``
    `` and ``
    `` and add the meta element to the beginning of the head element to give your document an encoding. + +The doctype and encoding are HTML 5 and you can check this at: + +http://validator.w3.org/#validate_by_input + +You now have a pretty full-featured html renderer -- play with it, add some new tags, etc.... + +See :download:`test_html_output8.html <./test_html_output8.html>` + + +Step 9: Adding Indentation +-------------------------- + +Indentation is not strictly required for html -- html ignores most whitespace. + +But it can make it much easier to read for humans, and it's a nice exercise to see how one might make it work in arbitrarily nested html. + +There is also more than one way to indent html -- so you have a bit of flexibility here. + +You will need to enhance your code in a couple ways to add indentation. + +A. Specify the indentation level +................................ + +Add a class attribute to the ``Element`` base class that indicates how much indentation you want -- you can either use a simple string: 2 or four spaces: + +.. code-block:: python + + class Element: + indent = " " + +Or you can use an integer to specify how many spaces you want to use: + +.. code-block:: python + + class Element: + indent = 4 + +Your render method(s) can access this attribute to know how much to indent a element. You want it as a class attribute in the base class, so that all the instances of all the subclasses will share the same value -- to indent all the html consistently. + +Then you need to pass this indentation down the tree as you render the page. + +B. Pass the "current level" of indentation down the tree of elements +.................................................................... + +html elements can be nested arbitrarily deep: + +.. code-block:: html + + + + + PythonClass = Revision 1087: + + +

    + Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

    +
      +
    • + The first item in a list +
    • +
    • + This is the second item +
    • +
    + + + +So how does a given element know where it is in the tree? And therefore how deep to indent itself? + +One way: extend your ``render`` method(s) to take another parameter: + +.. code-block:: python + + def render(out_file, cur_ind=""): + + +``cur_ind`` is a string (or number) with the current level of indentation in it: the amount that the entire tag should be indented for pretty printing. + +This is a little tricky: ``cur_ind`` will be the amount that this element should be indented already. It will be from zero (an empty string) to a lot of spaces, depending on how deep it is in the tree. You could use an integer for the number of spaces to indent -- or keep it simple and just use a string with the correct number of spaces in it. + +The amount of each level of indentation should be set by the class attribute: ``indent`` + +So: + +* You probably want ``cur_ind`` to be an optional argument to render -- so it will not indent if nothing is passed in. + +* But if it is passed in, you want your code to USE the ``cur_ind`` parameter -- it is supposed to indicate how much this entire tag is already indented. + +* When a given element gets rendered, you don't know where it is in a potentially deeply nested hierarchy -- it could be at the top level or ten levels deep. passing ``cur_ind`` into the render method is how this is communicated. + +* So when you call ``render`` from *inside* a render method -- you need to tell the nested elements how deep to render themselves -- usually one more level of indentation deep. Probably something like: + + + +``sub_element.render(out_file, cur_ind + self.indent)`` + + +* Remember to keep the amount of spaces per indentation defined as a class attribute of the base class (the ``Element`` class). That way, you could change it in one place, and it would change everywhere and remain consistent. + +* Be sure to test that the indentation of the result changes if you change the class attribute! + +You should have nice pretty indented html now! + +See :download:`test_html_output9.html <./test_html_output9.html>` + + +.. _notes_on_handling_duck_typing: + +Notes on handling "Duck Typing" +=============================== + +In this exercise, we need to deal with the fact that XML (and thus HTML) allows *either* plain text *or* other tags to be the content of a tag. So our code needs to handle the fact that there are two possible types that we need to be able to render. + +There are two primary ways to address this (and multiple ways to actually write the code for each of these). + +1) Make sure that the content only has renderable objects in it. + +2) Make sure the render() method can handle either type on the fly. + +The difference is where you handle the multiple types -- in the render method itself, or ahead of time, when you append new content to the ``Element``. + +The Ahead of Time Option: +------------------------- + +You can handle it ahead of time by creating a simple object that wraps a string and gives it a render method. As simple as: + +.. code-block:: python + + class TextWrapper: + """ + A simple wrapper that creates a class with a render method + for simple text + """ + def __init__(self, text): + self.text = text + + def render(self, file_out): + file_out.write(self.text) + + +You could require your users to use the wrapper, so instead of just appending a string, they would do: + +.. code-block:: python + + an_element.append(TextWrapper("the string they want to add")) + +But this is not very Pythonic style -- it's OO heavy. Strings for text are so common you want to be able to simply use them: + +.. code-block:: python + + an_element.append("the string they want to add") + +So much easier. + +To accomplish this, you can update the ``append()`` method to put this wrapper around plain strings when something new is added. + + +Checking if it's the Right Type +------------------------------- + +How do you decide if the wrapper is required? + +**Checking it it's an instance of Element:** + +You could check and see if the object being appended is an Element: + +.. code-block:: python + + if isinstance(content, Element): + self.content.append(content) + else: + self.content.append(TextWrapper(content)) + +This would work well, but closes the door to using any other type that may not be a strict subclass of Element, but can render itself. Not too bad in this case, but in general, frowned upon in Python. + + +Alternatively, you could check for the string type: + +.. code-block:: python + + if isinstance(content, str): + self.content.append(TextWrapper(content)) + else: + self.content.append(content) + +I think this is a little better -- strings are a pretty core type in Python, so it's not likely that anyone is going to need to use a "string-like" object. + +Duck Typing +----------- + +The Python model of duck typing is: If quacks like a duck, then treat it like a duck. + +But in this case, we're not actually rendering the object at this stage, so calling the method isn't appropriate. + +**Checking for an attribute** + +Instead of calling the method, see if it's there. You can do that with ``hasattr()`` + +Check if the passed-in object has a ``render`` attribute: + +.. code-block:: python + + if hasattr(content, 'render'): + self.content.append(content) + else: + self.content.append(TextWrapper(str(content)) + + +Note that I added a ``str()`` call too -- so you can pass in anything -- it will get stringified -- this will be ugly for many objects, but will work fine for numbers and other simple objects. + +This is my favorite. + + +Duck Typing on the Fly +---------------------- + +The other option is to simply put both elements and text in the content list, and figure out what to do in the ``render()`` method. + +Again, you could type check -- but I prefer the duck typing approach, and EAFP: + +.. code-block:: python + + try: + content.render(out_file) + except AttributeError: + outfile.write(content) + +If content is a simple string then it won't have a render method, and an ``AttributeError`` will be raised. + +You can catch that, and simply write the content directly instead. + + +You may want to turn it into a string, first:: + + outfile.write(str(content)) + +Then you could write just about anything -- numbers, etc. + + +Where did the Exception come from? +---------------------------------- + +**Caution** + +If the object doesn't have a ``render`` method, then an AttributeError will be raised. But what if it does have a render method, but that method is broken? + +Depending on what's broken, it could raise any number of exceptions. Most will not get caught by the except clause, and will halt the program. + +But if, just by bad luck, it has an bug that raises an ``AttributeError`` -- then this could catch it, and try to simply write it out instead. So you may get something like: ```` in the middle of your html. + +**The beauty of testing** + +If you have a unit test that calls every render method in your code -- then it should catch that error, and in the unit test it will be clear where it is coming from. + + +.. _html_primer: + +HTML Primer +============ + + +The very least you need to know about html to do this assignment. + + +If you are familiar with html, then this will all make sense to you. If you have never seen html before, this might be a bit intimidating, but you really don't need to know much to do this assignment. + +First of all, sample output from each step is provided. So all you really need to do is look at that, and make your code do the same thing. But it does help understand a little bit about what you trying to do. + +HTML +---- + +HTML is "Hyper Text Markup Language". Hypertext, because it can contain links +to other pages, and markup language means that text is "marked up" with +instructions about how to format the text, etc. + +Here is a good basic intro: + +http://www.w3schools.com/html/html_basic.asp + +And there are countless others online. + +As html is XML -- the XML intro is a good source of the XML syntax, too: + +http://www.w3schools.com/xml/default.asp + +But here is a tiny summary of just what you need to know for this project. + +Elements +-------- + +Modern HTML is a particular dialect of XML (eXtensible Markup Language), +which is itself a special case of SGML (Standard Generalized Markup Language) + +It inherits from SGML a basic structure: each piece of the document is an element. Each element is described by a "tag". Each tag has a different meaning, but they all have the same structure:: + + some content + +That is, the tag name is surrounded by < and >, which marks the beginning of +the element, and the end of the element is indicated by the same tag with a slash. + +The real power is that these elements can be nested arbitrarily deep. In order to keep that all readable, we often want to indent the content inside the tags, so it's clear what belongs with what. That is one of the tricky bits of this assignment. + + +Basic tags +---------- + +.. code-block:: html + + is the core tag indicating the entire document + +

    is a single paragraph of text

    + + is the tag that indicated the text of the document + + defines the header of the document -- a place for metadata + +Attributes: +------------ + +In addition to the tag name and the content, extra attributes can be attached to a tag. These are added to the "opening tag", with name="something", another_name="something else" format: + +.. code-block:: html + +

    + +There can be all sorts of stuff stored in attributes -- some required for specific tags, some extra, like font sizes and colors. Note that since tags can essentially have any attributes, your code will need to support that -- doesn't it kind of look like a dict? And keyword arguments? + +Special Elements +---------------- + +The general structure is everything in between the opening and closing tag. But some elements don't really have content -- just attributes. So the slash goes at the end of the tag, after the attributes. We can call these self-closing tags: + +.. code-block:: html + + + +To make a link, you use an "anchor" tag: ````. It requires attributes to indicate what the link is: + +.. code-block:: html + + link + +The ``href`` attribute is the link (hyper reference). + +lists +----- + +To make a bulleted list, you use a

      tag (unordered list), and inside that, you put individual list items
    • : + +.. code-block:: html + +
        +
      • + The first item in a list +
      • +
      • + This is the second item +
      • +
      + +Note that the list itself *and* the list items can both take various attributes (all tags can...) + +Section Headers are created with "h" tags:

      is the biggest (highest level), and there is

      ,

      , etc. for sections, sub sections, subsub sections... + +.. code-block:: html + +

      PythonClass -- Example

      + +I think that's all you need to know! diff --git a/_sources/exercises/html_renderer/html_renderer_tutorial.rst.txt b/_sources/exercises/html_renderer/html_renderer_tutorial.rst.txt new file mode 100644 index 0000000..6539107 --- /dev/null +++ b/_sources/exercises/html_renderer/html_renderer_tutorial.rst.txt @@ -0,0 +1,1620 @@ +.. _html_renderer_tutorial: + +####################################### +Tutorial for the Html Render Assignment +####################################### + +If you are finding that you don't really know where to start with the html render assignment, this tutorial will walk you through the process. + +However, you generally learn more if you figure things out for yourself. So I highly suggest that you give each step a try on your own first, before reading that step in this tutorial. Then, if you are really stuck -- follow the process here. + +.. _render_tutorial_1: + +Step 1: +------- + +Step one is a biggie -- that's 'cause you need a fair bit all working before you can actually have anything to test, really. But let's take it bit by bit. + +First, we are doing test driven development, and we already have a test or two. So let's run those, and see what we get. + +You should now be in a terminal with the current working directory set to where you downloaded ``html_render.py`` and ``test_html_render.py``. If you run pytest, it will find the ``test_html_render.py`` file and run the tests in it: + +.. code-block:: bash + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 3 items + + test_html_render.py ..F [100%] + + =================================== FAILURES =================================== + _____________________________ test_render_element ______________________________ + + def test_render_element(): + """ + Tests whether the Element can render two pieces of text + So it is also testing that the append method works correctly. + + It is not testing whether indentation or line feeds are correct. + """ + e = Element("this is some text") + e.append("and this is some more text") + + # This uses the render_results utility above + file_contents = render_result(e).strip() + + # making sure the content got in there. + > assert("this is some text") in file_contents + E AssertionError: assert 'this is some text' in 'just something as a place holder...' + + test_html_render.py:72: AssertionError + ====================== 1 failed, 2 passed in 0.06 seconds ====================== + +Hey! that's pretty cool -- two tests are already passing! Let's take a quick look at those: + +.. code-block:: python + + def test_init(): + """ + This only tests that it can be initialized with and without + some content -- but it's a start + """ + e = Element() + + e = Element("this is some text") + +So that one simply tested that an ``Element`` class exists, and that you can pass in a string when you initialize it -- not a bad start, but it doesn't show that you can *do* anything with it. + + +.. code-block:: python + + def test_append(): + """ + This tests that you can append text + + It doesn't test if it works -- + that will be covered by the render test later + """ + e = Element("this is some text") + e.append("some more text") + +And this one shows that you can call the ``append()`` method with some text -- nice, but again, it doesn't test if that appended text was used correctly. But it does show you got the API right. + +But this one failed: + +.. code-block:: python + + def test_render_element(): + """ + Tests whether the Element can render two pieces of text + So it is also testing that the append method works correctly. + + It is not testing whether indentation or line feeds are correct. + """ + e = Element("this is some text") + e.append("and this is some more text") + + # This uses the render_results utility above + file_contents = render_result(e).strip() + + # making sure the content got in there. + assert("this is some text") in file_contents + assert("and this is some more text") in file_contents + + # make sure it's in the right order + assert file_contents.index("this is") < file_contents.index("and this") + + # making sure the opening and closing tags are right. + assert file_contents.startswith("") + assert file_contents.endswith("") + +OK -- this one really does something real -- it tries to render an html element -- which did NOT pass -- so it's time to put some real functionality in the ``Element`` class. + +This is the code: + +.. code-block:: python + + class Element: + + def __init__(self, content=None): + pass + + def append(self, new_content): + pass + + def render(self, out_file): + out_file.write("just something as a place holder...") + +Looking there, we can see why the tests did what they did -- we have the three key methods, but they don't actually do anything. But the ``render`` method is the only one that actually provides some results to test. + +So back to the assignment: + + The ``Element`` class should have a class attribute for the tag name ("html" first). + +Each html element has a different "tag", specifying what kind of element it is. so our class needs one of those. Why a class attribute? Because each *instance* of each type (or class) of element will share the same tag. And we don't want to store the tag in the render method, because then we couldn't reuse that render method for a different type of element. + +So we need to add a tiny bit of code: + +.. code-block:: python + + class Element: + + tag = "html" + + def __init__(self, content=None): + pass + +That's not much -- will the test pass now? Probably not, because we aren't doing anything with the tag. But you can run it to see if you'd like. It's always good to run tests frequently to make sure you haven't inadvertently broken anything. + +Back to the task at hand: + + The class should have an ``append`` method that can add another string to the content. + + ... + + So your class will need a way to store the content in a way that you can keep adding more to it. + +OK, so we need a way to store the content: both what gets passed in to the ``__init__`` and what gets added with the ``append`` method. We need a data structure that can hold an ordered list of things, and can be added to in the future -- sounds like a list to me. So let's create a list in __init__ and store it in ``self`` for use by the other methods: + +.. code-block:: python + + def __init__(self, content=None): + self.contents = [content] + + def append(self, new_content): + self.contents.append(new_content) + +OK -- let's run the tests and see if anything changed:: + + > assert("this is some text") in file_contents + E AssertionError: assert 'this is some text' in 'just something as a place holder...' + + test_html_render.py:72: AssertionError + +Nope -- still failed at the first assert in test_render. This makes sense because we haven't done anything with the render method yet! + +.. rubric:: 1c. + +From the assignment: + + It should have a ``render(file_out)`` method that renders the tag and the strings in the content. + +We have the render method, but it's rendering arbitrary text to the file, not an html tag or contents. So let's add that. + +First let's add the contents, adding a newline in between to keep it readable. Remember that there can be multiple pieces of content, so we need to loop though the list: + +.. code-block:: python + + def render(self, out_file): + # loop through the list of contents: + for content in self.contents: + out_file.write(content) + out_file.write("\n") + +And run the tests:: + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 3 items + + test_html_render.py ..F [100%] + + =================================== FAILURES =================================== + _____________________________ test_render_element ______________________________ + + def test_render_element(): + """ + Tests whether the Element can render two pieces of text + So it is also testing that the append method works correctly. + + It is not testing whether indentation or line feeds are correct. + """ + e = Element("this is some text") + e.append("and this is some more text") + + # This uses the render_results utility above + file_contents = render_result(e).strip() + + # making sure the content got in there. + assert("this is some text") in file_contents + assert("and this is some more text") in file_contents + + # make sure it's in the right order + assert file_contents.index("this is") < file_contents.index("and this") + + # making sure the opening and closing tags are right. + > assert file_contents.startswith("") + E AssertionError: assert False + E + where False = ('') + E + where = 'this is some text\nand this is some more text'.startswith + + test_html_render.py:79: AssertionError + ====================== 1 failed, 2 passed in 0.05 seconds ====================== + +Failed in test_render again. But look carefully. It didn't fail on the first assert! It failed on this line:: + + assert file_contents.startswith("") + +This makes sense because we haven't rendered anything like that yet. So let's add that now. Recall that we want the results to look something like this: + +.. code-block:: html + + + Some content. + Some more content. + + +In this case, the "html" part is stored in a class attribute. So how would you make that tag? Looks like a good place for string formatting:: + + "<{}>".format(self.tag) + +and:: + + "".format(self.tag) + +So the method looks something like this: + +.. code-block:: python + + def render(self, out_file): + out_file.write("<{}>\n".format(self.tag)) + # loop through the list of contents: + for content in self.contents: + out_file.write(content) + out_file.write("\n") + out_file.write("\n".format(self.tag)) + +Now run the tests again:: + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 3 items + + test_html_render.py ... [100%] + + =========================== 3 passed in 0.02 seconds =========================== + +Whoo Hoo! All tests pass! But wait, there's more. Comprehensive testing is difficult. We tested that you could initialize the element with one piece of content, and then add another, and we checked that the opening and closing tag are there correctly. But is it actually rendering correctly? We may not have tested for everything. So we should take a look at the results, and see how it's doing. My trick for this is to print what I want to see in the test:: + + print(file_contents) + +and add a forced test failure at the end of the test, so we'll see that print:: + + assert False + +And let's run it:: + + =================================== FAILURES =================================== + _____________________________ test_render_element ______________________________ + + def test_render_element(): + """ + Tests whether the Element can render two pieces of text + So it is also testing that the append method works correctly. + + It is not testing whether indentation or line feeds are correct. + """ + e = Element("this is some text") + e.append("and this is some more text") + + # This uses the render_results utility above + file_contents = render_result(e).strip() + print(file_contents) + # making sure the content got in there. + assert("this is some text") in file_contents + assert("and this is some more text") in file_contents + + # make sure it's in the right order + assert file_contents.index("this is") < file_contents.index("and this") + + # making sure the opening and closing tags are right. + assert file_contents.startswith("") + assert file_contents.endswith("") + > assert False + E assert False + + test_html_render.py:82: AssertionError + ----------------------------- Captured stdout call ----------------------------- + + this is some text + + + and this is some more text + + +It failed on the ``assert False``. It's a good sign that it didn't fail before that. We can now look at the results we printed, and whoops! we actually got *two* html elements, rather than one with two pieces of content. Why is that? Before you look at the code again, let's make sure the test catches that and fails. How about this? + +.. code-block:: python + + assert file_contents.count("") == 1 + assert file_contents.count("") == 1 + +And it does indeed fail on this line:: + + > assert file_contents.count("") == 1 + E AssertionError: assert 2 == 1 + E + where 2 = ('') + E + where = '\nthis is some text\n\n\nand this is some more text\n'.count + + test_html_render.py:83: AssertionError + +Now that we know we can test for the issue, we can try to fix it, and we'll know it's fixed when the tests pass. + +So looking at the code -- why did I get two ```` tags? + +.. code-block:: python + + def render(self, out_file): + # loop through the list of contents: + for content in self.contents: + out_file.write("<{}>\n".format(self.tag)) + out_file.write(content) + out_file.write("\n") + out_file.write("\n".format(self.tag)) + +Hmm -- when are those tags getting rendered? *Inside* the loops through the contents! Oops! We want to write the tag *before* the loop, and the closing tag *after* loop. (Did you notice that the first time? I hope so.) So a little restructuring is in order. + +.. code-block:: python + + def render(self, out_file): + # loop through the list of contents: + out_file.write("<{}>\n".format(self.tag)) + for content in self.contents: + out_file.write(content) + out_file.write("\n") + out_file.write("\n".format(self.tag)) + +That's it -- let's see if the tests pass now:: + + + > assert False + E assert False + + test_html_render.py:86: AssertionError + ----------------------------- Captured stdout call ----------------------------- + + this is some text + and this is some more text + + +Mine failed on the ``assert False``. So the actual test passed -- good. And the rendered html tag looks right, too. So we can go ahead and remove that ``assert False``, and move on! + +We have tested to see that we could initialize with one piece of content, and then add another, but what if you initialized it with nothing, and then added some content? Try uncommenting the next test: ``test_render_element2`` and see what you get. + +This is what I got with my code:: + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 4 items + + test_html_render.py ...F [100%] + + =================================== FAILURES =================================== + _____________________________ test_render_element2 _____________________________ + + def test_render_element2(): + """ + Tests whether the Element can render two pieces of text + So it is also testing that the append method works correctly. + + It is not testing whether indentation or line feeds are correct. + """ + e = Element() + e.append("this is some text") + e.append("and this is some more text") + + # This uses the render_results utility above + > file_contents = render_result(e).strip() + + test_html_render.py:95: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + test_html_render.py:30: in render_result + element.render(outfile) + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + + self = + out_file = <_io.StringIO object at 0x10c4881f8> + + def render(self, out_file): + # loop through the list of contents: + for content in self.contents: + out_file.write("<{}>\n".format(self.tag)) + > out_file.write(content) + E TypeError: string argument expected, got 'NoneType' + + html_render.py:23: TypeError + ====================== 1 failed, 3 passed in 0.08 seconds ====================== + +Darn! Something is wrong here. And this time it errored out before it even got results to test. So look and see exactly what the error is. (pytest does a really nice job of showing you the errors):: + + out_file.write("<{}>\n".format(self.tag)) + > out_file.write(content) + E TypeError: string argument expected, got 'NoneType' + +It failed when we tried to write to the file. We're trying to write a piece of content, and we got a ``NoneType``. How in the world did a ``NoneType`` (which is the type of None) get in there? + +Where does the ``self.contents`` list get created? In the ``__init__``. Let's do a little print debugging here. Add a print to the ``__init__``: + +.. code-block:: python + + def __init__(self, content=None): + self.contents = [content] + print("contents is:", self.contents) + +And run the tests again:: + + > out_file.write(content) + E TypeError: string argument expected, got 'NoneType' + + html_render.py:24: TypeError + ----------------------------- Captured stdout call ----------------------------- + contents is: [None] + ====================== 1 failed, 3 passed in 0.06 seconds ====================== + + +Same failure -- but pytest does a nice job of showing you what was printed (stdout) when a test fails. So in this case, at the end of the ``__init__`` method, the contents list looks like ``[None]`` -- a list with a single None object in it. No wonder it failed later when we tried to write that ``None`` to a file! + +But why? Well, looking at the ``__init__``, it looks like content gets set to None by default: + +.. code-block:: python + + def __init__(self, content=None): + +Then we put that ``None`` in the ``self.contents`` list. What do we want when content is ``None``? +An empty list, so that we can add to it later, not a list with a ``None`` object in it. So you need some code that checks for ``None`` (hint: use ``is None`` or ``is not None`` to check for ``None``), and only adds content to the list if it is *not* ``None``. + +I'll leave it as an exercise for the reader to figure out how to do that, but make sure all tests are passing before you move on! And once the tests pass, you may want to remove that ``print()`` line. + +.. _render_tutorial_2_A: + +Step 2: +------- + +OK, we have nice little class here; it has a class attribute to store information about the tag, information that's the same for all instances. + +And we are storing a list of contents in "self" -- information that each instance needs its own copy of. + +And we are using that data to render an element. + +So we're ready to move on. + +Part A +...... + +.. rubric:: Instructions: + + +"Create a couple subclasses of ``Element``, for each of ````, ````, and ``

      `` tags. +All you should have to do is override the ``tag`` class attribute (you may need to add a ``tag`` class attribute to the ``Element`` class first, if you haven't already)." + +So this is very straightforward. We have a class that represents an element, and the only difference between basic elements is that they have a different tag. For example:: + + + Some content. + Some more content. + + +and:: + +

      + Some content. + Some more content. +

      + + +The ```` tag is for the entire contents of an html page, and the ``

      `` tag is for a paragraph. +But you can see that the form of the tags is identical, so we don't have to change much to make classes for these tags. +In fact, all we need to change is the ``tag`` class attribute. + +Before we do that -- let's do some test-driven development. Uncomment the next few tests in ``test_html_render.py``: ``test_html``, ``test_body``, and ``test_p``, and run the tests:: + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 7 items + + test_html_render.py ....FFF [100%] + + =================================== FAILURES =================================== + __________________________________ test_html ___________________________________ + + def test_html(): + > e = Html("this is some text") + E NameError: name 'Html' is not defined + + test_html_render.py:117: NameError + __________________________________ test_body ___________________________________ + + def test_body(): + > e = Body("this is some text") + E NameError: name 'Body' is not defined + + test_html_render.py:129: NameError + ____________________________________ test_p ____________________________________ + + def test_p(): + > e = P("this is some text") + E NameError: name 'P' is not defined + + test_html_render.py:142: NameError + ====================== 3 failed, 4 passed in 0.08 seconds ====================== + +So we have three failures. Of course we do, because we haven't written any new code yet! +Yes, this is pedantic, and there is no real reason to run tests you know are going to fail. +But there is a reason to *write* tests that you know are going to fail, and you have to run them to know that you have written them correctly. + +Now we can write the code for those three new element types. Try to do that yourself first, before you read on. + +OK, did you do something as simple as this? + +.. code-block:: python + + class Body(Element): + tag = 'body' + +(and similarly for ``Html`` and ``P``) + +That's it! But what does that mean? This line: + +.. code-block:: python + + class Body(Element): + +means: make a new subclass of the ``Element`` tag called ``Body``. + +and this line: + +.. code-block:: python + + tag = 'body' + +means: set the ``tag`` class attribute to ``'body'``. Since this class attribute was set on the ``Element`` class already, this is called "overriding" the tag attribute. + +The end result is that we now have a class that is exactly the same as the ``Element`` class, except with a different tag. Where is that attribute used? It is used in the ``render()`` method. + +Let's run the tests and see if this worked:: + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 7 items + + test_html_render.py ....... [100%] + + =========================== 7 passed in 0.02 seconds =========================== + +Success! We now have three different tags. + +.. note:: + Why the ``Html`` element? Doesn't the ``Element`` class already use the "html" tag? + Indeed it does, but the goal of the ``Element`` class is to be a base class for the other tags, rather than being a particular element. + Sometimes this is called an "abstract base class": a class that can't do anything by itself, but exists only to provide an interface (and partial functionality) for subclasses. + But we wanted to be able to test that partial functionality, so we had to give it a tag to use in the initial tests. + If you want to be pure about it, you could use something like ``abstract_tag`` in the ``Element`` class to make it clear that it isn't supposed to be used alone. And later on in the assignment, we'll be adding extra functionality to the ``Html`` element. + +Making a subclass where the only thing you change is a single class attribute may seem a bit silly -- and indeed it is. +If that were going to be the *only* difference between all elements, There would be other ways to accomplish that task that would make more sense -- perhaps passing the tag in to the initializer, for instance. +But have patience, as we proceed with the exercise, some element types will have more customization. + +Another thing to keep in mind. The fact that writing a new subclass is ALL we need to do to get a new type of element demonstrates the power of subclassing. With the tiny change of adding a subclass with a single class attribute, we get a new element that we can add content to, and render to a file, etc., with virtually no repeated code. + +.. _render_tutorial_2_B: + +Part B: +....... + +Now it gets more interesting, and challenging! + +The goal is to be able to render nested elements, like so: + +.. code-block:: html + + + +

      + a very small paragraph +

      +

      + Another small paragraph. + This one with multiple lines. +

      + + + +This means that we need to be able to append not just text to an element, but also other elements. The appending is easy -- the tricky bit is when you want to render those enclosed elements. + +Let's take this bit by bit -- first with a test or two. +Uncomment ``test_subelement`` in the test file, and run the tests:: + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 8 items + + test_html_render.py .......F [100%] + + =================================== FAILURES =================================== + _______________________________ test_sub_element _______________________________ + + def test_sub_element(): + """ + tests that you can add another element and still render properly + """ + page = Html() + page.append("some plain text.") + page.append(P("A simple paragraph of text")) + page.append("Some more plain text.") + + > file_contents = render_result(page) + + test_html_render.py:163: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + test_html_render.py:30: in render_result + element.render(outfile) + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + + self = + out_file = <_io.StringIO object at 0x10325b5e8> + + def render(self, out_file): + out_file.write("<{}>\n".format(self.tag)) + # loop through the list of contents: + for content in self.contents: + > out_file.write(content) + E TypeError: string argument expected, got 'P' + + html_render.py:26: TypeError + ====================== 1 failed, 7 passed in 0.11 seconds ====================== + +Again, the new test failed; no surprise because we haven't written any new code yet. +But do read the error report carefully; it did not fail on an assert, but rather with a ``TypeError``. The code itself raised an exception before it could produce results to test. + +So now it's time to write the code. Look at where the exception was raised: line 26 in my code, inside the ``render()`` method. The line number will likely be different in your code, but it probably failed on the render method. Looking closer at the error:: + + > out_file.write(content) + E TypeError: string argument expected, got 'P' + +It occurred in the file ``write`` method, complaining that it expected to be writing a string to the file, but it got a ``'P'``. +``'P'`` is the name of the paragraph element class. +So we need a way to write an element to a file. How might we do that? + +Inside the element's render method, we need to render an element... + +Well, elements already know how to render themselves. This is what is meant by a recursive approach. In the ``render`` method, we want to make use of the ``render`` method itself. + +Looking at the signature of the render method: + +.. code-block:: python + + def render(self, out_file): + +it becomes clear -- we render an element by passing the output file to the element's render method. Here is what mine looks like now: + +.. code-block:: python + + def render(self, out_file): + out_file.write("<{}>\n".format(self.tag)) + # loop through the list of contents: + for content in self.contents: + out_file.write(content) + out_file.write("\n") + out_file.write("\n".format(self.tag)) + +So let's update our render by replacing that ``out_file.write()`` call with a call to the content's ``render`` method: + +.. code-block:: python + + def render(self, out_file): + out_file.write("<{}>\n".format(self.tag)) + # loop through the list of contents: + for content in self.contents: + # out_file.write(content) + content.render(out_file) + out_file.write("\n") + out_file.write("\n".format(self.tag)) + +And let's see what happens when we run the tests:: + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 8 items + + test_html_render.py ..FFFFFF [100%] + + =================================== FAILURES =================================== + + ... lots of failures here + + _______________________________ test_sub_element _______________________________ + + def test_sub_element(): + """ + tests that you can add another element and still render properly + """ + page = Html() + page.append("some plain text.") + page.append(P("A simple paragraph of text")) + page.append("Some more plain text.") + + > file_contents = render_result(page) + + test_html_render.py:163: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + test_html_render.py:30: in render_result + element.render(outfile) + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + + self = + out_file = <_io.StringIO object at 0x10b123828> + + def render(self, out_file): + # loop through the list of contents: + for content in self.contents: + out_file.write("<{}>\n".format(self.tag)) + # out_file.write(content) + > content.render(out_file) + E AttributeError: 'str' object has no attribute 'render' + + html_render.py:27: AttributeError + ====================== 6 failed, 2 passed in 0.12 seconds ====================== + +Whoaa! Six failures! We really broke something! But that is a *good* thing. It's the whole point of unit tests. When you are making a change to address one issue, you know right away that you broke previously working code. + +So let's see if we can fix these tests, while still allowing us to add the feature we intended to add. + +Again -- look carefully at the error, and the solution might pop out at you:: + + > content.render(out_file) + E AttributeError: 'str' object has no attribute 'render' + +Now we are trying to call a piece of content's ``render`` method, but we got a simple string, which does not *have* a ``render`` method. +This is the challenge of this part of the exercise. It's easy to render a string, and it's easy to render an element, but the content list could have either one. So how do we switch between the two methods? + +There are a number of approaches you can take. This is a good time to read the notes about this here: :ref:`notes_on_handling_duck_typing`. +You may want to try one of the more complex methods, but for now, we're going to use the one that suggests itself from the error. + +We need to know whether we want to call a ``render()`` method, or simply write the content to the file. How would we know which to do? Again, look at the error: + +We tried to call the render() method of a piece of content, but got an ``AttributeError``. So the way to know whether we can call a render method is to try to call it -- if it works, great! If not, we can catch the exception, and do something else. In this case, the something else is to try to write the content directly to the file: + +.. code-block:: python + + def render(self, out_file): + out_file.write("<{}>\n".format(self.tag)) + # loop through the list of contents: + for content in self.contents: + try: + content.render(out_file) + except AttributeError: + out_file.write(content) + out_file.write("\n") + out_file.write("\n".format(self.tag)) + +And run the tests again:: + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 8 items + + test_html_render.py ........ [100%] + + =========================== 8 passed in 0.03 seconds =========================== + +Yeah! all eight tests pass! I hope you found that at least a little bit satisfying. And pretty cool, really, that the solution requires only two extra lines of code. This is an application of the EAFP method: it's Easier to Ask Forgiveness than Permission. You simply try to do one thing, and if that raises the exception you expect, than do something else. + +It's also taking advantage of Python's "Duck Typing". Notice that we don't know if that piece of content is actually an ``Element`` object. All we know is that it has a ``render()`` method that we can pass a file-like object to. +This is quite deliberate. If some future user (that might be you) wants to write their own element type, they can write it any way they like, as long as it defines a ``render()`` method that can take a file-like object to write to. + +So what are the downsides to this method? Well, there are two: + +1. When we successfully call the ``render`` method, we have no idea if it's actually done the right thing -- it could do anything. If someone puts some completely unrelated object in the content list that happens to have a ``render`` method, this is not going to work. But what are the odds of that? + +2. This is the bigger one: if the object *HAS* a render method, but that render method has something wrong with it, then it could conceivably raise an ``AttributeError`` itself, but it would not be the ``AttributeError`` we are expecting. The trick here is that this can be very hard to debug. + +However, we are saved by tests. If the render method works in all the other tests, It's not going to raise an ``AttributeError`` only in this case. Another reason to have a good test suite. + + +.. _render_tutorial_3: + +Step 3: +------- + +Now we are getting a little more interesting. + +"Create a ```` element -- a simple subclass." + +This is easy; you know how to do that, yes? + +But the training wheels are off -- you are going to need to write your own tests now. So before you create the ``Head`` element class, write a test for it. You should be able to copy and paste one of the previous tests, and just change the name of the class and the tag value. Remember to give your test a new name, or it will simply replace the previous test. + +I like to run the tests as soon as I make a new one. If nothing else, I can make sure I have one more test. + +OK, that should have been straightforward. Now this part: + + Create a ``OneLineTag`` subclass of ``Element``: + + * It should override the render method, to render everything on one line -- for the simple tags, like:: + + PythonClass - Session 6 example + +Some html elements don't tend to have a lot of content, such as the document title. So it makes sense to render them all on one line. This is going to require a new render method. Since there are multiple types of elements that should be rendered on one line, we want to create a base class for all one-line elements. It should subclass from ``Element``, and override the render method with a new one, which will be pretty much the same as the main ``Element`` method, but without the newlines. + +Before we do that though -- let's write a test for that! Because the ``OneLineTag`` class is a base class for actual elements that should be rendered on one line, we really don't need to write a test directly for it. We can write one for its first subclass: ``Title``. The title elements should be rendered something like this:: + + PythonClass - title example + +Which should be generated by code like this:: + + Title("PythonClass - title example") + +Take a look at one of the other tests to get ideas, and maybe start with a copy and paste, and then change the names: + +.. code-block:: python + + def test_title(): + e = Title("this is some text") + e.append("and this is some more text") + + file_contents = render_result(e).strip() + + assert("this is some text") in file_contents + assert("and this is some more text") in file_contents + print(file_contents) + assert file_contents.startswith("") + assert file_contents.endswith("") + +That's not going to pass, as there is no ``Title`` class. But before we get that far -- what else do we need to change about this test? +For starters, this test is appending additional content. +That's not very likely for a title, is it? So let's get rid of that line. + +.. code-block:: python + + def test_title(): + e = Title("This is a Title") + + file_contents = render_result(e).strip() + + assert("This is a Title") in file_contents + print(file_contents) + assert file_contents.startswith("") + assert file_contents.endswith("") + +So that's a bit cleaner. But let's look at those asserts -- what are we testing for? Looks like we're testing for the correct start and end tags, and that the content is there. That's a pretty good start, but it isn't checking for newlines at all. In fact, all the previous tests would pass even if our render method did not have any newlines in it at all. Which is probably OK -- html does not require newlines. You could go back and update the tests to check for the proper newlines, though later on, when we get to indenting, we'll be doing that anyway. + +But for this element, we want to make sure that we don't have any newlines. So let's add an assert for that: + +.. code-block:: python + + assert "\n" not in file_contents + +You can run the tests now if you like -- it will fail due to there being no Title element. So let's make one now. Remember that we want to start with a ``OneLineTag`` element, and then subclass ``Title`` from that. + +.. code-block:: python + + class OneLineTag(Element): + pass + + + class Title(OneLineTag): + tag = "title" + +The ``pass`` means "do nothing." But it is required to satisfy Python. There needs to be *something* in the class definition. So in this case, we have a ``OneLineTag`` class that is exactly the same as the ``Element`` class, and a ``Title`` class that is the same except for the tag. Time to test again:: + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 10 items + + test_html_render.py .........F [100%] + + =================================== FAILURES =================================== + __________________________________ test_title __________________________________ + + def test_title(): + e = Title("This is a Title") + + file_contents = render_result(e).strip() + + assert("This is a Title") in file_contents + print(file_contents) + assert file_contents.startswith("") + assert file_contents.endswith("") + > assert "\n" not in file_contents + E AssertionError: assert '\n' not in '\nThis is a Title\' + E '\n' is contained here: + E + E ? ------- + E This is a Title + E + + test_html_render.py:203: AssertionError + ----------------------------- Captured stdout call ----------------------------- + + This is a Title + + ====================== 1 failed, 9 passed in 0.12 seconds ====================== + +The title test failed on this assertion:: + + > assert "\n" not in file_contents + +Which is what we expected because we haven't written a new render method yet. But look at the end of the output, where is says ``-- Captured stdout call --``. That shows you how the title element is being rendered -- with the newlines. That's there because there is a print in the test: + +.. code-block:: python + + print(file_contents) + +.. note:: + + pytest is pretty slick with this. It "Captures" the output from print calls, etc., and then only shows them to you if a test fails. + So you can sprinkle print calls into your tests, and it won't clutter the output -- you'll only see it when a test fails, which is when you need it. + +This is a good exercise to go through. If a new test fails, it lets you know that the test itself is working, testing what it is supposed to test. + +So how do we get this test to pass? We need a new render method for ``OneLineTag``. For now, you can copy the render method from ``Element`` to ``OneLineTag``, and remove the newlines: + +.. code-block:: python + + class OneLineTag(Element): + + def render(self, out_file): + out_file.write("<{}>".format(self.tag)) + # loop through the list of contents: + for content in self.contents: + try: + content.render(out_file) + except AttributeError: + out_file.write(content) + out_file.write("\n".format(self.tag)) + +Notice that I left the newline in at the end of the closing tag -- we do want a newline there, so the next element won't get rendered on the same line. And the tests:: + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 10 items + + test_html_render.py .......... [100%] + + ========================== 10 passed in 0.03 seconds =========================== + +We done good. But wait! there *is* a newline at the end, and yet the assert: ``assert "\n" not in file_contents`` passed! Why is that? + +Take a look at the code in the tests that renders the element: + +.. code-block:: python + + file_contents = render_result(e).strip() + +It's calling ``.strip()`` on the rendered string. That will remove all whitespace from both ends -- removing that last newline. + +However, there is still some extra code in that ``render()`` method. It's still looping through the contents and checking for an ``Element`` type. But for this, we hope that there will only be one piece of content, and it should not be an element. So we can make the render method simpler: + +.. code-block:: python + + class OneLineTag(Element): + def render(self, out_file): + out_file.write("<{}>".format(self.tag)) + out_file.write(self.contents[0]) + out_file.write("\n".format(self.tag)) + +If you are nervous about people appending content that will then be ignored, you can override the append method, too: + +.. code-block:: python + + def append(self, content): + raise NotImplementedError + +``NotImplementedError`` means just what it says -- this method is not implemented. My tests still pass, but how do I test to make sure that I can't append to a OneLineTag? Let's try that: + +.. code-block:: python + + def test_one_line_tag_append(): + """ + You should not be able to append content to a OneLineTag + """ + e = OneLineTag("the initial content") + e.append("some more content") + + file_contents = render_result(e).strip() + print(file_contents) + +And run the tests:: + + test_html_render.py:199: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + + self = + content = 'some more content' + + def append(self, content): + > raise NotImplementedError + E NotImplementedError + + html_render.py:57: NotImplementedError + ===================== 1 failed, 10 passed in 0.09 seconds ====================== + +Hmm. It raised a ``NotImplementedError``, which is what we want, but it is logging as a test failure. An exception raised in a test is going to cause a failure -- but what we want is for the test to pass only *if* that exception is raised. +Fortunately, pytest has a utility to do just that. Make sure there is an ``import pytest`` in your test file, and then add this code: + +.. code-block:: python + + def test_one_line_tag_append(): + """ + You should not be able to append content to a OneLineTag + """ + e = OneLineTag("the initial content") + with pytest.raises(NotImplementedError): + e.append("some more content") + +That ``with`` is a "context manager" (kind of like the file ``open()`` one). More on that later in the course, but what this means is that the test will pass if and only if the code inside that ``with`` block raised a ``NotImplementedError``. If it raises something else, or it doesn't raise an exception at all, then the test will fail. + +OK, I've got 11 tests passing now. How about you? Time for the next step. + +.. _render_tutorial_4: + +Step 4. +------- + +From the exercise instructions: + +"Extend the ``Element`` class to accept a set of attributes as keywords to the constructor, e.g. ``run_html_render.py``" + +If you don't know what attributes of an element are, read up a bit more on html on the web, and/or take another look at :ref:`html_primer`. But in short, attributes are a way to "customize" an element -- give it some extra information. The syntax looks like this:: + +

      + +Inside the opening tag, there is the tag name, then a space, then the attributes separated by spaces. Each attribute is a ``name="value"`` pair, with the name in plain text, and the value in quotes. + +Note that these name:value pairs look a lot like python keyword arguments, which lends itself to an initialization signature. For the above example, we would create the element like so: + +.. code-block:: python + + el = P("A paragraph of text", style="text-align: center", id="intro") + +Which should result in the following html:: + +

      + a paragraph of text +

      + + +Now that we know how to initialize an element with attributes, and how it should get rendered, we can write a test that will check if the attributes are rendered correctly. Something like: + +.. code-block:: python + + def test_attributes(): + e = P("A paragraph of text", style="text-align: center", id="intro") + + file_contents = render_result(e).strip() + print(file_contents) # so we can see it if the test fails + + # note: The previous tests should make sure that the tags are getting + # properly rendered, so we don't need to test that here. + # so using only a "P" tag is fine + assert "A paragraph of text" in file_contents + # but make sure the embedded element's tags get rendered! + # first test the end tag is there -- same as always: + assert file_contents.endswith("

      ") + + # but now the opening tag is far more complex + # but it starts the same: + assert file_contents.startswith(" e = P("A paragraph of text", style="text-align: center", id="intro") + E TypeError: __init__() got an unexpected keyword argument 'style' + + test_html_render.py:217: TypeError + ===================== 1 failed, 11 passed in 0.19 seconds ====================== + +Yes, the new test failed -- isn't TDD a bit hard on the ego? So many failures! But why? Well, we passed in the ``style`` and ``id`` attributes as keyword arguments, but the ``__init__`` doesn't expect those arguments. Hence the failure. + +So should be add those two as keyword parameters? Well, no we shouldn't because those are two arbitrary attribute names; we need to support virtually any attribute name. So how do you write a method that will accept ANY keyword argument? Time for our old friend ``**kwargs``. ``**kwargs`` will allow any keyword argument to be used, and will store them in the ``kwargs`` dict. So time to update the ``Element.__init__`` like so: + +.. code-block:: python + + def __init__(self, content=None, **kwargs): + +But then, make sure to *do* something with the ``kwargs`` dict -- you need to store those somewhere. Remember that they are a collection of attribute names and values -- and you will need them again when it's time to render the opening tag. How do you store something so that it can be used in another method? I'll leave that as an exercise for the reader. + +And let's try to run the tests again:: + + ========================== 12 passed in 0.07 seconds =========================== + +They passed! Great, but did we test whether the attributes get rendered in the tag correctly? No -- not yet, let's make sure to add that. It may be helpful to add an ``assert False`` in there, so we can see what our tag looks like while we work on it:: + + ... + assert False + E assert False + + test_html_render.py:243: AssertionError + ----------------------------- Captured stdout call ----------------------------- +

      + A paragraph of text +

      + ===================== 1 failed, 11 passed in 0.11 seconds ====================== + +OK, so we have a regular old

      element -- no attributes at all -- no surprise here. Let's first add a couple tests for the attributes: + +.. code-block:: python + + # order of the tags is not important in html, so we need to + # make sure not to test for that + # but each attribute should be there: + assert 'style="text-align: center"' in file_contents + assert 'id="intro"' in file_contents + +We know the tests will fail -- so let's go straight to the code. We need to update our ``render()`` method to put the attributes in the opening tag. Let's remind ourselves what this needs to look like:: + +

      + +So we need to render the ``<``, then the ``p``, then a bunch of attribute name=value pairs. Let's start with breaking up the rendering of the opening tag, and make sure the existing tests still pass. + +.. code-block:: python + + def render(self, out_file): + open_tag = ["<{}".format(self.tag)] + open_tag.append(">\n") + out_file.write("".join(open_tag)) + ... + +OK, the rest of the tests are still passing for me; I haven't broken anything else. Now to add code to render the attributes. You'll need to write some sort of loop to loop through each attribute, probably looping through the keys and the values: + +.. code-block:: python + + for key, value in self.attributes: + +Then you can render them in html form inside that loop. + +Once you've done that, run the tests again. When I do that, mine passes the asserts checking the attributes, but fails on the ``assert False``, so I can see how it's rendering:: + + > assert False + E assert False + + test_html_render.py:247: AssertionError + ----------------------------- Captured stdout call ----------------------------- + + A paragraph of text +

      + ===================== 1 failed, 11 passed in 0.11 seconds ====================== + +Hmmm -- the attributes are rendered correctly, but there is no space between the "p" (that tag name) and the first attribute. So let's update our tests to make sure that we check for that: + +.. code-block:: python + + assert file_contents.startswith("

      + A paragraph of text +

      + +However, my code for rendering the opening tag is a bit klunky -- how about yours? Perhaps you'd like to refactor it? Before you do that, you might want to make your tests a bit more robust. This is really tricky. It's very hard to test for everything that might go wrong, without nailing down the expected results too much. For example, in this case, we haven't tested that there is a space between the two attributes. In fact, this would pass our test:: + +

      + A paragraph of text +

      + +See how there is no space before "id"? But the order of the attributes is arbitrary, so we don't want to assume that the style will come before id. You could get really fancy with parsing the results, but I think we could get away with assuring there are the right number of spaces in the opening tag. + +.. code-block:: python + + assert file_contents[:file_contents.index(">")].count(" ") == 3 + +This now fails with my broken code, but passes when I fix it with that space between the attributes. What else might you want to check to make sure it's all good? + +Here's my final test for attribute rendering: + +.. code-block:: python + + def test_attributes(): + e = P("A paragraph of text", style="text-align: center", id="intro") + + file_contents = render_result(e).strip() + print(file_contents) # so we can see it if the test fails + + # note: The previous tests should make sure that the tags are getting + # properly rendered, so we don't need to test that here. + # so using only a "P" tag is fine + assert "A paragraph of text" in file_contents + # but make sure the embedded element's tags get rendered! + # first test the end tag is there -- same as always: + assert file_contents.endswith("

      ") + + # but now the opening tag is far more complex + # but it starts the same: + assert file_contents.startswith("

      ") > file_contents.index('id="intro"') + assert file_contents[:file_contents.index(">")].count(" ") == 3 + +With this test in place, you can safely refactor your attribute rendering if you like. I know I did. + +Step 5: +------- + +Create a ``SelfClosingTag`` subclass of Element, to render tags like:: + +


      + +and:: + +
      + +(horizontal rule and line break) + +Including with attributes:: + +
      + +So let's start with two tests: + +.. code-block:: python + + def test_hr(): + """a simple horizontal rule with no attributes""" + hr = Hr() + file_contents = render_result(hr) + print(file_contents) + assert file_contents == '
      \n' + + + def test_hr_attr(): + """a horizontal rule with an attribute""" + hr = Hr(width=400) + file_contents = render_result(hr) + print(file_contents) + assert file_contents == '
      \n' + +Which, of course, will fail initially. + +We'll want multiple self closing tags -- so we'll start with a base class, and then derive the Hr tag from that: + +.. code-block:: python + + class SelfClosingTag(Element): + pass + + + class Hr(SelfClosingTag): + tag = "hr" + +Test still fails -- but gets further. +You'll need to override the ``render()`` method: + +.. code-block:: python + + class SelfClosingTag(Element): + + def render(self, outfile): + # put rendering code here. + +What needs to be there? Well, self closing tags can have attributes, same as other elements. +So we need a lot of the same code here as with the other ``render()`` methods. You could copy and paste the ``Element.render()`` method, and edit it a bit. But that's a "Bad Idea" -- remember DRY (Don't Repeat Yourself)? +You really don't want two copies of that attribute rendering code you worked so hard on. +How do we avoid that? We take advantage of the power of subclassing. If you put the code to render the opening (and closing) tags in its own method, then we can call that method from multiple render methods, something like: + +.. code-block:: python + + def render(self, out_file): + # loop through the list of contents: + out_file.write(self._open_tag()) + out_file.write("\n") + for content in self.contents: + try: + content.render(out_file) + except AttributeError: + out_file.write(content) + out_file.write("\n") + out_file.write(self._close_tag()) + out_file.write("\n") + +So instead of making the tag in the render method itself, we call the ``_open_tag`` and ``_close_tag`` methods. Note that I gave those names with a single underscore at the beginning. This is a Python convention for indicating a "private" method -- one that is expected to be used internally, rather than by client code. + +Are all the existing tests still passing? + +Now that you've got the tag-building code in its own method, we can give the self closing tag a render method something like this: + +.. code-block:: python + + def render(self, outfile): + tag = self._open_tag()[:-1] + " />\n" + outfile.write(tag) + +And do your tests pass? Once they do add a couple more for the "br" element: + +.. code-block:: python + + def test_br(): + br = Br() + file_contents = render_result(br) + print(file_contents) + assert file_contents == "
      \n" + + + def test_content_in_br(): + with pytest.raises(TypeError): + br = Br("some content") + + + def test_append_content_in_br(): + with pytest.raises(TypeError): + br = Br() + br.append("some content") + +Getting that first test to pass should be straightforward -- but what about the other two? Self closing tags are now supposed to contain any content. So you want your ``SelfClosingTag`` class to raise an exception if you try to create one with some content. But you also want it to raise an exception if you try to append content. So do we need to override both the ``__init__()`` and ``append()`` methods? Maybe not. + +The ``__init__`` needs to do some other initializing, so not as easy to override as ``append``. Let's start with the ``append`` method. We need it to do nothing other than raise a ``TypeError``: + +.. code-block:: python + + def append(self, *args): + raise TypeError("You can not add content to a SelfClosingTag") + +And run your tests. I still get a single failure:: + + =================================== FAILURES =================================== + ______________________________ test_content_in_br ______________________________ + + def test_content_in_br(): + with pytest.raises(TypeError): + > br = Br("some content") + E Failed: DID NOT RAISE + + test_html_render.py:297: Failed + ===================== 1 failed, 17 passed in 0.08 seconds ====================== + +So ``append`` did the right thing. But we still have a failure when we try to initialize it with content. So we want to override the ``__init__``, check if there was any content passed in, and if there is, raise an error. And if not, then call the usual ``__init__``. + +.. code-block:: python + + class SelfClosingTag(Element): + + def __init__(self, content=None, **kwargs): + if content is not None: + raise TypeError("SelfClosingTag can not contain any content") + super().__init__(content=content, **kwargs) + +What's that ``super()`` call? That's a way to call a method on the "super class'" -- that is, the class that this class is derived from. In this case, that's exactly the same as if we had written: + +.. code-block:: python + + def __init__(self, content=None, **kwargs): + if content is not None: + raise TypeError("SelfClosingTag can not contain any content") + Element.__init__(self, content=content, **kwargs) + +But ``super`` provides some extra features if you are doing multiple inheritance. And it makes your intentions clear. + +I've now got 18 tests passing. How about you? And I can also uncomment step 5 in ``run_html_render.py``, and get something reasonable:: + + $ python run_html_render.py + + + PythonClass = Revision 1087: + + +

      + Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

      +
      + + + +If you get anything very different than this, write some tests to catch the error, and then fix them :-) + + +Step 6: +------- + +Create an ``A`` class for an anchor (link) element. Its constructor should look like:: + + A(self, link, content) + +where ``link`` is the link, and ``content`` is what you see. It can be called like so:: + + A("http://google.com", "link to google") + +and it should render like:: + + link to google + +Notice that while the a ("anchor") tag is kind of special, the link is simply an "href" (hyperlink reference) attribute. So we should be able to use most of our existing code, but simply add the link as another attribute. + +You know that drill now. Create a test first: one that makes the above call, and then checks that you get the results expected. Notice that this is a single line tag, so it should subclass from OneLineTag. If I start with that, I get:: + + =================================== FAILURES =================================== + _________________________________ test_anchor __________________________________ + + def test_anchor(): + > a = A("http://google.com", "link to google") + E TypeError: __init__() takes from 1 to 2 positional arguments but 3 were given + + test_html_render.py:307: TypeError + +Hmm -- a ``TypeError`` in the ``__init__``. Well, that makes sense because we need to be able to pass the link in to it. We will need to override it, of course: + +.. code-block:: python + + class A(OneLineTag): + + tag = 'a' + + def __init__(self, link, content=None, **kwargs): + super().__init__(content, **kwargs) + +Notice that I added the "link" parameter at the beginning, and that the rest of the parameters are the same as for the base ``Element`` class. This is a good approach. If you need to add an extra parameter when subclassing, put it at the front of the parameter list. We then call ``super().__init__`` with the content and any other keyword arguments. We haven't actually done anything with the link, but when I run the tests, it gets further, failing on the rendering. + +So we need to do something with the link. But what? Do we need a new render method? Maybe not. After all, the link is really just the value of the "href" attribute. So we can simply create an href attribute, and the existing rendering code should work. + +How are the attributes passed in? They are passed in in the ``kwargs`` dict. So we can simply add the link to the ``kwargs`` dict before calling the superclass initializer: + +.. code-block:: python + + def __init__(self, link, content=None, **kwargs): + kwargs['href'] = link + super().__init__(content, **kwargs) + +And run the tests:: + + =================================== FAILURES =================================== + _________________________________ test_anchor __________________________________ + + def test_anchor(): + a = A("http://google.com", "link to google") + file_contents = render_result(a) + print(file_contents) + > assert file_contents.startswith('(' = 'link to google\n'.startswith + + test_html_render.py:310: AssertionError + ----------------------------- Captured stdout call ----------------------------- + link to google + + ===================== 1 failed, 18 passed in 0.07 seconds ===================== + +Darn -- not passing! (Did yours pass?) Even though we added the ``href`` to the kwargs dict, it didn't get put in the attributes of the tag. Why not? Think carefully about the code. Where should the attributes be added? In the ``render()`` method. +But *which* render method is being used here? Well, the ``A`` class is a subclass of ``OneLineTag``, which has defined its own ``render()`` method. +So take a look at the ``OneLineTag`` ``render()`` method. Oops, mine doesn't have anything in to render attributes -- I wrote that before we added that feature. +So it's now time to go in and edit that render method to use the ``_open_tag`` and ``_close_tag`` methods. + +The tests should all pass now -- and you have a working anchor element. + +Step 7: +------- + +Making the list elements is pretty straightforward -- go ahead and do those -- and write some tests for them! + +Header Elements +............... + +You should have the tools to do this. First, write a couple tests. + +Then decide what to subclass for the header elements? Which of the base classes you've developed is most like a header? + +Then think about how you will update the ``__init__`` of your header subclass. It will need to take an extra parameter -- the level of the header: + +.. code-block:: python + + def __init__(self, level, content=None, **kwargs): + +But what to do with the level parameter? In this case, each level will have a different tag: ``h1``, ``h2``, etc. So you need to set the tag in the ``__init__``. So far, the tag has been a class attribute -- all instances of the class have the same tag. +In this case, each instance can have a different tag -- determined at initialization time. But how to override a class attribute? Think about how you access that attribute in your render methods: ``self.tag``. +When you make a reference to ``self.something``, Python first checks if "something" is an instance attribute. Then, if not, it checks for a class attribute, and, if not, then it looks in the superclasses. +So in this case, of you set an instance attribute for the tag -- that is what will be found in the other methods. So in the ``__init__``, you can set ``self.tag=the_new_tag_value``, which will be ``h1``, or ``h2``, or ... + +Step 8: +------- + +You have all the tools now for making a proper html element -- it should render as so:: + + + + + + Python Class Sample page + + ... + +That is, put a doctype tag at the top, before the html opening tag. + +(Note that that may break an earlier test. Update that test!) + +Step 9: +------- + +**Adding Indentation** + +Be sure to read the instructions for this carefully -- this is a bit tricky. But it's also fairly straightforward once you "get it." The trick here is that a given element can be indented some arbitrary amount and there is no way to know until render time how deep it is. But when a given element is rendering itself, it needs to know how deep it's indented, and it knows that the sub-elements need to be indented one more level. So by passing a parameter to each ``render()`` method that tells that element how much to indent itself, we can build a flexible system. + +Begin by uncommenting the tests in the test file for indentation: + +``test_indent``, ``test_indent_contents``, ``test_multiple_indent``, and ``test_element_indent1``. + + +These are probably not comprehensive, but they should get you started. If you see something odd in your results, make sure to add a test for that before you fix it. + +Running these new tests should result in 4 failures -- many (all?) of them like this:: + + AttributeError: type object 'Element' has no attribute 'indent' + +So the first step is to give you Element base class an ``indent`` attribute. This is the amount that you want one level of indentation to be -- maybe two or four spaces. You want everything the be indented the same amount, so it makes sense that you put it as a class attribute of the base class -- then *all* elements will inherit the same value. And you can change it in that one place if you want. + +Once I add the ``indent`` parameter, I still get four failures -- three of them are for the results being incorrect -- which makes sense, because we haven't implemented that code yet. One of them is:: + + # this so the tests will work before we tackle indentation + if ind: + > element.render(outfile, ind) + E TypeError: render() takes 2 positional arguments but 3 were given + +This is the next one to tackle; our ``render()`` methods all need to take an additional optional parameter, the current level of indentation. Remember to add that to *all* of your ``render()`` methods: + +.. code-block:: python + + def render(self, out_file, cur_ind=""): + +Once I do that, I still get four failures. But they are all about the rendered results being incorrect; the rendered indentation levels are not right. + +Now it's time to go in one by one and add indentation code to get those tests to pass. + +I got them all to pass. But when I rendered a full page (by running ``run_html_render.py``), I found some issues. The elements that overrode the ``render()`` methods where not indented properly: ``OneLineTag`` and ``SelfClosingTag``. + +Write at least one test for each of those, and then go fix those ``render()`` methods! + +What have you done? +------------------- + +**Congrats!** + +You've gotten to the end of the project. By going through this whole procedure, you've done a lot of test-driven development, and built up a class system that takes advantage of the key features of object oriented systems in Python. I hope this has given you a better understanding of: + +* class attributes vs. instance attributes + +* subclassing + +* overriding: + + - class attributes + + - class attributes with instance attributes + + - methods + +* calling a superclass' method from an overridden method. + +* defining a "private" method to be used by sub-classes overridden methods to make DRY code. + +* polymorphism -- having multiple classes all be used in the same way (calling the ``render`` method in this case) diff --git a/_sources/exercises/html_renderer_tutorial.rst.txt b/_sources/exercises/html_renderer_tutorial.rst.txt new file mode 100644 index 0000000..762688c --- /dev/null +++ b/_sources/exercises/html_renderer_tutorial.rst.txt @@ -0,0 +1,1612 @@ +.. _html_renderer_tutorial: + +####################################### +Tutorial for the Html Render Assignment +####################################### + +If you are finding that you don't really know where to start with the html render assignment, this tutorial will walk you through the process. + +However, you generally learn more if you figure things out for yourself. So I highly suggest that you give each step a try on your own first, before reading that step in this tutorial. Then, if you are really stuck -- follow the process here. + +.. _render_tutorial_1: + +Step 1: +------- + +Step one is a biggie -- that's 'cause you need a fair bit all working before you can actually have anything to test, really. But let's take it bit by bit. + +First, we are doing test driven development, and we already have a test or two. So let's run those, and see what we get. + +You should now be in a terminal with the current working directory set to where you downloaded ``html_render.py`` and ``test_html_render.py``. If you run pytest, it will find the ``test_html_render.py`` file and run the tests in it: + +.. code-block:: bash + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 3 items + + test_html_render.py ..F [100%] + + =================================== FAILURES =================================== + _____________________________ test_render_element ______________________________ + + def test_render_element(): + """ + Tests whether the Element can render two pieces of text + So it is also testing that the append method works correctly. + + It is not testing whether indentation or line feeds are correct. + """ + e = Element("this is some text") + e.append("and this is some more text") + + # This uses the render_results utility above + file_contents = render_result(e).strip() + + # making sure the content got in there. + > assert("this is some text") in file_contents + E AssertionError: assert 'this is some text' in 'just something as a place holder...' + + test_html_render.py:72: AssertionError + ====================== 1 failed, 2 passed in 0.06 seconds ====================== + +Hey! that's pretty cool -- two tests are already passing! Let's take a quick look at those: + +.. code-block:: python + + def test_init(): + """ + This only tests that it can be initialized with and without + some content -- but it's a start + """ + e = Element() + + e = Element("this is some text") + +So that one simply tested that an ``Element`` class exists, and that you can pass in a string when you initialize it -- not a bad start, but it doesn't show that you can *do* anything with it. + + +.. code-block:: python + + def test_append(): + """ + This tests that you can append text + + It doesn't test if it works -- + that will be covered by the render test later + """ + e = Element("this is some text") + e.append("some more text") + +And this one shows that you can call the ``append()`` method with some text -- nice, but again, it doesn't test if that appended text was used correctly. But it does show you got the API right. + +But this one failed: + +.. code-block:: python + + def test_render_element(): + """ + Tests whether the Element can render two pieces of text + So it is also testing that the append method works correctly. + + It is not testing whether indentation or line feeds are correct. + """ + e = Element("this is some text") + e.append("and this is some more text") + + # This uses the render_results utility above + file_contents = render_result(e).strip() + + # making sure the content got in there. + assert("this is some text") in file_contents + assert("and this is some more text") in file_contents + + # make sure it's in the right order + assert file_contents.index("this is") < file_contents.index("and this") + + # making sure the opening and closing tags are right. + assert file_contents.startswith("") + assert file_contents.endswith("") + +OK -- this one really does something real -- it tries to render an html element -- which did NOT pass -- so it's time to put some real functionality in the Element class. + +This is the code: + +.. code-block:: python + + class Element(object): + + def __init__(self, content=None): + pass + + def append(self, new_content): + pass + + def render(self, out_file): + out_file.write("just something as a place holder...") + +Looking there, we can see why the tests did what they did -- we have the three key methods, but they don't actually do anything. But the ``render`` method is the only one that actually provides some results to test. + +So back to the assignment: + + The ``Element`` class should have a class attribute for the tag name ("html" first). + +Each html element has a different "tag", specifying what kind of element it is. so our class needs one of those. Why a class attribute? Because each *instance* of each type (or class) of element will share the same tag. And we don't want to store the tag in the render method, because then we couldn't reuse that render method for a different type of element. + +So we need to add a tiny bit of code: + +.. code-block:: python + + class Element(object): + + tag = "html" + + def __init__(self, content=None): + pass + +That's not much -- will the test pass now? Probably not, because we aren't doing anything with the tag. But you can run it to see if you'd like. It's always good to run tests frequently to make sure you haven't inadvertently broken anything. + +Back to the task at hand: + + The class should have an ``append`` method that can add another string to the content. + + ... + + So your class will need a way to store the content in a way that you can keep adding more to it. + +OK, so we need a way to store the content: both what gets passed in to the ``__init__`` and what gets added with the ``append`` method. We need a data structure that can hold an ordered list of things, and can be added to in the future -- sounds like a list to me. So let's create a list in __init__ and store it in ``self`` for use by the other methods: + +.. code-block:: python + + def __init__(self, content=None): + self.contents = [content] + + def append(self, new_content): + self.contents.append(new_content) + +OK -- let's run the tests and see if anything changed:: + + > assert("this is some text") in file_contents + E AssertionError: assert 'this is some text' in 'just something as a place holder...' + + test_html_render.py:72: AssertionError + +Nope -- still failed at the first assert in test_render. This makes sense because we haven't done anything with the render method yet! + +.. rubric:: 1c. + +From the assignment: + + It should have a ``render(file_out)`` method that renders the tag and the strings in the content. + +We have the render method, but it's rendering arbitrary text to the file, not an html tag or contents. So let's add that. + +First let's add the contents, adding a newline in between to keep it readable. Remember that there can be multiple pieces of content, so we need to loop though the list: + +.. code-block:: python + + def render(self, out_file): + # loop through the list of contents: + for content in self.contents: + out_file.write(content) + out_file.write("\n") + +And run the tests:: + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 3 items + + test_html_render.py ..F [100%] + + =================================== FAILURES =================================== + _____________________________ test_render_element ______________________________ + + def test_render_element(): + """ + Tests whether the Element can render two pieces of text + So it is also testing that the append method works correctly. + + It is not testing whether indentation or line feeds are correct. + """ + e = Element("this is some text") + e.append("and this is some more text") + + # This uses the render_results utility above + file_contents = render_result(e).strip() + + # making sure the content got in there. + assert("this is some text") in file_contents + assert("and this is some more text") in file_contents + + # make sure it's in the right order + assert file_contents.index("this is") < file_contents.index("and this") + + # making sure the opening and closing tags are right. + > assert file_contents.startswith("") + E AssertionError: assert False + E + where False = ('') + E + where = 'this is some text\nand this is some more text'.startswith + + test_html_render.py:79: AssertionError + ====================== 1 failed, 2 passed in 0.05 seconds ====================== + +Failed in test_render again. But look carefully. It didn't fail on the first assert! It failed on this line:: + + assert file_contents.startswith("") + +This makes sense because we haven't rendered anything like that yet. So let's add that now. Recall that we want the results to look something like this: + +.. code-block:: html + + + Some content. + Some more content. + + +In this case, the "html" part is stored in a class attribute. So how would you make that tag? Looks like a good place for string formatting:: + + "<{}>".format(self.tag) + +and:: + + "".format(self.tag) + +So the method looks something like this: + +.. code-block:: python + + def render(self, out_file): + # loop through the list of contents: + for content in self.contents: + out_file.write("<{}>\n".format(self.tag)) + out_file.write(content) + out_file.write("\n") + out_file.write("\n".format(self.tag)) + +Now run the tests again:: + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 3 items + + test_html_render.py ... [100%] + + =========================== 3 passed in 0.02 seconds =========================== + +Whoo Hoo! All tests pass! But wait, there's more. Comprehensive testing is difficult. We tested that you could initialize the element with one piece of content, and then add another, and we checked that the opening and closing tag are there correctly. But is it actually rendering correctly? We may not have tested for everything. So we should take a look at the results, and see how it's doing. My trick for this is to print what I want to see in the test:: + + print(file_contents) + +and add a forced test failure at the end of the test, so we'll see that print:: + + assert False + +And let's run it:: + + =================================== FAILURES =================================== + _____________________________ test_render_element ______________________________ + + def test_render_element(): + """ + Tests whether the Element can render two pieces of text + So it is also testing that the append method works correctly. + + It is not testing whether indentation or line feeds are correct. + """ + e = Element("this is some text") + e.append("and this is some more text") + + # This uses the render_results utility above + file_contents = render_result(e).strip() + print(file_contents) + # making sure the content got in there. + assert("this is some text") in file_contents + assert("and this is some more text") in file_contents + + # make sure it's in the right order + assert file_contents.index("this is") < file_contents.index("and this") + + # making sure the opening and closing tags are right. + assert file_contents.startswith("") + assert file_contents.endswith("") + > assert False + E assert False + + test_html_render.py:82: AssertionError + ----------------------------- Captured stdout call ----------------------------- + + this is some text + + + and this is some more text + + +It failed on the ``assert False``. It's a good sign that it didn't fail before that. We can now look at the results we printed, and whoops! we actually got *two* html elements, rather than one with two pieces of content. Why is that? Before you look at the code again, let's make sure the test catches that and fails. How about this? + +.. code-block:: python + + assert file_contents.count("") == 1 + assert file_contents.count("") == 1 + +And it does indeed fail on this line:: + + > assert file_contents.count("") == 1 + E AssertionError: assert 2 == 1 + E + where 2 = ('') + E + where = '\nthis is some text\n\n\nand this is some more text\n'.count + + test_html_render.py:83: AssertionError + +Now that we know we can test for the issue, we can try to fix it, and we'll know it's fixed when the tests pass. + +So looking at the code -- why did I get two ```` tags? + +.. code-block:: python + + def render(self, out_file): + # loop through the list of contents: + for content in self.contents: + out_file.write("<{}>\n".format(self.tag)) + out_file.write(content) + out_file.write("\n") + out_file.write("\n".format(self.tag)) + +Hmm -- when are those tags getting rendered? *Inside* the loops through the contents! Oops! We want to write the tag *before* the loop, and the closing tag *after* loop. (Did you notice that the first time? I hope so.) So a little restructuring is in order. + +.. code-block:: python + + def render(self, out_file): + # loop through the list of contents: + out_file.write("<{}>\n".format(self.tag)) + for content in self.contents: + out_file.write(content) + out_file.write("\n") + out_file.write("\n".format(self.tag)) + +That's it -- let's see if the tests pass now:: + + + > assert False + E assert False + + test_html_render.py:86: AssertionError + ----------------------------- Captured stdout call ----------------------------- + + this is some text + and this is some more text + + +Mine failed on the ``assert False``. So the actual test passed -- good. And the rendered html tag looks right, too. So we can go ahead and remove that ``assert False``, and move on! + +We have tested to see that we could initialize with one piece of content, and then add another, but what if you initialized it with nothing, and then added some content? Try uncommenting the next test: ``test_render_element2`` and see what you get. + +This is what I got with my code:: + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 4 items + + test_html_render.py ...F [100%] + + =================================== FAILURES =================================== + _____________________________ test_render_element2 _____________________________ + + def test_render_element2(): + """ + Tests whether the Element can render two pieces of text + So it is also testing that the append method works correctly. + + It is not testing whether indentation or line feeds are correct. + """ + e = Element() + e.append("this is some text") + e.append("and this is some more text") + + # This uses the render_results utility above + > file_contents = render_result(e).strip() + + test_html_render.py:95: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + test_html_render.py:30: in render_result + element.render(outfile) + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + + self = + out_file = <_io.StringIO object at 0x10c4881f8> + + def render(self, out_file): + # loop through the list of contents: + for content in self.contents: + out_file.write("<{}>\n".format(self.tag)) + > out_file.write(content) + E TypeError: string argument expected, got 'NoneType' + + html_render.py:23: TypeError + ====================== 1 failed, 3 passed in 0.08 seconds ====================== + +Darn! Something is wrong here. And this time it errored out before it even got results to test. So look and see exactly what the error is. (pytest does a really nice job of showing you the errors):: + + out_file.write("<{}>\n".format(self.tag)) + > out_file.write(content) + E TypeError: string argument expected, got 'NoneType' + +It failed when we tried to write to the file. We're trying to write a piece of content, and we got a ``NoneType``. How in the world did a ``NoneType`` (which is the type of None) get in there? + +Where does the ``self.contents`` list get created? In the ``__init__``. Let's do a little print debugging here. Add a print to the ``__init__``: + +.. code-block:: python + + def __init__(self, content=None): + self.contents = [content] + print("contents is:", self.contents) + +And run the tests again:: + + > out_file.write(content) + E TypeError: string argument expected, got 'NoneType' + + html_render.py:24: TypeError + ----------------------------- Captured stdout call ----------------------------- + contents is: [None] + ====================== 1 failed, 3 passed in 0.06 seconds ====================== + + +Same failure -- but pytest does a nice job of showing you what was printed (stdout) when a test fails. So in this case, at the end of the ``__init__`` method, the contents list looks like ``[None]`` -- a list with a single None object in it. No wonder it failed later when we tried to write that ``None`` to a file! + +But why? Well, looking at the ``__init__``, it looks like content gets set to None by default: + +.. code-block:: python + + def __init__(self, content=None): + +Then we put that ``None`` in the ``self.contents`` list. What do we want when content is ``None``? +An empty list, so that we can add to it later, not a list with a ``None`` object in it. So you need some code that checks for ``None`` (hint: use ``is None`` or ``is not None`` to check for ``None``), and only adds content to the list if it is *not* ``None``. + +I'll leave it as an exercise for the reader to figure out how to do that, but make sure all tests are passing before you move on! And once the tests pass, you may want to remove that ``print()`` line. + +.. _render_tutorial_2_A: + +Step 2: +------- + +OK, we have nice little class here; it has a class attribute to store information about the tag, information that's the same for all instances. + +And we are storing a list of contents in "self" -- information that each instance needs its own copy of. + +And we are using that data to render an element. + +So we're ready to move on. + +Part A +...... + +.. rubric:: Instructions: + + +"Create a couple subclasses of ``Element``, for each of ````, ````, and ``

      `` tags. All you should have to do is override the ``tag`` class attribute (you may need to add a ``tag`` class attribute to the ``Element`` class first, if you haven't already)." + +So this is very straightforward. We have a class that represents an element, and the only difference between basic elements is that they have a different tag. For example:: + + + Some content. + Some more content. + + +and:: + +

      + Some content. + Some more content. +

      + + +The ```` tag is for the entire contents of an html page, and the ``

      `` tag is for a paragraph. But you can see that the form of the tags is identical, so we don't have to change much to make classes for these tags. In fact, all we need to change is the ``tag`` class attribute. + +Before we do that -- let's do some test-driven development. Uncomment the next few tests in ``test_html_render.py``: ``test_html``, ``test_body``, and ``test_p``, and run the tests:: + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 7 items + + test_html_render.py ....FFF [100%] + + =================================== FAILURES =================================== + __________________________________ test_html ___________________________________ + + def test_html(): + > e = Html("this is some text") + E NameError: name 'Html' is not defined + + test_html_render.py:117: NameError + __________________________________ test_body ___________________________________ + + def test_body(): + > e = Body("this is some text") + E NameError: name 'Body' is not defined + + test_html_render.py:129: NameError + ____________________________________ test_p ____________________________________ + + def test_p(): + > e = P("this is some text") + E NameError: name 'P' is not defined + + test_html_render.py:142: NameError + ====================== 3 failed, 4 passed in 0.08 seconds ====================== + +So we have three failures. Of course we do, because we haven't written any new code yet! +Yes, this is pedantic, and there is no real reason to run tests you know are going to fail. But there is a reason to *write* tests that you know are going to fail, and you have to run them to know that you have written them correctly. + +Now we can write the code for those three new element types. Try to do that yourself first, before you read on. + +OK, did you do something as simple as this? + +.. code-block:: python + + class Body(Element): + tag = 'body' + +(and similarly for ``Html`` and ``P``) + +That's it! But what does that mean? This line: + +.. code-block:: python + + class Body(Element): + +means: make a new subclass of the ``Element`` tag called ``Body``. + +and this line: + +.. code-block:: python + + tag = 'body' + +means: set the ``tag`` class attribute to ``'body'``. Since this class attribute was set on the ``Element`` class already, this is called "overriding" the tag attribute. + +The end result is that we now have a class that is exactly the same as the ``Element`` class, except with a different tag. Where is that attribute used? It is used in the ``render()`` method. + +Let's run the tests and see if this worked:: + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 7 items + + test_html_render.py ....... [100%] + + =========================== 7 passed in 0.02 seconds =========================== + +Success! We now have three different tags. + +.. note:: + Why the ``Html`` element? Doesn't the ``Element`` class already use the "html" tag? + Indeed it does, but the goal of the ``Element`` class is to be a base class for the other tags, rather than being a particular element. + Sometimes this is called an "abstract base class": a class that can't do anything by itself, but exists only to provide an interface (and partial functionality) for subclasses. + But we wanted to be able to test that partial functionality, so we had to give it a tag to use in the initial tests. + If you want to be pure about it, you could use something like ``abstract_tag`` in the ``Element`` class to make it clear that it isn't supposed to be used alone. And later on in the assignment, we'll be adding extra functionality to the ``Html`` element. + +Making a subclass where the only thing you change is a single class attribute may seem a bit silly -- and indeed it is. +If that were going to be the *only* difference between all elements, There would be other ways to accomplish that task that would make more sense -- perhaps passing the tag in to the initializer, for instance. +But have patience, as we proceed with the exercise, some element types will have more customization. + +Another thing to keep in mind. The fact that writing a new subclass is ALL we need to do to get a new type of element demonstrates the power of subclassing. With the tiny change of adding a subclass with a single class attribute, we get a new element that we can add content to, and render to a file, etc., with virtually no repeated code. + +.. _render_tutorial_2_B: + +Part B: +....... + +Now it gets more interesting, and challenging! + +The goal is to be able to render nested elements, like so: + +.. code-block:: html + + + +

      + a very small paragraph +

      +

      + Another small paragraph. + This one with multiple lines. +

      + + + +This means that we need to be able to append not just text to an element, but also other elements. The appending is easy -- the tricky bit is when you want to render those enclosed elements. + +Let's take this bit by bit -- first with a test or two. +Uncomment ``test_subelement`` in the test file, and run the tests:: + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 8 items + + test_html_render.py .......F [100%] + + =================================== FAILURES =================================== + _______________________________ test_sub_element _______________________________ + + def test_sub_element(): + """ + tests that you can add another element and still render properly + """ + page = Html() + page.append("some plain text.") + page.append(P("A simple paragraph of text")) + page.append("Some more plain text.") + + > file_contents = render_result(page) + + test_html_render.py:163: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + test_html_render.py:30: in render_result + element.render(outfile) + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + + self = + out_file = <_io.StringIO object at 0x10325b5e8> + + def render(self, out_file): + # loop through the list of contents: + for content in self.contents: + out_file.write("<{}>\n".format(self.tag)) + > out_file.write(content) + E TypeError: string argument expected, got 'P' + + html_render.py:26: TypeError + ====================== 1 failed, 7 passed in 0.11 seconds ====================== + +Again, the new test failed; no surprise because we haven't written any new code yet. But do read the report carefully; it did not fail on an assert, but rather with a ``TypeError``. The code itself raised an exception before it could produce results to test. + +So now it's time to write the code. Look at where the exception was raised: line 26 in my code, inside the ``render()`` method. The line number will likely be different in your code, but it probably failed on the render method. Looking closer at the error:: + + > out_file.write(content) + E TypeError: string argument expected, got 'P' + +It occurred in the file ``write`` method, complaining that it expected to be writing a string to the file, but it got a ``'P'``. ``'P'`` is the name of the paragraph element class. +So we need a way to write an element to a file. How might we do that? Inside the element's render method, we need to render an element... + +Well, elements already know how to render themselves. This is what is meant by a recursive approach. In the ``render`` method, we want to make use of the ``render`` method itself. + +Looking at the signature of the render method: + +.. code-block:: python + + def render(self, out_file): + +it becomes clear -- we render an element by passing the output file to the element's render method. Here is what mine looks like now: + +.. code-block:: python + + def render(self, out_file): + # loop through the list of contents: + for content in self.contents: + out_file.write("<{}>\n".format(self.tag)) + out_file.write(content) + out_file.write("\n") + out_file.write("\n".format(self.tag)) + +So let's update our render by replacing that ``out_file.write()`` call with a call to the content's ``render`` method: + +.. code-block:: python + + def render(self, out_file): + # loop through the list of contents: + for content in self.contents: + out_file.write("<{}>\n".format(self.tag)) + # out_file.write(content) + content.render(out_file) + out_file.write("\n") + out_file.write("\n".format(self.tag)) + +And let's see what happens when we run the tests:: + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 8 items + + test_html_render.py ..FFFFFF [100%] + + =================================== FAILURES =================================== + + ... lots of failures here + + _______________________________ test_sub_element _______________________________ + + def test_sub_element(): + """ + tests that you can add another element and still render properly + """ + page = Html() + page.append("some plain text.") + page.append(P("A simple paragraph of text")) + page.append("Some more plain text.") + + > file_contents = render_result(page) + + test_html_render.py:163: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + test_html_render.py:30: in render_result + element.render(outfile) + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + + self = + out_file = <_io.StringIO object at 0x10b123828> + + def render(self, out_file): + # loop through the list of contents: + for content in self.contents: + out_file.write("<{}>\n".format(self.tag)) + # out_file.write(content) + > content.render(out_file) + E AttributeError: 'str' object has no attribute 'render' + + html_render.py:27: AttributeError + ====================== 6 failed, 2 passed in 0.12 seconds ====================== + +Whoaa! Six failures! We really broke something! But that is a *good* thing. It's the whole point of unit tests. When you are making a change to address one issue, you know right away that you broke previously working code. + +So let's see if we can fix these tests, while still allowing us to add the feature we intended to add. + +Again -- look carefully at the error, and the solution might pop out at you:: + + > content.render(out_file) + E AttributeError: 'str' object has no attribute 'render' + +Now we are trying to call a piece of content's ``render`` method, but we got a simple string, which does not *have* a ``render`` method. +This is the challenge of this part of the exercise. It's easy to render a string, and it's easy to render an element, but the content list could have either one. So how do we switch between the two methods? + +There are a number of approaches you can take. This is a good time to read the notes about this here: :ref:`notes_on_handling_duck_typing`. +You may want to try one of the more complex methods, but for now, we're going to use the one that suggests itself from the error. + +We need to know whether we want to call a ``render()`` method, or simply write the content to the file. How would we know which to do? Again, look at the error: +We tried to call the render() method of a piece of content, but got an ``AttributeError``. So the way to know whether we can call a render method is to try to call it -- if it works, great! If not, we can catch the exception, and do something else. In this case, the something else is to try to write the content directly to the file: + +.. code-block:: python + + def render(self, out_file): + # loop through the list of contents: + for content in self.contents: + out_file.write("<{}>\n".format(self.tag)) + try: + content.render(out_file) + except AttributeError: + out_file.write(content) + out_file.write("\n") + out_file.write("\n".format(self.tag)) + +And run the tests again:: + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 8 items + + test_html_render.py ........ [100%] + + =========================== 8 passed in 0.03 seconds =========================== + +Yeah! all eight tests pass! I hope you found that at least a little bit satisfying. And pretty cool, really, that the solution requires only two extra lines of code. This is an application of the EAFP method: it's Easier to Ask Forgiveness than Permission. You simply try to do one thing, and if that raises the exception you expect, than do something else. + +It's also taking advantage of Python's "Duck Typing". Notice that we don't know if that piece of content is actually an ``Element`` object. All we know is that it has a ``render()`` method that we can pass a file-like object to. +This is quite deliberate. If some future user (that might be you) wants to write their own element type, they can write it any way they like, as long as it defines a ``render()`` method that can take a file-like object to write to. + +So what are the downsides to this method? Well, there are two: + +1. When we successfully call the ``render`` method, we have no idea if it's actually done the right thing -- it could do anything. If someone puts some completely unrelated object in the content list that happens to have a ``render`` method, this is not going to work. But what are the odds of that? + +2. This is the bigger one: if the object *HAS* a render method, but that render method has something wrong with it, then it could conceivably raise an ``AttributeError`` itself, but it would not be the Attribute Error we are expecting. The trick here is that this is very hard to debug. + +However, we are saved by tests. If the render method works in all the other tests, It's not going to raise an ``AttributeError`` only in this case. Another reason to have a good test suite. + + +.. _render_tutorial_3: + +Step 3: +------- + +Now we are getting a little more interesting. + +"Create a ```` element -- a simple subclass." + +This is easy; you know how to do that, yes? + +But the training wheels are off -- you are going to need to write your own tests now. So before you create the ``Head`` element class, write a test for it. You should be able to copy and paste one of the previous tests, and just change the name of the class and the tag value. Remember to give your test a new name, or it will simply replace the previous test. + +I like to run the tests as soon as I make a new one. If nothing else, I can make sure I have one more test. + +OK, that should have been straightforward. Now this part: + + Create a ``OneLineTag`` subclass of ``Element``: + + * It should override the render method, to render everything on one line -- for the simple tags, like:: + + PythonClass - Session 6 example + +Some html elements don't tend to have a lot of content, such as the document title. So it makes sense to render them all on one line. This is going to require a new render method. Since there are multiple types of elements that should be rendered on one line, we want to create a base class for all one-line elements. It should subclass from ``Element``, and override the render method with a new one, which will be pretty much the same as the main ``Element`` method, but without the newlines. + +Before we do that though -- let's write a test for that! Because the ``ONeLineTag`` class is a base class for actual elements that should be rendered on one line, we really don't need to write a test directly for it. We can write one for its first subclass: ``Title``. The title elements should be rendered something like this:: + + PythonClass - title example + +Which should be generated by code like this:: + + Title("PythonClass - title example") + +Take a look at one of the other tests to get ideas, and maybe start with a copy and paste, and then change the names: + +.. code-block:: python + + def test_title(): + e = Title("this is some text") + e.append("and this is some more text") + + file_contents = render_result(e).strip() + + assert("this is some text") in file_contents + assert("and this is some more text") in file_contents + print(file_contents) + assert file_contents.startswith("") + assert file_contents.endswith("") + +That's not going to pass, as there is no ``Title`` class. But before we get that far -- what else do we need to change about this test? +For starters, this test is appending additional content. +That's not very likely for a title, is it? So let's get rid of that line. + +.. code-block:: python + + def test_title(): + e = Title("This is a Title") + + file_contents = render_result(e).strip() + + assert("This is a Title") in file_contents + print(file_contents) + assert file_contents.startswith("") + assert file_contents.endswith("") + +So that's a bit cleaner. But let's look at those asserts -- what are we testing for? Looks like we're testing for the correct start and end tags, and that the content is there. That's a pretty good start, but it isn't checking for newlines at all. In fact, all the previous tests would pass even if our render method did not have any newlines in it at all. Which is probably OK -- html does not require newlines. You could go back and update the tests to check for the proper newlines, though later on, when we get to indenting, we'll be doing that anyway. + +But for this element, we want to make sure that we don't have any newlines. So let's add an assert for that: + +.. code-block:: python + + assert "\n" not in file_contents + +You can run the tests now if you like -- it will fail due to there being no Title element. So let's make one now. Remember that we want to start with a ``OneLineTag`` element, and then subclass ``Title`` from that. + +.. code-block:: python + + class OneLineTag(Element): + pass + + + class Title(OneLineTag): + tag = "title" + +The ``pass`` means "do nothing." But it is required to satisfy Python. There needs to be *something* in the class definition. So in this case, we have a ``OneLineTag`` class that is exactly the same as the ``Element`` class, and a ``Title`` class that is the same except for the tag. Time to test again:: + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 10 items + + test_html_render.py .........F [100%] + + =================================== FAILURES =================================== + __________________________________ test_title __________________________________ + + def test_title(): + e = Title("This is a Title") + + file_contents = render_result(e).strip() + + assert("This is a Title") in file_contents + print(file_contents) + assert file_contents.startswith("") + assert file_contents.endswith("") + > assert "\n" not in file_contents + E AssertionError: assert '\n' not in '\nThis is a Title\' + E '\n' is contained here: + E + E ? ------- + E This is a Title + E + + test_html_render.py:203: AssertionError + ----------------------------- Captured stdout call ----------------------------- + + This is a Title + + ====================== 1 failed, 9 passed in 0.12 seconds ====================== + +The title test failed on this assertion:: + + > assert "\n" not in file_contents + +Which is what we expected because we haven't written a new render method yet. But look at the end of the output, where is says ``-- Captured stdout call --``. That shows you how the title element is being rendered -- with the newlines. That's there because there is a print in the test: + +.. code-block:: python + + print(file_contents) + +.. note:: + + pytest is pretty slick with this. It "Captures" the output from print calls, etc., and then only shows them to you if a test fails. + So you can sprinkle print calls into your tests, and it won't clutter the output -- you'll only see it when a test fails, which is when you need it. + +This is a good exercise to go through. If a new test fails, it lets you know that the test itself is working, testing what it is supposed to test. + +So how do we get this test to pass? We need a new render method for ``OneLineTag``. For now, you can copy the render method from ``Element`` to ``OneLineTag``, and remove the newlines: + +.. code-block:: python + + class OneLineTag(Element): + + def render(self, out_file): + # loop through the list of contents: + for content in self.contents: + out_file.write("<{}>".format(self.tag)) + try: + content.render(out_file) + except AttributeError: + out_file.write(content) + out_file.write("\n".format(self.tag)) + +Notice that I left the newline in at the end of the closing tag -- we do want a newline there, so the next element won't get rendered on the same line. And the tests:: + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.7.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/Junk/lesson07, inifile: + collected 10 items + + test_html_render.py .......... [100%] + + ========================== 10 passed in 0.03 seconds =========================== + +We done good. But wait! there *is* a newline at the end, and yet the assert: ``assert "\n" not in file_contents`` passed! Why is that? + +Take a look at the code in the tests that renders the element: + +.. code-block:: python + + file_contents = render_result(e).strip() + +It's calling ``.strip()`` on the rendered string. That will remove all whitespace from both ends -- removing that last newline. + +However, there is still some extra code in that ``render()`` method. It's still looping through the contents and checking for an ``Element`` type. But for this, we hope that there will only be one piece of content, and it should not be an element. So we can make the render method simpler: + +.. code-block:: python + + class OneLineTag(Element): + def render(self, out_file): + out_file.write("<{}>".format(self.tag)) + out_file.write(self.contents[0]) + out_file.write("\n".format(self.tag)) + +If you are nervous about people appending content that will then be ignored, you can override the append method, too: + +.. code-block:: python + + def append(self, content): + raise NotImplementedError + +``NotImplementedError`` means just what it says -- this method is not implemented. My tests still pass, but how do I test to make sure that I can't append to a OneLineTag? Let's try that: + +.. code-block:: python + + def test_one_line_tag_append(): + """ + You should not be able to append content to a OneLineTag + """ + e = OneLineTag("the initial content") + e.append("some more content") + + file_contents = render_result(e).strip() + print(file_contents) + +And run the tests:: + + test_html_render.py:199: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + + self = + content = 'some more content' + + def append(self, content): + > raise NotImplementedError + E NotImplementedError + + html_render.py:57: NotImplementedError + ===================== 1 failed, 10 passed in 0.09 seconds ====================== + +Hmm. It raised a ``NotImplementedError``, which is what we want, but it is logging as a test failure. An exception raised in a test is going to cause a failure -- but what we want is for the test to pass only *if* that exception is raised. +Fortunately, pytest has a utility to do just that. Make sure there is an ``import pytest`` in your test file, and then add this code: + +.. code-block:: python + + def test_one_line_tag_append(): + """ + You should not be able to append content to a OneLineTag + """ + e = OneLineTag("the initial content") + with pytest.raises(NotImplementedError): + e.append("some more content") + +That ``with`` is a "context manager" (kind of like the file ``open()`` one). More on that later in the course, but what this means is that the test will pass if and only if the code inside that ``with`` block raised a ``NotImplementedError``. If it raises something else, or it doesn't raise an exception at all, then the test will fail. + +OK, I've got 11 tests passing now. How about you? Time for the next step. + +.. _render_tutorial_4: + +Step 4. +------- + +From the exercise instructions: + +"Extend the ``Element`` class to accept a set of attributes as keywords to the constructor, e.g. ``run_html_render.py``" + +If you don't know what attributes of an element are, read up a bit more on html on the web, and/or take another look at :ref:`html_primer`. But in short, attributes are a way to "customize" an element -- give it some extra information. The syntax looks like this:: + +

      + +Inside the opening tag, there is the tag name, then a space, then the attributes separated by spaces. Each attribute is a ``name="value"`` pair, with the name in plain text, and the value in quotes. + +Note that these name:value pairs look a lot like python keyword arguments, which lends itself to an initialization signature. For the above example, we would create the element like so: + +.. code-block:: python + + el = P("A paragraph of text", style="text-align: center", id="intro") + +Which should result in the following html:: + +

      + a paragraph of text +

      + + +Now that we know how to initialize an element with attributes, and how it should get rendered, we can write a test that will check if the attributes are rendered correctly. Something like: + +.. code-block:: python + + def test_attributes(): + e = P("A paragraph of text", style="text-align: center", id="intro") + + file_contents = render_result(e).strip() + print(file_contents) # so we can see it if the test fails + + # note: The previous tests should make sure that the tags are getting + # properly rendered, so we don't need to test that here. + # so using only a "P" tag is fine + assert "A paragraph of text" in file_contents + # but make sure the embedded element's tags get rendered! + # first test the end tag is there -- same as always: + assert file_contents.endswith("

      ") + + # but now the opening tag is far more complex + # but it starts the same: + assert file_contents.startswith(" e = P("A paragraph of text", style="text-align: center", id="intro") + E TypeError: __init__() got an unexpected keyword argument 'style' + + test_html_render.py:217: TypeError + ===================== 1 failed, 11 passed in 0.19 seconds ====================== + +Yes, the new test failed -- isn't TDD a bit hard on the ego? So many failures! But why? Well, we passed in the ``style`` and ``id`` attributes as keyword arguments, but the ``__init__`` doesn't expect those arguments. Hence the failure. + +So should be add those two as keyword parameters? Well, no we shouldn't because those are two arbitrary attribute names; we need to support virtually any attribute name. So how do you write a method that will accept ANY keyword argument? Time for our old friend ``**kwargs``. ``**kwargs**`` will allow any keyword argument to be used, and will store them in the ``kwargs`` dict. So time to update the ``Element.__init__`` like so: + +.. code-block:: python + + def __init__(self, content=None, **kwargs): + +But then, make sure to *do* something with the ``kwargs`` dict -- you need to store those somewhere. Remember that they are a collection of attribute names and values -- and you will need them again when it's time to render the opening tag. How do you store something so that it can be used in another method? I'll leave that as an exercise for the reader. + +And let's try to run the tests again:: + + ========================== 12 passed in 0.07 seconds =========================== + +They passed! Great, but did we test whether the attributes get rendered in the tag correctly? No -- not yet, let's make sure to add that. It may be helpful to add and ``assert False`` in there, so we can see what our tag looks like while we work on it:: + + ... + assert False + E assert False + + test_html_render.py:243: AssertionError + ----------------------------- Captured stdout call ----------------------------- +

      + A paragraph of text +

      + ===================== 1 failed, 11 passed in 0.11 seconds ====================== + +OK, so we have a regular old

      element -- no attributes at all -- no surprise here. Let's first add a couple tests for the attributes: + +.. code-block:: python + + # order of the tags is not important in html, so we need to + # make sure not to test for that + # but each attribute should be there: + assert 'style="text-align: center"' in file_contents + assert 'id="intro"' in file_contents + +We know the tests will fail -- so let's go straight to the code. We need to update our ``render()`` method to put the attributes in the opening tag. Let's remind ourselves what this needs to look like:: + +

      + +So we need to render the ``<``, then the ``p``, then a bunch of attribute name=value pairs. Let's start with breaking up the rendering of the opening tag, and make sure the existing tests still pass. + +.. code-block:: python + + def render(self, out_file): + # loop through the list of contents: + open_tag = ["<{}".format(self.tag)] + open_tag.append(">\n") + out_file.write("".join(open_tag)) + ... + +OK, the rest of the tests are still passing for me; I haven't broken anything else. Now to add code to render the attributes. You'll need to write some sort of loop to loop through each attribute, probably looping through the keys and the values: + +.. code-block:: python + + for key, value in self.attributes: + +Then you can render them in html form inside that loop. + +Once you've done that, run the tests again. When I do that, mine passes the asserts checking the attributes, but fails on the ``assert False``, so I can see how it's rendering:: + + > assert False + E assert False + + test_html_render.py:247: AssertionError + ----------------------------- Captured stdout call ----------------------------- + + A paragraph of text +

      + ===================== 1 failed, 11 passed in 0.11 seconds ====================== + +Hmmm -- the attributes are rendered correctly, but there is no space between the "p" (that tag name) and the first attribute. So let's update our tests to make sure that we check for that: + +.. code-block:: python + + assert file_contents.startswith("

      + A paragraph of text +

      + +However, my code for rendering the opening tag is a bit klunky -- how about yours? Perhaps you'd like to refactor it? Before you do that, you might want to make your tests a bit more robust. This is really tricky. It's very hard to test for everytihng that might go wrong, without nailing down the expected results too much. For example, in this case, we haven't tested that there is a space between the two attributes. In fact, this would pass our test:: + +

      + A paragraph of text +

      + +See how there is no space before "id"? But the order of the attributes is arbitrary, so we don't want to assume that the style will come before id. You could get really fancy with parsing the results, but I think we could get away with assuring there are the right number of spaces in the opening tag. + +.. code-block:: python + + assert file_contents[:file_contents.index(">")].count(" ") == 3 + +This now fails with my broken code, but passes when I fix it with that space between the attributes. What else might you want to check to make sure it's all good? + +Here's my final test for attribute rendering: + +.. code-block:: python + + def test_attributes(): + e = P("A paragraph of text", style="text-align: center", id="intro") + + file_contents = render_result(e).strip() + print(file_contents) # so we can see it if the test fails + + # note: The previous tests should make sure that the tags are getting + # properly rendered, so we don't need to test that here. + # so using only a "P" tag is fine + assert "A paragraph of text" in file_contents + # but make sure the embedded element's tags get rendered! + # first test the end tag is there -- same as always: + assert file_contents.endswith("

      ") + + # but now the opening tag is far more complex + # but it starts the same: + assert file_contents.startswith("

      ") > file_contents.index('id="intro"') + assert file_contents[:file_contents.index(">")].count(" ") == 3 + +With this test in place, you can safely refactor your attribute rendering if you like. I know I did. + +Step 5: +------- + +Create a ``SelfClosingTag`` subclass of Element, to render tags like:: + +


      + +and:: + +
      + +(horizontal rule and line break) + +Including with attributes:: + +
      + +So let's start with two tests: + +.. code-block:: python + + def test_hr(): + """a simple horizontal rule with no attributes""" + hr = Hr() + file_contents = render_result(hr) + print(file_contents) + assert file_contents == '
      \n' + + + def test_hr_attr(): + """a horizontal rule with an attribute""" + hr = Hr(width=400) + file_contents = render_result(hr) + print(file_contents) + assert file_contents == '
      \n' + +Which, of course, will fail initially. + +We'll want multiple self closing tags -- so we'll start with a base class, and then derive the Hr tag from that: + +.. code-block:: python + + class SelfClosingTag(Element): + pass + + + class Hr(SelfClosingTag): + tag = "hr" + +Test still fails -- but gets further. +You'll need to override the ``render()`` method: + +.. code-block:: python + + class SelfClosingTag(Element): + + def render(self, outfile): + # put rendering code here. + +What needs to be there? Well, self closing tags can have attributes, same as other elements. +So we need a lot of the same code here as with the other ``render()`` methods. You could copy and paste the ``Element.render()`` method, and edit it a bit. But that's a "Bad Idea" -- remember DRY (Don't Repeat Yourself)? +You really don't want two copies of that attribute rendering code you worked so hard on. +How do we avoid that? We take advantage of the power of subclassing. If you put the code to render the opening (and closing) tags in it's own method, then we can call that method from multiple render methods, something like: + +.. code-block:: python + + def render(self, out_file): + # loop through the list of contents: + out_file.write(self._open_tag()) + out_file.write("\n") + for content in self.contents: + try: + content.render(out_file) + except AttributeError: + out_file.write(content) + out_file.write("\n") + out_file.write(self._close_tag()) + out_file.write("\n") + +So instead of making the tag in the render method itself, we call the ``_open_tag`` and ``_close_tag`` methods. Note that I gave those names with a single underscore at the beginning. This is a Python convention for indicating a "private" method -- one that is expected to be used internally, rather than by client code. + +Are all the existing tests still passing? + +Now that you've got the tag-building code in its own method, we can give the self closing tag a render method something like this: + +.. code-block:: python + + def render(self, outfile): + tag = self._open_tag()[:-1] + " />\n" + outfile.write(tag) + +And do your tests pass? Once they do add a couple more for the "br" element: + +.. code-block:: python + + def test_br(): + br = Br() + file_contents = render_result(br) + print(file_contents) + assert file_contents == "
      \n" + + + def test_content_in_br(): + with pytest.raises(TypeError): + br = Br("some content") + + + def test_append_content_in_br(): + with pytest.raises(TypeError): + br = Br() + br.append("some content") + +Getting that first test to pass should be straightforward -- but what about the other two? Self closing tags are now supposed to contain any content. So you want your ``SelfClosingTag`` class to raise an exception if you try to create one with some content. But you also want it to raise an exception if you try to append content. So do we need to override both the ``__init__()`` and ``append()`` methods? Maybe not. + +The ``__init__`` needs to do some other initializing, so not as easy to override as ``append``. Let's start with the ``append`` method. We need it to do nothing other than raise a ``TypeError``: + +.. code-block:: python + + def append(self, *args): + raise TypeError("You can not add content to a SelfClosingTag") + +And run your tests. I still get a single failure:: + + =================================== FAILURES =================================== + ______________________________ test_content_in_br ______________________________ + + def test_content_in_br(): + with pytest.raises(TypeError): + > br = Br("some content") + E Failed: DID NOT RAISE + + test_html_render.py:297: Failed + ===================== 1 failed, 17 passed in 0.08 seconds ====================== + +So ``append`` did the right thing. But we still have a failure when we try to initialize it with content. So we want to override the ``__init__``, check if there was any content passed in, and if there is, raise an error. And if not, then call the usual ``__init__``. + +.. code-block:: python + + class SelfClosingTag(Element): + + def __init__(self, content=None, **kwargs): + if content is not None: + raise TypeError("SelfClosingTag can not contain any content") + super().__init__(content=content, **kwargs) + +What's that ``super()`` call? That's a way to call a method on the "super class'" -- that is, the class that this class is derived from. In this case, that's exactly the same as if we had written: + +.. code-block:: python + + def __init__(self, content=None, **kwargs): + if content is not None: + raise TypeError("SelfClosingTag can not contain any content") + Element.__init__(self, content=content, **kwargs) + +But ``super`` provides some extra features if you are doing multiple inheritance. And it makes your intentions clear. + +I've now got 18 tests passing. How about you? And I can also uncomment step 5 in ``run_html_render.py``, and get something reasonable:: + + $ python run_html_render.py + + + PythonClass = Revision 1087: + + +

      + Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text +

      +
      + + + +If you get anything very different than this, write some tests to catch the error, and then fix them :-) + + +Step 6: +------- + +Create an ``A`` class for an anchor (link) element. Its constructor should look like:: + + A(self, link, content) + +where ``link`` is the link, and ``content`` is what you see. It can be called like so:: + + A("http://google.com", "link to google") + +and it should render like:: + + link to google + +Notice that while the a ("anchor") tag is kind of special, the link is simply an "href" (hyperlink reference) attribute. So we should be able to use most of our existing code, but simply add the link as another attribute. + +You know that drill now. Create a test first: one that makes the above call, and then checks that you get the results expected. Notice that this is a single line tag, so it should subclass from OneLineTag. If I start with that, I get:: + + =================================== FAILURES =================================== + _________________________________ test_anchor __________________________________ + + def test_anchor(): + > a = A("http://google.com", "link to google") + E TypeError: __init__() takes from 1 to 2 positional arguments but 3 were given + + test_html_render.py:307: TypeError + +Hmm -- a ``TypeError`` in the ``__init__``. Well, that makes sense because we need to be able to pass the link in to it. We will need to override it, of course: + +.. code-block:: python + + class A(OneLineTag): + + tag = 'a' + + def __init__(self, link, content=None, **kwargs): + super().__init__(content, **kwargs) + +Notice that I added the "link" parameter at the beginning, and that the rest of the parameters are the same as for the base ``Element`` class. This is a good approach. If you need to add an extra parameter when subclassing, put it at the front of the parameter list. We then call ``super().__init__`` with the content and any other keyword arguments. We haven't actually done anything with the link, but when I run the tests, it gets further, failing on the rendering. + +So we need to do something with the link. But what? Do we need a new render method? Maybe not. After all, the link is really just the value of the "href" attribute. So we can simply create an href attribute, and the existing rendering code should work. + +How are the attributes passed in? They are passed in in the ``kwargs`` dict. So we can simply add the link to the ``kwargs`` dict before calling the superclass initializer: + +.. code-block:: python + + def __init__(self, link, content=None, **kwargs): + kwargs['href'] = link + super().__init__(content, **kwargs) + +And run the tests:: + + =================================== FAILURES =================================== + _________________________________ test_anchor __________________________________ + + def test_anchor(): + a = A("http://google.com", "link to google") + file_contents = render_result(a) + print(file_contents) + > assert file_contents.startswith('(' = 'link to google\n'.startswith + + test_html_render.py:310: AssertionError + ----------------------------- Captured stdout call ----------------------------- + link to google + + ===================== 1 failed, 18 passed in 0.07 seconds ===================== + +Darn -- not passing! (Did yours pass?) Even though we added the ``href`` to the kwargs dict, it didn't get put in the attributes of the tag. Why not? Think carefully about the code. Where should the attributes be added? In the ``render()`` method. +But *which* render method is being used here? Well, the ``A`` class is a subclass of ``OneLineTag``, which has defined its own ``render()`` method. +So take a look at the ``OneLineTag`` ``render()`` method. Oops, mine doesn't have anything in to render attributes -- I wrote that before we added that feature. +So it's now time to go in and edit that render method to use the ``_open_tag`` and ``_close_tag`` methods. + +The tests should all pass now -- and you have a working anchor element. + +Step 7: +------- + +Making the list elements is pretty straightforward -- go ahead and do those -- and write some tests for them! + +Header Elements +............... + +You should have the tools to do this. First, write a couple tests. + +Then decide what to subclass for the header elements? Which of the base classes you've developed is most like a header? + +Then think about how you will update the ``__init__`` of your header subclass. It will need to take an extra parameter -- the level of the header: + +.. code-block:: python + + def __init__(self, level, content=None, **kwargs): + +But what to do with the level parameter? In this case, each level will have a different tag: ``h1``, ``h2``, etc. So you need to set the tag in the ``__init__``. So far, the tag has been a class attribute -- all instances of the class have the same tag. +In this case, each instance can have a different tag -- determined at initialization time. But how to override a class attribute? Think about how you access that attribute in your render methods: ``self.tag``. +When you make a reference to ``self.something``, Python first checks if "something" is an instance attribute. Then, if not, it checks for a class attribute, and, if not, then it looks in the superclasses. +So in this case, of you set an instance attribute for the tag -- that is what will be found in the other methods. So in the ``__init__``, you can set ``self.tag=the_new_tag_value``, which will be ``h1``, or ``h2``, or ... + +Step 8: +------- + +You have all the tools now for making a proper html element -- it should render as so:: + + + + + + Python Class Sample page + + ... + +That is, put a doctype tag at the top, before the html opening tag. + +(Note that that may break an earlier test. Update that test!) + +Step 9: +------- + +**Adding Indentation** + +Be sure to read the instructions for this carefully -- this is a bit tricky. But it's also fairly straightforward once you "get it." The trick here is that a given element can be indented some arbitrary amount and there is no way to know until render time how deep it is. But when a given element is rendering itself, it needs to know how deep it's indented, and it knows that the sub-elements need to be indented one more level. So by passing a parameter to each ``render()`` method that tells that element how much to indent itself, we can build a flexible system. + +Begin by uncommenting the tests in the test file for indentation: + +``test_indent``, ``test_indent_contents``, ``test_multiple_indent``, and ``test_element_indent1``. + + +These are probably not comprehensive, but they should get you started. If you see something odd in your results, make sure to add a test for that before you fix it. + +Running these new tests should result in 4 failures -- many (all?) of them like this:: + + AttributeError: type object 'Element' has no attribute 'indent' + +So the first step is to give you Element base class an ``indent`` attribute. This is the amount that you want one level of indentation to be -- maybe two or four spaces. You want everything the be indented the same amount, so it makes sense that you put it as a class attribute of the base class -- then *all* elements will inherit the same value. And you can change it in that one place if you want. + +Once I add the ``indent`` parameter, I still get four failures -- three of them are for the results being incorrect -- which makes sense, because we haven't implemented that code yet. One of them is:: + + # this so the tests will work before we tackle indentation + if ind: + > element.render(outfile, ind) + E TypeError: render() takes 2 positional arguments but 3 were given + +This is the next one to tackle; our ``render()`` methods all need to take an additional optional parameter, the current level of indentation. Remember to add that to *all* of your ``render()`` methods: + +.. code-block:: python + + def render(self, out_file, cur_ind=""): + +Once I do that, I still get four failures. But they are all about the rendered results being incorrect; the rendered indentation levels are not right. + +Now it's time to go in one by one and add indentation code to get those tests to pass. + +I got them all to pass. But when I rendered a full page (by running ``run_html_render.py``), I found some issues. The elements that overrode the ``render()`` methods where not indented properly: ``OneLineTag`` and ``SelfClosingTag``. + +Write at least one test for each of those, and then go fix those ``render()`` methods! + +What have you done? +------------------- + +**Congrats!** + +You've gotten to the end of the project. By going through this whole procedure, you've done a lot of test-driven development, and built up a class system that takes advantage of the key features of object oriented systems in Python. I hope this has given you a better understanding of: + +* class attributes vs. instance attributes + +* subclassing + +* overriding: + + - class attributes + + - class attributes with instance attributes + + - methods + +* calling a superclass' method from an overridden method. + +* defining a "private" method to be used by sub-classes overridden methods to make DRY code. + +* polymorphism -- having multiple classes all be used in the same way (calling the ``render`` method in this case) diff --git a/_sources/exercises/index.rst.txt b/_sources/exercises/index.rst.txt new file mode 100644 index 0000000..4cdcd39 --- /dev/null +++ b/_sources/exercises/index.rst.txt @@ -0,0 +1,146 @@ +############# +All Exercises +############# + +These are the exercises associated with each Lesson: + +See the Canvas or EdX instance for your class for the full lesson descriptions. + + +Lesson 1: Introductions and Setting up your Environment +------------------------------------------------------- + +.. toctree:: + :maxdepth: 1 + + python_pushups.rst + + +Lesson 2: Basic Python and Functions +------------------------------------ + +.. toctree:: + :maxdepth: 1 + + grid_printer.rst + fizz_buzz.rst + fib_and_lucas.rst + + +Lesson 3: Booleans, Sequences, Iteration, and Strings +------------------------------------------------------ + +.. toctree:: + :maxdepth: 1 + + slicing.rst + list_lab.rst + string_formatting.rst + mailroom/mailroom.rst + mailroom/mailroom_tutorial.rst + + +Lesson 4: Dictionaries, Sets, and Files +--------------------------------------- + +.. toctree:: + :maxdepth: 1 + + dict_lab.rst + file_lab.rst + trigrams/trigrams.rst + mailroom/mailroom_with_dicts.rst + + +Lesson 5: Exceptions, Modules, and Comprehensions +------------------------------------------------- + +.. toctree:: + :maxdepth: 1 + + exceptions_lab.rst + except_exercise.rst + mailroom/mailroom_with_exceptions.rst + comprehensions_lab.rst + mailroom/mailroom_with_comprehensions.rst + + +Lesson 6: Unit Testing and Advanced Argument Passing +---------------------------------------------------- + +.. toctree:: + :maxdepth: 1 + + unit_testing/unit_testing.rst + args_kwargs_lab.rst + mailroom/mailroom_with_tests.rst + + +Lesson 7: Intro to Object Oriented Programing +--------------------------------------------- + +.. toctree:: + :maxdepth: 1 + + oo_intro.rst + html_renderer.rst + html_renderer_tutorial.rst + + +Lesson 8: Properties and Magic Methods +-------------------------------------- + +.. toctree:: + :maxdepth: 1 + + circle/circle_class.rst + +Lesson 9: Multiple Inheritance +------------------------------ + +.. toctree:: + :maxdepth: 1 + + mailroom/mailroom-oo.rst + + +Lesson 10: Catch up +------------------- + + + +Extra Exercises +---------------- + +The following are not required, but can be useful learning tools. Feel free to try them out! + +.. toctree:: + :maxdepth: 1 + + rot13.rst + + lambda_magic.rst + + sparse_array.rst + + context-managers-exercise.rst + + mailroom/mailroom-pkg.rst + + trapezoid.rst + + context-managers-exercise.rst + + mailroom/mailroom-decorator.rst + + mailroom/mailroom-meta.rst + + mailroom/mailroom-mock.rst + + threaded_downloader.rst + + + + + + diff --git a/_sources/exercises/kata_fourteen.rst.txt b/_sources/exercises/kata_fourteen.rst.txt new file mode 100644 index 0000000..4d27eff --- /dev/null +++ b/_sources/exercises/kata_fourteen.rst.txt @@ -0,0 +1,378 @@ +.. _exercise_trigrams: + +==================================== +Trigrams -- Simple Text Manipulation +==================================== + +.. rubric:: Kata Fourteen: Tom Swift Under the Milk Wood + +Adapted from Dave Thomas's work: + +http://codekata.com/kata/kata14-tom-swift-under-the-milkwood/ + + +Trigrams +======== + +Trigrams can be used to mutate text into new, surreal, forms. But what +heuristics do we apply to get a reasonable result? + +The Problem +------------ + +As a boy, one of my treats was go to the shops on a Saturday and spend part +of my allowance on books; for a nine-year old, I had quite a collection of books from the +Tom Swift and Hardy Boys series. Wouldn’t it be great to be able to create +more and more of these classic books, to be able to generate a new Tom +Swift adventure on demand? + + +OK, perhaps not. But that won’t stop us trying. I coded up a quick +program to generate some swashbuckling scientific adventure on demand. It +came up with: + + ... it was in the wind that was what he thought was his companion. I + think would be a good one and accordingly the ship their situation + improved. Slowly so slowly that it beat the band! You’d think no one + was a low voice. "Don’t take any of the elements and the + inventors of the little Frenchman in the enclosed car or cabin completely + fitted up in front of the gas in the house and wringing her hands. + "I’m sure they’ll fall!" + + She looked up at them. He dug a mass of black vapor which it had + refused to accept any. As for Mr. Swift as if it goes too high I’ll + warn you and you can and swallow frequently. That will make the airship was + shooting upward again and just before the raid wouldn’t have been + instrumental in capturing the scoundrels right out of jail." + + +Stylistically, it’s Victor Appleton (pseudonymous author of the Tom Swift series) meets Dylan Thomas (Welsh poet). Technically, +it’s all done with trigrams. + +Trigram analysis is very simple. Look at each set of three adjacent words +in a document. Use the first two words of the set as a key, and remember +the fact that the third word followed that key. Once you’ve finished, +you know the list of individual words that can follow each two word +sequence in the document. For example, given the input:: + + I wish I may I wish I might + +You might generate:: + + "I wish" => ["I", "I"] + "wish I" => ["may", "might"] + "may I" => ["wish"] + "I may" => ["I"] + + +This says that the words "I wish" are twice followed by the word +"I", the words "wish I" are followed once by "may" and once by "might" +and so on. + +To generate new text from this analysis, choose an arbitrary word pair as a +starting point. Use this pair of words to look up a random next word (using the table +above) and append this new word to the text so far. This now gives you three words with a +new word pair (second and third words) at the end of the three-word text. Look up a potential next word +based on this pair. This generates another pair to add to the list, and so on. In the previous example, +we could start with "I may". The only possible next word is +"I", so now we have:: + + I may I + +The last two words are "may I," so the next word is +"wish". We then look up "I wish," and find our choice +is constrained to another "I":: + + I may I wish I + + +Now we look up "wish I," and find we have a choice. Let’s +choose "may":: + + I may I wish I may + +Now we’re back where we started from, with "I may." +Following the same sequence, but choosing "might" this time, we +get:: + + I may I wish I may I wish I might + +At this point we stop, as no sequence starts "I might." + + +Given a short input text, the algorithm isn’t too interesting. Feed +it a book, however, and you give it more options, so the resulting output +can be surprising. + +For this exercise, try implementing a trigram algorithm that generates a couple +of hundred words of text using a book-sized file as input. + +`Project Gutenberg `_ is a good source of online +books (*Tom Swift and His Airship* is `here `_.) + +Be warned that these files have DOS line endings (carriage return followed by +newline). + + +Here is a copy of short-story collection *The Adventures of Sherlock Holmes*: + +:download:`sherlock.txt <./sherlock.txt>`. + +And a shorter copy for testing (a paragraph from one of the stories, "A Scandal in Bohemia"): + +:download:`sherlock_small.txt <./sherlock_small.txt>`. + + +Objectives +----------- + +Katas are about trying something many times. In this one, what +we’re experimenting with is not just the code, but the heuristics of +processing the text. What do we do with punctuation? Paragraphs? Do we have +to implement backtracking if we chose a next word that turns out to be a +dead end? + +I’ll fire the signal and the fun will commence... + +Developing Your Solution +======================== + +This assignment has two parts: the key one is the trigrams exercise itself, but you also need to do some text processing to get a full book in shape for processing. + +I suggest you write the trigrams part first; it's more interesting :-) + +trigrams +-------- + +Key to the trigrams problem is the selection of the data structure to use to hold the "trigrams" themselves. What do we need here? + +The text +........ + +First, you'll want a bit of text to try your code out on. Why not try the example here:: + + I wish I may I wish I might + +You need that in a python data structure somehow, so how about: + +.. code-block:: python + + words = "I wish I may I wish I might".split() + +This produces an (ordered) list of words:: + + ['I', 'wish', 'I', 'may', 'I', 'wish', 'I', 'might'] + +Now you've got some words to play with. Once you think you've got it working, try a bit longer piece of text. But this will do for now, and it's small and simple enough that you can immediately see if your code is working. + +The trigrams structure +---------------------- + +From above, this is what you need to build up something like this:: + + "I wish" => ["I", "I"] + "wish I" => ["may", "might"] + "may I" => ["wish"] + "I may" => ["I"] + +Hmmm, in a way, that's almost pseudo code. You have a bunch of word pairs, and for each word pair, there are one or more words that follow it. + +Those following words look a lot like they are in a list, yes? Perfect, the list structure keeps order, and you can keep adding (appending) new words to it. + +Each of those lists of words needs to be mapped to a particular pair. Each pair is unique; it only shows up once (when that same pair is encountered again in the text, you add the follower to the list). + +That sounds a lot like a dictionary. The keys (word pairs) are unique, and map to a list of following words. (Note that, technically, in python the dictionary is only one implementation of a +`Mapping `_.) + +Now you have a choice of data structures: string or tuple. + +String: The keys are a pair of words and can be represented as a string of two words with a space like so: + +.. code-block:: python + + trigrams = {"I wish": ["I", "I"], + "wish I": ["may", "might"], + "may I": ["wish"], + "I may": ["I"], + } + +Tuple: But strings are not the only type that you can use as keys in a dict; you can use any *immutable* type. Recall that tuples are immutable (they can't be changed once they have been created). Since each pair of words is, well, a pair, it makes sense to store each pair in a tuple, keeping the individual words separate: + +.. code-block:: python + + trigrams = {("I", "wish"): ["I", "I"], + ("wish", "I"): ["may", "might"], + ("may", "I"): ["wish"], + ("I", "may"): ["I"], + } + +I like the example that uses tuples better, but either one will work. + +Building the Trigrams dict +.......................... + +So you've got a list of words, and you need to build up a dict like one of the above. + +It's time to create a python file and start writting some code! + +.. code-block:: python + + #!/usr/bin/env python3 + + words = "I wish I may I wish I might".split() + + + def build_trigrams(words): + """ + build up the trigrams dict from the list of words + + returns a dict with: + keys: word pairs + values: list of followers + """ + trigrams = {} + + # build up the dict here! + + return trigrams + + + if __name__ == "__main__": + trigrams = build_trigrams(words) + print(trigrams) + +So how do you actually build up that dict? That's kind of the point of the exercise, so I won't tell you that ... but here are some hints: + +**Looping through the words** + +Obviously you need to loop through all the words, so a ``for`` loop makes sense. However, this is a bit tricky. Usually in Python you loop through all the items in a list, and don't worry about the indices: + +.. code-block:: python + + for item in a_list: + ... + +But in this case, we don't need to work with one word at a time, we need to work with three at a time (a pair of words, and the single word that follows it). +So contrary to the usual practice, an index can be helpful here: + +.. code-block:: python + + for i in range(len(words)-2): # why -2 ? + pair = words[i:i + 2] + follower = words[i + 2] + +**Adding a pair to the dict:** + +For each pair in the text, you need to add it to the dict. But: + +- ``words[i:i + 2]`` is a list with two words in it. Can that be used as a key in a dict? (Try it.) If not, how can you make a valid key out of it? + +- As you loop through the text, you will collect pairs of words. Each time, a given pair may already be in the dict. + + - If the pair is not in the dict, you want to put it in the dict, with value being a list with the follower in it:: + + ("may", "I"): ["wish"] + + - If the pair already is in the dict, then you want to add the follower (the second word in the pair) to the list that's already there:: + + ("wish", "I"): ["may", "might"] + +Note that the example above suggests the basic logic; it's almost pseudo-code. And that logic will work. But it turns out that this is a common enough operation that python dicts have a method that lets you do that logic in one step? Can you find it? + +`Python dict Documentation `_ + +You should now have code that will return a dict like we noted above:: + + {("I", "wish"): ["I", "I"], + ("wish", "I"): ["may", "might"], + ("may", "I"): ["wish"], + ("I", "may"): ["I"]} + +Try it out on a longer bit of text (your choice) before you go any further. + +Using the Trigrams dict +....................... + +This is the fun part. Once you have a mapping of word pairs to following words, you can build up some new "fake" text. Re-read the previous sections again to remind yourself of the procedure. Here are a couple of additional hints and questions to consider: + +- The ``random`` module is your friend here: + +.. code-block:: python + + import random + + # returns a number between a and b (including a and b) + random.randint(a, b) + + # pick a random item from a sequence + random.choice(a_list) + +- You need to start with the first word pair; picking a random key from a dict is actually a bit tricky. Start with this known pair, and once you have the code working, you can figure out a better way to pick a pair to start with. + +- As you build up your text, you probably want to build it up in a list, appending one word at a time. You can join it together at the end with ``" ".join(the_list_of_words)`` + +- Remember that after adding a word to a pair to make a three-word text, the next pair is the last two words in that three-word text. + +- What to do if you end up with a word pair that isn't in the original text? + +- How to terminate? Probably have a pre-defined length of text! + +Once you have the basics working, try your code on a longer piece of input text. Then think about making it fancy. Can you make sentences with capitalized first words and punctuation? Anything else to make the text more "real"? + +Processing the Input Text +------------------------- + +If you get a book from Project Gutenberg (or anywhere else), it will not be "clean." That is, it will have header information, footer information, chapter headings, punctuation, what have you. So you'll need to clean it up somehow to get a simple list of words to use to build your trigrams. + +The first part of the process is pretty straightforward; open the file and loop through the lines of text. + +You may want to skip the header. How would you do that?? +Hint: in a Project Gutenberg e-book, there is a line of text that starts with:: + + *** START OF THIS PROJECT GUTENBERG EBOOK + +In the loop, you can process a single line of text to break it into words: + + - calling ``.split()`` + +Optional steps to cleaning up the text: + + - Strip out punctuation? + - If you do this, what about contractions, i.e. the appostrophe in "can't" vs. a single quotation mark -- which are the same character. + + - Remove capitalization? + - If you do this, what about "I"? And proper nouns? + +Any other ideas you may have. + +**Hints:** + +The ``string`` methods are your friend here. + +There are also handy constants in the ``string`` module: ``import string`` + +Check out the ``str.translate()`` method; it can make multiple replacements very fast. + +Do get the full trigrams code working first, then play with some of the fancier options. + +Code Structure +-------------- + +Break your code down into a handful of separate functions. This way you can test each on its own, and it's easier to refactor one part without messing with the others. For instance, your ``__main__`` block might look something like: + +.. code-block:: python + + if __name__ == "__main__": + # get the filename from the command line + try: + filename = sys.argv[1] + except IndexError: + print("You must pass in a filename") + sys.exit(1) + + in_data = read_in_data(filename) + words = make_words(in_data) + word_pairs = build_trigram(words) + new_text = build_text(word_pairs) + + print(new_text) diff --git a/_sources/exercises/lambda_magic.rst.txt b/_sources/exercises/lambda_magic.rst.txt new file mode 100644 index 0000000..7041495 --- /dev/null +++ b/_sources/exercises/lambda_magic.rst.txt @@ -0,0 +1,59 @@ +.. _exercise_lambda_magic: + +************************ +lambda and keyword Magic +************************ + +Goals +===== + + + * A bit of lambda + * functions as objects + * keyword evaluation + + +Task +---- + +Write a function that returns a list of n functions, +such that each one, when called, will return the input value, +incremented by an increasing number. + +Use a for loop, ``lambda``, and a keyword argument + +**Extra credit:** + +Do it with a list comprehension, instead of a for loop + +Not clear? here's what you should get... + +Example calling code +--------------------- + +.. code-block:: ipython + + In [96]: the_list = function_builder(4) + ### so the_list should contain n functions (callables) + In [97]: the_list[0](2) + Out[97]: 2 + ## the zeroth element of the list is a function that add 0 + ## to the input, hence called with 2, returns 2 + In [98]: the_list[1](2) + Out[98]: 3 + ## the 1st element of the list is a function that adds 1 + ## to the input value, thus called with 2, returns 3 + In [100]: for f in the_list: + print(f(5)) + .....: + 5 + 6 + 7 + 8 + ### If you loop through them all, and call them, each one adds one more + to the input, 5... i.e. the nth function in the list adds n to the input. + + +See the test code in :download:`test_lambda.py <../solutions/extras/lambda/test_lambda.py>` + + diff --git a/_sources/exercises/list_lab.rst.txt b/_sources/exercises/list_lab.rst.txt new file mode 100644 index 0000000..b366e56 --- /dev/null +++ b/_sources/exercises/list_lab.rst.txt @@ -0,0 +1,116 @@ +.. _exercise_list_lab: + +******** +List Lab +******** + +Learning about lists +==================== + +Inspired by: http://www.upriss.org.uk/python/session5.html + +Goal: +----- + +Learn the basic ins and outs of Python lists. + +Hint +---- + +to query the user for info at the command line, you use: + +.. code-block:: python + + response = input("a prompt for the user > ") + +``response`` will be a string of whatever the user types (until a ). + + +Procedure +--------- + +In the github classroom repo for this exercise, you will find a``list_lab.py`` file (if it not there, you can create it and add it to git yourself). + +The file should be made an executable Python script. That is to say that one +should be able to run the script directly like so: + +.. code-block:: bash + + $ ./list_lab.py + +(At least on OS-X and Linux). + +-- you do that with this command: + +.. code-block:: bash + + $ chmod +x list_lab.py + +(The +x means make this executable). + +The file will also need this on the first line:: + + #!/usr/bin/env python + +This is known as the "she-bang" line -- it tells the shell how to execute that file -- in this case, with ``python`` + +NOTE: on Windows, there is a python launcher which, if everything is configured correctly will look at that line to know you want python if there is more than one python on your system. + +If this doesn't work on Windows, just run the file some other way: + + - ``python list_lab.py`` + - with ``run`` in ipython + - from your IDE or editor is you are using one + + +Make sure the file is added to your clone of the repository and commit changes frequently +while working on the following tasks. When you are done, push your changes to +GitHub and issue a pull request to let the instructors know it is ready for review. + +When the script is run, it should accomplish the following four series of actions: + +Series 1 +-------- + +- Create a list that contains "Apples", "Pears", "Oranges" and "Peaches". +- Display the list (plain old ``print()`` is fine...). +- Ask the user for another fruit and add it to the end of the list. +- Display the list. +- Ask the user for a number and display the number back to the user + and the fruit corresponding to that number (on a 1-is-first basis). Remember that Python uses zero-based indexing, so you will need to correct for that. +- Add another fruit to the beginning of the list using "+" and display the + list. +- Add another fruit to the beginning of the list using ``insert()`` and display the list. +- Display all the fruits that begin with "P", using a for loop. + + +Series 2 +-------- + +Using the list created in series 1 above: + +- Display the list. +- Remove the last fruit from the list. +- Display the list. +- Ask the user for a fruit to delete, find it and delete it. +- (Bonus: Multiply the list times two. Keep asking until a match is found. Once found, delete all occurrences.) + +Series 3 +-------- + +Again, using the list from series 1: + +- Ask the user for input displaying a line like "Do you like apples?" for each fruit in the list (making the fruit all lowercase). +- For each "no", delete that fruit from the list. +- For any answer that is not "yes" or "no", prompt the user to answer + with one of those two values (a while loop is good here) +- Display the list. + +Series 4 +-------- + +Once more, using the list from series 1: + +- Make a new list with the contents of the original, but with all the letters in each item reversed. + +- Delete the last item of the original list. Display the original list and the copy. diff --git a/_sources/exercises/mailroom-decorator.rst.txt b/_sources/exercises/mailroom-decorator.rst.txt new file mode 100644 index 0000000..6900ae7 --- /dev/null +++ b/_sources/exercises/mailroom-decorator.rst.txt @@ -0,0 +1,17 @@ +.. _exercise_mailroom_decorator: + + +Mailroom -- Decoratoring it +=========================== + +We want to know who is making changes to our database, so make a decorator that will ask for a user's name, before making any changes to our donor list. If you want to get really fancy, figure out how to cache the answer, so you don't have to ask more than once per session. + +The Goal +-------- + +Think about which methods will affect the database. Will sending a letter add a user? (Should it? Maybe this is a good time for refactoring.) These are the methods that should get decorated. The decorator should get the user's name. Eventually, we will want to store this permanently, but that is for a different day. However, you might want to figure out how to store it temporarily, so you don't repeatedly ask the same user for their name. This could be done on a timeout basis, or you could assume that the user only changes when the interface is instantiated. + + + + + diff --git a/_sources/exercises/mailroom-fp.rst.txt b/_sources/exercises/mailroom-fp.rst.txt new file mode 100644 index 0000000..6088e99 --- /dev/null +++ b/_sources/exercises/mailroom-fp.rst.txt @@ -0,0 +1,107 @@ +:orphan: + +.. _exercise_mailroom_fp: + +##################### +Mailroom - Functional +##################### + +Making Mailroom Functional + +or: Adding a few functional features :-) + +A new approach +============== + +It was reasonable to build the simple MailRoom program using a single module, a simple data structure, and functions that manipulate that data structure. It was reasonable to refactor Mailroom around classes when it started to become unwieldy with all of the features we wanted to add to it. + +Due to the success of Mailroom and the good PR it has generated with all its thank you letters, the donations are pouring in. This has raised the attention of wealthy philanthropists who are willing to contribute matching funds. Our problem now is to expand Mailroom so that it can run projections and account for matching funds offered by philanthropists. Management is tremendously happy with us and has offered to support our expansion of Mailroom toward a distributed architecture capable of running calculations on matching funds across the donor database in parallel. + +This is a good candidate for a functional programming approach. + +Updating the code +================= + +Preparation +----------- + +Mailroom is our longest running exercise. At this point you may have several versions of the program in separate files or indeed many versions of the program within the same python source file with some sections commented in and others commented out. + +In terms of Mailroom's developmental chronology the original exercise added features over the course of several weeks. A major branch likely occurred when refactoring around object orientation. Another major branch may be occurring here as we investigate functional programming concepts. Is it starting to feel disorganized and a little out of control? Welcome to the world of real software development. + +If you are familiar with git as we have been using it so far, it may be a good time to "branch out" (pun intended), and start using git's branching features. + +On the other hand, if git is still painful for you, feel free to simply make a copy of your currently working mailroom code (and tests) in a new directory. + +IF you want to try branching, there are some notes here: :ref:`git_branching` + +Making new files +---------------- + +If you don't want to mess around with branching: + +You will need to come up with a good naming convention for your source files. You could consider separate source file names all within the same directory like this: + + +.. code-block:: bash + + mailroom/mailroom.py + mailroom/mailroom_oo.py + mailroom/mailroom_fp.py + +Or separate directories for each of your versions like this: + +.. code-block:: bash + + mailroom/mailroom.py + mailroom_oo/mailroom.py + mailroom_fp/mailroom.py + +Regardless of how you decide to manage names and track versions you may also need to decide where to start on this exercise. Should you start from your most recent version which likely involves classes or do you back out to an earlier version before we studied object orientation? Another option is to start again from scratch. The choice is yours. + + +Map, Filter, Reduce +------------------- + +1. Add a new feature to Mailroom using ``map`` so that each donation on record can be doubled, tripled or indeed multiplied by any arbitrary factor based on the whims of philanthropists who would like to support our cause. + + This will require a new function (or method in your donor database class) called ``challenge(factor)`` that takes a multiplier ``(factor)``, and multiplies all the donations of all the donors by the factor. The function returns a NEW donor database, with the new data. + +2. Add a new feature to Mailroom using filter so that donations either above or below a specified dollar amount are included in the map operations of #1 above. + + You can do this by adding ``min_donation`` ``and max_donation`` optional keyword parameters to your ``challenge`` function. You'll want to filter the donations before passing them to map. + +3. Refactor the new features outlined in #1 and #2 above such that they can be used to run projections. Imagine the following scenario. You are an account manager out in the field meeting with philanthropists and talking with them about the many ways they might structure their matching contributions. You would like a feature that could show them, based on past contributions, what their total contribution would become under different scenarios. For instance, based on donations in the current database, show them (a) what their total contribution would come to in dollars if they were to double contributions under $100. And then (b) show them what their total contribution would come to if they were to triple contributions over $50. + + This may require another option in your menu-driven interface. + +Use ``map``, ``filter`` and either ``sum`` or ``reduce`` to accomplish the goals above. + + +Distributed Processing +---------------------- + +Map, filter and reduce lend themselves to parallel, distributed programming. Indeed algorithms that lend themselves gracefully to map/filter solutions tend to lend themselves equally well to parallel processing. We have a name for it: `embarrassingly parallel`_. + +Our next expansion of Mailroom stands on the shoulders of the work you did with map, filter and reduce. We are going to take advantage of the embarrassingly parallel property of map/filter algorithms to run map operations in parallel. + +We have many avenues open to us in terms of setting up the back-end infrastructure for this exercise. In lieu of a supercomputer cluster, which we could "easily" spin up on any of several cloud computing services, we are going to use `IPython Parallel`_. Note that we could spin up IPython Parallel across a cluster of machines, but for the purposes of this exercise running it locally will suffice. + +To get started follow the instructions in this :ref:`ipyparallel_quickstart`. + +Look now at the code you created for the map, filter, reduce exercise above. Focus on the map operations. Your task is to replace the map operations using one of IPython Parallel's `apply`_ functions. For this exercise ``apply_sync()`` may be your most straightforward option. For our purposes Python's built-in ``map`` and IPyParallel's ``apply`` are conceptually identical: they both apply a function to all the elements of a data structure. + +.. _embarrassingly parallel: https://en.wikipedia.org/wiki/Map_(parallel_pattern) +.. _IPython Parallel: https://ipyparallel.readthedocs.io/en/latest/ +.. _apply: http://ipyparallel.readthedocs.io/en/6.0.2/multiengine.html?highlight=apply_sync#calling-python-functions + + +Closures +-------- + +**Do this only after you learn about closures next class!** + +Closures are another functional programming strategy. They allow you to create functions according to parameters that are known only at runtime and not beforehand. + +Your task now is to replace the functions that you have been passing to ``map()`` or ``apply()`` with a closure. + diff --git a/_sources/exercises/mailroom-meta.rst.txt b/_sources/exercises/mailroom-meta.rst.txt new file mode 100644 index 0000000..d668529 --- /dev/null +++ b/_sources/exercises/mailroom-meta.rst.txt @@ -0,0 +1,42 @@ +.. _exercise_mailroom_meta: + + +Mailroom -- metaprogramming it! +=============================== + +So far, your mailroom program may not have any way to save or re-load the donor data. Some of you may have added code to save and load the data in a text file or JSON, but even if you have, you might want a more flexivle and extensible system once your data gets more complicated. + + +JSON +---- + +`JavaScript Object Notation (JSON) `_ is a format borrowed from the Web -- Javascript being the de-facto scripting language in browsers. It is a great format for communicating with browsers, but it has become a common serialization format for many other uses: it is simple, flexible, and human-readable and writable. + +It also maps pretty much directly to (some of) the core Python datatypes: lists, dictionaries, strings, and numbers. + +So JSON is a nice way to save data for a program like Mailman. + +Goal +---- + +Your goal is to use a JSON-save system started in the Metaprogramming Lesson (:ref:`metaprogramming`) to make your model classes saveable and loadable as JSON. + +YOu can download the package here: + +:download:`json_save.zip ` + +And it may also be in your class repo solutions dir: + +``solutions/metaprogramming/json_save/`` + +You can use either the decorator-based or meta-class based approach. + +You may need to extend the JSON-save module a bit to make it work for you! + +When you are done, your class that holds the database of donors and their data should have ``save`` and ``load`` methods that will, naturally, save and load the entire dataset. + +**make sure it's tested!** + + + + diff --git a/_sources/exercises/mailroom-mock.rst.txt b/_sources/exercises/mailroom-mock.rst.txt new file mode 100644 index 0000000..c69a678 --- /dev/null +++ b/_sources/exercises/mailroom-mock.rst.txt @@ -0,0 +1,57 @@ +.. _exercise_mailroom_mocking: + +Mocking Mailroom +================ + +You should now have a nice mailroom program, complete with an object-oriented structure and bundled up into a nice python package. + +And a complete set of unit tests for the logic code. + +But your command line user interface code is likely not tested. It's hard to auto-test user-interaction... + +Your mission is to get mailroom fully tested. + +Start with your object oriented mailroom in a proper python package. + +Coverage +-------- + +First run coverage on your current tests. I like pytest-cov: + +.. code-block:: bash + + $ pip install pytest-cov + + $ pytest --cov=mailroom + +or, to get the nifty html output: + +.. code-block:: bash + + $ pytest --cov=mailroom --cov-report html + +That will result in a pile of html in an ``htmlcov`` directory -- point your browser to the index.html file in there, and click away... + +Once you've run coverage -- add tests to get it up close to 100% on your logic code. + +Fixtures +-------- + +Fixtures are a really good way to make your tests cleaner and more independent. With mailroom, you should have a couple fixtures that set up a donor database with some data in it -- and maybe one or two for ``Donor`` objects. + +Clean up your tests with fixtures -- and keep the coverage up! + +One possible use for a fixture is providing a file to write to for the code that writes files. Or maybe a directory to put the files in, and then it can clean up the dir in teardown. + + +Mocking input +------------- + +Once you have 100% coverage for the logic code -- it's time to test the UI. + +You should be able to use mocking to mock the ``input`` function, and then actually test your user interface code, too. + +Can you get 100% test coverage? + + + diff --git a/_sources/exercises/mailroom-oo.rst.txt b/_sources/exercises/mailroom-oo.rst.txt new file mode 100644 index 0000000..643db12 --- /dev/null +++ b/_sources/exercises/mailroom-oo.rst.txt @@ -0,0 +1,252 @@ +.. _exercise_mailroom_oo: + +########################## +Mailroom - Object Oriented +########################## + +Making Mailroom Object Oriented. + +**Goal:** Refactor the mailroom program using classes to help organize the code. + +The functionality is the same as the earlier mailroom: + +:ref:`exercise_mailroom_part1` + +But this time, we want to use an OO approach to better structure the code to make it more extensible. + +It was quite reasonable to build the simple mailroom program using a +single module, a simple data structure, and functions that manipulate +that data structure. In fact, you've already done that :-) + +But if one were to expand the program with additional functionality, it +would start to get a bit unwieldy and hard to maintain. So it's a pretty good candidate for an object-oriented approach. + +As you design appropriate classes, keep in mind these three guidelines for good code structure: + + +1) Encapsulation: You have a data structure that holds your data, and functions that manipulate that data; you want data and methods "bundled up" in a neat package so that everything that works with that data structure are within one unit. The rest of the code doesn't need to know about the data structure you are using. + +2) Separation of Concerns: The user-interaction code should be cleanly separated from the data handling code. + + https://en.wikipedia.org/wiki/Separation_of_concerns + + There should be no use of the ``input()`` function in the classes that hold the data! Nor any use of ``print()`` -- these are for user interaction, and you want the data handling code to be potentially usable with totally different user interaction -- such as a desktop GUI or Web interface. + +3) As always: **DRY** (Don't Repeat Yourself): Anywhere you see repeated code; refactor it! + + +The Program +=========== + +See: :ref:`exercise_mailroom_part1` to remind yourself what the program needs to do. + + +Guidelines +----------- + +One of the hardest parts of OO design (particularly in Python) is to know how "low" to go with the classes and data structures. In particular, you might have a bit of data collected together (say, a donor's name and donation history). This can be a simple tuple with a few items in it; a dict with those same data available as ``key:value`` pairs; or a class, with class attributes (and, possibly, methods). + +There are no hard and fast rules, but here are some guidelines: + +For this simple problem, simple tuples could work fine. However, in order for the code to be more flexible in the future: for example, if new "fields" were added to the donor object, it's probably better to use a more structured data type, so you don't have to worry about changing the order or number of fields. + +So now you have to think about using a dict or class. Again for flexibility, a dict is a bit easier; you can add fields to it very easily. However, with a class, you can build some functionality in there, too. This is where encapsulation comes in. For instance, one thing you might want to do is get the total of all donations a donor has made in the past. If you add a method to compute that (or a property!), then the rest of the code doesn't need to know how the donations are stored. + +Consider ``data[0]`` vs ``data["name"]`` vs ``data.name``. Which one is more readable? Keep in mind that another benefit of using OO for data encapsulation is ability of modern IDEs to provide auto-completion, which reduces the number of bugs and helps to produce code faster. + +Below are more detailed suggestions on breaking down your existing code into multiple modules that will be part of a single mailroom program. + + +Modules and Classes +................... + +You may organize your code to your preference and keep it simple by having all of the code in a single file. + +Optionally, you could organize your code into modules, which helps to keep code organized and re-usable. + +What is a module? A module is a python file with a collection of code that can be imported into other python files. + +Modules can contain functions, classes, and even variables (constants). + +Here is an example file structure for ``mailroom_oo`` package that contains 3 modules: + +.. code-block:: bash + + └── mailroom_oo + ├── cli_main.py + ├── donor_models.py + └── test_mailroom_oo.py + +The module ``donor_models.py`` can contain the ``Donor`` and ``DonorCollection`` classes. + +The module ``cli_main.py`` would include all of your user interaction functions and main program flow. + +``Donor`` Class +............... + +**Class responsible for donor data encapsulation** + +This class will hold all the information about a *single* donor, and have attributes, properties, and methods to provide access to the donor-specific information that is needed. +Any code that only accesses information about a single donor should be part of this class. + + +``DonorCollection`` Class +......................... + +**Class responsible for donor collection data encapsulation** + +This class will hold all of the donor objects, as well as methods to add a new donor, search for a given donor, etc. If you want a way to save and re-load your data, this class would hold that method, too. + +Your class for the collection of donors will also hold the code that generates reports about multiple donors. + +In short: if the functionality involves more than one donor -- it belongs in this class. + +Note that the ``DonorCollection`` class should be holding, and working with, ``Donor`` objects -- it should NOT work directly with a list of donations, etc. + +The main data structure in your class can be a dictionary with a key as donor name and value as donor object: + + +.. code-block:: python + class DonorCollection: + def __init__(self, *donors): + self.donors = {obj.name: obj for obj in donors} + + +this design allows you to quickly look up donor by their name and get a donor object instance to work with. + +Another option is to simply use a list of donor objects. You get to choose which you think is more appropriate. + +Remember that you should use `self.donors` attribute any time you want to work with data about a single donor, most of your methods in this class will utilize it in some way. This is really what classes are desined for. + +**Examples:** + +Generating a thank you letter to a donor only requires knowledge of that one donor -- so that code belongs in the ``Donor`` class. + +Generating a report about all the donors requires knowledge of all the donors, so that code belongs in the ``DonorCollection`` class. + +Hint: +You've previously sorted simple data structures like list and dictionaries, but here we're dealing with objects - not to worry that is a really simple thing to do with python! +You can use `operator.attrgetter` with a sorted function (review python docs for usage documentation). + +Command Line Interface +....................... + +**Module responsible for main program flow (CLI - Command Line Interface)** + +Let's call this module ``cli_main.py`` to represent the entry point for the mailroom program. +This module will be using the classes we defined: ``Donor`` and ``DonorCollection``. +It will also handle interaction with the user via the ``input`` function calls that gather user input and to provide the output to the console. + +What should go into this module? + +A set of user-interaction menu functions -- to handle each of the modes of the program. + +These will include ``input()`` function calls to gather user input, and ``print()`` functions to print results to console. + +.. note:: Console print statements don't belong in your data classes. So for features such as "send letters," in which we are simply printing instead of "sending", the data class methods should return a string, and let the UI code do the printing. This will mean there may be very simple functions in the UI code that simply call a method and print the results -- but that does keep flexibility for other ways of handling user interaction. + +.. rubric:: Why is this separation of data and method so important? + +The idea here is that we should be able to fairly easy replace this CLI program with a different type of interface, +such as a GUI (Graphical User Interface), without having to make any changes to our data classes. +If that was the case, then you would implement the GUI elements and use your data classes the same way as they are used in CLI. + + +Test-Driven Development +----------------------- + +At this point we have done a great job refactoring the more complex code out of data-holding classes and we are left with simple classes that are more straightforward to unit test. As you build your classes, update the tests you already have to the logic code to the new API. Ideally, update the tests first, then the code. + +The ``Donor`` and ``DonorCollection`` classes should now have 100 percent code coverage, which means that every line of code in your ``donor_models.py`` file will be run at least once when your tests are run. + +For the moment, don't worry about testing most of the command line interface code. That requires simulating user input, which is an advanced testing topic. But you can (hopefully) see some of the benefits of separating the user-interaction code from the logic code; your logic code is much easier to test with no user-interaction involved. + +.. rubric:: refactoring non-OO code + +In this case, you already have working code without an OO structure. You should be able to re-use a fair bit of your existing code. +However, you should still start with the OO structure/design. +That is, rather than take a non-OO function and try to make it a method of a class, decide what method you need, and what it's API should be, and then see if you have code you can use to fill in that function. + +You should expect to re-use a lot of the command line interface code, while refactoring most of the logic code. + +If you are not sure at the start what functionality you data classes will need, you can start with the CLI code, and as you find the need for a function, add it to your data classes (after writing a test first, of course). + + +Exercise Guidelines +=================== + +OO mailroom is the final project for the class. + +So this is your chance to really do things "right". Strive to make this code as good, by every definition, as you can. + +With that in mind: + +Functionality +------------- + +* The logic is correct -- i.e. the program works :-) + +* The logic is robust -- you are handling obvious expected errors reasonably: + + - User inputting a non-number as a donation + + - Trying to make a negative donation + + - User getting capitalization or spacing or ??? wrong with a name. + + - Maybe add logic where you tell them that the name is not in the DB, and do they want to create it, rather than simply creating a new record for a typo in a donor name. + +.. rubric:: Code structure + +* Classes should have clear purpose and encapsulation: only the code within a class should know exactly how the data are stored, for instance. + +* Anything that only needs to know about one donor should be in the ``Donor`` class + +* Anything that needs to know about the collection should be in a ``DonorCollection`` class. + +* Any user interaction should be outside the "logic" code. (Sometimes called the "Model", or "Business logic") + + - You should be able to re-use all the logic code with a different UI -- Web App, GUI, etc. + + - There should be no ``input()`` or ``print`` functions in the logic code. + + - The logic code should be 100% testable (without mocking input() or any fancy stuff like that) + +.. rubric:: Testing + +* All logic code should be tested. + +* Tests should be isolated to test one thing each + +* Tests should (reasonably) check for handling of weird input. + +* Tests should be isolated -- that is, they will work if run by themselves, and in any order. + + - This means they should not rely on any global state. + + - you'll probably find this easier with a well structured OO approach -- that is, you can test an individual donor functionality without knowing about the rest of the donors. + + +.. rubric:: The "soft" stuff: + +Style: + - conform to PEP8! (or another consistent style) + + - You can use 95 or some other reasonable number for line length + +Docstrings: + Functions and classes should all have good docstrings. They can be very short if the function does something simple. + +Naming: + All classes, functions, methods, attributes, variables should have appropriate names: meaningful, but not too detailed. + +Extra Ideas: +------------ + +In case you are bored -- what features can you add? + +* How about an html report using your html_render code? + +* Fancier reporting + +* The sky's the limit diff --git a/_sources/exercises/mailroom-part1.rst.txt b/_sources/exercises/mailroom-part1.rst.txt new file mode 100644 index 0000000..53866fc --- /dev/null +++ b/_sources/exercises/mailroom-part1.rst.txt @@ -0,0 +1,98 @@ +.. _exercise_mailroom_part1: + + +Mailroom Part 1 +================ + + +**Overall Assignment Structure** + +This is the first in a four-part that will make use of your Python programming skills as you develop them during this course. You will start work on the program in this assignment (Part 1) that will will submit for. Lesson 3. You will submit Part 2 for Lesson 4, Part 3 for Lesson 5, and Part 4 for Lesson 6. + +This progressive work will give you a strong foundation for success in the final project, a Mailroom program using object-oriented programming in lesson 9. + + +Overall Program Goal: +--------------------- + +You work in the mail room at a local charity. Part of your job is to write +incredibly boring, repetitive emails thanking your donors for their generous +gifts. You are tired of doing this over and over again, so you've decided to +let Python help you out of a jam and do your work for you. + + +The Program: Part 1 +------------------- + +Write a small command-line script called ``mailroom.py``. This script should be executable. The script should accomplish the following goals: + +* It should have a data structure that holds a list of your donors and a + history of the amounts they have donated. This structure should be populated + at first with at least five donors, with between 1 and 3 donations each. You can store that data structure in the global namespace. + +* The script should prompt the user (you) to choose from a menu of 3 actions: + "Send a Thank You", "Create a Report" or "quit". + +Send a Thank You +------------------- + +* If the user (you) selects "Send a Thank You" option, prompt for a Full Name. + + * If the user types ``list`` show them a list of the donor names and re-prompt. + * If the user types a name not in the list, add that name to the data structure and use it. + * If the user types a name in the list, use it. +* Once a name has been selected, prompt for a donation amount. + + * Convert the amount into a number; it is OK at this point for the program to crash if someone types a bogus amount. + * Add that amount to the donation history of the selected user. + +* Finally, use string formatting to compose an email thanking the donor for their generous donation. Print the email to the terminal and return to the original prompt. + +It is fine (for now) for the program not to store the names of the new donors that had been added, in other words, to forget new donors once the script quits running. + +Create a Report +----------------- + +* If the user (you) selected "Create a Report," print a list of your donors, + sorted by total historical donation amount. + + - Include Donor Name, total donated, number of donations, and average donation amount as values in each row. You do not need to print out all of each donor's donations, just the summary info. + - Using string formatting, format the output rows as nicely as possible. The end result should be tabular (values in each column should align with those above and below). + - After printing this report, return to the original prompt. + +* At any point, the user should be able to quit their current task and return + to the original prompt. + +* From the original prompt, the user should be able to quit the script cleanly. + + +Your report should look something like this:: + + Donor Name | Total Given | Num Gifts | Average Gift + ------------------------------------------------------------------ + William Gates, III $ 653784.49 2 $ 326892.24 + Mark Zuckerberg $ 16396.10 3 $ 5465.37 + Jeff Bezos $ 877.33 1 $ 877.33 + Paul Allen $ 708.42 3 $ 236.14 + +Guidelines +---------- + +First, factor your script into separate functions. Each of the above +tasks can be accomplished by a series of steps. Write discreet functions +that accomplish individual steps and call them. + +Second, use loops to control the logical flow of your program. Interactive +programs are a classic use case for the ``while`` loop. + +Of course, ``input()`` will be useful here. + +Put the functions you write into the script at the top. + +Put your main interaction into an ``if __name__ == '__main__'`` block. + +Finally, for Part 1 use only functions and the basic Python data types you've learned +about so far in Lessons 1-3. There is no need to go any farther than that for this assignment. + +If you're having a hard time getting started, or need some pointers, you should read the tutorial here: :ref:`exercise_mailroom_part1_tutorial` + diff --git a/_sources/exercises/mailroom-part2.rst.txt b/_sources/exercises/mailroom-part2.rst.txt new file mode 100644 index 0000000..52250ec --- /dev/null +++ b/_sources/exercises/mailroom-part2.rst.txt @@ -0,0 +1,100 @@ +.. _exercise_mailroom_part2_dict_files: + + +Mailroom Part 2 +================= + +**Incorporate file writing and dictionary use.** + +Use dicts where appropriate. +---------------------------- + +Part 1 of this assignment used these basic data types: numbers, strings, lists and tuples. + +However, using dictionaries, covered in Lesson 4, will let you re-write your program a bit more simply and efficiently. + +Update your mailroom program to: + + - Use dicts where appropriate. + + - See if you can use a dict to switch between the user's selections. + + - See if you can use a dict to switch between the users selections. + see :ref:`dict_as_switch` for what this means. + + - Convert your main donor data structure to be a dict. + + - Try to use a dict and the ``.format()`` method to produce the letter as one + big template, rather than building up a big string that produces the letter in parts. + + +Example: + +.. code-block:: ipython + + In [3]: d + Out[3]: {'first_name': 'Chris', 'last_name': 'Barker'} + + + In [5]: "My name is {first_name} {last_name}".format(**d) + Out[5]: 'My name is Chris Barker' + +Don't worry too much about the ``**``. We'll get into the details later, but for now it means, more or less, "pass this whole dict in as a bunch of keyword arguments." + + +Update mailroom with file writing. +---------------------------------- + +**Goal: Write a full set of letters to all donors to individual files on disk.** + +In the first version of mailroom, you generated a letter to a donor who had just made a new donation, and printed it to the screen. + +In this version of your program, add a function (and a menu item to invoke it), that goes through all the donors in your donor data structure, generates a thank you letter for each donor, and writes each letter to disk as a text file. + +Your main menu may look something like:: + + Choose an action: + + 1 - Send a Thank You to a single donor. + 2 - Create a Report. + 3 - Send letters to all donors. + 4 - Quit + +The letters should each get a unique file name -- you can keep it really simple and just use the donor's name or add a date timestamp for additional uniqueness. + +You want to avoid specifying a hardcoded file path when creating the files, for example don't to this: + +.. code-block:: python + + open("/home/users/bob/dev/mailroom/thank_you.txt", "w") + + +Doing so will prevent other users from running the program as it will fail to find your path. Instead, you can create files in the current working directory or you can use a temporary directory. +To identify a temporary directory you can use a handy function like `tempfile.gettempdir() `_ which is also OS agnostic (meaning it can handle temp directory differences between different operating systems). + +After running the "send letters to everyone" option, you should see some new files in the directory. There should be a file for each donor in the database, in this case 4. + +After choosing action (3) above, using my example database, I get these files:: + + Jeff_Bezos.txt + Mark_Zuckerberg.txt + Paul_Allen.txt + William_Gates_III.txt + +(If you want to get really fancy, ask the user for a directory name to write to!) + +An example file content looks like this:: + + Dear Jeff Bezos, + + Thank you for your very kind donation of $877.33. + + It will be put to very good use. + + Sincerely, + -The Team + +Feel free to enhance your letter template with some more information about past generosity, etc.... + +The idea is to require you to structure your code so that you can write the same letter to the screen or to disk (and thus anywhere else) and also exercise a bit of file writing. Remember to review the `with `_ statement as it is the preferred method when working with files. + diff --git a/_sources/exercises/mailroom-part3.rst.txt b/_sources/exercises/mailroom-part3.rst.txt new file mode 100644 index 0000000..71701d2 --- /dev/null +++ b/_sources/exercises/mailroom-part3.rst.txt @@ -0,0 +1,47 @@ +.. _exercise_mailroom_part3_exceptions: + + +Mailroom Part 3 +================= + +**Improve your mailroom by adding exceptions and comprehensions.** + +Exceptions +---------- + +Now that you've learned about exception handling, you can update your code to handle errors better, such as when a user inputs bad data. + + +Comprehensions +-------------- + +Can you use comprehensions to clean up your code a bit? + +Note: you may be tempted to replace loops like this: + +.. code-block:: python + + for donor in donors: + print(donor) + +with + +.. code-block:: python + + [print(donor) for donor in donors] + + +That's not the intended use of comprehensions. Because ``print`` function does not return a value, this code will allocate a space for an "empty" result list filled with None values: + + >>> [print(donor) for donor in donors] + jane + wendy + [None, None] + >>> + +List comprehensions are designed for a very specific use case: + +*Processing a sequence of items to create another sequence.* + +They are not designed to replace all for loops. + diff --git a/_sources/exercises/mailroom-part4.rst.txt b/_sources/exercises/mailroom-part4.rst.txt new file mode 100644 index 0000000..88f928d --- /dev/null +++ b/_sources/exercises/mailroom-part4.rst.txt @@ -0,0 +1,88 @@ +.. _exercise_mailroom_part4_testing: + +Mailroom Part 4 +================= + +**Add a full suite of unit tests.** + +"Full suite" means all the code is tested. In practice, it's very hard to test the user interaction, but you can test everything else. Therefore you should make sure that there is as little logic and untested code in the user interaction portion of the program as possible. + +This is a big step; you may find that your code is hard to test. If that's the case, it's a good sign that you *should* refactor your code. + +I like to say: "If it's hard to test, it's not well structured." + +Put in the tests **before** you make the other changes below. That's much of the point of tests. You can know that you haven't broken anything when you refactor! + +.. Confusing last sentence. Do you mean that if you test the components beforehand, you will know that the components work before refacotring so that any breakage that occurs after refacatoring will have been caused by the refactoring? + +Guidelines +---------- + +Here are some suggestions on what should be refactored in your mailroom code. + +As mentioned above, testing user interaction code is harder (code with ``print`` and ``input`` functions) then testing the rest of your code. Testing user interaction code requires more advanced unit testing methodologies that will be revisited in future courses. Therefore, you should refactor your code so that the user interaction code contains as little business logic as possible. It should only interact with the user either by asking them for input or by responding to their request to print out data. Separating business logic from user interaction code is a good practice in general and we will come back to this concept in later lessons. + +The refactor in this lesson will allow you to unit test functions with business logic. + +We will go over the components that should be refactored so that you are able to unit test your mailroom. After the refactor, your code should improve and be better modularized. If that's not the case then maybe you should revisit your refactor approach. + +For unit testing framework you should use `pytest `_; it has a simple interface and rich features. + +Your code should have 3 main features so far: + +* Send a thank you (adds a new donor or updates existing donor info) +* Create a report +* Send letters (creates files) + + +Send Thank You +............... + +Even though every mailroom implementation will be unique, most likely this function will require a significant refactor for most of you. +You can break up the code into components that handle user flow and data manipulation logic. Your unit tests should test the data manipulation logic code: generating thank you text, adding or updating donors, and listing donors. + + +Create Report +............. + +This function should only need slight modification. Split up user presentation (``print`` function calls) and data logic (actual creating of rows). +Your data logic function can either return the report string already formatted or return a list of formatted rows that can be joined and printed in the user presentation function. +Then you can write a unit test for your data logic function. + +Example: + +.. code-block:: python + + def display_report(): + for row in get_report(): + print(row) + + + +Here you would write a unit test for ``get_report`` function. + +Send Letters +............ + +This function should require very little or no change to make it unit-testable. +The unit test can assert that a file is created per donor entry (hint: ``os.path`` module), and that the file content contains text as expected. + +Note that you should test the file creation separately from testing the file content (that the correct text being generated). That way you don't need to read each file generated to know it contains correct text. So the function that generates the text should be separate from the function that writes the file. + +.. entirely possible I made errors here. Not sure if the test is that the file contains text or that the file contains the correct text. + +For example: + +.. code-block:: python + + def get_letter_text(name): + """Get letter text for file content""" + return f"{name}, thanks a lot!" + + + def test_get_letter_text(): + expected = "Frank, thanks a lot!" + assert get_letter_text("Frank") == expected + + + diff --git a/_sources/exercises/mailroom-pkg.rst.txt b/_sources/exercises/mailroom-pkg.rst.txt new file mode 100644 index 0000000..947120b --- /dev/null +++ b/_sources/exercises/mailroom-pkg.rst.txt @@ -0,0 +1,105 @@ +.. _exercise_mailroom_package: + +Mailroom -- as a Python Package +=============================== + +The mailroom program, particularly the Object Oriented version, is a pretty complete system -- if you wanted to make it available for others to test snd run, making a "proper" python package is a great idea. + +Code Structure +-------------- + +Start with an Object Oriented mailroom. + +It should already be structured with the "logic" code distinct from the user interface (yes, a command line *is* a user interface). But you may have it all in one file. This isn't *too* bad for such a small program, but as a program grows, you really want to keep things separate, in a well organized package. + +The first step is to re-structure your code into separate files: + + - one (or more) for the logic code (the Donor and DonorDB classes) + - one for the user-interface code + - one (or more) for tests. + +Then you are going to want a top-level script file that does little but import the ui code and run a ``main()`` function. + +Making the Package +------------------ + +Put all these in a python package structure, something like this:: + + mailroom + setup.py + mailroom\ + __init__.py + model.py + ui + tests\ + test_model.py + test_ui.py + bin + mailroom.py + +You will need to import the logic code from model.py in the ui code in order to use it. You can wait until you learn about mocking to write the code in test_ui.py + +Now write your ``setup.py`` to support your package. + +Making the top-level script runnable +------------------------------------ + +To get the script installed you have two options. I prefer the more straightforward one, `the scripts keyword argument `_ + +But if you want to get fancy, you can use ``setuptools``'s `entry points `_ + + +Including data files +-------------------- + +NOTE: If you have a database of donors in a file that you load, then that should go in the package as well. Probably inside the mailroom dir, in a ``data`` dir or similar. Then you need to add it to your setup.py to make sure it gets copied into the installed package. + +There are a few ways to do this: + +http://setuptools.readthedocs.io/en/latest/setuptools.html#including-data-files + +I personally like the simiplest one with the least magic: + +`include_package_data=True `_ + +Then you'll get the data file included in the package in the same place. + +Now you'll need to write your code to find that data file. You can do that by using the __file__ module attribute -- then the location of the data file will be relative to the __file__ that your code is in. A little massaging with a ``pathlib.Path`` should do it. + +Testing your Package +-------------------- + +When you are done, you should be able to both install your package fully: + +.. code-block:: bash + + $ pip install . + +or in "editable" mode: + +.. code-block:: bash + + $ pip install -e . + +When that is done, you should be able to run the top-level script from anywhere: + +.. code-block:: bash + + $ mailroom.py + +and run the test from within the package: + +.. code-block:: bash + + $ pytest --pyargs mailroom + + + + + + + + + + + diff --git a/_sources/exercises/mailroom/mailroom-decorator.rst.txt b/_sources/exercises/mailroom/mailroom-decorator.rst.txt new file mode 100644 index 0000000..ba496b2 --- /dev/null +++ b/_sources/exercises/mailroom/mailroom-decorator.rst.txt @@ -0,0 +1,18 @@ + +.. _exercise_mailroom_decorator: + + +Mailroom -- Decoratoring it +=========================== + +We want to know who is making changes to our database, so make a decorator that will ask for a user's name, before making any changes to our donor list. If you want to get really fancy, figure out how to cache the answer, so you don't have to ask more than once per session. + +The Goal +-------- + +Think about which methods will affect the database. Will sending a letter add a user? (Should it? Maybe this is a good time for refactoring.) These are the methods that should get decorated. The decorator should get the user's name. Eventually, we will want to store this permanently, but that is for a different day. However, you might want to figure out how to store it temporarily, so you don't repeatedly ask the same user for their name. This could be done on a timeout basis, or you could assume that the user only changes when the interface is instantiated. + + + + + diff --git a/_sources/exercises/mailroom/mailroom-fp.rst.txt b/_sources/exercises/mailroom/mailroom-fp.rst.txt new file mode 100644 index 0000000..6088e99 --- /dev/null +++ b/_sources/exercises/mailroom/mailroom-fp.rst.txt @@ -0,0 +1,107 @@ +:orphan: + +.. _exercise_mailroom_fp: + +##################### +Mailroom - Functional +##################### + +Making Mailroom Functional + +or: Adding a few functional features :-) + +A new approach +============== + +It was reasonable to build the simple MailRoom program using a single module, a simple data structure, and functions that manipulate that data structure. It was reasonable to refactor Mailroom around classes when it started to become unwieldy with all of the features we wanted to add to it. + +Due to the success of Mailroom and the good PR it has generated with all its thank you letters, the donations are pouring in. This has raised the attention of wealthy philanthropists who are willing to contribute matching funds. Our problem now is to expand Mailroom so that it can run projections and account for matching funds offered by philanthropists. Management is tremendously happy with us and has offered to support our expansion of Mailroom toward a distributed architecture capable of running calculations on matching funds across the donor database in parallel. + +This is a good candidate for a functional programming approach. + +Updating the code +================= + +Preparation +----------- + +Mailroom is our longest running exercise. At this point you may have several versions of the program in separate files or indeed many versions of the program within the same python source file with some sections commented in and others commented out. + +In terms of Mailroom's developmental chronology the original exercise added features over the course of several weeks. A major branch likely occurred when refactoring around object orientation. Another major branch may be occurring here as we investigate functional programming concepts. Is it starting to feel disorganized and a little out of control? Welcome to the world of real software development. + +If you are familiar with git as we have been using it so far, it may be a good time to "branch out" (pun intended), and start using git's branching features. + +On the other hand, if git is still painful for you, feel free to simply make a copy of your currently working mailroom code (and tests) in a new directory. + +IF you want to try branching, there are some notes here: :ref:`git_branching` + +Making new files +---------------- + +If you don't want to mess around with branching: + +You will need to come up with a good naming convention for your source files. You could consider separate source file names all within the same directory like this: + + +.. code-block:: bash + + mailroom/mailroom.py + mailroom/mailroom_oo.py + mailroom/mailroom_fp.py + +Or separate directories for each of your versions like this: + +.. code-block:: bash + + mailroom/mailroom.py + mailroom_oo/mailroom.py + mailroom_fp/mailroom.py + +Regardless of how you decide to manage names and track versions you may also need to decide where to start on this exercise. Should you start from your most recent version which likely involves classes or do you back out to an earlier version before we studied object orientation? Another option is to start again from scratch. The choice is yours. + + +Map, Filter, Reduce +------------------- + +1. Add a new feature to Mailroom using ``map`` so that each donation on record can be doubled, tripled or indeed multiplied by any arbitrary factor based on the whims of philanthropists who would like to support our cause. + + This will require a new function (or method in your donor database class) called ``challenge(factor)`` that takes a multiplier ``(factor)``, and multiplies all the donations of all the donors by the factor. The function returns a NEW donor database, with the new data. + +2. Add a new feature to Mailroom using filter so that donations either above or below a specified dollar amount are included in the map operations of #1 above. + + You can do this by adding ``min_donation`` ``and max_donation`` optional keyword parameters to your ``challenge`` function. You'll want to filter the donations before passing them to map. + +3. Refactor the new features outlined in #1 and #2 above such that they can be used to run projections. Imagine the following scenario. You are an account manager out in the field meeting with philanthropists and talking with them about the many ways they might structure their matching contributions. You would like a feature that could show them, based on past contributions, what their total contribution would become under different scenarios. For instance, based on donations in the current database, show them (a) what their total contribution would come to in dollars if they were to double contributions under $100. And then (b) show them what their total contribution would come to if they were to triple contributions over $50. + + This may require another option in your menu-driven interface. + +Use ``map``, ``filter`` and either ``sum`` or ``reduce`` to accomplish the goals above. + + +Distributed Processing +---------------------- + +Map, filter and reduce lend themselves to parallel, distributed programming. Indeed algorithms that lend themselves gracefully to map/filter solutions tend to lend themselves equally well to parallel processing. We have a name for it: `embarrassingly parallel`_. + +Our next expansion of Mailroom stands on the shoulders of the work you did with map, filter and reduce. We are going to take advantage of the embarrassingly parallel property of map/filter algorithms to run map operations in parallel. + +We have many avenues open to us in terms of setting up the back-end infrastructure for this exercise. In lieu of a supercomputer cluster, which we could "easily" spin up on any of several cloud computing services, we are going to use `IPython Parallel`_. Note that we could spin up IPython Parallel across a cluster of machines, but for the purposes of this exercise running it locally will suffice. + +To get started follow the instructions in this :ref:`ipyparallel_quickstart`. + +Look now at the code you created for the map, filter, reduce exercise above. Focus on the map operations. Your task is to replace the map operations using one of IPython Parallel's `apply`_ functions. For this exercise ``apply_sync()`` may be your most straightforward option. For our purposes Python's built-in ``map`` and IPyParallel's ``apply`` are conceptually identical: they both apply a function to all the elements of a data structure. + +.. _embarrassingly parallel: https://en.wikipedia.org/wiki/Map_(parallel_pattern) +.. _IPython Parallel: https://ipyparallel.readthedocs.io/en/latest/ +.. _apply: http://ipyparallel.readthedocs.io/en/6.0.2/multiengine.html?highlight=apply_sync#calling-python-functions + + +Closures +-------- + +**Do this only after you learn about closures next class!** + +Closures are another functional programming strategy. They allow you to create functions according to parameters that are known only at runtime and not beforehand. + +Your task now is to replace the functions that you have been passing to ``map()`` or ``apply()`` with a closure. + diff --git a/_sources/exercises/mailroom/mailroom-meta.rst.txt b/_sources/exercises/mailroom/mailroom-meta.rst.txt new file mode 100644 index 0000000..08ab46b --- /dev/null +++ b/_sources/exercises/mailroom/mailroom-meta.rst.txt @@ -0,0 +1,42 @@ +.. _exercise_mailroom_meta: + + +Mailroom -- metaprogramming it! +=============================== + +So far, your mailroom program may not have any way to save or re-load the donor data. Some of you may have added code to save and load the data in a text file or JSON, but even if you have, you might want a more flexible and extensible system once your data gets more complicated. + + +JSON +---- + +`JavaScript Object Notation (JSON) `_ is a format borrowed from the Web -- Javascript being the de-facto scripting language in browsers. It is a great format for communicating with browsers, but it has become a common serialization format for many other uses: it is simple, flexible, and human-readable and writable. + +It also maps pretty much directly to (some of) the core Python datatypes: lists, dictionaries, strings, and numbers. + +So JSON is a nice way to save data for a program like mailroom. + +Goal +---- + +Your goal is to use a JSON-save system started in the Metaprogramming Lesson (:ref:`metaprogramming`) to make your model classes saveable and loadable as JSON. + +You can download the package here: + +:download:`json_save.zip ` + +And it may also be in your class repo solutions dir: + +``solutions/metaprogramming/json_save/`` + +You can use either the decorator-based or meta-class based approach. + +You may need to extend the JSON-save module a bit to make it work for you! + +When you are done, your class that holds the database of donors and their data should have ``save`` and ``load`` methods that will, naturally, save and load the entire dataset. + +**Make Sure it's Tested!** + + + + diff --git a/_sources/exercises/mailroom/mailroom-mock.rst.txt b/_sources/exercises/mailroom/mailroom-mock.rst.txt new file mode 100644 index 0000000..fc2a575 --- /dev/null +++ b/_sources/exercises/mailroom/mailroom-mock.rst.txt @@ -0,0 +1,57 @@ +.. _exercise_mailroom_mocking: + +Mocking Mailroom +================ + +You should now have a nice mailroom program, complete with an object-oriented structure and bundled up into a nice python package. + +And a complete set of unit tests for the logic code. + +But your command line user interface code is likely not tested. It's hard to auto-test user-interaction... + +Your mission is to get mailroom fully tested. + +Start with your object oriented mailroom in a proper python package. + +Coverage +-------- + +First run coverage on your current tests. I like pytest-cov: + +.. code-block:: bash + + $ pip install pytest-cov + + $ pytest --cov=mailroom + +or, to get the nifty html output: + +.. code-block:: bash + + $ pytest --cov=mailroom --cov-report html + +That will result in a pile of html in an ``htmlcov`` directory -- point your browser to the index.html file in there, and click away... + +Once you've run coverage -- add tests to get it up close to 100% on your logic code, if it's not there already. + +Fixtures +-------- + +Fixtures are a really good way to make your tests cleaner and more independent. With mailroom, you should have a couple fixtures that set up a donor database with some data in it -- and maybe one or two for ``Donor`` objects. + +Clean up your tests with fixtures -- and keep the coverage up! + +One possible use for a fixture is providing a file to write to for the code that writes files. Or maybe a directory to put the files in, and then it can clean up the dir in teardown. + + +Mocking input +------------- + +Once you have 100% coverage for the logic code -- it's time to test the UI. + +You should be able to use mocking to mock the ``input`` function, and then actually test your user interface code, too. + +Can you get 100% test coverage? + + + diff --git a/_sources/exercises/mailroom/mailroom-oo.rst.txt b/_sources/exercises/mailroom/mailroom-oo.rst.txt new file mode 100644 index 0000000..5484002 --- /dev/null +++ b/_sources/exercises/mailroom/mailroom-oo.rst.txt @@ -0,0 +1,273 @@ +.. _exercise_mailroom_oo: + +########################## +Mailroom - Object Oriented +########################## + +Making Mailroom Object Oriented. + +**Goal:** Refactor the mailroom program using classes to help organize the code. + +The functionality is the same as the earlier mailroom(s). + +But this time, we want to use an OO approach to better structure the code to make it more extensible. + +It was quite reasonable to build the simple mailroom program using a +single module, a simple data structure, and functions that manipulate +that data structure. In fact, you've already done that :-) + +But if one were to expand the program with additional functionality, it +would start to get a bit unwieldy and hard to maintain. So it's a pretty good candidate for an object-oriented approach. + +As you design appropriate classes, keep in mind these three guidelines for good code structure: + + +1) Encapsulation: You have a data structure that holds your data, and functions that manipulate that data; you want data and methods "bundled up" in a neat package so that everything that works with that data structure are within one unit. The rest of the code doesn't need to know about the data structure you are using. + +2) Separation of Concerns: The user-interaction code should be cleanly separated from the data handling code. + + https://en.wikipedia.org/wiki/Separation_of_concerns + + There should be no use of the ``input()`` function in the classes that hold the data! Nor any use of ``print()`` -- these are for user interaction, and you want the data handling code to be potentially usable with totally different user interaction -- such as a desktop GUI or Web interface. + +In the spirit of separation of concerns, you also want keep your Command Line Interface code in a separate file(s), as they already should be in your mailroom-as-a-package version. In fact, you should be able to keep the structure of your package pretty much the same -- simply replacing the model code with classes. + + +3) As always: **DRY** (Don't Repeat Yourself): Anywhere you see repeated code; refactor it! + + +The Program +=========== + +See: :ref:`exercise_mailroom_part1` to remind yourself what the program needs to do. + + +Guidelines +----------- + +One of the hardest parts of OO design (particularly in Python) is to know how "low" to go with the classes and data structures. In particular, you might have a bit of data collected together (say, a donor's name and donation history). This can be a simple tuple with a few items in it; a dict with those same data available as ``key:value`` pairs; or a class, with class attributes (and, possibly, methods). + +There are no hard and fast rules, but here are some guidelines: + +For this simple problem, simple tuples could work fine. However, in order for the code to be more flexible in the future: for example, if new "fields" were added to the donor object, it's probably better to use a more structured data type, so you don't have to worry about changing the order or number of fields. + +So now you have to think about using a dict or class. Again for flexibility, a dict is a bit easier; you can add fields to it very easily. However, with a class, you can build some functionality in there, too. This is where encapsulation comes in. For instance, one thing you might want to do is get the total of all donations a donor has made in the past. If you add a method to compute that (or a property!), then the rest of the code doesn't need to know how the donations are stored. + +Consider ``data[0]`` (stored in a tuple) vs ``data["name"]`` (stored in a dict) vs ``data.name`` (a class). +Which one is more readable? +Keep in mind that another benefit of using OO for data encapsulation is ability of modern IDEs to provide auto-completion, which reduces the number of bugs and helps to produce code faster. + +Another way to think about it is "data" vs "code" -- dicts are for data, classes are for code. +Sometimes it's not totally clear which is which, but if you think about how static is, that can be be a guide. +In this example, every donor is going to have a name, and a donation history, and maybe in the future a bunch of other information (address, email, phone number ....). +Whatever it is, every donor will have the same set -- so it's good to use code to manage it. + +Below are more detailed suggestions on breaking down your existing code into multiple modules that will be part of a single mailroom program. + + +Modules and Classes +................... + +You may organize your code to your preference and keep it simple by having all of the model code in a single file. But you should at least separate the code that manipulates data (the "model" code) from the command line interface code, as you did for mailroom-as-a-package. + +Optionally, you could further organize your model code into separate modules: maybe one for the Donor object, one for the DonorCollection object. + +What is a module? A module is a python file with a collection of code that can be imported into other python files. + +Modules can contain functions, classes, and even variables (constants). + +Here is an example file structure for ``mailroom_oo`` package -- it should be similar to what you already have: + +:: + + └── mailroom + ├── __init__.py + ├── cli.py + ├── model.py + └── tests + ├── __init__.py + ├── test_cli.py + └── test_model.py + + +The module ``model.py`` can contain the ``Donor`` and ``DonorCollection`` classes. + +The module ``cli.py`` would include all of your user interaction functions and main program flow (and a ``main()`` function that can be called to start up the program (and used as an entry_point). + + +``Donor`` Class +............... + +**Class responsible for donor data encapsulation** + +This class will hold all the information about a *single* donor, and have attributes, properties, and methods to provide access to the donor-specific information that is needed. +Any code that only accesses information about a single donor should be part of this class. + + +``DonorCollection`` Class +......................... + +**Class responsible for donor collection data encapsulation** + +This class will hold all of the donor objects, as well as methods to add a new donor, search for a given donor, etc. If you want a way to save and re-load your data, this class would hold that method, too. + +Your class for the collection of donors will also hold the code that generates reports about multiple donors. + +In short: if the functionality involves more than one donor -- it belongs in this class. + +Note that the ``DonorCollection`` class should be holding, and working with, ``Donor`` objects -- it should NOT work directly with a list of donations, etc. + +The main data structure in your class can be a dictionary with a key as donor name and value as donor object: + + +.. code-block:: python + + class DonorCollection: + def __init__(self, *donors): + self.donors = {obj.name: obj for obj in donors} + + +This design allows you to quickly look up donor by their name and get a donor object instance to work with. + +Another option is to simply use a list of donor objects. You get to choose which you think is more appropriate. + +Remember that you should use `self.donors` attribute in any methods that need access the individual donors. Most of your methods in this class will utilize it in some way. This is really what classes are designed for. + +Note that external code probably shouldn't access the ``.donors`` list (or dict, or ...) directly but rather ask the DonorCollection class for the information it needs: e.g. if you need to add a new donation to a particular donor, calla method like ``find_donor()`` to get the donor you want, and then work with that Donor object directly. + +**Examples:** + +Generating a thank you letter to a donor only requires knowledge of that one donor -- so that code belongs in the ``Donor`` class. + +Generating a report about all the donors requires knowledge of all the donors, so that code belongs in the ``DonorCollection`` class. + +.. note:: You've previously sorted simple data structures like list and dictionaries, but here we're dealing with objects -- not to worry, that is a really simple thing to do with python! You can use ``operator.attrgetter`` with the ``sorted`` function (review the python docs for usage documentation) to make it easy to sort based on various attributes of a Donor. + + +Command Line Interface +....................... + +**Module responsible for main program flow (CLI - Command Line Interface)** + +Let's call this module ``cli.py``. This module will be using the classes we defined: ``Donor`` and ``DonorCollection``. +It will also handle interaction with the user via the ``input`` function calls that gather user input and to provide the output to the console. + +What should go into this module? + +A set of user-interaction menu functions -- to handle each of the modes of the program. + +These will include ``input()`` function calls to gather user input, and ``print()`` functions to print results to console. + +.. note:: Console print statements don't belong in your data classes. So for features such as "send letters," in which we are simply printing instead of "sending", the data class methods should return a string, and let the UI code do the printing. This will mean there may be very simple functions in the UI code that simply call a method and print the results -- but that does keep flexibility for other ways of handling user interaction. + +.. rubric:: Why is this separation of data and method so important? + +The idea here is that we should be able to fairly easy replace this CLI program with a different type of interface, +such as a GUI (Graphical User Interface), without having to make any changes to our data classes. +If that was the case, then you would implement the GUI elements and use your data classes the same way as they are used in CLI. + +Note that this arrangement is a one-way street: the CLI code will need to know about the Donor and DonorCollection classes, but the model code shouldn't need to know anything about the CLI code: it manages the information, and returns what's asked for (like a donor letter or report), but it doesn't know what is done with that information. + +In short: the cli.py module will import the model module, but the model module shouldn't import the cli module. + + +Test-Driven Development +----------------------- + +At this point we have done a great job refactoring the more complex code out of data-holding classes and we are left with simple classes that are more straightforward to unit test. As you build your classes, update the tests you already have to the logic code to the new API. Ideally, update the tests first, then the code. + +The ``Donor`` and ``DonorCollection`` classes should now have 100 percent code coverage, which means that every line of code in your ``donor_models.py`` file will be run at least once when your tests are run. + +For the moment, don't worry about testing most of the command line interface code. That requires simulating user input, which is an advanced testing topic. But you can (hopefully) see some of the benefits of separating the user-interaction code from the logic code; your logic code is much easier to test with no user-interaction involved. + +.. rubric:: refactoring non-OO code + +In this case, you already have working code without an OO structure. You should be able to re-use a fair bit of your existing code. +However, you should still start with the OO structure/design. +That is, rather than take a non-OO function and try to make it a method of a class, decide what method you need, and what it's API should be, and then see if you have code you can use to fill in that function. + +You should expect to re-use a lot of the command line interface code, while refactoring most of the logic code. + +If you are not sure at the start what functionality you data classes will need, you can start with the CLI code, and as you find the need for a function, add it to your model classes (after writing a test first, of course). + + +Exercise Guidelines +=================== + +OO mailroom is the final step in the mailroom project. And you are near the end of the class. + +So this is your chance to really do things "right". Strive to make this code as good, by every definition, as you can. If you have gotten feedback from your instructors, now is the chance to incorporate recommended changes. + +With that in mind: + +Functionality +------------- + +* The logic is correct -- i.e. the program works :-) + +* The logic is robust -- you are handling obvious expected errors reasonably: + + - User inputting a non-number as a donation + + - Trying to make a negative donation + + - User getting capitalization or spacing or ??? wrong with a name. + + - Maybe add logic where you tell them that the name is not in the DB, and do they want to create it, rather than simply creating a new record for a typo in a donor name. + +.. rubric:: Code structure + +* Classes should have clear purpose and encapsulation: only the code within a class should know exactly how the data are stored, for instance. + +* Anything that only needs to know about one donor should be in the ``Donor`` class + +* Anything that needs to know about the collection should be in a ``DonorCollection`` class. + +* Any user interaction should be outside the "logic" code. (Sometimes called the "Model", or "Business logic") + + - You should be able to re-use all the logic code with a different UI -- Web App, GUI, etc. + + - There should be no ``input()`` or ``print()`` functions in the logic code. + + - The logic code should be 100% testable (without mocking ``input()`` or any fancy stuff like that) + +.. rubric:: Testing + +* All logic code should be tested. + +* Tests should be isolated to test one thing each + +* Tests should (reasonably) check for handling of weird input. + +* Tests should be isolated -- that is, they will work if run by themselves, and in any order. + + - This means they should not rely on any global state. + + - you'll probably find this easier with a well structured OO approach -- that is, you can test an individual donor functionality without knowing about the rest of the donors. + + +.. rubric:: The "soft" stuff: + +Style: + - Conform to PEP8! (or another consistent style) + (You can use 95 char or some other reasonable number for line length) + +Docstrings: + Functions and classes should all have good docstrings. They can be very short if the function does something simple. + +Naming: + All classes, functions, methods, attributes, variables should have appropriate names: meaningful, but not too detailed. + + +Extra Ideas: +------------ + +In case you are bored -- what features can you add? + +* Fancier reporting + +* More error checking -- does the user really want to create a new donor? or was it a typo in the donor name? + +* The next project is an html renderer -- maybe use it to make a nice html report? + +* The sky's the limit! diff --git a/_sources/exercises/mailroom/mailroom-pkg.rst.txt b/_sources/exercises/mailroom/mailroom-pkg.rst.txt new file mode 100644 index 0000000..faa8a62 --- /dev/null +++ b/_sources/exercises/mailroom/mailroom-pkg.rst.txt @@ -0,0 +1,143 @@ +.. _exercise_mailroom_package: + +Mailroom -- as a Python Package +=============================== + +The mailroom program is a small but complete system -- if you wanted to make it available for others to test and run, making a "proper" python package is a great idea. Making a package of it will also make it easier to develop and test, even if you are the only one to use it. + +Code Structure +-------------- + +Start with your existing version of mailroom. + +It may already be structured with the "logic" code distinct from the user interface (yes, a command line *is* a user interface). But you may have it all in one file. This isn't *too* bad for such a small program, but as a program grows, you really want to keep things separate, in a well organized package. + +The first step is to re-structure your code into separate files: + + - One (or more) for the logic code: the code that manipulates the data + - One for the user-interface code: the code with the interactive loops and all the "input" and "print" statements + - One (or more) for the unit tests. + +You should have all this pretty distinct after having refactored for the unit testing. If not, this is a good time to do it! + +In addition to those three, you will need a single function to call that will start the program. +That can be defined in a new file, as a "script", but for something as simple as this, it can be in with your interface code. +That file can then have an ``if __name__ == "__main__"`` block +which should be as simple as: + +.. code-block:: python + + if __name__ == "__main__": + main() + + +Making the Package +------------------ + +Put all these in a python package structure, something like this:: + + mailroom + setup.py + mailroom\ + __init__.py + model.py + cli.py + tests\ + __init__.py + test_model.py + test_cli.py + +You will need to import the logic code from model.py in the cli code in order to use it. +You can wait until you learn about mocking to write the code in test_cli.py (so you can leave that out) + +Now write a ``setup.py`` file to support the installation of your package. + + +Making the top-level script runnable +------------------------------------ + +To get the script installed you have two options. I used to prefer the more straightforward one, `the scripts keyword argument `_ + +But it turns out that while the simple ``scripts`` keyword argument method works well and is simple, it may not work as well on Windows -- it relies in your script being named ``something.py`` and that Windows is configured to run all files with ``.py`` extensions. Not all windows systems are set up this way. But the "entry points" method builds a little ``.exe`` file to call your script, so it's more reliable. + +And the Python community has moved very much towards using setuptools entry points, so that's really the way to go now: + +http://python-packaging.readthedocs.io/en/latest/command-line-scripts.html#the-console-scripts-entry-point + +In this case, that will look a lot like this: + +.. code-block:: python + + entry_points={'console_scripts': ['mailroom=mailroom.cli:main']}, + +That's a bit complicated, so I'll break it down for you. In this case, we only want a single script, but setuptools allows multiple types of entry points, and multiple instances of each type (e.g. you can have more than one script) to the code, so the argument is a potentially large dictionary. +The keys of the dict are the various types of entry points. +In this case, we want a single script that can be run in a terminal (or "console"), so we have a dict with one key: ``console_scripts``. + +The value of that entry is a list of strings -- each one describing the console script. This string is of the form:: + + SCRIPTNAME=MODULE:FUNCTION_NAME + +setuptools will create a wrapper script with the name given, and that wrapper will call the function in the module that is specified. +So: ``'mailroom=mailroom.cli:main'`` means: create a start up script called "mailroom" that will then call the ``main`` function in the ``cli`` module in the ``mailroom`` package. + +Once this is all set up, and you install the package (either in editable mode or not):: + + pip install -e ./ + +you should then be able to type "mailroom" at the command line and have your program run. + + +Including Data Files +-------------------- + +If you have a database of donors in a file that you load, then that should go in the package as well. Probably inside the mailroom dir, in a ``data`` dir or similar. Then you need to add it to your setup.py to make sure it gets copied into the installed package. + +(If you are not saving the donor data to a file -- that's fine. You can ignore this section for now) + +There are a few ways to do this: + +http://setuptools.readthedocs.io/en/latest/setuptools.html#including-data-files + +I personally like the simplest one with the least magic: + +`include_package_data=True `_ + +Then you'll get the data file included in the package in the same place. + +Now you'll need to write your code to find that data file. +You can do that by using the ``__file__`` module attribute, which is the path to a python module at run time -- then the location of the data file will be relative to the path that your code is in. +A little massaging with a ``pathlib.Path`` should do it. + + +Testing your Package +-------------------- + +When you are done, you should be able to both install your package fully: + +.. code-block:: bash + + $ pip install . + +or in "editable" mode: + +.. code-block:: bash + + $ pip install -e . + +When that is done, you should be able to run the top-level script from anywhere: + +.. code-block:: bash + + $ mailroom + +and run the test from within the package: + +.. code-block:: bash + + $ pytest --pyargs mailroom + +(or run the tests from the test dir as well) + +If you installed in editable mode, then you can update the code and re-run the tests or the script, and it will use the new code right away. + diff --git a/_sources/exercises/mailroom/mailroom.rst.txt b/_sources/exercises/mailroom/mailroom.rst.txt new file mode 100644 index 0000000..22c4d85 --- /dev/null +++ b/_sources/exercises/mailroom/mailroom.rst.txt @@ -0,0 +1,98 @@ +.. _exercise_mailroom_part1: + + +Mailroom +======== + + +**Overall Assignment Structure** + +This is the first in a multi-part exercise that will make use of your Python programming skills as you develop them during this course. You will start work on the program in this assignment, and then you will build on it in future lessons, as you learn more of Python's powerful features. + +This progressive work will give you a strong foundation for success in the final project, a Mailroom program using object-oriented structure, fully tested, and bundled up as a Python package. + + +Overall Program Goal: +--------------------- + +You work in the mail room at a local charity. Part of your job is to write +incredibly boring, repetitive emails thanking your donors for their generous +gifts. You are tired of doing this over and over again, so you've decided to +let Python help you out of a jam and do your work for you. + + +The Program: +------------ + +Write a small command-line script called ``mailroom.py``. This script should be executable. The script should accomplish the following goals: + +* It should have a data structure that holds a list of your donors and a + history of the amounts they have donated. This structure should be populated + at first with at least five donors, with between 1 and 3 donations each. You can store that data structure in the global namespace. + +* The script should prompt the user (you) to choose from a menu of 3 actions: + "Send a Thank You", "Create a Report" or "quit". + +Send a Thank You +---------------- + +* If the user (you) selects "Send a Thank You" option, prompt for a Full Name. + + * If the user types ``list`` show them a list of the donor names and re-prompt. + * If the user types a name not in the list, add that name to the data structure and use it. + * If the user types a name in the list, use it. +* Once a name has been selected, prompt for a donation amount. + + * Convert the amount into a number; it is OK at this point for the program to crash if someone types a bogus amount. + * Add that amount to the donation history of the selected user. + +* Finally, use string formatting to compose an email thanking the donor for their generous donation. Print the email to the terminal and return to the original prompt. + +It is fine (for now) for the program not to store the names of the new donors that had been added, in other words, to forget new donors once the script quits running. + +Create a Report +----------------- + +* If the user (you) selected "Create a Report," print a list of your donors, + sorted by total historical donation amount. + + - Include Donor Name, total donated, number of donations, and average donation amount as values in each row. You do not need to print out all of each donor's donations, just the summary info. + - Using string formatting, format the output rows as nicely as possible. The end result should be tabular (values in each column should align with those above and below). + - After printing this report, return to the original prompt. + +* At any point, the user should be able to quit their current task and return + to the original prompt. + +* From the original prompt, the user should be able to quit the script cleanly. + + +Your report should look something like this:: + + Donor Name | Total Given | Num Gifts | Average Gift + ------------------------------------------------------------------ + William Gates, III $ 653784.49 2 $ 326892.24 + Mark Zuckerberg $ 16396.10 3 $ 5465.37 + Jeff Bezos $ 877.33 1 $ 877.33 + Paul Allen $ 708.42 3 $ 236.14 + +Guidelines +---------- + +First, factor your script into separate functions. Each of the above +tasks can be accomplished by a series of steps. Write discreet functions +that accomplish individual steps and call them. + +Second, use loops to control the logical flow of your program. Interactive +programs are a classic use case for the ``while`` loop. + +Of course, ``input()`` will be useful here. + +Put the functions you write into the script at the top. + +Put your main interaction into an ``if __name__ == '__main__'`` block. + +Finally, for Part 1 use only functions and the basic Python data types you've learned +about so far in Lessons 1-3. There is no need to go any farther than that for this assignment. + +If you're having a hard time getting started, or need some pointers, you should read the tutorial here: :ref:`exercise_mailroom_part1_tutorial` + diff --git a/_sources/exercises/mailroom/mailroom_tutorial.rst.txt b/_sources/exercises/mailroom/mailroom_tutorial.rst.txt new file mode 100644 index 0000000..9c2f9ff --- /dev/null +++ b/_sources/exercises/mailroom/mailroom_tutorial.rst.txt @@ -0,0 +1,198 @@ +.. _exercise_mailroom_part1_tutorial: + + +Mailroom Tutorial +================= + +Controlling Main Program Flow +----------------------------- + +One of the key components of the mailroom program is managing program flow and interacting with the user. Ideally main flow code should be cleanly separate from your feature code. + +The best way to manage the program flow of an interactive prompt is to use a ``while True`` loop, which means you will keep asking the user for input until the user selects a feature or exits. + +There are several ways to write your main interactive loop. Let's consider these two options: + + +Option 1: +......... + +.. code-block:: python + + def do_something(): + # do things + + def main(): + while True: + do_something() + + main() + +Option 2: +......... + +.. code-block:: python + + def do_something() + # do things + main() + + def main(): + do_something() + + main() + + +Can you see the advantages of one example over the other? + +In the first one, ``do_something`` is not aware of how the main function works and as you add more features they don't need to know about how the main function works either. +The call stack will also keep getting deeper and deeper, which can make error stack traces hard to debug. + +Another advantage is simpler code logic, and simpler code logic means fewer bugs! + +Let's look at a simple program to utilize the ``while True`` loop and how we can handle user response: + +.. code-block:: python + + import sys # imports go at the top of the file + + + fruits = ['Apples', 'Oranges', 'Pears'] + + prompt = "\n".join(("Welcome to the fruit stand!", + "Please choose from below options:", + "1 - View fruits", + "2 - Add a fruit", + "3 - Remove a fruit", + "4 - Exit", + ">>> ")) + + + def view_fruits(): + print("\n".join(fruits)) + + + def add_fruit(): + new_fruit = input("Name of the fruit to add?").title() + fruits.append(new_fruit) + + + def remove_fruit(): + purge_fruit = input("Name of the fruit to remove?").title() + if purge_fruit not in fruits: + print("This fruit does not exist!") + else: + fruits.remove(purge_fruit) + + def exit_program(): + print("Bye!") + sys.exit() # exit the interactive script + + + def main(): + while True: + response = input(prompt) # continuously collect user selection + # now redirect to feature functions based on the user selection + if response == "1": + view_fruits() + elif response == "2": + add_fruit() + elif response == "3": + remove_fruit() + elif response == "4": + exit_program() + else: + print("Not a valid option!") + + + if __name__ == "__main__": + # don't forget this block to guard against your code running automatically if this module is imported + main() + + + +Choosing A Data Structure +------------------------- + + +So far in this course, we have learned about strings, tuples, and lists. We will apply these data structures to hold our mailroom donor information. +Choosing the right data structure is critical and our donor data structure will change in Parts 2 and 3 of this assignment as we learn about additional structures. + +What goes into this decision to use a specific data structure? Here are a couple of things to consider. + +* Efficiency: We often need to look up data; are you able to efficiently look up the data you need? +* Ease of use: Is the code straightforward and simple for basic operations? +* Features: Does the code do everything you need to do for your requirements? + +Let's consider each data structure. + +A simple string would probably be able to do what we need feature-wise but the code to implement these features would be quite complex and not very efficient. + +A tuple would be an issue when adding donors since it is an immutable data structure. + +A list would satisfy all of the needed features with a fairly simple code to implement. It makes the most sense to use a list for the main data structure. Actually, we can use a combination of both tuples and a list. + +Here is a potential data structure to consider: + +.. code-block:: python + + donor_db = [("William Gates, III", [100.0, 120.10]), + ("Jeff Bezos", [877.33]), + ("Paul Allen", [663.23, 343.87, 411.32]), + ("Mark Zuckerberg", [1660.23, 4320.87, 10432.0]), + ] + +Here we have the first item in a tuple as a donor name, which we will use to determine if we need to add to existing donor or add a new one and the second item is a list of donation values. + +Why choose tuples for the inner donor record? Well, another part of using the right data structure is to reduce bugs; you are setting clear expectations that a single donor entry only contains two items. + + +Sorting +------- + +Python makes sorting fairly easy and has utilities for sorting simple lists as well as more complex structures like lists of tuples as above. + +Let's start with a structure that represents student records: student name and age. + +:: + + >>> students = [('Bob', 39), ('Joe', 26), ('Jimmy', 40)] + +We will use the ``sorted`` function to do the sorting and either sort by name or age. There are actually several ways to accomplish that, we will look at some of them. + +The first option is to use optional ``key`` param, which accepts a function object - it can be any custom function we define as long as input and output are correctly implemented. + + >>> def sort_key(student): + return student[1] + >>> sorted(students, key=sort_key) + [('Joe', 26), ('Bob', 39), ('Jimmy', 40)] + +``sort_key`` function takes in a single parameter that represents the item in the list, in our case the student record, you then need to return which field should be used for sort comparison. We are using field at index 1, that's the age. + + +Another option is to use a ``itemgetter`` function from ``operator`` module, it accepts a parameter for list item index value, similar to our ``sort_key`` function: + + >>> from operator import itemgetter + >>> sorted(students, key=itemgetter(1)) + [('Joe', 26), ('Bob', 39), ('Jimmy', 40)] + >>> sorted(students, key=itemgetter(0)) + [('Bob', 39), ('Jimmy', 40), ('Joe', 26)] + +Using second option makes the most sense in simple cases like above since we're not doing anything complicated and simply need to sort on the index. If our student record also included the last name: + + >>> students = [('Bob Mac', 39), ('Joe Acer', 26), ('Jimmy Lenovo', 40)] + +Then the custom function becomes really handy to sort on the last name: + + >>> def sort_key(student): + return student[0].split(" ")[1] + >>> sorted(students, key=sort_key) + [('Joe Acer', 26), ('Jimmy Lenovo', 40), ('Bob Mac', 39)] + + +Note: you might see a lot of examples online using the ``lambda`` statement, it is valid and can be used but isn't preferred because the syntax isn't elegant or very readable: + +.. code-block:: python + + sorted(students, key=lambda x: x[0].split(" ")[1], reverse=True) + diff --git a/_sources/exercises/mailroom/mailroom_with_comprehansions.rst.txt b/_sources/exercises/mailroom/mailroom_with_comprehansions.rst.txt new file mode 100644 index 0000000..a1086a3 --- /dev/null +++ b/_sources/exercises/mailroom/mailroom_with_comprehansions.rst.txt @@ -0,0 +1,54 @@ +.. _exercise_mailroom_comprehensions: + + +Mailroom With Comprehensions +============================ + +**Improve your mailroom by adding (maybe) comprehensions.** + + +Comprehensions +-------------- + +Can you use comprehensions to clean up your code a bit? + +Note: you may be tempted to replace loops like this: + +.. code-block:: python + + for donor in donors: + print(donor) + +with + +.. code-block:: python + + [print(donor) for donor in donors] + + +That's not the intended use of comprehensions. Because ``print`` function does not return a value, this code will allocate a space for an "empty" result list filled with None values: + + >>> [print(donor) for donor in donors] + jane + wendy + [None, None] + >>> + +List comprehensions are designed for a very specific use case: + +*Processing a sequence of items to create another sequence.* + +They are not designed to replace all for loops. + +So if you have code that looks like: + +.. code-block:: python + + new_list = [] + for item in old_list: + new_list.append(do_something_to(item)) + +Then you have a candidate for a comprehension. + +In your version of mailroom -- there may not be any such constructs -- that's OK, don't use a comprehension unless it cleans up your code. + diff --git a/_sources/exercises/mailroom/mailroom_with_comprehensions.rst.txt b/_sources/exercises/mailroom/mailroom_with_comprehensions.rst.txt new file mode 100644 index 0000000..a1086a3 --- /dev/null +++ b/_sources/exercises/mailroom/mailroom_with_comprehensions.rst.txt @@ -0,0 +1,54 @@ +.. _exercise_mailroom_comprehensions: + + +Mailroom With Comprehensions +============================ + +**Improve your mailroom by adding (maybe) comprehensions.** + + +Comprehensions +-------------- + +Can you use comprehensions to clean up your code a bit? + +Note: you may be tempted to replace loops like this: + +.. code-block:: python + + for donor in donors: + print(donor) + +with + +.. code-block:: python + + [print(donor) for donor in donors] + + +That's not the intended use of comprehensions. Because ``print`` function does not return a value, this code will allocate a space for an "empty" result list filled with None values: + + >>> [print(donor) for donor in donors] + jane + wendy + [None, None] + >>> + +List comprehensions are designed for a very specific use case: + +*Processing a sequence of items to create another sequence.* + +They are not designed to replace all for loops. + +So if you have code that looks like: + +.. code-block:: python + + new_list = [] + for item in old_list: + new_list.append(do_something_to(item)) + +Then you have a candidate for a comprehension. + +In your version of mailroom -- there may not be any such constructs -- that's OK, don't use a comprehension unless it cleans up your code. + diff --git a/_sources/exercises/mailroom/mailroom_with_dicts.rst.txt b/_sources/exercises/mailroom/mailroom_with_dicts.rst.txt new file mode 100644 index 0000000..d0a46e9 --- /dev/null +++ b/_sources/exercises/mailroom/mailroom_with_dicts.rst.txt @@ -0,0 +1,39 @@ +.. _exercise_mailroom_with_dicts: + + +Mailroom With Dicts +=================== + +**Incorporate dictionary use.** + +Use dicts where appropriate. +---------------------------- + +The first version of this assignment used these basic data types: numbers, strings, lists and tuples. + +However, using dictionaries, an important built in data structure in Python, will let you re-write your program a bit more simply and efficiently. + +Update your mailroom program to: + + - Convert your main donor data structure to be a dict. Think about an appropriate key for easy donor look up. + + - Use dicts anywhere else, as appropriate. + + - Use a dict to switch between the users selections. + see :ref:`dict_as_switch` for what this means. + + - Use a dict and the ``.format()`` method to produce the letter as one big template, rather than building up a big string that produces the letter in parts. + + +Example: + +.. code-block:: ipython + + In [3]: d + Out[3]: {'first_name': 'Chris', 'last_name': 'Barker'} + + + In [5]: "My name is {first_name} {last_name}".format(**d) + Out[5]: 'My name is Chris Barker' + +Don't worry too much about the ``**``. We'll get into the details later, but for now it means, more or less, "pass this whole dict in as a bunch of keyword arguments." diff --git a/_sources/exercises/mailroom/mailroom_with_exceptions.rst.txt b/_sources/exercises/mailroom/mailroom_with_exceptions.rst.txt new file mode 100644 index 0000000..be9e084 --- /dev/null +++ b/_sources/exercises/mailroom/mailroom_with_exceptions.rst.txt @@ -0,0 +1,29 @@ +.. _exercise_mailroom_exceptions: + + +Mailroom With Exceptions +======================== + +**Improve your mailroom by adding exception handling** + +Exceptions +---------- + +Now that you've learned about exception handling, you can update your code to handle errors better, such as when a user inputs bad data. + +This is a great use case for "Easier to Ask Forgiveness Than Permission" (EAFTP) + +For example, when you are asking the user for a donation amount, it should be a number. rather than trying to parse the text they input to make sure it's a number, simply try to "turn it into" an integer (or float, you decide), and if you get an error, you know they didn't pass in a number: + +.. code-block:: python + + amount = input("how much?") + try: + amount = int(amount) + except ValueError: + # report the error and go back + print("you need to provide a number") + + # If the code gets here, then you know that the amount is now an integer. + +There may be other places in your code where handling an Exception would be helpful -- keep them in mind! You never want your program to crash if the error is recoverable. diff --git a/_sources/exercises/mailroom/mailroom_with_files.rst.txt b/_sources/exercises/mailroom/mailroom_with_files.rst.txt new file mode 100644 index 0000000..8baeace --- /dev/null +++ b/_sources/exercises/mailroom/mailroom_with_files.rst.txt @@ -0,0 +1,65 @@ +.. _exercise_mailroom_with_files: + + +Mailroom With Files +=================== + +**Incorporate file writing** + + +Update mailroom with file writing. +---------------------------------- + +**Goal: Write a full set of letters to all donors to individual files on disk.** + +In the first version of mailroom, you generated a letter to a donor who had just made a new donation, and printed it to the screen. + +In this version of your program, add a function (and a menu item to invoke it), that goes through all the donors in your donor data structure, generates a thank you letter for each donor, and writes each letter to disk as a text file. + +Your main menu may look something like:: + + Choose an action: + + 1 - Send a Thank You to a single donor. + 2 - Create a Report. + 3 - Send letters to all donors. + 4 - Quit + +The letters should each get a unique file name -- you can keep it really simple and just use the donor's name or add a date timestamp for additional uniqueness. + +You want to avoid specifying a hardcoded file path when creating the files, for example don't to this: + +.. code-block:: python + + open("/home/users/bob/dev/mailroom/thank_you.txt", "w") + + +Doing so will prevent other users from running the program as it will fail to find your path. Instead, you can create files in the current working directory or you can use a temporary directory. +To identify a temporary directory you can use a handy function like `tempfile.gettempdir() `_ which is also OS agnostic (meaning it can handle temp directory differences between different operating systems). + +After running the "send letters to everyone" option, you should see some new files in the directory. There should be a file for each donor in the database, in this case 4. + +After choosing action (3) above, using my example database, I get these files:: + + Jeff_Bezos.txt + Mark_Zuckerberg.txt + Paul_Allen.txt + William_Gates_III.txt + +(If you want to get really fancy, ask the user for a directory name to write to!) + +An example file content looks like this:: + + Dear Jeff Bezos, + + Thank you for your very kind donation of $877.33. + + It will be put to very good use. + + Sincerely, + -The Team + +Feel free to enhance your letter template with some more information about past generosity, etc.... + +The idea is to require you to structure your code so that you can write the same letter to the screen or to disk (and thus anywhere else) and also exercise a bit of file writing. Remember to review the `with `_ statement as it is the preferred method when working with files. + diff --git a/_sources/exercises/mailroom/mailroom_with_full_tests.rst.txt b/_sources/exercises/mailroom/mailroom_with_full_tests.rst.txt new file mode 100644 index 0000000..0cf5eff --- /dev/null +++ b/_sources/exercises/mailroom/mailroom_with_full_tests.rst.txt @@ -0,0 +1,23 @@ +.. _exercise_mailroom_full_testing: + + +Mailroom With Full Unit Tests +============================= + +You should now have a version of the mailroom program that has complete tests for all the logic code. +That is: every function in mailroom that does not contain a ``print`` or ``input`` call should be tested. + +And, critically: every function that contains a ``print`` or ``input`` should contain *no other logic at all*. + +But now you've learned about more advanced techniques, like mocking and parameterized tests, so you should be able get your mailroom tests to 100% coverage -- and maybe even make the tests more efficient and complete with some other techniques. + +The Task +-------- + +Use mocking of the ``input()`` function to write a full set of tests for the interactive portion of your mailroom program. + +Then run ``coverage`` and make sure everything is covered -- if not, then find those holes and fill them. You should be able to provide a report indicating 100% coverage. + + + + diff --git a/_sources/exercises/mailroom/mailroom_with_tests.rst.txt b/_sources/exercises/mailroom/mailroom_with_tests.rst.txt new file mode 100644 index 0000000..a7bc268 --- /dev/null +++ b/_sources/exercises/mailroom/mailroom_with_tests.rst.txt @@ -0,0 +1,124 @@ +.. _exercise_mailroom_testing: + + +Mailroom With Unit Tests +======================== + +**Add a full suite of unit tests.** + +"Full suite" means all the code is tested. In practice, it's very hard to test the user interaction, but you can test everything else. Therefore you should make sure that there is as little logic and untested code in the user interaction portion of the program as possible. + +This is a big step; you may find that your code is hard to test. If that's the case, it's a good sign that you *should refactor your code*. + +I like to say: "If it's hard to test, it's not well structured." + +Put in the tests **before** you make the other changes below. That's much of the point of tests. And while in this case, it's not really TDD, as you already have working code, you still want to write the tests as early as possible, so you know your refactored code is correct. + +Guidelines +---------- + +Here are some suggestions on what should be refactored in your mailroom code. + +As mentioned above, testing user interaction code (code with ``print`` and ``input`` functions) is harder than testing the rest of your code. +Testing user interaction code requires more advanced unit testing methodologies that will be revisited in future courses. +Therefore, you should refactor your code so that the user interaction code contains as little business logic as possible. +It should only interact with the user either by asking them for input or by responding to their request to print out data. +Separating business logic from user interaction code is a good practice in general and we will come back to this concept in later lessons. + +The refactoring in this lesson will allow you to unit test the functions with business logic, even if you don't test the user interaction code. + +We will go over the components that should be refactored so that you are able to unit test your mailroom. After the refactor, your code should improve and be better modularized. If that's not the case then maybe you should revisit your refactoring approach. + +For unit testing framework you should use `pytest `_; it has a simple interface and rich features. + + +Mailroom Code Structure +----------------------- + +Your code should have 2 main features so far: + +* Send a thank you (adds a new donor or updates existing donor info) +* Create a report + + +Send Thank You +............... + +Even though every mailroom implementation will be unique, most likely this function will require a significant refactor for most of you. + +You can break up the code into components that handle user interaction and data manipulation logic. Your unit tests should test the data manipulation logic code: + +* generating the thank you text + +* adding or updating donors + +* listing donors + + +Create Report +............. + +This function should only need slight modification. Split up user presentation (``print`` function calls) and data logic (actual creating of rows). +Your data logic function can either return the report string already formatted or return a list of formatted rows that can be joined and printed in the user presentation function. + +This allows you to write a unit test for your data logic function. + +Example: + +.. code-block:: python + + def display_report(): + for row in get_report(): + print(row) + + +Here you would write a unit test for ``get_report`` function. Remember: TDD -- write that test *before* the function! + +Send Letters +............ + +After adding unit tests, in a future assignment, you will be extending mailroom to save the thank you letters to files on disk. + +Don't worry about that now -- but when you get there, keep testing in mind! + +This function should be easy to make unit-testable. + +To make it testable, you'll need to follow the same "separation of concerns" approach: the code that creates the letters should be separate from the code that prints them to the screen. + +This both allows you to test the letter creation, and leaves the door open to do something else with the letters: save to to a file, send as an email, etc. +So the code that makes a letter likely will return a string with the entire letter contents. + +For example: + +.. code-block:: python + + def get_letter_text(name): + """Get letter text for file content""" + return f"{name}, thanks a lot!" + + + def test_get_letter_text(): + expected = "Frank, thanks a lot!" + assert get_letter_text("Frank") == expected + +Note that some thought should go into the test of the letter. If it's really simple, then simiply comparing to a full letter is OK. But it might be better to test the important parts of the letter: Does it contain the correct name? does it contain the right amounts of money? rather than the entire text. + +When you are done, every function in mailroom that does not contain a ``print`` or ``input`` call should be tested. + +And, critically: every function that contains a ``print`` or ``input`` should contain *no other logic at all*. + +Yes, that does mean that that you'll have some very simple functions like: + +.. code-block:: python + + def print_letter(donor): + print(make_letter(donor)) + +But trust me -- that is a Good Thing™ + +.. note:: Testing print() is rearely neccesasry if you factor your code correctly. But it would be able to test your menu code with `input()` in it. This is a pretty advanced topic, but if you want to give it a try, there is more on advanced testing here: :ref:`advanced_testing` + + + + + diff --git a/_sources/exercises/mailroom_tutorial.rst.txt b/_sources/exercises/mailroom_tutorial.rst.txt new file mode 100644 index 0000000..4ee1920 --- /dev/null +++ b/_sources/exercises/mailroom_tutorial.rst.txt @@ -0,0 +1,194 @@ +.. _exercise_mailroom_part1_tutorial: + + +Mailroom Tutorial +================= + +Controlling Main Program Flow +----------------------------- + +One of the key components of the mailroom program is managing program flow and interacting with the user. Ideally main flow code should be cleanly separate from your feature code. + +The best way to manage the program flow is to use a ``while True`` loop, which means you will keep asking the user for input until user selects a feature or exits. + +There are several ways to write your main program flow. Let's consider these two options: + + +Option 1: + +.. code-block:: python + + def do_something(): + # do things + + def main(): + while True: + do_something() + + main() + +Option 2: + +.. code-block:: python + + def do_something() + # do things + main() + + def main(): + do_something() + + main() + + +Can you see the advantages of one example over the other? + +In the first one, ``do_something`` is not aware of how the main works and as you add more features they shouldn't manage the main either. +The call stack will also keep getting deeper and deeper, which can make error stack traces hard to debug. + +Another advantage is simpler code logic, and simpler code logic means less bugs! + +Let's look at a simple program to utilize the ``while True`` loop and how we can handle user response: + +.. code-block:: python + + import sys # imports go at the top of the file + + + fruits = ['Apples', 'Oranges', 'Pears'] + + prompt = "\n".join(("Welcome to the fruit stand!", + "Please choose from below options:", + "1 - View fruits", + "2 - Add a fruit", + "3 - Remove a fruit", + "4 - Exit", + ">>> ")) + + + def view_fruits(): + print("\n".join(fruits)) + + + def add_fruit(): + new_fruit = input("Name of the fruit to add?").title() + fruits.append(new_fruit) + + + def remove_fruit(): + purge_fruit = input("Name of the fruit to remove?").title() + if purge_fruit not in fruits: + print("This fruit does not exist!") + else: + fruits.remove(purge_fruit) + + def exit_program(): + print("Bye!") + sys.exit() # exit the interactive script + + + def main(): + while True: + response = input(prompt) # continuously collect user selection + # now redirect to feature functions based on the user selection + if response == "1": + view_fruits() + elif response == "2": + add_fruit() + elif response == "3": + remove_fruit() + elif response == "4": + exit_program() + else: + print("Not a valid option!") + + + if __name__ == "__main__": + # don't forget this block to guard against your code running automatically if this module is imported + main() + + + +Choosing A Data Structure +------------------------- + + +So far in this course, we have learned about strings, tuples, and lists. We will apply these data structures to hold our mailroom donor information. +Choosing the right data structure is critical and our donor data structure will change in Parts 2 and 3 of this assignment as we learn about additional structures. + +What goes into this decision to use a specific data structure? Here are a couple of things to consider. + +* Efficiency: We often need to look up data; are you able to efficiently look up the data you need? +* Ease of use: Is the code straightforward and simple for basic operations? +* Features: Does the code do everything you need to do for your requirements? + +Let's consider each data structure. + +A simple string would probably be able to do what we need feature-wise but the code to implement these features would be quite complex and not very efficient. + +A tuple would be an issue when adding donors since it is an immutable data structure. + +A list would satisfy all of the needed features with a fairly simple code to implement. It makes the most sense to use a list for the main data structure. Actually, we can use a combination of both tuples and a list. + +Here is a potential data structure to consider: + +.. code-block:: python + + donor_db = [("William Gates, III", [653772.32, 12.17]), + ("Jeff Bezos", [877.33]), + ("Paul Allen", [663.23, 43.87, 1.32]), + ("Mark Zuckerberg", [1663.23, 4300.87, 10432.0]), + ] + +Why choose tuples for the inner donor record? Well, another part of using the right data structure is to reduce bugs; you are setting clear expectations that a single donor entry only contains two items. + + +Sorting +------- + +Python makes sorting fairly easy and has utilities for sorting simple lists as well as more complex structures like lists of tuples as above. + +Let's start with a structure that represents student records: student name and age. + +:: + + >>> students = [('Bob', 39), ('Joe', 26), ('Jimmy', 40)] + +We will use the ``sorted`` function to do the sorting and either sort by name or age. There are actually several ways to accomplish that, we will look at some of them. + +The first option is to use optional ``key`` param, which accepts a function object - it can be any custom function we define as long as input and output are correctly implemented. + + >>> def sort_key(student): + return student[1] + >>> sorted(students, key=sort_key) + [('Joe', 26), ('Bob', 39), ('Jimmy', 40)] + +``sort_key`` function takes in a single parameter that represents the item in the list, in our case the student record, you then need to return which field should be used for sort comparison. We are using field at index 1, that's the age. + + +Another option is to use a ``itemgetter`` function from ``operator`` module, it accepts a parameter for list item index value, similar to our ``sort_key`` function: + + >>> from operator import itemgetter + >>> sorted(students, key=itemgetter(1)) + [('Joe', 26), ('Bob', 39), ('Jimmy', 40)] + >>> sorted(students, key=itemgetter(0)) + [('Bob', 39), ('Jimmy', 40), ('Joe', 26)] + +Using second option makes the most sense in simple cases like above since we're not doing anything complicated and simply need to sort on the index. If our student record also included the last name: + + >>> students = [('Bob Mac', 39), ('Joe Acer', 26), ('Jimmy Lenovo', 40)] + +Then the custom function becomes really handy to sort on the last name: + + >>> def sort_key(student): + return student[0].split(" ")[1] + >>> sorted(students, key=sort_key) + [('Joe Acer', 26), ('Jimmy Lenovo', 40), ('Bob Mac', 39)] + + +Note: you might see a lot of examples online using the ``lambda`` statement, it is valid and can be used but isn't preferred because the syntax isn't elegant or very readable: + +.. code-block:: python + + sorted(students, key=lambda x: x[0].split(" ")[1], reverse=True) + diff --git a/_sources/exercises/oo_intro.rst.txt b/_sources/exercises/oo_intro.rst.txt new file mode 100644 index 0000000..241f4dd --- /dev/null +++ b/_sources/exercises/oo_intro.rst.txt @@ -0,0 +1,38 @@ +.. _oo_intro: + +######################## +OO Intro - Report Class +######################## + +This assignment uses Object Oriented Programming to design a class that can be used to manage data reporting. + +We have done some reporting in our mailroom program, but that report was pretty simple and used simple functions to accomplish the work. + +We will explore here how one can utilize OO to improve and enhance reporting capabilities. + + +Procedure +========= + +You will use :download:`../examples/oo_intro/oo_intro.py` file as a starting point for your code. + +You will notice that ``Report`` class will have attributes and methods defined for you, including input parameters (and their types) as well as expected output. You will need to fill out the code for each defined method and docstrings containing additional information on what is expected. + +The ``Report`` class uses another class that is fully defined for you, +the ``Row`` class. This class represents a single row in your report, and the report class will hold a list of the row instances. +There are big advantages to using a class like ``Row`` in contrast to a simple dictionary, this design creates a clear contract on what's expected to be as part of a row, where with a dictionary it is easy to misspell or miss a key. +A class like ``Row`` is often called a "data class" (since it only holds "data" and doesn't actually have any logic to it). + +This is such a popular pattern that python introduced ``dataclasses`` in +`version 3.7 `_ to make this even simpler. You do not use them in this assignment but you should know that they exist and why. + +You can read more about ``dataclasses`` in the Python docs here: + +https://docs.python.org/3/library/dataclasses.html + +Or a more descriptive tutorial here: + +https://realpython.com/python-data-classes/. + +Reminder: use Test Driven Development and make sure you have complete unit tests for all of your class methods. + diff --git a/_sources/exercises/oo_intro/oo_intro.rst.txt b/_sources/exercises/oo_intro/oo_intro.rst.txt new file mode 100644 index 0000000..82bd05a --- /dev/null +++ b/_sources/exercises/oo_intro/oo_intro.rst.txt @@ -0,0 +1,49 @@ +.. _oo_intro: + +####################### +OO Intro - Report Class +####################### + +This assignment uses Object Oriented Programming to design a class that can be used to manage data reporting. + +We have done some reporting in our mailroom program, but that report was pretty simple and used simple functions to accomplish the work. + +We will explore here how one can utilize OO to improve and enhance reporting capabilities. + + +Procedure +========= + +Use the :download:`report.py` and :download:`test_report.py` files as a starting point for your code. + +Notice that the ``Report`` class has attributes and methods defined for you, including input parameters as well as expected output. + +You will need to fill out the code and docstrings for each defined method containing additional information on what is expected. + +There is also some example code in the ``__main__`` block, to see how it's used. + +Use TDD: write a test for each method before you add that functionality. There is a test file started for you -- it has one (failing) test for one of the methods. + +The Row class +------------- + +The ``Report`` class uses another class, ``Row``, that is fully defined (and tested) for you. This class represents a single row in your report, and the report class will hold a list of the row instances. + +There are big advantages to using a class like ``Row`` rather than a simple dictionary. This design creates a clear contract about what is part of a row, whereas with a dictionary you can put anything in it, so it is harder to catch when a key is misspelled or missing. + +A class like ``Row`` is often called a "data class" (since it only holds "data" and doesn't actually have any logic to it). + +This is such a popular pattern that Python introduced ``dataclasses`` in +`version 3.7 `_ to make this even simpler. +You do not use them in this assignment but you should know that they exist and why. + +You can read more about ``dataclasses`` in the Python docs here: + +https://docs.python.org/3/library/dataclasses.html + +Or a more descriptive tutorial here: + +https://realpython.com/python-data-classes/. + +Reminder: use Test Driven Development and make sure you have complete unit tests for all of your class methods. + diff --git a/_sources/exercises/packaging/package_lab.rst.txt b/_sources/exercises/packaging/package_lab.rst.txt new file mode 100644 index 0000000..47c4762 --- /dev/null +++ b/_sources/exercises/packaging/package_lab.rst.txt @@ -0,0 +1,35 @@ +A Small Example Package +======================= + +* Create a small package + + - package structure + + - ``setup.py`` + + - ``python setup.py develop`` + + - ``at least one working test`` + + +* Here is a ridiculously simple and useless package to use as an example: + +:download:`capitalize.zip ` + +Unzip that, and you will find:: + + capital_mod.py: The module that does the "real work" + + cap_data.txt: A data file needed by the module + + main.py: the actual main function that runs the script. + + cap_script.py: A simple top-level script -- all it does is call ``main`` + + test_capital_mod.py: test code for the module + + sample_text_file.txt: an example file you can use to try it out. + +Your mission is to put all these files in an package hierarchy, and then write a simple ``setup.py`` file that will build and install it as a package. + + diff --git a/_sources/exercises/python_pushups.rst.txt b/_sources/exercises/python_pushups.rst.txt new file mode 100644 index 0000000..61f13bc --- /dev/null +++ b/_sources/exercises/python_pushups.rst.txt @@ -0,0 +1,34 @@ +.. _python_pushups: + +############## +Python Pushups +############## + +These are a quick exercise to kick you off with Python: + + +Explore Errors +============== + +* Create a new directory in your working dir for the class:: + + $ mkdir pushups + $ cd pushups + +* Add a new file to it called ``break_me.py`` + +* In the ``break_me.py`` file write four simple Python functions: + + * Each function, when called, should cause an exception to happen + + * Each function should result in one of the four most common exceptions you'll find. + + * for review: + - ``NameError`` + - ``TypeError`` + - ``SyntaxError`` + - ``AttributeError`` + +(hint -- the interpreter will quit when it hits a Exception -- so you can comment out all but the one you are testing at the moment) + + * Use the Python standard library reference on `Built In Exceptions `_ as a reference diff --git a/_sources/exercises/roman.rst.txt b/_sources/exercises/roman.rst.txt new file mode 100644 index 0000000..556955c --- /dev/null +++ b/_sources/exercises/roman.rst.txt @@ -0,0 +1,13 @@ +.. _roman: + +Roman/Arabic Numeral Converter +============================== + +Roman and Arabic numerals are different enough that a program to convert back and forth between the two systems would be useful assuming, of course, that you are a Roman trader living sometime in the Middle Ages with access to a computer with a Python interpreter. + +Write a class-based program to convert between the two formats. + +https://en.wikipedia.org/wiki/Roman_numerals + +https://en.wikipedia.org/wiki/Arabic_numerals + diff --git a/_sources/exercises/rot13.rst.txt b/_sources/exercises/rot13.rst.txt new file mode 100644 index 0000000..55d7190 --- /dev/null +++ b/_sources/exercises/rot13.rst.txt @@ -0,0 +1,78 @@ +.. _exercise_rot13: + +##### +ROT13 +##### + +Goal +==== + +Get used to working with the number values (ordinals) for characters. + +Get a bit of practice with string methods and string processing. + + +ROT13 encryption +================ + +The ROT13 encryption scheme is a simple substitution cypher where each letter +in a text is replace by the letter 13 away from it (imagine the alphabet as a +circle, so it wraps around, or "rotates" by 13 letters, hence "rot13"). + +The task +-------- + +Create a python module named ``rot13.py``. This module should provide at least one function called ``rot13`` that takes any amount of text and returns that same text encrypted by ROT13. + +This function should preserve whitespace, punctuation and capitalization. + +Your module should include an ``if __name__ == '__main__':`` block with tests (asserts) that demonstrate that your ``rot13`` function and any helper functions you add work properly. + +ordinals... +----------- + +"Ordinals" are the numerical values associated with characters. Python strings are native unicode, so they are the number values of any character, or "code point". + +To get the ordinal of a character in Python, you use the `ord()` function:: + + In [6]: ord('A') + Out[6]: 65 + + In [7]: ord('B') + Out[7]: 66 + + In [8]: ord('a') + Out[8]: 97 + +To make a character from the ordinal value, use the `chr()` function:: + + In [9]: chr(65) + Out[9]: 'A' + +Note that the "modulo" operator (`%`) could be useful here as well:: + + In [10]: 29 % 26 + Out[10]: 3 + + +Hints +----- + +Note that the alphabet has 26 letters, so if you "rotate" by 13 letters twice, you will be back were you started. So if you call your function twice on a string, you should get the same string back. + +``rot13(rot13(something)) == something`` + +There is a "short-cut" available that will help you accomplish this task. Some +spelunking in +`the documentation for strings `_ +should help you to find it. If you do find it, using it is completely fair game. + +As usual, add your new file to your local clone right away. Make commits +early and often and include commit messages that are descriptive and concise. + +When you are done, if you want it to be reviewed, submit it via gitHub classroom. + +Try decrypting this: + +"Zntargvp sebz bhgfvqr arne pbeare" + diff --git a/_sources/exercises/series/fib_and_lucas.rst.txt b/_sources/exercises/series/fib_and_lucas.rst.txt new file mode 100644 index 0000000..abaf68f --- /dev/null +++ b/_sources/exercises/series/fib_and_lucas.rst.txt @@ -0,0 +1,120 @@ +.. _exercise_fibonacci: + +************************* +Fibonacci Series Exercise +************************* + +Computing the Fibonacci and Lucas Series +======================================== + +Some nifty uses of recursion + +Goal: +----- + +The `Fibonacci Series`_ is a numeric series starting with the integers 0 and 1. + +In this series, the next integer is determined by summing the previous two + +This gives us:: + + 0, 1, 1, 2, 3, 5, 8, 13, ... + +.. note: 0+1 is 1; 1+1 is 2; 1+2 is 3; 2+3 is 5; 3+5 is 8; and so on forever... + +We will write a function that computes this series -- then generalize it. + +.. _Fibonacci Series: http://en.wikipedia.org/wiki/Fibbonaci_Series + + +Step 1 +------ + +* Create a new file ``series.py`` in the exercise repo. + + - In it, add a function called ``fibonacci``. + + - The function should have one parameter ``n``. + + - The function should return the ``nth`` value in the Fibonacci series (starting with zero index). + +* Ensure that your function has a well-formed "docstring" + +Note that the Fibonacci series is naturally recursive -- the value is +defined by previous values: + +fib(n) = fib(n-2) + fib(n-1) + + +Lucas Numbers +-------------- + +The `Lucas Numbers`_ are a related series of integers that start with the +values 2 and 1 rather than 0 and 1. The resulting series looks like this:: + + 2, 1, 3, 4, 7, 11, 18, 29, ... + +.. _Lucas Numbers: http://en.wikipedia.org/wiki/Lucas_number + + +In your ``series.py`` file, add a new function ``lucas`` that returns the +``nth`` value in the *Lucas numbers* series (starting with zero index). + +Ensure that your function has a well-formed "docstring" + +You should find it's *very* similar to the ``fibonacci()`` function. + + +Generalizing +------------ + +Both the *fibonacci series* and the *lucas numbers* are based on an identical formula: + +f(n) = f(n-2) + f(n-1) + +That's why the code is so similar. + +This formula creates a class of series that are all related -- each with a different two starting numbers. + +Add a third function called ``sum_series`` that can compute all of these related series. + +It should have one required parameter and two optional parameters. +The required parameter will determine which element in the +series to print. +The two optional parameters will have default values of 0 and 1 and will determine the first two values for the series to be produced. + +Calling this function with no optional parameters will produce numbers from the *Fibonacci series* (because 0 and 1 are the defaults). + +Calling it with the optional arguments 2 and 1 will produce values from the *Lucas numbers*. + +Other values for the optional parameters will produce other series. + +**Note:** While you *could* check the input arguments, and then call one +of the functions you wrote, the idea of this exercise is to make a general +function, rather than one specialized. So you should re-implement the code +in this function. + +In fact, you could go back and re-implement your Fibonacci and Lucas +functions to call ``sum-series`` with particular arguments. + +Ensure that your function has a well-formed "docstring" + + +Tests... +-------- + +Add a block of code to the end of your ``series.py`` module. +Use the block to write a series of ``assert`` statements that +demonstrate that your three functions work properly. + +Use comments in this block to inform the observer what your tests do. + +We have created a template for you to use to clarify what we mean by these asserts: + +:download:`series_template.py <./series_template.py>` + +Add your new module to your personal git repo and commit frequently while working on your implementation. +Include good commit messages that explain concisely both *what* you are doing and *why*. + +When you are finished, push your changes to your fork of exercise repository in GitHub and make a pull request to submit it. + diff --git a/_sources/exercises/slicing.rst.txt b/_sources/exercises/slicing.rst.txt new file mode 100644 index 0000000..987c030 --- /dev/null +++ b/_sources/exercises/slicing.rst.txt @@ -0,0 +1,61 @@ +.. _exercise_slicing: + +########### +Slicing Lab +########### + +Goal +==== + +Get the basics of sequence slicing down. + +Tasks +----- + +Write some functions that take a sequence as an argument, and return a copy of that sequence: + +* with the first and last items exchanged. +* with every other item removed. +* with the first 4 and the last 4 items removed, and then every other item in the remaining sequence. +* with the elements reversed (just with slicing). +* with the last third, then first third, then the middle third in the new order. + + - Example: ``(1,2,3,4,5,6)`` should return: ``(5,6,1,2,3,4)`` (start with a length that's a multiple of three, but make sure it doesn't crash for other lengths) + +**NOTE:** These should work with ANY sequence -- but you can use strings to test, if you like. + +Your functions should look like: + +.. code-block:: python + + def exchange_first_last(seq): + return a_new_sequence + + +**Hint:** + +Your functions should work with ALL sequences. That means that you cannot use list methods, like ``.append``, because that won't work with strings and tuples. But all sequences support concatenation with the ``+`` operator. + +Item or Sequence? +................. + +A key difference between using a single index: ``seq[i]`` and using a slice: ``seq[i:j], seq[:i], seq[i:]`` is that using an index returns a single item, whereas a slice always returns a sequence -- even if that sequence is of length one or even empty. And concatenation requires a sequence, so make sure you use slicing if you want to concatenate the results. + +.. note:: Python "gotcha" with strings. Python does not have a character type. Instead of a character, you get a length-one string. This can cause confusion sometimes, as other sequences return a single item when you index, so when you index into a list of numbers, you get number -- which is not a list (or any type of sequence). But with strings, when you index into (or loop through) a string, you get a length-one string, which is, itself a string, and therefor a valid sequence. So: ``a_string[i] + another_string`` works, but ``a_list[i] + another_list`` does not work. + + + +Tests: +------ + +.. code-block:: python + + a_string = "this is a string" + a_tuple = (2, 54, 13, 12, 5, 32) + + assert exchange_first_last(a_string) == "ghis is a strint" + assert exchange_first_last(a_tuple) == (32, 54, 13, 12, 5, 2) + +Write a test or two like that for each of the above functions. + + diff --git a/_sources/exercises/sparse_array.rst.txt b/_sources/exercises/sparse_array.rst.txt new file mode 100644 index 0000000..939ebcb --- /dev/null +++ b/_sources/exercises/sparse_array.rst.txt @@ -0,0 +1,83 @@ +.. _exercise_sparse_array: + +====================== +Sparse Array Exercise +====================== + +Sparse Array +============ + + + Goal: + +Learn how to emulate a built-in class. + +Sparse Array: +------------- + +Oftentimes, at least in computation programming, we have large arrays of data that hold mostly zeros. + +These are referred to as "sparse" as the information in them is widely scattered, or sparse. + +Since they are mostly zeros, it can be memory and computationally efficient to store only the value that are non-zero. + +But you want it to look like a regular array in user code. + +In the real world, these are usually 2 dimensional arrays. But to keep it a bit simpler, we'll make a 1 dimensional sparse array in this class. + +(feel free to make it 2d for an extra challenge!). + +A Sparse array class +-------------------- + +A sparse array class should present to the user the same interface as a regular list. + +Some ideas of how to do that: + +* Internally, it can store the values in a dict, with the index as the keys, so that only the indexes with non-zero values will be stored. + +* It should take a sequence of values as an initializer: + +.. code-block:: python + + sa = SparseArray([1,2,0,0,0,0,3,0,0,4]) + +* you should be able to tell how long it is: + +.. code-block:: python + + len(my_array) + +This will give its "virtual" length -- with the zeros + + +* It should support getting and setting particular elements via indexing: + +.. code-block:: python + + sa[5] = 12 + sa[3] = 0 # the zero won't get stored! + val = sa[13] # it should get a zero if not set + +* It should support deleting an element by index: + +.. code-block:: python + + del sa[4] + +* It should raise an ``IndexError`` if you try to access an index beyond the end. + +* it should have an append() method. + + +* Can you make it support slicing? + +* How else can you make it like a list? + +.. code-block:: ipython + + In [10]: my_array = SparseArray( (1,0,0,0,2,0,0,0,5) ) + In [11]: my_array[4] + Out[11]: 2 + In [12]: my_array[2] + Out[12]: 0 diff --git a/_sources/exercises/string_formatting.rst.txt b/_sources/exercises/string_formatting.rst.txt new file mode 100644 index 0000000..ed20aea --- /dev/null +++ b/_sources/exercises/string_formatting.rst.txt @@ -0,0 +1,240 @@ +.. _exercise_string_formatting: + +########################## +String Formatting Exercise +########################## + +Goal +==== +In this exercise we will reinforce the important concepts of string formatting, so that these start to become second nature! + +Procedure +========= +Create a new file called ``string_formatting.py`` in the exercise repo -- or use the one that's there, if there is one. + +When the empty script is available and runnable, complete the following four tasks. + + +Task One +-------- +* Write a format string that will take the following four values: + + ``(2, 123.4567, 10000, 12345.67)`` + + and produce: + + ``'file_002 : 123.46, 1.00e+04, 1.23e+04'`` + +It should look like: + + ``print("some_stuff_in_here".format(2, 123.4567, 10000, 12345.67))`` + +Let's look at each of the four tuple elements in turn: + +1) The first element is used to generate a filename that can help with file sorting. The idea behind the "file_002" is that if you have a bunch of files that you want to name with numbers that can be sorted, you need to "pad" the numbers with zeros to get the right sort order. + +To illustrate this further let's look at an example: + +.. code-block:: ipython + + In [10]: fnames = ['file1', 'file2', 'file10', 'file11'] + In [11]: fnames.sort() + In [12]: fnames + Out[12]: ['file1', 'file10', 'file11', 'file2'] + +That is probably not what you wanted. However: + +.. code-block:: ipython + + In [1]: fnames = ['file001', 'file002', 'file010', 'file011'] + In [3]: sorted(fnames) + Out[3]: ['file001', 'file002', 'file010', 'file011'] + +That works! + +So you need to find a string formatting operator that will "pad" the number with zeros for you. + +2) The second element is a floating point number. You should display it with 2 decimal places shown. + +3) The third value is an integer, but could be any number. You should display it in scientific notation, with 2 decimal places shown. + +4) The fourth value is a float with a lot of digits -- display it in scientific notation with 3 significant figures. + + +Task Two +-------- + +Using your results from Task One, repeat the exercise, but this time use an alternate type of format string (hint: think about alternative ways to use .format() (keywords anyone?, or indexes?), and also consider f-strings if you've not used them already). + + +Task Three +---------- + +Dynamically Building up format strings +-------------------------------------- + +* Rewrite: + +``"the 3 numbers are: {:d}, {:d}, {:d}".format(1,2,3)`` + +to take an arbitrary number of values. + +Hint: You can pass in a tuple of values to a function with a ``*``: + +.. code-block:: ipython + + In [52]: t = (1,2,3) + + In [53]: "the 3 numbers are: {:d}, {:d}, {:d}".format(*t) + Out[53]: 'the 3 numbers are: 1, 2, 3' + +The idea here is that you may have a tuple of three numbers, but might also have 4 or 5 or 2 or.... + +So you can dynamically build up the format string to accommodate the length of the tuple. + +The string object has the ``format()`` method, so you can call it with a string that is bound to a name, not just a string literal. For example: + +.. code-block:: ipython + + In [16]: form_string = "{:d}, {:d}" + + In [17]: nums = (34, 56) + + In [18]: fstring.format(*nums) + Out[18]: '34, 56' + +So in the example above, how would you make a form_string that was the right length for an arbitrary tuple? + + +Put your code in a function that will return the final string like so: + +.. code-block:: ipython + + In [20]: formatter((2,3,5)) + Out[20]: 'the 3 numbers are: 2, 3, 5' + + In [21]: formatter((2,3,5,7,9)) + Out[21]: 'the 5 numbers are: 2, 3, 5, 7, 9' + +It will look like: + +.. code-block:: python + + def formatter(in_tuple): + do_something_here_to_make_a_format_string + + return form_string.format(*in_tuple) + + +Task Four +---------- + +* Given a 5 element tuple: + + ``( 4, 30, 2017, 2, 27)`` + + use string formating to print: + + ``'02 27 2017 04 30'`` + +Hint: use index numbers to specify positions. + + +Task Five +--------- +f-strings are new to Python (version 3.6), but are very powerful and efficient. This means they are worth understanding and using. And this is made easier than it might be because they use the same, familiar formatting language that is conventionally used in Python (in ``.format()``). + +So in this exercise we are going to specifically use f-strings. + +Here's the simplest example, to show how you can use available variables in a f-string: + +.. code-block:: ipython + + In [2]: name = 'Andy' + In [3]: f'Your name is {name}' + Out[3]: 'Your name is Andy' + +In addition to referencing variables in the local scope, f-strings can evaluate simple expressions in line like so: + +.. code-block:: ipython + + In [5]: f"Your name is {name.upper()}" + Out[5]: 'Your name is ANDY' + + In [6]: name = "andy" + + In [7]: f"Your name is {name.upper()}" + Out[7]: 'Your name is ANDY' + +or + +.. code-block:: ipython + + In [8]: a = 5 + + In [9]: b = 10 + + In [10]: f"The sum is: {a+b}" + Out[10]: 'The sum is: 15' + + +* Here's a task for you: Given the following four element list: + + ``['oranges', 1.3, 'lemons', 1.1]`` + +* Write an f-string that will display: + + ``The weight of an orange is 1.3 and the weight of a lemon is 1.1`` + +* Now see if you can change the f-string so that it displays the names of the fruit in upper case, and the weight 20% higher (that is 1.2 times higher). + + +Task Six +-------- +Often it's convenient to display data in columns. String formatting helps to make this straightforward. + +Suppose you'd like to display something like: + + 'First $99.01 Second $88.09 ' + +One way to do that is: + +.. code-block:: ipython + + '{:20}{:10}{:20}{:8}'.format('First', '$99.01', 'Second', '$88.09') + + +In this simple example everything aligns nicely. But that will not be the case when the numbers to the left of the decimal place vary. +Then you will need to use alignment specifiers. Do some research on this using the links below. Then: + +* Write some Python code to print a table of several rows, each with a name, an age and a cost. Make sure some of the costs are in the hundreds and thousands to test your alignment specifiers. + +* And for an extra task, given a tuple with 10 consecutive numbers, can you work how to quickly print the tuple in columns that are 5 characters wide? It can be done on one short line! + + +Resources on string formatting +============================== + +The official reference docs: + +https://docs.python.org/3/library/string.html#format-string-syntax + +And a more human-readable intro: + +https://pyformat.info/ + +A nice "Cookbook": + +https://mkaz.blog/code/python-string-format-cookbook/ + + +Submitting Your Work +==================== + +Add the file to the develop brnach of your repo for this excercise. + +Make frequent commits with good, clear messages about what you're doing and why. + +When you're done and ready for the instructors to review your work, push your changes to gitHub fork and then go to the gitHub website and make a pull request. + +Copy the gitHub link to the pull request, and provide it to the insstructors when you submit it in your LMS. diff --git a/_sources/exercises/threaded_downloader.rst.txt b/_sources/exercises/threaded_downloader.rst.txt new file mode 100644 index 0000000..b9f2555 --- /dev/null +++ b/_sources/exercises/threaded_downloader.rst.txt @@ -0,0 +1,48 @@ +.. _exercise_downloader: + +#################### +Threaded Web Scraper +#################### + +If you have a lot of web sites (or web services) to hit at once, you may find that you're waiting a long time for each request to return. + +In that case, your computer isn't doing much, and it could be waiting on multiple requests all at once. + +In the examples, we had a news site scraper that used asyncio to asynchronously gather a bunch of data. + +That approach worked well. But it was only about 100 or so simultaneous requests. Async works great for that many, but when it really shines is when you have thousands of (mostly sleeping) connections. + +Another option that works well for tens to hundreds of simultaneous connections is threading. Threading works well for this sort of thing as well, because the GIL isn't a problem -- the GIL is released when the system is waiting for a connection to return + +So your job is to write a version of the newsAPI scraper that uses threads, instead. + +Does is run faster or slower than the async version? + +Did you find it easier or harder to write / understand the code? + +A Queue? +======== + +You will need a way to launch a bunch of threads at once, but probably not one for each request you want to make -- that could be a lot of threads! In this case, 100 or so threads would probably work OK, but if you want the code to be extensible to larger numbers, you may want to pre-define a maximum number of threads in a thread pool. + +Then you'll want to use a job Queue -- and then launch a handful of threads to process those jobs. + +Experiment a bit -- how many threads give you maximum performance? + + +Hints +===== + +Making requests +--------------- +Python has a built-in client http lib (urllib). But almost everyone uses the "requests" package: + +`Requests: HTTP for Humans `_ + +.. code-block:: bash + + $ pip install requests + +Should do it. + + diff --git a/_sources/exercises/trapezoid.rst.txt b/_sources/exercises/trapezoid.rst.txt new file mode 100644 index 0000000..8651603 --- /dev/null +++ b/_sources/exercises/trapezoid.rst.txt @@ -0,0 +1,317 @@ +.. _exercise_trapezoidal_rule: + +################ +Trapezoidal Rule +################ + +**Passing functions around** + +Goal: + +Making use of functions as objects -- functions that act on functions. + + +Trapezoidal rule +---------------- + +The "trapezoidal rule": + +https://en.wikipedia.org/wiki/Trapezoidal_rule + +Is one of the easiest "quadrature" methods. + +Otherwise known as computing a definite integral, or, simply: + +Computing the area under a curve. + +The task +-------- + +Your task is to write a ``trapz()`` function that will compute the area under an arbitrary function, using the trapezoidal rule. + +The function will take another function as an argument, as well as the start and end points to compute, and return the area under the curve. + +Example: +-------- + +.. code-block:: python + + def line(x): + '''a very simple straight horizontal line at y = 5''' + return 5 + + area = trapz(line, 0, 10) + + area + 50 + +About the simplest "curve" you can have is a horizontal straight line, in this case, at y = 5. The area under that line from 0 to 10 is a rectangle that is 10 wide and 5 high, so with an area of 50. + +Of course in this case, it's easiest to simply multiply the height times the width, but we want a function that will work for **Any** curve. + +HINT: this simple example could be a good test case! + +The Solution: +------------- + +Your function definition should look like: + +.. code-block:: python + + def trapz(fun, a, b): + """ + Compute the area under the curve defined by + y = fun(x), for x between a and b + + :param fun: the function to evaluate + :type fun: a function that takes a single parameter + + :param a: the start point for the integration + :type a: a numeric value + + :param b: the end point for the integration + :type b: a numeric value + """ + pass + + +In the function, you want to compute the following equation: + +.. math:: + + area = \frac{b-a}{2N}(f(x_0) + 2f(x_1) + 2f(x_2) + \dotsb + 2f(x_{N-1}) + f(x_N)) + +So you will need to: + + - create a list of x values from a to b (maybe 100 or so values to start) + + - compute the function for each of those values and double them + + - add them all up + + - multiply by the half of the difference between a and b divided by the number of steps. + + +Note that the first and last values are not doubled, so it may be more efficient to rearrange it like this: + +.. math:: + + area = \frac{b-a}{N} \left( \frac{f(x_0) + f(x_{N})}{2} + \sum_{i=1}^{N-1} f(x_i) \right) + +**NOTE:** for those of you confused by that weird big greek letter (no it's a name for a fraternity...) -- see: :ref:`sum_explained` + +Can you use comprehensions for this? + +NOTE: ``range()`` only works for integers -- how can you deal with that? + + +Once you have that, it should work for any function that can be evaluated between a and b. + +Try it for some built-in math functions, like ``math.sin`` + +tests +----- + +Do this using test-drive development. + +A few examples of analytical solutions you can use for tests: + +A simple horizontal line -- see above. + + +A sloped straight line: + +.. math:: + + \int_a^b y = mx + B = \frac{1}{2} m (b^2-a^2) + B (b-a) + +The quadratic: + +.. math:: + + \int_a^b y = Ax^2 + Bx + C = \frac{A}{3} (b^3-a^3) + \frac{B}{2} (b^2-a^2) + C (b-a) + + +The sine function: + +.. math:: + + \int_a^b \sin(x) = \cos(a) - \cos(b) + +Computational Accuracy +---------------------- + +In the case of the linear functions, the result should theoretically be exact. But with the vagaries of floating point math may not be. + +And for non-linear functions, the result will certainly not be exact. + +So you want to check if the answer is *close* to what you expect. + +In py3.5+ -- there is an ``isclose()`` function (PEP485) + +https://www.python.org/dev/peps/pep-0485/ + +Stage Two: +---------- + +Some functions need extra parameters to do their thing. But the above will only handle a single parameter. For example, a quadratic function: + +.. math:: + + y = A x^2 + Bx + C + +Requires values for A, B, and C in order to compute y from an given x. + +You could write a specialized version of this function for each A, B, and C: + +.. code-block:: python + + def quad1(x): + return 3 * x**2 + 2*x + 4 + +But then you need to write a new function for any value of these parameters you might need. + + +Instead, you can pass in A, B and C each time: + +.. code-block:: python + + def quadratic(x, A=0, B=0, C=0): + return A * x**2 + B * x + C + +Nice and general purpose. + +But how would we compute the area under this function? + +The function we wrote above only passes x in to the function it is integrating. + +Passing arguments through: +-------------------------- + +Update your trapz() function so that you can give it a function that takes arbitrary extra arguments, either positional or keyword, after the x. + +So you can do: + +.. code-block:: python + + trapz(quadratic, 2, 20, A=1, B=3, C=2) + +or + +.. code-block:: python + + trapz(quadratic, 2, 20, 1, 3, C=2) + +or + +.. code-block:: python + + coef = {'A':1, 'B':3, 'C': 2} + trapz(quadratic, 2, 20, **coef) + + +**NOTE:** Make sure this will work with ANY function, with **ANY** additional positional or keyword arguments -- not just this particular function. + +This is pretty conceptually challenging -- but it's very little code! + +If you are totally lost -- look at the lecture notes from previous classes -- how can you both accept and pass arbitrary arguments to/from a function? + + +You want your trapz function to take ANY function that can take ANY arbitrary extra arguments -- not just the quadratic function, and not just ``A,B, and C``. So good to test with another example. + +The generalized sine function is: + +.. math:: + + A \sin(\omega t) + +where :math:`A` is the amplitude, and :math:`\omega` is the frequency of the function. In this case, the area under the curve from a to b is: + +.. math:: + + \frac{A}{\omega} \left( \cos(\omega a) - \cos(\omega b) \right) + +The test code has a test for this one, too. + +Currying +-------- + +Another way to solve the above problem is to use the original ``trapz``, and create a custom version of the quadratic() function instead. + +Write a function that takes ``A, B, and C`` as arguments, and returns a function that evaluates the quadratic for those particular coefficients. + +Try passing the results of this into your ``trapz()`` and see if you get the same answer. + +partial +------- + +Do the above with ``functools.partial`` as well. + +Extra credit +------------ + +This isn't really the point of the exercise, but see if you can make it dynamically accurate. + +How accurate it is depends on how small the chunks are that you break the function up into. + +See if you can think of a way to dynamically determine how small a step you should use. + +This is one for the math and computational programming geeks! + + +.. _sum_explained: + +A bit about math symbology +-------------------------- + +Those of you without a lot of math background may be confused by the symbols. So here's a quick intro to the "Summation Symbol" (Greek Capital sigma) + +.. math:: + + \sum_{i=a}^{b} x_i + +.. \sum_{i=1}^{N-1} f(x_i) \right) + +Is shorthand for "add up a bunch of values, with varying i from a to b". where each x is a different value each time. Translating this into code you get: + +.. code-block:: python + + x = a_list_of_numbers + total = 0 + for i in range(a, b+1): + total += x[i] + +or, in more compact python: + +.. code-block:: python + + x = an iterable_of_numbers + total = sum(x[a:b+1]) + +So the full expression used above: + +.. math:: + + \sum_{i=1}^{N-1} fun(x_i) + +Can be written as: + +.. code-block:: python + + sum(fun(x) for x in list_of_x[1:-1]) + + + + + + + + + + + + + + + + diff --git a/_sources/exercises/trigrams/trigrams.rst.txt b/_sources/exercises/trigrams/trigrams.rst.txt new file mode 100644 index 0000000..5b28c14 --- /dev/null +++ b/_sources/exercises/trigrams/trigrams.rst.txt @@ -0,0 +1,620 @@ +.. _exercise_trigrams: + +==================================== +Trigrams -- Simple Text Manipulation +==================================== + +.. rubric:: Kata Fourteen: Tom Swift Under the Milk Wood + +Adapted from Dave Thomas's work: + +http://codekata.com/kata/kata14-tom-swift-under-the-milkwood/ + + +Trigrams +========= + +Trigrams can be used to mutate text into new, surreal, forms. But what +heuristics do we apply to get a reasonable result? + +The Problem +------------ + +As a boy, one of my treats was go to the shops on a Saturday and spend part +of my allowance on books; for a nine-year old, I had quite a collection of books from the +Tom Swift and Hardy Boys series. Wouldn’t it be great to be able to create +more and more of these classic books, to be able to generate a new Tom +Swift adventure on demand? + + +OK, perhaps not. But that won’t stop us trying. I coded up a quick +program to generate some swashbuckling scientific adventure on demand. It +came up with: + + ... it was in the wind that was what he thought was his companion. I + think would be a good one and accordingly the ship their situation + improved. Slowly so slowly that it beat the band! You’d think no one + was a low voice. "Don’t take any of the elements and the + inventors of the little Frenchman in the enclosed car or cabin completely + fitted up in front of the gas in the house and wringing her hands. + "I’m sure they’ll fall!" + + She looked up at them. He dug a mass of black vapor which it had + refused to accept any. As for Mr. Swift as if it goes too high I’ll + warn you and you can and swallow frequently. That will make the airship was + shooting upward again and just before the raid wouldn’t have been + instrumental in capturing the scoundrels right out of jail." + + +Stylistically, it’s Victor Appleton (pseudonymous author of the Tom Swift series) meets Dylan Thomas (Welsh poet). Technically, +it’s all done with trigrams. + +Trigram analysis is very simple. Look at each set of three adjacent words +in a document. Use the first two words of the set as a key, and remember +the fact that the third word followed that key. Once you’ve finished, +you know the list of individual words that can follow each two word +sequence in the document. For example, given the input:: + + I wish I may I wish I might + +You might generate:: + + "I wish" => ["I", "I"] + "wish I" => ["may", "might"] + "may I" => ["wish"] + "I may" => ["I"] + + +This says that the words "I wish" are twice followed by the word +"I", the words "wish I" are followed once by "may" and once by "might" +and so on. + +To generate new text from this analysis, choose an arbitrary word pair as a +starting point. Use this pair of words to look up a random next word (using the table +above) and append this new word to the text so far. This now gives you three words with a +new word pair (second and third words) at the end of the three-word text. Look up a potential next word +based on this pair. This generates another pair to add to the list, and so on. In the previous example, +we could start with "I may". The only possible next word is +"I", so now we have:: + + I may I + +The last two words are "may I," so the next word is +"wish". We then look up "I wish," and find our choice +is constrained to another "I":: + + I may I wish I + + +Now we look up "wish I," and find we have a choice. Let’s +choose "may":: + + I may I wish I may + +Now we’re back where we started from, with "I may." +Following the same sequence, but choosing "might" this time, we +get:: + + I may I wish I may I wish I might + +At this point we stop, as no sequence starts "I might." + + +Given a short input text, the algorithm isn’t too interesting. Feed +it a book, however, and you give it more options, so the resulting output +can be surprising. + +For this exercise, try implementing a trigram algorithm that generates a couple +of hundred words of text using a book-sized file as input. +`Project Gutenberg `_ is a good source of online +books (*Tom Swift and His Airship* is `here `_.) + +Be warned that these files have DOS line endings (carriage return followed by +newline). + + +Here is a copy of short-story collection *The Adventures of Sherlock Holmes*: + +:download:`sherlock.txt <./sherlock.txt>`. + +And a shorter copy for testing (a paragraph from one of the stories, "A Scandal in Bohemia"): + +:download:`sherlock_small.txt <./sherlock_small.txt>`. + + +Objectives +----------- + +Katas are about trying something many times. In this one, what +we’re experimenting with is not just the code, but the heuristics of +processing the text. What do we do with punctuation? Paragraphs? Do we have +to implement backtracking if we chose a next word that turns out to be a +dead end? + +I’ll fire the signal and the fun will commence... + +Developing Your Solution +======================== + +This assignment has two parts: the key one is the trigrams exercise itself, but you also need to do some text processing to get a full book in shape for the building the trigrams. + +I suggest you write the trigrams part first; it's more interesting :-) + + +Test Driven Development +----------------------- + +You've recently learned about unit testing and Test Driven Development (TDD). Let's put that to work on this exercise. Remember that the key to TDD is that you first decide on what you need a piece of your code to do, then you write a test to check that it does that, and only then do you write the actual code itself. + +Because you're new to this, we're going to give you some tests to get you started. You'll find them in: :download:`test_trigrams.py <./test_trigrams.py>`, which should be with this assignment. + +Running the Tests +................. + +To run the tests, use the ``pytest`` test runner: set your working directory the dir with the test file, and run the ``pytest`` command: + +.. code-block:: bash + + $ pytest + ======================= test session starts ======================= + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.8.2, pluggy-0.13.1 + rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/exercises/trigrams + collected 0 items / 1 error + + ============================= ERRORS ============================== + ________________ ERROR collecting test_trigrams.py ________________ + ImportError while importing test module '/Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/exercises/trigrams/test_trigrams.py'. + Hint: make sure your test modules/packages have valid Python names. + Traceback: + test_trigrams.py:17: in + import trigrams + E ModuleNotFoundError: No module named 'trigrams' + ===================== short test summary info ===================== + ERROR test_trigrams.py + !!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!! + ======================== 1 error in 0.13s ========================= + +You should have gotten something like that error: it is indicating that the "trigrams" module does not exist -- which makes sense, as you haven't written it yet. So the first step is to create your code file: name it ``trigrams.py`` and put it in the same directory as the ``test_trigrams.py`` file. It can be empty for now. Now try running pytest again, and it should get farther: you'll have a lot of test failures, but the test should actually run. You should get something like: + +.. code-block:: bash + + test_trigrams.py:130: AttributeError + ====================== short test summary info ======================= + FAILED test_trigrams.py::test_trigrams_pairs - AttributeError: modu... + FAILED test_trigrams.py::test_trigrams_following_words - AttributeE... + FAILED test_trigrams.py::test_pick_random_pair - AttributeError: mo... + FAILED test_trigrams.py::test_get_last_pair - AttributeError: modul... + FAILED test_trigrams.py::test_get_random_follower - AttributeError:... + FAILED test_trigrams.py::test_get_random_follower_not_there - Attri... + FAILED test_trigrams.py::test_make_sentence - AttributeError: modul... + ========================= 7 failed in 0.18s ========================== + + +You get a bunch of AttributeErrors, as you haven't defined anything in your ``trigrams.py`` file. So now it's time to actually work on the code! + +trigrams +-------- + +Key to the trigrams problem is the selection of the data structure to use to hold the "trigrams" themselves. What do we need here? + +The text +........ + +First, you'll want a bit of text to try your code out on. Why not try the example here:: + + I wish I may I wish I might + +You need that in a python data structure somehow, so how about: + +.. code-block:: python + + words = "I wish I may I wish I might".split() + +This produces an (ordered) list of words:: + + ['I', 'wish', 'I', 'may', 'I', 'wish', 'I', 'might'] + +Now you've got some words to play with. Once you think you've got it working, try a bit longer piece of text. But this will do for now, and it's small and simple enough that you can immediately see if your code is working. + +You will find that example in the test file, so we can write tests against it: + +.. code-block:: python + + IWISH = "I wish I may I wish I might".split() + + +The trigrams structure +---------------------- + +Now we need to think a bit about what we want the trigrams structure to be. + +From above, we know that we need to build up something like this:: + + "I wish" => "I", "I" + "wish I" => "may", "might" + "may I" => "wish" + "I may" => "I" + +Hmmm, in a way, that's almost pseudo code. You have a bunch of word pairs, and for each word pair, there are one or more words that follow it. + +Those following words look a lot like they could be in a list, yes? Perfect, the list structure keeps order, and you can keep adding (appending) new words to it. + +Each of those lists of words needs to be mapped to a particular pair. Each pair is unique; it only shows up once (when that same pair is encountered again in the text, you add the follower to the list of following words). + +That sounds a lot like a dictionary. The keys (word pairs) are unique, and map to a list of following words. (Note that, technically, in python the dictionary is only one implementation of a +`Mapping `_.) + +Now you have a choice of data structures for the word pairs, or keys in the dict: a string or a tuple. + +**String**: The keys are a pair of words and can be represented as a string of two words with a space like so: + +.. code-block:: python + + trigrams = {"I wish": ["I", "I"], + "wish I": ["may", "might"], + "may I": ["wish"], + "I may": ["I"], + } + +**Tuple**: But strings are not the only type that you can use as keys in a dictionary; you can use any *immutable* type. Recall that tuples are immutable (they can't be changed once they have been created). Since each pair of words is, well, a pair, it makes sense to store each pair in a tuple, keeping the individual words separate: + +.. code-block:: python + + trigrams = {("I", "wish"): ["I", "I"], + ("wish", "I"): ["may", "might"], + ("may", "I"): ["wish"], + ("I", "may"): ["I"], + } + +I like the version that uses tuples better, but either one will work. The test code is designed to check for word pairs in tuples. If you want to write your code using space separated strings, you can modify the tests. + +Building the Trigrams dict +.......................... + +So you've got a list of words, and you need to build up a dict like one of the above. + +It's time to create a python file and start writing some code! + +Put this in your ``trigrams.py`` file + +.. code-block:: python + + #!/usr/bin/env python3 + + def build_trigram(words): + """ + build up the trigrams dict from the list of words + + :param words: a list of individual words in order + + :returns: a dict with: + keys: word pairs in tuples + values: list of the words that follow the pain in the key + """ + + trigrams = {} + + # build up the dict here! + + return trigrams + + +So how do you actually build up that dict? That's kind of the point of the exercise, so I won't tell you that ... but here are some hints: + +**Looping through the words** + +Obviously you need to loop through all the words, so a ``for`` loop makes sense. However, this is a bit tricky. Usually in Python you loop through all the items in a list, and don't worry about the indexes: + +.. code-block:: python + + for item in a_list: + ... + +But in this case, we don't need to work with one word at a time, we need to work with three at a time (a pair of words, and the single word that follows it). +So contrary to the usual practice, an index can be helpful here: + +.. code-block:: python + + for i in range(len(words)-2): # why -2 ? + pair = words[i:i + 2] + follower = words[i + 2] + +**Adding a pair to the dict:** + +For each pair in the text, you need to add it to the dict. But: + +- ``words[i:i + 2]`` is a list with two words in it. Can that be used as a key in a dict? (try it.) If not, how can you make a valid key out of it? + +- As you loop through the text, you will collect pairs of words. Each time, a given pair may already be in the dict. + + - If the pair is not in the dict, you want to put it in the dict, with value being a list with the follower in it:: + + ("may", "I"): ["wish"] + + - If the pair already is in the dict, then you want to add the follower (the second word in the pair) to the list that's already there:: + + ("wish", "I"): ["may", "might"] + +Note that the description above suggests the basic logic; it's almost pseudo-code. And that logic will work. But it turns out that this is a common enough operation that python dicts have a method that lets you do that logic in one step? Can you find it? + +`Python dict Documentation `_ + +As you develop this code, run the tests each time you think you have made some progress:: + + $ pytest test_trigrams.py + +In that test file, there are two tests of the `trigrams()` function: One that tests that it gets the right word pairs as keys: + +.. code-block:: python + + def test_trigrams_pairs(): + """ + test that the build_trigram function creates the right pairs of words + """ + tris = trigrams.build_trigram(IWISH) + + pairs = tris.keys() + + # using a set here, as the dict_keys object is a set as well + # And keys are always unique and hashable + # and the order does not matter, so perfect for a set + assert pairs == {("I", "wish"), + ("wish", "I"), + ("may", "I"), + ("I", "may"), + } + +and one that tests if the following word lists are correct: + +.. code-block:: python + + def test_trigrams_following_words(): + """ + test that the following words are correct + """ + tris = trigrams.build_trigram(IWISH) + + # this will only print if the test fails + # but if if does, you can see what's going on to try to fix it. + print(tris) + + # a separate assert for each pair: + assert tris[("I", "wish")] == ["I", "I"] + assert tris[("wish", "I")] == ["may", "might"] + assert tris[("may", "I")] == ["wish"] + assert tris[("I", "may")] == ["I"] + +Note that if the first test fails, almost certainly the second will too (the second test explicitly looks for all the same keys). That's OK. It's still good to keep them separate, because the first test could pass while the second one fails -- it's nice to know you've made progress! + +If it seems like we have hard-coded a lot of detail into the tests -- you are right. But this is quite deliberate. And it is why we chose such a simple set of words to start out with. If you want to read a bit more about this approach, this blogger puts it nicely: +`Write Explicit Tests `_ + +If both tests pass, you should now have code that will return a dict like we noted above:: + + {("I", "wish"): ["I", "I"], + ("wish", "I"): ["may", "might"], + ("may", "I"): ["wish"], + ("I", "may"): ["I"]} + +Try it out on a longer bit of text (your choice) before you go any further. If it doesn't work correctly, make sure to write a test that catches the problem before you fix it! + + +Using the Trigrams dict +....................... + +This is the fun part. Once you have a mapping of word pairs to following words, you can build up some new "fake" text. Re-read the previous sections again to remind yourself of the procedure. Here are a couple of additional hints and questions to consider: + +- The ``random`` module: https://docs.python.org/3/library/random.html#module-random is your friend here: + +.. code-block:: python + + import random + + # returns a number between a and b (including a and b) + random.randint(a, b) + + # pick a random item from a sequence + random.choice(a_list) + +This is all pretty tricky to test -- after all, you are selecting random words -- you can't know what the result should be! There are two tactics you can take to test code with ``random`` calls in it. + +Tactic one is to break you code down into pieces that you *can* test -- everything BUT the random choices. + +Tactic two is to set the random seed before each test, to assure the same result. +The built in ``random`` module `provides a way to set the seed `_: the ``random.seed()`` function. + +.. note:: Computers don't really make truly random numbers. What they do is compute a sequence of numbers that are statistically very much like random numbers, known as `"pseudo random numbers" `_. If you start with the same initial value, known as the "seed", then you will get the same sequence of numbers (`random seed `_). + +The provided tests use both of these tactics. + +- You need to start with the first word pair; picking a random key from a dict is actually a bit tricky. But we have a test for it: + +.. code-block:: python + + def test_pick_random_pair(): + test_pairs = {("one", "two"): [], + ("one", "three"): [], + ("four", "five"): [], + ("six", "seven"): [], + ("eight", "nine"): [], + } + # set the seed so we'll always get the same one + random.seed(1234) + pair = trigrams.pick_random_pair(test_pairs) + print("the pair is:", pair) + assert pair == ('six', 'seven') + +So you'll need to define a function: ``pick_random_pair()`` that takes your trigram dict as input, and returns a random key. + +Note that the particular result in the test is using a particular algorithm -- if you use a different one, you might get a different pair -- but since the seed is set, you should get the same one every time the test is run, so you can make the test check for the one your code returns. + +Once you've got the first starting pair, you'll need to make your text, +so you'll need a data structure to build it up in. You probably want to build it up in a list, appending one word at a time. You can join it together at the end with ``" ".join(the_list_of_words)``, which will make a string, separating the words with a space. + +Remember that after adding a word to a pair to make a three-word text, the next pair is the last two words in that three-word text. + +Here is a test for that step: + +.. code-block:: python + + def test_get_last_pair(): + words = ["this", "that", "the", "other"] + + assert trigrams.get_last_pair(words) == ("the", "other") + +write a function: ``get_last_pair()`` that takes a list of words, and returns the last two words as a tuple. + +Then you'll need to pick a random word from the "followers" -- the words that followed that pair of words in the original text. There is a test for that, too: + +.. code-block:: python + + def test_get_random_follower(): + """ + test getting a random word from the trigrams dict + """ + # we only need one entry for this test + tri_dict = {("one", "two"): ["four", "five", "six", "seven"]} + + # set the seed so the answer will be consistent + random.seed(1234) + word = trigrams.get_random_follower(tri_dict, ("one", "two")) + print("got word:", word) + assert word == "seven" + +Again, this sets the random seed so that you will always get the same answer. If your code returns a different word -- change the test to match. + +But what if the word pair is not in the dict? It's not that likely in a long text, but it can happen. So make sure that your code handles that situation by making sure it passes this test: + +.. code-block:: python + + def test_get_random_follower_not_there(): + """ + test what happens when the word pair is not there + """ + # we only need one entry for this test + tri_dict = {("one", "two"): ["four", "five", "six", "seven"]} + + # here's a word pair that isn't there + # make sure you get something back! + word = trigrams.get_random_follower(tri_dict, ("one", "one")) + print("got word:", word) + assert word # this asserts that you got a non-empty string + +Note that there are a number of options here as to what to do -- but make sure it returns *something*. + +Putting it Together +................... + +You now have the pieces you need to make some new text. Let's write a function that will make a single sentence a specified number of words long. The first word should be capitalized, and it should end with a period. Here is the test for that function: + +.. code-block:: python + + def test_make_sentence(): + """ + test making a trigrams sentence + + as it is supposed to be random, this tests for things other than + the actual results. + + NOTE that this test relies on the build_trigram() function, so it + will fail if that doesn't work. + """ + # use the already tested build_trigram function to make the dict + tri_dict = trigrams.build_trigram(LONGER_TEXT) + + + # make a sentence of 6 words + sentence = trigrams.make_sentence(tri_dict, 6) + + print(sentence) + # check that it has 6 words + assert len(sentence.split()) == 6 + # check that the first letter is a capital + assert sentence[0] == sentence[0].upper() + # check that it ends with a period + assert sentence[-1] == "." + # check that there is not a space between the period and the last word. + assert not sentence[-2].isspace() + +Notice that this test did not set the random seed. Rather, it checked for various properties of the results, without checking for specific words. This is a helpful tactic -- have your tests check for what is important about the results, not necessarily the specific results. + +You can now use the previous functions to make a ``make_sentence()`` function that passes these tests. + + +Once you have the basics working, try your code on a longer piece of input text. Then think about making it fancy: put a number of sentences of random length to form a paragraph? Add in some other random punctuation? Anything else to make the text more "real"? + + +Processing the Input Text +------------------------- + +If you get a book from Project Gutenberg (or anywhere else), it will not be "clean." That is, it will have header information, footer information, chapter headings, punctuation, what have you. So you'll need to clean it up somehow to get a simple list of words to use to build your trigrams. + +The first part of the process is pretty straightforward; open the file and loop through the lines of text and process them. + +You may want to skip the header. How would you do that?? + +Hint: in a Project Gutenberg e-book, there is a line of text that starts with:: + + *** START OF THIS PROJECT GUTENBERG EBOOK + +In the loop, you can process a single line of text to break it into words by calling ``.split()``. + +Optional steps to cleaning up the text: + + - Strip out punctuation? + - If you do this, what about contractions, i.e. the apostrophe in "can't" vs. a single quotation mark -- which are the same character. + + - Remove capitalization? + - If you do this, what about "I"? And proper nouns? + +Any other ideas you may have. + +Be sure to use TDD as you develop the "clean up" code: write a test for one feature, and then make sure your code passes that test. + +Lather, rinse and repeat. + +There are a number of tests for cleaning up the code commented out in the test file. Feel free to use these as a starting point. + + +**Hints:** + +The ``string`` methods are your friend here. + +There are also handy constants in the ``string`` module: ``import string`` +(https://docs.python.org/3/library/string.html) + +Check out the ``str.translate()`` method; it can make multiple replacements very fast. + +Do get the full trigrams code working first, then play with some of the fancier options. + + +Code Structure +-------------- + +You will have found that following TDD forces you to break your code down into a handful of separate functions, each of which does only one thing. This lets you test each function on its own, and it's easier to refactor one part without messing with the others. Then you can put them all together into a simple program. + + +For instance, your ``__main__`` block might look something like: + +.. code-block:: python + + if __name__ == "__main__": + # get the filename from the command line + try: + filename = sys.argv[1] + except IndexError: + print("You must pass in a filename") + sys.exit(1) + + in_data = read_in_data(filename) + words = make_words(in_data) + word_pairs = build_trigram(words) + new_text = build_text(word_pairs) + + print(new_text) + +**Have Fun!** + + + + diff --git a/_sources/exercises/unit_testing.rst.txt b/_sources/exercises/unit_testing.rst.txt new file mode 100644 index 0000000..9728eb3 --- /dev/null +++ b/_sources/exercises/unit_testing.rst.txt @@ -0,0 +1,124 @@ +.. _exercise_unit_testing: + +############################ +Introduction To Unit Testing +############################ + +Preparation +----------- + +In order to do unit testing, you need a framework in which to write and run your tests. +Earlier in this class, you've been adding "asserts" to your modules -- perhaps in the ``__name__ == "__main__"`` block. These are, in fact, a kind of unit test. +But as you build larger systems, you'll want a more structured way to write and run your tests. + +We will use the pytest testing system for this class. + +If you have not already done so -- install pytest like so: + +.. code-block:: bash + + $ python3 -m pip install pytest + +Once this is complete, you should have a ``pytest`` command you can run +at the command line: + +.. code-block:: bash + + $ pytest + ====================== test session starts ====================== + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.8.2, pluggy-0.13.1 + rootdir: /Users/chris.barker/Junk/junk + collected 0 items + + ===================== no tests ran in 0.00s ===================== + + +If you already HAVE some tests -- you may see something different! + + +Test Driven Development +----------------------- + +Download these files, or find them in the exercise repo: + +:download:`test_walnut_party.py ` + +and: + +:download:`walnut_party.py ` + +(This is the adapted from the codingbat site: http://codingbat.com/prob/p195669) + +In the directory where you put the files, run: + +.. code-block:: bash + + $ pytest test_walnut_party.py + +You will get a LOT of test failures! + +What you've done here is the first step in what is called: + +.. centered:: **Test Driven Development (TDD)** + +A bunch of tests exist, but the code to make them pass does not yet exist. + +The red you see in the terminal when we run our tests is a goad to us to write the code that fixes these tests. + +The tests all failed because currently ``walnut_party()`` looks like: + +.. code-block:: python + + def walnut_party(walnuts, is_weekend): + pass + +A totally do nothing function -- of course the tests all fail! + + +Making tests pass +----------------- + +Open: + +``test_walnut_party.py`` + +and: + +``walnut_party.py`` + +In your editor. + +Now edit the function in ``walnut_party.py``, and each time you make a change, run the tests again. Continue until all the tests pass. + +When the tests pass -- you are done! That's the beauty of test-driven development. + +Doing your own: +--------------- + +Pick another example from codingbat: + +``http://codingbat.com`` + +Do a bit of test-driven development on it: + +* Run something on the web site. +* Write a few tests using the examples from the site. +* Then write the function, and fix it 'till it passes the tests. + +These tests should be in a file named ``test_something.py`` -- I usually name the test file the same as the module it tests, +with ``test_`` prepended. + +.. note:: + Technically, you can name your test files anything you want. But there are two reasons to use standard naming conventions. + One is that it is clear to anyone looking at the code what is and isn't a test module. The other is that pytest, and other testing systems, use + `naming conventions `_ to find your test files. + If you name your test files: ``test_something.py`` then pytest will find them for you. And if you use the name of the module being tested: + ``test_name_of_tested_module.py`` then it will be clear which test files belong to which modules. + + +Do at least two of these to get the hang of the process. + +Also -- once you have the tests passing, look at your solution -- is there a way it could be refactored to be cleaner? + +Give it a shot -- you'll know if it still works if the tests still pass! + diff --git a/_sources/exercises/unit_testing/unit_testing.rst.txt b/_sources/exercises/unit_testing/unit_testing.rst.txt new file mode 100644 index 0000000..e8658e9 --- /dev/null +++ b/_sources/exercises/unit_testing/unit_testing.rst.txt @@ -0,0 +1,124 @@ +.. _exercise_unit_testing: + +############################ +Introduction To Unit Testing +############################ + +Preparation +----------- + +In order to do unit testing, you need a framework in which to write and run your tests. +Earlier in this class, you've been adding "asserts" to your modules -- perhaps in the ``__name__ == "__main__"`` block. These are, in fact, a kind of unit test. +But as you build larger systems, you'll want a more structured way to write and run your tests. + +We will use the pytest testing system for this class. + +If you have not already done so -- install pytest like so: + +.. code-block:: bash + + $ python3 -m pip install pytest + +Once this is complete, you should have a ``pytest`` command you can run +at the command line: + +.. code-block:: bash + + $ pytest + ====================== test session starts ====================== + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.8.2, pluggy-0.13.1 + rootdir: /Users/chris.barker/Junk/junk + collected 0 items + + ===================== no tests ran in 0.00s ===================== + + +If you already HAVE some tests -- you may see something different! + + +Test Driven Development +----------------------- + +Download these files, or find them in the exercise repo: + +:download:`test_walnut_party.py ` + +and: + +:download:`walnut_party.py ` + +(This is the adapted from the codingbat site: http://codingbat.com/prob/p195669) + +In the directory where you put the files, run: + +.. code-block:: bash + + $ pytest test_walnut_party.py + +You will get a LOT of test failures! + +What you've done here is the first step in what is called: + +.. centered:: **Test Driven Development (TDD)** + +A bunch of tests exist, but the code to make them pass does not yet exist. + +The red you see in the terminal when we run our tests is a goad to us to write the code that fixes these tests. + +The tests all failed because currently ``walnut_party()`` looks like: + +.. code-block:: python + + def walnut_party(walnuts, is_weekend): + pass + +A totally do nothing function -- of course the tests all fail! + + +Making tests pass +----------------- + +Open: + +``test_walnut_party.py`` + +and: + +``walnut_party.py`` + +In your editor. + +Now edit the function in ``walnut_party.py``, and each time you make a change, run the tests again. Continue until all the tests pass. + +When the tests pass -- you are done! That's the beauty of test-driven development. + +Doing your own: +--------------- + +Pick another example from codingbat: + +``http://codingbat.com`` + +Do a bit of test-driven development on it: + +* Run something on the web site. +* Write a few tests using the examples from the site. +* Then write the function, and fix it 'till it passes the tests. + +These tests should be in a file named ``test_something.py`` -- I usually name the test file the same as the module it tests, +with ``test_`` prepended. + +.. note:: + Technically, you can name your test files anything you want. But there are two reasons to use standard naming conventions. + One is that it is clear to anyone looking at the code what is and isn't a test module. The other is that pytest, and other testing systems, use + `naming conventions `_ to find your test files. + If you name your test files: ``test_something.py`` then pytest will find them for you. And if you use the name of the module being tested: + ``test_name_of_tested_module.py`` then it will be clear which test files belong to which modules. + + +Do at least two of these to get the hang of the process. + +Also -- once you have the tests passing, look at your solution -- is there a way it could be refactored to be cleaner? + +Give it a shot -- you'll know if it still works if the tests still pass! + diff --git a/_sources/index.rst.txt b/_sources/index.rst.txt new file mode 100644 index 0000000..cbd2abd --- /dev/null +++ b/_sources/index.rst.txt @@ -0,0 +1,57 @@ +.. Python 210 documentation master file, created by + sphinx-quickstart on Sun May 31 20:06:00 2020. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +##################### +Programming in Python +##################### + +This site holds many of the materials for the +`University of Washington Professional and Continuing Education Python Certificate `_ +`Introductory class `_ + +This site can be thought of as the textbook for Programming in Python: the first course in the program. It contains notes about the topics covered in the classes, programming exercises, supplemental materials about setting up a development environment, and assorted references about Python-related topics. + +Many of these topics can be useful on their own, but each assumes that you know concepts that were introduced earlier in the program, so working through them in order can be helpful. + + +.. toctree:: + :numbered: 1 + :caption: Topics in the Program + :titlesonly: + :maxdepth: 1 + :glob: + + topics/??-*/index + + +About this Site +=============== + +These materials are Open Source, released under the Creative Commons Attribution-ShareAlike 4.0 license. + +They are built with the Sphinx documentation system, utilizing Restructured Text (rst) markup. + +It is managed in this gitHub repository: + +https://github.com/UWPCE-PythonCert/ProgrammingInPython + +Readers are encouraged to report omissions, typos, or make suggestions for improvements via issues and pull requests on that repository. + + +Example Code +============ + +Assorted Example code can be found in the source repository for these documents. Most of the examples are linked to directly from these documents, but it might be helpful to have them all in one place: + +https://github.com/UWPCE-PythonCert/ProgrammingInPython/tree/master/source/examples + + +.. Indices and tables +.. ================== + +.. * :ref:`genindex` +.. * :ref:`modindex` + +* :ref:`search` diff --git a/_sources/modules/AdvancedArgumentPassing.rst.txt b/_sources/modules/AdvancedArgumentPassing.rst.txt new file mode 100644 index 0000000..cc5f159 --- /dev/null +++ b/_sources/modules/AdvancedArgumentPassing.rst.txt @@ -0,0 +1,322 @@ +.. _advanced_argument_passing: + +######################### +Advanced Argument Passing +######################### + +This is a very, very nifty Python feature -- it really lets you write dynamic programs. + +Keyword arguments +================= + +When defining a function, you can specify only what you need -- in any order + +.. code-block:: ipython + + In [151]: def fun(x=0, y=0, z=0): + print(x,y,z) + .....: + In [152]: fun(1,2,3) + 1 2 3 + In [153]: fun(1, z=3) + 1 0 3 + In [154]: fun(z=3, y=2) + 0 2 3 + + +A Common Idiom: +--------------- + +.. code-block: python + + def fun(x, y=None): + if y is None: + do_something_different + go_on_here + + + +Can set defaults to variables +----------------------------- + +.. code-block:: ipython + + In [156]: y = 4 + In [157]: def fun(x=y): + print("x is:", x) + .....: + In [158]: fun() + x is: 4 + + +Defaults are evaluated when the function is defined + +.. code-block:: ipython + + In [156]: y = 4 + In [157]: def fun(x=y): + print("x is:", x) + .....: + In [158]: fun() + x is: 4 + In [159]: y = 6 + In [160]: fun() + x is: 4 + +This is a **very** important point. + + +Function arguments in variables +------------------------------- + +When a function is called, its arguments are really just: + +* a tuple (positional arguments) +* a dict (keyword arguments) + +.. code-block:: python + + def f(x, y, w=0, h=0): + print("position: {}, {} -- shape: {}, {}".format(x, y, w, h)) + + position = (3,4) + size = {'h': 10, 'w': 20} + + >>> f(*position, **size) + position: 3, 4 -- shape: 20, 10 + + +Function parameters in variables +-------------------------------- + +You can also pull the parameters out in the function as a tuple and a dict: + +.. code-block:: ipython + + def f(*args, **kwargs): + print("the positional arguments are:", args) + print("the keyword arguments are:", kwargs) + + In [389]: f(2, 3, this=5, that=7) + the positional arguments are: (2, 3) + the keyword arguments are: {'this': 5, 'that': 7} + +This can be very powerful... + +Passing a dict to str.format() +------------------------------- + +Now that you know that keyword args are really a dict, +you know how this nifty trick works: + +The string ``format()`` method takes keyword arguments: + +.. code-block:: ipython + + In [24]: "My name is {first} {last}".format(last="Barker", first="Chris") + Out[24]: 'My name is Chris Barker' + +Build a dict of the keys and values: + +.. code-block:: ipython + + In [25]: d = {"last":"Barker", "first":"Chris"} + +And pass to ``format()`` with ``**`` + +.. code-block:: ipython + + In [26]: "My name is {first} {last}".format(**d) + Out[26]: 'My name is Chris Barker' + +Kinda handy for the dict lab, eh? + +This: + +.. code-block:: ipython + + print("{} is from {}, and he likes " + "{} cake, {} fruit, {} salad, " + "and {} pasta.".format(food_prefs["name"], + food_prefs["city"], + food_prefs["cake"], + food_prefs["fruit"], + food_prefs["salad"], + food_prefs["pasta"])) + +Becomes: + +.. code-block:: ipython + + print("{name} is from {city}, and he likes " + "{cake} cake, {fruit} fruit, {salad} salad, " + "and {pasta} pasta.".format(**food_prefs)) + +Note that this is particularity useful when the same value is used in multiple places in the format string. + +.. _keyword_only_arguments: + +Keyword Only Arguments +====================== + +The usual function signature looks something like: + +.. code-block:: python + + def fun (pos1, pos2, key1='this', key2='that'): + print(pos1, pos2, key1, key2) + +In this case, we have two positional parameters and two keyword parameters. + +But all four can be passed as either positional or keyword arguments: + +.. code-block:: ipython + + In [21]: fun(1,2,3,4) + 1 2 3 4 + + In [22]: fun(pos1=1, pos2=2, key1=3, key2=4) + 1 2 3 4 + +or out of order: + +.. code-block:: ipython + + In [23]: fun(key1=1, pos2=2, pos1=3, key2=4) + 3 2 1 4 + +And the positional arguments are all required: + +.. code-block:: ipython + + In [24]: fun(3) + --------------------------------------------------------------------------- + TypeError Traceback (most recent call last) + in () + ----> 1 fun(3) + + TypeError: fun() missing 1 required positional argument: 'pos2' + +**But:** Notice that you can either have a required argument with no keyword, or an optional argument with a keyword (and a default). And keyword arguments can also be passed as positional arguments. + +This was considered less than ideal -- with some APIs, you want to require a keyword be used -- and you may have a required argument that you want users to pass as a keyword (rather than positional) argument. + +In Python 3 -- "keyword only" arguments were added: + +https://www.python.org/dev/peps/pep-3102/ + +So you can do: + +.. code-block:: python + + def fun (pos1, pos2, *, key1='this'): + print(pos1, pos2, key1) + +Now the user can only provide a value for key1 as a keyword argument. If they pass a third positional argument, it'll be an error: + +.. code-block:: ipython + + In [26]: fun(1,2,3) + --------------------------------------------------------------------------- + TypeError Traceback (most recent call last) + in () + ----> 1 fun(1,2,3) + + TypeError: fun() takes 2 positional arguments but 3 were given + +So Python will not just move that third argument along for you. You need to use the keyword: + +.. code-block:: ipython + + In [29]: fun(1,2, key1=3) + 1 2 3 + +But you can still let it be the default: + +.. code-block:: ipython + + In [30]: fun(1,2) + 1 2 this + +However, with keyword only arguments you can make it required by providing no default: + +.. code-block:: python + + def fun(pos1, pos2, *, key1): + print(pos1, pos2, key1) + +.. code-block:: ipython + + In [32]: fun(1,2) + --------------------------------------------------------------------------- + TypeError Traceback (most recent call last) + in () + ----> 1 fun(1,2) + + TypeError: fun() missing 1 required keyword-only argument: 'key1' + +So you HAVE to provide it, and you HAVE to provide it as a keyword argument. + +.. code-block:: ipython + + In [34]: fun(1,2, key1='that') + 1 2 that + +What about ``*args``? +--------------------- + +Asside from allowing keyword-only paramters with or without defaults, a key addition is that you can now have variable numbers of positional arguments, without them getting confused with the keyword arguments: + +.. code-block:: python + + def fun (pos1, pos2, *args, key1='this'): + print(pos1, pos2, args, key1) + +.. code-block:: ipython + + In [36]: fun(1,2) + 1 2 () this + + In [37]: fun(1,2,3) + 1 2 (3,) this + +Notice how the third argument did NOT get assigned to key1? + +And you can pass any number in: + +.. code-block:: ipython + + In [39]: fun(1,2,3,4,5,6,7, key1='that') + 1 2 (3, 4, 5, 6, 7) that + +This is actually the primary motivation for the PEP -- it makes a cleaner separation of positional and keyword arguments. + +So for ALL the features in one function: + +.. code-block:: python + + def fun (pos1, pos2, *args, key1='this', **kwargs): + print(pos1, pos2, args, key1, kwargs) + +.. code-block:: ipython + + In [42]: fun(1,2,3,4, this='that', fred='bob') + 1 2 (3, 4) this {'this': 'that', 'fred': 'bob'} + +or: + +.. code-block:: ipython + + In [44]: args = (1,2,3,4) + + In [45]: kwargs = {'this':'that', 'fred':'bob'} + + In [46]: fun(*args, **kwargs) + 1 2 (3, 4) this {'this': 'that', 'fred': 'bob'} + +Lots of Flexibility!! + + + + + diff --git a/_sources/modules/Async.rst.txt b/_sources/modules/Async.rst.txt new file mode 100644 index 0000000..3b08d74 --- /dev/null +++ b/_sources/modules/Async.rst.txt @@ -0,0 +1,640 @@ +.. _async: + +####################### +Asychronous Programming +####################### + +**Async:** Not knowing what is happening when... + +Asynchronous Programming +======================== + +Another way to achieve concurrency + +Approaches: + +- Event Loops +- Callbacks +- Coroutines + +Why Async? +---------- + +This is a pretty good overview of why you might want to use async: + +https://hackernoon.com/asyncio-for-the-working-python-developer-5c468e6e2e8e#.dlhcuy23h + +In: "A Web Crawler With asyncio Coroutines", Guido himself writes: + + Many networked programs spend their time not computing, but holding open many connections that are slow, or have infrequent events. These programs present a very different challenge: to wait for a huge number of network events efficiently. A contemporary approach to this problem is asynchronous I/O, or "async". + +http://www.aosabook.org/en/500L/a-web-crawler-with-asyncio-coroutines.html + +My take: +........ + +Async is the good approach to support many connections that are spending a lot of time waiting, and doing short tasks when they do have something to do. + +**NOTE:** the backbone of the web is HTTP -- which is a "stateless" protocol. That is, each request is independent (state is "faked" with sessions via cookies). So "classic" web apps are NOT keeping many connections alive, there may be many clients at once, but each request is still independent. And often there is substantial work to be done with each one. A multi-threaded or multi-processes web server works fine for this. + +Single Page Apps and WebSockets +------------------------------- + +**"Single Page Apps"** + + A single-page application (SPA) is a web application ... providing a user experience similar to that of a desktop application. ... *Interaction with the single page application often involves dynamic communication with the web server behind the scenes.* + + https://en.wikipedia.org/wiki/Single-page_application + +Communication with the web service can be regular old http (AJAX), or in modern implementations: + +**WebSocket**: + + WebSocket is a computer communications protocol, providing full-duplex communication channels over a single TCP connection. + + https://en.wikipedia.org/wiki/WebSocket + +WebSocket gives the advantage of "pushing" -- the server can push information to the client, rather than the client having to poll the server to see if anything needs to be updated. + +Either HTTP or WebSocket can generate many small requests to the server, which async is good for, but WebSocket pretty much requires an async server if you want it to scale well, as each active client is keeping a connection open. + +Also: often a web service is depending on other web services to do its task. Kind of nice if your web server can do other things while waiting on a third-party service. + +Client-side HTTP +---------------- + +Another nice use for async is client side HTTP: + +When you make an http request, there is often a substantial lag time between making the request and getting the response. + +The server receives the request, and it may have to do a fair bit of processing before it can return something -- and it takes time for the response to travel over the wire. + +With "regular" requests -- the program is halted while it's waiting for the server to do its thing. ("Blocking" -- see below) + +With async, the program can do other things while the request is waiting for the server to respond. + +Blocking +-------- + +A "Blocking" call means a function call that does not return until it is complete. That is, an ordinary old function call: + +.. code-block:: python + + call_a_func() + +The program will stop there and wait until the function returns before moving on. Nothing else can happen. Usually this is fine, the program may not be able to do anything else until it gets the result of that function anyway. + +But what if: + +- That function will take a while? +- And it's mostly just waiting for the network or database, or.... + +Maybe your application needs to be responsive to user input, or you want it to do other work while that function is doing its thing. Especially if it's mostly just waiting for a response to return. How do you deal with that? + +Event Loops +----------- + +Asynchronous programming is not new -- it is the key component of traditional desktop Graphical User Interface Programs. The GUI version is often referred to as "event-driven" development: + +You write "event handlers" that respond to particular events in the GUI: moving the mouse, clicking on a button, etc. + +The trick is that you don't know in what order anything might happen -- there are multiple GUI objects on the screen at a given time, and users could click on any of them in any order. + +This is all handled by an "event loop", essentially code like this: + +.. code-block:: python + + while True: + evt = event_queue.pop() + if evt: + evt.call_handler() + +That's it -- it is an infinite loop that continually looks to see if there are any events to handle, and if there are, it calls the event handler for that event. Meanwhile, the system is putting events on the event queue as they occur: someone moving the mouse, typing in a control, etc. + +It's important that event handlers run quickly -- if they take a long time to run, then the GUI is "locked up", or not responsive to user input. + +If the program does need to do some work that takes time, it needs to do that work in another thread or processes, and then put an event on the event queue when it is done. + +For some examples of this, see: + +`How To Communicate With Your GUI Via Sockets `_ + + +Callbacks +--------- + +Callbacks are a way to tell a non-blocking function what to do when they are done. This is a common way for systems to handle non-blocking operations. For instance, in Javascript, http requests are non-blocking. The request function call will return right away. + +.. code-block:: javascript + + request('http://www.google.com', + function(error, response, body){ + console.log(body); + }); + +What this means is: + +Make a request to Google, and when the request is complete, call the function with three parameters: ``error``, ``response``, and ``body``. This function is defined inline, and simply passes the body to the console log. But it could do anything. + +That function is put on the event queue when the request is done, and will be called when the other events on the queue are processed. + +Contrast with the "normal" python request library: + +.. code-block:: python + + import requests + r = requests.get('http://www.google.com') + print(r.text) + +The difference here is that the program will wait for ``requests.get()`` call to return, and that won't happen until the request is complete. If you are making a lot of requests and they take a while, that is a lot of time sitting around waiting for the server when your computer isn't doing anything. + +Note that javascript began as a way to automate stuff on web pages -- it lets you attach actions to various events in the browser: clicking button or what have you. The "callback" approach is natural for this. And once that structure was there, it made sense to keep it when making requests directly from code, that is doing: *Asychronous Javascript and XML* -- i.e. AJAX. That's why callback-based async is "built in" to Javascript. + +Async programming usually (always?) involves an event loop to schedule operations. + +But callbacks are only one way to communicate with the event loop. + +Coroutines +---------- + + Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing multiple entry points for suspending and resuming execution at certain locations. Coroutines are well-suited for implementing more familiar program components such as cooperative tasks, exceptions, event loops, iterators, infinite lists and pipes. + +https://en.wikipedia.org/wiki/Coroutine + + Coroutines are functions that can hold state, and varies between invocations; there can be multiple instances of a given coroutine at once. + +This may sound a bit familiar from generators -- a generator function can hold state when it yields, and there can be multiple instances of the same generator function at once. + +In fact, you can use generators and yield to make coroutines, and that was done in Python before version 3.5 added new features to directly support coroutines. + +**Warning:** This is really hard stuff to wrap your head around! + +.. image:: /_static/coroutines_plot.png + +(from: http://www.dabeaz.com/coroutines/Coroutines.pdf -- which is a pretty good talk to read if you want to understand this stuff) + +``async`` / ``await`` +--------------------- + +In Python 3.5, the ``async`` and ``await`` keywords were added to make coroutines "native" and more clear. + +**NOTE:** ``async`` and ``await`` are still pretty new to Python. So if you look for tutorials, blog posts, etc. about asynchronous programming, they mostly either use or refer to the "old" way to do it (Including David Beazley's talk above). In these notes, I am ONLY talking about the new way. I hope that's less confusing. But it can be confusing to read older materials. + +**NOTE2:** In addition to older documentation, the ``asyncio`` package in the standard library pre-dates ``async`` and ``await`` -- so it supports the older style as well as the new style -- another source of confusion. +`The Trio project `_ is worth a look for a cleaner API. + +Using ``async/await`` +--------------------- + +You define a coroutine with the ``async`` keyword: + +.. code-block:: python + + async def ping_server(ip): + pass + +When you call ``ping_server()``, it doesn't run the code. What it does is return a coroutine, all set up and ready to go. + +.. code-block:: ipython + + In [12]: cr = ping_server(5) + + In [13]: cr + Out[13]: + +Running a Coroutine +.................... + +So how do you actually *run* the code in a coroutine? + +**await** + +``await a_coroutine`` + +It's kind of like yield (from generators), but instead it returns the next value from the coroutine, and *pauses execution* so other things can run. + +``await`` suspends the execution (letting other code run) until the object called returns. + +When you call await on an object, it needs to be an "awaitable" object: an object that defines an ``__await__()`` method which returns either an iterator which is not a coroutine itself, or a coroutine -- which are considered awaitable objects. + +Scheduling it to run +.................... + +Schedule it with + +``asyncio.ensure_future()`` + +or + +``event_loop.create_task()`` + + +Think of ``async/await`` as an API for asynchronous programming +------------------------------------------------------------------- + +``async/await`` is really an API for asynchronous programming: People shouldn't think that ``async/await`` as synonymous with asyncio, but instead think that asyncio is a framework that can utilize the ``async/await`` API for asynchronous programming. In fact, this view is supported by the fact that there are other async frameworks that use async/await -- like the Trio package mentioned above. + + +Future objects +-------------- + +A Future object encapsulates the asynchronous execution of a callable -- it "holds" the code to be run later. + +It also contains methods like: + +``cancel()``: + Cancel the future and schedule callbacks. + +``done()``: + Return True if the future is done. + +``result()``: + Return the result this future represents. + +``add_done_callback(fn)``: + Add a callback to be run when the future becomes done. + +``set_result(result)``: + Mark the future done and set its result. + +A coroutine isn't a future, but they can be wrapped in one by the event loop. + +For the most part, you don't need to work directly with futures. + +**NOTE:** there is also the ``concurrent.futures`` module, which provides "future" objects that work with threads or processes, rather than an async event loop. + + +The Event Loop +-------------- + +The whole point of this to to pass events along to an event loop. So you can't really do anything without one. + +The ``asyncio`` package provides an event loop: + +The ``asyncio`` event loop can do a lot: + + * Register, execute, and cancel delayed calls (asynchronous functions) + * Create client and server transports for communication + * Create subprocesses and transports for communication with another program + * Delegate function calls to a pool of threads + +But the simple option is to use it to run coroutines: + +.. code-block:: python + + import asyncio + + async def say_something(): + print('This was run by the loop') + + # getting an event loop + loop = asyncio.get_event_loop() + # run it: + loop.run_until_complete(say_something()) + +Note that ``asyncio.get_event_loop()`` will create an event loop in the main thread if one doesn't exist -- and return the existing loop if one does exist. So you can use it to get the already existing, and maybe running, loop from anywhere. + +This is not a very interesting example -- after all, the coroutine only does one thing and exits out, so the loop simply runs one event and is done. + +Let's make that a tiny bit more interesting with multiple events: + +.. code-block:: python + + import asyncio + + async def say_lots(num): + for i in range(num): + print('This was run by the loop:') + await asyncio.sleep(0.2) + + # getting an event loop + loop = asyncio.get_event_loop() + # run it: + loop.run_until_complete(say_lots(5)) + print("done with loop") + +:download:`ultra_simple.py ` + +Still not very interesting -- technically async, but with only one coroutine, not much to it. + +**NOTE:** The event loop requires some setup, and it's not very happy when you stop and try to restart it. So you may have issues if you run this kind of code from iPython -- each time you run it, you're still in the same Python process, so the event loop is whatever state it was left by the previous code. If you get any errors, simply restart iPython, or just run the scripts by themselves: + +.. code-block:: bash + + $ python ultra_simple.py + +So let's see an even more interesting example: + +:download:`async_time.py ` + +.. code-block:: python + + import asyncio + import time + import datetime + import random + + # using "async" makes this a coroutine: + # its code can be run by the event loop + async def display_date(num): + end_time = time.time() + 10.0 # we want it to run for 10 seconds. + while True: # keep doing this until break + print("instance: {} Time: {}".format(num, datetime.datetime.now())) + if (time.time()) >= end_time: + print("instance: {} is all done".format(num)) + break + # pause for a random amount of time + await asyncio.sleep(random.randint(0, 3)) + + def shutdown(): + print("shutdown called") + # you can access the event loop this way: + loop = asyncio.get_event_loop() + loop.stop() + + + # You register "futures" on the loop this way: + asyncio.ensure_future(display_date(1)) + asyncio.ensure_future(display_date(2)) + + loop = asyncio.get_event_loop() + + # or add tasks to the loop like this: + loop.create_task(display_date(3)) + loop.create_task(display_date(4)) + + # this will shut the event loop down in 15 seconds + loop.call_later(15, shutdown) + + print("about to run loop") + # this is a blocking call + loop.run_forever() + print("loop exited") + +Calling a regular function +-------------------------- + +The usual way to use the event loop is to schedule "awaitable" tasks -- i.e. coroutines. + +But sometimes you need to call a regular old function. + +This is more like the traditional "callback" style: + +You can do that with: + +``event_loop.call_soon(callback, *args)`` + +This will put an event on the event loop, and call the function (callable) passed in, passing on any extra arguments as keyword arguments. It will run "soon" + +Similarly, you can schedule a callable to be run some number of seconds in the future: + +``event_loop.call_later(delay, callback, *args)`` + +Or at some specified time: + +``event_loop.call_at(when, callback, *args)`` + +Absolute time corresponds to the event loop's time() method: ``event_loop.time()`` + +If you need to put an event on the loop from a separate thread, you can use: + +``event_loop.call_soon_threadsafe(callback, *args)`` + + +Giving up control +----------------- + +``await`` passes control back to the event loop -- cooperative multitasking! + +Usually, you actually need to wait for a task of some sort. but if not, and you still need to give up control, you can use: + +``await asyncio.sleep(0)`` + +You can, of course, pause for a period of time (greater than zero), but other than demos, I'm not sure why you'd want to do that. + +Running Blocking Code +--------------------- + +Sometimes you really do need to run "blocking" code -- maybe a long computation, or reading a big file, or..... + +In that case, if you don't want your app locked up -- you need to put it in a separate thread (or process). Use: + +result = await loop.run_in_executor(Executor, function) + +This will run the function in the specified Executor: + +https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor + +If Executor is None -- the default is used. + +:download:`async_executor.py ` + +.. code-block:: python + + import asyncio + import time + import datetime + import random + + + async def small_task(num): + """ + Just something to give us little tasks that run at random intervals + These will go on forever + """ + while True: # keep doing this until break + print("task: {} run".format(num)) + # pause for a random amount of time between 0 and 2 seconds + await asyncio.sleep(random.random() * 2) + + async def slow_task(): + while True: # keep going forever + print("running the slow task- blocking!") + # This will block for 2-10 seconds! + result = slow_function(random.random() * 8 + 2) + # uncomment to put it on a different thread: + # result = await loop.run_in_executor(None, + # slow_function, + # random.random() * 8 + 2) + print("slow function done: result", result) + await asyncio.sleep(0.1) # to release the loop + + + def slow_function(duration): + """ + this is a fake function that takes a long time, and blocks + """ + time.sleep(duration) + print("slow task complete") + return duration + + + # get a loop going: + loop = asyncio.get_event_loop() + + # or add tasks to the loop like this: + loop.create_task(small_task(1)) + loop.create_task(small_task(2)) + loop.create_task(small_task(3)) + loop.create_task(small_task(4)) + + # Add the slow one + loop.create_task(slow_task()) + + print("about to run loop") + # this is a blocking call + # we will need to hit ^C to stop it... + loop.run_forever() + print("loop exited") + +Running a bunch of tasks +------------------------ + +Sometimes you have a bunch of individual tasks to complete, but it does not matter in what order they are done. + +``asyncio.gather()`` collects a bunch of individual coroutines (or futures) together, runs them all (in parallel), and puts the results in a list. + +Remember that they are now run in arbitrary order. + +:download:`gather.py ` + + +Doing real work with async +========================== + +So what kinds of real things can you do with asynchronous programming? + +``asyncio`` provides the core tools to write asynchronous programs: + +* An event loop with a lot of features +* Asynchronous versions of core network protocols: i.e. sockets. +* file watching +* ... + +But chances are, if you want to do something real, you'll use a library.. + + +Web servers and clients +----------------------- + +There have been a few async frameworks around for Python for a while: + +The granddaddy of them all: + +Twisted https://twistedmatrix.com/trac/ + +Relative Newcomer: + +Tornado: +http://www.tornadoweb.org/en/stable/ + +Using the latest and greatest: + +Once the asyncio package was added to the standard lib the tools are there to build "proper" http servers, etc: + +``aiohttp`` is an http server (and client) built on top of ``asyncio``: + +http://aiohttp.readthedocs.io/ + +(Twisted, Tornado, and the others have their own implementation of much +of what is in asyncio, as they existed before asyncio existed) + +As it's the most "modern" implementation -- we will use it for examples in the rest of this class: + +``aiohttp`` +----------- + +* Supports both Client and HTTP Server. +* Supports both Server WebSockets and Client WebSockets out-of-the-box. +* Web-server has Middlewares, Signals and pluggable routing. + +Installing: + +.. code-block:: bash + + pip install aiohttp + +An async client example: +------------------------ + +If you need to make a lot of requests to collect data, or whatever, it's likely your code is taking a lot of time to wait for the server to return. If it's a slow server, it could be much more time waiting than doing real work. + +This is where async shines! + +This example borrowed from: + +`Asynchronous HTTP Requests in Python `_ + +It's a really nice example. + +The goal is to collect statistics for various NBA players. It turns out the NBA has an API for accessing statistics: + +http://stats.nba.com/ + +It's kinda slow, but has a lot of great data -- if you're into that kind of thing. + +Turns out that it's a picky API -- and I can't get the async version to work -- maybe the server gets upset when you hit it too hard? + +Can you get it to work? + +Synchronous version: +:download:`nba_stats_sync.py ` + +And the Asynchronous version: +:download:`nba_stats_async.py ` + +One that works: +............... + +Here is a similar example that works: + +This is a "classic" regular old synchronous version: + +:download:`get_news_sync.py ` + +and here is an async version + +:download:`get_news_async.py ` + +Let's take a look. + + +TODO: Look at async example in multi-threading server example. + + +References: +=========== + +The Asyncio Cheat Sheet: This is a pretty helpful, how to do it guide. + +http://cheat.readthedocs.io/en/latest/python/asyncio.html + +David Beazley: Concurrency from the ground up. + +He writes a full async client server from scratch before your eyes -- +this guy can write code faster than most of us can read it... + +https://youtu.be/MCs5OvhV9S4 + +David Beazley: asyncio: + +https://youtu.be/ZzfHjytDceU + +https://www.youtube.com/watch?v=lYe8W04ERnY + +And David Beazley's "Curio" package -- an async package designed primarily for learning, rather than production use. + + + + + + + + + + + + + + + diff --git a/_sources/modules/BasicPython.rst.txt b/_sources/modules/BasicPython.rst.txt new file mode 100644 index 0000000..ac6a9b3 --- /dev/null +++ b/_sources/modules/BasicPython.rst.txt @@ -0,0 +1,1178 @@ +.. _basic_python_syntax: + +Basic Python +============ + +Values, Types, and Symbols + +Expressions and Statements + +(Follow along in the iPython interpreter...) + +Values +------ + +All of programming is really about manipulating values. + + +* Values are pieces of unnamed data: ``42``, ``'Hello, world'`` + +* In Python, all values are objects. + + - Try ``dir(42)`` - lots going on behind the curtain! + +* Every value has a type + + - Try ``type(42)`` - the type of a value determines what it can do. + + +Literals for the Basic Value types: +------------------------------------ + +A "literal" is something you can put in your code to directly get a value. Python has literals for the key built in types. + +Numbers: + - floating point: ``3.4`` + - integers: ``456`` + +Text: + - ``"a bit of text"`` + - ``'a bit of text'`` + - (either single or double quotes work -- why? If you don't know, try looking it up in one of the referenced sources!) + +Boolean values: + - ``True`` + - ``False`` + +The nothing object: + - ``None`` + +(There are intricacies to all of these that we'll get into later.) + + +Code structure +-------------- + +Each line is a unit of code. Each line is made up of these components: + +**Comments:** + +.. code-block:: ipython + + In [3]: # everything after a '#' is a comment + + +**Expressions:** + +An expression is a unit of code that evaluates to a value: + +.. code-block:: ipython + + In [4]: # evaluating an expression results in a value + + In [5]: 3 + 4 + Out[5]: 7 + + +**Statements:** + +statements carry out an action, but do not evaluate to a value, that is, you can't assign to them (or put them in a lamda, or...) + +.. code-block:: ipython + + In [6]: # statements carry out an action, do not evaluate a value, may contain an expression + + In [7]: line_count = 42 + + In [8]: return something + +Statements include function (and class) definitions (``def``), loop constructs (``for``, ``while``), code forking constructs (``if``), exception handling (``try``, ``except``), and a handful of other more advanced constructs. + + +The ``print()`` function does what you'd expect, and is very handy when playing with code: + +.. code-block:: ipython + + In [1]: print("something") + something + +You can print multiple things: + +.. code-block:: ipython + + In [2]: print("the value is", 5) + the value is 5 + + +Any Python object can be printed (though it might not be pretty...) + +.. code-block:: ipython + + In [1]: class bar(object): + ...: pass + ...: + + In [2]: print(bar) + + +Code Blocks +........... + +Separate blocks of code are delimited by a colon and indentation. Everything indented after a colon is "inside" that block. It can be a function definition, or a loop construct, or a handful of other more advanced constructs. + +.. code-block:: python + + def a_function(): + a_new_code_block + # end_of_the_block on previous line + +.. code-block:: python + + for i in range(100): + print(i**2) + +.. code-block:: python + + try: + do_something_bad() + except: + fix_the_problem() + + +Python uses indentation to delineate structure. This means that in Python, whitespace is **significant** (but **ONLY** for newlines and indentation). + +The standard is to indent with **4 spaces**. + +**SPACES ARE NOT TABS** + +**TABS ARE NOT SPACES** + +Python requires spaces for indents. You can probably set your editor to replace tabs with spaces. +This is a good idea as it is easier to type one tab than 4 spaces. + + +These two blocks look the same: + +.. code-block:: python + + for i in range(100): + print(i**2) + +.. code-block:: python + + for i in range(100): + print(i**2) + + + +But they are not: + +.. code-block:: python + + for i in range(100): + \s\s\s\sprint i**2 + +.. code-block:: python + + for i in range(100): + \tprint i**2 + + +.. centered:: **ALWAYS INDENT WITH 4 SPACES** + + +Make sure your editor is set to use spaces only -- + +Even when you hit the key + +[Python itself allows any number of spaces (and tabs), but you are just going to confuse yourself and others if you do anything else] + + +Expressions +------------ + +An *expression* is made up of values and operators. + + +* An expression is evaluated to produce a new value: ``2 + 2`` + + * The Python interpreter can be used as a calculator to evaluate expressions. + +* Integer vs. float arithmetic + + * (Python 3 smooths this out). + * Always use ``/`` when you want division with float results, ``//`` when you want floored (integer) results (no remainder): + + +.. code-block:: ipython + + In [1]: 3 / 4 + Out[1]: 0.75 + + In [2]: 3 // 4 + Out[2]: 0 + +* Type conversions: You usually need to convert types explicitly: + +.. code-block:: ipython + + In [4]: 3 * "4" + Out[4]: '444' + + In [5]: 3 * int("4") + Out[5]: 12 + +* Type errors - checked at run time only: + +.. code-block:: ipython + + In [10]: '3' * '4' + --------------------------------------------------------------- + TypeError Traceback (most recent call last) + in + ----> 1 '3' * '4' + + TypeError: can't multiply sequence by non-int of type 'str' + +Symbols +------- + +Symbols are how we give names to values (objects). + + +* Symbols must begin with an underscore or letter. +* Symbols can contain any number of underscores, letters and numbers. + + * ``this_is_a_symbol`` + * ``this_is_2`` + * ``_AsIsThis`` + * ``1butThisIsNot`` + * ``nor-is-this`` + +* Symbols (names) don't have a type; values do. + + * This is why Python is "Dynamic". + + +Symbols and Type +---------------- + +Evaluating the type of a *symbol* will return the type of the *value* to which +it is bound. + +.. code-block:: ipython + + In [19]: type(42) + Out[19]: int + + In [20]: type(3.14) + Out[20]: float + + In [21]: a = 42 + + In [22]: b = 3.14 + + In [23]: type(a) + Out[23]: int + + In [25]: a = b + + In [26]: type(a) + Out[26]: float + +*wait!* ``a`` has a different type?!? -- yes, because it's the type of the value: ``3.1``, names don't actually have a type, the same name can refer to any type. + + +Assignment +---------- + +A *symbol* is **bound** to a *value* with the assignment operator: ``=`` + +* This attaches a name to a value. +* A value can have many names (or none!) +* Assignment is a statement, it returns no value. + + +Evaluating the name will return the value to which it is bound + +.. code-block:: ipython + + In [26]: name = "value" + + In [27]: name + Out[27]: 'value' + + In [28]: an_integer = 42 + + In [29]: an_integer + Out[29]: 42 + + In [30]: a_float = 3.14 + + In [31]: a_float + Out[31]: 3.14 + + +Variables? +---------- + + +* In most languages, what Python calls symbols or names are called "variables". + +* In fact, we will probably call them variables in this class. + +* That's because they are used, for the most part, for the same purposes. + +* But often a "variable" is defined as something like: + "a place in memory that can store values". + +* That is **NOT** the same thing as a symbol or name in Python! + +* A name can be bound to a value -- but that has nothing to do with a + location in memory. + + +In-Place Assignment +------------------- + +You can also do "in-place" assignment with ``+=``. + +.. code-block:: ipython + + In [32]: a = 1 + + In [33]: a + Out[33]: 1 + + In [34]: a = a + 1 + + In [35]: a + Out[35]: 2 + + In [36]: a += 1 + + In [37]: a + Out[37]: 3 + +also: ``-=, *=, /=, **=, \%=`` + +**Note:** This is a bit tricky -- if the value is mutable, it is in-place assignment -- that is the object itself is changed. But if the value is immutable (can't be changed), then it is replaced with a new object. + +Example with an immutable type: + +.. code-block:: ipython + + In [11]: a = 5 # a is an integer -- an immutable type. + + In [12]: b = a # a and b are names for the SAME integer + + In [13]: a += 5 + + In [14]: a + Out[14]: 10 # a is changed + + In [15]: b + Out[15]: 5 # b is not. + +Example with a mutable type: + +.. code-block:: ipython + + In [16]: a = [1, 2, 3] # a is a mutable list + + In [17]: b = a # b is now another name for the same list + + In [18]: a += [4, 5, 6] # in-place add more to a + + In [19]: b + Out[19]: [1, 2, 3, 4, 5, 6] + + In [20]: # b is changed --it's the SAME list. + + +Multiple Assignment +------------------- + +You can assign multiple names from multiple expressions in one +statement: + +.. code-block:: ipython + + In [48]: x = 2 + + In [49]: y = 5 + + In [50]: i, j = 2 * x, 3 ** y + + In [51]: i + Out[51]: 4 + + In [52]: j + Out[52]: 243 + + +Python evaluates all the expressions on the right before doing any assignments. + + +Nifty Python Trick +------------------ + +Using this feature, we can swap values between two names in one statement: + +.. code-block:: ipython + + In [51]: i + Out[51]: 4 + + In [52]: j + Out[52]: 243 + + In [53]: i, j = j, i + + In [54]: i + Out[54]: 243 + + In [55]: j + Out[55]: 4 + +Multiple assignment and symbol swapping can be very useful in certain contexts. + +Deleting +-------- + +You can't actually directly delete values in Python... + +``del`` only deletes a name (or "unbinds" the name...) + +.. code-block:: ipython + + In [56]: a = 5 + + In [57]: b = a + + In [58]: del a + + In [59]: a + --------------------------------------------------------------------------- + NameError Traceback (most recent call last) + in () + ----> 1 a + + NameError: name 'a' is not defined + + +The object is still there...Python will only delete it if there are no +references to it. + +.. code-block:: ipython + + In [15]: a = 5 + + In [16]: b = a + + In [17]: del a + + In [18]: a + --------------------------------------------------------------------------- + NameError Traceback (most recent call last) + in () + ----> 1 a + + NameError: name 'a' is not defined + + In [19]: b + Out[19]: 5 + + +Identity +-------- + +Every value in Python is an object. + +Every object is unique and has a unique *identity*, which you can inspect with +the ``id`` *builtin*: + +.. code-block:: ipython + + In [68]: id(i) + Out[68]: 140553647890984 + + In [69]: id(j) + Out[69]: 140553647884864 + + In [70]: new_i = i + + In [71]: id(new_i) + Out[71]: 140553647890984 + + +Testing Identity +---------------- + +You can find out if the values bound to two different symbols are the **same +object** using the ``is`` operator: + +.. code-block:: ipython + + In [72]: count = 23 + + In [73]: other_count = count + + In [74]: count is other_count + Out[74]: True + + In [75]: count = 42 + + In [76]: other_count is count + Out[76]: False + +**NOTE:** Checking the id of an object, or using "is" to check if two objects are the same is rarely used except for debugging and understanding what's going on under the hood. They are not used regularly in production code. + + +Equality +-------- + +You can test for the equality of certain values with the ``==`` operator + +.. code-block:: ipython + + In [77]: val1 = 20 + 30 + + In [78]: val2 = 5 * 10 + + In [79]: val1 == val2 + Out[79]: True + + In [80]: val3 = '50' + + In [81]: val1 == val3 + Out[84]: False + +A string is never equal to a number! + + +Singletons +---------- + +Python has three "singletons" -- a value for which there is only one instance: + + ``True``, ``False``, and ``None`` + +To check if a name is bound to one of these, you use ``is``: + +.. code-block:: python + + a is True + + b is False + + x is None + +Note that in contrast to English -- "is" is asking a question, not making an assertion -- ``a is True`` means "is a set to the True object?" + + +Operator Precedence +------------------- + +Operator Precedence determines what evaluates first: + +.. code-block:: python + + 4 + 3 * 5 != (4 + 3) * 5 + +To force statements to be evaluated out of order, use parentheses -- expressions in parentheses are always evaluated first: + + (4 + 3) * 5 != 4 + (3 * 5) + +Python follows the "usual" rules of algebra. + + +Python Operator Precedence +-------------------------- + +Parentheses and Literals: + ``(), [], {}`` + + ``"", b'', ''`` + +Function Calls: + ``f(args)`` + +Slicing and Subscription: + ``a[x:y]`` + + ``b[0], c['key']`` + +Attribute Reference: + ``obj.attribute`` + +Exponentiation: + ``**`` + +Bitwise NOT, Unary Signing: + ``~x`` + + ``+x, -x`` + +Multiplication, Division, Modulus: + ``*, /, %`` + +Addition, Subtraction: + ``+, -`` + +Bitwise operations: + ``<<, >>,`` + + ``&, ^, |`` + +Comparisons: + ``<, <=, >, >=, !=, ==`` + +Membership and Identity: + ``in, not in, is, is not`` + +Boolean operations: + ``or, and, not`` + +Anonymous Functions: + ``lambda`` + + +String Literals +--------------- + +A "string" is a chunk of text. + +You define a ``string`` value by writing a string *literal*: + +.. code-block:: ipython + + In [1]: 'a string' + Out[1]: 'a string' + + In [2]: "also a string" + Out[2]: 'also a string' + + In [3]: "a string with an apostrophe: isn't it cool?" + Out[3]: "a string with an apostrophe: isn't it cool?" + + In [4]: 'a string with an embedded "quote"' + Out[4]: 'a string with an embedded "quote"' + +.. code-block:: ipython + + In [5]: """a multi-line + ...: string + ...: all in one + ...: """ + Out[5]: 'a multi-line\nstring\nall in one\n' + + In [6]: "a string with an \n escaped character" + Out[6]: 'a string with an \n escaped character' + + In [7]: r'a "raw" string, the \n comes through as a \n' + Out[7]: 'a "raw" string, the \\n comes through as a \\n' + +Python3 strings fully support Unicode, which means they can support literally all the languages in the world (and then some -- Klingon, anyone? -- well `sort of. `_) + +Because Unicode is native to Python strings, you can get very far without even thinking about it. Anything you can type in your editor will work fine. + + +Keywords +-------- + +Python defines a number of **keywords** + +These are language constructs. + +You *cannot* use these words as symbols. + +:: + + False class finally is return + None continue for lambda try + True def from nonlocal while + and del global not with + as elif if or yield + assert else import pass + break except in raise + + + +If you try to use any of the keywords as symbols, you will cause a +``SyntaxError``: + +.. code-block:: ipython + + In [13]: del = "this will raise an error" + File "", line 1 + del = "this will raise an error" + ^ + SyntaxError: invalid syntax + +.. code-block:: ipython + + In [14]: def a_function(else='something'): + ....: print(else) + ....: + File "", line 1 + def a_function(else='something'): + ^ + SyntaxError: invalid syntax + + +__builtins__ +------------ + +Python also has a number of pre-bound symbols, called **builtins** + +Try this: + +.. code-block:: ipython + + In [6]: dir(__builtins__) + Out[6]: + ['ArithmeticError', + 'AssertionError', + 'AttributeError', + 'BaseException', + 'BufferError', + ... + 'vars', + 'xrange', + 'zip'] + + +You are free to rebind these symbols: + +.. code-block:: ipython + + In [15]: type('a new and exciting string') + Out[15]: str + + In [16]: type = 'a slightly different string' + + In [17]: type('type is no longer what it was') + --------------------------------------------------------------------------- + TypeError Traceback (most recent call last) + in () + ----> 1 type('type is no longer what it was') + + TypeError: 'str' object is not callable + +In general, this is a **BAD IDEA** -- hopefully your editor will warn you. + + +Exceptions +---------- + +Notice that the first batch of ``__builtins__`` are all *Exceptions* + +Exceptions are how Python tells you that something has gone wrong. + +There are several exceptions that you are likely to see a lot of: + + +* ``NameError``: indicates that you have tried to use a symbol that is not bound to a value. + +* ``TypeError``: indicates that you have tried to use the wrong kind of object for an operation. + +* ``SyntaxError``: indicates that you have mis-typed something. + +* ``AttributeError``: indicates that you have tried to access an attribute or + method that an object does not have (this often means you have a different + type of object than you expect) + + +Functions +--------- + +**What is a function?** + +A function is a self-contained chunk of code. + +You use them when you need the same code to run multiple times, +or in multiple parts of the program. + +Functions allow you to take code that would otherwise be duplicated potentially many times, and put it in one place. Then all you do is call that code to use it. + +This is often referred to as "DRY" -- "Don't Repeat Yourself". + +It also helps to keep the code clean and maintainable, as there is only one place to make a change. This in turn helps reduce defects. + +Functions can take and return information. + +The minimal function has at least one statement. + +.. code-block:: python + + def a_name(): + a_statement + + +Pass Statement does nothing (Note the indentation!) + +.. code-block:: python + + def minimal(): + pass + +This, of course, has limited use -- you will generally have multiple statements in a function -- and they will do something. + +However, the pass statement can help you by allowing you to create placeholder functions that you will come back to later to develop and embelish. + +Functions: ``def`` +------------------ + +``def`` is a *statement*: + + * it is executed + * it creates a local name + * it does *not* return a value + + +Function defs must be executed before the functions can be called: + +.. code-block:: ipython + + In [23]: unbound() + --------------------------------------------------------------------------- + NameError Traceback (most recent call last) + in () + ----> 1 unbound() + + NameError: name 'unbound' is not defined + +.. code-block:: ipython + + In [18]: def simple(): + ....: print("I am a simple function") + ....: + + In [19]: simple() + I am a simple function + + +Calling Functions +----------------- + +You **call** a function using the function call operator (parentheses): + +.. code-block:: ipython + + In [2]: type(simple) + Out[2]: function + + In [3]: simple + Out[3]: + + In [4]: simple() + I am a simple function + +Calling a function is how you run the code in that function. + + +Functions: Call Stack +--------------------- + +Functions can call functions -- this makes what is called an execution stack. That is what a "trace back", often referred to in exceptions, is -- the function call stack. + +.. code-block:: ipython + + In [5]: def exceptional(): + ...: print("I am exceptional!") + ...: print 1/0 + ...: + In [6]: def passive(): + ...: pass + ...: + In [7]: def doer(): + ...: passive() + ...: exceptional() + ...: + +You've defined three functions, one of which will *call* the other two. + +When an error occurs, you are presented with a "traceback" of the call stack: + +Functions: Tracebacks +--------------------- + +.. code-block:: ipython + + In [8]: doer() + I am exceptional! + --------------------------------------------------------------------------- + ZeroDivisionError Traceback (most recent call last) + in () + ----> 1 doer() + + in doer() + 1 def doer(): + 2 passive() + ----> 3 exceptional() + 4 + + in exceptional() + 1 def exceptional(): + 2 print("I am exceptional!") + ----> 3 print(1/0) + 4 + + ZeroDivisionError: integer division or modulo by zero + +The error occurred in the ``doer`` function -- but the traceback shows you where that was called from. + +Note that this listed in reverse order -- reverse of the order in which the functions are called. + +In a more complex system, this can be VERY useful -- learn to read tracebacks! + + +Functions: ``return`` +--------------------- + +Every function ends by returning a value. + +This is actually the simplest possible function: + +.. code-block:: python + + def fun(): + return None + + +If you don't explicitly put ``return`` there, Python will return ``None``: + +.. code-block:: ipython + + In [9]: def fun(): + ...: pass + ...: + In [10]: fun() + In [11]: result = fun() + In [12]: print(result) + None + +Note that the interpreter eats ``None`` -- you need to call ``print()`` to see it. + +More on return +-------------- + +Only one return statement in a function will ever be executed. + +Ever. + +Anything after an executed return statement will never get run. + +This is useful when debugging! + +.. code-block:: ipython + + In [14]: def no_error(): + ....: return 'done' + ....: # no more will happen + ....: print(1/0) + ....: + In [15]: no_error() + Out[15]: 'done' + + +However, functions *can* return multiple results: + +.. code-block:: ipython + + In [16]: def fun(): + ....: return 1, 2, 3 + ....: + In [17]: fun() + Out[17]: (1, 2, 3) + + +Remember multiple assignment? + +.. code-block:: ipython + + In [18]: x, y, z = fun() + In [19]: x + Out[19]: 1 + In [20]: y + Out[20]: 2 + In [21]: z + Out[21]: 3 + + +Functions: parameters +--------------------- + +In a ``def`` statement, the values written *inside* the parens are +**parameters** + +.. code-block:: ipython + + In [22]: def fun(x, y, z): + ....: q = x + y + z + ....: print(x, y, z, q) + ....: + +x, y, z are *local* names -- so is q + + +Functions: arguments +-------------------- + +When you call a function, you pass values to the function parameters as +**arguments** + +.. code-block:: ipython + + In [23]: fun(3, 4, 5) + 3 4 5 12 + +The values you pass in are *bound* to the names inside the function and used. + +The name used outside the object is separate from the name used inside the function. + +Making a Decision +------------------ + +**"Conditionals"** + +In order to do anything interesting at all, you need to be able to write code to make a decision. + +``if`` and ``elif`` (else if) allow you to make decisions: + +.. code-block:: ipython + + In [12]: def test(a): + ....: if a == 5: + ....: print("that's the value I'm looking for!") + ....: elif a == 7: + ....: print("that's an OK number") + ....: else: + ....: print("that number won't do!") + + In [13]: test(5) + that's the value I'm looking for! + + In [14]: test(7) + that's an OK number + + In [15]: test(14) + that number won't do! + +There is more to it than that, but this will get you started. + + +What's the difference between these two? + +.. code-block:: python + + if a: + print('a') + elif b: + print('b') + + ## versus... + if a: + print('a') + if b: + print('b') + +Lists +----- + +A way to store a bunch of stuff in order. + +Pretty much like an "array" or "vector" in other languages. + +To make a list literal you use square brackets and commas between the items: + +.. code-block:: python + + a_list = [2,3,5,9] + a_list_of_strings = ['this', 'that', 'the', 'other'] + +You can put any type of object in a list... + +Lists are a key Python data type with lots of functionality that we will get into later. + +``for`` loops +-------------- + +Sometimes called a 'determinate' loop. + +When you need to do something to all the objects in a sequence: + +.. code-block:: ipython + + In [10]: a_list = [2,3,4,5] + + In [11]: for item in a_list: + ....: print(item) + ....: + 2 + 3 + 4 + 5 + + +``range()`` and for +------------------- + +``range`` builds sequences of numbers automatically + +Use it when you need to do something a set number of times: + +.. code-block:: ipython + + num_stars = 4 + In [31]: for i in range(num_stars): + print('*', end=' ') + ....: + * * * * + +NOTE: ``range(n)`` creates an "iterable" -- something you can loop over. +We will cover iterables in greater depth in a later lesson. + +``assert`` +---------- + +Writing ``tests`` that demonstrate that your program works is an important part of learning to program. + +The Python ``assert`` statement is useful in writing simple tests: +for your code. + +.. code-block:: ipython + + In [1]: def add(n1, n2): + ...: return n1 + n2 + ...: + + In [2]: assert add(3, 4) == 7 + + In [3]: assert add(3, 4) == 10 + + --------------------------------------------------------------------- + AssertionError Traceback (most recent call last) + in () + ----> 1 assert add(3, 4) == 10 + + AssertionError: + + +Intricacies +------------ + +This is enough to get you started. + +Each of the feature we have covered has intricacies special to Python. + +We'll get to those over the next couple of lessons -- or really, the rest of the program! + + +Enough For Now +-------------- + +That's it for our basic intro to Python. + +You now know enough Python to do some basic exercises in Python programming. diff --git a/_sources/modules/Booleans.rst.txt b/_sources/modules/Booleans.rst.txt new file mode 100644 index 0000000..26208e7 --- /dev/null +++ b/_sources/modules/Booleans.rst.txt @@ -0,0 +1,299 @@ +.. _booleans: + +################### +Boolean Expressions +################### + +"Boolean" logic is the logic of binary values -- things that can be ony one of two values. Usually, the two values are considered to be true or false. + +In programming languages, "booleans" are often a data type -- one that captures this notion of true and false. + +Python has a boolean type as well: the singletons ``True`` and ``False``. + +Booleans are used in ``if`` statements, as well as the boolean operators, ``and`` and ``or``. + +But Python is not limited to using the actual boolean type in logic expressions -- in the spirit of dynamic languages, virtually any type can have values that are considered True or False. + +Some like to refer to this concept by the moniker given by Stephan Colbert: "Truthiness" + + +Truthiness +---------- + +What is true or false in Python? + +* The Booleans: ``True`` and ``False`` + +* "Something or Nothing"; that is the presence or absence of a value. + +* http://mail.python.org/pipermail/python-dev/2002-April/022107.html + + +Determining Truthiness: If you want to know if a value is "truthy", you can use the bool constructor to make a boolean out of it: + +.. code-block:: python + + bool(something) + +This is similar to making an integer out of another value, such a float or a string: + +.. code-block:: ipython + + In [1]: int(4.5) + Out[1]: 4 + + In [2]: int("345") + Out[2]: 345 + +``bool(something)`` will always evaluate to either ``True`` or ``False``. + + +What is Falsy? +-------------- + +* ``None`` + +* ``False`` + +* **Nothing:** + + - The zero value of any numeric type: ``0, 0.0, 0j``. + + - Any empty sequence, for example, ``"", (), []``. + + - Any empty mapping, for example, ``dict()``. + + - Instances of user-defined classes: + + * for which ``__bool__()`` returns False + + * for which ``__len__()`` returns 0 + +* http://docs.python.org/library/stdtypes.html + +(Don't worry about that last one -- what that means is that user-defined types can control their truthiness behavior). + +What is Truthy? +--------------- + +Everything else. + + +Pythonic Booleans +----------------- + +Any object in Python, when passed to the ``bool()`` type object, will +evaluate to ``True`` or ``False``. + +When you use the ``if`` keyword, it automatically does this to the expression provided. + +Which means that this is redundant, and not Pythonic: + +.. code-block:: python + + if xx == True: + do_something() + # or even worse: + if bool(xx) == True: + do_something() + +Instead, use what Python gives you: + +.. code-block:: python + + if xx: + do_something() + + +``and``, ``or`` and ``not`` +--------------------------- + +Python has three boolean operators: ``and``, ``or`` and ``not``. + +``and`` and ``or`` are binary expressions, and evaluate from left to right. + +``and`` will return the first operand that evaluates to False, or the last +operand if none are True: + +.. code-block:: ipython + + In [35]: 0 and 456 + Out[35]: 0 + +``or`` will return the first operand that evaluates to True, or the last +operand if none are True: + +.. code-block:: ipython + + In [36]: 0 or 456 + Out[36]: 456 + + +On the other hand, ``not`` is a unary expression (takes one operand) and inverts the boolean value +of this operand: + +.. code-block:: ipython + + In [39]: not True + Out[39]: False + + In [40]: not False + Out[40]: True + +Shortcutting +------------ + +``and`` and ``or`` returning teh first value that determines the result is known as "shortcutting". If you think about it, what ``and`` and ``or`` are doing is as little work as possible. They will only evaluate as much as they need to get the answer. + +Think about ``and``: it is testing if *both* the operands are True. If the first one is False, there is no need to bother checking the second. + +Alternatively, ``or`` is trying to see if only one of the operands is True. So if the first one is True, it can stop, and does not need to evaluate the second. + +Also key is that if an operation is "shortcut" -- the second part of the expression will not be evaluated -- so it could be an invalid expression that will never raise an error: + +.. code-block:: ipython + + In [3]: 34 or (10/0) + Out[3]: 34 + +Since the expression was known to be true after the first value was checked (a number that is nonzero), the second was never evaluated. + +.. code-block:: ipython + + In [4]: 34 and (10 / 0) + --------------------------------------------------------------------------- + ZeroDivisionError Traceback (most recent call last) + in () + ----> 1 34 and (10 / 0) + + ZeroDivisionError: division by zero + +In this case, the second expression needs to be evaluated -- so it DID raise an error. + +This can be exploited to provide compact logic -- but it can also hide bugs! + + +Because of the return value of the boolean operators, you can write concise +statements, rather than a full ``if -- else`` block like so: + +:: + + if bool(x) is False: + x or y return y + else: return x + + if bool(x) is False: + x and y return x + else: return y + + if bool(x) is False: + not x return True + else: return False + +Chaining +-------- + +.. code-block:: python + + a or b or c or d + a and b and c and d + + +The first value that defines the result is returned + + + (take a moment to experiment...) + + +Conditional Expressions +----------------------- + +This is a fairly common idiom: + +.. code-block:: python + + if something: + x = a_value + else: + x = another_value + +In other languages, this can be compressed with a "ternary operator":: + + result = a > b ? x : y; + +(this is the syntax from the C family of languages) + +In Python, the same is accomplished with the conditional expression: + +.. code-block:: python + + y = 5 if x > 2 else 3 + +It's pretty self explanatory + +PEP 308: +(http://www.python.org/dev/peps/pep-0308/) + + +Boolean Return Values +--------------------- + +Remember this puzzle from the CodingBat exercises? + +.. code-block:: python + + def sleep_in(weekday, vacation): + if weekday == True and vacation == False: + return False + else: + return True + +Though correct, that's not a particularly Pythonic way of solving the problem. + +Here's a better solution: + +.. code-block:: python + + def sleep_in(weekday, vacation): + return not (weekday == True and vacation == False) + + +And here's an even better one: + +.. code-block:: python + + def sleep_in(weekday, vacation): + return (not weekday) or vacation + + +bools are integers? +------------------- + +In Python, the boolean types are subclasses of integer: + +.. code-block:: ipython + + In [1]: True == 1 + Out[1]: True + In [2]: False == 0 + Out[2]: True + + +And you can even do math with them (though it's a bit odd to do so): + +.. code-block:: ipython + + In [6]: 3 + True + Out[6]: 4 + +This is left over from history -- in early versions of Python, there were no boolean types -- folks used integers, with zero as false. And this is true of other languages as well, like classic C. To keep backward compatibility and allow some nifty tricks to still work, bools are subclassed from integers. + +It's good to know this if you read others' code, but I do NOT recommend you use this feature! + +Try it out: +----------- + +Now that you know a bit more about Python boolean operations, it's a good time to visit some coding bat exercises and see if you can make your solutions cleaner and more compact. + + + diff --git a/_sources/modules/CallableClasses.rst.txt b/_sources/modules/CallableClasses.rst.txt new file mode 100644 index 0000000..8f98da2 --- /dev/null +++ b/_sources/modules/CallableClasses.rst.txt @@ -0,0 +1,13 @@ +:orphan: + +.. NOTE: this needs to be written!! + +Callable Classes +================ + +One nifty Python feature is that any class can be "callable" -- that is, be called like a function. This is done by adding a ``__call__`` dunder method. + + +.. code-block:: python + + class Callable \ No newline at end of file diff --git a/_sources/modules/Class_introduction.rst.txt b/_sources/modules/Class_introduction.rst.txt new file mode 100644 index 0000000..363e042 --- /dev/null +++ b/_sources/modules/Class_introduction.rst.txt @@ -0,0 +1,142 @@ +.. _class_introduction: + +################################################### +Introduction to the in-class version of the program +################################################### + +In which you are introduced to this class, your instructors, your environment and your new best friend, Python. + + +.. image:: /_static/python.png + :align: center + :width: 80% + + +`xkcd.com/353`_ + +.. _xkcd.com/353: http://xkcd.com/353 + + +Who are we? +=========== + +Introduction to your instructors. + +Who are you? +------------ + +Despite the common myth of the lone programmer, most software development is a collaborative activity. As such, we encourage students in this program to work together whenever possible. + +As you will be working with your fellow students for the rest of the program +Take a couple minutes now to get to know your neighbors -- what is their favorite coffee shop or bar? + +Then we'll go around the room and introduce ourselves: + +Tell us a tiny bit about yourself: + +* name +* programming background: what languages have you used? +* neighbor's name +* neighbor's favorite coffee shop or bar + + +Introduction to This Class +========================== + +The overall class is managed by a learning management system -- Canvas or EdX. + +You have gotten a link to the instance for the class sent to you. + +Class Structure +--------------- + +We will be using a variation of a +`"flipped classroom" `_ +for this program. + +This means that the "homework" will be reading, watching videos, coding, etc. + +And class time will be spent primarily coding: + + * Still some lecture -- as little as possible + * Lots of demos + * Working on Coding Exercises: + - On your own, with us to help + - In small groups + - Instructor led. + +This means that you are expected to complete the reading (and video watching) BEFORE each class. That way, we don't have to take class time introducing the basic material and can focus on questions and applying what you've read about. + +Interrupt us with questions -- please! + +(Some of the best learning prompted by questions) + +Homework: +--------- + +* Homework will be reading: a handful of videos, and links to optional external materials -- videos, blog posts, etc. + +* Exercises will be started in class -- but you can finish them at home (and you will need time to do that!) + +* You are adults -- it's up to you to do the homework. But if you don't code, you won't learn to code. And we can't give you a certificate if you haven't demonstrated that you've done the work. + +* To submit your work, you can do a gitHub "pull request" to the class repo. + +There is a video about that, and we will show you in the first class as well. + +Communication +------------- + +**Mailing List** + +There should have been a Mailing List set up for this class. You should have been invited to join -- if not, let your instructors know. Also let them know if you would prefer a different email address. + +Anything Python related is fair game. Questions and discussion about the assignments are encouraged. + +We highly encourage you to work together. You will learn at a much deeper level if you work together, and it gets you ready to collaborate with colleagues. + + +Office Hours +------------ + +We will generally will hold "office hours" at a coffee shop for a couple hours each weekend. We will try to have one on Saturday, and one on Sunday. + +Please feel free to attend even if you do not have a specific question. It is an opportunity to work with the instructors and fellow students, and learn from each other. + +What are good times for you? + +And what locations? + +.. _lightning_talks: + +Lightning Talks +=============== + +"Lightning Talks" are a tradition in open-source technical conferences (and maybe others?). The idea is that people can do a quick talk about a topic of their choice -- much lower pressure than a "real" talk -- but gives folks a chance to show off something they have worked on. + +For this class, it's a chance to us to learn a bit about each-other and maybe something new about Python. + +Each of you will be required to give one lightning talk at some point during the course. + +**Lightning Talks Requirements** + + * 5 minutes each (including setup) - no kidding! + * Every student will give one + * Purposes: introduce yourself, share interests, show Python applications + * Any topic you like that is related to Python -- according to you! + +Schedule the lightning talks: +----------------------------- + +We need to schedule your lightning talks. + +**Let's use Python for that !** + +There is a class list in the class repo here: + +``examples/session01/students.txt`` + +Let's write a script to generate a random talk schedule... + + + diff --git a/_sources/modules/Closures.rst.txt b/_sources/modules/Closures.rst.txt new file mode 100644 index 0000000..c7c559e --- /dev/null +++ b/_sources/modules/Closures.rst.txt @@ -0,0 +1,592 @@ +.. _closures: + +############################## +Closures and Function Currying +############################## + +Defining specialized functions on the fly. + +A "Closure" is a fancy CS term that can be very hard to understand. Partly because they are expressed a little differently in every computer language that supports them But this is easiest definition I could find: + + Closure is when a function “remembers” its lexical scope even when the function is executed outside that lexical scope + +This definition is provided by Kyle Simpson +(by way of `an article about closures in Javascript `_). + +The basic idea behind the concept of a closure lies in the understanding of the fact that closure is a mechanism by which an inner function will have access to the variables defined in its outer function’s lexical scope even after the outer function has returned. + +Which brings us to the key practical part of how this works in Python: + + +Scope +===== + +In order to get a handle on all this, it's important to understand variable scoping rules in Python. + +"Scope" is the word for `where` the names in your code are accessible. Another word for a scope is namespace. + +Global Scope +------------ + +The simplest is the global scope. This is where all the names defined right in your code file (module) are. When running in an interactive interpreter, it is in the global namespace as well. + +You can get the global namespace with the ``globals()`` function, but ... + +The Python interpreter defines a handful of names when it starts up, and iPython defines a whole bunch more. +Recall that a convention in Python is that names that start with an underscore are "special" in some way -- double underscore names have a special meaning to Python, and single underscore names are considered "private". +Most of the extra names defined by the Python interpreter or iPython that are designed for internal use start with an underscore. These can really "clutter up" the namespace, but they can be filtered out for a more reasonable result: + +.. code-block:: python + + def print_globals(): + ipy_names = ['In', 'Out', 'get_ipython', 'exit', 'quit'] + for name in globals().keys(): + if not (name.startswith("_") or name in ipy_names): + print(name) + +Try running that in a newly started interpreter: + +.. code-block:: ipython + + In [3]: def print_globals(): + ...: ipy_names = ['In', 'Out', 'get_ipython', 'exit', 'quit'] + ...: for name in globals().keys(): + ...: if not (name.startswith("_") or name in ipy_names): + ...: print(name) + ...: + + In [4]: print_globals() + print_globals + +The only name left is "print_globals" itself -- created when we defined the function. + +.. note:: Try running ``globals()`` by itself to see all the cruft iPython adds. Also note that ``globals`` returns not just the names, but a dictionary, where the keys are the names, and the items are the values bound to those names. + +If we add a name or two, they show up in the global scope: + +.. code-block:: ipython + + In [6]: x = 5 + + In [7]: this = "that" + + In [8]: print_globals() + print_globals + x + this + +names are created by assignment, and by ``def`` and ``class`` statements. We already saw a ``def``, here is a ``class`` definition. + +.. code-block:: ipython + + In [11]: class TestClass: + ...: pass + ...: + + In [12]: print_globals() + print_globals + x + this + test + TestClass + +Always keep in mind that in Python, "global" means "global to the module", *not* global to the entire program. In the case of the interactive interpreter, the module is the "__main__" module (remember ``if __name__ == __main__:``?). But in a particular python file (usually one file is one module), the global scope is global to that one file. + + +Local Scope +----------- + +So that's the global scope -- what creates a new scope? + +A new, "local" scope is created by a function or class definition: + +There is a built-in function to get the names in the local scope, too, so we can use it to show us the names in a function's local namespace. There isn't a lot of cruft in the local namespace, so we don't need a special function to print it. + +Note that ``locals()`` and ``globals()`` returns a dict of the names and the objects they are bound to, so we can print the keys to get the names: + +.. code-block:: ipython + + In [15]: def test(): + ...: x = 5 + ...: y = 6 + ...: print(locals().keys()) + ...: + + In [16]: test() + dict_keys(['y', 'x']) + +When a function is called, it creates a clean local namespace. + +Similarly a class definition does the same thing: + +.. code-block:: ipython + + In [18]: class Test: + ...: this = "that" + ...: z = 54 + ...: def __init__(self): + ...: pass + ...: print(locals().keys()) + ...: + dict_keys(['__module__', '__qualname__', 'this', 'z', '__init__']) + +Interesting -- that print statement ran when the class was defined... + +But you see that class attributes are there, as is the ``__init__`` function. + +So each function gets a local namespace (or scope), and so does each class. And it follows that each method (function) in the class gets its own namespace as well. + +Turns out that this holds true for functions defined within functions also: + +.. code-block:: ipython + + In [23]: def outer(): + ...: x = 5 + ...: y = 6 + ...: def inner(): + ...: w = 7 + ...: z = 8 + ...: print("inner scope:", locals().keys()) + ...: print("outer scope:", locals().keys()) + ...: inner() + + In [24]: outer() + outer scope: dict_keys(['inner', 'y', 'x']) + inner scope: dict_keys(['z', 'w']) + +Function Parameters +------------------- + +The other way you can define names in a function's local namespace is with function parameters: + + +.. code-block:: ipython + + In [14]: def fun_with_parameters(a, b=0): + ...: print("local names are:", locals().keys()) + ...: + ...: + + In [15]: fun_with_parameters(4) + local names are: dict_keys(['a', 'b']) + +Notice that no other names have been defined in the function, but both of the parameters (positional and keyword) are local names. + + +Finding Names +------------- + +At any point, there are multiple scopes in play: the local scope, and all the surrounding scopes. +When you use a name, python checks in the local scope first, then moves out one by one until it finds the name. +If you define a new name inside a function, it "overrides" the name in any of the outer scopes. +But any names not defined in an inner scope will be found by looking in the enclosing scopes. + +.. code-block:: ipython + + In [33]: name1 = "this is global" + + In [34]: name2 = "this is global" + + In [35]: def outer(): + ...: name2 = "this is in outer" + ...: def inner(): + ...: name3 = "this is in inner" + ...: print(name1) + ...: print(name2) + ...: print(name3) + ...: inner() + ...: + + In [36]: outer() + this is global + this is in outer + this is in inner + +Look carefully to see where each of those names came from. All the print statements are in the inner function, so its local scope is searched first, and then the outer function's scope, and then the global scope. ``name1`` is only defined in the global scope, so that one is found. but ``name2`` is redfined in the scope of the ``outer`` function, so that one is found. And ``name3`` is only defined in the ``inner`` function scope. + +The ``global`` keyword +---------------------- + +Global names can be accessed from within functions, but not if that same name is created in the local scope. So you can't change an immutable object that is outside the local scope: + +.. code-block:: ipython + + In [37]: x = 5 + + In [38]: def increment_x(): + ...: x += 5 + ...: + + In [39]: increment_x() + --------------------------------------------------------------------------- + UnboundLocalError Traceback (most recent call last) + in () + ----> 1 increment_x() + + in increment_x() + 1 def increment_x(): + ----> 2 x += 5 + 3 + + UnboundLocalError: local variable 'x' referenced before assignment + +The problem here is that ``x += 5`` is the same as ``x = x + 5``, so it is creating a local name, but it can't be incremented, because it hasn't had a value set yet. + +How does the interpreter know that ``x`` is a local name? When it compiles the function definition, it marks all the names assigned in the function as local. So when the function runs, it knows that ``x`` is local, and thus it won't go look in the global scope for it. + +The ``global`` keyword tells python that you want to use the global name, rather than create a new, local name: + +.. code-block:: ipython + + In [40]: def increment_x(): + ...: global x + ...: x += 5 + ...: + ...: + + In [41]: increment_x() + + In [42]: x + Out[42]: 10 + +**NOTE:** The use of ``global`` is frowned upon -- having global variables manipulated in arbitrary other scopes makes for buggy, hard to maintain code! You hardly ever need to use ``global`` -- if a function needs to manipulate a value, you should pass that value into the function, or have it return a value that can then be used to change the global name. + + +``nonlocal`` keyword +-------------------- + +The other limitation with ``global`` is that there is only one global namespace. What if you are in a nested scope, and want to get at the value outside the current scope, but not all the way up at the global scope: + +.. code-block:: ipython + + In [1]: x = 5 + + In [2]: def outer(): + ...: x = 10 + ...: def inner(): + ...: x += 5 + ...: inner() + ...: print("x in outer is:", x) + +That's not going to work as the inner x hasn't been initialized: + +``UnboundLocalError: local variable 'x' referenced before assignment`` + +But if we use ``global``, we'll get the global ``x``: + +.. code-block:: ipython + + In [4]: def outer(): + ...: x = 10 + ...: def inner(): + ...: global x + ...: x += 5 + ...: inner() + ...: print("x in outer is:", x) + ...: + + In [5]: x + Out[5]: 5 + + In [6]: outer() + x in outer is: 10 + + In [7]: x + Out[7]: 10 + + In [8]: outer() + x in outer is: 10 + + In [9]: x + Out[9]: 15 + +This indicates that the global ``x`` is getting changed, but not the one in the ``outer`` scope. + +This is enough of a limitation that Python 3 added a new keyword: ``nonlocal``. +What it means is that the name should be looked for outside the local scope, but only as far as you need to go to find it: + +.. code-block:: ipython + + In [10]: def outer(): + ...: x = 10 + ...: def inner(): + ...: nonlocal x + ...: x += 5 + ...: inner() + ...: print("x in outer is:", x) + ...: + + In [11]: outer() + x in outer is: 15 + +So the ``x`` in the ``outer`` function scope is the one being changed. + +While using ``global`` is discouraged, ``nonlocal`` is safer -- as long as it is referring to a name in a scope that is closely defined like the above example. In fact, ``nonlocal`` will not go all the way up to the global scope to find a name: + +.. code-block:: ipython + + In [15]: def outer(): + ...: def inner(): + ...: nonlocal x + ...: x += 5 + ...: inner() + ...: print("x in outer is:", x) + ...: + File "", line 3 + nonlocal x + ^ + SyntaxError: no binding for nonlocal 'x' found + +But it will go up multiple levels in nested scopes: + +.. code-block:: ipython + + In [16]: def outer(): + ...: x = 10 + ...: def inner(): + ...: def inner2(): + ...: nonlocal x + ...: x += 10 + ...: inner2() + ...: inner() + ...: print("x in outer is:", x) + ...: + + In [17]: outer() + x in outer is: 20 + + +Closures +======== + +Now that we have a good handle on namespace scope, we can get to see why this is all really useful. + +"Closures" is a cool CS term for what is really just defining functions on the fly with some saved state. You can find a "proper" definition here: + +`Closures on Wikipedia `_ + +But I have trouble following that, so we'll look at real world examples to get the idea -- it's actually pretty logical, once you have idea about how scope works in Python. + + +Functions Within Functions +-------------------------- + +We've been defining functions within functions to explore namespace scope. But functions are "first class objects" in python, so we can not only define them and call them, but we can assign names to them and pass them around like any other object. + +So after we define a function within a function, we can actually return that function as an object: + +.. code-block:: python + + def counter(start_at=0): + count = start_at + def incr(): + nonlocal count + count += 1 + return count + return incr + +This looks a lot like the previous examples, but we are returning the function that was defined inside the function. Which means is can be used elsewhere. + +What's going on here? +..................... + +We have passed the ``start_at`` value into the ``counter`` function. + +We have stored it in ``counter``'s scope as a local variable: ``count`` + +Then we defined a function, ``incr`` that adds one to the value of count, and returns that value. + +Note that we declared ``count`` to be nonlocal in ``incr``'s scope, so that it would be the same ``count`` that's in counter's scope. + +What type of object do you get when you call ``counter()``? + +.. code-block:: ipython + + In [37]: c = counter(start_at=5) + + In [38]: type(c) + Out[38]: function + +So we get a function back -- makes sense. The ``def`` defines a function, and that function is what's getting returned. + +Being a function, we can, of course, call it: + +.. code-block:: ipython + + In [39]: c() + Out[39]: 6 + + In [40]: c() + Out[40]: 7 + +Each time is it called, it increments the value by one -- as you'd expect. + +But what happens if we call ``counter()`` multiple times? + +.. code-block:: ipython + + In [41]: c1 = counter(5) + + In [42]: c2 = counter(10) + + In [43]: c1() + Out[43]: 6 + + In [44]: c2() + Out[44]: 11 + +So each time ``counter()`` is called, a new ``incr`` function is created. Along with the new function, a new namespace is created that holds the ``count`` name. So the new ``incr`` function is holding a reference to that new ``count`` name. + +This is what makes it a "closure" -- it carries with it the scope in which it was created (or enclosed - I guess that's where the word closure comes from). + +The returned ``incr`` function is a "curried" function -- a function with some parameters pre-specified. + +Let's experiment a bit more with these ideas: + +:download:`play_with_scope.py <../examples/closures_currying/play_with_scope.py>` + +Currying +======== + +"Currying" is a special case of closures: + +`Currying on Wikipedia `_ + +The idea behind currying is that you may have a function with a number of parameters, and you want to make a specialized version of that function with a couple of parameters pre-set. + + +Real world Example +------------------ + +I was writing some code to compute the concentration of a contaminant in a river, as it was reduced by exponential decay, defined by a half-life: + +https://en.wikipedia.org/wiki/Half-life + +So I wanted a function that would compute how much the concentration would reduce as a function of time -- that is: + +.. code-block:: python + + def scale(time): + return scale_factor + +The trick is, how much the concentration would be reduced depends on both time and the half life. And for a given material, and given flow conditions in the river, that half life is pre-determined. Once you know the half-life, the scale is given by: + +.. code-block:: python + + scale = 0.5 ** (time / (half_life)) + +So to compute the scale, I could pass that half-life in each time I called the function: + +.. code-block:: python + + def scale(time, half_life): + return 0.5 ** (time / (half_life)) + +But this is a bit klunky -- I need to keep passing that ``half_life`` around, even though it isn't changing. And there are places, like ``map`` that require a function that takes only one argument! + +What if I could create a function, on the fly, that had a particular half-life "baked in"? + +*Enter Currying* -- Currying is a technique where you reduce the number of parameters that function takes, creating a specialized function with one or more of the original parameters set to a particular value. Here is that technique, applied to the half-life decay problem: + +.. code-block:: python + + def get_scale_fun(half_life): + def half_life_fun(time): + return 0.5 ** (time / half_life) + return half_life_fun + +**NOTE:** This is simple enough to use a lambda for a bit more compact code: + +.. code-block:: python + + def get_scale_fun(half_life): + return lambda time: 0.5 ** (time / half_life) + +Using the Curried Function +.......................... + +Create a scale function with a half-life of one hour: + +.. code-block:: ipython + + In [8]: scale = get_scale_fun(1) + + In [9]: [scale(t) for t in range(7)] + Out[9]: [1.0, 0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625] + +The value is reduced by half every hour. + +Now create one with a half life of 2 hours: + +.. code-block:: ipython + + In [10]: scale = get_scale_fun(2) + + In [11]: [scale(t) for t in range(7)] + Out[11]: + [1.0, + 0.7071067811865476, + 0.5, + 0.3535533905932738, + 0.25, + 0.1767766952966369, + 0.125] + +And the value is reduced by half every two hours... + +And it can be used with ``map``, too: + +.. code-block:: ipython + + In [13]: list(map(scale, range(7))) + Out[13]: + [1.0, + 0.7071067811865476, + 0.5, + 0.3535533905932738, + 0.25, + 0.1767766952966369, + 0.125] + + +``functools.partial`` +--------------------- + +The ``functools`` module in the standard library provides utilities for working with functions: + +https://docs.python.org/3.5/library/functools.html + +Creating a curried function turns out to be common enough that the ``functools.partial`` function provides an optimized way to do it: + +What ``functools.partial`` does is: + + * Makes a new version of a function with one or more arguments already filled in. + * The new version of a function documents itself. + +Example: + +.. code-block:: python + + def power(base, exponent): + """returns based raised to the give exponent""" + return base ** exponent + +Simple enough. but what if we wanted a specialized ``square`` and ``cube`` function? + +We can use ``functools.partial`` to *partially* evaluate the function, giving us a specialized version: + +.. code-block:: python + + square = partial(power, exponent=2) + cube = partial(power, exponent=3) + + + + + + + + + + + diff --git a/_sources/modules/CodeReviews.rst.txt b/_sources/modules/CodeReviews.rst.txt new file mode 100644 index 0000000..0c55b23 --- /dev/null +++ b/_sources/modules/CodeReviews.rst.txt @@ -0,0 +1,100 @@ +:orphan: + +.. _code_review: + +############ +Code Reviews +############ + + +Why code review +=============== +As professional developers, we need to always be learning and improving. + +Code review is one of the best tools for this. + +Code review is like having a personal tutor. + +Code review helps more people to become familiar with the code. + +Code review squishes bugs. + + +Getting code ready for review +----------------------------- + +- Write tests - if there are tests, much easier to make changes on the fly during review +- First draft is messy, refactor before code review +- To get the most out of it, correct all that you can before review +- Expect advice and corrections, and learn from them! +- For this class, messy is okay. :-) + +Size of code +------------ + +- Code to review should be between 200 and 400 lines of code +- Code can, and often will, be part of a bigger project +- Code should be modular, so can examine one smallish piece during review: + - and be able to explain how it fits in the bigger project + - and be able to explain what the smallish piece is doing in a few sentences + + +When/Why to review code +----------------------- + +- Think your code is ready for production +- Have code working, but seems there is probably a better way +- Totally Stuck +- Having difficulty making it modular, can help you break large chunks into smaller chunks + + +What to look for +---------------- + +- readability +- 'pythonic' +- tests +- short functions/methods +- anything not clear +- logic issues + + +Types of code review +-------------------- + +- in person +- in-line comments +- using tools https://en.wikipedia.org/wiki/List_of_tools_for_code_review + + +When refactoring or doing code reviews in person +------------------------------------------------ + +Write comments explaining what the code is doing (if unclear) and/or questions about the code + +Then, if time permits you can jointly: + +- work on making the code clearer +- run tests after changes +- goal is to make it clear enough to get rid of the comment(s) that were added + +Or do this yourself afterwards, as you would for written code reviews + + +How this will work in class +--------------------------- + +- Code can be pretty rough, no judging. :) + +- Person having code reviewed will send out link to code (a couple of days out, preferably) + +- At the beginning of the code review, reviewee will: + + - give a quick overview of the bigger picture + + - give a quick overview of the specific code we will look at + +- We will go over code together. Everyone is encouraged to make comments and ask questions. + +Feel free to ask your classmates in slack or on the mailing list about code you are writing. +This is why you have access to everyone's code, to share and learn from each other. diff --git a/_sources/modules/CollectionsModule.rst.txt b/_sources/modules/CollectionsModule.rst.txt new file mode 100644 index 0000000..842e2fa --- /dev/null +++ b/_sources/modules/CollectionsModule.rst.txt @@ -0,0 +1,81 @@ +.. _collections_module: + +###################### +The Collections Module +###################### + +Python has a very complete set of built in standard types that support most programming tasks. These include strings and numbers, and also types that can be used to hold other objects -- or "collection" types. + +* tuples +* lists +* dictionaries + +You can get pretty darn far with just these basic types -- but some problems require (or could be helped by) more complex collection types. + +This was recognised by the Python development team, so a number of genreally useful collection types are provided in the collections module. + +The collections module +---------------------- + +The first step is to see what's there by looking at the documentation: + +https://docs.python.org/3/library/collections.html + +You can see a pretty nice list of options (kind of in order of usefulness) + +* ``namedtuple()``: factory function for creating tuple subclasses with named fields +* ``deque``: list-like container with fast appends and pops on either end +* ``Counter``: dict subclass for counting hashable objects +* ``OrderedDict``: dict subclass that remembers the order entries were added +* ``defaultdict``: dict subclass that calls a factory function to supply missing values +* ``ChainMap``: dict-like class for creating a single view of multiple mappings + +These are just the regular builtin types, but in a form that they can be subclassed -- to make your own custom version. + +* ``UserDict``: wrapper around dictionary objects for easier dict subclassing +* ``UserList``: wrapper around list objects for easier list subclassing +* ``UserString``: wrapper around string objects for easier string subclassing + +To get an idea what these all are, read the docs, or a nice overview Python Module of the Week: + +https://pymotw.com/3/collections/ + +Using the collection types +-------------------------- + +To use these special types, they must be imported: + +.. code-block:: ipython + + In [4]: from collections import defaultdict + +Then you can use it -- creating a ``defaultdict`` with a empty list as a default: + +.. code-block:: ipython + + In [8]: dd = defaultdict(list) + + In [9]: dd['this'].append(23) + + In [10]: dd + Out[10]: defaultdict(list, {'this': [23]}) + + In [11]: dd['this'].append(4) + + In [12]: dd['this'].append(4) + + In [13]: dd + Out[13]: defaultdict(list, {'this': [23, 4, 4]}) + + In [14]: dd['that'].append(4) + +And you'll get a dict that will automatically put an empty list in when the key isn't there yet. Kind of a handy replacement from having to call ``dict.setdefault`` each time. + +Similarly for the others. + +Take a bit of time to try them out -- you may find them really useful. + + + + + diff --git a/_sources/modules/Comprehensions.rst.txt b/_sources/modules/Comprehensions.rst.txt new file mode 100644 index 0000000..397933f --- /dev/null +++ b/_sources/modules/Comprehensions.rst.txt @@ -0,0 +1,480 @@ +.. _comprehensions: + +############## +Comprehensions +############## + +**A bit of functional programming.** + + +List Comprehensions +------------------- + +The concept of "functional programming" is clearly defined in some contexts, but is also used in a less strict sense. Python is **not** a functional language in the strict sense, but it does support a number of functional paradigms. + +In general, code is considered "Pythonic" that uses functional paradigms where they are natural, but not when they have to be forced in. + +We will cover functional programming concepts more clearly later in the program, but for now, we'll talk about the syntax for a common functional paradigm: applying an expression to all the members of a sequence to produce another sequence. + +Consider this common ``for`` loop structure: + +.. code-block:: python + + new_list = [] + for variable in a_list: + new_list.append(expression_with_variable)) + +This is such a common pattern that python added syntax to directly support it. This syntax is known as "comprehensions". The most common of which is a list comprehension, used to build up a new list. There are a couple others, which we will get too later, but they all share a similar structure. + +The above structure can be expressed with a single line using a "list comprehension" like so: + +.. code-block:: python + + new_list = [expression_with_variable for variable in a_list] + +Nice and clear and compact, and the use of the "list" brackets (``[...]``) makes it clear you are making a list. + +Recall what an expression is in Python: a bit of code (names and operators) that evaluates to a value. So in the beginning of a comprehension, you can put anything that evaluates to a value -- and that value is what gets added to the new list. +This can be a simple (or complex) math operation: ``x * 3``, or a function or method call: ``a_string.upper()``, ``int(x)``, etc. +But it can not contain any statements: code that does not return a value, such as assignment (``x = 5``), or ``for`` loops, or ``if`` blocks. + + +Nested Loops +............ + +What about nested for loops? Sometimes you need to build up a list by looping over two sequences like so: + +.. code-block:: python + + new_list = [] + for var in a_list: + for var2 in a_list2: + new_list.append(expression_with_var_and_var2) + +This can also be expressed with a comprehension in one line: + +.. code-block:: python + + new_list = [expression_with_var_and_var2 for var in a_list for var2 in a_list2] + +But the two lists are not looped through in parallel. Rather, you get all combinations of the two lists -- Sometimes called the "outer product". + +For example: + +.. code-block:: ipython + + In [33]: list1 = [1, 2, 3] + + In [34]: list2 = [4, 5] + + In [35]: [(a, b) for a in list1 for b in list2] + Out[35]: [(1, 4), (1, 5), (2, 4), (2, 5), (3, 4), (3, 5)] + +Note that it makes every combination of the two input lists, and thus will be ``len(list1) * len(list2)`` in size. And there is no reason for them to be the same size. + +zip() with comprehensions +......................... + +If you want them paired up instead, you can use ``zip()``: + +.. code-block:: ipython + + In [31]: [(a, b) for a, b in zip(list1, list2)] + Out[31]: [(1, 4), (2, 5)] + + +Comprehensions and map() +........................ + +Comprehensions are another way of expressing the "map" pattern from functional programming. + +Python does have a ``map()`` function, which pre-dates comprehensions. But it does much of the same things -- and most folks think comprehensions are the more "Pythonic" way to do it. And there is nothing that can be expressed with ``map()`` that cannot be done with a comprehension. If you are not familiar with ``map()``, you can safely skip this, but if you are: + +.. code-block:: python + + map(a_function, an_iterable) + +is the same as: + +.. code-block:: python + + [a_function(item), for item in an_iterable] + +In this case, the comprehension is a tad wordier than ``map()``. But comprehensions really shine when you don't already have a handy function to pass to map: + +.. code-block:: python + + [x**2 for x in an_iterable] + +To use ``map()``, you need a function: + +.. code-block:: python + + def square(x): + return x**2 + + map(square, an_iterable) + +There are shortcuts of course, including ``lambda`` (stay tuned for more about that): + +.. code-block:: python + + map(lambda x: x**2, an_iterable) + +But is that easier to read or write? + + +What about filter? +.................. + +"filtering" is another functional concept: building a new list with only *some* of the elements -- "filtering" out the ones you don't want. Python has a ``filter()`` function, also pre-dating comprehensions, but you can do it with a comprehension as well, and it does the application of the expression and the filtering in one construct, rather than having to nest ``map`` and ``filter`` calls. + +This supports the common case of having a conditional in the loop: + +.. code-block:: python + + new_list = [] + for variable in a_list: + if something_is_true: + new_list.append(expression) + +This kind of "filtering" loop can be achieved by adding a conditional to the comprehension: + +.. code-block:: python + + new_list = [expr for var in a_list if something_is_true] + +This is expressing the "filter" pattern and the "map" pattern at the same time -- one reason I like the comprehension syntax so much. + + +.. rubric:: Examples: + +.. code-block:: ipython + + In [341]: [x**2 for x in range(3)] + Out[341]: [0, 1, 4] + + In [342]: [x+y for x in range(3) for y in range(5,7)] + Out[342]: [5, 6, 6, 7, 7, 8] + + In [343]: [x*2 for x in range(6) if not x%2] + Out[343]: [0, 4, 8] + + +Get creative.... + +How do I see all the built in Exceptions? + +.. code-block:: python + + [name for name in dir(__builtin__) if "Error" in name] + ['ArithmeticError', + 'AssertionError', + 'AttributeError', + 'BufferError', + 'EOFError', + .... + +Note that the last one was only filtering (``if "Error" in name``), without applying any expression to the items (``name for name``). + + +Set Comprehensions +------------------ + +You can do a similar thing with sets, as well: + +.. code-block:: python + + new_set = {expression_with_variable for variable in a_sequence} + +The curly brackets (``{...}``) indicate a set. + +This results in the same set as this for loop: + +.. code-block:: python + + new_set = set() + for variable in a_sequence: + new_set.add(expression_with_variable) + +or, indeed, the same as passing a list comp to ``set()``. + +.. code-block:: python + + new_set = set([expression_with_variable for variable in a_sequence]) + + +**Example:** Finding all the vowels in a string... + +.. code-block:: ipython + + In [19]: s = "a not very long string" + + In [20]: vowels = set('aeiou') + + In [21]: { l for l in s if l in vowels } + Out[21]: {'a', 'e', 'i', 'o'} + +.. note:: + + Why did I use ``set('aeiou')`` rather than just ``'aeiou'`` ? ... ``in`` works with strings as well, but is it efficient? + + +Dict Comprehensions +------------------- + +You can also build up a dictionary with a comprehension: + +.. code-block:: python + + new_dict = {key: value for variable in a_sequence} + + +Which is the same as this for loop: + +.. code-block:: python + + new_dict = {} + for key in a_list: + new_dict[key] = value + +A dict comprehension also uses curly brackets like the set comprehension -- Python knows it's a dict comprehension due to the ``key: value`` construct. + +**Example:** + +.. code-block:: ipython + + In [22]: { i: "this_%i"%i for i in range(5) } + Out[22]: {0: 'this_0', 1: 'this_1', 2: 'this_2', + 3: 'this_3', 4: 'this_4'} + + +A bit of History: +................. + +dict comps are not as useful as they used to be, now that we have the ``dict()`` constructor. + +In the early days of Python the only way to create a dict was with a literal:: + + a_dict = {} # an empty dict + +or a dict that was already populated with a bunch of data. + +If you had a bunch of data in some other form, like a couple of lists, you'd need to write a loop to fill it in: + +.. code-block:: ipython + + In [1]: names = ["fred", "john", "mary"] + + In [2]: ids = [1, 2, 3] + + In [4]: d = {} + + In [5]: for id, name in zip(names, ids): + ...: d[id] = name + ...: + + In [6]: d + Out[6]: {'fred': 1, 'john': 2, 'mary': 3} + +now, with dict comps, you can do: + +.. code-block:: ipython + + In [9]: d = {id: name for id, name in zip(ids, names)} + + In [10]: d + Out[10]: {1: 'fred', 2: 'john', 3: 'mary'} + +But there is also a ``dict()`` constructor (actually the type object for dict): + +.. code-block:: ipython + + In [13]: dict? + Init signature: dict(self, /, *args, **kwargs) + Docstring: + dict() -> new empty dictionary + dict(mapping) -> new dictionary initialized from a mapping object's + (key, value) pairs + dict(iterable) -> new dictionary initialized as if via: + d = {} + for k, v in iterable: + d[k] = v + dict(**kwargs) -> new dictionary initialized with the name=value pairs + in the keyword argument list. For example: dict(one=1, two=2) + Type: type + +``dict()`` can take different types of arguments, and will do something different with each one. + +The first option (no argument) is an empty dict -- simple enough. + +The option makes a dict from the contents of another dict or similar object (called a "mapping"). + +The options is of interest here -- it makes a dict from an iterable of key, value pairs -- exactly what ``zip()`` gives you. + +So we can create a dict from data like so: + +.. code-block:: ipython + + In [14]: d = dict(zip(ids, names)) + + In [15]: d + Out[15]: {1: 'fred', 2: 'john', 3: 'mary'} + +Which is more compact, and arguably more clear, than the dict comprehension. + +dict comps are still nice if you need to filter the results, though: + +.. code-block:: ipython + + In [16]: d = {id: name for id, name in zip(ids, names) if name != 'mary'} + + In [17]: d + Out[17]: {1: 'fred', 2: 'john'} + + +Generator Comprehensions +------------------------ + +There is yet another type of comprehension: generator comprehensions, technically known as "generator expressions". They are very much like a list comprehension, except that they evaluate to a lazy-evaluated "iterable", rather than a list. That is, they *generate* the items on the fly. + +This is useful, because we often create a comprehension simply to loop over it right away: + +.. code-block:: python + + for x in [y**2 for y in a_sequence]: + outfile.write(f"The number is: {x}") + +In this case, the list comprehension: ``[y**2 for y in a_sequence]`` iterates over ``a_sequence``, computes the square of each item, and creates a whole new list with the new values. +All this, just so it can be iterated over again right away. If the original sequence is large (or is itself a lazy-evaluated iterable), then the step of creating the extra list can be expensive and unnecessary. + +Generator comprehensions, on the other hand, create an iterable that evaluates the items as they are iterated over, rather than all at once ahead of time -- so the entire collection is never stored. + +The syntax for a generator comprehension is the same as a list comp, except it uses regular parentheses:: + + (y**2 for y in a_sequence) + +So what does that evaluate to? A list comp evaluates to a list: + +.. code-block:: ipython + + In [1]: l = [x**2 for x in range(4)] + + In [2]: l + Out[2]: [0, 1, 4, 9] + + In [3]: type(l) + Out[3]: list + +A generator comp evaluates to a generator: + +.. code-block:: ipython + + In [4]: g = (x**2 for x in range(4)) + + In [5]: g + Out[5]: at 0x102bbed00> + + In [6]: type(g) + Out[6]: generator + +A generator is an object that can be iterated over with a for loop, and it will return the values as they are asked for: + +.. code-block:: ipython + + In [7]: for i in g: + ...: print(i) + ...: + 0 + 1 + 4 + 9 + +You will learn more about generators and other ways to make them in future lessons. + +Let's use a little function to make this clear: + +.. code-block:: ipython + + In [8]: def test(x): + ...: print("test called with: ", x) + ...: return x ** 2 + +It simply returns the square of the passed-in value, but prints it as it does so, so we can see when it is called. + +.. note:: + Having a "print" in a function is a example of a "side effect" -- something that is an effect of the function being called that is not reflected in the return value of that function. + As a rule, it's not a good idea to use functions with side effects in comprehensions. We're only doing it here as a debugging aid -- so we can clearly see when the function is being called. + +If we use it in a list comp: + +.. code-block:: ipython + + In [10]: [test(x) for x in range(3)] + test called with: 0 + test called with: 1 + test called with: 2 + Out[10]: [0, 1, 4] + +We see that ``test()`` gets called for all the values, and then a list is returned with all the results. +But if we use it in a generator comprehension: + +.. code-block:: ipython + + In [11]: g = (test(x) for x in range(3)) + +Nothing gets printed (the function has not been called) until you loop through it: + +.. code-block:: ipython + + In [16]: for i in g: + ...: print(i) + ...: + test called with: 0 + 0 + test called with: 1 + 1 + test called with: 2 + 4 + +You can see that ``test()`` is getting called for each item *as* the loop is run. + +You usually don't assign a generator expression to a variable, but rather, loop through it right away: + +.. code-block:: ipython + + In [17]: for i in (test(x) for x in range(3)): + ...: print(i) + ...: + test called with: 0 + 0 + test called with: 1 + 1 + test called with: 2 + 4 + +When to Use What +................ + +It's pretty simple: + +If you need a list (or a set or dict) for further work, then use a list comp. + +If you are going to immediately loop through the items created by the comprehension, use a generator comprehension. + +.. note:: + + The "official" term is "generator expression" -- that is what you will see in the Python docs, and a lot of online discussions. I've used the term "generator comprehension" here to better make clear the association with list comprehensions. + +References +---------- + +This is a nice intro to comprehensions from Trey Hunner: + +https://treyhunner.com/2015/12/python-list-comprehensions-now-in-color/ + +Once you've got the hang of it, you may want to read this so you don't overdo it :-) + +https://treyhunner.com/2019/03/abusing-and-overusing-list-comprehensions-in-python/ + +Trey writes a lot of good stuff -- I recommend browsing his site. diff --git a/_sources/modules/Concurrency.rst.txt b/_sources/modules/Concurrency.rst.txt new file mode 100644 index 0000000..e38ebfe --- /dev/null +++ b/_sources/modules/Concurrency.rst.txt @@ -0,0 +1,365 @@ + +.. _concurrency: + +###################### +Concurrent Programming +###################### + +What does it mean to do something "Concurrently" ? It means multiple things are happening at the same time. But what are those "things"? + + - Parallelism is about processing multiple things at the same time -- true parallelism requires multiple processors (or cores). + - Concurrency is about handling multiple things at the same time -- which may or may not be actually running in the processor at the same time (like network requests for instance). + - Parallelism needs concurrency, but concurrency need not be in parallel. + + +Whirlwind Tour of Concurrency +============================= + +**Concurrency:** + +Having different code running at the same time, or kind of the same time. + +**Asynchrony:** + +The occurrence of events independent of the main program flow and ways to deal with such events. + +Asynchrony and Concurrency are really two different things -- you can do either one without the other -- but they are closely related, and often used together. They solve different problems, but the problems and the solutions overlap. + +"Concurrency is not parallelism" -- Rob Pike: https://vimeo.com/49718712 + +Despite Rob Pike using an example about burning books, I recommend listening to at least the first half of his talk. + +In that talk Rob Pike makes a key point: Breaking down tasks into concurrent subtasks only allows parallelism, it’s the scheduling of these subtasks that creates it. + +And, indeed, once you have a set of subtasks, they can be scheduled in a truly parallel fashion, or managed asynchronously in a single thread (concurrent, but not parallel) + + +Types of Concurrency +-------------------- + +**Multithreading:** + + Multiple code paths sharing memory -- one Python interpreter, one set of Python objects. + +**Multiprocessing:** + + Multiple code paths with separate memory space -- completely separate Python interpreter. + +**Asyncronous programming:** + + Multiple "jobs" run at "arbitrary" times -- but usually in one thread -- i.e. only one code path, one interpreter. + +Lots of different packages for both in the standard library and 3rd party libraries. + +How to know what to choose? + + - IO bound vs. CPU bound -- CPU bound requires multiprocessing (at least with pure Python) + - Event driven cooperative multitasking vs. preemptive multitasking + - Callbacks vs coroutines + scheduler/event loop + +Motivations for parallel execution +---------------------------------- + +- Performance + - Limited by "Amdahl's Law": http://en.wikipedia.org/wiki/Amdahl%27s_law + + - CPUs aren't getting much faster + +- Event handling + - If a system handles asynchronous events, a separate thread of execution could handle those events and let other threads do other work + + - Examples: + - Network applications + - User interfaces + +Parallel programming can be hard! + +If your problem can be solved sequentially, consider the costs and +benefits before going parallel. + + +Parallelization Strategy for Performance +---------------------------------------- + +| 1. Break problem down into chunks +| 2. Execute chunks in parallel +| 3. Reassemble output of chunks into result + +.. image:: /_static/OPP.0108.gif + :align: right + :height: 450px + :alt: multitasking flow diagram + + +| +| + +- Not every problem is parallelizable +- There is an optimal number of threads for each problem in each + environment, so make it tunable +- Working concurrently opens up synchronization issues +- Methods for synchronizing threads: + + - locks + - queues + - signaling/messaging mechanisms + +Other options +------------- + +Traditionally, concurency has been achieved through multiple process +communication and in-process threads, as we've seen. + +Another strategy is through micro-threads, implemented via coroutines +and a scheduler. + +A coroutine is a generalization of a subroutine which allows multiple +entry points for suspending and resuming execution. + +The threading and the multiprocessing modules follow a +`preemptive multitasking model `_ + +Coroutine based solutions follow a +`cooperative multitasking model: `_ + +Threads versus processes in Python +---------------------------------- + +Threads are lightweight processes_, run in the address space of an OS +process, true OS level threads. + +Therefore, a component of a process. + +.. _processes: https://en.wikipedia.org/wiki/Light-weight_process + +This allows multiple threads access to data in the same scope. + +Threads can not gain the performance advantage of multiple processors +due to the Global Interpreter Lock (GIL) + +But the GIL is released during IO, allowing IO bound processes to +benefit from threading + +Processes +--------- + +A process contains all the instructions and data required to execute +independently, so processes do not share data! + +Mulitple processes best to speed up CPU bound operations. + +The Python interpreter isn't lightweight! + +Communication between processes can be achieved via: + +``multiprocessing.Queue`` + +``multiprocessing.Pipe`` + +and regular IPC (inter-process communication) + +Data moved between processes must be pickleable + + +Advantages / Disadvantages of Threads +------------------------------------- + +Advantages: +........... + +They share memory space: + + - Threads are relatively lightweight -- shared memory means they can be created fairly quickly without much memory use. + + - Easy and cheap to pass data around (you are only passing a reference). + +Disadvantages: +.............. + +They share memory space: + + - Each thread is working with the *same* python objects. + - Operations often take several steps and may be interrupted mid-stream + - Thus, access to shared data is also non-deterministic + + (race conditions) + +Creating threads is easy, but programming with threads is difficult. + + Q: Why did the multithreaded chicken cross the road? + + A: to To other side. get the + + -- Jason Whittington + +GIL +--- + +**Global Interpreter Lock** + +(**GIL**) + +This is a lock which must be obtained by each thread before it can +execute, ensuring thread safety + +.. image:: /_static/gil.png + :width: 100.0% + + +The GIL is released during IO operations, so threads which spend time +waiting on network or disk access can enjoy performance gains + +The GIL is not unlike multitasking in humans, some things can truly be +done in parallel, others have to be done by time slicing. + +Note that potentially blocking or long-running operations, such as I/O, image processing, and NumPy number crunching, happen outside the GIL. Therefore it is only in multithreaded programs that spend a lot of time inside the GIL, interpreting CPython bytecode, that the GIL becomes a bottleneck. But: it can still cause performance degradation. + +Not only will threads not help cpu-bound problems, but it can actually make things *worse*, especially on multi-core machines! + +Python threads do not work well for computationally intensive work. + +Python threads work well if the threads are spending time waiting for something: + + - Database Access + - Network Access + - File I/O + +Some alternative Python implementations such as Jython and IronPython +have no GIL + +cPython and PyPy have one + +More about the gil + +More on the GIL: + +https://emptysqua.re/blog/grok-the-gil-fast-thread-safe-python/ + +If you really want to understand the GIL -- and get blown away -- watch this one: + +http://pyvideo.org/pycon-us-2010/pycon-2010--understanding-the-python-gil---82.html + + +- http://wiki.python.org/moin/GlobalInterpreterLock + +- https://docs.python.org/3/c-api/init.html#threads + +- http://hg.python.org/cpython/file/05e8dde3229c/Python/pystate.c#l761 + + +**NOTE:** The GIL *seems* like such an obvious limitation that you've got to wonder why it's there. And there have been multiple efforts to remove it. But it turns out that Python's design makes that very hard (impossible?) without severely reducing performance on single threaded programs. + +The current "Best" effort is Larry Hastings' `gilectomy `_ + +But that may be stalled out at this point, too. No one should count on it going away in cPython. + +But: **Personal Opinion:** Python is not really (directly) suited to the kind of computationally intensive work that the GIL really hampers. And extension modules (i.e. numpy) can release the GIL! + + +Posted without comment +---------------------- +.. figure:: /_static/killGIL.jpg + :class: fill + + +Advantages / Disadvantages of Processes +--------------------------------------- + +Processes are heavier weight -- each process makes a copy of the entire interpreter (Mostly...) -- uses more resources. + +You need to copy the data you need back and forth between processes. + +Slower to start, slower to use, more memory. + +But as the entire python process is copied, each subprocess is working with the different objects -- they can't step on each other. So there is: + + **no GIL** + +Multiprocessing is suitable for computationally intensive work. + +Works best for "large" problems with not much data to pass back and forth, as that's what's expensive. + +Note that there are ways to share memory between processes, if you have a lot of read-only data that needs to be used. (see `Memory Maps `_) + + + +Synchronization options: + + - Locks (Mutex: mutual exclusion, Rlock: reentrant lock) + - Semaphore + - BoundedSemaphore + - Event + - Condition + - Queues + + +Mutex locks (``threading.Lock``) +-------------------------------- + + - Probably most common + - Only one thread can modify shared data at any given time + - Thread determines when unlocked + - Must put lock/unlock around critical code in ALL threads + - Difficult to manage + +Easiest with context manager: + +.. code-block:: python + + x = 0 + x_lock = threading.Lock() + + # Example critical section + with x_lock: + # statements using x + + +Only one lock per thread! (or risk mysterious deadlocks) + +Or use RLock for code-based locking (locking function/method execution rather than data access) + + +Subprocesses (``subprocess``) +----------------------------- + +Subprocesses are completely separate processes invoked from a master process (your python program). + +Usually used to call non-python programs (shell commands). But of course, a Python program can be a command line program as well, so you can call either your or other python programs this way. + +Easy invocation: + +.. code-block:: python + + import subprocess + + subprocess.run('ls') + +The program halts while waiting for the subprocess to finish. (unless you call it from a thread!) + +You can control communication with the subprocess via: + +``stdout``, ``stdin``, ``stderr`` with: + +``subprocess.Popen`` + +Lots of options there! + + +Pipes and ``pickle`` and ``subprocess`` +....................................... + + - Very low level, for the brave of heart + - Can send just about any Python object + +For this to work, you need to send messages, as each process runs its own independent Python interpreter. + + +When to Use What +================ + +.. image:: /_static/proc_thread_async.png + + + + + diff --git a/_sources/modules/ContextManagers.rst.txt b/_sources/modules/ContextManagers.rst.txt new file mode 100644 index 0000000..60e5d8c --- /dev/null +++ b/_sources/modules/ContextManagers.rst.txt @@ -0,0 +1,321 @@ +.. _context_managers: + +################ +Context Managers +################ + +You've seen the ``with`` statement -- probably used for working with files. Now you'll learn what that's all about. + +Managing Resources +================== + +**Repetition in code stinks (DRY!)** + +A large source of repetition in code deals with the handling of external +resources. + +As an example, how many times do you think you might type something like the following code: + +.. code-block:: python + + file_handle = open('filename.txt', 'r') + file_content = file_handle.read() + file_handle.close() + # do some stuff with the contents + +Not only is this a couple extra lines of code to write, it's also prone to error: + +What happens if you forget to call ``.close()``? + +What happens if reading the file raises an exception? + +So you really should write it something like: + +.. code-block:: python + + try: + file_handle = open(...) + file_content = file_handle.read() + except IOError: + print("The file couldn't be opened") + finally: + file_handle.close() + +And that is getting pretty ugly, and hard to get right. + +Handling General Resources +-------------------------- + +Leaving an open file handle laying around is bad enough. What if the resource +is a network connection, or a database cursor? + +Starting in version 2.5, Python provides a structure for reducing the +repetition needed to handle resources like this. + +**Context Managers** + +You can encapsulate the setup, error handling, and teardown of resources in a +few simple steps. + +The key is to use the ``with`` statement. + +``with`` a little help +---------------------- + +Since the introduction of the ``with`` statement in `pep343 `_, the above seven lines of defensive code have been replaced with this simple form: + +.. code-block:: python + + with open('filename', 'r') as file_handle: + file_content = file_handle.read() + # do something with file_content + +The ``open`` builtin is defined as a *context manager*. + +The resource it returns (``file_handle``) is automatically and reliably closed +when the code block ends. + +At this point in Python history, many functions you might expect to behave this way do: + +* ``open`` works as a context manager. +* network connections via ``socket`` do as well. +* most implementations of database wrappers can open connections or cursors as + context managers. +* ... + +* But what if you are working with a library that doesn't support this + (``urllib``)? + + +Close It Automatically +---------------------- + +There are a couple of ways you can go. + +If the resource in questions has a ``.close()`` method, then you can simply use the ``closing`` context manager from ``contextlib`` to handle the issue: + +.. code-block:: python + + from urllib import request + from contextlib import closing + + with closing(request.urlopen('http://google.com')) as web_connection: + # do something with the open resource + # and here, it will be closed automatically + +But what if the thing doesn't have a ``close()`` method, or you're creating +the thing and it shouldn't have a ``close()`` method? + +(full confession: ``urlib.request`` was not a context manager in py2 -- but it is in py3 -- but the issue still comes up with third-party packages and your own code!) + +Do It Yourself +-------------- + +If you do need to support resource management of some sort, you can create a context manager of your own with the context manager protocol. + +The interface is simple. It must be a class that implements two +more of the nifty python *special methods* + +``__enter__(self)``: + Called when the ``with`` statement is run, it should return something to work with in the created context. + +``__exit__(self, e_type, e_val, e_traceback)``: + Clean-up that needs to happen is implemented here. + +The arguments will be the exception raised in the context. + +If the exception will be handled here, return ``True``. If not, return ``False``. + +Let's see this in action to get a sense of what happens. + +An Example +---------- + +Consider this code: + +.. code-block:: python + + class Context(object): + """from Doug Hellmann, PyMOTW + https://pymotw.com/3/contextlib/#module-contextlib + """ + def __init__(self, handle_error): + print('__init__({})'.format(handle_error)) + self.handle_error = handle_error + + def __enter__(self): + print('__enter__()') + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + print('__exit__({}, {}, {})'.format(exc_type, exc_val, exc_tb)) + return self.handle_error + +:download:`context_manager.py <../examples/context_managers/context_manager.py>` + +This class doesn't do much of anything, but playing with it can help +clarify the order in which things happen: + +.. code-block:: ipython + + In [46]: with Context(True) as foo: + ....: print('This is in the context') + ....: raise RuntimeError('this is the error message') + ....: + __init__(True) + __enter__() + This is in the context + __exit__(, this is the error message, + ) + +Because the ``__exit__`` method returns ``True``, the raised error is 'handled'. + +What if we try with ``False``? + +.. code-block:: ipython + + In [3]: with Context(False) as foo: + ...: print("this is in the context") + ...: raise RuntimeError('this is the error message') + ...: + __init__(False) + __enter__() + this is in the context + __exit__(, this is the error message, ) + --------------------------------------------------------------------------- + RuntimeError Traceback (most recent call last) + in () + 1 with Context(False) as foo: + 2 print("this is in the context") + ----> 3 raise RuntimeError('this is the error message') + + RuntimeError: this is the error message + +So this time, the context manager did not catch the error -- so it was raised in the usual way. + +The parameters to ``__exit__`` +------------------------------ + +In real life, a context manager could have pretty much any error raised in its context. And the context manager will likely only be able to "properly" handle particular Exceptions. + +So the ``__exit__`` method takes all the information about the exception as parameters: + +``def __exit__(self, exc_type, exc_val, exc_tb)`` + +``exc_type``: the type of the Exception + +``exc_val``: the value of the Exception + +``exc_tb``: the Exception Traceback object + +The type lets you check if this is a type you know how to handle: + +.. code-block:: python + + if exc_type is RuntimeError: + +The value is the exception object itself. + +And the traceback is a full traceback object. Traceback objects hold all the information about the context in which an error occurred. It's pretty advanced stuff, so you can mostly ignore it, but if you want to know more, there are tools for working with them in the ``traceback`` module. + +https://docs.python.org/3/library/traceback.html + +The ``contextmanager`` decorator +-------------------------------- + +Similar to writing iterable classes, there's a fair bit of bookkeeping involved. It turns out you can take advantage of generator functions to do that bookkeeping for you. + +``contextlib.contextmanager`` decorator will turn a generator function into context manager. + +Consider this code: + +.. code-block:: python + + from contextlib import contextmanager + + @contextmanager + def context(boolean): + print("__init__ code here") + try: + print("__enter__ code goes here") + yield object() + except Exception as e: + print("errors handled here") + if not boolean: + raise e + finally: + print("__exit__ cleanup goes here") + +The code is similar to the class defined previously. + +And using it has similar results. We can handle errors: + +.. code-block:: ipython + + In [96]: with context(True): + ....: print("in the context") + ....: raise RuntimeError("error raised") + ....: + __init__ code here + __enter__ code goes here + in the context + errors handled here + __exit__ cleanup goes here + + +Or, we can allow them to propagate: + +.. code-block:: ipython + + In [51]: with context(False): + ....: print("in the context") + ....: raise RuntimeError("error raised") + __init__ code here + __enter__ code goes here + in the context + errors handled here + __exit__ cleanup goes here + --------------------------------------------------------------------------- + RuntimeError Traceback (most recent call last) + in () + 1 with context(False): + 2 print "in the context" + ----> 3 raise RuntimeError("error raised") + 4 + RuntimeError: error raised + +Mixing context_managers with generators +--------------------------------------- + +You can put a ``yield`` inside a context manager as well. + +here is a generator function that gives yields all the files in a directory: + +.. code-block:: python + + import pathlib + + def file_yielder(dir=".", pattern="*"): + """ + iterate over all the files that match the pattern + + pattern use a "glob" pattern, like: *.py + """ + for filename in pathlib.Path(dir).glob(pattern): + with open(filename) as file_obj: + yield file_obj + +:download:`file_yielder.py <../examples/context_managers/file_yielder.py>` + +So the ``yield`` is inside the file context manager, so that state will be preserved while the file object is in use. + +This generator can be used like so: + +.. code-block:: ipython + + In [20]: for f in file_yielder(pattern="*.py"): + ...: print("The first line of: {} is:\n{}".format(f.name, f.readline())) + +Each iteration through the loop, the previous file gets closed, and the new one opened. If there is an exception raised inside that loop, the last file will get properly closed. + + diff --git a/_sources/modules/Coroutines.rst.txt b/_sources/modules/Coroutines.rst.txt new file mode 100644 index 0000000..df920f3 --- /dev/null +++ b/_sources/modules/Coroutines.rst.txt @@ -0,0 +1,109 @@ +.. _coroutines: + +################### +Notes on Coroutines +################### + +.. note:: These notes are incomplete, but maybe the first section is still useful? + +Coroutines are a key feature required to do "proper" async programming in Python. + +In practical use, coroutines are used in the context of an async framework that +provides handy utilities, and most importantly, an event loop to actually run the code. + +But it's helpful to play around a bit with coroutines on their own, to get a better understanding of what they really are, and how they work. + +What is a coroutine? +==================== + +Coroutines are functions that can hold state, and vary between invocations; +there can be multiple instances of a given coroutine at once. + +This may sound a bit familiar from generators -- a generator function can hold +state when it yields, and there can be multiple instances of the same generator +function at once. + +The difference is that coroutines, in addition to holding state, can also return +control flow back to the system while they are holding that state. + +Hopefully this will make a bit more sense after we've experimented a bit. + + +Coroutines By Themselves +======================== + +Coroutines are really only useful when controlled by an event loop. And for the most part, you are going to use an event loop provided by an async framework, like the built in``asyncio`` package. + +But it can be instructive to know about what is going on directly with coroutines, so we'll experiment a bit here: + +We can make a coroutine with the ``async`` keyword: + +.. code-block:: python + + async def corout(): + print("running corout") + +This, of course is a coroutine that does nothing but print a message. But let's run it and see what happens: + +.. code-block:: ipython + + In [28]: corout() + Out[28]: + +Hmm -- nothing. The print statement didn't happen. But what we got back is a "coroutine object". So calling a coroutine function doesn't run the code in the function, but rather creates a coroutine object and returns that. In fact, you can make any number of coroutine objects with the same "async def" function. + +Why is that? + +Recall from the definition of coroutines: "... that can hold state". So you want to be able to create multiple instances of a coroutine, so each one can hold different state (again, very similar to generators). + +So how do we actually run the code in the coroutine instance? First, we need to save it in a variable so we can refer to it, and then we can call its ``send`` method. + +.. code-block:: ipython + + In [33]: cr = corout() + + In [34]: type(cr) + Out[34]: coroutine + + In [35]: cr.send(None) + running corout + --------------------------------------------------------------------------- + StopIteration Traceback (most recent call last) + in () + ----> 1 cr.send(None) + + StopIteration: + +So calling ``send`` ran the print statement, and then raised a ``StopIteration`` exception. This is looking even more like a generator, isn't it? + +.. code-block:: ipython + + In [36]: def genfunction(): + ...: print("in the generator") + ...: yield None + ...: + + In [37]: g = genfunction() + + In [38]: next(g) + in the generator + + In [39]: next(g) + --------------------------------------------------------------------------- + StopIteration Traceback (most recent call last) + in () + ----> 1 next(g) + + StopIteration: + +And indeed, they have a lot on common -- in fact, before Python 3.5, when the ``async`` keyword was added, you used generator functions to make coroutines. + +But if a coroutine raises ``StopIteration`` right away, what's the point? Well, recall that the point of a coroutine (and asnyc in general), is to be able to return control to the system, while you wait for something else to happen. And thus the "await" keyword. So a coroutine isn't useful unless it uses ``await`` + +``await an_awaitable`` suspends the coroutine until something is done, then returns the "awaitable"'s result. + +hmm -- we have a trick here -- we need an "awaitable" object -- how do we get one of those? Well, a coroutine is awaitable, so let's make the simplest one of those: + + + + diff --git a/_sources/modules/DIP-unit-testing.rst.txt b/_sources/modules/DIP-unit-testing.rst.txt new file mode 100644 index 0000000..a63afd6 --- /dev/null +++ b/_sources/modules/DIP-unit-testing.rst.txt @@ -0,0 +1,1261 @@ +Unit testing - Dive Into Python 3 +  + +You are here: `Home `__ ‣ `Dive Into Python +3 `__ ‣ + + +Unit Testing +============ + + | ❝ Certitude is not the test of certainty. We have been cocksure of + many things that were not so. ❞ + | — `Oliver Wendell Holmes, + Jr. `__ + +  + +.. _divingin: + +(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 chapter, you’re going to write and debug a set of utility +functions to convert to and from Roman numerals. You saw the mechanics +of constructing and validating Roman numerals in `“Case study: roman +numerals” `__. Now step back and +consider what it would take to expand that into a two-way utility. + +`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 chapter, 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 +imports them. Python has a framework for unit testing, the +appropriately-named ``unittest`` module. + +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 in this chapter. +But unit tests are beneficial 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.) + +⁂ + +.. _romantest1: + +A Single Question +----------------- + +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... + +- ...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: + +#. The ``to_roman()`` function should return the Roman numeral + representation for all integers ``1`` to ``3999``. + +It is not immediately obvious how this code does… well, *anything*. It +defines a class which has no ``__init__()`` method. The class *does* +have another method, but it is never called. The entire script has a +``__main__`` block, but it doesn’t reference the class or its method. +But it does do something, I promise. + +[`download ``romantest1.py`` `__] + +.. code:: pp + + import roman1 + import unittest + + class KnownValues(unittest.TestCase): ① + 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(self): ③ + '''to_roman should give known result with known input''' + for integer, numeral in self.known_values: + result = roman1.to_roman(integer) ④ + self.assertEqual(numeral, result) ⑤ + + if __name__ == '__main__': + unittest.main() + +#. 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. +#. This is a tuple 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. +#. Every individual test is its own method. A test method takes no + parameters, returns no value, and must have a name beginning with the + four letters ``test``. If a test method exits normally without + raising an exception, the test is considered passed; if the method + raises an exception, the test is considered failed. +#. Here 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. 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. +#. 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. + +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. + +.. code:: pp + + # roman1.py + + def to_roman(n): + '''convert 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 test needs to fail + first.) To stub it out, use the Python reserved word ``pass``, which + does precisely nothing. + +Execute ``romantest1.py`` on the command line to run the test. If you +call it with the ``-v`` command-line option, it will give more verbose +output so you can see exactly what’s going on as each test case runs. +With any luck, your output should look like this: + +.. code:: screen + + you@localhost:~/diveintopython3/examples$ python3 romantest1.py -v + test_to_roman_known_values (__main__.KnownValues) ① + to_roman should give known result with known input ... FAIL ② + + ====================================================================== + FAIL: to_roman should give known result with known input + ---------------------------------------------------------------------- + Traceback (most recent call last): + File "romantest1.py", line 73, in test_to_roman_known_values + self.assertEqual(numeral, result) + AssertionError: 'I' != None ③ + + ---------------------------------------------------------------------- + Ran 1 test in 0.016s ④ + + FAILED (failures=1) ⑤ + +#. Running the script runs ``unittest.main()``, which runs each test + case. Each test case is a method within a class in ``romantest.py``. + There is no required organization of these test classes; they can + each contain a single test method, or you can have one class that + contains multiple test methods. The only requirement is that each + test class must inherit from ``unittest.TestCase``. +#. 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 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`` `__] + +.. code:: pp + + roman_numeral_map = (('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1)) ① + + def to_roman(n): + '''convert integer to Roman numeral''' + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: ② + result += numeral + n -= integer + return 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: + +.. code:: nd + + while n >= integer: + result += numeral + n -= integer + print('subtracting {0} from input, adding {1} to output'.format(integer, numeral)) + +With the debug ``print()`` statements, the output looks like this: + +.. code:: nd + + >>> import roman1 + >>> roman1.to_roman(1424) + subtracting 1000 from input, adding M to output + subtracting 400 from input, adding CD to output + subtracting 10 from input, adding X to output + subtracting 10 from input, adding X to output + subtracting 4 from input, adding IV to output + 'MCDXXIV' + +So the ``to_roman()`` function appears to work, at least in this manual +spot check. But will it pass the test case you wrote? + +.. code:: nd + + you@localhost:~/diveintopython3/examples$ python3 romantest1.py -v + test_to_roman_known_values (__main__.KnownValues) + to_roman should give known result with known input ... ok ① + + ---------------------------------------------------------------------- + Ran 1 test in 0.016s + + OK + +#. 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? + +⁂ + +.. _romantest2: + +“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. + +.. code:: screen + + >>> import roman1 + >>> roman1.to_roman(4000) + 'MMMM' + >>> roman1.to_roman(5000) + 'MMMMM' + >>> roman1.to_roman(9000) ① + 'MMMMMMMMM' + +#. That’s definitely not what you wanted — that’s not even a valid Roman + numeral! In fact, 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. The Pythonic 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 ``to_roman()`` function should raise an ``OutOfRangeError`` when + given an integer greater than ``3999``. + +What would that test look like? + +[`download ``romantest2.py`` `__] + +.. code:: pp + + import unittest, roman2 + class ToRomanBadInput(unittest.TestCase): ① + def test_too_large(self): ② + '''to_roman should fail with large input''' + self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000) ③ + +#. Like the previous test case, you create a class that inherits from + ``unittest.TestCase``. You can have more than one test per class (as + you’ll see later in this chapter), but I chose to create a new class + here because this test is something different than the last one. + We’ll keep all the good input tests together in one class, and all + the bad input tests together in another. +#. Like the previous test case, the test itself is a method of the + class, with a name starting with ``test``. +#. The ``unittest.TestCase`` class provides the ``assertRaises`` method, + which takes the following arguments: the exception you’re expecting, + the function you’re testing, and the arguments you’re passing to that + function. (If the function you’re testing takes more than one + argument, pass them all to ``assertRaises``, in order, and it will + pass them right along to the function you’re testing.) + +Pay close attention to this last line of code. Instead of calling +``to_roman()`` directly and manually checking that it raises a +particular exception (by wrapping it in `a ``try...except`` +block `__), the +``assertRaises`` method has encapsulated all of that for us. All you do +is tell it what exception you’re expecting (``roman2.OutOfRangeError``), +the function (``to_roman()``), and the function’s arguments (``4000``). +The ``assertRaises`` method takes care of calling ``to_roman()`` and +checking that it raises ``roman2.OutOfRangeError``. + +Also note that you’re passing the ``to_roman()`` function itself as an +argument; you’re not calling it, and you’re not passing the name of it +as a string. Have I mentioned recently how handy it is that `everything +in Python is an +object `__? + +So what happens when you run the test suite with this new test? + +.. code:: screen + + you@localhost:~/diveintopython3/examples$ python3 romantest2.py -v + test_to_roman_known_values (__main__.KnownValues) + to_roman should give known result with known input ... ok + test_too_large (__main__.ToRomanBadInput) + to_roman should fail with large input ... ERROR ① + + ====================================================================== + ERROR: to_roman should fail with large input + ---------------------------------------------------------------------- + Traceback (most recent call last): + File "romantest2.py", line 78, in test_too_large + self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000) + AttributeError: 'module' object has no attribute 'OutOfRangeError' ② + + ---------------------------------------------------------------------- + Ran 2 tests in 0.000s + + FAILED (errors=1) + +#. You should have expected this to fail (since you haven’t written any + code to pass it yet), but... it didn’t actually “fail,” it had an + “error” instead. This is a subtle but important distinction. A unit + test actually has *three* return values: pass, fail, and error. Pass, + of course, means that the test passed — the code did what you + expected. “Fail” is what the previous test case did (until you wrote + code to make it pass) — it executed the code but the result was not + what you expected. “Error” means that the code didn’t even execute + properly. +#. Why didn’t the code execute properly? The traceback tells all. The + module you’re testing doesn’t have an exception called + ``OutOfRangeError``. Remember, you passed this exception to the + ``assertRaises()`` method, because it’s the exception you want the + function to raise given an out-of-range input. But the exception + doesn’t exist, so the call to the ``assertRaises()`` method failed. + It never got a chance to test the ``to_roman()`` function; it didn’t + get that far. + +To solve this problem, you need to define the ``OutOfRangeError`` +exception in ``roman2.py``. + +.. code:: pp + + class OutOfRangeError(ValueError): ① + pass ② + +#. Exceptions are classes. An “out of range” error is a kind of value + error — the argument value is out of its acceptable range. So this + exception inherits from the built-in ``ValueError`` exception. This + is not strictly necessary (it could just inherit from the base + ``Exception`` class), but it feels right. +#. Exceptions don’t actually do anything, but you need at least one line + of code to make a class. Calling ``pass`` does precisely nothing, but + it’s a line of Python code, so that makes it a class. + +Now run the test suite again. + +.. code:: screen + + you@localhost:~/diveintopython3/examples$ python3 romantest2.py -v + test_to_roman_known_values (__main__.KnownValues) + to_roman should give known result with known input ... ok + test_too_large (__main__.ToRomanBadInput) + to_roman should fail with large input ... FAIL ① + + ====================================================================== + FAIL: to_roman should fail with large input + ---------------------------------------------------------------------- + Traceback (most recent call last): + File "romantest2.py", line 78, in test_too_large + self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000) + AssertionError: OutOfRangeError not raised by to_roman ② + + ---------------------------------------------------------------------- + Ran 2 tests in 0.016s + + FAILED (failures=1) + +#. The new test is still not passing, but it’s not returning an error + either. Instead, the test is failing. That’s progress! It means the + call to the ``assertRaises()`` method succeeded this time, and the + unit test framework actually tested the ``to_roman()`` function. +#. Of course, the ``to_roman()`` function isn’t raising the + ``OutOfRangeError`` exception you just defined, because you haven’t + told it to do that yet. That’s excellent news! It means this is a + valid test case — it fails before you write the code to make it pass. + +Now you can write the code to make this test pass. + +[`download ``roman2.py`` `__] + +.. code:: pp + + def to_roman(n): + '''convert integer to Roman numeral''' + if n > 3999: + raise OutOfRangeError('number out of range (must be less than 4000)') ① + + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + return result + +#. This is straightforward: if the given input (``n``) is greater than + ``3999``, raise an ``OutOfRangeError`` exception. The unit test does + not check the human-readable string that accompanies the exception, + although you could write another test that did check it (but watch + out for internationalization issues for strings that vary by the + user’s language or environment). + +Does this make the test pass? Let’s find out. + +.. code:: screen + + you@localhost:~/diveintopython3/examples$ python3 romantest2.py -v + test_to_roman_known_values (__main__.KnownValues) + to_roman should give known result with known input ... ok + test_too_large (__main__.ToRomanBadInput) + to_roman should fail with large input ... ok ① + + ---------------------------------------------------------------------- + Ran 2 tests in 0.000s + + OK + +#. Hooray! Both tests pass. Because you worked iteratively, bouncing + back and forth between testing and coding, you can be sure that the + two lines of code you just wrote were the cause of that one test + going from “fail” to “pass.” That kind of confidence doesn’t come + cheap, but it will pay for itself over the lifetime of your code. + +⁂ + +.. _romantest3: + +More Halting, More Fire +----------------------- + +Along with testing numbers that are too large, you need to test numbers +that are too small. As `we noted in our functional +requirements <#divingin>`__, Roman numerals cannot express 0 or negative +numbers. + +.. code:: nd + + >>> import roman2 + >>> roman2.to_roman(0) + '' + >>> roman2.to_roman(-1) + '' + +Well *that’s* not good. Let’s add tests for each of these conditions. + +[`download ``romantest3.py`` `__] + +.. code:: pp + + class ToRomanBadInput(unittest.TestCase): + def test_too_large(self): + '''to_roman should fail with large input''' + self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 4000) ① + + def test_zero(self): + '''to_roman should fail with 0 input''' + self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 0) ② + + def test_negative(self): + '''to_roman should fail with negative input''' + self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, -1) ③ + +#. The ``test_too_large()`` method has not changed since the previous + step. I’m including it here to show where the new code fits. +#. Here’s a new test: the ``test_zero()`` method. Like the + ``test_too_large()`` method, it tells the ``assertRaises()`` method + defined in ``unittest.TestCase`` to call our ``to_roman()`` function + with a parameter of 0, and check that it raises the appropriate + exception, ``OutOfRangeError``. +#. The ``test_negative()`` method is almost identical, except it passes + ``-1`` to the ``to_roman()`` function. If either of these new tests + does *not* raise an ``OutOfRangeError`` (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: + +.. code:: nd + + you@localhost:~/diveintopython3/examples$ python3 romantest3.py -v + test_to_roman_known_values (__main__.KnownValues) + to_roman should give known result with known input ... ok + test_negative (__main__.ToRomanBadInput) + to_roman should fail with negative input ... FAIL + test_too_large (__main__.ToRomanBadInput) + to_roman should fail with large input ... ok + test_zero (__main__.ToRomanBadInput) + to_roman should fail with 0 input ... FAIL + + ====================================================================== + FAIL: to_roman should fail with negative input + ---------------------------------------------------------------------- + Traceback (most recent call last): + File "romantest3.py", line 86, in test_negative + self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, -1) + AssertionError: OutOfRangeError not raised by to_roman + + ====================================================================== + FAIL: to_roman should fail with 0 input + ---------------------------------------------------------------------- + Traceback (most recent call last): + File "romantest3.py", line 82, in test_zero + self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 0) + AssertionError: OutOfRangeError not raised by to_roman + + ---------------------------------------------------------------------- + Ran 4 tests in 0.000s + + FAILED (failures=2) + +Excellent. Both tests failed, as expected. Now let’s switch over to the +code and see what we can do to make them pass. + +[`download ``roman3.py`` `__] + +.. code:: pp + + def to_roman(n): + '''convert integer to Roman numeral''' + if not (0 < n < 4000): ① + raise OutOfRangeError('number out of range (must be 1..3999)') ② + + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + return result + +#. This is a nice Pythonic shortcut: multiple comparisons at once. This + is equivalent to ``if not ((0 < n) and (n < 4000))``, but it’s much + easier to read. This 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. The ``unittest`` framework + 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. + +.. code:: nd + + you@localhost:~/diveintopython3/examples$ python3 romantest3.py -v + test_to_roman_known_values (__main__.KnownValues) + to_roman should give known result with known input ... ok + test_negative (__main__.ToRomanBadInput) + to_roman should fail with negative input ... ok + test_too_large (__main__.ToRomanBadInput) + to_roman should fail with large input ... ok + test_zero (__main__.ToRomanBadInput) + to_roman should fail with 0 input ... ok + + ---------------------------------------------------------------------- + Ran 4 tests in 0.016s + + OK + +⁂ + +.. _romantest4: + +And One More Thing… +------------------- + +There was one more `functional requirement <#divingin>`__ for converting +numbers to Roman numerals: dealing with non-integers. + +.. code:: screen + + >>> import roman3 + >>> roman3.to_roman(0.5) ① + '' + >>> roman3.to_roman(1.0) ② + 'I' + +#. Oh, that’s bad. +#. Oh, that’s even worse. Both of these cases should raise an exception. + Instead, they give bogus results. + +Testing for non-integers is not difficult. First, define a +``NotIntegerError`` exception. + +.. code:: nd + + # roman4.py + class OutOfRangeError(ValueError): pass + class NotIntegerError(ValueError): pass + +Next, write a test case that checks for the ``NotIntegerError`` +exception. + +.. code:: nd + + class ToRomanBadInput(unittest.TestCase): + . + . + . + def test_non_integer(self): + '''to_roman should fail with non-integer input''' + self.assertRaises(roman4.NotIntegerError, roman4.to_roman, 0.5) + +Now check that the test fails properly. + +.. code:: nd + + you@localhost:~/diveintopython3/examples$ python3 romantest4.py -v + test_to_roman_known_values (__main__.KnownValues) + to_roman should give known result with known input ... ok + test_negative (__main__.ToRomanBadInput) + to_roman should fail with negative input ... ok + test_non_integer (__main__.ToRomanBadInput) + to_roman should fail with non-integer input ... FAIL + test_too_large (__main__.ToRomanBadInput) + to_roman should fail with large input ... ok + test_zero (__main__.ToRomanBadInput) + to_roman should fail with 0 input ... ok + + ====================================================================== + FAIL: to_roman should fail with non-integer input + ---------------------------------------------------------------------- + Traceback (most recent call last): + File "romantest4.py", line 90, in test_non_integer + self.assertRaises(roman4.NotIntegerError, roman4.to_roman, 0.5) + AssertionError: NotIntegerError not raised by to_roman + + ---------------------------------------------------------------------- + Ran 5 tests in 0.000s + + FAILED (failures=1) + +Write the code that makes the test pass. + +.. code:: pp + + def to_roman(n): + '''convert integer to Roman numeral''' + if not (0 < n < 4000): + raise OutOfRangeError('number out of range (must be 1..3999)') + if not isinstance(n, int): ① + raise NotIntegerError('non-integers can not be converted') ② + + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + return result + +#. The built-in ``isinstance()`` function tests whether a variable is a + particular type (or, technically, any descendant type). +#. If the argument ``n`` is not an ``int``, raise our newly minted + ``NotIntegerError`` exception. + +Finally, check that the code does indeed make the test pass. + +.. code:: nd + + you@localhost:~/diveintopython3/examples$ python3 romantest4.py -v + test_to_roman_known_values (__main__.KnownValues) + to_roman should give known result with known input ... ok + test_negative (__main__.ToRomanBadInput) + to_roman should fail with negative input ... ok + test_non_integer (__main__.ToRomanBadInput) + to_roman should fail with non-integer input ... ok + test_too_large (__main__.ToRomanBadInput) + to_roman should fail with large input ... ok + test_zero (__main__.ToRomanBadInput) + to_roman should fail with 0 input ... ok + + ---------------------------------------------------------------------- + Ran 5 tests in 0.000s + + OK + +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()``. + +⁂ + +.. _romantest5: + +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 already constructed `a regular expression to check for +Roman numerals `__, so that part +is done. + +That leaves 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 <#romantest1>`__; let’s reuse that. + +.. code:: nd + + def test_from_roman_known_values(self): + '''from_roman should give known result with known input''' + for integer, numeral in self.known_values: + 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. + +.. code:: nd + + 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. + +.. code:: nd + + class RoundtripCheck(unittest.TestCase): + def test_roundtrip(self): + '''from_roman(to_roman(n))==n for all n''' + for integer in range(1, 4000): + numeral = roman5.to_roman(integer) + result = roman5.from_roman(numeral) + self.assertEqual(integer, result) + +These new tests won’t even fail yet. We haven’t defined a +``from_roman()`` function at all, so they’ll just raise errors. + +.. code:: nd + + you@localhost:~/diveintopython3/examples$ python3 romantest5.py + E.E.... + ====================================================================== + ERROR: test_from_roman_known_values (__main__.KnownValues) + from_roman should give known result with known input + ---------------------------------------------------------------------- + Traceback (most recent call last): + File "romantest5.py", line 78, in test_from_roman_known_values + result = roman5.from_roman(numeral) + AttributeError: 'module' object has no attribute 'from_roman' + + ====================================================================== + ERROR: test_roundtrip (__main__.RoundtripCheck) + from_roman(to_roman(n))==n for all n + ---------------------------------------------------------------------- + Traceback (most recent call last): + File "romantest5.py", line 103, in test_roundtrip + result = roman5.from_roman(numeral) + AttributeError: 'module' object has no attribute 'from_roman' + + ---------------------------------------------------------------------- + Ran 7 tests in 0.019s + + FAILED (errors=2) + +A quick stub function will solve that problem. + +.. code:: nd + + # roman5.py + 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. + +.. code:: nd + + you@localhost:~/diveintopython3/examples$ python3 romantest5.py + F.F.... + ====================================================================== + FAIL: test_from_roman_known_values (__main__.KnownValues) + from_roman should give known result with known input + ---------------------------------------------------------------------- + Traceback (most recent call last): + File "romantest5.py", line 79, in test_from_roman_known_values + self.assertEqual(integer, result) + AssertionError: 1 != None + + ====================================================================== + FAIL: test_roundtrip (__main__.RoundtripCheck) + from_roman(to_roman(n))==n for all n + ---------------------------------------------------------------------- + Traceback (most recent call last): + File "romantest5.py", line 104, in test_roundtrip + self.assertEqual(integer, result) + AssertionError: 1 != None + + ---------------------------------------------------------------------- + Ran 7 tests in 0.002s + + FAILED (failures=2) + +Now it’s time to write the ``from_roman()`` function. + +.. code:: pp + + def from_roman(s): + """convert Roman numeral to integer""" + result = 0 + index = 0 + for numeral, integer in roman_numeral_map: + while s[index:index+len(numeral)] == numeral: ① + result += integer + index += len(numeral) + return result + +#. The pattern here is the same as the ```to_roman()`` <#romantest1>`__ + 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`` +statement to the end of the ``while`` loop: + +:: + + def from_roman(s): + """convert Roman numeral to integer""" + result = 0 + index = 0 + for numeral, integer in roman_numeral_map: + while s[index:index+len(numeral)] == numeral: + result += integer + index += len(numeral) + print('found', numeral, 'of length', len(numeral), ', adding', integer) + +.. code:: nd + + >>> import roman5 + >>> roman5.from_roman('MCMLXXII') + found M of length 1, adding 1000 + found CM of length 2, adding 900 + found L of length 1, adding 50 + found X of length 1, adding 10 + found X of length 1, adding 10 + found I of length 1, adding 1 + found I of length 1, adding 1 + 1972 + +Time to re-run the tests. + +.. code:: nd + + you@localhost:~/diveintopython3/examples$ python3 romantest5.py + ....... + ---------------------------------------------------------------------- + Ran 7 tests in 0.060s + + OK + +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 <#romantest1>`__. 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.) + +⁂ + +.. _romantest6: + +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 <#romantest3>`__ in the +``to_roman()`` function, but you have a powerful tool at your disposal: +regular expressions. (If you’re not familiar with regular expressions, +now would be a good time to read `the regular expressions +chapter `__.) + +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: + +- 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``\ ”). + +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. + +.. code:: nd + + class FromRomanBadInput(unittest.TestCase): + def test_too_many_repeated_numerals(self): + '''from_roman should fail with too many repeated numerals''' + for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'): + self.assertRaises(roman6.InvalidRomanNumeralError, roman6.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. + +.. code:: nd + + def test_repeated_pairs(self): + '''from_roman should fail with repeated pairs of numerals''' + 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``. This test includes a randomly chosen set of invalid +antecedents: ``I`` before ``M``, ``V`` before ``X``, and so on. + +.. code:: nd + + def test_malformed_antecedents(self): + '''from_roman should fail with malformed antecedents''' + for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV', + 'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'): + self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s) + +Each of these tests relies the ``from_roman()`` function raising a new +exception, ``InvalidRomanNumeralError``, which we haven’t defined yet. + +.. code:: nd + + # roman6.py + class InvalidRomanNumeralError(ValueError): pass + +All three 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:: nd + + you@localhost:~/diveintopython3/examples$ python3 romantest6.py + FFF....... + ====================================================================== + FAIL: test_malformed_antecedents (__main__.FromRomanBadInput) + from_roman should fail with malformed antecedents + ---------------------------------------------------------------------- + Traceback (most recent call last): + File "romantest6.py", line 113, in test_malformed_antecedents + self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s) + AssertionError: InvalidRomanNumeralError not raised by from_roman + + ====================================================================== + FAIL: test_repeated_pairs (__main__.FromRomanBadInput) + from_roman should fail with repeated pairs of numerals + ---------------------------------------------------------------------- + Traceback (most recent call last): + File "romantest6.py", line 107, in test_repeated_pairs + self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s) + AssertionError: InvalidRomanNumeralError not raised by from_roman + + ====================================================================== + FAIL: test_too_many_repeated_numerals (__main__.FromRomanBadInput) + from_roman should fail with too many repeated numerals + ---------------------------------------------------------------------- + Traceback (most recent call last): + File "romantest6.py", line 102, in test_too_many_repeated_numerals + self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s) + AssertionError: InvalidRomanNumeralError not raised by from_roman + + ---------------------------------------------------------------------- + Ran 10 tests in 0.058s + + FAILED (failures=3) + +Good deal. Now, all we need to do is add the `regular expression to test +for valid Roman numerals `__ +into the ``from_roman()`` function. + +.. code:: nd + + roman_numeral_pattern = re.compile(''' + ^ # beginning of string + M{0,3} # thousands - 0 to 3 Ms + (CM|CD|D?C{0,3}) # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 Cs), + # or 500-800 (D, followed by 0 to 3 Cs) + (XC|XL|L?X{0,3}) # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 Xs), + # or 50-80 (L, followed by 0 to 3 Xs) + (IX|IV|V?I{0,3}) # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 Is), + # or 5-8 (V, followed by 0 to 3 Is) + $ # end of string + ''', re.VERBOSE) + + def from_roman(s): + '''convert Roman numeral to integer''' + if not roman_numeral_pattern.search(s): + raise InvalidRomanNumeralError('Invalid Roman numeral: {0}'.format(s)) + + result = 0 + index = 0 + for numeral, integer in roman_numeral_map: + while s[index : index + len(numeral)] == numeral: + result += integer + index += len(numeral) + return result + +And re-run the tests… + +.. code:: nd + + you@localhost:~/diveintopython3/examples$ python3 romantest7.py + .......... + ---------------------------------------------------------------------- + Ran 10 tests in 0.066s + + OK + +And the anticlimax award of the year goes to… the word “\ ``OK``\ ”, +which is printed by the ``unittest`` module when all the tests pass. + +`☜ `__ `☞ `__ + +© 2001–11 `Mark Pilgrim `__ diff --git a/_sources/modules/Debugging.rst.txt b/_sources/modules/Debugging.rst.txt new file mode 100644 index 0000000..47847ff --- /dev/null +++ b/_sources/modules/Debugging.rst.txt @@ -0,0 +1,597 @@ +:orphan: + +.. _debugging: + +######### +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 + +http://www.pythontutor.com/visualize.html#mode=edit + + +Visualize the stack! +-------------------- + +.. image:: /_static/program_callstack.png + :height: 580 px + + +.. rubric:: How deep can that stack be? + +:: + + i = 0 + + def recurse(): + global i + i += 1 + print(i) + recurse() + + recurse() + + +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 + + +.. code-block:: ipython + + import inspect + + def recurse(limit): + local_variable = '.' * limit + print(limit, inspect.getargvalues(inspect.currentframe())) + if limit <= 0: + return + recurse(limit - 1) + return + + 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) + +When either the interpreter or your own code detects an error condition, +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 + + +Handling exceptions +------------------- + +The most basic form uses the builtins try and except + +:: + + def temp_f_to_c(var): + try: + 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 +-------------------------------------------------------------------- + +.. code-block:: python + + x = 5 + y = "this" + + try: + 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 + errors = e.args + print("Error({0})".format(errors)) + # or you can just print e + print("unhandled, unexpected exception:\n", e) + raise + else: + print("do this if there is code you want to run only if no exceptions, caught or not") + 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') + + +It is even possible to use a try block without the exception clause: + +:: + + try: + 5/0 + finally: + print('did it work? why would you do this?') + + +.. rubric:: Built-in exceptions + :name: built-in-exceptions + +:: + + [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: + +:: + + raise SyntaxError("That was a mispelling") + +If no builtin exceptions work, define a new exception type by subclassing Exception. + +:: + + class MyException(Exception): + pass + + raise MyException("An exception doesn't always prove the rule!") + +It is possible, but discouraged to catch all exceptions. + +:: + + try: + my_cool_code() + except: + 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. + + +.. rubric:: Further reading + :name: further-reading + +- 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 + + +Tools +..... + +- interpreter hints +- print() +- logging +- assert() +- tests +- debuggers + + +The Stack Trace +............... + +You already know what it looks like. Simple traceback: + +:: + + maria$ python3 define.py python + Traceback (most recent call last): + File "define.py", line 15, in + definition = Definitions.article(title) + File "/Users/maria/python/300/Py300/Examples/debugging/wikidef/definitions.py", line 7, in article + return Wikipedia.article(title) + File "/Users/maria/python/300/Py300/Examples/debugging/wikidef/api.py", line 26, in article + contents = json_response['parse']['text']['*'] + TypeError: 'method' object is not subscriptable + +But things can quickly get complicated. You may have already run into stacktraces that go on for a 50 lines or more. + + +Some helpful hints with stacktraces: +.................................... + +- 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) + + +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 + +If all else fails... + +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. + + +Print +..... + +- print("my_module.py: my_variable: ", my_variable) +- 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) +...................................... + +- Winpdb +- IDEs: Eclipse, Wing IDE, PyCharm, Visual Studio Code + +.. rubric:: help from the interpreter + :name: help-from-the-interpreter + +1. investigate import issues with -v: + +:: + + 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: +.............................. + +- In IPython, 'who' will list all currently defined variables +- locals() +- globals() +- dir() + +.. rubric:: `Pdb - The Python + Debugger `__ + :name: pdb---the-python-debugger + +.. rubric:: Pros: + +- 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 + +.. rubric:: Cons: + +- Steep-ish learning curve +- Easy to get lost in a deep stack +- Watching variables isn't hard, but non-trivial + +.. rubric:: `Pdb - The Python Debugger `_ + +The 4-fold ways of invoking pdb +............................... + +- Postmortem mode +- Run mode +- Script mode +- Trace 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. + +.. rubric:: Postmortem mode + :name: postmortem-mode + +For analyzing crashes due to uncaught exceptions + +:: + + python -i script.py + import pdb; pdb.pm() + +More info on using Postmortem mode: + +http://www.almarklein.org/pm-debugging.html + +.. rubric:: Run mode + :name: run-mode + +:: + + pdb.run('some.expression()') + +.. rubric:: Script mode + :name: script-mode + +:: + + python -m pdb script.py + + +"-m [module]" finds [module] in sys.path and executes it as a script + + +.. rubric:: Trace mode + :name: trace-mode + +Insert the following line into your code where you want execution to +halt: + +:: + + import pdb; pdb.set_trace() + + +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 + +.. code-block:: ipython + + In [2]: pdb + Automatic pdb calling has been turned ON + + %run app.py + + # 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'). + +.. rubric:: Navigating pdb + :name: 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. + +:: + + % python -m pdb define.py + pdb> args # print arguments and values to current function + pdb> pp a_variable # pretty-print a_variable + 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 + +:: + + # execute until current function returns + pdb> return + # Execute the current line, stop at the first possible occasion + pdb> step + # Continue execution until the next line in the current function is reached or it returns. + pdb> next + # Continue execution until the line with a number greater than the current one is reached + or until the current frame returns. Good for exiting loops. + pdb> until + # move one level up the stack + pdb> up + # move one level down the stack + pdb> down + pdb> continue # goes until next breakpoint or end of program + # advanced: create commands to be executed on a breakpoint + pdb> commands + + +.. rubric:: Breakpoints + :name: breakpoints + +:: + + pdb> help break + b(reak) ([file:]lineno | function) [, condition] + With a line number argument, set a break there in the current + file. With a function name, set a break at first executable line + of that function. Without argument, list all breaks. If a second + argument is present, it is a string specifying an expression + which must evaluate to true before the breakpoint is honored. + + The line number may be prefixed with a filename and a colon, + to specify a breakpoint in another file (probably one that + 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. + +:: + + pdb> break api.py:21 # set a breakpoint file:line # + pdb> break # list breakpoints + pdb> clear 1 # get rid of first breakpoint + pdb> break 35 # set a breakpoint in current file at line 35 + # print lines in range + pdb> list 1,28 + + +You can also delete(clear), disable and enable breakpoints + + +:: + + clear [bpnumber [bpnumber...]] + + disable [bpnumber [bpnumber...]] + + enable [bpnumber [bpnumber...]] + + +.. rubric:: Conditional Breakpoints + :name: conditional-breakpoints + +:: + + pdb> break 9, j>3 + Breakpoint 1 at .../pdb_break.py:9 + + pdb> break + Num Type Disp Enb Where + 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 + + +pytest allows one to drop into the PDB prompt via a command line option:: + + 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:: + + 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 + + +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 + +Easier to start up and get debugging:: + + winpdb your_app.py + +http://winpdb.org/tutorial/WinpdbTutorial.html + + +Remote debugging +---------------- + +To debug an application running a different Python, even remotely: + +remote-pdb + +https://pypi.python.org/pypi/remote-pdb + + diff --git a/_sources/modules/Decorators.rst.txt b/_sources/modules/Decorators.rst.txt new file mode 100644 index 0000000..105cc64 --- /dev/null +++ b/_sources/modules/Decorators.rst.txt @@ -0,0 +1,567 @@ +.. _decorators: + +########## +Decorators +########## + +.. NOTE: bring over some of the good stuff from the Py300 version + +**A Short Reminder** + + +Functions are things that generate values based on input (arguments). + +In Python, functions are first-class objects. + +This means that you can bind names to them, pass them around, etc., just like +other objects. + +Because of this fact, you can write functions that take functions as +arguments and/or return functions as values: + +.. code-block:: python + + def substitute(a_function): + def new_function(*args, **kwargs): + return "I'm not that other function" + return new_function + + +A Definition +------------ + +There are many things you can do with a simple pattern like this one. +So many, that we give it a special name: + +**Decorator** + + "A decorator is a function that takes a function as an argument and + returns a function as a return value." + + That's nice and all, but why is that useful? + +An Example +---------- + +Imagine you are trying to debug a module with a number of functions like this one: + +.. code-block:: python + + def add(a, b): + return a + b + +You want to see when each function is called, with what arguments and +with what result. So you rewrite each function as follows: + +.. code-block:: python + + def add(a, b): + print("Function 'add' called with args: {}, {}".format(a, b) ) + result = a + b + print("\tResult --> {}".format(result)) + return result + + +That's not particularly nice, especially if you have lots of functions +in your module. + +Now imagine we defined the following, more generic *decorator*: + +.. code-block:: python + + def logged_func(func): + def logged(*args, **kwargs): + print("Function {} called".format(func.__name__)) + if args: + print("\twith args: {}".format(args)) + if kwargs: + print("\twith kwargs: {}".format(kwargs)) + result = func(*args, **kwargs) + print("\t Result --> {}".format(result)) + return result + return logged + + +We could then make logging versions of our module functions: + +.. code-block:: python + + logging_add = logged_func(add) + +Then, where we want to see the results, we can use the logged version: + +.. code-block:: ipython + + In [37]: logging_add(3, 4) + Function 'add' called + with args: (3, 4) + Result --> 7 + Out[37]: 7 + + +This is nice, but we have to call the new function wherever we originally +had the old one. + +It'd be nicer if we could just call the old function and have it log. + +Remembering that you can easily rebind symbols in Python using *assignment +statements* leads you to this form: + +.. code-block:: python + + def logged_func(func): + # implemented above + + def add(a, b): + return a + b + add = logged_func(add) + +And now you can simply use the code you've already written and calls to +``add`` will be logged: + +.. code-block:: ipython + + In [41]: add(3, 4) + Function 'add' called + with args: (3, 4) + Result --> 7 + Out[41]: 7 + +Syntax +------ + +Rebinding the name of a function to the result of calling a decorator on that +function is called **decoration**. + +Because this is so common, Python provides a special operator to perform it +more *declaratively*: the ``@`` operator -- I told you I'd eventually explain what was going on under the hood with +that weird `@` symbol. + +This is rebinding the name: + +.. code-block:: python + + def add(a, b): + return a + b + add = logged_func(add) + +And this means exactly the same thing, with the decoration syntax: + +.. code-block:: python + + @logged_func + def add(a, b): + return a + b + + +The declarative form (called a decorator expression) is far more common, +but both have the identical result, and can be used interchangeably. + +Here's another simple example. First we define a decorator -- note that it is a function that takes an argument, and returns a function: + +.. code-block:: python + + In [1]: def my_decorator(func): + ...: def inner(): + ...: print('running inner') + ...: return inner + ...: + +And we can apply it with the regular calling and rebinding syntax: + +.. code-block:: ipython + + In [2]: def other_func(): + ...: print('running other_func') + + In [3]: other_func() + running other_func + + In [4]: other_func = my_decorator(other_func) + + In [5]: other_func() + In [5]: running inner + + In [6]: other_func + Out[6]: .inner> + +Notice that ``other_func`` is now the "inner" function, which lives in the "my_decorator" namespace... + +And this is the same with the decoration syntax: + +.. code-block:: python + + In [7]: @my_decorator + ...: def other_func(): + ...: print('running other_func') + ...: + + In [8]: other_func() + running inner + + In [9]: other_func + Out[9]: .inner> + +Notice that ``other_func`` is the "inner" function here as well. + +Decorators have the power to replace the decorated function with a different one! + +And they do it with compact, declarative syntax that has the decoration right at the top where the function is defined. + + +Callables +--------- + +Our original definition of a *decorator* was nice and simple, but a tiny bit incomplete. + +In reality, decorators can be used with anything that is *callable*. + +Remember that a *callable* is a function, a class object, a method in a class, or a instance of a class that implements the ``__call__`` special method. + +So in fact the definition should be updated as follows: + + "A decorator is a callable that takes a callable as an argument and returns a callable as a return value." + + +An Example +---------- + +Consider a decorator that would save the results of calling an expensive +function with given arguments so that it would not have to be re-computed with the same input (which is known an memoizing...). + +.. code-block:: python + + class Memoize: + """ + memoize decorator from avinash.vora + http://avinashv.net/2008/04/python-decorators-syntactic-sugar/ + """ + def __init__(self, function): # runs when memoize class is called + self.function = function + self.memoized = {} + + def __call__(self, *args): # runs when memoize instance is called + try: + return self.memoized[args] + except KeyError: + self.memoized[args] = self.function(*args) + return self.memoized[args] + + +Let's try that out with a potentially expensive function: + +.. code-block:: ipython + + In [56]: @Memoize + ....: def sum2x(n): + ....: return sum(2 * i for i in range(n)) + ....: + + In [57]: sum2x(10000000) + Out[57]: 99999990000000 + + In [58]: sum2x(10000000) + Out[58]: 99999990000000 + +Run that code yourself and see how much faster it returns the second time. + +It's nice to see that in action, but what if we want to know *exactly* +how much difference it made? + + +Nested Decorators +----------------- + +You can stack decorator expressions. The result is like calling each +decorator in order, from bottom to top: + +.. code-block:: python + + @decorator_two + @decorator_one + def func(x): + pass + + # is exactly equal to: + def func(x): + pass + func = decorator_two(decorator_one(func)) + + +Let's define another decorator that will time how long a given call takes: + +.. code-block:: python + + import time + def timed_func(func): + def timed(*args, **kwargs): + start = time.time() + result = func(*args, **kwargs) + elapsed = time.time() - start + print("time expired: {}".format(elapsed)) + return result + return timed + + +And now we can use this new decorator stacked along with our memoizing +decorator: + +.. code-block:: ipython + + In [71]: @timed_func + ....: @Memoize + ....: def sum2x(n): + ....: return sum(2 * i for i in range(n)) + In [72]: sum2x(10000000) + time expired: 0.997071027756 + Out[72]: 99999990000000 + In [73]: sum2x(10000000) + time expired: 4.05311584473e-06 + Out[73]: 99999990000000 + + +Parameterized Decorators +------------------------ + +The purpose of the outer function in the decorator is to receive the function to be decorated, adding anything to scope that should be there before the decorated function is called. + +The inner function runs the function being decorated, so its inputs are the same as the function being decorated. + +How do we add more input parameters to our decorator? Like this example from Django: + +.. code-block:: python + + @register.filter(name='cut') + def cut(value, arg): + return value.replace(arg, '') + + +Add yet another function in scope: + +.. code-block:: python + + def decorator(arg1, arg2): + def real_decorator(function): + def wrapper(*args, **kwargs): + print("Congratulations. You decorated a function that does + something with {} and {}".format(arg1, arg2)) + function(*args, **kwargs) + return wrapper + return real_decorator + + + @decorator("arg1", "arg2") + def print_args(*args): + for arg in args: + print(arg) + + +Last example from: http://scottlobdell.me/2015/04/decorators-arguments-python/ + + +Examples from the Standard Library +---------------------------------- + +It's going to be a lot more common for you to use pre-defined decorators than for you to be writing your own. + +We've seen a few already: + +For example, ``@staticmethod`` and ``@classmethod`` can also be used as simple +callables, without the nifty decorator expression: + +.. code-block:: python + + class C: + @staticmethod + def add(a, b): + return a + b + +Is exactly the same as: + +.. code-block:: python + + class C: + def add(a, b): + return a + b + add = staticmethod(add) + +Note that the "``def``" binds the name ``add``, then the next line +rebinds it. + +[Note that this is exactly how you defined a ``staticmethod`` before the decoration syntax was added in python 2.4] + +The ``classmethod()`` builtin can do the same thing: + +.. code-block:: python + + # in declarative style + class C: + @classmethod + def from_iterable(cls, seq): + # method body + + # in imperative style: + class C: + def from_iterable(cls, seq): + # method body + from_iterable = classmethod(from_iterable) + + +property() +----------- + +Remember the ``property()`` builtin? + +Perhaps most commonly, you'll see the ``property()`` builtin used this way. + +Previously, we saw this code: + +.. code-block:: python + + class C: + def __init__(self): + self._x = None + @property + def x(self): + return self._x + @x.setter + def x(self, value): + self._x = value + @x.deleter + def x(self): + del self._x + + +But this could also be accomplished like so: + +.. code-block:: python + + class C: + def __init__(self): + self._x = None + def getx(self): + return self._x + def setx(self, value): + self._x = value + def delx(self): + del self._x + x = property(getx, setx, delx, + "I'm the 'x' property.") + + +:download:`property_ugly.py <../examples/decorators/property_ugly.py>` + + +Note that in this case, the decorator object returned by the property decorator +itself implements additional decorators as attributes on the returned method +object. So you could actually do this: + + +.. code-block:: python + + class C: + def __init__(self): + self._x = None + def x(self): + return self._x + x = property(x) + def _set_x(self, value): + self._x = value + x = x.setter(_set_x) + def _del_x(self): + del self._x + x = x.deleter(_del_x) + +But that's getting really ugly! Makes you appreciate the ``@``, doesn't it? + + +Import Time vs. Run Time +------------------------ + +Decorators are run at import time. Run this code and see what happens when: + +:download:`play_with_imports.py <../examples/decorators/play_with_imports.py>` + + +What if my decorated function uses unknown inputs? +-------------------------------------------------- + +If you don't know what parameters the decorated function will take (and you usually don't), you want to make sure the inner function that you are replacing the decorated function with takes ANY arguments, and passes them on to the decorated function. + +``*args, **kwargs`` is your friend here: + +A decorator that wraps an html `

      ` tag around the output of any decorated function. + +.. code-block:: python + + def p_decorate(func): + def func_wrapper(*args, **kwargs): + return "

      {0}

      ".format(func(*args, **kwargs)) + return func_wrapper + + + @p_decorate + def get_fullname(first_name, last_name): + return f"{first_name} {last_name}" + + In [124]: get_fullname('Chris', 'Barker') + Out[124]: '

      Chris Barker

      ' + + +Functools Library +----------------- + +Single dispatch: + - create many functions that do the same sort of thing, but based on type + - decorator determines type, and decides which function is run + +https://docs.python.org/3/library/functools.html#functools.singledispatch + +Memoize decorator we created earlier is in Functools: + +https://docs.python.org/3/library/functools.html#functools.lru_cache + +LAB +=== + +A little excercise. See the "p_decorate" decorator defined above -- it wrapped an html

      tag (paragraph) around the results of any function that returned a string. + +Can you make a version that will wrap any other tag -- specified as a parameter of the decorator itself? For example: + +.. code-block:: ipython + + @add_tag('p') + def get_fullname(first_name, last_name): + return f"{first_name} {last_name}" + + In [124]: get_fullname('Chris', 'Barker') + Out[124]: '

      Chris Barker

      ' + +Just like the ``p_decorate`` one above. + +But: + +.. code-block:: ipython + + @add_tag('div') + def get_fullname(first_name, last_name): + return f"{first_name} {last_name}" + + In [124]: get_fullname('Chris', 'Barker') + Out[124]: '
      Chris Barker
      ' + +and you could pass any tag in. + +This can be accomplished either with a closure --nesting another level of functions in the decorator, or with a callable class, like the memoize example. Maybe try both, and decide which you like better. + + +Further Reading: +---------------- + +*Fluent Python* by Luciano Ramalho, Chapter 7. + +Another good overview: + +https://dbader.org/blog/python-decorators + + diff --git a/_sources/modules/DictionaryAsSwitch.rst.txt b/_sources/modules/DictionaryAsSwitch.rst.txt new file mode 100644 index 0000000..822a6af --- /dev/null +++ b/_sources/modules/DictionaryAsSwitch.rst.txt @@ -0,0 +1,137 @@ +.. _dict_as_switch: + +################################ +Using a Dictionary to ``switch`` +################################ + +Python does not have a ``switch/case statement``. Why not? + +https://www.python.org/dev/peps/pep-3103/ + +So what to use instead of "switch-case"? + +``switch`` / ``case`` +===================== + +What is ``switch``? +------------------- + +Many languages have a "switch-case" construct:: + + switch(argument) { + case 0: + return "zero"; + case 1: + return "one"; + case 2: + return "two"; + default: + return "nothing"; + }; + +.. Fixme -- use a Ruby example? + +How do you say this in Python? + +``if-elif`` chains +------------------ + +The obvious way to say it is a chain of ``elif`` statements: + +.. code-block:: python + + if argument == 0: + return "zero" + elif argument == 1: + return "one" + elif argument == 2: + return "two" + else: + return "nothing" + +And there is nothing wrong with that, but.... + +dict as switch +-------------- + +The ``elif`` chain is neither elegant nor efficient. There are a number of ways to say it in python -- but one elegant one is to use a dict: + +.. code-block:: python + + arg_dict = {0:"zero", 1:"one", 2: "two"} + arg_dict.get(argument, "nothing") + +Simple, elegant and fast. + +You can do a dispatch table by putting functions as the value. + +Example: The mailroom2 solution. + +Switch with functions +--------------------- + +What would this be like if you used functions instead? Think of the possibilities. + +.. code-block:: ipython + + In [11]: def my_zero_func(): + return "I'm zero" + + In [12]: def my_one_func(): + return "I'm one" + + In [13]: switch_func_dict = { + 0: my_zero_func, + 1: my_one_func, + } + + In [14]: switch_func_dict.get(0)() + Out[14]: "I'm zero" + +Again, fast and efficient. + +This is possible because functions are "first class objects" in Python. + +OO switch/case +-------------- + +Another way to do the equivalent of switch / case is subclassing. + +If you haven't learned about classes in Python this will be pretty confusing. But here's a high level overview: + +In C, before C++, a common idiom was something like:: + + switch(object_type) { + case circle: + draw_a_circle(); + case square: + draw_a_square(); + case polygon: + draw_a_polygon(); + default: + draw_nothing(); + }; + +That is, a different function is called depending on what type of "thing" you are dealing with. + +This is actually a really common idiom in C. And even in modern OO code written by old C developers -- I had a developer on my team do exactly this in a program we were working on. It was a map drawing program (written in Python), and there was code all over it like:: + + if layer.type == "tiles": + do_something_with_tiles + elif layer.type == "grid": + do_somethign_with_grid + +This was a maintainability nightmare -- if you added a new layer type, you had to find every one of these constructs and add another ``elif`` block to it. + +The OO way +---------- + +With object oriented programming, you can "subclass" objects, and use "polymorphism" to achieve this kind of selection. Say you have a bunch of objects you want to be able to draw. Give each of them a ``draw()`` method, and then the above switch statement becomes: + +the_object.draw() + +That's IT! + +You don't have to test to see which type of object it is, you only have to know that it knows how to draw itself. + +Now when you add a new object type -- all you need to do is make sure it has a draw() method (and other needed methods) and then all the other code will know how to use it without your changing anything. diff --git a/_sources/modules/DictsAndSets.rst.txt b/_sources/modules/DictsAndSets.rst.txt new file mode 100644 index 0000000..388de53 --- /dev/null +++ b/_sources/modules/DictsAndSets.rst.txt @@ -0,0 +1,707 @@ +.. _dicts_and_sets: + +##################### +Dictionaries and Sets +##################### + +Dictionary +========== + +Python calls it a ``dict`` + +Other languages call it: + + * dictionary + * associative array + * map + * hash table + * hash + * key-value pair + +It is also known in Python as a "mapping", as it "maps" keys to values. + +It is like an array, in that it holds a number of items, and you can index into it to get at particular items. But it can use arbitrary indexes, rather than a sequence of numbers. + +These indexes are called "keys", and the items stored are called "values". + +So for any Python sequence, you might do:: + + item = stuff[3] + +for a dict, you do:: + + item = stuff["third"] + +.. note:: It is *very* common to use strings as keys in a dictionary. So common that virtually all examples you'll see both here and on the Internet use strings as keys. And many other languages have similar objects that only allow strings (like JavaScript objects, for instance). Python dicts, on the other hand, can use many types as keys, and this can be a very powerful feature to keep in mind. + + +Dictionary Constructors +----------------------- + +So how does one make a dict? There are a few ways... + +The dict "literal": + +.. code-block:: python + + # This dict "lies" + >>> {'key1': 3, 'key2': 5} + {'key1': 3, 'key2': 5} + +Calling the dict type object constructor: + +.. code-block:: python + + # passing in a sequence of (key,value) pairs: + >>> dict([('key1', 3), ('key2', 5)]) + {'key1': 3, 'key2': 5} + + # passing keys and values as keyword arguments: + >>> dict(key1=3, key2= 5) + {'key1': 3, 'key2': 5} + + # creating an empty dict, and then populating it: + >>> d = {} + >>> d['key1'] = 3 + >>> d['key2'] = 5 + >>> d + {'key1': 3, 'key2': 5} + + +Dictionary Indexing +------------------- + +And how do you get stuff out (index it)? + +The same way you index a sequence, except the index (now called a key) can be various types, rather than just an integer: + +.. code-block:: python + + >>> d = {'name': 'Brian', 'score': 42} + + >>> d['score'] + 42 + + >>> d = {1: 'one', 0: 'zero'} + + >>> d[0] + 'zero' + +What if the key doesn't exist in the dict? You get a ``KeyError`` exception: + +.. code-block:: python + + >>> d['non-existing key'] + Traceback (most recent call last): + File "", line 1, in + KeyError: 'non-existing key' + + + +What can keys be? +----------------- + +Surely not ANYTHING? + +Not quite: keys can be any "immutable": + + * number + * string + * tuple + +.. code-block:: ipython + + In [325]: d[3] = 'string' + In [326]: d[3.14] = 'pi' + In [327]: d['pi'] = 3.14 + In [328]: d[ (1,2,3) ] = 'a tuple key' + +What if you try to use a mutable type? + +.. code-block:: ipython + + In [329]: d[ [1,2,3] ] = 'a list key' + TypeError: unhashable type: 'list' + +Actually -- any "hashable" type. + +So, technically, it's not mutability, but hashability that matters. + +Although for most intents and purposes, you want to use immutable types as keys in dicts. + +Hashing +------- + +What does "hashable" mean? (https://computersciencewiki.org/index.php/Hashing) + +Hash functions convert arbitrarily large data to a small proxy (usually an integer). + +They always return the same proxy for the same input. + +MD5, SHA, etc, are some well known hash algorithms. + +Dictionaries hash the key to an integer proxy and use it to find the key and value. + +Key lookup is efficient because the hash function leads directly to a bucket with very few keys (often just one). + +What would happen if the proxy (hash) changed after storing a key? + +(Answer: you wouldn't be able to find it again!) + +Hashability requires immutability. + +Key lookup is very efficient: + +The access time is constant regardless of the size of the dict. + +Dictionary indexing +------------------- + +Note: cPython name look-ups are implemented with dicts -- it's highly optimized. + +Key to value: + + * lookup is one way. + +Value to key: + + * requires visiting the whole dict. + +If you need to check dict values often, create another dict or set. + +(But note that it's then up to you to keep them in sync). + + +Dictionary Ordering (not) +------------------------- + +Traditionally, dictionaries have had no defined order. See this example from Python 3.5: + +.. code-block:: ipython + + In [352]: d = {'one':1, 'two':2, 'three':3} + In [353]: str(d) + Out[353]: {'one': 1, 'three': 3, 'two': 2} + In [354]: d.keys() + Out[354]: dict_keys(['three', 'two', 'one']) + +Note how I defined the dict in a natural order, but when it gets printed, or you display the keys, they are in a different order. + +However, In cPython 3.6, the internal implementation was changed, and it *does* happen to preserve order. In cPython 3.6, that is considered an implementation detail -- and you should not count on it! However, as of cPython 3.7, dictionaries preserving order are part of the language specification. This was declared by Guido on the python-dev mailing list on +Dec 15, 2017 . + +.. code-block:: ipython + + In Python 3.6, the above code results in: + + In [9]: d = {'one':1, 'two':2, 'three':3} + + In [10]: str(d) + Out[10]: "{'one': 1, 'two': 2, 'three': 3}" + + In [11]: d.keys() + Out[11]: dict_keys(['one', 'two', 'three']) + + +When new items are added to a dict, they go on the "end": + +.. code-block:: ipython + + In [12]: d = {} + + In [13]: d['one'] = 1 + + In [14]: d['two'] = 2 + + In [15]: d['three'] = 3 + + In [16]: str(d) + Out[16]: "{'one': 1, 'two': 2, 'three': 3}" + +and ``dict.popitem()`` will remove the "last" item in the dict. + +**CAUTION** This is new behavior in cPython 3.6 -- older versions of Python (notably including Python 2) do not preserve order. In older versions, there is a special version of a dict in the collections module: ``collections.OrderedDict`` which preserves order in all versions of Python, and has a couple extra features. + +Also: while Python dicts now *preserve* order, they are not really a fully ordered object: there is no direct way to get, say, the "third" item in a dict, or to inset an item at a particular location. + + + +Dictionary Iterating +-------------------- + +``for`` iterates over the keys + +.. code-block:: ipython + + In [23]: d = {'name': 'Brian', 'score': 42} + + In [24]: for x in d: + ...: print(x) + ...: + name + score + +dict keys and values +-------------------- + +.. code-block:: ipython + + In [25]: d = {'name': 'Brian', 'score': 42} + + In [26]: d.keys() + Out[26]: dict_keys(['name', 'score']) + + In [27]: d.values() + Out[27]: dict_values(['Brian', 42]) + + In [28]: d.items() + Out[28]: dict_items([('name', 'Brian'), ('score', 42)]) + +Notice that these are of type ``dict_keys`` and ``dict_values``. These are special types that provide iteration, printing and other features, but are tied to the underlying dict, rather than copies. They are actually Sets (see below), so can be compared to sets, and the ``in`` operator is efficient. + +(Python2 would simply create lists of keys and values -- but then you were making a copy when you probably didn't need one). If you do need a copy, or a proper Sequence, simmply wrap them in a ``list()``: + +.. code-block:: python + + the_values_as_a_list = list(a_dict.values()) + + +dict keys and values +-------------------- + +Iterating on everything + +.. code-block:: ipython + + In [26]: d = {'name': 'Brian', 'score': 42} + + In [27]: for k, v in d.items(): + print("%s: %s" % (k,v)) + ....: + name: Brian + score: 42 + + +Dictionary Performance +----------------------- + + * Indexing is fast and constant time: O(1). + + * ``x in s`` is fast and constant time: O(1). + + * Visiting all items is proportional to n: O(n). + + * Inserting is constant time: O(1). + + * Deleting is constant time: O(1). + + + http://wiki.python.org/moin/TimeComplexity + + +Other dict operations: +---------------------- + +See them all here: + +https://docs.python.org/3/library/stdtypes.html#mapping-types-dict + +Is it in there? + +.. code-block:: ipython + + In [5]: d + Out[5]: {'that': 7, 'this': 5} + + In [6]: 'that' in d + Out[6]: True + + In [7]: 'this' not in d + Out[7]: False + +Containment is on the keys. + +Think of it like a "real" dictionary, where the keys are the words, and the values are the definitions. + +Is the word "gullible" in the dictionary? is asking if the key is in the dict. + + +Getting something: (like indexing) +---------------------------------- + +.. code-block:: ipython + + In [9]: d.get('this') + Out[9]: 5 + +But you can specify a default: + +.. code-block:: ipython + + In [11]: d.get('something', 'a default') + Out[11]: 'a default' + +`get()` never raises an Exception (default is None). + + +iterating +--------- + +.. code-block:: ipython + + In [13]: for item in d: + ....: print(item) + ....: + this + that + +Which is equivalent to, but a bit faster than: + +.. code-block:: ipython + + In [15]: for key in d.keys(): + print(key) + ....: + this + that + +In fact, there are very few things you can do with the ``dict_keys`` that you can't do directly with the dict. + +But to get values, you must specify you want the values: + +.. code-block:: ipython + + In [16]: for val in d.values(): + print(val) + ....: + 5 + 7 + +and to get both, you use ``.items``: + +.. code-block:: ipython + + In [4]: for item in d.items(): + ...: print(item) + ...: + ('this', 5) + ('that', 7) + + +``pop()`` +--------- + +"Popping": getting the value while removing it. + +Pop out a particular key: + +.. code-block:: ipython + + In [19]: d.pop('this') + Out[19]: 5 + + In [20]: d + Out[20]: {'that': 7} + +pop out an arbitrary key, value pair + +.. code-block:: ipython + + In [23]: d.popitem() + Out[23]: ('that', 7) + + In [24]: d + Out[24]: {} + +note that it's the last item, so not completely arbitrary. + +``setdefault`` +-------------- + +This one is handy: + +``setdefault(key[, default])`` + +gets the value if it's there, sets it to the specified default if it's not. Returns the value in either case. + +.. code-block:: ipython + + In [4]: d = {} + + In [5]: d.setdefault('something', 'a value') + Out[5]: 'a value' + + In [6]: d + Out[6]: {'something': 'a value'} + +The next time you call it, it gets the already set value: + +.. code-block:: ipython + + In [7]: d.setdefault('something', 'a different value') + Out[7]: 'a value' + + +Assignment +---------- + +Assignment (with ``=``) is a link to the original dict, just like lists or anything else. Remember that assignment is simply binding a new name to something. + +And dicts are mutable -- so be careful! + +.. code-block:: ipython + + In [47]: d + Out[47]: {'something': 'a value'} + + In [48]: item_view = d + + In [49]: d['something else'] = 'another value' + + In [50]: item_view + Out[50]: {'something': 'a value', 'something else': 'another value'} + + +If you want a copy, use the explicit copy method to get a copy: + +.. code-block:: ipython + + In [51] item_copy = d.copy() + + In [52]: d['another thing'] = 'different value' + + In [53]: d + Out[53]: + {'another thing': 'different value', + 'something': 'a value', + 'something else': 'another value'} + + In [54]: item_copy + Out[54]: {'something': 'a value', 'something else': 'another value'} + +or pass any mapping into the dict constructor:: + +.. code-block:: python + + new_dict = dict(old_dict) + + + +Sets +==== + +a ``set`` is an unordered collection of distinct values. + +Essentially, a ``set`` is a dict with only keys. + +https://docs.python.org/3.8/library/stdtypes.html#set-types-set-frozenset + +Set Constructors: +----------------- + +.. code-block:: ipython + + >>> set() + set() + + >>> set([1, 2, 3]) + {1, 2, 3} + + >>> {1, 2, 3} + {1, 2, 3} + + >>> s = set() + + >>> s.update([1, 2, 3]) + >>> s + {1, 2, 3} + + +Set Properties +--------------- + +``Set`` members must be hashable, like dictionary keys -- and for same reason (efficient lookup). + +No indexing (unordered). + +.. code-block:: ipython + + >>> s[1] + Traceback (most recent call last): + File "", line 1, in + TypeError: 'set' object does not support indexing + + +Set Methods +----------- + +.. code-block:: ipython + + >> s = set([1]) + >>> s.pop() # an arbitrary member + 1 + >>> s.pop() + Traceback (most recent call last): + File "", line 1, in + KeyError: 'pop from an empty set' + >>> s = set([1, 2, 3]) + >>> s.remove(2) + >>> s.remove(2) + Traceback (most recent call last): + File "", line 1, in + KeyError: 2 + + +All the "set" operations from math class... + +.. code-block:: python + + s.isdisjoint(other) + + s.issubset(other) + + s.union(other, ...) + + s.intersection(other, ...) + + s.difference(other, ...) + + s.symmetric_difference( other, ...) + +Set Operators +------------- + +All the basic set operations are support with math-class like operators: + +Test whether every element in the set is in other:: + + set <= other + + +Test whether the set is a proper subset of other, that is, set <= other and set != other:: + + set < other + +Test whether every element in other is in the set:: + + set >= other + +Test whether the set is a proper superset of other, that is, ``set >= other and set != other``:: + + set > other + +Union: Return a new set with elements from the set and all others:: + + set | other | ... + +Intersection: Return a new set with elements common to the set and all others:: + + set & other & ... + +Difference: Return a new set with elements in the set that are not in the others:: + + set - other - ... + +Symmetric difference: return a new set with elements in either the set or other but not both:: + + set ^ other + +In fact, it is the operator versions that make the ``set`` object "officially" a Set: (`Set ABC `_) + + +Frozen Set +---------- + +Another kind of set: ``frozenset`` + +immutable -- for use as a key in a dict (or another set...): + +.. code-block:: python + + >>> fs = frozenset((3,8,5)) + >>> fs.add(9) + Traceback (most recent call last): + File "", line 1, in + AttributeError: 'frozenset' object has no attribute 'add' + +A few added notes: +================== + +The count() method +------------------ + +All Python sequences (including strings) have a ``count()`` method: + +.. code-block:: ipython + + In [1]: s = "This is an arbitrary string" + + In [2]: s.count('t') + Out[2]: 2 + +What if you want a case-insensitive count? + +.. code-block:: ipython + + In [3]: s.lower().count('t') + Out[3]: 3 + +set.update() +------------ + +If you want to add a bunch of stuff to a set, you can use update: + +.. code-block:: ipython + + In [1]: s = set() + + In [2]: s.update + Out[2]: + + In [3]: s.update(['this', 'that']) + + In [4]: s + Out[4]: {'that', 'this'} + + In [5]: s.update(['this', 'thatthing']) + + In [6]: s + Out[6]: {'that', 'thatthing', 'this'} + +**NOTE:** It's VERY often the case that when you find yourself writing a trivial loop -- there is a way to do it with a built in method! + + + +Sorting stuff in dictionaries: +------------------------------- + +dicts aren't sorted, so what if you want to do something in a sorted way? + +The "standard" way: + +.. code-block:: python + + for key in sorted(d.keys()): + ... + +As dicts DO preserve order, you can make a sorted version of a dict: + +.. code-block:: python + + sorted_dict = dict(sorted(dict.items(), key=sort_key)) + +where sort_key is a function that takes the (key, value) tuple and returns a value to sort on. + + +Another option: + +.. code-block:: python + + collections.OrderedDict + +Also other nifty stuff in the ``collections`` module: + +https://docs.python.org/3.6/library/collections.html + +**NOTE:** As of Python 3.6, dicts do preserve order. But they are not full featured ordered objects. If you want a "properly" ordered object, use ``OrderedDict``. + diff --git a/_sources/modules/Documentation.rst.txt b/_sources/modules/Documentation.rst.txt new file mode 100644 index 0000000..d49e4dd --- /dev/null +++ b/_sources/modules/Documentation.rst.txt @@ -0,0 +1,130 @@ +.. _documentation: + +============= +Documentation +============= + +It's often helpful to leave information in your code about what you were +thinking when you wrote it. + +This can help reduce the number of `WTFs per minute `_ when reading it later. + +There are two approaches to this in Python: + +* Comments +* Docstrings + +Comments in Python are much the same as any other programing language. + +Docstrings are more unusual. + +Comments +-------- + +Comments go inline in the body of your code, to explain reasoning: + +.. code-block:: python + + if (frobnaglers > whozits): + # borangas are shermed to ensure frobnagler population + # does not grow out of control + sherm_the_boranga() + +You can use them to mark places you want to revisit later: + +.. code-block:: python + + for partygoer in partygoers: + for balloon in balloons: + for cupcake in cupcakes: + # TODO: Reduce time complexity here. It's killing us + # for large parties. + resolve_party_favor(partygoer, balloon, cupcake) + +Comments about Comments +----------------------- + + * Be judicious in your use of comments. + + * Use them when you need to. + + * Make them useful. + + * Do not use them merely to restate what the code is / should be doing. Make the code self explanatory! + +This is not useful: + +.. code-block:: python + + for sponge in sponges: + # apply soap to each sponge + worker.apply_soap(sponge) + +Note: Nothing special about Python here -- basic good programing practice. Note that you will need a lot fewer comments if you choose your names well! + +Docstrings +---------- + +In Python, "docstrings" are used to provide in-line documentation in a number of places. + +The first place we will see is in the definition of functions. + +As you know, to define a function you use the ``def`` keyword. + +If a "string literal" is the first thing in the function block following the +``def`` line, it is a "docstring": + +.. code-block:: python + + def complex_function(arg1, arg2, kwarg1='banana'): + """Return a value resulting from a complex calculation.""" + # code block here + +You can then read this in the interpreter as the ``__doc__`` attribute of the +function object. Docstrings can also be read and processed by documentation systems and IDEs like iPython and Sphinx. + +A Function Docstring Should: +............................ + +* Be a complete sentence in the form of a command describing what the function + does. + + * ``"""Return a list of values based on blah blah"""`` is a good docstring + + * ``"""Returns a list of values based on blah blah"""`` is *not* as good.. + +* Have a useful single line. + + * If more description is needed, make the first line a complete sentence and + add more lines below for enhancement. + +* Be enclosed with triple-quotes. + + * This allows for easy expansion if required at a later date. + +For any functions that are less than trivial, and particularly if they take multiple parameters, the parameters should be described in the docstring: + +.. code-block:: python + + def complex_function(arg1, arg2, kwarg1='banana'): + """ + Return a value resulting from a complex calculation. + + :param arg1: The first very important parameter. And a bit about + what it means. + :param arg2: The second very important parameter. And now some + description of how this is used + :param kwarg1='banana': An optional parameter. Some text describing + what it means and why you might specify it. + + """ + # The actual code here + +The ``:param arg1:`` notation is "restructured text" -- very handy if you want your docstrings to be able to be automatically processed by documentation systems such as `Sphinx `_ + +The docstring PEP +................. + +For the full "official" recommendations about docstrings, see `PEP 257: Docstring Conventions `_. + + diff --git a/_sources/modules/EnvironmentOverview.rst.txt b/_sources/modules/EnvironmentOverview.rst.txt new file mode 100644 index 0000000..d9f61fc --- /dev/null +++ b/_sources/modules/EnvironmentOverview.rst.txt @@ -0,0 +1,309 @@ +############################################ +Introduction to your Programming Environment +############################################ + +This program is a course of instruction in developing with the Python programming language. + +Python is a language with multiple implementations, and many different ways to edit and run code. + +One's development environment is a personal thing. What is most productive for you depends on what platform you use, how you like to work, what the people you work with are using, etc. It is a very personal choice. + +Each of you starting this program comes with a different background and experience. So we do not require that you use a particular development environment. Indeed, each of the instructors in the program uses their own tools and approach. + +However, there are some core requirements, and we provide advice for what to do if you are just getting started. + +Core Requirements for Python Development +======================================== + +There are three basic elements to your environment when working with Python for this class: + +* A way to run your code, add packages to Python, use git, etc. + + - You really need at least a little familiarity with the command line for this. + +* A Python interpreter + + - We use "cPython" version 3.8 or greater for this class. + +* A way to edit your code. + + - Any good programmer's text editor with a Python mode will work well. + +If you are already set up with all this, then go straight here: + +:ref:`testing_your_setup` + +and give it a try. + +If you are not sure, then read on ... + +The Command Line (cli) +---------------------- + +Having some facility on the command line is important for a software developer. + +We won't cover this much in class, so if you are not comfortable, +please bone up on your own. + +We have some resources here: :ref:`command_line_basics` + +**Windows:** + +Most of the demos in class, etc., will be done using the "bash" command line shell on OS-X or Linux. + +Windows provides the "DOS" command line, which is OK, but pretty old and limited, or "Power Shell," a more modern, powerful, flexible command shell. + +If you are comfortable with either of these, go for it. + +If not, you can use the "git Bash" shell, which is much like the bash shell on OS-X and Linux. Or, on Windows 10, look into the "bash shell for Windows," otherwise known as the "Windows Subsystem for Linux." More info is available here: + +:ref:`windows_bash` + +Accessing the Command Line +-------------------------- + +On any system, make sure you know how to get to a command line, and set the "working directory" to where your code resides. +Most modern UIs provide a way to start up a teminal from the file manager: + +Windows +....... + +Windows used to provide a "open command window here" options with a ``shift+right-click`` menu. Give a try; if it works, great. If not, then this might help: + +`Open Command Window Here on Windows 10 `_ + +OS-X +.... + +On The Mac, you can add a "New Terminal at Folder" right-click menu item by: + + + Head into System Preferences and select Keyboard => Shortcuts => Services. Find "New Terminal at Folder" in the settings and click the box. Now, when you're in Finder, just right-click a folder and you're shown the open to open Terminal. When you do, it'll start right in the folder you're in. + +`Launch an OS-X Terminal Window `_ + +Linux +..... + +Whether you use the KDE or GNOME Desktop (or anything else), there should be a way to open a shell from the file manager. Find it, it's very handy. + + +The Python Interpreter +---------------------- + +Python comes with a built-in interpreter. + +You see it when you type ``python3`` at the command line: + +.. code-block:: bash + + $ python3 + Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 26 2018, 23:26:24) + [Clang 6.0 (clang-600.0.57)] on darwin + Type "help", "copyright", "credits" or "license" for more information. + >>> + +That last thing you see, ``>>>`` is the "Python prompt". + +This is where you can type code. + +Notice that when it starts up, it tells you the version info. If the ``python3`` command did not work for you, or you got a version that is older than Python 3.6.4, you will need to install a new Python. Follow one of these: + +:ref:`python_for_mac` + +:ref:`python_for_windows` + +:ref:`python_for_linux` + +Note that you can use the interpreter to run a Python script as well: + +.. code-block:: bash + + $ python3 the name_of_the_file.py + +More on that here: + +:ref:`how_to_run_a_python_file` + + +Other interpreters +.................. + +In addition to the built-in interpreter, there are several more advanced +interpreters available to you. + +We'll be using one in this course called ``iPython`` -- more on that elsewhere. + +The Editor +---------- + +Typing code in an interpreter is great for exploring. + +But for anything "real," you'll want to save the work you are doing in a more permanent fashion. + +This is where a "programmer's text editor" fits in. + +Any good text editor will do. + +MS Word is **not** a text editor. + +Nor is *TextEdit* on a Mac. + +``Notepad`` on Windows is a text editor, but a crappy one. + +You need a real "programmers text editor." + +A text editor saves only what it shows you, with no special formatting +characters hidden behind the scenes. + +At a minimum, your editor should have: + +* Syntax Colorization +* Automatic Indentation + +In addition, great features to add include: + +* Tab completion +* Code linting +* Jump-to-definition + +Have an editor that does all this? Feel free to use it, and you can skip to the next section. + +If not, we recommend SublimeText or Atom: + +SublimeText: +............ + +`Sublime Text `_ + +:ref:`sublime_as_ide` + +Atom +.... + +`Atom `_ + +:ref:`atom_as_ide` + +And, of course, vim or Emacs on Linux, if you are familiar with those. + +Why No IDE? +----------- + +An IDE does not give you much that you can't get with a good editor plus a good interpreter. + +An IDE often weighs a great deal. + +Setting up IDEs to work with different projects can be challenging and time-consuming. + +An IDE, once set up, can hide a a lot of what is going on under the hood. Particularly when you are first learning, you don't want too much done for you, So we recommend using an editor and the command line. + +**That said ...** + +You may want to try the educational edition of PyCharm, which some people like a lot: + +https://www.jetbrains.com/pycharm-edu/ + +.. _testing_your_setup: + +Testing Your setup +================== + +If you have access to a command line, and Python installed, and a text editor or IDE ready to go, here's how you can make sure it's all working correctly. + +Python Interpreter +------------------ + +If you have Python installed and know how to run a python file, give this a try to make sure you're all setup: + +Create a file called ``install_test.py``, with the following content: + +.. code-block:: python + + #!/usr/bin/env python3 + + import sys + print("This is my first python program") + + version = sys.version_info + + if version.major == 3: + if version.minor < 6: + print("You should be running version 3.6 or 3.7") + else: + print("You are running python {}.{} -- all good!".format( + version.major, version.minor)) + + else: + print("You need to run Python 3!") + print("This is version: {}.{}".format(version.major, version.minor)) + +Run it with your version of python. It should result in something like this:: + + This is my first python program + You are running python3.6 -- all good! + +(Version 3.6 or 3.7 is fine) + +If you can't figure out how to run it, see: :ref:`how_to_run_a_python_file` + +If you can run, it but don't get that nice "all good" message, then you either do not have Python installed, or you have the wrong version. + +Go to one of: + +:ref:`python_for_mac` + +:ref:`python_for_windows` + +:ref:`python_for_linux` + +And try again. + +iPython +------- + +You should also be able to run iPython: + +.. code-block:: bash + + $ ipython + Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 26 2018, 23:26:24) + Type 'copyright', 'credits' or 'license' for more information + IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help. + + In [1]: + +If that doesn't work, try: + +.. code-block:: bash + + $ python3 -m pip install iPython + +And try it again (you may need to restart your terminal). + +If that doesn't work, go back to the install instructions. + +git +--- + +We will be using the git Source Code Version Control System (along with the gitHub service) to manage your assignments. + +There will be another lesson on getting that all set up for class, but for now, you should have a git client installed. Try: + +.. code-block:: bash + + $ git --version + git version 2.15.2 (Apple Git-101.1) + +If that reports a version newer than about 2.15, you are all set (as of this writing the latest version is 2.18). + +If the git command does not work, go back to the install instructions for your platform above, and get it installed. + +Other Helpful Hints +=================== + +There are a number of other assorted helpful materials here: + +:ref:`installing_python` + + diff --git a/_sources/modules/Exceptions.rst.txt b/_sources/modules/Exceptions.rst.txt new file mode 100644 index 0000000..d6078ba --- /dev/null +++ b/_sources/modules/Exceptions.rst.txt @@ -0,0 +1,432 @@ +.. _exceptions: + +################## +Exception Handling +################## + +Exceptions are a really nifty Python feature -- really handy! + +From the zen: + +.. centered:: "Errors should never pass silently." + +.. centered:: "Unless explicitly silenced." + + +That's what exception handling is all about. + +Exceptions +---------- + +An "Exception" is an indication that something out of the ordinary (exceptional) happened. + +Note that they are NOT called "Errors" -- often they are errors, but an Exception is not an indication of an error per se. + +This is why exception handling exists -- we often know that exceptions will occur, and know how to handle them -- we don't want the program to crash out. + +NOTE: if an Exception is raised in a Python program, and it has not been handled, then the program will stop, and report what happened. I'm sure you have seen this many times while working on your code! + + +Handling Exceptions +------------------- + +So far, Exceptions in your code have indicated a bug that you need to fix. But frequently you can anticipate where an Exception might occur, and your code can do something about it -- give a nice message to the user, or try the operation again in a different way -- the options are endless. Doing something after an Exception has occurred is known as "handling" the Exception. + +Exceptions are handled with a "try -- except" block. + +This provides another branching structure (kind of like if) -- a way for different code to run depending on what happens in a code block. + +Here is an example: + +.. code-block:: python + + try: + with open('missing.txt') as data_file: + process(data_file) # never called if file missing + except FileNotFoundError: + print("Couldn't find missing.txt") + +The ``try:`` block is code that you want to "try" to run. In this case, it's opening and processing a file. But if the file isn't there, then a ``FileNotFoundError`` is "raised". When an Exception is raised, no further code is run -- so the ``process()`` function will not be called. Once an exception is raised, Python looks for an ``except`` line. If the raised Exception matches the one in the ``except`` line, then the code in that block is run. + +If there is no ``except``, or the Exception doesn't match, then python will keep moving "up the stack", until the Exception is caught. If it is never caught, then the program will terminate. + + +Bare ``except`` +--------------- + +*Never* do this: + +.. code-block:: python + + try: + with open('missing.txt') as data_file: + process(data_file) # never called if file missing + except: + print "couldn't find missing.txt" + +If you don't specify a particular exception, ``except`` will catch *All* exceptions. + +**Always** capture the *particular* Exception(s) you know how to handle. + +Trust me, you can't anticipate everything, and you want the exception to propagate if it is not the one expected when you wrote the code. + + +Testing for errors "by hand": +----------------------------- + +Use Exceptions, rather than your own tests: + +Don't do this: + +.. code-block:: python + + do_something() + if os.path.exists('missing.txt'): + f = open('missing.txt') + process(f) + +It will almost always work -- but the *almost* will drive you crazy. + +It is "possible" that the file got deleted by another process in the precise moment between checking for it and opening it. Rare, but possible. Catching the exception will always work -- even in that rare case. + + +Example from mailroom exercise: +------------------------------- + +You want to convert the user's input into an integer. And you want to give a nice message if the user didn't provide a valid input. + +So you could do this: + +.. code-block:: python + + if num_in.isdigit(): + num_in = int(num_in) + +But -- ``int(num_in)`` will only work if the string can be converted to an integer. + +So you can also do: + +.. code-block:: python + + try: + num_in = int(num_in) + except ValueError: + print("Input must be an integer, try again.") + continue + +This is particularly helpful for things like converting to a float -- much more complicated to check -- and all that logic is already in the ``float()`` constructor. + +Or let the Exception be raised if you can't handle it. + +EAFP +---- + +This is all an example of the EAFP principle: + + "It's Easier to Ask Forgiveness than Permission" + + -- Grace Hopper + +The idea is that you want to try to do what you want to do -- and then handle it if it doesn't work (forgiveness). + +Rather than check to see if you can do it before trying (permission). + +Here's a nice PyCon talk by Alex Martelli about that: + +http://www.youtube.com/watch?v=AZDWveIdqjY + +(Alex Martelli is a Python Luminary -- read / watch anything you find by him). + + +Do you catch all Exceptions? +---------------------------- + +For simple scripts, let exceptions happen. + +Only handle the exception if the code can and will do something (useful) about it. + +This results in much better debugging info when an error does occur. The user will see the exception, and where in the code it happened, etc. + + +Exceptions -- ``finally`` +------------------------- + +There is another component to exception handling control structures: + +.. code-block:: python + + try: + do_something() + f = open('missing.txt') + process(f) # never called if file missing + except FileNotFoundError: + print("couldn't open missing.txt") + finally: + do_some_clean-up + +The code in the ``finally:`` clause will always run. + +This is really important if your code does anything before the exception occurred that needs to be cleaned up -- open database connection, etc... + +**NOTE:** In the above example, you can often avoid all that exception handling code using a with statement: + +.. code-block:: python + + with open('missing.txt') as f: + process(f) + +In this case, the file will be properly closed regardless. And many other systems, like database managers, etc. can also be used with ``with``. + +This is known as a "context manager", and was added to Python specifically to handle the common cases that required ``finally`` clauses. But if your use case does not already have a context manager that handles the cleanup you may need. + +Exceptions -- ``else`` +---------------------- + +Yet another flow control option: + +.. code-block:: python + + try: + do_something() + f = open('missing.txt') + except IOError: + print("couldn't open missing.txt") + else: + process(f) # only called if there was no exception + +So the ``else`` block only runs if there was no exception. That was also the case in the previous code, so what's the difference? + +**Advantage of** ``else`` **:** + +Using the ``else`` block lets you catch the exception as close to where it occurred as possible -- always a good thing. + +Why? -- because maybe the ``process(f)`` could raise an exception, too? Then you don't know if the exception came from the ``open()`` call or in some code after that. + +This bears repeating: + +**Always catch exceptions as close to where they might occur as you can**. + +Exceptions -- using the Exception object +---------------------------------------- + +What can you do in an ``except`` block? + +If your code can continue along fine, you can do very little and move along: + +.. code-block:: python + + try: + do_something() + except ValueError: + print("That wasn't any good") + +And that's that. + +But if your code *can't* continue on, you can re-raise the exception: + +.. code-block:: python + + try: + do_something() + except ValueError: + print("That wasn't any good") + raise + +The ``raise`` statement will re-raise the same exception object, where it may get caught higher up in the code, or even end the program. + +Exception objects are full-fledged Python objects -- they can contain data, and you can add data to them. You can give a name to a raised Exception with ``as``: + +.. code-block:: python + + try: + do_something() + f = open('missing.txt') + except IOError as the_error: + print(the_error) + the_error.extra_info = "some more information" + raise + +This prints the exception, then adds some extra information to it, and then re-raises the same exception object -- so it will have that extra data when it gets handled higher up on the stack. + +This is particularly useful if you catch more than one exception: + +.. code-block:: python + + except (IOError, BufferError, OSError) as the_error: + do_something_with(the_error) + +You may want to do something different depending on which exception it is. And you can inspect the Exception object to find out more about it. Each Exception has different information attached to it -- you'll need to read its docs to see. + +For an example -- try running this code: + +.. code-block:: ipython + + In [34]: try: + ...: f = open("blah") + ...: except IOError as err: + ...: print(err) + ...: print(dir(err)) + ...: the_err = err + +The ``print(dir(err))`` will print all the names (attributes) in the error object. A number of those are ordinary names that all objects have, but a few are specific to this error. + +the ``the_err = err`` line is there so that we can keep a name bound to the ``err`` after the code is run. ``err`` as bound by the except line only exists inside the following block. + +Now that we have a name to access it, we can look at some of its attributes. The name of the file that was attempted to be opened: + +.. code-block:: ipython + + In [35]: the_err.filename + Out[35]: 'blah' + +The message that will be printed is usually in the ``.args`` attribute: + +.. code-block:: ipython + + In [37]: the_err.args + Out[37]: (2, 'No such file or directory') + +the ``.__traceback__`` attribute hold the actual traceback object -- all the information about the context the exception was raised in. That can be inspected to get all sorts of info. That is very advanced stuff, but you can investigate the ``inspect`` module if you want to know how. + +Multiple Exceptions +------------------- + +As seen above, you can catch multiple exceptions with a single ``except`` statement by putting them all in a tuple: + +.. code-block:: python + +try: + some_code() +except (Exception1, Exception2, Exception3): + handle_them_all + +You should do this if the action required is same for all those Exceptions. + + +But if you want to do something different with each exception type, you can have multiple ``except`` blocks: + + +.. code-block:: python + + try: + some_code + except IOError: + handle_the_error + except BufferError: + handle_the_error + except OSError: + handle_the_error + +So a full-featured ``try`` block has all of this: + +.. code-block:: python + + try: + some_code + except IOError: + handle_the_error + except BufferError: + handle_the_error + ... + else: + some code to run if none of these exceptions occurred + finally: + some code to run always. + +The minimal try block is a ``try``, and one ``except``. + +Raising Exceptions +------------------- + +Many times, Exceptions will be raised by a built in python function, or from some library code that you are using. But there are times when the code you write may not directly handle some particular behavior. In that case, you can raise an exception yourself, and then it can be caught by code higher up the stack. This is done with the ``raise`` statement: + +.. code-block:: python + + def divide(a,b): + if b == 0: + raise ZeroDivisionError("b can not be zero") + else: + return a / b + +(OK, this is a stupid example, as that error will be raised for you anyway. But bear with me). + +When you call that function with a zero: + +.. code-block:: ipython + + In [515]: divide (12, 0) + ZeroDivisionError: b can not be zero + +Note how you can pass a message to the exception object constructor. It will get printed when the exception is printed. (and it is stored the in the Exception object's ``.args`` attribute) + + +Built in Exceptions +------------------- + +You can create your own custom exceptions. + +But for the most part, you can/should use a built in one ... + +.. code-block:: python + + exp = \ + [name for name in dir(__builtin__) if "Error" in name] + len(exp) + 48 + +There are 48 built-in Exceptions -- odds are good that there's one that matches your use-case. + +Also -- custom exceptions require subclassing -- and you haven't learned that yet :-). + + +Choosing an Exception to raise +------------------------------ + +Choose the best match you can for the built in Exception you raise. + +Example:: + + if (not isinstance(m, int)) or (not isinstance(n, int)): + raise ValueError + +Is the *value* of the input the problem here? + +Nope: the *type* of the input is the problem:: + + if (not isinstance(m, int)) or (not isinstance(n, int)): + raise TypeError + +but should you be checking type anyway? (EAFP) + +What I usually do is run some code that's similar that raises a built-in exception, and see what kind it raises, then I use that. + + +Knowing what Exception to catch +------------------------------- + +I usually figure out what exception to catch with an iterative process. + +I write the code without a try block, pass in "bad data", or somehow trigger the exception, then see what it is. + +Example: + +What if the file I want to read doesn't exist? + +.. code-block:: ipython + + In [7]: open("some_non_existant_file") + --------------------------------------------------------------------------- + FileNotFoundError Traceback (most recent call last) + in () + ----> 1 open("some_non_existant_file") + + FileNotFoundError: [Errno 2] No such file or directory: 'some_non_existant_file' + +Now I know to use:: + + except FileNotFoundError: + +In the ``try`` block where I am opening the file. + + + diff --git a/_sources/modules/Files.rst.txt b/_sources/modules/Files.rst.txt new file mode 100644 index 0000000..622e1ab --- /dev/null +++ b/_sources/modules/Files.rst.txt @@ -0,0 +1,389 @@ +.. _files: + +######################## +File Reading and Writing +######################## + +Saving and loading data. + + +Files +===== + +Text Files +---------- + +.. code-block:: python + + f = open('secrets.txt') + secret_data = f.read() + f.close() + +``secret_data`` is a string + +.. note:: In Python 3, files are opened by default in text mode, and the default encoding is UTF-8. This means that in the usual case, you get a proper Unicode string to work with, as UTF-8 is the most common encoding for text. Also, it is ASCII compatible, so ASCII Files with "just work". IF "Unicode" and "ASCII" mean nothing to you -- don't worry about it, just know that things will usually work for text, even non-English text. And if you get odd characters or an ``EncodingError``, then your file is not UTF-8, and it's time to Google "Python Unicode". (more info here: :ref:`unicode`) + + +Binary Files +------------ + +.. code-block:: python + + f = open('secrets.bin', 'rb') + secret_data = f.read() + f.close() + +``secret_data`` is a byte string (with arbitrary bytes in it -- well, not arbitrary -- whatever is in the file!) + +(See the ``struct`` module to unpack binary data ) + + +File Opening Modes +------------------ + +.. code-block:: python + + f = open('secrets.txt', [mode]) + 'r', 'w', 'a' + 'rb', 'wb', 'ab' + 'r+', 'w+', 'a+' + 'r+b', 'w+b', 'a+b' + + +These follow the Unix conventions, and aren't all that well documented +in the Python docs. But these BSD docs make it pretty clear: + +http://www.manpagez.com/man/3/fopen/ + +**Gotcha** -- 'w' modes always clear the file if it already exists! + +Text File Notes +--------------- + +Text is default: + + * Newlines are translated: ``\r\n`` -> ``\n`` + * -- reading and writing! + * Use \*nix-style in your code: ``\n`` + + +Gotcha: + + * no difference between text and binary on \*nix + * but this is not true on Windows, and will cause an error. + + +File Reading +------------ + +Reading part of a file: + +.. code-block:: python + + header_size = 4096 + f = open('secrets.txt') + secret_header = f.read(header_size) + secret_rest = f.read() + f.close() + + +Common Idioms +------------- + +.. code-block:: python + + for line in open('secrets.txt'): + print(line) + +(The file object is an iterable that iterates through the lines in a text file.) + +.. code-block:: python + + f = open('secrets.txt') + while True: + line = f.readline() + if not line: + break + do_something_with_line() + + +We will learn more about the keyword ``with`` later (it creates a "context manager"), but for now, just understand the syntax and the advantage over simply opening the file: + +.. code-block:: python + + with open('workfile', 'r') as f: + read_data = f.read() + f.closed + True + +You use ``with`` to open the file, and assign it a name (``f`` in this case). +The file remains open while in the ``with`` block. +At the end of the ``with`` block, the file is unconditionally closed, even if an Exception is raised. You code will (mostly) work without it, but it's a good habit to get into to always use ``with`` to open a file. + +File Writing +------------ + +.. code-block:: python + + outfile = open('output.txt', 'w') + for i in range(10): + outfile.write("this is line: %i\n"%i) + outfile.close() + + with open('output.txt', 'w') as f: + for i in range(10): + f.write("this is line: %i\n"%i) + + +File Methods +------------ + +Commonly Used Methods: + +.. code-block:: python + + f.read() f.readline() f.readlines() + + f.write(str) f.writelines(seq) + + f.seek(offset) f.tell() # for binary files, mostly + + f.close() + +``StringIO`` +------------ + +A ``StringIO`` method is a "file like" object that stores the content in memory. +That is, it has all the methods of a file, and behaves the same way, but never writes anything to disk. + +.. code-block:: python + + In [6]: import io + + In [7]: f = io.StringIO() + + In [8]: f.write("some stuff") + Out[8]: 10 + + In [9]: f.seek(0) + Out[9]: 0 + + In [10]: f.read() + Out[10]: 'some stuff' + + In [11]: f.getvalue() + Out[11]: 'some stuff' + + In [12]: f.close() + +(This can be handy for testing file handling code...) + + +Paths and Directories +===================== + +Paths +----- + +Paths are generally handled with simple strings. + +Relative paths: + +.. code-block:: python + + 'secret.txt' + './secret.txt' + +Absolute paths: + +.. code-block:: python + + '/home/chris/secret.txt' + + +Either works with ``open()`` , etc. + +Relative paths are relative to the current working directory, which is only relevant to command-line programs. + +``os`` module +------------- + +.. code-block:: python + + os.getcwd() + os.chdir(path) + + +``os.path`` module +------------------ + +.. code-block:: python + + os.path.split() + os.path.splitext() + os.path.basename() + os.path.dirname() + os.path.join() + os.path.abspath() + os.path.relpath() + + +(all platform independent) + +Directories +----------- + +.. code-block:: python + + os.listdir() + os.mkdir() + os.walk() + +(Note the ``shutil`` module provides higher level operations.) + +pathlib +------- + +``pathlib`` is a package for handling paths in an OO way: + +http://pathlib.readthedocs.org/en/pep428/ + +All the stuff in os.path and more: + +.. code-block:: ipython + + In [14]: import pathlib + + In [15]: pth = pathlib.Path('./') + + In [16]: pth.is_dir() + Out[16]: True + + In [17]: pth.absolute() + Out[17]: PosixPath('/Users/Chris/PythonStuff/UWPCE/Fall2018-PY210A/examples/Session02') + + In [18]: for f in pth.iterdir(): + ...: print(f) + ...: + ...: + +And it has a really nifty way to join paths, by overloading the "division" operator: + +.. code-block:: ipython + + In [49]: p = pathlib.Path.home() # create a path to the user home dir. + + In [50]: p + Out[50]: PosixPath('/Users/Chris') + + In [51]: p / "a_dir" / "one_more" / "a_filename" + Out[51]: PosixPath('/Users/Chris/a_dir/one_more/a_filename') + +Kinda slick, eh? + +For the full docs: + +https://docs.python.org/3/library/pathlib.html + +The Path Protocol +----------------- + +As of Python 3.6, there is now a protocol for making arbitrary objects act like paths: + +Read about it in PEP 519: + +https://www.python.org/dev/peps/pep-0519/ + +This was added because most built-in file handling modules, as well as any number of third party packages that needed a path, worked only with string paths. + +Even after ``pathlib`` was added to the standard library, you couldn't pass a ``Path`` object in where a path was needed --even the most common ones like ``open()``. + +So you could use the nifty path manipulation stuff, but still needed to call ``str`` on it: + +.. code-block:: python + + p = pathlib.Path.home() / a_filename.txt + + f = open(str(p), 'r') + +Rather than add explicit support for ``Path`` objects, a new protocol was defined, and most of the standard library was updated to support the new protocol. + +This way, third party path libraries could be used with the standard library as well. + +What this means to you +---------------------- + +Unless you are writing a path manipulation library, or a library that deals with paths other than with the stdlib packages (like ``open()``), all you need to know is that you can use ``Path`` objects most places you need a path. + +I expect we will see expanded use of pathlib as python 3.6 and 3.7 becomes widely used. + +Some added notes: +================= + +Using files and "with" +----------------------- + +Sorry for the confusion, but I'll be more clear now. + +When working with files, unless you have a good reason not to, use ``with``: + +.. code-block:: python + + with open(the_filename, 'w') as outfile: + outfile.write(something) + do_some_more... + # now done with out file -- it will be closed, regardless of errors, etc. + do_other_stuff + +``with`` invokes a context manager -- which can be confusing, but for now, just follow this pattern -- it really is more robust. + +And you can even do two at once: + +.. code-block:: python + + with open(source, 'rb') as infile, open(dest, 'wb') as outfile: + outfile.write(infile.read()) + + +Binary files +------------ + +Python can open files in one of two modes: + + * Text + * Binary + +This is just what you'd think -- if the file contains text, you want text mode. If the file contains arbitrary binary data, you want binary mode. + +All data in all files is binary -- that's how computers work. So in Python3, "text" actually means Unicode -- which is a particular system for matching characters to binary data. + +But this too is complicated -- there are multiple ways that binary data can be mapped to Unicode text, known as "encodings". In Python, text files are by default opened with the "utf-8" encoding. These days, that mostly "just works". + +But if you read a binary file as text, then Python will try to interpret the bytes as utf-8 encoded text -- and this will likely fail: + +.. code-block:: ipython + + In [13]: open("a_photo.jpg").read() + --------------------------------------------------------------------------- + UnicodeDecodeError Traceback (most recent call last) + in () + ----> 1 open("PassportPhoto.JPG").read() + + /Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/codecs.py in decode(self, input, final) + 319 # decode input (taking the buffer into account) + 320 data = self.buffer + input + --> 321 (result, consumed) = self._buffer_decode(data, self.errors, final) + 322 # keep undecoded input until the next call + 323 self.buffer = data[consumed:] + + UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte + + +In Python2, it's less likely that you'll get an error like this -- it doesn't try to decode the file as it's read -- even for text files -- so it's a bit tricky and more error prone. + +**NOTE:** If you want to actually DO anything with a binary file, other than passing it around, then you'll need to know a lot about how the details of what the bytes in the file mean -- and most likely, you'll use a library for that -- like an image processing library for the jpeg example above. + + + + + diff --git a/_sources/modules/Functions.rst.txt b/_sources/modules/Functions.rst.txt new file mode 100644 index 0000000..f436060 --- /dev/null +++ b/_sources/modules/Functions.rst.txt @@ -0,0 +1,262 @@ +.. _more_on_functions: + +More on Functions +================= + +From the materials you have covered up to this point you should have mastered the basics of writing functions. + +In particular, you know that functions can contain a chunk of code that can be written once, and used multiple times from other parts of the code. + +You know that you can pass values into the function, and that it can return values to the "calling" code. + +Now we will dig a bit deeper down into the specifics of functions in Python: + +Variable scope +-------------- + +Defining a function: + +.. code-block:: python + + def fun(x, y): + z = x + y + return z + +``x``, ``y``, ``z`` are *local* names. + +``x`` and ``y`` because they are function *parameters* + +``z`` because it was "bound" inside the function. + + +Local vs. Global +................ + +Names bound in Python have a *scope* + +That *scope* determines where a name is visible, and what value it has in a +given block. + +.. code-block:: ipython + + In [14]: x = 32 + In [15]: y = 33 + In [16]: z = 34 + In [17]: def fun(y, z): + ....: print(x, y, z) + ....: + In [18]: fun(3, 4) + 32 3 4 + +``x`` is global, while ``y`` and ``z`` are local to the function. + +But, did the value of ``y`` and ``z`` change in the *global* scope? + +.. code-block:: ipython + + In [19]: y + Out[19]: 33 + + In [20]: z + Out[20]: 34 + +No -- they did not. The "y" and "z" names *inside* the function are completely separate from the "y" and "z" outside the function. + +The ones outside the function are "global" names. + +When you use a name in Python, it first checks if it's a local name. Then, if that name is not in the local scope, it will look in the global scope for it. + +**NOTE:** "global" in Python means global to the module (generally a single file), not global to an entire program. Which is really good, as you have little way of knowing what names are being used in packages you are using, but are not writing yourself! + +In general, you should use global names mostly for constants. + +The Python convention is to designate global constants by typing the +names we bind to them in ALL_CAPS: + +.. code-block:: python + + INSTALLED_APPS = ['foo', 'bar', 'baz'] + CONFIGURATION_KEY = 'some secret value' + ... + +This is just a convention, but it's a good one to follow. + + +Global Gotcha +............. + +Take a look at this function definition: + +.. code-block:: ipython + + In [21]: x = 3 + + In [22]: def f(): + ....: y = x + ....: x = 5 + ....: print(x) + ....: print(y) + ....: + +What is going to happen when we call ``f``? + + +Try it and see: + +.. code-block:: ipython + + In [34]: f() + --------------------------------------------------------------------------- + UnboundLocalError Traceback (most recent call last) + in () + ----> 1 f() + + in f() + 1 def f(): + ----> 2 y = x + 3 x = 5 + 4 print(x) + 5 print(y) + + UnboundLocalError: local variable 'x' referenced before assignment + +Because you are binding the symbol ``x`` locally, it becomes a local and masks +the global value already bound. So in the line that caused the error: + +.. code-block:: python + + y = x + +Python knows that x is a local name, as it is assigned on the next line. But on this line, x has not yet been given a value -- hence the error. + + +Globals are "read only" +....................... + +While you have access to the global names in side a function, you can't change what those names are bound to. Take a look at the previous examples -- when we set a new value to a name (using the equal sign), that makes the name local -- so it will not change what the global name refers to. + + +Parameters +---------- + +So far we've seen simple parameter lists: + +.. code-block:: python + + def fun(x, y, z): + print(x, y, z) + +These types of parameters are called *positional* + +When you call a function, you **must** provide arguments for all *positional* +parameters *in the order they are listed*. + +Defaults for parameters: +........................ + +You can provide *default values* for parameters in a function definition: + +.. code-block:: ipython + + In [24]: def fun(x=1, y=2, z=3): + ....: print(x, y, z) + ....: + +When parameters are given with default values, they become *optional*. + +.. code-block:: ipython + + In [25]: fun() + 1 2 3 + +You can provide arguments to a function call for *optional* parameters +positionally: + +.. code-block:: ipython + + In [26]: fun(6) + 6 2 3 + In [27]: fun(6, 7) + 6 7 3 + In [28]: fun(6, 7, 8) + 6 7 8 + +Or, you can use the parameter name as a *keyword* to indicate which you mean: + +.. code-block:: ipython + + In [29]: fun(y=4, x=1) + 1 4 3 + + +This allows you to specify only those optional parameters that you need to, and keep using the defaults for the rest. +This is a very powerful feature of Python -- you'll find it's common to have a pretty long optional parameter list to functions. +It allows a lot of flexibility (the hard stuff is possible), while in common use, it's easy to use (the easy stuff is easy). + +Once you've provided a *keyword* argument in this way, you can no longer +provide any *positional* arguments: + +.. code-block:: ipython + + In [30]: fun(x=5, 6) + File "", line 1 + fun(x=5, 6) + SyntaxError: non-keyword arg after keyword arg + + +Recursion +--------- + +You've seen functions that call other functions. + +If a function calls *itself*, we call that **recursion**. + +Like with other functions, a call within a call establishes a *call stack*. + +With recursion, if you are not careful, this stack can get *very* deep. + +Python has a maximum limit to how much it can recurse. This is intended to +save your machine from running out of RAM. + +Recursion can be Useful +----------------------- + +Recursion is especially useful for a particular set of problems. + +For example, take the case of the *factorial* function. + +In mathematics, the *factorial* of an integer is the result of multiplying that integer by every integer smaller than itself down to 1. + +:: + + 5! == 5 * 4 * 3 * 2 * 1 + +We can use a recursive function nicely to model this mathematical function: + +.. code-block:: python + + def fact(n): + """compute the factorial of the input value, n""" + if n == 0: + return 1 + else: + return n * fact(n-1) + +This is a typical structure for a recursive function: + +A) It starts with a check to see if the recursive process is "done" -- can it simply return a simple value. + +B) If not, then it does a computation using the same function with another value. + +It is critical that the first check is there, or the function will never terminate. + +Further Reading +--------------- + +Here's a nice blog post about writting better functions: + +https://jeffknupp.com/blog/2018/10/11/write-better-python-functions/ + + + + diff --git a/_sources/modules/Git.rst.txt b/_sources/modules/Git.rst.txt new file mode 100644 index 0000000..0ee378a --- /dev/null +++ b/_sources/modules/Git.rst.txt @@ -0,0 +1,329 @@ +.. _git: + +############ +Intro to Git +############ + +What is Git? +------------ + +A "version control system" + +Why Version Control? +-------------------- + +.. figure:: http://phdcomics.com/comics/archive/phd101212s.gif + + "Piled Higher and Deeper" by Jorge Cham: www.phdcomics.com + +A history of everything everyone does to 'your' code + +A graph of "states" in which the code has existed + +That last one is a bit tricky, and is not necessary to understand right out of the gate. When you are ready, you can look at this supplement to gain a better understanding: + +:ref:`git_overview` + +There are other versioning systems, such as Mercurial and Subversion (and commercial offerings), but Git is the most popular. + +It is incredibly important to learn and understand version control to work as a developer today, so we have incorporated Git into our work flow for submitting students' work in this class. + + +Setting up Git +-------------- + +You should have git installed on your machine and accessible from the command line. If you don't have git working on the command line, revisit the appropriate instructions for your platform here: :ref:`installing_python`. + +Once git is installed and working, there is a little bit of setup for git that you should only have to do once: + +Letting git know your identity +.............................. + +.. code-block:: bash + + $ git config --global user.name "Marie Curie" + $ git config --global user.email "marie@radioactive.com" + +(using your email and name, of course) + +Editor +...... + +* git needs an editor occasionally +* default is VI, which is not very intuitive to non-Unix Geeks +* Nano is simple, easy solution for Macs and Linux +* Nano no longer available for windows, use Sublime Text or Notepad++ or Atom + +For windows users: :ref:`install_nano_win` + +Once you have chosen/installed an editor, configure git to use it: + +(full notes here: `GitHub help on Editors `_) + +**nano:** + +``$ git config --global core.editor "nano -w"`` + +**Sublime Text (mac):** + +``$ git config --global core.editor "subl -n -w"`` + +**Sublime Text(win):** + +``$ git config --global core.editor "'c:/program files/sublime text 2/sublime_text.exe' -w"`` + +**Notepad++ (Win):** + +``$ git config --global core.editor "'c:/program files (x86)/Notepad++/notepad++.exe' -multiInst -notabbar -nosession -noPlugin"`` + +**Atom:** + +``git config --global core.editor "atom --wait"`` + +Repositories +------------ + +A repository is just a collection of files that 'belong together'. + +Since ``git`` is a *distributed* versioning system, there is no **primary** +repository that serves as the one to rule them all. This simply means that all repositories on each users machine should look the same. + +However, to keep things sane, there is generally one "central" repository chosen that users check with for changes. For us this is the one hosted on GitHub in the UWPCE-PythonCert-ClassRepos organization. + + +Working with Remotes +-------------------- + +With git, you work with *local* repositories and the *remotes* that they are connected to. + +Git uses shortcuts to address *remotes*. When you *clone* a repository from its remote location to your local machine, you get an *origin* shortcut for free: + +.. code-block:: bash + + $ git remote -v + origin https://github.com/UWPCE-PythonCert-ClassRepos/ExampleRepo.git (fetch) + origin https://github.com/UWPCE-PythonCert-ClassRepos/ExampleRepo.git (push) + +This shows that the local repo on my machine *originated* from one in +the UWPCE-PythonCert-ClassRepos GitHub account (it shows up twice, because there is a shortcut for both ``fetch`` from and ``push`` to this remote). + +GitHub forks +------------ + +You can work on any project you wish to that has a public repository on GitHub. However, since you won't have permission to edit most projects directly, there is such a thing as *forking* a project. + +When you *fork* a repository, you make a copy of that repository in your own (GitHub) account. + +When you have made changes that you believe the rest of the community will want to adopt, you make a *pull request* to the original project. The maintainer(s) of that project than have the option of accepting your changes, in which case your changes will become part of that project. + +This is how we will be working in this class. When you are ready to submit an assignment, you will make a *pull request* to main class repo, and your instructors can review it. + +The class repositories are on *GitHub* in the *UWPCE-PythonCert-ClassRepos* organization: + +.. figure:: /_static/remotes_start.png + :width: 50% + :class: center + +https://github.com/UWPCE-PythonCert-ClassRepos + +Each class will have a repository created specifically for it, called something like: "Wi2018-Online". + +In examples below it is called YourClassRepoNameHere, so replace that in your head with the name of your class' repository. + +This class repository will include examples and relevant materials (and some exercise solutions) will be added throughout the class. + +There will be a folder called students at the top level, and everyone will create their own directory within it. + +Note that you can use any name you want for your personal working directory -- it can be your first name, you full name, or maybe your gitHub handle if you want to remain anonymous. Just make sure you let your instructors know what name you've used so that they can credit you for your work. + +Everyone will commit to this repository, and everyone will have access to everyone's code. + +This will make it easier to collaborate. Weirdly enough, collaborating is important for developing code, both for class and in the *real world*. + +Setting up Your Fork of the Class Repository +-------------------------------------------- + +The first thing we have to do is on the GitHub website. You will create a fork of the class repository from the ``UWPCE-PythonCert-ClassRepos`` account on GitHub into your personal account. + +Before you can do that, you need to create a GitHub account, if you don't have one already. Your gitHub id will be associated with this class' public repo, so it is up to you if you want to use your real name for your gitHub account, or an alias to maintain your privacy. + +Once you are logged in to your gitHub account, go to the appropriate class repository here: + +https://github.com/UWPCE-PythonCert-ClassRepos + +Make sure you find the right repo for YOUR class! + +Once in the repo for your class, click on the "fork" button in the upper right of the page to create a fork in your gitHub account. You will now have a copy of the class repo, and can then set up your personal machine to connect to that copy. + +.. figure:: /_static/remotes_fork.png + :width: 50% + :class: center + +Yoy should now have a copy of the class repository in your account on the GitHub website. + +The next step is to make a *clone* of your fork on your own computer, which means that **your fork** in GitHub is the *origin*: + +.. figure:: /_static/remotes_clone.png + :width: 50% + :class: center + +Begin in a directory on your computer where you want to keep your cloned version of the class repository. +This can live anywhere on your file system but this outer directory should not be tracked in git. + +From that directory, run + +.. code-block:: bash + + $ git clone https://github.com/your_github_id/YourClassRepoNameHere + +Be sure to replace "YourClassRepoNameHere" with the name of your class repository (you can get the url by going to the class repo on gitHub and clicking "clone or download"). + +This will download the repository from your GitHub account into the local directory that you ran the git *clone* command from. + +Adding a remote +............... + +Since you are working on a repository that you do not own, you will need to make a git shortcut to the original repository, so that you can get changes made by other contributors (i.e. the instructors and other students) before you start working. + +You can add *remotes* at will, to connect your *local* repository to other copies of it in different remote locations. + +This allows you to grab changes made to the repository in these other +locations. + +For this class, you will add an *upstream* remote to our local copy that points to the original copy of the material in the +``UWPCE-PythonCert-ClassRepos`` account, and we will call it, appropriately, "upstream". +Change directories into your local version of the class repository: + +.. code-block:: bash + + $ cd YourClassRepoNameHere + +and run (remembering to use the name of your class): + + +.. code-block:: bash + + $ git remote add upstream https://github.com/UWPCE-PythonCert-ClassRepos/YourClassRepoNameHere + +You can get that full url by going to GitHub, finding the repo, and copying the "clone" url. then you can type the command, and simply paste the url. + +Your local setup should now look something like this: + +.. code-block:: bash + + $ git remote -v + origin https://github.com/your_github_id/YourClassRepoNameHere (fetch) + origin https://github.com/your_github_id/YourClassRepoNameHere (push) + upstream https://github.com/UWPCE-PythonCert-ClassRepos/YourClassRepoNameHere (fetch) + upstream https://github.com/UWPCE-PythonCert-ClassRepos/YourClassRepoNameHere (push) + +This should leave you in a situation that looks like this: + +.. figure:: /_static/remotes_upstream.png + :width: 50% + :class: center + +To get the updates from your new remote, you'll need first to fetch everything: + +.. code-block:: bash + + $ git fetch --all + Fetching origin + Fetching upstream + ... + +Then you can see the branches you have locally available: + +.. code-block:: bash + + $ git branch -a + * master + remotes/origin/HEAD -> origin/master + remotes/origin/master + remotes/upstream/master + +Finally, you can fetch and then merge changes from the upstream master. + +Start by making sure you are on your own master branch: + +.. code-block:: bash + + $ git checkout master + +This is **really really** important. Take the time to ensure you are where you think you are, in other words, that your origin is your own GitHub repository and that you are working on master from that remote. +You can use `git remote -v` and `git branch -a` to verify. + +Now, fetch the upstream master branch and merge it into your master. +You can do this in one step with: + +.. code-block:: bash + + $ git pull upstream master + Updating 3239de7..9ddbdbb + Fast-forward + Examples/README.rst | 4 ++++ + ... + create mode 100644 Examples/README.rst + ... + + +Now all the changes from *upstream* are present in your local clone. +You should do this pull every time you start to work on code. + +In order to preserve the changes made by others in your fork on GitHub, you'll have to push: + +.. code-block:: bash + + $ git status + On branch master + Your branch is ahead of 'origin/master' by 10 commits. + (use "git push" to publish your local commits) + $ git push origin master + Counting objects: 44, done. + ... + $ + +(A simple ``git push`` will usually do the right thing) + +You are now set up to work with this repository, and the next steps will be similar every time you work on code. + +Go now to this page: :ref:`git_workflow`, where you will learn what to do each time you have work to submit for review. + + +Privacy Note: +............. + +Because of the way we have set up the class, you will be able +to see all work submitted to us from everyone in the class in +the students directory on your machine. This is not a bad thing. +And the files tend to be small. + +We encourage sharing of knowledge in this class. Helping your +fellow students will also help you to better understand. Share +your code, and get used to giving / receiving feedback on how to +improve your code, if you are not already. + +However, you are free to use any name you like for your working directory -- it does not have to be your real name, if you want to keep your privacy. + +Structure of multiple git repos +------------------------------- + +Each repository will have a directory called ``.git`` that is normally +not seen. This directory is how git keeps track of everything. Leave it alone. :) + +Please do not set up a git repository inside another git repository, this can lead to heartache. + +Absolutely, do NOT set up a git repository in your home root directory. +This will put everything in your home directory up on GitHub, and you do not want that. + +Setting up new repositories can be confusing because when you clone a git repository it creates the directory that will be the repository, but when you are creating a new repository, you need to first be **IN** the directory in which you want the repository to be rooted. Please ask if this does not make sense. + +It’s also important to note that you do not run the ``$ git init`` command at any point in the process of cloning and configuring your local copy of a remote repo. The ``init`` git command is used to initialize a git repository on your local machine and is not necessary in our case because the cloned repository has already been initialized. + +Additional Resources: + +git tutorial: +https://try.github.io/levels/1/challenges/1 + +basic git commands: +https://confluence.atlassian.com/bitbucketserver/basic-git-commands-776639767.html diff --git a/_sources/modules/GitWorkflow.rst.txt b/_sources/modules/GitWorkflow.rst.txt new file mode 100644 index 0000000..1c23a2b --- /dev/null +++ b/_sources/modules/GitWorkflow.rst.txt @@ -0,0 +1,201 @@ +.. _git_workflow: + +git Workflow +============ + +Git is a very flexible system that can be used in a lot of different ways to manage code development. This page describes the workflow we are using for this class -- about as simple a workflow as you can have with git. + +We start with an overview of the usual process. This overview may be all you need for future work, once you have created your home directory within the students directory. + +The instructions following the overview are very explicit for those new to git and the command line. + +The usual process +----------------- + +This is the usual series of steps you will want to go through when you do a new project / assignment. + +First make sure you are on the command line "in" your copy of the class repo. + +Remember that ``git status`` is your friend -- when in doubt, run that command to see what's going on in your repo. + +1. Make sure you are on the correct branch -- though if you never branch, you'll only need to do this once: + + ``$ git checkout master`` + +2. Get any changes from class repository -- good to do this whenever you start on something new + + ``$ git pull upstream master`` + +3. If there are changes merged in from upstream, you want to push them to your repository on GitHub. + + ``$ git push`` + +4. Make sure you are in your student directory, do some work -- create new files, edit then, etc. Verify you are happy with changes. Check the status of your repo: + + ``$ git status`` + + Make sure you add any new files: + + ``$ git add the_name_of_new_file`` + + you may want to do something like ``git add *.py`` if there are multiple files to add. + +5. Commit the all the changes -- make sure to add a good commit message. + + ``$ git commit -m 'my message here'`` + +6. Push your changes to your remote github account. + + ``$ git push`` + +7. If your are ready to submit your work, make a pull request on the gitHub website. + +Note that when you are working, you may want to do steps 2-6 far more often than step 7. (Don't go too crazy here, we don't want you to wait until the end of the quarter to get to step 7. ;-)) + +Now put that to work to get you set up for class: + +Required Initial Setup +----------------------- + +The first step in getting ready for the class is to create an individual directory for yourself inside the class repository, initiated with a README file. This step is necessary to ensure you have everything setup correctly and understand the process for future assignment submissions. Your instructor can review the workflow and give you early feedback before you start working on your first assignment submission. + +When you start a new class project or exercise, you should create a folder within this folder for that particular project (ex. lesson1). You should only ever add things inside your OWN directory -- don't add or change anything anywhere else. + +Note that when you start doing projects on your own (outside of classwork), you will want to create a whole new repository for each project. + +Create Your Own Working Directory +................................. + +The first step is to ``cd`` to the students directory: + +``$ cd students`` + +Then create a directory for yourself. You can use your first name, your gitHub handle (username), or any nickname you like -- just make sure your instructor knows who you are so you can get credit for your work. + +``$ mkdir marie_curie`` + +Switch into that dir: + +``$ cd marie_curie`` + +Adding a new file +................. + +Note that git does not track directories -- you do not have to add a new dir to git -- when you add a new file in that dir, git will track where it is. + +Now you can do your coding. For this example, that is simply adding a readme file. You can do that with your text editor, or directly on the command line:: + + cat > README.rst + Python code for UWPCE-PythonCert class, written by Marie Curie + ctrl+D + +Now tell git to track that new file: + +``git add README.rst`` + +Once you are done coding, always a good idea to look at what you have done. + +``$ git status`` + +Carefully observe new files or files that you have changed to ensure no other files are being committed outside of your student directory. + +Committing your changes +....................... + +Commit the changes with a summary of what you have done: + +``$ git commit -a -m 'added a readme file'`` + +Push your changes to your repo on gitHub: + +``$ git push origin master`` + +"origin" is the default name given by git referring to the server you cloned (in this case your github repository) + +"master" is the branch that you are currently pushing to that server. + +Since these are the default, you can usually simply do: + +``git push`` + +Make a PR +......... + +In high level overview, pull request provides a view to see the difference between a source branch (your fork) and a target branch (the main class repo), this view is used for code reviews and to provide feedback to the author. Keep in mind that this view is not static, meaning any subsequent commits to the source branch will show in this diff view. + +Now go onto GitHub, and make your first pull request (PR)! + +Here is some gitHub help for that: + +https://help.github.com/articles/creating-a-pull-request-from-a-fork/ + +You've pushed your own changes to that fork, and then issued pull requests to have that work merged back to the main class repo in (UWPCE-PythonCert-ClassRepos). An instructor will look at your code, make comments and approve your pull request if your work is satisfactory. + +Do that now with just the README file, so we can get the class repo all set up, and so that both you and your instructors know you have your gitHub repo all set up correctly. + +Starting a new Exercise +----------------------- + +Once you have created your directory, and are starting a new project, the process will look very much the same. This example is for marie_curie working on her mailroom exercise: + +Make sure you are "in" your copy of the class repo on your machine: + +``$ cd students/marie_curie`` + +Regardless of what you are working on, first make sure you don't have anything in your repository that you forgot to commit: + +``$ git status`` + +Note that when git status tells you that 'Your branch is up-to-date with 'origin/master', that does NOT mean that you are up-to-date with stuff that has been pushed to the github repository, only, confusingly, with what your local machine currently knows about. + +So, your next step is to make sure you have any changes that other people have made recently to the *remote* repository. + +``$ git pull upstream master`` + +"upstream" is the name we gave to the repository as it sits in the UWPCE github site. If you get an error message, check with the :ref:`git` documentation to make sure you set up the upstream shortcut correctly. + +"master" is the branch that you are currently pulling from that server, for the purpose of this class, we will always use master. + +If there are changes upstream that you did not have, it is a good idea to go ahead and push these changes to your github account right away so they don't confuse things: + +``$ git push`` + +Now you can begin your work: + +create a dir to do the Exercise in: + +``$ mkdir mailroom`` + +(remember to make sure you are creating this new dir in *your own working directory*) + +Create your new python file(s) in that new directory. Then add it to git before you start writing any real code -- just to make sure you don't forget: + +``$ git add mailroom.py`` + +Then as you work, each time you get to a good saving point, make a commit: + +``git commit -a -m "added the donation listing feature"`` + +And when you are done, push it to gitHub: + +``$ git push`` + +If you are ready for an instructor to review it, go to your repo on the gitHub website and make a pull request. + +Final Thoughts +-------------- + +We are using gitHub to submit and review your work because it provides a nice interface for code review. But more importantly, because the git revision control system, and the gitHub collaborative code development platform are industry standard tools for developing code. + +Learning git is a great skill -- we are only requiring the very basics for this class, but do take the opportunity to explore git a bit more -- making branches, reverting to older versions, etc. + +Also -- by doing it this way, you are getting an automatic back up of your work. Each time you "push", a copy of your work is getting backed up on gitHub. And you can also use it to coordinate your work among multiple computers -- you can have as many clones of your repo on gitHub as you like -- say one on a computer at work, and one at home. If you push a change from one computer, then running: + +``$ git pull`` + +on the other will bring that change down. This makes it really easy to do your classwork (or any work) in multiple places. + + + + + diff --git a/_sources/modules/GraphDatabases.rst.txt b/_sources/modules/GraphDatabases.rst.txt new file mode 100644 index 0000000..ceafdd4 --- /dev/null +++ b/_sources/modules/GraphDatabases.rst.txt @@ -0,0 +1,208 @@ +:orphan: + +.. _graph_databases: + +############### +Graph Databases +############### + +In computing, a graph database is a database that uses graph structures for semantic queries with nodes, edges and properties to represent and store data. + +https://en.wikipedia.org/wiki/Graph_database + +For those of you that aren't familiar with the mathematical concept of a "graph" -- what all this means is that the database itself stores not just the data, but the relationships between the data. + +This is in contrast to RDBMSs, where the data are stored in individual tables, with the relationships between the tables maintained via primary and foreign keys, and the actual relationships determined on the fly by searching multiple tables during "join" queries. RDBMSs are well optimized for these kinds of queries, but Graph Databases can be much more efficient for data retrieval when the records have complex relationships. + +I find it a bit ironic that "relational" databases, aren't directly storing the relationships :-) + +The wikipedia page has a pretty good description / example of how that works. + +There are a number of commercial and open source Graph databases out there, and more than a few have Python drivers. + +neo4j +===== + +`neo4j `_ is perhaps the most `popular `_ graph database as of this writing, and it comes with a Python driver and good documentation, so we'll use that one for examples. + +Here is a nice Python based tutorial about graph databases and neo4j: + +`Talking About your Data Relationships `_ + +And `here are the docs `_ for the python driver. + +And the Python API documentation: `python-driver API `_ + +There are a lot of other great docs and tutorial on the neo4j web site -- well worth checking out if you want to really learn how to use it. + +And here is the "official" :download:`neo4j developer manual: Python
      ` + + +neo4j example +============= + +Setup +----- + +When we use databases, we introduce additional setup that we must perform before we can create and access our data. These setup activities can get really complex, and in reality, when developing software professionally, we may find that the setup is performed by someone other than a developer -- that's what sys admins are for :-). + +To allow us to focus on Python development we are going to use the simplest way possible to get a database running, that will work whether you use Linux, MacOS or Windows. + +Here, we’ll talk you through the steps to get Neo4j working. + +What are we going to do? + +* First we’ll sign up for a free, online Neo4j account. + +* Then, we’ll configure the online database so we can start developing. + +* Next, we’ll make sure we have secure access to our database. + +* Final step is to install the requisite Python modules. + +At that point, Python development can commence! + +Let’s get started. + +GraphenDB +......... + +`GraphenDB `_ is a hosting service for neo4j databases. They provide free "sandbox" accounts for small databases you can use to test and learn how to use it. + +The getting started guide is here: `Getting Started `_ + +Getting an account: +................... + +1. Go to https://www.graphenedb.com/ + +2. Click on the "Sign up" button in the upper right to get signed up for an account. + +3. Once you have created an account, you need to create a database. You can create a small (but not that small!) "Hobby" database for free. + +4. Once you create the database, it will create a username (the name of the database you gave it) and generated password. Be sure to record your user name and password. + +Note that when your database is set up, you also get connection strings for both "bolt" and http REST interfaces. Originally designed for neo4j, Bolt is a highly efficient, lightweight client-server protocol designed for database applications. + +https://boltprotocol.org/ + + +Managing your password: +....................... + +We always have to sign on to our network database, using our user name and password. That means these credentials must be accessible to our Python program. But we must make sure that our password is secure. If we check code containing the password in to github, it will give access to anyone who reads our repo. With many online services, that will incur costs for which we would be responsible. + +But don’t worry, we can guard against that easily. Here’s how: + +First, edit your ``.gitignore`` file and add the following 2 lines at the end of the file, exactly as shown: + +:: + + # secrets + .config/ + +This will ensure that you don't accidentally add your password to git. + +NOTE: this still puts your password in plain text on your computer! So not really secure for really critical use! + +Now, in the parent directory of your local project, make a new directory called ``.config``. Note the leading period. + +In the newly create ``.config`` directory create a new file called ``config``. Note no leading period. + +Edit the ``config`` file using your preferred editor, creating lines as follows: + +:: + + [configuration] + + neo4juser = + neo4jpw = + +At the end of the lines, enter a space after the =, and then the user name and password created in step 4. above. Your config file will look something like this: + +:: + + [configuration] + + neo4juser = example1 + neo4jpw = f.wJRVveeeg9LL.CyWKF4RbGf2SWTKp + +Save that config file + +Your user name and password are now safely stored where Python can access them. The ``.gitignore`` change will prevent the ``.config`` files from being accidentally pushed to github. + +So now we need to setup access to Neo4j from Python. To do that we need to install the neo4j driver, which wires up Python to Neo4j. + +.. code-block:: bash + + pip install neo4j-driver + +Now, we are ready to start using our database! + +Cypher +------ + +Neo4j uses a query language called Cypher. It plays the same role as SQL for RDBMSs -- and the official driver uses it to "talk" to the database. + +https://neo4j.com/developer/cypher-query-language/ + +And here is a nice introduction: + +https://www.airpair.com/neo4j/posts/getting-started-with-neo4j-and-cypher + + +Quick test +.......... + +You can find example code in the class repo in: + +IntroPython-2017/examples/nosql/neo4j + +We are going to play with the code to get a feel for neo4j. + + +Other interfaces for neo4j +========================== + +neo4j-client is the default Python interface developed by the neo4j team. There are other options: + +neomodel +-------- + +Is a Django ORM-like Object Mapper for neo4j + +http://neomodel.readthedocs.io/en/latest/ + +Py2neo +------ + +Py2neo is a client library and toolkit for working with Neo4j from within Python applications and from the command line. The core library has no external dependencies and has been carefully designed to be easy and intuitive to use. + +It "speaks" the bolt protocol directly. + +http://py2neo.org/v3/ + +A bit more about Graphs +======================= + +Graph data structures can be very useful for certain catagories of problems: + +If you Google something like: "applications of graph data structure in computer science" you will get a lot of pages to explore, like this one: + +http://www.cs.cmu.edu/afs/cs/academic/class/15210-s14/www/lectures/graphs.pdf + +I encourage you to read up about them. + +If you do find a use-case, or simply want to explore the topic experimentally with Python, the main package for working with graphs in python is `networkx`: + +https://networkx.github.io/ + +It provides a pretty fully featured set of graph data structures, and the common algorithms for manipulating and exploring them. + +There is even a package for storing networkx graphs in neo4j: + +https://neonx.readthedocs.io + +So you can store your graph in the neo4j database, and work with it with networkx. This may even give you a nicer, more pythonic interface to neo4j. + + diff --git a/_sources/modules/HowToRunAPythonFile.rst.txt b/_sources/modules/HowToRunAPythonFile.rst.txt new file mode 100644 index 0000000..15fb6d4 --- /dev/null +++ b/_sources/modules/HowToRunAPythonFile.rst.txt @@ -0,0 +1,41 @@ +.. _how_to_run_a_python_file: + +How to run a python file +======================== + +A file with python code in it is a 'module' or 'script' + +(more on the distinction later on...) + +It should be named with the ``.py`` extension: ``some_name.py`` + +If you want to run the code directly (it is a script), you have a couple options: + +1) call python on the command line, and pass in your module name + +.. code-block:: bash + + $ python3 the_name_of_the_script.py + +2) On \*nix (linux, OS-X, Windows bash), you can make the file "executable":: + + $ chmod +x the_file.py + + And make sure it has a "shebang" line at the top:: + + #!/usr/bin/env python + + Then you can run it directly:: + + ./the_file.py + +3) On Windows, the `.py` extensions can be associated with the python interpreter, so it can be run directly. This is clunkier than the \*nix "shebang line" approach, so I don't recommend it -- but it is an option. But Windows does come with the "py" executable, that will examine a python file, look for a "shebang" line, and then run your file with the right executable. + + +4) run ``ipython``, and run it from within iPython with the ``run`` command + +.. code-block:: ipython + + In [1]: run the_file.py + +5) Various IDEs (PyCharm, IDLE, etc) have a way to run the module you are currently editing -- if you use one of these tools, learn how to do that. Make sure that it is using the version of Python that you want it to be. diff --git a/_sources/modules/IPythonIntroduction.rst.txt b/_sources/modules/IPythonIntroduction.rst.txt new file mode 100644 index 0000000..6952a8e --- /dev/null +++ b/_sources/modules/IPythonIntroduction.rst.txt @@ -0,0 +1,74 @@ +IPython Introduction +==================== + +You have installed `IPython`_. + +IPython is an advanced Python interpreter that offers enhancements. + +You can read more about it in the `official documentation`_. + +Specifically, you'll want to pay attention to the information about + +`Using IPython for Interactive Work`_. + +.. _IPython: http://ipython.org +.. _official documentation: http://ipython.org/ipython-doc/stable/index.html +.. _Using IPython for Interactive Work: http://ipython.org/ipython-doc/stable/interactive/index.html + +The very basics of IPython +-------------------------- + +IPython can do a lot for you, but for starters, here are the key pieces +you'll want to know: + +Start it up + +.. code-block:: bash + + $ ipython + Python 3.5.0 (v3.5.0:374f501f4567, Sep 12 2015, 11:00:19) + Type "copyright", "credits" or "license" for more information. + + IPython 4.0.0 -- An enhanced Interactive Python. + ? -> Introduction and overview of IPython's features. + %quickref -> Quick reference. + help -> Python's own help system. + object? -> Details about 'object', use 'object??' for extra details. + + +This is the stuff I use every day: + +* command line recall: + + - hit the "up arrow" key + - if you have typed a bit, it will find the last command that starts the same way. + +* basic shell commands: + + - ``ls``, ``cd``, ``pwd`` + +* any shell command: + + - ``! the_shell_command`` + +* pasting from the clipboard: + + - ``%paste`` (this keeps whitespace cleaner for you) + + + +* getting help: + + - ``something?`` + +* tab completion: + + - ``something.`` + +* running a python file: + + - ``run the_name_of_the_file.py`` + + +That's it -- you can get a lot done with those. + diff --git a/_sources/modules/IPythonParallel.rst.txt b/_sources/modules/IPythonParallel.rst.txt new file mode 100644 index 0000000..2b1a747 --- /dev/null +++ b/_sources/modules/IPythonParallel.rst.txt @@ -0,0 +1,32 @@ +.. _ipyparallel_quickstart: + +IPython Parallel Quickstart +=========================== + +Aside from its `official documentation`_ the following steps will get you quickly started with IPython Parallel. + +.. code-block:: bash + + $ pip install ipyparallel + +To start 1 controller and 4 engines: + +.. code-block:: bash + + $ ipcluster start -n 4 + +At this point you can interact with the cluster via IPython. + +.. code-block:: Ipython + + In [1]: import ipyparallel as ipp + + In [2]: c = ipp.Client() + + In [3]: c.ids + Out[3]: [0, 1, 2, 3] + + In [4]: c[:].apply_sync(lambda : "Hello, World") + Out[4]: [ 'Hello, World', 'Hello, World', 'Hello, World', 'Hello, World' ] + +.. _official documentation: https://ipyparallel.readthedocs.io/en/latest/ diff --git a/_sources/modules/IndividualProject.rst.txt b/_sources/modules/IndividualProject.rst.txt new file mode 100644 index 0000000..10c25b6 --- /dev/null +++ b/_sources/modules/IndividualProject.rst.txt @@ -0,0 +1,53 @@ +:orphan: + +.. _individual_project: + +################## +Individual Project +################## + +For the second quarter of the class, in addition to the weekly exercises, you are expected to complete an individual project. + +The goals of this are twofold: + +1) To show us that you have learned something in this class. One of the goals of the class is that at the end, you will be "able to do something useful with Python". + +2) To give you the opportunity to work on something that is of interest to you, and get a bit of code review from us in the process. Many of you are taking this class with a particular goal or use-case in mind, here's your chance to work on that. + + +Requirements +============ + +The requirements are pretty simple: + +* It can be for fun, or something you need for your job. + +* It should be large enough to take a couple weeks homework time to do. And not so large as to take more than that. + +* **It should demonstrate that you can do something useful with python.** + +* It should follow PEP8 (https://www.python.org/dev/peps/pep-0008). + +* It should have unit tests! -- and near 100% test coverage. + +* Ideally, it will be in version control (gitHub or similar -- you can use the class repo, or create a new one just for your project). + +* You are not required to use any specific python features (i.e. classes): use what is appropriate for your project. + +* If more than a single script, it should be in a python package with a setup.py script. + + +** Due the Sunday after the last class ** + + +**NOTE:** If you are doing a work related project, and are not able to put your code online, that's fine -- you can send your instructors a copy via email or whatever -- we just need to know what you've done. + +Make sure to get in touch with your instructors if you haven't figured out what to do by the eighth week! + + + + + + + + diff --git a/_sources/modules/Iteration.rst.txt b/_sources/modules/Iteration.rst.txt new file mode 100644 index 0000000..e030218 --- /dev/null +++ b/_sources/modules/Iteration.rst.txt @@ -0,0 +1,337 @@ +.. _iteration: + +######### +Iteration +######### + +Repetition, Repetition, Repetition, Repe... + + +For Loops +========= + +We've seen simple iteration over a sequence with ``for ... in``: + +.. code-block:: ipython + + In [170]: for x in "a string": + .....: print(x) + .....: + a + + s + t + r + i + n + g + + +No Indexing Required +-------------------- + +Contrast this with other languages, where you must build and use an ``index`` to iterate through an array. + +.. code-block:: javascript + + for(var i = 0; i < arr.length; i++) { + var value = arr[i]; + alert(i + ") " + value); + +If you *do* need an index, you can use ``enumerate``: + +.. code-block:: ipython + + In [140]: for idx, letter in enumerate('Python'): + .....: print(idx, letter, end=' ') + .....: + 0 P 1 y 2 t 3 h 4 o 5 n + + +``range`` and ``for`` Loops +--------------------------- + +The ``range`` builtin is useful for looping a known number of times: + +.. code-block:: ipython + + In [171]: for i in range(5): + .....: print(i) + .....: + 0 + 1 + 2 + 3 + 4 + +But you don't really need to do anything at all with ``i`` + +In fact, it's a common convention to make this clear with a "nothing" name: + +.. code-block:: ipython + + In [21]: for __ in range(5): + ....: print("*") + ....: + * + * + * + * + * + + +No Namespace +------------ + +Be alert that a loop does not create a local namespace: + +.. code-block:: ipython + + In [172]: x = 10 + In [173]: for x in range(3): + .....: pass + .....: + In [174]: x + Out[174]: 2 + +Loop Control +------------ + +Sometimes you want to interrupt or alter the flow of control through a loop. + +Loops can be controlled in two ways, with ``break`` and ``continue``. + + +The ``break`` keyword will cause a loop to immediately terminate: + +.. code-block:: ipython + + In [141]: for i in range(101): + .....: print(i) + .....: if i > 50: + .....: break + .....: + 0 1 2 3 4 5... 46 47 48 49 50 51 + + +The ``continue`` keyword will skip later statements in the loop block, but +allow iteration to continue: + +.. code-block:: ipython + + In [143]: for in in range(101): + .....: if i > 50: + .....: break + .....: if i < 25: + .....: continue + .....: print(i, end=' ') + .....: + 25 26 27 28 29 ... 41 42 43 44 45 46 47 48 49 50 + +Take some time to look at these examples carefully, and make sure you understand them. It's probably a good idea to write a bit of code to experiment as well. + +else +---- + +For loops can also take an optional ``else`` block. + +This is **not** a feature of most languages, but it can be handy. + +Executed only when the loop exits normally (not via break): + +.. code-block:: ipython + + In [147]: for x in range(10): + .....: if x == 11: + .....: break + .....: else: + .....: print('finished') + finished + In [148]: for x in range(10): + .....: if x == 5: + .....: print(x) + .....: break + .....: else: + .....: print('finished') + 5 + +This is a really nice, unique Python feature! + +If Python didn't have ``else`` on loops, you'd need to set a flag, something like: + +.. code-block:: python + + it_did_break = False + for x in range(10): + if x == 11: + it_did_break = True + break + if not it_did_break: + print('finished') + +That's klunkier, no? + +Make sure to try this a bit yourself too, to make sure you get it. + + +While Loops +=========== + +While loops are different -- they are not for iterating over a collection, but rather for repeating something an unknown number of times -- and maybe even forever -- or until the program terminates. + +The ``while`` keyword is for when you don't know how many loops you need. + +It continues to execute the body until the associated condition does not evaluate to True: + +.. code-block:: python + + while a_condition: + some_code + in_the_body + +``while`` vs. ``for`` +--------------------- + +``while`` is more general than ``for`` + +-- you can always express ``for`` as ``while``, but not always vice-versa. + +``while`` is more error-prone -- requires some care to terminate. + +The loop body must make progress, so the associated condition can become ``False``. + +Care must be taken to avoid an unintended error -- infinite loops: + +.. code-block:: python + + i = 0; + while i < 5: + print(i) + + +Terminating a while Loop +------------------------ + +Use ``break``: + +.. code-block:: ipython + + In [150]: while True: + .....: i += 1 + .....: if i > 10: + .....: break + .....: print(i) + .....: + 1 2 3 4 5 6 7 8 9 10 + +Set a flag: + +.. code-block:: ipython + + In [156]: import random + In [157]: keep_going = True + In [158]: while keep_going: + .....: num = random.choice(range(5)) + .....: print(num) + .....: if num == 3: + .....: keep_going = False + .....: + 3 + + +Use a condition: + +.. code-block:: ipython + + In [161]: while i < 10: + .....: i += random.choice(range(4)) + .....: print(i) + .....: + 0 0 2 3 4 6 8 8 8 9 12 + + +Similarities +------------ + +Both ``for`` and ``while`` loops can use ``break`` and ``continue`` for +internal flow control. + +Both ``for`` and ``while`` loops can have an optional ``else`` block. + +In both loops, the statements in the ``else`` block are only executed if the +loop terminates normally (no ``break``). + +Pythonic Iteration +================== + +I've already said it, but it bears repeating: + +for loops are for iterating over something (an "iterable") -- you almost never want to iterate over the indexes, and then access items with the index. + +Nifty for loop tricks +--------------------- + +**tuple unpacking:** + +remember this? + +.. code-block:: python + + x, y = 3, 4 + +You can do that in a for loop, also: + +.. code-block:: ipython + + In [4]: l = [(1, 2), (3, 4), (5, 6)] + + In [5]: for i, j in l: + print("i:{}, j:{}".format(i, j)) + + i:1, j:2 + i:3, j:4 + i:5, j:6 + + +Looping through two iterables at once: +-------------------------------------- + + ``zip`` + +.. code-block:: ipython + + In [10]: l1 = [1, 2, 3] + + In [11]: l2 = [3, 4, 5] + + In [12]: for i, j in zip(l1, l2): + print("i:{}, j:{}".format(i, j)) + + i:1, j:3 + i:2, j:4 + i:3, j:5 + +There can be more than two: + +.. code-block:: python + + for i, j, k, l in zip(l1, l2, l3, l4): + + +Need the index and the item? +---------------------------- + + ``enumerate`` + +.. code-block:: ipython + + In [2]: l = ['this', 'that', 'the other'] + + In [3]: for i, item in enumerate(l): + ...: print("the {:d}th item is: {:s}".format(i, item)) + ...: + the 0th item is: this + the 1th item is: that + the 2th item is: the other + + diff --git a/_sources/modules/IteratorsAndGenerators.rst.txt b/_sources/modules/IteratorsAndGenerators.rst.txt new file mode 100644 index 0000000..42195ac --- /dev/null +++ b/_sources/modules/IteratorsAndGenerators.rst.txt @@ -0,0 +1,603 @@ +.. _iterators_generators: + +Iterators and Generators +========================= + + The Tools of Pythonicity + + What goes on in those for loops? + + +A note about Python History +--------------------------- + +Python 2 +......... + +Python used to be all about sequences -- a good chunk of anything you did +was stored in a sequence, or involved manipulating a sequence. + +- lists +- tuples +- strings + +- ``dict.keys()`` +- ``dict.values()`` +- ``dict.items()`` +- ``zip()`` +- ... + +In python2 -- those are all sequences. (in the case of zip and dict methods, they return actual lists) + +But it turns out that the most common operation for sequences is to iterate through them: + +.. code-block:: python + + for item in a_sequence: + do_something_with_item + +So fairly early in Python2, Python introduced the idea of the "iterable". + +More or less, an "iterable" is something you can, well, iterate over in +a for loop, but often does not keep the whole sequence in memory at once. + +After all -- why make a copy of something just to look at all its items? + +Example: + +In python2: ``dict.keys()`` returns a list of all the keys in the dict. +But why make a full copy of all the keys, when all you want to do is: + +.. code-block:: python + + for k in dict.keys(): + do_something_with(k) + +Even worse: ``dict.items()`` created a full list of ``(key, value)`` tuples. +-- a complete copy of all the data in the dict. + +Even worse still: ``enumerate(dict.items())`` created a whole list of +``(index, (key, value))`` tuples -- lots of copies of everything. + +Enter ``iter*`` + +Python2 then introduced "iterable" versions of a number of functions and methods: + +``itertools.izip`` +``dict.iteritems()`` +``dict.iterkeys()`` +``dict.itervalues()`` + +So you could now iterate through that stuff without copying anything. Nice performance benefits, but a somewhat ugly interface. + +Python3 +....... + +Python3 embraces iterables -- now everything that could be an iterable without making a copy is done that way -- no unnecessary copies. + +If you DO need an actual sequence (becasue you want to do something with it other than iterate over it), you have to make a list out of them explicitly: + +``list(dict.keys())`` + +Then there is an entire module: ``itertools`` that provides nifty ways +to iterate through stuff. + +That will be covered elsewhere. + +So while I used to say that python was "all about sequences", I know say: + + "Python is all about iterables + + +Iterators and Iterables +----------------------- + +Iteration is one of the main reasons Python code is so readable: + +.. code-block:: python + + for x in just_about_anything: + do_stuff(x) + +An "iterable" is anything that can be looped over sequentially, so it does not have to be +a "sequence": list, tuple, etc. For example, a string is iterable. So is a set. + +An iterator is an iterable that remembers state. All sequences are iterable, but +not all sequences are iterators. To make a sequence an iterator, you can call it with iter: + +.. code-block:: python + + my_iter = iter(my_sequence) + +Iterator Types: + +https://docs.python.org/3/library/stdtypes.html#iterator-types + +Iterables +--------- + +To make an object iterable, you simply have to implement the ``__getitem__`` method. + +.. code-block:: python + + class T: + def __getitem__(self, position): + if position > 5: + raise IndexError + return position + + +``iter()`` +---------- + +How do you get the iterator object from an "iterable"? + +The ``iter`` function will make any iterable an iterator. It first looks for the ``__iter__`` +method, and if none is found, uses ``__getitem__`` to create the iterator. + +The ``iter()`` function: + +.. code-block:: ipython + + In [20]: iter([2,3,4]) + Out[20]: + + In [21]: iter("a string") + Out[21]: + + In [22]: iter( ('a', 'tuple') ) + Out[22]: + + +List as an Iterator: +-------------------- + +.. code-block:: ipython + + In [10]: a_list = [1,2,3] + + In [11]: list_iter = iter(a_list) + + In [12]: next(list_iter) + Out[12]: 1 + + In [13]: next(list_iter) + Out[13]: 2 + + In [14]: next(list_iter) + Out[14]: 3 + + In [15]: next(list_iter) + -------------------------------------------------- + StopIteration Traceback (most recent call last) + in () + ----> 1 next(list_iter) + StopIteration: + +Using iterators when you can +---------------------------- + +consider the example from the trigrams problem: + +(http://codekata.com/kata/kata14-tom-swift-under-the-milkwood/) + +You have a list of words: ``words`` + +And you want to go through it, three at a time, and match up pairs with +the following word. + +The *non-pythonic* way to do that is a loop through the indices: + +.. code-block:: python + + for i in range(len(words)-2): + triple = words[i:i+3] + +It works, and is fairly efficient, but what about: + +.. code-block:: python + + for triple in zip(words[:-2], words[1:-1], words[2:]): + + +``zip()`` returns an iterable -- it does not build up the whole list. +So this is quite efficient. + +but we are still slicing: ([1:]), which produces a copy -- so we are creating three copies of +the list -- not so good if memory is tight. Note that they are shallow copies, so not **that** bad. + +Nevertheless, we can do better: + +The ``itertools`` module has an ``islice()`` (iterable slice) function. +It returns an iterator over a slice of a sequence -- so no more copies: + +.. code-block:: ipython + + from itertools import islice + + In [68]: triplets = zip(words, islice(words, 1, None), islice(words, 2, None)) + + In [69]: for triplet in triplets: + ...: print(triplet) + ...: + ('this', 'that', 'the') + ('that', 'the', 'other') + ('the', 'other', 'and') + ('other', 'and', 'one') + ('and', 'one', 'more') + + +The Iterator Protocol +---------------------- + +The main thing that differentiates an iterator from an iterable (sequence) +is that an iterator saves state. + +An iterable must have the following methods: + +.. code-block:: python + + an_iterator.__iter__() + +Usually returns the iterator object itself. + +.. code-block:: python + + an_iterator.__next__() + +Returns the next item from the container. If there are no further items, +raises the ``StopIteration`` exception. + +An *iterable*, on the other hand, must have a ``__iter__`` method that returns an initialized iterator (which may or may not be itself). It doesnot have a ``__next__`` method. So you oculd say that the difference between and iterator and a iterable, is htat iterables do not have ``__next__`` methods: you cannont call ``next()`` on them. + + +Making an Iterator +------------------ + +A simple version of ``range()`` + +.. code-block:: python + + class IterateMe_1: + def __init__(self, stop=5): + self.current = 0 + self.stop = stop + def __iter__(self): + return self + def __next__(self): + if self.current < self.stop: + self.current += 1 + return self.current + else: + raise StopIteration + + +What does ``for`` do? +---------------------- + +Now that we know the iterator protocol, we can write something like a for loop: + +:download:`my_for.py <../examples/iterators_generators/my_for.py>` + +.. code-block:: python + + def my_for(an_iterable, func): + """ + Emulation of a for loop. + + func() will be called with each item in an_iterable + """ + # equiv of "for i in l:" + iterator = iter(an_iterable) + while True: + try: + i = next(iterator) + except StopIteration: + break + func(i) + + +Itertools +--------- + +``itertools`` is a collection of utilities that make it easy to +build an iterator that iterates over sequences in various common ways + +http://docs.python.org/3/library/itertools.html + +https://pymotw.com/3/itertools/index.html + +NOTE: + +iteratables are not *only* for ``for`` + +They can be used with anything that expects an iterable: + +``sum``, ``tuple``, ``sorted``, ``list``, ... + +Is an iterator a type? +---------------------- + +Iterators are not a type. An "iterable" is anything that has an ``__iter__`` +method that returns an iterator and/or has a ``__getitem__`` method that takes 0-based indexes. + +An "iterator" is anything that conforms to the "iterator protocol": + + - Has a ``__next__()`` method that returns objects. + - Raises ``StopIteration`` when their are no more objects to be returned. + - Has a ``__iter__()`` method that returns an iterator -- usually itself. + - sometimes the ``__iter__()`` method re-sets the iteration... + +https://docs.python.org/3/glossary.html#term-iterator + +Lots of common iterators are different types: + +.. code-block:: ipython + + In [23]: type(iter(range(5))) + Out[23]: range_iterator + + In [24]: iter(list()) + Out[24]: + + In [27]: type(iter(zip([],[]))) + Out[27]: zip + +Here's a nice overview: + +http://treyhunner.com/2016/12/python-iterator-protocol-how-for-loops-work/ + +LAB +---- + + +:download:`iterator_1.py <../examples/iterators_generators/iterator_1.py>` + +* Extend (``iterator_1.py`` ) to be more like ``range()`` -- add three input parameters: ``iterator_2(start, stop, step=1)`` + +* What happens if you break from a loop and try to pick it up again: + +.. code-block:: python + + it = IterateMe_2(2, 20, 2) + for i in it: + if i > 10: break + print(i) + +.. code-block:: python + + for i in it: + print(i) + +* Does ``range()`` behave the same? + + - make yours match ``range()`` + + - is range an iterator or an iteratable? + + +Generators +---------- + +Generators + +* give you an iterator object +* no access to the underlying data ... if it even exists + + +Conceptually: + Iterators are about various ways to loop over data. + + Generators can generate the data on the fly. + +Practically: + You can use either one either way (and a generator is one type of iterator). + + Generators do some of the book-keeping for you -- simpler syntax. + + Generators also can be used for times you want to pause a function + and pick it back up later where you left off. + + +yield +------ + +``yield`` is a way to make a quickie generator with a function: + +.. code-block:: python + + def a_generator_function(params): + some_stuff + yield something + +Generator functions "yield" a value, rather than returning a value. + +It *does* 'return' a value, but rather than ending execution of the +function -- it preserves the state so it can pick up where it left off. + +State is preserved in between yields. + +A function with ``yield`` in it is a "factory" for a generator + +Each time you call it, you get a new generator: + +.. code-block:: python + + gen_a = a_generator() + gen_b = a_generator() + +Each instance keeps its own state. + +Really just a shorthand for an iterator class that does the book keeping for you. + +To master yield, you must understand that when you call the function, +the code you have written in the function body does not run. The function +only returns the generator object. The actual code in the function is run +when ``next()`` is called on the generator itself. + +And note that each time you call the "generator function" you get a new +instance of a generator object that saves state separately from other instances. + +An example: like ``range()`` + +.. code-block:: python + + def y_range(start, stop, step=1): + i = start + while i < stop: + yield i + i += step + +Real World Example from FloatCanvas: + +https://github.com/svn2github/wxPython/blob/master/3rdParty/FloatCanvas/floatcanvas/FloatCanvas.py#L100 + + + +Note: + +.. code-block:: ipython + + In [164]: gen = y_range(2,6) + In [165]: type(gen) + Out[165]: generator + In [166]: dir(gen) + Out[166]: + ... + '__iter__', + ... + '__next__', + + +So the generator **is** an iterator + +Note: A generator function can also be a method in a class + +In fact, this is a nice way to provide different ways to iterate over +the data in a class in multiple ways. + +This is done by the dict protocol with ``dict.keys()`` and ``dict.values()``. + +More about iterators and generators: + +Chapter 14 in Fluent Python by Luciano Ramalho + +http://www.learningpython.com/2009/02/23/iterators-iterables-and-generators-oh-my/ + +:download:`yield_example.py <../examples/iterators_generators/yield_example.py>` + +generator comprehensions +------------------------ + +yet another way to make a generator: + +.. code-block:: python + + >>> [x * 2 for x in [1, 2, 3]] + [2, 4, 6] + >>> (x * 2 for x in [1, 2, 3]) + at 0x10911bf50> + >>> for n in (x * 2 for x in [1, 2, 3]): + ... print n + ... 2 4 6 + + +More interesting if [1, 2, 3] is also a generator + +Note that `map` and `filter` produce iterators. + +Keep in mind -- if all you need to do with the results is loop over it +-- use a generator expression rather than a list comprehension. + +Other uses for ``yield`` +------------------------ + +The ``yield`` keyword and generator functions were designed with classic "generators" in mind. + +That is -- objects that generate values on the fly. + +But, as we alluded to earlier, ``yield`` can be used for other things as well. + +Anytime you want to return a value, and then hold state until later, +``yield`` can be used. + +**Example:** pytest fixtures: + +.. code-block:: python + + @pytest.fixture + def example_fixture(request): + # setup code here + value = something() + yield value # provide the fixture value + # do the teardown + something_with(value) + +In this case, the ``yield`` isn't in any sort of loop or anything. +It will only get run once. But the generator will maintain state, +so the value can be used after the yield to do the teardown. + +How would this be done without yield? You'd need to store the value in a class: + +.. code-block:: python + + class a_fixture(): + + def __call__(self): + # make it callable so it can provide the value + # setup code here + value = something() + self.value = value + return value + + def teardown(self): + something_with(self.value) + +Not horrible, but not as clean and simple. + +LAB +--- + +Write a few generators: + +* Sum of integers +* Doubler +* Fibonacci sequence +* Prime numbers + +Test code in: + +:download:`test_generator.py <../examples/iterators_generators/test_generator.py>` + +Descriptions: + +Sum of the integers: + keep adding the next integer + + 0 + 1 + 2 + 3 + 4 + 5 + ... + + so the sequence is: + + 0, 1, 3, 6, 10, 15 ..... + + +Doubler: + Each value is double the previous value: + + 1, 2, 4, 8, 16, 32, + +Fibonacci sequence: + The fibonacci sequence as a generator: + + f(n) = f(n-1) + f(n-2) + + 1, 1, 2, 3, 5, 8, 13, 21, 34... + +Prime numbers: + Generate the prime numbers (numbers only divisible by them self and 1): + + 2, 3, 5, 7, 11, 13, 17, 19, 23... + +Others to try: + Try x^2, x^3, counting by threes, x^e, counting by minus seven, ... + diff --git a/_sources/modules/Lambda.rst.txt b/_sources/modules/Lambda.rst.txt new file mode 100644 index 0000000..9eac678 --- /dev/null +++ b/_sources/modules/Lambda.rst.txt @@ -0,0 +1,193 @@ +.. _anonymous_functions: + +########################### +Anonymous Functions: Lambda +########################### + +The ``lambda`` keyword is a way to create a function on the fly, without giving it a name. Hence the term "anonymous" -- it has no name. + +Using ``lambda`` +================ + +Structure +--------- + +"lambda" is a keyword -- it can not be used as a variable name, etc. A lambda expression looks like: + +.. code-block:: python + + lambda : + +It is an expression -- which means it evaluates to a value -- which means that it can be assigned to a variable, put in a container (such as a list or dict) etc. + +The parameters are just like function parameters -- but without the parentheses. And they exist in the local scope of the lambda, and can thus be used in the expression. + +The expression can be any expression using global variables or the parameters -- but not a statement -- anyone remember what the difference is? + +An expression is something that evaluates to a value, and can be assigned to a variable etc. but things like ``for`` and ``while`` loops and ``if...else`` structures are NOT expressions, and thus can't be put in a lambda. + +In short, lambda is only for very simple functions with no real flow control. + +Example +------- + +.. code-block:: ipython + + In [1]: f = lambda x, y: x + y + + In [2]: f(3,4) + Out[2]: 7 + +This creates a function that takes two positional parameters, ``x`` and ``y``, and returns their sum. + +This is ALMOST the same as: + +.. code-block:: ipython + + In [3]: def f2(x, y): + ...: return x + y + + In [4]: f2(3,4) + Out[4]: 7 + +So what is the difference? Well, ``lambda`` creates an "anonymous" function -- it doesn't have a name: + +.. code-block:: ipython + + In [10]: f.__name__ + Out[10]: '' + +All lambda functions are called . But: + +.. code-block:: ipython + + In [11]: f2.__name__ + Out[11]: 'f2' + +Regular "def" defined functions know their name. + +So ``lambda`` creates a function, but it doesn't have a name, and it can only have a single expression in it -- no flow control. + +What's the point? +----------------- + +There is nothing you can do with lambda that you can't do with regular ``def`` functions. But there are times when you just need a simple quick function that you don't need to keep around or refer to again -- so it's a handy syntax for these situations. + +A sorting key function is a great example: + +.. code-block:: ipython + + In [55]: lst = [("Chris","Barker"), ("Fred", "Jones"), ("Zola", "Adams")] + + In [56]: lst.sort() + + In [57]: lst + Out[57]: [('Chris', 'Barker'), ('Fred', 'Jones'), ('Zola', 'Adams')] + +Tuples are sorted, be default, by their first element. But what if you wanted to sort by the second (index 1) element -- last name in this case: + +.. code-block:: ipython + + In [13]: def sort_key(item): + ...: return item[1] + ...: + + In [14]: lst.sort(key=sort_key) + + In [15]: lst + Out[15]: [('Zola', 'Adams'), ('Chris', 'Barker'), ('Fred', 'Jones')] + +straightforward enough. But kind a lot of extra code, eh? and now there is this function: "sort_key" hanging around. You could delete it: ``del sort_key``, but that would be even more code. + +But with a lambda, you simply define it inline: + +.. code-block:: ipython + + In [16]: lst = [("Chris","Barker"), ("Fred", "Jones"), ("Zola", "Adams")] + + In [17]: lst.sort(key=lambda x: x[1]) + + In [18]: lst + Out[18]: [('Zola', 'Adams'), ('Chris', 'Barker'), ('Fred', 'Jones')] + +Nice and compact and clear, with no extra names hanging around. + +You'll find them useful with things like ``map``, ``filter``, and ``reduce``. as well. + + +Functions as first class objects +================================ + +lambda functions are python objects; they can be stored in a list or other container: + +.. code-block:: ipython + + In [7]: l = [lambda x, y: x+y] + In [8]: type(l[0]) + Out[8]: function + + +And you can call it by indexing the container: + +.. code-block:: ipython + + In [9]: l[0](3,4) + Out[9]: 7 + +You can do that with "regular" functions too: + +.. code-block:: ipython + + In [12]: def fun(x,y): + ....: return x+y + ....: + In [13]: l = [fun] + In [14]: type(l[0]) + Out[14]: function + In [15]: l[0](3,4) + Out[15]: 7 + +If the goal is to have that little function in the list, and you don't need to give it a name and/or reference it anywhere else, then lambda is a cleaner way to do it. + +lambda and keyword arguments +---------------------------- + +lambda functions can take keyword arguments as well: + +.. code-block:: ipython + + In [20]: (lambda x=None: x * 2)(x=4) + Out[20]: 8 + +Remember that default arguments get evaluated when the function is defined. This is the case with lambda as well. This can get you in trouble if you use a mutable in a function definition. But it also can be a handy way to "bake in" a value into a function to be used later: + +.. code-block:: ipython + + In [186]: l = [] + In [187]: for i in range(3): + l.append(lambda x, e=i: x**e) + +This creates a list, in this case with three items. Each of those items is a function. Note that the lambda is called each time through the loop, so each one has that default parameter defined separately -- and each time the default is set to the current value of ``i`` in the loop. So we get three functions, all the same except for the default value of ``e`` -- that is, each function will raise the input value to a different power. + +We can loop through that list, and call each function in turn with the same input value: + +.. code-block:: ipython + + In [25]: for f in func_list: + ...: print(f(2)) + ...: + 1 + 2 + 4 + 8 + +and presto! 2 raised to the zeroth, then first, the second, ... power. + +This may seem pretty obscure, but it's a handy way to auto-generate custom functions on the fly -- like for GUI callbacks, for instance: + +https://wiki.wxpython.org/Passing%20Arguments%20to%20Callbacks + + + + + diff --git a/_sources/modules/Logging.rst.txt b/_sources/modules/Logging.rst.txt new file mode 100644 index 0000000..bf39a28 --- /dev/null +++ b/_sources/modules/Logging.rst.txt @@ -0,0 +1,340 @@ +:orphan: + +.. _logging: + +############################### +Logging and the logging module +############################### + + +What is Logging? +================ + +.. rst-class:: left small + + What is logging? + + In computing, a logfile is a file that records either events that occur in an operating system or other software runs, or messages between different users of a communication software. + + Logging is the act of keeping a log. In the simplest case, messages are written to a single logfile. + + + (https://en.wikipedia.org/wiki/Logfile) + + But in fact, a file is only *one* place to keep a log. You may want to send a log of what your program is doing to another system, to the console, or???? + +what to log? +------------ + +.. rst-class:: medium:: + + What might you want to log? + +System information + +Error messages + +Fine-grain tracing output + + +The logging module +------------------- + +A flexible logging system that comes with the standard library + +Any module using the logging api can have logging output routed the same +as your code. + +Resources for learning more: + +https://docs.python.org/3.5/howto/logging.html + +http://docs.python-guide.org/en/latest/writing/logging/ + +https://pymotw.com/3/logging/ + + +Why not ``print()``? +-------------------- + +We've all been using ``print()`` all over the place to track what's going on in a program. + +And I still use it -- a lot. + +But we (usually) don't want all sorts of crap sent to stdout when the program is running in production. + +So we comment out or delete those ``print()`` s -- but if we wanted to know what the program was doing when developing -- maybe we want to know when something unanticipated goes wrong, too? + +The ``logging`` module give you a flexible system that allows you to monitor what's going on in your system, when you need to, without cluttering things up when you don't need it. + + +Background +========== + +.. rst-class:: left + + There are lots of good tutorials, etc, on the web for getting you started with *using* the logging module. + + But not much about how it works -- how it is structured. + + I found it hard to get beyond the basics without that knowledge, so the following should help. + + The logging module provides a very flexible framework for customizing the logging in a simple or complex application. + +The ``logging`` module +----------------------- + +.. code-block:: python + + import logging + +The logging module not only provides the classes and functions required to build a logging system, but also a place to centrally manage the logging for an entire application. + +This allows you to set up logging in one place, and everywhere in the app, the system can be used. + +So, for instance, when developing and debugging, you may want logging messages to go to the console, but for deployment, to log files. + +That configuration can be changed in one place. + +(NOTE: this is one good reason to prefer logging over ``print()``) + +Logging "levels" +---------------- + +The built in way to catagorize logging messages is by level. + +Levels are ordered numerically, so you can think of them as in order of importance, and it's easy to choose how much detail you want. + +The built-in set is:: + + CRITICAL 50 + ERROR 40 + WARNING 30 + INFO 20 + DEBUG 10 + +so DEBUG provides the most detail, and CRITICAL you'd pretty much always want to see. + + +The logging API provides easy ways to send messages with these levels: + +.. code-block:: python + + logging.debug('this is a debugging message') + + +https://docs.python.org/3/library/logging.html#levels + +similarly: + +.. code-block:: python + + logging.critical('this is a critical message') + logging.error('this is a error message') + logging.warning('this is a warning message') + logging.info('this is a information message') + logging.debug('this is a debug message') + +Note that if you have not configured the logger, a default configuration will automatically be set up -- so you can alwlays call these in yoru code, and it won't fail. + +This is actually particularly nice -- you can add logging messages to your code, and they will "do the right thing" when run inside any application, whether it's been spcifically configured or not. + +The logging classes +------------------- + +The four main classes (you need to deal with) for logging + +- Loggers - the interface for your code +- Handlers - handle log routing +- Filters - define which log messages to let through +- Formatters - how the log messages get rendered + +The ``Logger`` class +-------------------- + +The ``Logger`` class is the core class that handles logging. + +Messages get sent to a ``Logger`` instance, and it is responsibile for routing them appropriately. + +``Logger`` s can be nested in a hierarchical fashion, so that a message can be sent to sub-loggers, and any messages not handled will be passed the chain to eventually be handled by the "root" logger. + +There is always a root logger, and often the only one you need. + +Each ``Logger`` represents a single logging channel. + + +``Logger`` instances are given text names, with module-style "dots" representing the hierarchy: + +.. code-block:: python + + "main" + "main.sub_logger1" + "main.sub_logger2" + ... + +The "root" logger has no name, but is the root of all created loggers + +The logging module keeps track of all the loggers you create, so you can reference them by name. + +``logging.getLogger()`` +------------------------ + +The ``logging.getLogger()`` function returns the logger you ask for: + +.. code-block:: python + + the_root_logger = logging.getLogger() + another_logger = logging.getLogger("name") + +If the logger you ask for doesn't exist, ``getLogger()`` will create a new one for you by that name. (It won't be configured, though...) + +This whole system allows you to have multiple loggers without having to pass logging instances around. + +The ``Handler`` classes +----------------------- + +logging ``Handler`` s are what actually do the work of, well, handling, the log message. + +Formatting it, and actually writing to a file or somehow performing the 'log' duty. + +There are handlers for writing to files, streams (stdout, stderr), sockets, and nifty things like automaticaly rotating log files. + +And, of course, you can make your own. + +Each logger can have multiple Handlers + + +You will most likely use: + + - ``FileHandler`` + - ``StreamHandler`` + +The others are documented here. + +https://docs.python.org/3/library/logging.handlers.html#module-logging.handlers + +The ``Formatter`` classes +------------------------- + +``Formatters`` are responsible for formatting the log message. + +Each log message is stored in a ``LogRecord`` object, which has a lot of data about the message and where it came from. + +So you can use a formatter to add the data you want to your log entry. + +.. code-block:: python + + formatter = logging.Formatter('%(levelname)s - %(module)s - %(message)s') + +``levelname`` is the "level" of the log message: debug, warning, etc. ``module`` is the name of the module the message came from. ``message`` is the message itself. + +There are lots of other options: + +https://docs.python.org/3/library/logging.html#logrecord-attributes + +Each ``Handler`` can have its own ``Formatter`` + +The Filter classes +------------------ + +Each ``Logger`` can have a ``Filter`` object. + +``Filters`` determine which messages will be handled by a given logger, and which pass on to other loggers up the hierarchy. + +They can do very flexible filtering based on where the message came from, etc. + +But only really needed for complex systems: + +Loggers filter by "level" by default -- which is enough for most uses. + + +Basic logging usage +------------------- + +As you can see from the above -- the logging system is a complex nest of classes that can be configured and mixed and matched in complex ways. + +The system was ported from Java -- can you tell? + +However, the module provides a Pythonic API for common usage: the ``logging.basicConfig()`` function. + +Example: + +.. code-block:: python + + import logging + + logging.basicConfig(filename='example.log', + filemode='w', + format='%(asctime)s %(message)s', + level=logging.DEBUG) + + +This creates a "root" logger, and sets it up with: + +* a ``FileHandler`` with the given filename and mode + + - The mode is the file opening mode: 'w' to clobber and make a new file each time, 'a' to append to an existing file + +* sets up the handler to use the provided format string + + - ``asctime`` provides a datetime stamp (you can specify a format for that, too) + +* sets the level to debug -- so all messages will get logged. + + +What does ``basicConfig`` do for you? +-------------------------------------- +A LOT! + +If you were to do this by hand: + +.. code-block:: python + + filename = 'example.log' + filemode = 'w' + handler = logger.FileHandler(filename, mode) + format_str = '%(asctime)s %(message)s' + fmt = logger.Formatter(format_str) + handler.setFormatter(fmt) + logging.root.addHandler(h) + logging.root.setLevel(logging.EBUG) + +Wouldn't that be fun? + + +A more complex logging setup +---------------------------- + +See: + +``examples/logging/example.py`` + +In there, a logging system is set up that logs to a file, and also the console. + +It calls a fake "application" that does things in random order, logging as it goes... + + +References +---------- + +The logging system is very powerful and flexible. + +And frankly, not as clean and Pythonic as it could be... + +So it's pretty tricky to figure out. + +I highly recommend the cookbook to get beyond the basics: + +https://docs.python.org/3/howto/logging-cookbook.html + +Also these: + +http://atlee.ca/blog/posts/diving-into-python-logging.html + +http://victorlin.me/posts/2012/08/26/good-logging-practice-in-python + + + + + + diff --git a/_sources/modules/MapFilterReduce.rst.txt b/_sources/modules/MapFilterReduce.rst.txt new file mode 100644 index 0000000..4d5255a --- /dev/null +++ b/_sources/modules/MapFilterReduce.rst.txt @@ -0,0 +1,140 @@ +.. _map_filter_reduce: + +##################### +Map Filter and Reduce +##################### + +``map``, ``filter``, and ``reduce`` are considered key parts of "functional programming" + +But there is no real consensus about what "functional" means. + +But Python does support these "classic" functional methods. + + +map +--- + +``map`` "maps" a function onto a sequence of objects -- It applies the function to each item in the sequence, returning an "iterable" "map object". + +The map object delays evaluation until you iterate over it. That way you can pass it to another map, or use it in a for loop, without creating an unnecessary copy of the list. + +.. code-block:: ipython + + In [4]: l = [2, 5, 7, 12, 6, 4] + + In [5]: def fun(x): + ...: return x*2 + 10 + ...: + + In [6]: map(fun, l) + Out[6]: + + In [7]: list(map(fun, l)) + Out[7]: [14, 20, 24, 34, 22, 18] + +And if it's a small function, and you only need it once, this is a great use for ``lambda`` + +.. code-block:: ipython + + In [26]: list(map(lambda x: x*2 + 10, l)) + Out[26]: [14, 20, 24, 34, 22, 18] + +filter +------ + +``filter`` "filters" a sequence of objects with a boolean function -- +It keeps only those for which the function is True -- filtering out the rest. + +It similarly returns an iterable object. + +To get only the even numbers: + +.. code-block:: ipython + + In [27]: l = [2, 5, 7, 12, 6, 4] + In [28]: list(filter(lambda x: not x%2, l)) + Out[28]: [2, 12, 6, 4] + +If you pass ``None`` to ``filter()``, you get only items that evaluate to true: + +.. code-block:: ipython + + In [1]: l = [1, 0, 2.3, 0.0, 'text', '', [1,2], [], False, True, None ] + + In [2]: list(filter(None, l)) + Out[2]: [1, 2.3, 'text', [1, 2], True] + +reduce +------ + +``reduce`` "reduces" a sequence of objects to a single object with a function that combines two arguments. + +In python3, reduce has been a bit hidden in the "functools" module: + +.. code-block:: python + + from functools import reduce + +To get the sum: + +.. code-block:: ipython + + In [11]: from functools import reduce + In [12]: l = [2, 5, 7, 12, 6, 4] + In [13]: reduce(lambda x,y: x+y, l) + Out[13]: 36 + + +To get the product: + +.. code-block:: ipython + + In [32]: reduce(lambda x,y: x*y, l) + Out[32]: 20160 + +or + +.. code-block:: ipython + + In [13]: import operator + In [14]: reduce(operator.mul, l) + Out[14]: 20160 + +Comprehensions +-------------- + +Couldn't you do all this with comprehensions? + +Yes: + +.. code-block:: ipython + + In [33]: [x+2 + 10 for x in l] + Out[33]: [14, 17, 19, 24, 18, 16] + + In [34]: [x for x in l if not x%2] + Out[34]: [2, 12, 6, 4] + + In [6]: l + Out[6]: [1, 0, 2.3, 0.0, 'text', '', [1, 2], [], False, True, None] + In [7]: [i for i in l if i] + Out[7]: [1, 2.3, 'text', [1, 2], True] + +(Except Reduce) + +But Guido thinks almost all uses of reduce are really ``sum()``. In fact, that's why it got moved out of built-ins into the ``functools`` module. + + +Functional Programming +---------------------- + +Comprehensions and map, filter, reduce are all "functional programming" approaches. + +``map, filter`` and ``reduce`` pre-date comprehensions in Python's history + +Some people like that syntax better. + +And "map-reduce" is a big concept these days for parallel processing of "Big Data" in NoSQL databases. + +(Hadoop, MongoDB, etc.) + diff --git a/_sources/modules/MetaProgramming.rst.txt b/_sources/modules/MetaProgramming.rst.txt new file mode 100644 index 0000000..efe62c0 --- /dev/null +++ b/_sources/modules/MetaProgramming.rst.txt @@ -0,0 +1,868 @@ +:orphan: + +.. _metaprogramming: + +################ +Metaprogramming +################ + +**Programs that write programs....** + +Metaprogramming +=============== + + "Metaprogramming is a programming technique in which computer programs have the ability to treat programs as their data. It means that a program can be designed to read, generate, analyze or transform other programs, and even modify itself while running." + +``https://en.wikipedia.org/wiki/Metaprogramming`` + +In other words: A metaprogram is a program that writes (or modifies) programs. + +As a dynamic language, Python is very well suited to metaprogramming, as it allows objects to be modified at run time. It also provides excellent tools for: + +**Introspection:** + + "The ability of a program to examine the type or properties of an object at runtime." + + +Everything is an object +------------------------ + +Everything is an object in python: simple types like numbers and strings, as well as functions, classes, etc. + +That means that everything: + +* Can be created at runtime + +* Passed as a parameter + +* Returned from a function + +* Assigned to a variable + +This "everything is an object" is what allows full introspection and metaprogramming. + +**Wait!** didn't we use these features with closures and decorators?? + +Yes, indeed we did. And decorators are one of Python's metaprogramming tools. In this case, it's manipulating functions (and methods, which are just functions in a class) with code. Now we're going to learn how to manipulate other objects as well. + + +Introspection and manipulation tools +==================================== + +``getattr()`` and ``setattr()`` +------------------------------- + +These are the basic tools for, well, getting and setting attributes. They allow you to get and set attributes of an object by name: + +.. code-block:: ipython + + In [1]: class Dummy(): + ...: """A class with nothing in it""" + ...: pass + ...: + + In [2]: obj = Dummy() + + In [3]: vars(obj) + Out[3]: {} + + In [4]: setattr(obj, 'this', 54) + + In [5]: vars(obj) + Out[5]: {'this': 54} + + In [6]: getattr(obj, 'this') + Out[6]: 54 + +Let's play with this: (demo) + +NOTE: Do attributes have to be legal python names?? Try it! + +Note: there is also ``delattr`` to remove an attribute. + +Namespaces are Dictionaries +--------------------------- + +Another cool feature of python is that namespaces are (often) dictionaries. That means that you can directly manipulate the names and associated values of many objects directly. + +You can get the dict of a namespace with the ``vars()`` builtin: + +From a note on python-ideas: + + "... It isn't to be + a slightly different version of dir(), instead vars() should return the + object's namespace. Not a copy of the namespace, but the actual + namespace used by the object." + +This is not always true, e.g. for classes vars() returns a mappingproxy. + +From the Python Docs: + +"Objects such as modules and instances have an updateable ``__dict__`` attribute; however, other objects may have write restrictions on their ``__dict__`` attributes (for example, classes use a types.MappingProxyType to prevent direct dictionary updates)." + +https://docs.python.org/3.6/library/functions.html#vars + + +``__dict__`` +------------ + +An object's ``__dict__`` special attribute is used as the namesapce of an updateable object -- it's what you might expect, an actual dictionary used to hold the names in the namespace. + +For the most part, ``vars()`` will return the ``__dict__`` of an object. It's kind of like ``len()`` and the ``__len__`` attribute. But it's a bit better to use ``vars()`` to access an object's namespace -- it will work in more places. + +``dir()`` +--------- + +You may have used ``dir()`` to see the names in an object. It looks a lot like vars().keys() -- but it's not. There are two key differences: + +``dir()`` walks the class hierarchy of an object to give you all the attributes available: + +Create a class with a class attribute and an instance attribute: + +.. code-block:: python + + In [7]: class C: + ...: a_class_attribute = 0 + ...: def __init__(self): + ...: self.an_instance_attribute = 0 + +create an instance of that class. + +.. code-block:: python + + In [8]: c = C() + + In [9]: dir(c) + Out[9]: + ['__class__', + '__delattr__', + '__dict__', + '__dir__', + ... + '__subclasshook__', + '__weakref__', + 'a_class_attribute', + 'an_instance_attribute'] + +Note that both the class attribute and the instance attribute are there. + +Let's see what ``vars()`` gives us: + +.. code-block:: python + + In [10]: vars(c) + Out[10]: {'an_instance_attribute': 0} + +Just the instance attribute. Now let's look at the class object: + +.. code-block:: python + + In [11]: vars(C) + Out[11]: + mappingproxy({'__dict__': , + '__doc__': None, + '__init__': , + '__module__': '__main__', + '__weakref__': , + 'a_class_attribute': 0}) + +Now we get the class attribute, and a bunch more, but not all of them by any means. That's because the rest are inherited from ``object``. + +``vars()`` is also giving the namespace dict -- both the names and the values. So it's what you want if you are going to manipulate an object. + + +Manipulating a namespace +------------------------ + +``vars()`` with no argument returns the local namespace (same as ``locals()``). So you can manipulate even the local module namespace directly: + +.. code-block:: ipython + + In [1]: fred + --------------------------------------------------------------------------- + NameError Traceback (most recent call last) + in () + ----> 1 fred + + NameError: name 'fred' is not defined + +Of course it's not -- we haven't defined it. But if I access the local namespace with vars, and then add a name: + +.. code-block:: ipython + + In [2]: local_ns = vars() + + In [3]: local_ns['fred'] = "This is a new name in the local namespace" + + In [4]: fred + Out[4]: 'This is a new name in the local namespace' + +Now the name ``fred`` is there, just as if we had assigned the name in the normal way: + +.. code-block:: ipython + + In [5]: fred = "now a different value" + + In [6]: fred + Out[6]: 'now a different value' + +and we can access names that way too: + +.. code-block:: ipython + + In [7]: local_ns['fred'] + Out[7]: 'now a different value' + +Note that I didn't call vars() again to get the new value -- ``vars()`` returns the actual dict used for the namespace -- so it's mutated, the change shows up everywhere. + +Keep in mind that not all namespaces are writable. class objects, for instance, return a ``mappingproxy``, which is the namespace of the class object, but it is not a regular dict -- it's essentially a read-only dict. + + +Example of Manipulating Instance Attributes +------------------------------------------- + +Check out the code here: +:download:`get_set_attr.py
      ` + +It uses ``vars()`` in the str method to dynamically create a nice printable class. + +Then there is a simple function that lets the user manipulate that class, changing and adding attributes. + +Can you add code to let the user delete an attribute? + + +Class Objects +============= + +Metaprogramming is all about creating and manipulating programs. Classes are a very important part of programming in Python, so naturally, to do proper metaprogramming, we need to be able to create and manipulate class objects as well. + +And classes can have a lot more complexity than simple objects (or instances). + + +What's in a Class? +------------------ + +A class (and instance) object stores its attributes in a dictionary, or dictionary-like object. instances use a regular old python dict. You can access that dict with the ``__dict__`` attribute or ``vars()`` function: + +.. code-block:: ipython + + In [56]: class Simple(): + ...: ...: this = "a class attribute" + ...: ...: def __init__(self): + ...: ...: self.that = "an instance attribute" + ...: + + In [57]: vars(Simple) + Out[57]: + mappingproxy({'__dict__': , + '__doc__': None, + '__init__': , + '__module__': '__main__', + '__weakref__': , + 'this': 'a class attribute'}) + +And an instance of that object: + +.. code-block:: ipython + + In [59]: obj = Simple() + + In [60]: obj.__dict__ + Out[60]: {'that': 'an instance attribute'} + + +What class does this object belong to? +-------------------------------------- + +Every object has a ``__class__`` attribute specifying what class the object belongs to: + +.. code-block:: ipython + + In [16]: obj.__class__ + Out[16]: __main__.Simple + +and that is the actual class object: + +.. code-block:: ipython + + In [17]: obj.__class__ is Simple + Out[17]: True + +what is the class of a class object itself? + +.. code-block:: ipython + + In [61]: Simple.__class__ + Out[61]: type + +Interesting -- we've seen ``type`` as a function that tells you what type an object is (which is it's ``__class__``, by the way...). But it turns out ``type()`` is so much more... + +"type" or "class" +----------------- + +We talk about "classes", and yet we get the class of an object with ``type()``. + +In python, "type" and "class" are essentially the same thing. + +So why the two names? + +History: in the early days of python, a "type" was a built-in object, and a "class" was an object created with code: + +type - class unification began in python 2.2: + +``https://www.python.org/download/releases/2.2/descrintro/`` + +In python3, the unification is complete -- types *are* classes and vice-versa -- the terms are interchangeable. + +``type()`` +---------- + +So: ``type()`` will tell you what type (or class) and object is if you pass it one parameter. But if you pass it more, it does something pretty cool -- it makes a brand new class object. + +From the docstring: + +.. code-block:: ipython + + Docstring: + type(object) -> the object's type + type(name, bases, dict) -> a new type + +So that means if you pass in a single parameter, an object -- it will return the type of that object. But if you pass in three arguments, you get a new class object! + + +Creating a class from scratch +----------------------------- + +.. code-block:: python + + In [14]: atts = {'foo':'nice', 'bar':'sweet'} + + In [15]: type("CoolClass", (), atts) + Out[15]: __main__.CoolClass + + In [16]: CoolClass = type("CoolClass", (object,), atts) + + In [19]: cc = CoolClass() + + In [20]: cc.foo + Out[20]: 'nice' + + In [21]: cc.bar + Out[21]: 'sweet' + + In [22]: vars(CoolClass) + Out[22]: + mappingproxy({'__dict__': , + '__doc__': None, + '__module__': '__main__', + '__weakref__': , + 'bar': 'sweet', + 'foo': 'nice'}) + + +That is equivalent to: + +.. code-block:: python + + class CoolClass: + foo = 'nice' + bar = 'sweet' + + +But it was created at runtime, returned from a function and assigned to a variable. + +http://eli.thegreenplace.net/2011/08/14/python-metaclasses-by-example + +And it is a *class object*, not and instance -- it can be used to make instances from there. + +The signature is: :: + + type(name, bases, dict) + +so you need to pass in three things to make a class object. + +``name``: the name of the class -- this is what comes after the ``class`` keyword in the usual way... + +``bases``: a tuple of base classes -- this is the same as passing them when contructing the class. + +``dict``: this is a dictionary of the class attributes -- this will become the ``__class__`` of the class object (after some standard stuff is added) + + + + +Using type() to build a class +----------------------------- + +The ``class`` keyword is syntactic sugar, we can get by without it by +using type + +.. code-block:: python + + class MyClass: + x = 1 + +or + +.. code-block:: python + + MyClass = type('MyClass', (), {'x': 1}) + +(``object`` is automatically a superclass) + + +Adding methods to a class built with ``type()`` +----------------------------------------------- + +remember that functions are objects, so methods are simply attributes of a class that happen to be functions. So to add a method to a class created with ``type()``, just define a function with the correct signature and add it to the attr dictionary: + +.. code-block:: python + + def my_method(self): + print("called my_method, x = %s" % self.x) + + MyClass = type('MyClass',(), {'x': 1, 'my_method': my_method}) + o = MyClass() + o.my_method() + +How would you do an __init__ this way? + +Try it yourself.....does it work? + + +What type is type? +------------------ + +.. code-block:: ipython + + In [30]: type(type) + Out[30]: type + +Hmm, so type is a a type --this is the special case -- it has to stop somewhere! + +Metaclasses +----------- + +Objects get created from classes. So what is the class of a class? + +The class of a Class is a metaclass + +The metaclass can be used to dynamically create a class + +The metaclass, being a class, also has a metaclass + + +What is a metaclass? +-------------------- + +- A class is something that makes instances +- A metaclass is something that makes classes +- A metaclass is most commonly used as a class factory +- Metaclasses allow you to do 'extra things' when creating a class, + like registering the new class with some registry, adding methods + dynamically, or even replace the class with something else entirely (sound familiar from decorators?) +- Every object in Python has a metaclass +- The default metaclass is ``type`` + + +``metaclass`` +------------- + +So the default metaclass is ``type`` -- that is, type is used to make the class. But now we get to the fun stuff -- we can write our own metaclass -- and use that to create new class objects. + +Setting a class' metaclass: +........................... + +.. code-block:: python + + class Foo(metaclass=MyMetaClass): + pass + + +The class assigned to the ``metaclass`` keyword argument will be used to create the object class ``Foo``. (instead of ``type``) + +If the ``metaclass`` kwarg is not defined, it will use type to create the class. + +Whatever is assigned to ``metaclass`` should be a callable with the +same signature as type(): (``(name, bases, dict)``) + +**Python2 NOTE:** + +In Python 2, instead of the keyword argument, a special class attribute: +``__metaclass__`` is used: + +.. code-block:: python + + class Foo(object): + __metaclass__ = MyMetaClass + +Otherwise it's the same. + +The __metaclass__ attribute is part of determining that function. If __metaclass__ is a key in the body dictionary then the value of that key is used. This value could be anything, although if not callable an exception will be raised. +from http://jfine-python-classes.readthedocs.io/en/latest/decorators-versus-metaclass.html + +Why use metaclasses? +-------------------- + +What a metaclass does is create a way to create custom classes on the fly. You can do it directly with the ``type``, but if you write a metaclass, new classes can be made with that metaclass in the usual way. + +They can be useful when creating an API or framework. + +Whenever you need to manage object creation for one or more classes. + +Examples may help, so take a look at: +:download:`singleton.py
      ` + +Or consider the Django ORM: + +.. code-block:: python + + class Person(models.Model): + name = models.CharField(max_length=30) + age = models.IntegerField() + + person = Person(name='bob', age=35) + print person.name + +When the Person class is created, it is dynamically modified to +integrate with the database configured backend. Thus, different +configurations will lead to different class definitions. This is +abstracted from the user of the Model class. And the user doesn't have to know anything about that ugly database stuff :-) + + +Here is the Django Model metaclass: + +https://github.com/django/django/blob/master/django/db/models/base.py#L61 + +pretty ugly, eh? + + +``__new__`` +----------- + +A bit of a sidetrack ... + +What is this ``__new__`` thing? It's another of Python's special dunder methods. ``__new__`` is called when you make a new instance of a class. + +Wait? isn't ``__init__`` the constructor of the class? + +Not really -- ``__init__`` is the *initializer* -- it initializes the instance -- setting instance attributes, etc. But remember its signature? + +.. code-block:: python + + def __init__(self, *args, **kwargs) + +What's that self thing? That's the instance that is being initialized -- but it already exists -- it has to already have been created. + +Most of the time, that's all you need -- you want the instance created in the usual default way, and then you can initialize it. But if you need to do something before the object is initialized -- you can define a ``__new__`` method. + +.. code-block:: python + + class Class(): + def __new__(cls, arg1, arg2): + some_code_here + return cls(...) + ... + +* ``__new__`` is called: it returns a new instance + +* The code in ``__new__`` is run to pre-initialize the instance + +* ``__init__`` is called + +* The code in ``__init__`` is run to initialize the instance + +``__new__`` is a static method (it can be called on the class object itself) -- but it must be called with a class object as the first argument. + +.. code-block:: python + + class Class(superclass): + def __new__(cls, arg1, arg2): + some_code_here + return superclass.__new__(cls) + ..... + +``cls`` is the class object. + +The arguments (arg1, arg2) are what's passed in when calling the class. + +It needs to return a class instance -- usually by directly calling the superclass ``__new__`` (which returns a new instance). + +If there are no superclasses, you can call ``object.__new__`` (or ``super().__new__``) + + +When to use ``__new__`` +------------------------ + +When would you need to use it: + +* Subclassing an immutable type: + + - It's too late to change it once you get to ``__init__`` + +* When ``__init__`` is not called: + + - unpickling + + - copying + +You may need to put some code in ``__new__`` to make sure things +go right. + +More detail here: + +https://docs.python.org/3/reference/datamodel.html#object.__new__ + + +``__new__`` vs ``__init__`` in Metaclasses +-------------------------------------------- + +Remember that metaclasses are used to create new class objects (instances of type) -- so ``__new__`` is critical to creating that class. + +``__new__`` is used when you want to control the creation of the class (object) + +``__init__`` is used when you want to control the initialization of the class (object) + +``__new__`` and ``__init__`` are both called when the module containing the class is imported for the first time. i.e. at compile time. + +``__call__`` is used when you want to control how a class (object) is called (instantiation) + + +.. code-block:: python + + class CoolMeta(type): + def __new__(meta, name, bases, dct): + print('Creating class', name) + return super(CoolMeta, meta).__new__(meta, name, bases, dct) + def __init__(cls, name, bases, dct): + print('Initializing class', name) + super(CoolMeta, cls).__init__(name, bases, dct) + def __call__(cls, *args, **kw): + print('Meta has been called') + return type(cls, *args, **kw) + + class CoolClass(metaclass=CoolMeta): + def __init__(self): + print('And now my CoolClass exists') + + print('Actually instantiating now') + foo = CoolClass() + +:download:`cool_meta.py
      ` + + +Metaclass example +----------------- + +Consider wanting a metaclass which mangles all attribute names to +provide uppercase and lower case attributes + +.. code-block:: python + + class Foo(metaclass=NameMangler): + x = 1 + + f = Foo() + print(f.X) + print(f.x) + + +NameMangler +----------- + +.. code-block:: python + + class NameMangler(type): + + def __new__(cls, clsname, bases, _dict): + uppercase_attr = {} + for name, val in _dict.items(): + if not name.startswith('__'): + uppercase_attr[name.upper()] = val + uppercase_attr[name] = val + else: + uppercase_attr[name] = val + + return super().__new__(cls, clsname, bases, uppercase_attr) + + + class Foo(metaclass=NameMangler): + x = 1 + + +LAB: Working with NameMangler +----------------------------- + +Download: :download:`mangler.py
      ` + +Modify the NameMangler metaclass such that setting an attribute f.x also +sets f.xx + +Now create a new metaclass, MangledSingleton, composed of the ``NameMangler`` class you just worked with, and the ``Singleton`` class here: +:download:`singleton.py
      ` + +Assign it to the ``metaclass`` keyword argument of a new class and verify that it works. + +Your code should look like this: + +.. code-block:: python + + class MyClass(metaclass=MangledSingleton) # define this + x = 1 + + o1 = MyClass() + o2 = MyClass() + print(o1.X) + assert id(o1) == id(o2) + + +The Singleton +------------- + +One common use of metaclasses is to create a singleton: + + "The singleton pattern is a software design pattern that restricts the instantiation of a class to one object." + +https://en.wikipedia.org/wiki/Singleton_pattern + +The above exercise provided an example of this +(:download:`singleton.py `) + +However, metaclasses are not the only way to create a singleton. It really depends on what you are trying to do with your singleton. + +http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Singleton.html + +http://stackoverflow.com/questions/6760685/creating-a-singleton-in-python + +Class decorators? +----------------- + +We touched on class decorators a bit when decorators were introduced: + +.. code-block:: python + + @a_decorator + class MyClass(): + ... + +A decorator is a "callable" that returns a "callable" -- usually a modified (or "wrapped") version of the one passed in. + +Class objects are callable -- you call them when you instantiate a instance: + +.. code-block:: python + + an_inst = MyClass() + +So you can decorate a class as well as functions and methods. + +In fact, you can do many of the same things that you can do with metaclasses. + +When you decorate a class, you can change it in some way, and then the +changed version replaces the one in the definition. + +This also happens at compile time, rather than run time, just like metaclasses. + +class decorators were actually introduced AFTER metaclasses -- maybe they +are a clearer solution to some problems? + +As an example, in Python 3.7, there is a new feature in the standard library: ``Data Classes``, introduced in +`PEP 557 `_ + +They are a quick way to make a simple class whose prime purpose is to store a set of fields -- kind of like a database record. What the new tool provides is auto-generation of all the boilerplate code for the ``__init__``, etc. They could have been implemented with a metaclass, but it was decided to use a class decorator instead. From the PEP: + + "No base classes or metaclasses are used by Data Classes. Users of these classes are free to use inheritance and metaclasses without any interference from Data Classes. The decorated classes are truly "normal" Python classes. The Data Class decorator should not interfere with any usage of the class." + +A key difference between using a class decorator and a metaclass is that a metaclass is used to create the class -- so you can manipulate things before the class is created. + +Class decorators, on the other hand, are applied *after* the class has been created. Python is pretty dynamic, so for the most part, you can change things after the fact, but there are a few exceptions. The docstring, for instance is not mutable. + +Also, due to this difference in timing, an attribute added to a class by a metaclass can be overridden by the class -- but an attribute added by a class decorator will override the class' version, if it exists. That could get a bit ugly. + +Here is a bit of discussion of metaclasses vs decorators: + +`Decorators versus __metaclass__ `_ + +And another one: + +`A Study of Python's More Advanced Features Part III: Classes and Metaclasses `_ + +And this is a argument for class decorators by the author or the patch that enabled them (in Python 2.6): + +`Jack Diederich: Class Decorators: Radically Simple `_ + + +NameMangler Decorator Edition +----------------------------- + +For a simple example, let's see how to make NameMangler with a decorator. + +Here is the code: +:download:`mangler_dec.py ` + +It is well commented, but a couple of key points to consider: + +1) A class decorator takes a class object as an argument: + +.. code-block:: python + + def name_mangler(cls): + +2) As a class object, you can get its attribute dict (__dict__) with: + +.. code-block:: python + + attr_dict = vars(cls) + +3) Class attribute dictionaries are not writable, so you need to use + ``setattr()`` (and potentially ``delattr()``) to change the class + attributes. + + +json_save +========= + +For a more involved (and useful!) example, see the json_save package: + +:download:`json_save.zip ` + +It may also be in your class repo solutions dir: + +``solutions/metaprogramming/json_save/`` + +It is a system for saving and re-loading objects. + +It works a bit like the ORMs -- you specify what attributes you want to save, and what their types are. + +JSON +---- + +If you are not familiar with JSON: + +`JavaScript Object Notation (JSON) `_ is a format borrowed from the Web -- Javascript being the de-facto scripting language in browsers. It is a great format for communicating with browsers, but it has become a common serialization format for many other uses: it is simple, flexible, and human-readable and writable. + +It also maps pretty much directly to (some of) the core Python datatypes: lists, dictionaries, strings, and numbers. + +But it does not directly support more complex objects -- that is what json_save is all about. + +Metaclass json_save +------------------- + +The first solution uses a metaclass: ``json_save_meta.py`` + +It turns out that the metaclass part of the code is pretty simple and small. + +But there is a lot of other nifty magic with classes in there +-- so let's take a look: + + +Decorator json_save +------------------- + +The second solution uses a decorator: ``json_save_dec.py`` + +As in the metaclass case, the actual decorator is pretty simple. + +And it can use much of the code from the metaclass solution -- since not much really had anything specific to metaclasses. + +Let's take a look at that, too: + + diff --git a/_sources/modules/Modules.rst.txt b/_sources/modules/Modules.rst.txt new file mode 100644 index 0000000..cffdc50 --- /dev/null +++ b/_sources/modules/Modules.rst.txt @@ -0,0 +1,490 @@ +.. _modules_and_namespaces: + +####################################### +Code Structure, Modules, and Namespaces +####################################### + + +.. centered:: **How to get what you want when you want it** + + +Code Structure +============== + +In Python, the structure of your code is determined by whitespace. This is nicely clear, and you've probably already figured it out, but we'll formally spell it out here: + +How you *indent* your code determines how it is structured + +:: + + block statement: + some code body + some more code body + another block statement: + code body in + that block + end of "another" block statement + still in the first block + outside of the block statement + +The colon that terminates a block statement is also important... + +One-liners +---------- + +You can put a one-liner after the colon: + +.. code-block:: ipython + + In [167]: x = 12 + In [168]: if x > 4: print(x) + 12 + +But this should only be done if it makes your code *more* readable. And that is rare. + +So you need both the colon and the indentation to start a new a block. But the end of the indented section is the only indication of the end of the block. + +Spaces vs. Tabs +--------------- + +Whitespace is important in Python. + +An indent *could* be: + +* Any number of spaces +* A tab +* A mix of tabs and spaces: + +If you want anyone to take you seriously as a Python developer: + +.. centered:: **Always use four spaces -- really!** (`PEP 8 `_) + +.. note:: + If you *do* use tabs (and really, don't do that!) python interprets them as the equivalent of *eight* spaces. Text editors can display tabs as any number of spaces, and most modern editors default to four -- so this can be *very* confusing! so again: + +.. centered:: **Never mix tabs and spaces in Python code** + + +Spaces Elsewhere +---------------- + +Other than indenting -- space doesn't matter, technically. + +.. code-block:: python + + x = 3*4+12/func(x,y,z) + x = 3*4 + 12 / func (x, y, z) + +These will give the exact same results. + +But you should strive for proper style. Isn't this easier to read? + +.. code-block:: python + + x = (3 * 4) + (12 / func(x, y, z)) + + +.. centered:: **Read** `PEP 8 `_ **and install a linter in your editor.** + + +Modules and Packages +==================== + +Python is all about *namespaces* -- the "dots" + +``name.another_name`` + +The "dot" indicates that you are looking for a name in the *namespace* of the given object. It could be: + +* A name in a module +* A module in a package +* An attribute of an object +* A method of an object + +The only way to know is to know what type of object the name refers to. But in all cases, it is looking up a name in the namespace of the object. + +So what *are* all these different types of namespaces? + +Modules +------- + +A module is simply a namespace. But a module more or less maps to a file with python code in it. + +It might be a single file, or it could be a collection of files that define a shared API. + +But in the common and simplest case, a single file is a single module. + +So you can think of the files you write that end in ``.py`` as modules. + +When a module is imported, the code in that file is run, and any names defined in that file are now available in the module namespace. + +For a really simple example, if you have the following in the ``trivial.py`` file: + +.. code-block:: python + :linenos: + + x = 1 + y = 2 + + def do_nothing(a, b, c): + print("do_nothing was called with:", a, b, c) + + print("at the end of the trivial module") + +What do you think happens when you import that module? What will get printed? + +What names will be defined in that module? + +How would you access those names? + +Before running this code, think about it a bit. Recall what happens when you import a module: + +* The code is run in the module, top to bottom. +* The names defined in the module (its global namespace) are made available in the modules namespace. + +So: Lines 1-2 assign two names, ``x`` and ``y``. lines 4-5 define a function, named ``do_nothing``. Line 7 prints something. + +So: when run, there are three names defined, and one print function run. + +Now try it: + +.. code-block:: python + + >>> import trivial + at the end of the trivial module + +yes, we got that print function run. + +Let's see if the names are there: + +.. code-block:: python + + >>> trivial.x + 1 + >>> trivial.y + 2 + +.. code-block:: python + + + >>> trivial.do_nothing(3,4,5) + do_nothing was called with: 3 4 5 + +yes, there are, in the ``trivial`` namespace. + + +Packages +-------- + +A package is a module with other modules in it. + +On a filesystem, this is represented as a directory that contains one or more ``.py`` files, one of which **must** be called ``__init__.py``. The ``__init__.py`` file can be empty (and often is) -- but it must be there. + +When there is a package available, you can import only the package, or any of the modules inside it. When a package is imported, the code in the ``__init__.py`` file is run, and any names defined in that file are available in the *package namespace*. + +Here we define about the simplest package possible: + +Create a directory (folder) for your package: + +.. code-block:: bash + + mkdir my_package + +Save a file in that package, called ``__init__.py``, and put this in it: + +.. code-block:: python + + name1 = "Fred" + name2 = "Bob" + +Save another file in your my_package dir called ``a_module.py``, and put this in it: + +.. code-block:: python + + name3 = "Mary" + name4 = "Jane" + + def a_function(): + print("a_function has been called") + +You now have about the simplest package you can have. Make sure your current working dir is the dir that ``my_package`` is in, and start python or iPython. Then try this code: + +.. code-block:: ipython + + In [1]: import my_package + + In [2]: my_package.name1 + Out[2]: 'Fred' + + In [3]: my_package.name2 + Out[3]: 'Bob' + +The names you've defined are available in the package namespace. + +What about the module? + +.. code-block:: ipython + + In [4]: my_package.a_module + --------------------------------------------------------------------------- + AttributeError Traceback (most recent call last) + in () + ----> 1 my_package.a_module + + AttributeError: module 'my_package' has no attribute 'a_module' + +the ``a_module`` name does not exist. It must be imported explicitly: + +.. code-block:: ipython + + In [1]: import my_package.a_module + +Now the names defined in the ``a_module.py`` file are all there: + +.. code-block:: ipython + + In [2]: my_package.a_module.name3 + Out[2]: 'Mary' + + In [3]: my_package.a_module.name4 + Out[3]: 'Jane' + + In [4]: my_package.a_module.a_function() + a_function has been called + +Note that you can also put a package inside a package. So you can create arbitrarily deeply nested hierarchy of packages. This can be helpful for a large, complex collection of related code, such as an entire Web Framework. But from the *Zen of Python*: + + "Flat is better than nested." + +So don't overdo it -- only go as deep as you really need to to keep your code organized. + + +Importing modules +----------------- + +You have probably imported a module or two already: + +.. code-block:: python + + import sys + import math + +But there a handful of ways to import modules and packages. + +.. code-block:: python + + import modulename + +Is the simplest way: this adds the name of the module to the global namespace, and lets you access the names defined in that module: + +.. code-block:: python + + modulename.a_name_in_the_module + +If you want only a few names in a module, and don't want to type the module name each time, you can import only the names you want: + +.. code-block:: python + + from modulename import this, that + +This brings only the names specified (``this``, ``that``) into the global namespace. All the code in the module is run, but the module's name is not available. But the explicitly imported names are directly available. + +Sometimes you want the entire module, but maybe not want to type its entire name eadh time you use. So you can rename a module when you import it. (you may also want to do this if a module has the same name as a variable you want to use...) + +.. code-block:: python + + import modulename as a_new_name + +This imports the module, and gives it a new name in the global namespace. For example, the numpy package is usually imported as: + +.. code-block:: python + + import numpy as np + +Because numpy has a LOT of names, some of which may conflict with builtins or other modules, and users want to be able to reference them without too much typing. + +You can also import a name within a module and rename it at the same time: + +.. code-block:: python + + from modulename import this as that + +This imports only one name from a module, while also giving it a new name in the global namespace. + + +Examples +-------- + +You can play with some of this with the standard library: + +.. code-block:: ipython + + In [1]: import math + + In [2]: math.sin(1.2) + Out[2]: 0.9320390859672263 + + In [3]: from math import cos + + In [4]: cos(1.2) + Out[4]: 0.3623577544766736 + + In [5]: import math as m + + In [6]: m.sin(1) + Out[6]: 0.8414709848078965 + + In [7]: from math import cos as cosine + + In [8]: cosine(1.2) + Out[8]: 0.3623577544766736 + + +My rules of thumb +----------------- + +If you only need a few names from a module, import only those: + +.. code-block:: python + + from math import sin, cos, tan + +If you need a lot of names from that module, just import the module: + +.. code-block:: python + + import math + math.cos(2 * math.pi) + +Or import it with a nice short name: + +.. code-block:: python + + import math as m + m.cos(2 * m.pi) + +import \* ? +----------- + +.. centered:: **Warning:** + +You can also import all the names in a module with: + +.. code-block:: python + + from modulename import * + +But this leads to name conflicts, and a cluttered namespace. It is NOT recommended practice anymore. + + +Importing from packages +----------------------- + +Packages can contain modules, which can be nested -- ideally not very deeply. + +In that case, you can simply add more "dots" and follow the same rules as above. + +.. code-block:: python + + from packagename import my_funcs.this_func + +.. Here's a nice reference for more detail: + +.. http://effbot.org/zone/import-confusion.htm + +.. And + +:ref:`packaging` goes into more detail about creating (and distributing!) your own package. + + +What does ``import`` actually do? +--------------------------------- + +When you import a module, or a symbol from a module, the Python code is *compiled* to *bytecode*. + +The result is a ``module.pyc`` file. + +Then after compiling, all the code in the module is run **at the module scope** -- that is, in the namespace of the module. + +For this reason, it is good to avoid module-scope statements that have global side-effects. This includes things as simple as a ``print()`` -- it will only print the first time the module is imported. + + +Re-import +---------- + +The code in a module is *not* re-run when imported again. This makes it efficient to import the same module multiple places in a program. But it means that if you change the code in a module after importing it, that change will not be reflected when it is imported again. + +If you DO want a change to be reflected, you can explicitly reload a module: + +.. code-block:: python + + import importlib + importlib.reload(modulename) + +This is rarely needed (which is why it's a bit buried in the ``importlib`` module), but is good to keep in mind when you are interactively working on code under development. + +Import Interactions +------------------- + +Another key point to keep in mind is that all code files in a given python program are sharing the same modules. So if you change a value in a module, that value's change will be reflected in other parts of the code that have imported that same module. + +This can create dangerous side effects and hard to find bugs if you change anything in an imported module, but it can also be used as a handy way to store truly global state, like application preferences, for instance. + +A rule of thumb for managing global state is to have only *one* part of your code change the values, and everywhere else considers them read-only. You can't enforce this, but you can structure you own code that way. + +Let's take a look at an example of this. + +Create three modules (python files): + +``mod1.py``, ``mod2.py``, ``mod3.py`` + +``mod1.py`` is very simple -- one name declared: + +.. code-block:: python + + x = 5 + +``mod2.py`` is where a bit actually goes on: + +.. code-block:: python + + #!/usr/bin/env python3 + + import mod1 + + print(f"In mod2: mod1.x = {mod1.x}") + + input("pausing (hit enter to continue >") + + print("importing mod3") + + import mod3 + + print(f"Still in mod2: mod1.x = {mod1.x}") + + print("mod3 changed the value in mod1, and that change shows up in mod2") + +Here, we import ``mod1``, and we can now see the names defined in it, and print the value of ``x``. Then it pauses, waiting for input. After the user hits the key, it then imports ``mod3``, and again prints the value of ``x`` that is in ``mod1``. Let's now look at ``mod3.py``: + +.. code-block:: python + + import mod1 + + print("In mod3 -- changing the value of mod1.x") + + mod1.x = 555 + +Other than the print -- all ``mod3`` does is re-set the value of ``x`` that is on ``mod1``. +Running ``mod2.py`` results in:: + + $ python mod2.py + In mod2: mod1.x = 5 + pausing (hit enter to continue > + importing mod3 + In mod3 -- changing the value of mod1.x + Still in mod2: mod1.x = 555 + mod3 changed the value in mod1, and that change shows up in mod2 + +You can see that when ``mod2`` changed the value of ``mod1.x``, that changed the value everywhere that ``mod1`` is imported. You want to be very careful about this. + +If you are writing ``mod2.py``, and did not write ``mod3`` (or wrote it long enough ago that you don't remember its details), you might be very surprised that a value in ``mod1`` changes simply because you imported ``mod3``. This is known as a "side effect", and you generally want to avoid them! diff --git a/_sources/modules/MoreOnMutability.rst.txt b/_sources/modules/MoreOnMutability.rst.txt new file mode 100644 index 0000000..e3ccd6b --- /dev/null +++ b/_sources/modules/MoreOnMutability.rst.txt @@ -0,0 +1,182 @@ +.. _a_bit_on_mutability: + + +===================================== +A bit more on mutability (and copies) +===================================== + +mutable objects +---------------- + +We've talked about this: mutable objects can have their contents changed in place. + +Immutable objects can not. + +This has implications when you have a container with mutable objects in it: + +.. code-block:: ipython + + In [28]: list1 = [ [1,2,3], ['a','b'] ] + +one way to make a copy of a list: + +.. code-block:: ipython + + In [29]: list2 = list1[:] + + In [30]: list2 is list1 + Out[30]: False + +they are different lists. + + +What if we set an element to a new value? + +.. code-block:: ipython + + In [31]: list1[0] = [5,6,7] + + In [32]: list1 + Out[32]: [[5, 6, 7], ['a', 'b']] + + In [33]: list2 + Out[33]: [[1, 2, 3], ['a', 'b']] + +So they are independent. + + +But what if we mutate an element? + +.. code-block:: ipython + + In [34]: list1[1].append('c') + + In [35]: list1 + Out[35]: [[5, 6, 7], ['a', 'b', 'c']] + + In [36]: list2 + Out[36]: [[1, 2, 3], ['a', 'b', 'c']] + +uuh oh! mutating an element in one list mutated the one in the other list. + + +Why is that? + +.. code-block:: ipython + + In [38]: list1[1] is list2[1] + Out[38]: True + +The elements are the same object! + +This is known as a "shallow" copy -- Python doesn't want to copy more than it needs to, so in this case, it makes a new list, but does not make copies of the contents. + +Same for dicts (and any container type -- even tuples!) + +If the elements are immutable, it doesn't really make a differnce -- but be very careful with mutable elements. + + +The copy module +---------------- + +most objects have a way to make copies (``dict.copy()`` for instance). + +but if not, you can use the ``copy`` module to make a copy: + +.. code-block:: ipython + + In [39]: import copy + + In [40]: list3 = copy.copy(list2) + + In [41]: list3 + Out[41]: [[1, 2, 3], ['a', 'b', 'c']] + +This is also a shallow copy. + + +But there is another option: + +.. code-block:: ipython + + In [3]: list1 + Out[3]: [[1, 2, 3], ['a', 'b', 'c']] + + In [4]: list2 = copy.deepcopy(list1) + + In [5]: list1[0].append(4) + + In [6]: list1 + Out[6]: [[1, 2, 3, 4], ['a', 'b', 'c']] + + In [7]: list2 + Out[7]: [[1, 2, 3], ['a', 'b', 'c']] + +``deepcopy`` recurses through the object, making copies of everything as it goes. + + + +I happened on this thread on stack overflow: + +http://stackoverflow.com/questions/3975376/understanding-dict-copy-shallow-or-deep + +The OP is pretty confused -- can you sort it out? + +Make sure you understand the difference between a reference, a shallow copy, and a deep copy. + +Mutables as default arguments: +------------------------------ + +Another "gotcha" is using mutables as default arguments: + +.. code-block:: ipython + + In [11]: def fun(x, a=[]): + ....: a.append(x) + ....: print(a) + ....: + +This makes sense: maybe you'd pass in a specific list, but if not, the default is an empty list. + +But: + +.. code-block:: ipython + + In [12]: fun(3) + [3] + + In [13]: fun(4) + [3, 4] + +Huh?! + + +Remember that that default argument is defined when the function is created: there will be only one list, and every time the function is called, that same list is used. + + +**The solution:** + +The standard practice for such a mutable default argument: + +.. code-block:: ipython + + In [15]: def fun(x, a=None): + ....: if a is None: + ....: a = [] + ....: a.append(x) + ....: print(a) + In [16]: fun(3) + [3] + In [17]: fun(4) + [4] + +You get a new list every time the function is called + +For more reading. + +This: http://python.net/crew/mwh/hacks/objectthink.html#question + +Is a link to a discussion on comp.lang.python from over 15 years ago -- but the issues are still the same. In particular, Alex Martelli's answer is brilliant. + +Go read it.... + diff --git a/_sources/modules/MultipleInheritance.rst.txt b/_sources/modules/MultipleInheritance.rst.txt new file mode 100644 index 0000000..b834b4c --- /dev/null +++ b/_sources/modules/MultipleInheritance.rst.txt @@ -0,0 +1,428 @@ +.. _multiple_inheritance: + + +#################### +Multiple Inheritance +#################### + + +Inheriting from more than one class. +==================================== + +The mechanics of multiple inheritance +------------------------------------- + +Simply provide more than one parent: + +.. code-block:: python + + class Combined(Parent1, Parent2, Parent3): + def __init__(self, something, something else): + # some custom initialization here. + Parent1.__init__(self, ......) + Parent2.__init__(self, ......) + Parent3.__init__(self, ......) + # possibly more custom initialization + +Calls to the parent class ``__init__`` are optional and case dependent. (and maybe you can use ``super()``...stay tuned) + +The Combined class now has ALL the attributes and methods of the multiple parent classes. You can bring a lot of functionality into a class that way. + + +Purpose +------- + +What was the purpose behind inheritance? + +*Code reuse* + +What is the purpose behind multiple inheritance? + +*Code reuse* + +What wasn't the purpose of inheritance? + +*Building massive class hierarchies for their own sake* + +What isn't the purpose of multiple inheritance? + +*Building massive class hierarchies for their own sake* + +Mix-ins +------- + +Why would you want to do this? + +Hierarchies are not always simple: + +* Animal + + * Mammal + + * give_birth() + + * Bird + + * lay_eggs() + +Where do you put a Platypus or Spiny Anteater? + +`Egg Laying Mammals `_ + +"mix-ins" can solve this problem. A mix-in is a class that can't do anything by itself, but rather, provides functionality that can be mixed into other classes. + +In the above contrived example, we could put "give_birth" (and associated methods) in a BirthGiver mixin, and lay_eggs in an EggLayer mixin, and then make our mammals, birds and platypi from them: + +.. code-block:: python + + class Platypus(Animal, EggLayer): + ... + + class Cat(Animal, BirthGiver): + ... + + class Duck(Animal, EggLayer): + ... + +But this is pretty darn contrived ... where do you use these for real? + +Here is a nice discussion: + +`mixins in Python ... `_ + +(Ignore the second part about Ruby...) + +Real World Example: The wxPython FloatCanvas: +............................................. + +https://github.com/wxWidgets/Phoenix/blob/master/wx/lib/floatcanvas/FCObjects.py + +I had read about mixins quite a while ago, and I thought they were pretty cool. But I couldn't imagine where I might actually use them. + +Then I set out to write FloatCanvas -- a scalable, pan-able, object-persistent drawing canvas for the wxPython toolkit. + +What I discovered is that the draw objects were not in a clean hierarchy -- some objects had a line (like a poly line), some had just a fill (like a dot), some had a fill and outline (polygon), some were defined by a single point (a dot again), some by a bunch of points (polygon), etc.... + +In order to not write a lot of repeated code -- remember, "classes are for code re-use", I put all the individual bits of functionality into mixin classes, and then simply put them together in different ways. + +Once the system was set up, all you needed to write was a ``__init__`` and a draw method to make a whole new graphic object. + +Take a look at the code --quite a bit in the ``DrawObject`` base class, then a bunch of ``*Mixin`` classes that define specific functionality. + +Now look at the real DrawObject classes, e.g. Line and Polygon. Not much code there: + +.. code-block:: python + + class Polygon(PointsObjectMixin, LineAndFillMixin, DrawObject): + ... + + def __init__(self, + ... + + def _Draw(self, + ... + +and: + +.. code-block:: python + + class Line(PointsObjectMixin, LineOnlyMixin, DrawObject): + ... + + def __init__(self, + ... + + def _Draw(self, + ... + +There is some real code in the ``__init__`` and ``_Draw`` -- but those are still the only two methods that need to be defined to make a fully functional drawobject. + + +FloatCanvas has a lot of complications with handling mouse events, and managing pens and brushes, and what have you, so a very trimmed down version, using the Python Imaging Library, is here to check out and modify: + +:download:`object_canvas.py <../examples/multiple_inheritance/object_canvas.py>` + +and + +:download:`test_object_canvas.py <../examples/multiple_inheritance/test_object_canvas.py>` + +This code requires the Python Imaging Library to do the rendering. You can get it by installing the "pillow" package from PyPi:: + + python -m pip install pillow + +Can you add other types of ``DrawObjects`` ? Maybe a polygon or ?? + + +Python's Multiple Inheritance Model +=================================== + +Cooperative Multiple Inheritance + +Emphasis on cooperative! + +* Play by the rules and everybody benefits (parents, descendants). +* Play by the rules and nobody gets hurt (yourself, mostly). +* We're all adults here. + +What could go wrong? + +The Diamond Problem +------------------- + +In Python, everything is descended from 'object'. Thus, the moment you invoke multiple inheritance you have the diamond problem. + +https://en.wikipedia.org/wiki/Multiple_inheritance#The_diamond_problem + +Here is a toy Python example: + +:download:`diamond.py ` + +Take a look at that code -- run it, and notice that class ``A``'s method gets run twice. Make sure you know why it is doing what it is doing. + + +``super()`` +----------- + +``super()`` can help. + +``super()``: use it to call a superclass method, rather than explicitly calling the unbound method on the superclass. + +instead of: + +.. code-block:: python + + class A(B): + def __init__(self, *args, **kwargs) + B.__init__(self, *argw, **kwargs) + ... + +You can do: + +.. code-block:: python + + class A(B): + def __init__(self, *args, **kwargs) + super().__init__(*args, **kwargs) + ... + + +MRO: Method Resolution Order +---------------------------- + +How does python decide which method to call, when multiple superclasses may have the *same* method ? + +.. code-block:: python + + class Combined(Super1, Super2, Super3) + +Attributes are located bottom-to-top, left-to-right + +* Is it an instance attribute ? +* Is it a class attribute ? +* Is it a superclass attribute ? + + - Is it an attribute of the left-most superclass? + - Is it an attribute of the next superclass? + - and so on up the hierarchy... + +* Is it a super-superclass attribute ? +* ... also left to right ... + +http://python-history.blogspot.com/2010/06/method-resolution-order.html + + +Super's Superpowers +------------------- + +The above system is clear when the hierarchy is simple -- but when you have the "diamond problem" -- or even more compexity, we need somethign smarter. Enter ``super()``. + +``super`` works out -- dynamically at runtime -- which classes are in the delegation order. + +Do not be afraid. And be very afraid. + + +What does super() do? +---------------------- + +.. code-block:: python + + class ChildB(Base): + def __init__(self): + mro = type(self).mro() + for next_class in mro[mro.index(ChildB) + 1:]: # slice to end + if hasattr(next_class, '__init__'): + next_class.__init__(self) + break + +http://stackoverflow.com/questions/576169/understanding-python-super-with-init-methods + +``super`` returns a "proxy object" that delegates method calls. + +It's not returning the object itself -- but you can call methods on it as though it were a class object. + +It runs through the method resolution order (MRO) to find the method +you call. + +Key point: the MRO is determined *at run time* + +https://docs.python.org/3.6/library/functions.html#super + +But it's not as simple as finding and calling the first superclass method it finds: ``super()`` will call all the sibling superclass methods: + +Here is an example of a class that inherits from three superclasses: + +.. code-block:: python + + class D(C, B, A): + def __init__(self): + super().__init__() + +Since you have called __init__ on the ``super()`` object, this is essentially the same as calling all three super class ``__init__`` methods: + +.. code-block:: python + + class D(C, B, A): + def __init__(self): + C.__init__() + B.__init__() + A.__init__() + +Keep in mind that ``super()`` can be used for any method, not just ``__init__`` -- while you usually *do* want to initiallize all the superclasses, you may not want to call the same method on every superclass if it's a more specialized method. + +But if you do, it's kind of handy. + +.. Dependency Injection +.. -------------------- + +.. Super() is the right way to do dependency injection. + +.. https://en.wikipedia.org/wiki/Dependency_injection + +.. Compare with Monkey Patching as done in other languages. + +.. https://en.wikipedia.org/wiki/Monkey_patch + +.. This "Dependency_injection" works, because the MRO is defined at run time --ao anything you add to a superclass will take effect the moment it is there. + +.. Read Hettinger's "super() considered super" (below) to get an idea about that + + +Using ``super()`` +================= + +The rules: +---------- + +Raymond Hettinger's rules for ``super()`` + +1. The method being called by super() needs to exist + +2. The caller and callee need to have a matching argument signature + +3. Every occurrence of the method needs to use super() + +(1) Is pretty obvious :-) + +(2) We'll get into in a moment + +(3) This is a tricky one -- you just need to remember it. What it means is that, for instance, if you are using super() to call ``__init__`` in the superclass(es), then all the superclasses ``__init__`` methods must ALSO call it: + +.. code-block:: python + + def __init__(self, *args, **kwargs) + ... + super().__init__(*args, **kwargs) + ... + +Failure to do that will cause odd errors! + +This is a bit weird -- it means that if you have a method that may get called with a super call, it needs to use super itself, EVEN if it doesn't need to call the superclass' method! + +See the example later for this... + + +Matching Argument Signature +--------------------------- + +Remember that super does not only delegate to your superclass, it delegates to any class in the MRO. + +Therefore you must be prepared to call any other class's method in the hierarchy and be prepared to be called from any other class's method. + +The general rule is to pass all arguments you received on to the super function. + +That means that all the methods with the same name need to be able to accept the same arguments. In some cases, that's straightforward -- they are all the same. But sometimes it gets tricky. + +Remember that if you write a function that takes: + +``def fun(self, *args, **kwargs)`` + +It can accept ANY arguments. But if you find yourself needing to do that -- maybe super isn't the right thing to use?? + +But a really common case, particularly for an ``__init__``, is for it to take a bunch of keyword arguments. And a subclass may take one or two more, and then want to pass the rest on. So a common pattern is: + +.. code-block:: python + + class Subclass(Superclass): + def __init__(self, extra_arg1, extra_arg2, *args, **kwargs): + super().__init__(*args, **kwargs) + +Now your subclass doesn't really need to think about all the arguments the superclass can take. + +Two seminal articles +-------------------- + +"Super Considered Harmful" -- James Knight + +https://fuhm.net/super-harmful/ + +"Super() considered super!" -- Raymond Hettinger + +http://rhettinger.wordpress.com/2011/05/26/super-considered-super/ + +https://youtu.be/EiOglTERPEo + +Both perspectives worth your consideration. In fact, they aren't that different... + +Both actually say similar things: + +* The method being called by super() needs to exist +* Every occurrence of the method needs to use super(): + + - Use it consistently, and document that you use it, as it is part + of the external interface for your class, like it or not. + +If you follow these rules, then it really can be *super* + +Example: +-------- + +First, let's look at the diamond problem again -- this time using super: + +:download:`diamond_super.py ` + +in this case, we are using ``super()``, rather than specifically calling the methods of the superclasses: + +.. code-block:: python + + class D(B, C): + def do_your_stuff(self): + super().do_your_stuff() + print("doing D's stuff") + +And when we run it, we see that calling ``super().do_your_stuff()`` once in D results in the method being called on all the superclasses, with no duplication:: + + calling D's method + doing A's stuff + doing C's stuff + doing B's stuff + doing D's stuff + +Some more experiments with ``super`` +------------------------------------ + +``super`` takes a while to wrap your head around -- try running the code in: + +:download:`super_test.py ` + +See if you can follow all that! + diff --git a/_sources/modules/NamingThings.rst.txt b/_sources/modules/NamingThings.rst.txt new file mode 100644 index 0000000..12e72f6 --- /dev/null +++ b/_sources/modules/NamingThings.rst.txt @@ -0,0 +1,144 @@ +:orphan: + +.. _style_and_naming: + +################ +Style and Naming +################ + +.. centered:: **Style matters!** + +PEP 8 reminder +============== + +PEP 8 (Python Enhancement Proposal 8): + +https://www.python.org/dev/peps/pep-0008/ + +Is the "official" style guide for Python code. + +Strictly speaking, you only need to follow it for code in the standard library. + +But style matters -- consistent style makes your code easier to read and understand. + +And everyone in the community has accepted PEP 8 as *the* Python style guide. + +So **follow PEP 8** + +**Exception:** if you have a company style guide -- follow that instead. + +Try the "pycodestyle" module on your code:: + + $ python3 -m pip install pycodestyle + $ pycodestyle my_python_file + +Try it now -- really! + +Note that ideally you have a linter installed in your editor that yells at you if you violate PEP8 -- no need to run ``pycodestyle`` if it's already in your editor. + +See: :ref:`editor_for_python` for suggestions on editors and configuration. + +Naming Things... +================ + +It matters what names you give your variables. + +Python has rules about what it *allows*. + +PEP 8 has rules for style: capitalization, and underscores and all that. + +But you still get to decide within those rules. + +So use names that make sense to the reader. + +Naming Guidelines +----------------- + +Whenever possible, use strong, unambiguous names that relate to a concept in the business area applicable for your program. +For example, ``cargo_weight`` is probably better than ``item_weight``, ``current_fund_price`` is better than ``value``. But all of those are better than ``item``, or ``x``, or ... + +Only use single-letter names for things with limited scope: indexes and the like: + +.. code-block:: python + + for i, item in enumerate(a_sequence): + do_something(i, item) + +But **don't** use a name like "item", when there is a meaning to what the item is: + +.. code-block:: python + + for name in all_the_names: + do_something_with(name) + +Use plurals for collections of things: + +.. code-block:: python + + names = ['Fred', 'George', ...] + +And then singular for a single item in that collection: + +.. code-block:: python + + for name in names: + ... + +**Do** re-use names when the use is essentially the same, and you don't need the old one: + +.. code-block:: python + + line = line.strip() + line = line.replace(",", " ") + .... + +What about Hungarian Notation? +------------------------------ + +`Hungarian Notation `_ +is a naming system where the data type is part of the name: + +.. code-block:: python + + strFirstName = "Chris" + + listDonations = [400.0, 125.0, 1000.0] + + int_num_days = 30 + +This method is not recommended nor widely used in the Python community. + +One reason is Python's dynamic typing -- it really isn't important what type a value is, but rather, what it means. +And you may end up refactoring the code to use a different type, and then do you want to have to rename everything? +Or worse, the type in the name no longer matches the actual type in the code -- and that's really bad. I have seen code like this: + +.. code-block:: python + + strNumber = input("How many would you like?") + strNumber = int(strNumber) + + for i in range(strNumber): + ... + +So you have a name used for a string, then it gets converted to an integer, and the data type no longer matches the name. Wouldn't you be better off if that had never been named with the type in the first place? + +While widely used in some circles, it is generally considered bad style in the Python community -- so: + +.. centered:: **Do Not Use Hungarian Notation** + + +More About Naming Things +------------------------ + +Here's a nice talk about naming: + +`Jack Diederich: Name Things Once `_ + +One note about that talk -- Jack is mostly encouraging people to not use names that are too long and unnecessarily specific. +However, with beginners, it's often tempting to use names that are too *short* and *non-specific*, like "x" and "item" -- so you need to strike a balance, but absolutely: + +.. centered:: **Use Meaningful Names** + + + + diff --git a/_sources/modules/NoSQL.rst.txt b/_sources/modules/NoSQL.rst.txt new file mode 100644 index 0000000..52a65f5 --- /dev/null +++ b/_sources/modules/NoSQL.rst.txt @@ -0,0 +1,435 @@ +:orphan: + +.. _nosql: + +################ +No SQL Databases +################ + +"No SQL"? +========= + +Structured Query Language (SQL) is the standard language for communicating with relational database management systems (RDBMS). + +But an RDBMS system is not always the best way to store your data. + +There are other alternatives, each with there own approach, but as RDBMSs and SQL are so ubiquitous, they are all lumped in under the moniker "NoSQL". + +I personally hate things that are defined by what they are NOT, rather than what they are, but that's the terminology these days. + +What is a Database? +------------------- + +A database is an organized collection of data. The data are typically organized to model relevant aspects of reality in a way that supports processes requiring this information. + +Usually a way to persist and recover that organized data. + +These days, when you say "Database" almost everyone thinks "Relational Database", and SQL is the standard way to do that. + +SQL RDBMS systems are robust, powerful, scalable, and very well optimized. + +But: They require you to adapt the relational data model. + + +Non RDBMS options: +------------------ + +A key buzzword these days is "NOSQL" + +OK: They don't use SQL -- but what are they? + +Not one thing, but key features are mostly shared: + +* "schema less" + + - Document oriented + +* More direct mapping to an object model. + +* Highly Scalable + + - Easier to distribute / parallelize than RDBMSs + + +Database Schema +--------------- + +**Schema:** + +A database schema is the organization of data, and description of how a database is constructed: Divided into database tables, and relationships: foreign keys, etc... + +Includes what fields in which tables, what data type each field is, normalization of shared data, etc. + +This requires a fair bit of work up-front, and can be hard to adapt as the system requirements changes. + +It also can be a bit ugly to map your programming data model to the schema. + + +Schemaless +---------- + +Schemaless databases generally follow a "document model". + +Each entry in the database is a "document": + +* essentially an arbitrary collection of fields. +* often looks like a Python dict. + +Not every entry has to have exactly the same structure. + +Maps well to dynamic programming languages. + +Adapts well as the system changes. + + +NoSQL in Python: +---------------- + +Three Categories: + + +1. Simple key-value object store: +--------------------------------- + +- shelve +- anydbm +- Can store (almost) any Python object +- Only provides storage and retrieval + + +2. External NoSQL system: +------------------------- + +* Python bindings to external NoSQL system + +* Doesn't store full Python objects + +* Generally stores arbitrary collections of data (but not classes) + +* Can be simple key-value stores: + + - Redis, etc... + +* Or a more full featured document database: + + - In-database searching, etc. + + - mongoDB, etc... + +* "Graph" databases (:ref:`graph_databases`): + + - neo4j, etc. + +* Or a Map/Reduce engine: + + - Hadoop + + +3. Python Object Database: +-------------------------- + +* Stores and retrieves arbitrary Python objects. + + - Don't need to adapt your data model at all. + +* ZODB is the only robust maintained system (I know of) + +* ZODB is as close a match as you can get between the store and your code -- references and everything. + +http://blog.startifact.com/posts/older/a-misconception-about-the-zodb.html + +(note that that post says "it's been around for more than a decade", and it was written a decade ago!) + +Why a DB at all? +---------------- + +Reasons to use a database: + +- Need to persist the data your application uses + +- May need to store more data than you can hold in memory + +- May need to have multiple applications (or multiple instances) accessing the same data + +- May need to scale -- have the DB running on a separate server(s) + +- May need to access data from systems written in different languages. + + +ZODB +---- + +The Zope Object Data Base: A native object database for Python + +* Transparent persistence for Python objects + +* Full ACID-compatible transaction support (including savepoints) + +* History/undo ability + +* Efficient support for binary large objects (BLOBs) + +* Pluggable storages + +* Scalable architecture + +`ZODB `_ + + +MongoDB +-------- + +Document-Oriented Storage + + * JSON-style documents with dynamic schemas offer simplicity and power. + +Full Index Support + * Index on any attribute, just like you're used to. + +Replication & High Availability + * Mirror across LANs and WANs for scale and peace of mind. + +Auto-Sharding + * Scale horizontally without compromising functionality. + +Querying + * Rich, document-based queries. + +`MongoDB Web Site `_ + + +Other Options to Consider: +-------------------------- + +Redis: Advanced, Scalable key-value store. +( not well supported on Windows :-( ) + +- http://redis.io/ + +Riak: High availablity/scalablity (but not so good for small) + +- http://docs.basho.com/riak/latest/dev/taste-of-riak/python/ + +HyperDex: "Next generation key-value store" + +- http://hyperdex.org/ + +Apache Cassandra: A more schema-based NoSQL solution + +- http://pycassa.github.io/pycassa/ + +This is a nice page with a summary: + +- https://www.fullstackpython.com/no-sql-datastore.html + +(there are some good links to other resources on that page, too) + +An Example +========== + +The following are examples of using some of these systems to store some data. + +The Data Model +-------------- + +To store your data, you need to have a structure for the data -- this is the data model. For this example, we will build an Address Book with a not quite trivial data model. + +I'm a programmer first, and a database guy second (or third or...) so I start with the data model I want in the code. + +There are people:: + + self.first_name + self.last_name + self.middle_name + self.cell_phone + self.email + +There are households:: + + self.name + self.people + self.address + self.phone + +(similarly businesses) + +:download:`address_book_model.py ` + +Using ZODB +---------- + +ZODB stored Python objects. + +To make an object persistent (persistent should be installed with zodb): + +.. code-block:: python + + import persistent + + class Something(persistent.Persistent): + def __init__(self): + self.a_field = '' + self.another_field '' + +When a change is made to the fields, the DB will keep it updated. + + +Mutable Attributes +------------------- + +``Something.this = that`` will trigger a DB action + +But: + +``Something.a_list.append`` will not trigger anything. + +The DB doesn't know that that the list has been altered. + +Solution: + +``from persistent.list import PersistentList`` + +``self.a_list = PersistentList()`` + +(also ``PersistantDict()`` ) + +(or write getters and setters...) + +``Examples/nosql/address_book_zodb.py`` + +mongoDB +------- + +Essentially a key-value store, but the values are JSON-like objects. +(Actually BSON (binary JSON) ) + +So you can store any object that can look like JSON: + * dicts + * lists + * numbers + * strings + * richer than JSON. + +mongoDB and Python +------------------ + +mongoDB is written in C++ -- can be accessed by various language drivers. + +http://docs.mongodb.org/manual/applications/drivers/ + +For Python: ``PyMongo`` + +http://api.mongodb.org/python/current/tutorial.html + +To install the python api for mongoDB: + +``pip install pymongo`` - binary wheels available! + +There are also various tools for integrating mongoDB with Frameworks: + +* Django MongoDB Engine +* mongodb_beaker +* MongoLog: Python logging handler +* Flask-PyMongo +* others... + +Getting started with mongoDB +---------------------------- + +The mongoDB (database) is a separate program. Installers here: + +http://www.mongodb.org/downloads + +**NOTE:** mongo is also available as a service, with a free "sandbox" to try it out: + +https://www.mongodb.com/cloud/atlas + +Installing Mongo +................ + +Simple copy and paste install or use homebrew (at least on OS-X) + +Drop the files from ``bin`` into ``usr/local/bin`` or similar, or in your home dir somewhere you can find them. + +- I put it in a "mongo" dir in my home dir. Then added it to my PATH for now: + + - Editing ``~/.bash_profile``, and adding: + +:: + + # Adding PATH for mongo local install + PATH="~/mongo/bin:${PATH} + export PATH + +Anaconda Install +................ + +If you are using the Anaconda Python distribution (or miniconda) Mongo is available from conda:: + + conda install mongodb pymongo + + +Starting Mongo +.............. + +Create a dir for the database: + +``$ mkdir mongo_data`` + +And start it up: + +``$ mongod --dbpath=mongo_data/`` + +It will give you a bunch of startup messages, and then end by indicating which port it is listening on:: + + I NETWORK [initandlisten] waiting for connections on port 27017 + +So you know you can connect to it on port 27017 + +Creating a DB: +-------------- + +Make sure you've got the mongo drivers installed: + +pip install pymongo + +.. code-block:: python + + # create the DB + from pymongo import MongoClient + + client = MongoClient('localhost', 27017) + store = client.store_name # creates a Database + people = store.people # creates a collection + +Mongo will link to the given database and collection, or create new ones if they don't exist. + +Adding some stuff: + +.. code-block:: python + + people.insert_one({'first_name': 'Fred', + 'last_name': 'Jones'}) + +Pulling Stuff Out: +------------------ + +And reading it back: + +.. code-block:: ipython + + In [16]: people.find_one({'first_name':"Fred"}) + Out[16]: + {'_id': ObjectId('534dcdcb5c84d28b596ad15e'), + 'first_name': 'Fred', + 'last_name': 'Jones'} + +Note that it adds an ObjectID for you. + +:download:`/examples/nosql/address_book_mongo.py` + +and + +:download:`/examples/nosql/test_address_book_mongo.py` + +(or in the class repo in : ``examples/nosql``) diff --git a/_sources/modules/OO_vs_functional.rst.txt b/_sources/modules/OO_vs_functional.rst.txt new file mode 100644 index 0000000..0733437 --- /dev/null +++ b/_sources/modules/OO_vs_functional.rst.txt @@ -0,0 +1,84 @@ +.. _oo_vs_functional: + +######################################### +Object Oriented vs Functional Programming +######################################### + +Functional Programming is an alternative to Object Oriented Programming, which is to say that it takes a different perspective. As to a definition, that can be rather tricky. + +Definitions--by definition--are a statement of the exact meaning of a word: an exact statement or description of the nature, scope or meaning of something -- I looked it up. + +What we're talking about here is more akin to two constellations of related ideas. I tend to picture a three dimensional space with the Functional Programming cloud here and a separate Object Oriented cloud coalescing over here. + +There are ideas that are somewhere in the middle between the two, and those might represent either equally shared ideas or in some cases, when you drill into them, you might find different approaches to address one thing, one idea. + +Let's think of it in another way…. + +Programming paradigms are like human cultures +============================================= + +Programming paradigms coalesce around certain values and tend to have unique aesthetics. The people of these cultures generally need to solve the same problems, but they often find different solutions, or prefer one approach to another due to their differential weighting of cultural values. In some cases a given culture might not even recognize a problem that another culture considers among its first priorities to solve. + +Cultures can have variations and can mix, borrowing ideas from one another as they see fit. Let's consider an example. + +There is, arguably, a North American culture with Canada, Mexico and the United States interacting with each other in commerce, academics, politics and in the general exchange of ideas. However, the three clearly have distinct ways of solving problems. And then, within any one country, there are sub cultures and cross-cutting cultures. The Northwest of the United States for instance has a culture which is different from the culture in the Southwest. Indeed the Northwestern United States likely has more in common with British Columbia in Canada than it has with Florida or Alabama in the Southeastern US. + +This is all to say that to try to define--to actually nail down a definition--of Object Oriented Programming, Functional Programming, or of any other paradigm is perhaps a misguided errand. It is perhaps better to think of them as constellations of ideas or as rich cultures that help you think about solutions to the problems you're trying to solve with software. + +Objects! +======== + +Take one of the main ideas in OBJECT Oriented programs: Objects. + +In Python perhaps we talk and even think more often about classes (or types), but when we instantiate a class, when we make an instance of a class, we have an object. + +Functions! +========== + +Take one of the main ideas in FUNCTIONAL programming: Functions. + +Since we discuss objects and classes elsewhere, let's jump into functions. We’ll start with high school math. + +As you may recall, functions take arguments and return a value. The strict definition can be found on Wikipedia: + +https://en.wikipedia.org/wiki/Function_(mathematics) + +In mathematics, a function is a relation between a set of inputs and a set of permissible outputs with the property that each input is related to exactly one output. + +So functions take arguments and return a single, deterministic output, and for a given set of arguments the same value is always returned. + +Keep this definition in mind as we work through this material. + +Keep in mind also the fact that functions in Python are first-class language constructs. Likewise we’ll talk about what that means as we work our way through the material. + +Mutability, Immutability and State Management +--------------------------------------------- + +Think about functions and the stability they imply. One set of inputs, one result. Hand in hand with this, is the idea of immutability. + +When you started with Python you might have been all over lists like I was. You might have wondered: + + “Why would I use these relatively less flexible things called Tuples when I can use these highly flexible things called Lists?” + +Well, a part of the answer there is immutability. When you start to think functionally you start to think about state -- the management of state -- in a different way. You start to think that nailing things down can be a good thing. + +Control Flow versus Data Flow +----------------------------- + +Another idea that typically falls more into the functional camp than into the object oriented camp is data flow. What is data flow? If you come from a programming environment like Matlab, Octave or R you do it all the time. + +You’re mapping transformations en masse across entire sets of data and transforming your way to a solution. You’re doing mutations on Numpy arrays or Pandas DataFrames. You’re not focused as much on if/then/else constructs. It’s more like working toward a solution with a Rubik’s Cube. + +You’re thinking, “What series of transformations do I need to make on this collection of data, in order to get it into a state that represents a solution?” + +That's what functional programming is all about: + +* Immutable types +* First class functions +* Functions without side effects +* Data transformations + + + + + diff --git a/_sources/modules/ObjectOrientationOverview.rst.txt b/_sources/modules/ObjectOrientationOverview.rst.txt new file mode 100644 index 0000000..b3a574c --- /dev/null +++ b/_sources/modules/ObjectOrientationOverview.rst.txt @@ -0,0 +1,192 @@ +.. _object_orientation_overview: + +########################### +Object Orientation Overview +########################### + +In the Beginning there was the GOTO. + +... and in fact, there wasn't even that. + + +Programming Paradigms +===================== + +https://en.wikipedia.org/wiki/Programming_paradigm + +Software Design +--------------- + +Good software design is about code re-use, clean separation of concerns, +refactorability, testability, etc... + +OO can help with all that, but: + + * It doesn't guarantee it. + + * It can get in the way. + +What is Object Oriented Programming? + +| + "Objects can be thought of as wrapping their data + within a set of functions designed to ensure that + the data are used appropriately, and to assist in + that use" +| + +- http://en.wikipedia.org/wiki/Object-oriented_programming + + +**Even simpler:** + +"Objects are data and the functions that act on them in one place." + +This is the core of "encapsulation" + + +The Dominant Model +------------------ + +OO is the dominant model for the past couple decades, but it is not the only model, and languages such as Python increasingly mix and blend models (such as Procedural, Object Oriented, Functional). In Python, it is best to choose the approach that best solves your problem. + + +Object Oriented Concepts +------------------------ + +These are all terms you will hear when talking about Object Oriented Programming: + + +Class + A category of objects: particular data and behavior: for example, a "circle" (same as a "type" in Python). + +Instance + A particular object of a class: a specific circle. + +Object + The general case of an instance -- really any value (in Python anyway). This term is a bit overloaded -- it also is the generic term for any class. So a class is a particular kind of object. + +Attribute + Something that belongs to an object (or class): generally thought of + as a simple value, variable, or single object, as opposed to a ... + +Method + A function that belongs to a class. In Python, functions *are* semantically the same as any other type -- so all methods are "attributes", but not all attributes are methods. Methods are the functions, or more strictly speaking: the 'callable' attributes. + +Encapsulation + The approach where the details of the structure are "hidden" in a class -- the user of the class does not need to know how the data is stored (and may not be able to know...) + +Data Protection + This is the concept that classes can hide data from outside access (sometimes called "private" attributes. Python does not strictly support data protection. + +Class vs. Instance Attributes + Attributes can be attached to a class -- that is, shared by all instances of that class, or they can be attached to only that instance. + +Subclassing + Subclassing is making a special version of a class. It is a class itself, but it gets ("inherits") all the attributes and methods of its "parent" class. This makes it easy to re-use code. + +Overriding methods + When subclassing, the subclass inherits the methods of its parent class. But it can replace them as well, which is called overriding a method. + +Operator Overloading + Python (and most languages) have operators, like `+`, `-`. `*`, etc. Overloading an operator is a way to define what that operator means to a new class that is not originally part of the language. + +Polymorphism + Allowing instances of multiple classes to be used in the same way. Literally means "having many forms". This simply happens with Python's "Duck Typing" -- An object with a given method can have that method called on it. But in statically typed languages, this is a big deal. + + +Python and OO +------------- + +Is Python a "True" Object-Oriented Language? There is often debate about this. + +It does not support *proper* encapsulation, i.e., it does not require classes, and classes don't have "private" attributes. + +**but ...** + +Folks can't even agree on what OO "really" means. + +See: `The Quarks of Object-Oriented Development `_ +(Deborah J. Armstrong) + + +But Frankly, it's not really an important question -- better is: + +What are Python's strengths and weaknesses vis-a-vis OO? + +I think Python hits a sweet spot -- it does not constrain you to OO methods, but it does support all the important features for when they are useful. + + +Object Oriented Design +---------------------- + +There are many books (and web sites, and blog posts, and ...) about "Object Oriented Design", which is an approach to designing your program by starting with the "nouns" (objects) the program needs to manipulate. + +This may be a good approach for a "pure" OO language, but with Python it tends to lead to verbose, poorly performing code. + +So my recommendation is to think in terms of what makes sense for your project: + +.. centered:: **There is no single best paradigm for software design** + +One of the key guides to use of OO (and program design in general) are the core principles of: + +**Separation of Concerns:** If you find yourself writing a collection of functions that all work with the same data structure -- put them in a class together. + +**DRY (Don't Repeat Yourself):** If you find yourself repeating code -- see if you can use classes and inheritance to reduce code repetition. + +In practice, the best way to get the hang of it is practice -- as you write code, always think of how it might be easier to refactor it. + +Python's roots +-------------- + +| +| C +| C with Classes (aka C++) +| Modula2 +| + +You can do OO in C +------------------ + +Which today is not considered an OO Language. See the GTK+ project. + +So OO is really a design approach -- putting the data together with the functions that manipulate that data. It isn't defined by language features. + +That being said: OO languages give you some handy tools to make it easier +(and safer): + + * Polymorphism (duck typing gives you this) + * Inheritance + +are the big ones. + +You will need to understand OO +------------------------------ + +- It's a good idea for a lot of problems. + +- You'll need to work with OO packages. + +(Much of the standard library is object oriented). + + +If not OO Design, then what? +---------------------------- + +I like to take an incremental design approach: + +You start with your specification -- what your program has to **do** + +Then you start to create the data structures you need and the functions you need to manipulate that data. + +If you find yourself needing more than one function that is manipulating the same data -- you may need a class. + +It's almost that simple :-) + +You may also find that you need multiple "things" that have slightly different properties or behavior -- that is a case for subclassing. + +As you learn what is possible, this will all start to make more sense. + +So time to move on to how to actually **do** OO in Python! + +Here's how to do it in Python: :ref:`python_classes` diff --git a/_sources/modules/Packaging.rst.txt b/_sources/modules/Packaging.rst.txt new file mode 100644 index 0000000..9b5524a --- /dev/null +++ b/_sources/modules/Packaging.rst.txt @@ -0,0 +1,1217 @@ +.. _packaging: + +###################### +Packages and Packaging +###################### + +Packages, Modules, Imports, Oh My! +================================== + +Before we get started on making your own package -- let's remind +ourselves about packages and modules, and importing.... + +.. rubric:: Modules + +A python "module" is a single namespace, with a collection of values: + + * functions + * constants + * class definitions + * really any old value. + +A module usually corresponds to a single file: ``something.py`` + + +.. rubric:: Packages + +A "package" is essentially a module, except it can have other modules (and indeed other packages) inside it. + +A package usually corresponds to a directory with a file in it called ``__init__.py`` and any number +of python files or other package directories:: + + a_package + __init__.py + module_a.py + a_sub_package + __init__.py + module_b.py + +The ``__init__.py`` can be totally empty -- or it can have arbitrary python code in it. +The code will be run when the package is imported -- just like a module. + +Modules inside packages are *not* automatically imported. So, with the above structure:: + + import a_package + +will run the code in ``a_package/__init__.py``. Any names defined in the ``__init__.py`` will be available in:: + + a_package.a_name + +But:: + + a_package.module_a + +will not exist. To get submodules, you need to explicitly import them like so: + + import a_package.module_a + + +More on Importing +----------------- + +You usually import a module like this: + +.. code-block:: python + + import something + +or:: + + from something import something_else + +or a few names from a package:: + + from something import (name_1, + name_2, + name_3, + x, + y) + +You also can optionally rename stuff as you import it:: + + import numpy as np + +This is a common pattern for using large packages (maybe with long names) and not having to type a lot. + + +``import *`` +------------ + +:: + + from something import * + +Means: "import all the names in the module, "something". + +You really don't want to do that! It is an old pattern that is now an anti-pattern. + +But if you do encounter it, it doesn't actually import all the names -- it imports the ones defined in the module's ``__all__`` variable. + +``__all__`` is a list of names that you want ``import *`` to import. +So the module author can control it, and not accidentally override builtins or bring a lot of extraneous names into your namespace. + +But really, + +.. centered:: **Don't Use ``import *``** + + +Relative imports +---------------- + +Relative imports were added with PEP 328: + +https://www.python.org/dev/peps/pep-0328/ + +The final version is described here: + +https://www.python.org/dev/peps/pep-0328/#guido-s-decision + +This gets confusing! There is a good discussion on Stack Overflow here: + +`Relative Imports for the Billionth Time `_ + +Relative imports allow you to refer to other modules relative to where the existing module is in the package hierarchy, rather than in the entire python module namespace. For instance, with the following package structure:: + + package/ + __init__.py + subpackage1/ + __init__.py + moduleX.py + moduleY.py + subpackage2/ + __init__.py + moduleZ.py + moduleA.py + +You can do (in ``moduleX.py``): + +.. code-block:: python + + from .moduleY import spam + from . import moduleY + from ..subpackage1 import moduleY + from ..subpackage2.moduleZ import eggs + from ..moduleA import foo + from ...package import bar + from ...sys import path + +Similarly to command line shells: + +"." means "the current package" + +".." means "the package above this one" + +Note that you have to use the ``from`` form of import when using relative imports. + +(That's current *package*, not current *module*!) + + +.. rubric:: Caveats: + +* you can only use relative imports from within a package + +* you can not use relative imports from the interpreter + +* you can not use relative imports from a top-level script + (if ``__name__`` is set to ``__main__``. So the same python file with relative imports can work if it's imported, but not if it's run as a script) + + +The alternative is to always use absolute imports: + +.. code-block:: python + + from package.subpackage import moduleX + from package.moduleA import foo + +.. rubric:: Advantages of Relative Imports: + +* Package does not have to be installed + +* You can move things around, and not much has to change + +.. rubric:: Advantages of Absolute Imports + +* explicit is better than implicit +* imports are the same regardless of where you put the package +* imports are the same in package code, command line, tests, scripts, etc. + +There is debate about which is the "one way to do it" -- a bit unpythonic, but you'll need to make your own decision. + + +``sys.modules`` +--------------- + +``sys.modules`` is simply a dictionary that stores all the already imported modules. +The keys are the module names, and the values are the module objects themselves. + +.. note:: Remember that everything in Python is an object -- including modules. So they can be stored in lists and dict, assigned names, even passed to functions -- just like any other object. They are not often used that way, but they can be. + +.. code-block:: ipython + + In [3]: import sys + + In [4]: type(sys.modules) + Out[4]: dict + + In [6]: sys.modules['textwrap'] + Out[6]: + + In [10]: [var for var in vars(sys.modules['textwrap']) if var.startswith("__")] + Out[10]: + ['__spec__', + '__package__', + '__loader__', + '__doc__', + '__cached__', + '__name__', + '__all__', + '__file__', + '__builtins__'] + +You can access the module through the ``sys.modules`` dict: + +.. code-block:: ipython + + In [12]: sys.modules['textwrap'].__file__ + Out[12]: '/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/textwrap.py' + +Which is the same as: + +.. code-block:: ipython + + In [13]: import textwrap + + In [14]: textwrap.__file__ + Out[14]: '/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/textwrap.py' + + In [15]: type(textwrap) + Out[15]: module + + In [16]: textwrap is sys.modules['textwrap'] + Out[16]: True + +So, more or less, when you import a module, the interpreter: + +* Looks to see if the module is already in ``sys.modules``. + +* If it is, it binds a name to the existing module in the current module's namespace. + +* If it isn't: + + - A module object is created + - The code in the file is run + - The module is added to sys.modules + - The module is added to the current namespace. + +Implications of module import process: +-------------------------------------- + +* The code in a module only runs once per program run. +* Importing a module again is cheap and fast. +* Every place your code imports a module it gets the *same* object + - You can use this to share "global" state where you want to. + +* If you change the code in a module while the program is running -- the change will **not** show up, even if re-imported. + + - That's what ``importlib.reload()`` is for. + + +The module search path +---------------------- + +The interpreter keeps a list (``sys.path``) of all the places that it looks for modules or packages when you do an import: + +.. code-block:: python + + import sys + for p in sys.path: + print p + +you can manipulate that list to add or remove paths to let python find modules in a new place. + +Every module has a ``__file__`` name that points to the path it lives in. This lets you add paths relative to where you are, etc. + + .. note:: It's usually better to use setuptools' "develop" mode (or ``pip install -e``) instead of messing with ``sys.path`` -- see below. + +.. rubric:: Gotcha! + +One "gotcha" in Python is "name shadowing". The interpreter automatically adds the "current working directory" to ``sys.path``. This means you can start the interpreter and just ``import something`` to work with your code. But if you happen to have a python file, or package, in your current working directory that's the same as an installed package, then it will get imported instead, which can lead to some odd errors. If you are getting confusing errors on import -- check for python modules in your current working directory that may match an installed package! + + +Reloading +--------- + +Once loaded, a module stays loaded. + +If you import it again (usually in another module) it will simply use the version already there -- rather than re-running the code. + +And you can access all the already loaded modules from ``sys.modules``. ``sys.modules`` is a dict with the module names as the keys, and the module objects as the values + +.. code-block:: ipython + + In [4]: import sys + + In [5]: sys.modules.keys() + Out[5]: dict_keys(['builtins', 'sys', '_frozen_importlib', '_imp', '_warnings', '_thread', '_weakref', '_frozen_importlib_external', '_io', 'marshal', 'posix', 'zipimport', 'encodings', 'codecs', '_codecs' + +A lot there! + +There's no reason too -- but you could import an already imported module like so: + +.. code-block:: ipython + + In [10]: math = sys.modules['math'] + + In [11]: math.sin(math.pi) + Out[11]: 1.2246467991473532e-16 + + In [12]: math.sin(math.pi / 2) + Out[12]: 1.0 + + +Python Distributions +==================== + +So far, we've used the Python from python.org. It works great, and supports a lots of packages via pip. + +But there are also a few "curated" distributions. These provide python and a package management system for hard-to-build packages. + +Widely used by the scipy community: + +(lots of hard to build stuff that needs to work together...) + + * Anaconda (https://store.continuum.io/cshop/anaconda/) and `miniconda `_ + + * ActivePython (http://www.activestate.com/activepython) + +Conda has seen a LOT of growth in the last few years -- it's based on the open-source conda packaging system, and provides both a commercial curated set of packages, and a community-developed collection of packages known as conda-forge: + +https://conda-forge.org/ + +If you are doing data science or scientific development -- I recommend you take a look at Anaconda, conda and conda-forge. + + +Installing Packages +=================== + +Every Python installation has its own stdlib and ``site-packages`` folder. + +``site-packages`` is the default place for third-party packages. + + +From source +----------- + +* (``setup.py install`` ) + +* With the system installer (apt-get, yum, etc...) + + +From binaries +------------- + +* Binary wheels -- (More and more of those available) + +* ``pip`` should find appropriate binary wheels if they are there. + + +A bit of history: +----------------- + +In the beginning, there was the ``distutils``: + +But ``distutils`` is missing some key features: + +* package versioning +* package discovery +* auto-install + +- And then came ``PyPi`` + +- And then came ``setuptools`` (with easy_install) + +- But that wasn't well maintained... + +- Then there was ``distribute/pip`` + +- Which has now been merged back into ``setuptools`` + +Now it's pretty stable: pip+setuptools+wheel: use them. + +**warning** -- setuptools still provides easy_install, but it has mostly been deprecated, so you really want to use pip. And sometimes setuptools will invoke it for you under the hood by accident :-( + + +Installing Packages +------------------- + +Actually, it's still a bit of a mess + +But getting better, and the mess is *almost* cleaned up. + + +Current State of Packaging +-------------------------- + +To build packages: setuptools +............................. + + * https://setuptools.readthedocs.io/en/latest/ + +setuptools provides extensions to the build-in distutils: + +https://docs.python.org/3/library/distutils.html + +But there are a couple of those extensions that you really do need, so most folks use setuptools for everything. In fact, pip itself requires setuptools. + + +To install packages: pip +........................ + + * https://pip.pypa.io/en/latest/installing.html + +For binary packages: wheels +........................... + + * http://www.python.org/dev/peps/pep-0427/ + +(installable by pip) + + +Compiled Packages +----------------- + +Biggest issue is with compiled extensions: + + * (C/C++, Fortran, etc.) + + * You need the right compiler set up + +Dependencies: + + * Here's where it gets really ugly + + * Particularly on Windows + +Linux +..... + +Pretty straightforward: + +1. Is there a system package? + + * use it (apt-get install the_package) + +2. Try ``pip install``: it may just work! + +3. Install the dependencies, build from source:: + + python setup.py build + + python setup.py install + +(may need "something-devel" packages) + + +Windows +....... + +Sometimes simpler: + +1) A lot of packages have Windows wheels now. + + - Often installable with pip (pip will install a wheel for you if it exists) + - Usually for python.org builds + - Excellent source: http://www.lfd.uci.edu/~gohlke/pythonlibs/ + - Make sure you get 32 or 64 bit consistent + +2) But if no binaries: + + - Hope the dependencies are available! + - Set up the compiler + +Each version of Python requires a particular version of the MS Compiler: + + +`MS compiler versions `_ + +You can get the one for recent Pythons +`here `_. + + +OS-X +.... + +Lots of Python versions: + + - Apple's built-in (different for each version of OS) + - python.org builds + - 32+64 bit Intel (and even PPC still kicking around) + - Macports + - Homebrew + +Binary wheels are pretty much compatible between them -- yeah! + +If you have to build it yourself + +Xcode compiler (the right version) + + - Version 3.* for 32 bit PPC+Intel + + - Version > 4.* for 32+64 bit Intel + +(make sure to get the SDKs for older versions) + +If extra dependencies: + + - macports or homebrew often easiest way to build them + + +Final Recommendations +--------------------- + +First try: ``pip install`` + +If that doesn't work: + +Read the docs of the package you want to install + +Do what they say + +(Or use conda!) + + +virtualenv +---------- + +``virtualenv`` is a tool to create isolated Python environments. + +Very useful for developing multiple apps + +Or deploying more than one app on one system + +http://www.virtualenv.org/en/latest/index.html} + +You can find some additional notes here: :ref:`virtualenv_section` + +**NOTE:** conda also provides a similar isolated environment system. + + +Building Your Own Package +========================= + +The term "package" is overloaded in Python. As defined above, it means a collection of python modules. But it often is used to refer to not just the modules themselves, but the whole collection, with documentation and tests, bundled up and installable on other systems. + +Here are the very basics of what you need to know to make your own package. + + +Why Build a Package? +-------------------- + +There are a bunch of nifty tools that help you build, install and +distribute packages. + +Using a well structured, standard layout for your package makes it +easy to use those tools. + +Even if you never want to give anyone else your code, a well +structured package eases development. + + +What is a Package? +------------------ + +**A collection of modules** + +* ... and the documentation + +* ... and the tests + +* ... and any top-level scripts + +* ... and any data files required + +* ... and a way to build and install it... + + +Python packaging tools: +----------------------- + +The ``distutils``:: + + from distutils.core import setup + +Getting klunky, hard to extend, maybe destined for deprecation... + +You really need to use ``setuptools`` these days, which fortunatly has a similar API: :: + + from setuptools import setup + +``pip``: for installing packages + +``wheel``: for binary distributions + +These are pretty much the standard now -- very well maintained by: + +"The Python Packaging Authority" -- `PaPA `_ + +This all continues to change quickly, see that site for up to date information. + + +Where do I go to figure this out? +--------------------------------- + +This is a really good guide: + +Python Packaging User Guide: + +https://packaging.python.org/ + +and a more detailed tutorial: + +http://python-packaging.readthedocs.io/en/latest/ + +**Follow one of them** + +There is a sample project here: + +https://github.com/pypa/sampleproject + +(this has all the complexity you might need...) + +You can use this as a template for your own packages. + +Here is an opinionated update -- a little more fancy, but some good ideas: + +https://blog.ionelmc.ro/2014/05/25/python-packaging/ + +Rather than doing it by hand, you can use the nifty "cookie cutter" project: + +https://cookiecutter.readthedocs.io/en/latest/ + +And there are a few templates that can be used with that. + +The core template written by the author: + +https://github.com/audreyr/cookiecutter-pypackage + +And one written by the author of the opinionated blog post above: + +https://github.com/ionelmc/cookiecutter-pylibrary + +Either are great starting points. + +.. note:: One confusion for folks new to this is that a LOT of the documentation (and tools) around packaging for Python assumes that you are writing a package that is generally useful, and you want to share it with others on PyPi. That is partly because all the people developing the tools and writing about them are doing just that. It's also harder to distribute a package properly than to simply make one for internal use, so more tools and docs are needed. But it is still useful to make a package of your code if you aren't going to distribute it, but you don't need to do everything that is recommended. See: `A Package Just for You `_ for a really simple way to do the basics. + + +Basic Package Structure: +------------------------ + +:: + + package_name/ + bin/ + CHANGES.txt + docs/ + LICENSE.txt + MANIFEST.in + README.txt + setup.py + package_name/ + __init__.py + module1.py + module2.py + test/ + __init__.py + test_module1.py + test_module2.py + + +``CHANGES.txt``: log of changes with each release + +``LICENSE.txt``: text of the license you choose (do choose one!) + +``MANIFEST.in``: description of what non-code files to include + +``README.txt``: description of the package -- should be written in ReST (for PyPi): + +(http://docutils.sourceforge.net/rst.html) + +(those are all "metadata" critical if you are distributing to the world -- not so much for your own use) + +``setup.py``: ``distutils``/``setuptools`` script for building/installing the package. + +``bin/``: This is where you put top-level scripts + + ( some folks use ``scripts`` ) + +``docs/``: the documentation + +``package_name/``: The main package -- this is where the code goes. + +``test/``: your unit tests. Options here: + +Put it inside the package -- this results in the tests getting isntalled with the package, so they can be run after installation, with:: + + $ pip install package_name + >> import package_name.test + >> package_name.test.runall() + +or :: + + $ pytest --pyargs package_name + + +Or, if you have a lot of tests, and do not want the entire set installed with the package, you can keep it at the top level. + +Some notes on that: `Where to put Tests `_ + + +The ``setup.py`` File +--------------------- + +Your ``setup.py`` file is what describes your package, and tells the setuptools how to package, build, and install it + +It is python code, so you can add anything custom you need to it. + +But in the simple case, it is essentially declarative. + +``http://docs.python.org/3/distutils/`` + +An example: +........... + +.. code-block:: python + + from setuptools import setup + + setup( + # the critical stuff + name='PackageName', + packages=['package_name', 'package_name.test'], + scripts=['bin/script1','bin/script2'], + + # the good to have stuff: particularly if you are distributing it + version='0.1.0', + author='An Awesome Coder', + author_email='aac@example.com', + url='http://pypi.python.org/pypi/PackageName/', + license='LICENSE.txt', + description='An awesome package that does something', + long_description=open('README.txt').read(), + install_requires=[ + "Django >= 1.1.1", + "pytest", + ], + ) + + +``setup.cfg`` +-------------- + +**NOTE:** this is usually a pretty advanced option -- simple packages don't need this. + +``setup.cfg`` provides a way to give the end user some ability to customize the install + +It's an ``ini`` style file:: + + [command] + option=value + ... + +simple to read and write. + +``command`` is one of the distutils commands (e.g. build, install) + +``option`` is one of the options that command supports. + +Note that an option spelled ``--foo-bar`` on the command-line is spelled f``foo_bar`` in configuration files. + + +Running ``setup.py`` +-------------------- + +With a ``setup.py`` script defined, setuptools, along with pip, can do a lot: + +* builds a source distribution (a tar archive of all the files needed to build and install the package):: + + python setup.py sdist + +* builds wheels:: + + ./setup.py bdist_wheel + +(you need the wheel package for this to work: ``pip install wheel``) + +* build from source:: + + python setup.py build + +* and install:: + + python setup.py install + +or:: + + pip install . + +(the dot means "this directory" -- pip will look in the current dir for a ``setup.py`` file) + +* install in "develop" or "editable" mode:: + + python setup.py develop + +or:: + + pip install -e . + +.. note:: setuptools can be used by itself to build and install packages. But over the years, pip has evolved to a more "modern" way of doing things. When you install from source with pip -- it is using setuptools to do the work, but it changes things around, and installs things in a more modern, up to date, and compatible way. For much use, you won't notice the difference, but it setuptools still has some old crufty ways of doing things, so it's better to use pip as a front end as much as possible. + +setuptools +----------- + +``setuptools`` is an extension to ``distutils`` that provides a number of extensions:: + + from setuptools import setup + +superset of the ``distutils setup`` + +This buys you a bunch of additional functionality: + + * auto-finding packages + * better script installation + * resource (non-code files) management + * **develop mode** + * a LOT more + +In fact, virtually all python packages use setuptools these days, and there is currently discussion of deprecating distutils, and making setuptools "official". So you really want to use it. + +http://pythonhosted.org//setuptools/ + +wheels +------ + +Wheels are a binary format for packages. + +http://wheel.readthedocs.org/en/latest/ + +Pretty simple, essentially a zip archive of all the stuff that gets installed, i.e. put in ``site-packages``. + +Can be just pure python or binary with compiled extensions + +Compatible with virtualenv. + +Building a wheel:: + + python setup.py bdist_wheel + +``pip install packagename`` will find wheels for Windows and OS-X and "manylinux" + +``pip install --no-use-wheel`` avoids that, and forces a source install. + +manylinux +--------- + +There are a lot of Linux distributions out there. So for a long time, there were not easily available binary wheels for Linux -- how could you define a standard with all the Linux distros out there? + +Enter "manylinux" -- no one thinks you can support all Linux distros, but it was found that you could support many of the common ones, by building on an older version and restricting system libraries. This approach worked well for Canopy and conda, so PyPi adopted a similar strategy with manylinux: + +https://github.com/pypa/manylinux + +So there are now binary wheels for Linux on PyPi. + +The core scipy stack is a great example -- you can now ``pip install numpy`` on all three systems easily with pip. + +PyPi +----- + +The Python package index: + +https://pypi.python.org/pypi + +You've all used this -- ``pip install`` searches it. + +To upload your package to PyPi:: + + python setup.py register + + python setup.py sdist bdist_wheel upload + +http://docs.python.org/2/distutils/packageindex.html + +NOTE: only do this if you really want to share your package with the world! + + +Under Development +------------------ + +Develop mode (or "editable install") is *really* *really* nice:: + + $ python setup.py develop + +or:: + + $ pip install -e ./ + +(the e stands for "editable" -- it is the same thing) + +It puts links into the python installation to your code, so that your package is installed, but any changes will immediately take effect. + +This way all your test code, and client code, etc, can all import your package the usual way. + +No ``sys.path`` hacking + +Good idea to use it for anything more than a single file project. + +(requires ``setuptools``) + +Running tests +------------- + +It can be a good idea to set up your tests to be run from ``setup.py`` + +So that you (or your users) can: + +.. code-block:: bash + + $ pip install . + $ python setup.py test + +**Note:** there is debate about whether this is a good idea. But if you want to: + +To do this, you need to add a ``test_suite`` stanza in setup.py. + +**pytest** + +.. code-block:: python + + setup( + #..., + setup_requires=['pytest-runner', ...], + tests_require=['pytest', ...], + #..., + ) + +And create an alias into setup.cfg file:: + + [aliases] + test=pytest + +https://pytest.org/latest/goodpractices.html#integrating-with-setuptools-python-setup-py-test-pytest-runner + +This may not be required, as pytest will also let you run the tests installed with a package with:: + + pytest --pyargs package_name + + +**unittest** + +.. code-block:: python + + test_suite="tests" + + +Handling the version number: +---------------------------- + +One key rule in software (and ANY computer use!): + +Never put the same information in more than one place! + +With a python package, you want: + +.. code-block:: python + + import the_package + + the_package.__version__ + +To return the version string -- something like: + +"1.2.3" + +Using ``__version__`` is not a requirement, but it is a very commonly used convention -- *use it*! + +But you also need to specify it in the ``setup.py``: + +.. code-block:: python + + setup(name='package_name', + version="1.2.3", + ... + ) + +Not Good. + +My solution: +............ + +Put the version in the package __init__ + +__version__ = "1.2.3" + +In the setup.py, you could import the package to get the version +number ... but it's not a safe practice to import your package when installing +it (or building it, or...) + +So: read the ``__version__`` string yourself with code like: + +.. code-block:: python + + def get_version(): + """ + Reads the version string from the package __init__ and returns it + """ + with open(os.path.join("capitalize", "__init__.py")) as init_file: + for line in init_file: + parts = line.strip().partition("=") + if parts[0].strip() == "__version__": + return parts[2].strip().strip("'").strip('"') + return None + +**Alternative:** + +You can have a script that automatically updates the version number in whatever places it needs to. For instance: + +https://pypi.python.org/pypi/bumpversion + +Though I think it's better to have the version set in the code itself. + + +Semantic Versioning +------------------- + +Another note on version numbers. + +The software development world (at least the open-source one...) has +established a standard for what version numbers mean, known as semantic +versioning. This is helpful to users, as they can know what to expect +they upgrade. + +In short, with a x.y.z version number: + +x is the Major Version -- it could mean changes in API, major features, etc. + + - Likely to to be incompatible with previous versions + +y is the Minor Version -- added features, etc, that are backwards compatible. + +z is the "Patch" Version -- bug fixes, etc. -- should be fully compatible. + +Read all about it: + +http://semver.org/ + + +Tools to help: +-------------- + +Tox: + +https://tox.readthedocs.io/en/latest/ + +Versioneer: + +https://github.com/warner/python-versioneer + +Dealing with data files +----------------------- + +Oftentimes a package will require some files that are not Python code. In that case, you need to make sure the files are included with the package some how. + +There are a few ways to do this: + +http://setuptools.readthedocs.io/en/latest/setuptools.html#including-data-files + +The simplest option: package_data +.................................. + +I personally like the simplest one with the least magic: + +.. code-block:: python + + setup( + ... + package_data={'pkg_name': ['data/datatfile1', + 'data/datafile2']}, + ... + ) + +This is a dict with the keys being the package(s) you want to add data files to. This is required, as a single setup command can install more than one package. The value(s) is a list of filenames, *relative to the package* - note that in the above example, the "pkg_name" is not part of the path to the file. + +WARNING: For some reason, setuptools does not give you an error or warning if it can't find the files you specify -- which is a real shame - makes it harder to debug. + +https://packaging.python.org/tutorials/distributing-packages/#package-data + +Then you'll get the data file included in the package in the same place relative to your code regardless of how (or whether) it is installed. + +.. note:: Debugging package building can be kind of tricky: if you install the package, and it doesn't work, what went wrong?!? One approach that can help is to "build" the package, separately from installing it. setuptools provides a build command: ``python setup.py build`` that does just that. It will create a ``build`` directory, and in there, a ``lib`` dir. In there is what will actually get installed -- your "built" package. So you can look there and see if your data files are getting included, and everything else about the package. + +Now you'll need to write your code to find that data file. You can do that by using the ``__file__`` module attribute -- then the location of the data file will be relative to the ``__file__`` that your code is in. A little massaging with a ``pathlib.Path`` should do it. Putting the path to the data directory in the package's ``__init__.py`` provides a way for the rest of your code to find it. + +In ``pkg_name/__init__.py``: + +.. code-block:: python + + from pathlib import Path + + data_dir = data_file = Path(__file__).parent / "data" + +Now you can get that dir anywhere else in your code: + +.. code-block:: python + + from pkg_name import data_dir + + +More complex option: data_files +............................... + +Using the data_files setup option lets you put data files outside your package. They will get installed into the path of ``sys.prefix``, so you can find them in your code with a path relative to ``sys.prefix``. + +However, this means that the location of the files is different depending on whether the code is properly installed or not. And develop mode (or editable mode) does NOT install the data files. + +I honestly can't think of any reason to do this. + +https://packaging.python.org/tutorials/distributing-packages/#data-files + +More magic: pkg_resources +......................... + +setuptools provides a pkg_resources system to access resources (such as data files) of the packages. It is a complex (and I think ugly) system, with lots of features. + +http://setuptools.readthedocs.io/en/latest/pkg_resources.html + +But for just using it to find data files, it has some advantages -- the primary one that is it can find data files that are inside zipped-up packages. (Python can import modules from zip files, but you can't easily read data files from inside a zipped package) + +To use pkg_resources, you include the files with ``package_data`` in setup.py but access them with the pkg_resources API: + +.. code-block:: python + + from pkg_resources import resource_string + foo_config = resource_string(__name__, 'foo.conf') + +http://setuptools.readthedocs.io/en/latest/pkg_resources.html#resourcemanager-api + +Command line scripts +-------------------- + +The "easy" and traditional way to isntall command line scripts is with the ``scripts`` keyword argument to the ``setup()`` command:: + + + setup(... + ... + scripts = ["bin/a_script.py"] + ... + ) + +This works well on Unix systems (including the mac), but is not as reliable on Windows. All it really does is put a slightly altered copy of the script on PATH -- so it will work if it is named with the ``.py`` extension and the system is set up to run ``.py`` files. + +entry points +............ + +A more complicated, but better maintained and robust way is to use setuptools "entry points". Entry points can provide a number of functions, but one of them is to make console scripts. Also an argument to ``setup()``, It is done like so:: + + setup( + ... + entry_points = { + 'console_scripts': ['script_name=package_name.module_name:main'], + } + ... + ) + +What this does is tell setuptools to make a little wrapper program called "script_name" that will start up python, and run the function called ``main`` in the package.module module. + + +Getting Started With a New Package +---------------------------------- + +For anything but a single-file script (and maybe even then): + +1. Create the basic package structure + +2. Write a ``setup.py`` + +3. ``pip install -e .`` + +4. Put some tests in ``package/test`` + +5. ``pytest`` in the test dir, or ``pytest --pyargs package_name`` + +or use "Cookie Cutter": + +https://cookiecutter.readthedocs.io/en/latest/ + + + +LAB: A Small Example Package +---------------------------- + +* Create a small package + + - package structure + + - ``setup.py`` + + - ``python setup.py develop`` + + - ``at least one working test`` + + + +* Here is a ridiculously simple and useless package to use as an example: + +:download:`capitalize.zip <../examples/packaging/capitalize.zip>` + +Or go straight to making a package of your mailroom project. + + diff --git a/_sources/modules/Pep8.rst.txt b/_sources/modules/Pep8.rst.txt new file mode 100644 index 0000000..c217450 --- /dev/null +++ b/_sources/modules/Pep8.rst.txt @@ -0,0 +1,366 @@ +:orphan: + +.. _pep8: + + +######################## +Coding Style and Linting +######################## + +**Readablity Counts** + +It really is worth it to have consistently styled code! + + +pep-8 +===== + +Style Guide for Python + +You should all be familiar with this by now -- but we're going to hammer it home anyway! + +Defines a good baseline for your own local style guide + +The 'rules' are just suggestions. From the document: + + *Most importantly: know when to be inconsistent -- sometimes the + style guide just doesn't apply* + +and: + + *A Foolish Consistency is the Hobgoblin of Little Minds* + + + +Important pep8 Recommendations +------------------------------ + +Tabs or Spaces +.............. + +This one is pretty much non-negotiable: + +- https://www.python.org/dev/peps/pep-0008/#tabs-or-spaces + +- Use 4 space indents. The tabs vs. spaces wars will likely never + completely come to peaceful conclusion. But the Python community has + standardized on 4 space indents. Unless you have compelling reasons + not to, you should too. + +- Python 3 disallows mixing of tabs and spaces. With Python 2, you can + run with the -tt flag which will force errors on mixed use of tabs + and spaces. + +- Let your editor help you by having it insert 4 spaces for the tab + key, turning on visible whitespace markers, and/or integrating a + PEP-8 plugin + +- If your editor doesn't do this right -- **Fix It** + +Line Length +........... + +- https://www.python.org/dev/peps/pep-0008/#maximum-line-length + +- Maximum line length of 79 characters + +- This is an easy one to let run away early. If 79 characters is too + short, find a line length your team can agree on and stick to it as + specified in PEP-8 -- I like 95 characters or so. + + + +Encoding +........ + +- https://www.python.org/dev/peps/pep-0008/#source-file-encoding + +- The default encoding in Python 2 is ASCII, in Python 3 it is UTF-8. + UTF-8 has become ubiquitous enough that you probably haven't noticed. + But do check your editor at some point -- or if something weird happens. + +- If you insert a non encodable character in a string literal, the + interpreter will throw a SyntaxError when it reaches it. + +- For py2, if you want to use non-ascii encoding, UTF-8 for example, insert + + :: + + # coding: utf-8 + + near the top of the file. + +Comparing to Singletons +....................... + +- When comparing with singletons such as None, use ``x is None``, not ``x == None``. `Why? `_ ( also ``x is True`` and ``x is False``) + + +Naming Conventions +------------------ + +- variables, attributes, and modules names should be lowercase, with + words separated by underscores + +- class names should be CamelCase, aka StudlyCaps + +- constants should be ALL CAPS + + +Argument Conventions +-------------------- + +- Instance methods should have a first argument called self +- Class methods should have a first argument called cls +- There's nothing magic about these names. You don't have to use this + convention, but not adopting it will likely confuse future readers + +Imports +------- + +- `imports `_ + should be near the top of the file + +- imports should be grouped in the following order, separated by a + blank line: + + #. standard library imports + #. related third party imports + #. local application/library specific imports + +- avoid wildcard imports (``import *``) to keep the namespace clean for + both humans and automated tools + + +Public and Non-public Class Members +----------------------------------- + +Python does not have a mechanism for restricting access to a variable or method, but it does have culture and convention. + +The default is public. This works for most things. + +If you do not want people to change your attribute or method, and especially if you do not want people to depend on this attribute or method always remaining the same, make it private by using a single underscore in front. + +``_my_private_method`` + +If you are particularly paranoid: A non-public attribute has two leading underscores and no trailing underscores and triggers Python's name mangling. + +``__my_paranoid_method`` + +Continuing long lines +--------------------- + +PEP-8 is pretty flexible about how to continue long lines: + +https://www.python.org/dev/peps/pep-0008/#indentation + +But I'm kind of opinionated, so: + +Lots of Arguments or Parameters: +................................ + +If your function defintion or call can fit on one line, great: + +.. code-block:: python + + def fun(arg1, arg2, arg3=None): + some_code + +but if not: + +.. code-block:: python + + def fun(arg1, arg2, arg3, arg4=None, kwargument="a_default", kwargument2="some other value", yet_another=something, **kwargs): + some_code + +Then you need to break it. You can break it after a few arguments, when you run out of space, but I find that very hard to read. So -- if they don't all fit on one line, put them each on their own line: + +.. code-block:: python + + def fun(arg1, + arg2, + arg3, + arg4=None, + kwargument="a_default", + kwargument2="some other value", + yet_another=something, + **kwargs + ): + some_code + +Isn't that easier to read? + +Tools to help +------------- + +- `pyflakes `__ - searches for + bugs, but without importing modules + +- `Pylint `__ - style guide, searches for bugs + +- `pycodestyle `__ - tests conformance to + PEP-8 + +- `flake8 `__ combines pyflakes, + pycodestyle, and mccabe, a code complexity analyzer + + +pylint +------ + +Interesting options: + +:: + + -d (msg ids), --disable=(msg ids) Disable the messages identified in the messages table + --generate-rcfile/--rcfile Saves/restores a configuration + +Poor code example: + +:download:`listing1.py ` + +was adapted from `Doug Hellman `_ + +What can you spot as an error, bad practice, or poor style? + +Now see what pylint listing1.py has to say: + +.. code-block:: bash + + $ pip install pylint + + $ pylint listing1.py + + +pyflakes +-------- + +Doesn't check style, just checks for functional errors, but does not run code. + +Now see what pyflakes listing1.py has to say + +.. code-block:: bash + + $ pip install pyflakes + + $ pyflakes listing1.py + +How much overlap with pylint? + +pycodestyle +----------- + +Used to be called "pep8" -- but Guido didn't like that it gave a tool apparent authority -- so they changed it. + +Only checks style + +Interesting options: + +:: + + --statistics count errors and warnings + --count print total number of errors and warnings to standard error and set exit code to 1 if total is not null + +Now see what pycodestyle listing1.py has to say + +.. code-block:: bash + + $ pip install pycodestyle + + $ pycodestyle listing1.py + +What's the overlap in pycodestyle's output versus the other two tools? + +flake8 +------ + +A tool which wraps pycodestyle, pyflakes, and mccabe + +`mccabe `_ +is a "microtool" written by Ned Batchelder (author of coverage) for +assessing +`Cyclomatic Complexity `__ + +Interesting options: + +:: + + --max-complexity=N McCabe complexity threshold + +Now see what flake8 listing1.py has to say + +.. code-block:: bash + + $ pip install pycodestyle + + $ pycodestyle listing1.py + +What's the overlap in flake8 output versus the other tools? + +Give them a try on your own code -- mailroom? + +skipping particular lines +------------------------- + +Each of the tools has a way to mark particular lines to be ignored. + +For instance, flake8 has the ``# noqa`` marker. It's a comment as far as Python is concerned, but flake8 will skip that line if you mark it that way: + +.. code-block:: python + + def functionName(self, int): + local = 5 + 5 # noqa + module_variable = 5*5 + return module_variable + +This can be very nice to make the linter in your editor stop bugging you, and even nicer if you have an automated linter running -- like on a CI system. + +Analyzing a large codebase in the wild +--------------------------------------- + +It can be instructive to see what happens if you run these tools on a large established code base... + +.. code-block:: bash + + $ pip install django + + cd /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages + + flake8 django + + pylint django + +Code Analysis Tool Battle Royale +-------------------------------- + +Try this! + +.. code-block:: bash + + $ pylint flake8 + $ flake8 pylint + +Analysis Tool Summary +--------------------- + +- There is no magic bullet that guarantees functional, beautiful code +- Some classes of programming errors can be found before runtime +- With the PEP-8 tools, it is easy to let rules such as line length + slip by +- It's up to you to determine your thresholds + +Conclusion: +----------- + +Personally, I use flake8 -- it gets most of it for me. Though a run with pylint isn't a bad idea once in a while.... + +Also -- if you set up your editor with a linter -- you'll be encouraged to fix it a bit at a time as you write -- much better way to go. + +Pythonic Style +============== + +Good "Pythonic" style goes beyond style guides and things linters can figure out for you. + +The `Hitchhiker's Guide to Python: Code Style `_ is a good read that gets into a nice level of detail. + + diff --git a/_sources/modules/PersistanceAndSerialization.rst.txt b/_sources/modules/PersistanceAndSerialization.rst.txt new file mode 100644 index 0000000..f564b51 --- /dev/null +++ b/_sources/modules/PersistanceAndSerialization.rst.txt @@ -0,0 +1,735 @@ +:orphan: + +.. _serialization: + +***************************** +Persistence and Serialization +***************************** + + +Overview +======== + +Persistence and Serialization are closely related. + +*Serialization* means taking a potentially complex data structure and converting it into a single string of bytes. + +https://en.wikipedia.org/wiki/Serialization + +*Persistence* is storing data in a way that it will persist beyond the run-time of your program. + +`Persistance on Wikipedia `_ + +They are closely related, because most forms of persistent storage -- simple text files, databases, etc., require that it be turned into a simple string of bytes first. After all, at the end of the day, everything done with computers is ultimately a serial string of bytes. + +Serialization is also very useful for transmitting information between systems -- over the network, etc. + + +Serialization +============= + +This module is less about concepts. + +More about learning to use a given module. + +So less talk, more coding. + +This material is focused on methods available in the Python standard library. + +There are third party packages with more options as well. + + +Persistence +=========== + +Persistence is saving your python data structure(s) to disk -- so they +will persist once the python process is finished. + +Any serial form can provide persistence (by dumping/loading it to/from +a file), but not all persistence mechanisms are serial (i.e RDBMS, etc.) + +http://wiki.python.org/moin/PersistenceTools + + +Python Specific Formats +======================= + +These are formats specific to python -- convenient to use, but not useful for interchange with other systems. + + +Python Literals +--------------- + +Putting plain old python literals in your file + +Gives a nice, human-editable form for config files, etc. + +Don't use for untrusted sources!!! + +Good for basic python types. + +(can work for your own classes, too -- if you write a good ``__repr__`` ) + +In theory, ``repr()`` always gives a form that can be re-constructed. + +Often the ``str()`` form works too. + +``pprint`` (pretty print) module can make it easier to read: + +https://docs.python.org/3.5/library/pprint.html + + +Python Literal Example +...................... + +.. code-block:: ipython + + # a list of dicts + data = [{'this':5, 'that':4}, {'spam':7, 'eggs':3.4}] + In [51]: s = repr(data) # save a string version: + In [52]: data2 = eval(s) # re-construct with eval: + In [53]: data2 == data # they are equal + Out[53]: True + In [54]: data is data2 # but not the same object + Out[54]: False + + +You can save the string to a file and even use ``import``. + +In fact, using a python file and importing it is a great way to handle configuration for your app -- very powerful and flexible. + + +NOTE: ``eval()`` is **DANGEROUS**: + +Not so bad if you know where your data is coming from, but ``eval()`` will run any code it gets, even: + +.. code-block:: python + + import sys + sys.system('cd /; rm -rf *') + +You really don't want that run on your machine! + +The alternative: + + ``ast.literal_eval`` is safer than eval: + + https://docs.python.org/3.5/library/ast.html#ast-helpers + +It will only evaluate literals. + + +pretty print +------------ + +.. code-block:: ipython + + In [68]: data = [{'this': 5, 'that': 4}, {'eggs': 3.4, 'spam': 7}, + {'foo': 86, 'bar': 4.5}, {'fun': 43, 'baz': 6.5}] + In [69]: import pprint + In [71]: repr(data) + Out[71]: "[{'this': 5, 'that': 4}, {'eggs': 3.4, 'spam': 7}, {'foo': 86, 'bar': 4.5}, {'fun': 43, 'baz': 6.5}]" + In [72]: s = pprint.pformat(data) + In [73]: print(s) + [{'that': 4, 'this': 5}, + {'eggs': 3.4, 'spam': 7}, + {'bar': 4.5, 'foo': 86}, + {'baz': 6.5, 'fun': 43}] + +This is a nice option if you want the saved form to be human readable / editable. + +Pickle +------ + +Pickle is a custom binary format for python objects. + +You can essentially dump any python object to disk (or string, or socket, or... + +.. code-block:: ipython + + In [87]: import pickle + In [83]: data + Out[83]: + [{'that': 4, 'this': 5}, + {'eggs': 3.4, 'spam': 7}, + {'bar': 4.5, 'foo': 86}, + {'baz': 6.5, 'fun': 43}] + In [84]: pickle.dump(data, open('data.pkl', 'wb')) + In [85]: data2 = pickle.load(open('data.pkl', 'rb')) + In [86]: data2 == data + Out[86]: True + +https://docs.python.org/3.5/library/pickle.html + + +**Warning** + +The pickle module is **not secure** against erroneous or maliciously constructed data. Never unpickle data received from an untrusted or unauthenticated source. + +``pickle`` is cool because it can serialize virtually ANY object -- including your self-defined classes. + +But to do this, it must run essentially arbitrary code -- so **not safe**. + +Do not use it for receiving data from an external source. + +But you probably won't want to do that anyway -- pickle is python-specific, not very useful for data interchange. + +Shelve +------ + +A "shelf" is a persistent, dictionary-like object. + +(It's also a place you can put a jar of pickles...) + +The values (not the keys!) can be essentially arbitrary Python objects (anything picklable) + +**NOTE:** it will not reflect changes in mutable objects without re-writing them to the db. (or use ``writeback=True``) + +If less that 100s of MB -- just use a dict and pickle it. + + +``shelve`` presents a ``dict`` interface: + +.. code-block:: ipython + + import shelve + d = shelve.open(filename) + d[key] = data # store data at key + data = d[key] # retrieve a COPY of data at key + del d[key] # delete data stored at key + flag = d.has_key(key) # true if the key exists + d.close() # close it + +(it uses pickle under the hood -- same security issues) + +https://docs.python.org/3.5/library/shelve.html + +LAB +--- + +Here are two datasets embedded in Python: + +:download:`add_book_data.py ` +and +:download:`add_book_data_flat.py ` + + +They can be loaded with:: + + from add_book_data import AddressBook + +They have address book data -- one with a nested dict, one "flat". Use the nested version for this exercise. + +* Write a module that saves the data as python literals in a file + + - and reads it back in + +* Write a module that saves the data as a pickle in a file + + - and reads it back in + +* Write a module that saves the data in a shelve + + - and accesses it one by one. + +**Write some tests to make sure its working!** + + +Interchange Formats +=================== + +These are formats suitable for interchanging data with other systems -- written in arbitrary other languages. + +In other words: standard formats. + +INI +--- + +INI files + +(the old Windows config files) + +:: + + [Section1] + int = 15 + bool = true + float = 3.1415 + [Section2] + int = 32 + ... + +Good for configuration data, etc. + +ConfigParser +............ + +The ``configparser`` module provides tools for working with INI files: + +Writing: + +.. code-block:: python + + import configparser + config = configparser.ConfigParser() + config.add_section('Section1') + config.set('Section1', 'an_integer', '15') + config.set('Section1', 'a_boolean', 'true') + config.set('Section1', 'a_float', '3.1415') + # Writing our configuration file to 'example.cfg' + config.write(open('example.cfg', 'w')) + +Note: all keys and values are strings + + +Reading ``ini`` files: +----------------------- + +.. code-block:: python + + >>> config = configparser.ConfigParser() + >>> config.read('example.cfg') + >>> config.sections() + ['Section1'] + >>> config.get('Section1', 'a_float') + '3.1415' + >>> config.items('Section1') + [('an_integer', '15'), ('a_boolean', 'true'), ('a_float', '3.1415')] + + +https://docs.python.org/3/library/configparser.html + +CSV +=== + +CSV (Comma Separated Values) format is the most common import and export format for spreadsheets and databases. + +No real standard -- the Python csv package more or less follows MS Excel "standard" (with other "dialects" available) + +Can use delimiters other than commas... (I like tabs better) + +Most useful for simple tabular data + +The CSV module +-------------- + +Reading ``CSV`` files: + +(uses: :download:`eggs.csv <../examples/persistence/eggs.csv>`) + +.. code-block:: ipython + + In [14]: import csv + In [17]: spam_reader = csv.reader(open('eggs.csv'), + skipinitialspace=True) + In [19]: for row in spam_reader: + ....: print(row) + ['Spam', ' Spam', ' Spam', ' Spam', ' Spam', ' Baked Beans'] + ['Spam', ' Lovely Spam', ' Wonderful Spam'] + + +The ``csv`` module takes care of string quoting, etc. for you. + +- This is a pretty big deal -- that can be a real pain! + +NOTE: ``skipinitialspace`` is False by default, which can mess up +interpreting quotes correctly. + + +Writing ``CSV`` files: + +.. code-block:: python + + >>> import csv + >>> with open('eggs2.csv', 'w') as outfile: + >>> spam_writer = csv.writer(outfile, + quoting=csv.QUOTE_MINIMAL) + >>> spam_writer.writerow(['Spam'] * 5 + ['Baked Beans']) + >>> spam_writer.writerow(['Spam', 'Lovely Spam', 'Wonderful Spam']) + >>> spam_writer.writerow(['Spam', 'Spam, Wonderful spam..', 'Very-Wonderful Spam']) + + +The ``csv`` module takes care of string quoting, etc. for you. + +You can set the ``quoting`` attribute on the dialect object to control that. + +https://docs.python.org/3/library/csv.html + + +JSON +==== + +JSON (JavaScript Object Notation) is a subset of JavaScript syntax used as a lightweight data interchange format. + +**LOTS** of systems can read JSON -- notably browsers... + +Python module has an interface similar to ``pickle`` + +Can handle the standard Python data types + +Specializable encoding/decoding for other types -- but I wouldn't do that! + +Commonly used for configuration files, etc. + + +Python json module +------------------ + +.. code-block:: ipython + + In [93]: import json + In [94]: s = json.dumps(data) + Out[95]: '[{"this": 5, "that": 4}, {"eggs": 3.4, "spam": 7}, + {"foo": 86, "bar": 4.5}, {"fun": 43, "baz": 6.5}]' + In [96]: data2 = json.loads(s) + Out[97]: + [{u'that': 4, u'this': 5}, + {u'eggs': 3.4, u'spam': 7}, + ... + In [98]: data2 == data + Out[98]: True # they are the same + +(also ``json.dump() and json.load()`` for files) + +**NOTE:** JSON is less "rich" than python -- no tuples, no distinction between integers and floats, no comments! And keys can only be strings. + +http://www.json.org/ + +https://docs.python.org/3/library/json.html + +LAB +--- + +Use the same addressbook data: + +:: + + # load with: + from add_book_data import AddressBook + +* Write a module that saves the data as an INI file + + - and reads it back in + +* Write a module that saves the data as a CSV file + + - and reads it back in + +( you'll need the "flat" version for this...) + +* Write a module that saves the data in JSON + + - and reads it back in + +XML +=== + +XML is a standardized version of SGML, designed for use as a data storage / interchange format. + +NOTE: HTML is also SGML, and modern versions conform to the XML standard. + +XML in the python std lib +------------------------- + +``xml.dom`` + +``xml.sax`` + +``xml.parsers.expat`` + +``xml.etree`` + +https://docs.python.org/3/library/xml.html + +elementtree +----------- + +``elementtree`` is the simplest tool -- maps pretty directly to XML. + +The Element type is a flexible container object, designed to store hierarchical data structures in memory. + +Essentially an in-memory XML -- can be read from/written to XML + +an ``ElementTree`` is an entire XML doc + +an ``Element`` is a node in that tree + +https://docs.python.org/3/library/xml.etree.elementtree.html + + +* Write a module that saves the data in XML + + - and reads it back in + + - this gets ugly! + +(NEED a good example here!) + + +DataBases +========= + +A database is a system for storing and retrieving data -- usually in a filesystem. + +We usually think RDBMS and SQL -- but there are simpler systems. + +dbm +--- + +``dbm`` is a generic interface to variants of the DBM database + +Suitable for storing data that fits well into a python dict with strings as both keys and values + +Note: dbm will use the dbm system that works on your system -- this may be different on different systems -- so the db files may NOT be compatible! ``whichdb`` will try to figure it out, but it's not guaranteed + +https://docs.python.org/3/library/dbm.html + +**NOTE:** dbm is getting pretty old fashioned -- e.g. it doesn't handle Unicode + +It's here for completeness, but there are probably better options! + + +the ``dbm`` module +------------------ + +Writing data: + +.. code-block:: python + + #creating a dbm file: + import dbm + dbm.open(filename, 'n') + +flag options are: + +* 'r' -- Open existing database for reading only (default) +* 'w' -- Open existing database for reading and writing +* 'c' -- Open database for reading and writing, creating it if it doesn’t exist +* 'n' -- Always create a new, empty database, open for reading and writing + +**caution** -- these are different than the file open modes! + + +``dbm`` provides a dict-like interface: + +.. code-block:: python + + import dbm + db = dbm.open("dbm", "c") + db["first"] = "bruce" + db["second"] = "micheal" + db["third"] = "fred" + db["second"] = "john" #overwrite + db.close() + # read it: + db = dbm.open("dbm", "r") + for key in db.keys(): + print(key, db[key]) + +(a lot like ``shelve``, though theoretically compatible with other systems) + +https://docs.python.org/3/library/dbm.html + + +sqlite +------ + +**SQLite:** + +a C library providing a lightweight disk-based single-file database + +Nonstandard variant of the SQL query language + +Very broadly used as as an embedded databases for storing application-specific data etc. + +Firefox plug-in: + +https://addons.mozilla.org/en-US/firefox/addon/sqlite-manager/ + + +python sqlite module +-------------------- + +``sqlite3`` Python module wraps C lib -- provides standard DB-API interface + +Allows (and requires) SQL queries + +Can provide high performance, flexible, portable storage for your app + + +Example: +........ + +.. code-block:: python + + import sqlite3 + # open a connection to a db file: + conn = sqlite3.connect('example.db') + # or build one in-memory + conn = sqlite3.connect(':memory:') + # create a cursor + c = conn.cursor() + + +Execute SQL with the cursor: +............................ + +.. code-block:: python + + # Create table + c.execute("'CREATE TABLE stocks (date text, trans text, symbol text, qty real, price real)'") + # Insert a row of data + c.execute("INSERT INTO stocks VALUES ('2006-01-05','BUY','RHAT',100,35.14)") + # Save (commit) the changes + conn.commit() + # Close the cursor if we are done with it + c.close() + + +``SELECT`` creates a cursor that can be iterated: + +.. code-block:: python + + >>> for row in c.execute('SELECT * FROM stocks ORDER BY price'): + print row + ('2006-01-05', 'BUY', 'RHAT', 100, 35.14) + ('2006-03-28', 'BUY', 'IBM', 1000, 45.0) + ... + + +Or you can get the rows one by one or in a list: + +.. code-block:: python + + c.fetchone() + c.fetchall() + + +Good idea to use the DB-API’s parameter substitution: + +.. code-block:: python + + t = (symbol,) + c.execute('SELECT * FROM stocks WHERE symbol=?', t) + print c.fetchone() + # Larger example that inserts many records at a time + purchases = [('2006-03-28', 'BUY', 'IBM', 1000, 45.00), + ('2006-04-05', 'BUY', 'MSFT', 1000, 72.00), + ('2006-04-06', 'SELL', 'IBM', 500, 53.00), + ] + c.executemany('INSERT INTO stocks VALUES (?,?,?,?,?)', purchases) + + +https://docs.python.org/3/library/sqlite3.html + +http://xkcd.com/327/ + + +DB-API +------ + +The DB-API spec (PEP 249) is a specification for interaction between Python and Relational Databases. + +Support for a large number of third-party Database drivers: + + * MySQL + * PostgreSQL + * Oracle + * MSSQL (?) + * ... + +http://www.python.org/dev/peps/pep-0249} + +LAB Extras: +=========== + +A few more things you could do: + +* Use pickle to save/reload a custom class of yours + (the Circle class from the first quarter?) + +* Try writing a json writer for a non-standard data type: + A custom class, or a more complex built-in? + + + + +Other Options +============= + +There are a lot of other possibilities outside the standard lib. + + +Object-Relation Mappers +----------------------- + +Systems for mapping Python objects to tables + +Saves you writing that glue code (and the SQL) + +Usually deal with mapping to variety of back-ends: + +- test with SQLite, deploy with PostgreSQL + +SQL Alchemy + +- http://www.sqlalchemy.org/ + +Django ORM + +- https://docs.djangoproject.com/en/dev/topics/db/ + + +Object Databases +---------------- + +(we'll be talking more about this in another class: :ref:`nosql`) + +Directly store and retrieve Python Objects. + +Kind of like ``shelve``, but more flexible, and give you searching, etc. + +ZODB: (http://www.zodb.org/) + + +NoSQL +----- +Map-Reduce, etc. + +-- Big deal for "Big Data": Amazon, Google, etc. + +Document-Oriented Storage + +* MongoDB (BSON interface, JSON documents) + +* CouchDB (Apache): + + * JSON documents + + * Javascript querying (MapReduce) + + * HTTP API + + +LAB +--- + +Load data with: + +.. code-block:: python + + from add_book_data import AddressBook + +* Write a module that saves the data in a dbm database + + - and reads it back in + +* Write a module that saves the data in an SQLite database + + - and reads it back in + + - helps to know SQL here... + +Optional: + +* Do the same with a ORM of your choice. diff --git a/_sources/modules/Profiling.rst.txt b/_sources/modules/Profiling.rst.txt new file mode 100644 index 0000000..aa133f0 --- /dev/null +++ b/_sources/modules/Profiling.rst.txt @@ -0,0 +1,701 @@ +:orphan: + +.. _profiling: + +************************* +Performance and Profiling +************************* + +============== +Today's topics +============== + +- Determining performance objectives +- Measuring performance a.k.a. profiling +- Performance optimizations + +What is Software Profiling +-------------------------- + +The act of using instrumentation to objectively measure the performance +of your application + +"Performance" can be a measure of any of the following: + +- resource use (CPU, memory) +- frequency or duration of function calls +- wall clock execution time of part or all of your application + +Collecting this data involves instrumenting the code. In Python, this +happens at runtime. + +The instrumentation creates overhead, so it has a performance cost + +The output data (a "profile") will be a statistical summary of the +execution of functions + +An optimization strategy +------------------------ + +#. Write the code for maintainability / readability +#. Test for correctness +#. Collect profile data +#. If it is fast enough, quit. Your job here is done. +#. Else optimize the most expensive parts based on profiling data +#. Repeat from 2) + + +Programmers waste enormous amounts of time thinking about, or +worrying about, the speed of noncritical parts of their programs, +and these attempts at efficiency actually have a strong negative +impact when debugging and maintenance are considered. We should +forget about small efficiencies, say about 97% of the time: + +*premature optimization is the root of all evil.* + +--Donald Knuth + +http://c2.com/cgi/wiki?PrematureOptimization + +http://c2.com/cgi/wiki?ProfileBeforeOptimizing + + +Steps to better performance +--------------------------- + +(In order of importance) + +#. Efficient Algorithms (big O, etc...) +#. Appropriate Python data types, etc. +#. Appropriate Python style +#. Specialized packages (numpy, scipy) +#. Calling external packages +#. Extending with C/C++/Fortran/Cython + + +Big O notation +-------------- + +The efficiency of an algorithm is often described in “big O” notation. + +The letter O is used because the growth rate of a function is also +referred to as Order of the function + +It describes how an algorithm behaves in terms of resource use as a +function of amount of input data + + +O(1) - (Constant performance) Execution time stays constant regardless of how much data is supplied + +- Example: adding to a dict + +O(n) - Time goes up linearly with number of items. + +- Example: scanning lists + +O(n\ :sup:`2`) - Time goes up quadratically with number of items. + +- Example: bubble sort, worst case + +O(log(n)) - goes up with the log of number of items + +- Example: bisection search + + +.. image:: /_static/big_o.png +.. :align: right +.. :height: 450px + :alt: big O notation plot + +**log?** you expect me to remember that math??? + +Let's think about that a bit.... + +Anyone know what a bisection search is? + +Why is that O(log(n))? + +| + +Reference: + +https://wiki.python.org/moin/TimeComplexity + + +Measuring time with a stopwatch +------------------------------- + +One way to measure performance is with a stopwatch. + +Start the clock when a unit of code such as a function begins, and stop +it when the code returns + +This is a the simplest method, and we can instrument our code to start +and stop the clock. + +Like most timing benchmarks, data obtained is valid only for the +particular test environment (machine/OS/Python version..) + +Relative timings may be valid across systems, but can also diverge + +For instance a run on a machine with fast network and slow disk may +produce much different results on a system with slow network and fast +disk + +``time.clock()`` : ``time.time()`` +---------------------------------- + +Using the time module as a profiling decorator + +``time.time()`` returns the unix system time (wall clock time) + +``time.clock()`` returns the CPU time of the current process + +Precision is system dependent + +Quite coarse, but can capture the big picture + +See :download:`/examples/profiling/timer/timer_test.py` + +.. code-block:: python + + import time + + def timer(func): + def timer(*args, **kwargs): + """a decorator which prints execution time of the decorated function""" + t1 = time.time() + result = func(*args, **kwargs) + t2 = time.time() + print("-- executed %s in %.4f seconds" % (func.func_name, (t2 - t1))) + return result + return timer + + @timer + def expensive_function(): + time.sleep(1) + + @timer + def less_expensive_function(): + time.sleep(.02) + + expensive_function() + less_expensive_function() + +timeit +------ + +Used for testing small bits of code + +Use to test hypotheses about efficiency of algorithms and Python idioms + +Will run the given statement many times and calculate the average +execution time + +Can be run from the command line: + +.. code-block:: python + + python -m timeit '"-".join(str(n) for n in range(100))' + +https://docs.python.org/3.5/library/timeit.html + +See the ``timeit.py`` source: + +https://hg.python.org/cpython/file/3.5/Lib/timeit.py + +``timeit`` command line interface +--------------------------------- + +options + +- ``-nN``: execute the given statement N times in a loop. If this value is + not given, a fitting value is chosen. +- ``-rR``: repeat the loop iteration R times and take the best result. + Default: 3 +- ``-t``: use time.time to measure the time, which is the default on Unix. + This function measures wall time. +- ``-c``: use time.clock to measure the time, which is the default on + Windows and measures wall time. On Unix, resource.getrusage is used + instead and returns the CPU user time. +- ``-pP``: use a precision of P digits to display the timing result. + Default: 3 + +.. code-block:: bash + + $ python -m timeit -n 1000 -t "len([x**2 for x in range(1000)])" + + +``timeit`` can also be imported as a module + +http://docs.python.org/3/library/timeit.html#timeit.timeit + +.. code-block:: python + + timeit.timeit(stmt='pass', + setup='pass', + timer=, + number=1000000) + +The setup kwarg contains a string of Python code to execute before the +loops start, so that code is not part of the test + +.. code-block:: python + + import timeit + statement = "char in text" + setup_code = """text = "sample string";char = "g" """ + timeit.timeit(statement, setup=setup_code) + + +``timeit`` via iPython magic +---------------------------- + +Note that all that setup_code stuff is kind of a pain. + +iPython has your back (again) + +.. code-block:: ipython + + %timeit pass + + u = None + %timeit u is None + + %timeit -r 4 u == None + + import time + + %timeit -n1 time.sleep(2) + + %timeit -n 10000 "f" in "food" + +`timeit magic `_ + + +Exercise +-------- + +We just tried determining if a character exists in a string: + +.. code-block:: python + + statement = "'f' in 'food'" + timeit.timeit(statement) + +Run timeit with an alternative statement: + +.. code-block:: python + + statement2 = "'food'.find('f') >= 0" + timeit.timeit(statement2) + +Which is faster? Why? + + +Getting more detailed with Profiling +------------------------------------ + +That kind of timing is only useful if you know what part of the code you want to optimize. + +But what if you know your program is "slow", but don't know where is is spending the time? + +**Do not Guess!** -- you will often be wrong, and you don't want to waste time optimizing the wrong thing. + +*Really* -- even very experienced programmers are often wrong about where the bottlenecks are. + +You really need to profile to be sure. + +Also: take into account the entire run-time: does it make sense to optimize an initialization routine that takes a few seconds before a multi-hour run? + + +A profiler takes measurements of runtime performance and summarizes results into a profile report + +Reported metrics could include + +- Memory used over time +- Memory allocated per function +- Frequency of function calls +- Duration of function calls +- Cumulative time spent in subfunction calls + + +Python's built-in profilers +--------------------------- + +Python comes with a couple profiling modules + +- profile - older, pure Python. If you need to extend the profiler, + this might be good. Otherwise, it's slow. + +- cProfile - same API as profile, but written in C for less overhead + +**You almost always want to use ``cProfile``** + +https://docs.python.org/3/library/profile.html + + +cProfile +-------- + +Can be run as a module on an entire application + +.. code-block:: bash + + python -m cProfile [-o output_file] [-s sort_order] read_bna.py + 11111128 function calls in 8.283 seconds + Ordered by: standard name + + ncalls tottime percall cumtime percall filename:lineno(function) + 1 0.000 0.000 0.000 0.000 integrate.py:1() + 11111110 2.879 0.000 2.879 0.000 integrate.py:1(f) + [....] + +- ncalls: number of calls +- tottime: total time spent in function, excluding time in sub-functions +- percall: tottime / ncalls +- cumtime: total time spent in function, including time in sub-functions +- percall: cumtime / ncalls +- filename:lineno -- location of function + + +Analyzing profile data +---------------------- + +Output to a binary dump with -o + +While doing performance work, save your profiles for comparison later + +This helps ensure that any changes do actually increase performance + +A profile dump file can be read with ``pstats`` + +.. code-block:: bash + + python -m pstats + +Gives you a command line interface + +(help for help...) + +``pstats`` +---------- + +.. code-block:: python + + python -m cProfile -o prof_dump ./read_bna.py + python -m pstats + % read prof_dump + + # show stats: + prof_dump% stats + + # only the top 5 results: + prof_dump% stats 5 + + # sort by cumulative time: + prof_dump% sort cumulative + + # shorten long filenames for display: + prof_dump% strip + # show results again: + prof_dump% stats 5 + + +pstats also has method calls: + +.. code-block:: python + + import pstats + p = pstats.Stats('prof_dump') + p.sort_stats('calls', 'cumulative') + p.print_stats() + + # Output can be restricted via arguments to print_stats(). + # Each restriction is either an integer (to select a count of lines), + # a decimal fraction between 0.0 and 1.0 inclusive (to select a percentage of lines), + # or a regular expression (to pattern match the standard name that is printed. + # If several restrictions are provided, then they are applied sequentially. + + +Analyzing profile data +---------------------- + +Inspect only your local code with regular expression syntax: + +.. code-block:: python + + import pstats + prof = pstats.Stats('prof_dump') + prof.sort_stats('cumulative') + prof.print_stats('^./[a-z]*.py:') + +I tend to write little scripts like this so I don't have to remember the commands. + +Exercise / Example +------------------ + +Real world example: + +``Examples/profiling/bna_reader/read_bna.py`` + +BNA is a (old) text file format for holding geospatial data. + +We were using some old code of mine that read these files, generated an internal data structure of polygons, and rendered them to a PNG. + +As these files got big -- this process started getting really slow. + +I had already optimized the file reading code a lot -- so could we do better? + + - I assumed not + +One of my team ran the profiler and identified the bottleneck -- and yes -- we could do better -- a lot. + +Let's try that out now. + + +============================ +Some other tools to consider +============================ + +* For better visualizing + +* For C extensions + +* For memory Profiling + + +SNAKEVIZ +-------- + +A graphical profile viewer for Python + +https://jiffyclub.github.io/snakeviz/ + +.. code-block:: python + + pip install snakeviz + +Inspired by "Run Snake Run": http://www.vrplumber.com/programming/runsnakerun/ + +(which only works with Python 2.* for now) + +.. image:: /_static/snakeviz.png +.. :align: right +.. :height: 450px + :alt: snakeviz visualization + + + +line profiler +------------- + +Thus far, we've seen how to collect data on the performance of functions +as atomic units + +``line_profiler`` is a module for doing line-by-line profiling of functions + +``line_profiler`` ships with its own profiler, ``kernprof.py``. + +Enable line-by-line profiling with -l + +Decorate the function you want to profile with ``@profile`` and run + +.. code-block:: bash + + # the -v option will display the profile data immediately, instead + # of just writing it to .lprof + $ kernprof -l -v integrate_main.py + + # load the output with + $ python -m line_profiler integrate_main.py.lprof + + +https://github.com/rkern/line_profiler + +qcachegrind / kcachegrind +------------------------- + +profiling tool based on Valgrind: + +http://kcachegrind.sourceforge.net/html/Valgrind.html + +a runtime instrumentation framework for Linux/x86 + +Can be used with Python profile data with a profile format conversion + +Doesn't give all the information that a native valgrind run would +provide + +.. code-block:: python + + # convert python profile to calltree format + pip install pyprof2calltree + + python -m cProfile -o dump.profile integrate_main.py + pyprof2calltree -i dump.profile -o dump.callgrind + + +http://kcachegrind.sourceforge.net/cgi-bin/show.cgi/KcacheGrindCalltreeFormat + + +Profiling C extensions +---------------------- + +Google Performance Tools: + +https://code.google.com/p/gperftools/ + +can be used to profile C extensions + +Just call ProfilerStart and ProfilerStop with ctypes around the code you +want to profile + + +.. code-block:: python + + import ctypes + + libprof = ctypes.CDLL('/usr/local/lib/libprofiler.0.dylib') + libprof.ProfilerStart('/tmp/out.prof') + import numpy + a=numpy.linspace(0,100) + a*=32432432 + libprof.ProfilerStop('/tmp/out.prof') + +.. code-block:: bash + + # convert the profile to qcachegrind's format with google's pprof tool + $ pprof --callgrind ~/virtualenvs/uwpce/lib/python2.7/site-packages/numpy/core/multiarray.so out.prof > output.callgrind + $ qcachegrind output.callgrind + + +memory profilers +---------------- + +There aren't any great ones + +One option is heapy, which comes with Guppy, a Python environment for +memory profiling + +.. code-block:: python + + from guppy import hpy; hp=hpy() + hp.doc.heap + hp.heap() + %run define.py Robot + hp.heap() + +Others: + +https://pypi.python.org/pypi/memory_profiler + +http://mg.pov.lt/objgraph/ + +https://launchpad.net/meliae + +http://pythonhosted.org/Pympler/muppy.html + +http://jmdana.github.io/memprof/ + +============================ +Boosting Python performance +============================ + +There are ways to better structure your Python code to improve performance + +A few key approaches +-------------------- + +- Overhead in function/method runtime lookup can be significant for + small frequent calls. + +- inlining code or caching function results might help. + +- Python string handling idioms: use ``"".join(list_of_strings)`` rather + than sequential calls to += See ``examples/strings/str_concat.py`` and + ``str_comprehensions.py`` + +- using list comprehensions, generator expressions, ``or map()`` instead of + for loops can be faster (see ``data_aggregation/loops.py``) + +- Leverage existing domain specific C extension libraries, for instance + numpy for fast array operations. + +- Rewrite expensive code as C modules. Use ctypes, Cython, SWIG, ... + +http://wiki.python.org/moin/PythonSpeed/PerformanceTips/ + + +Managing memory +--------------- + +Don’t forget memory: + +Processors are fast + +It can take longer to push the memory around than do the computation + +So keep in mind for big data sets: + +Use the right data structures + +Use efficient algorithms + +Use generators and iterators, rather than lists. + +Use iterators to pull in the data you need from databases, sockets, +files, ... + + +Distraction: pyGame +------------------- + +There is a nice profiling example that uses PyGame: + +http://www.pygame.org/hifi.html + +Which you can install from binaries: + +Windows: +http://www.lfd.uci.edu/~gohlke/pythonlibs/#pygame + +(you want the wheel file for the python you are running: probably cp35) + +Anaconda Python: + +First install miniconda. Then you can install pygame from anaconda.org. + +https://anaconda.org/cogsci + + +A more complex profile +---------------------- + +The amount of data in the previous example is readable, so now we'll +look at the output from a more complex application: +examples/profiling/pygame/swarm.py + +This program consists of calculating the gravitational acceleration of +bodies around a central mass and displaying them + +There are two major consumers of resources: one is our own code +calculating the physics, the other is pygame drawing the results on the +screen + +Our goal is to figure out whether the major bottleneck is in our own +logic or in the pygame operations + +A simple way to get data for our own code is + +.. code-block:: python + + python -m cProfile swarm.py &> /tmp/output.txt + grep swarm.py /tmp/output.txt + + + + + +Questions? +---------- diff --git a/_sources/modules/Properties.rst.txt b/_sources/modules/Properties.rst.txt new file mode 100644 index 0000000..dbe7287 --- /dev/null +++ b/_sources/modules/Properties.rst.txt @@ -0,0 +1,152 @@ +.. _properties: + +########## +Properties +########## + +https://en.wikipedia.org/wiki/Property_%28programming%29#Python + +Attributes are clear and concise +-------------------------------- + +.. container:: + + One of the strengths of Python is lack of clutter. + + .. code-block:: ipython + + In [5]: class C: + def __init__(self): + self.x = 5 + In [6]: c = C() + In [7]: c.x + Out[7]: 5 + In [8]: c.x = 8 + In [9]: c.x + Out[9]: 8 + +And we want to maintain this clarity as we develop our programs. + +Getter and Setters +------------------ + +But what if you need to add behavior later? + + +* do some calculation +* check data validity +* keep things in sync + + +.. code-block:: ipython + + In [5]: class C: + ...: def __init__(self): + ...: self.x = 5 + ...: def get_x(self): + ...: return self.x + ...: def set_x(self, x): + ...: self.x = x + ...: + In [6]: c = C() + In [7]: c.get_x() + Out[7]: 5 + In [8]: c.set_x(8) + In [9]: c.get_x() + Out[9]: 8 + +This is verbose -- `Java`_? + +.. _Java: http://dirtsimple.org/2004/12/python-is-not-java.html + +Properties +---------- + +.. code-block:: ipython + + In [27]: class C: + ....: _x = None + ....: @property + ....: def x(self): + ....: return self._x + ....: @x.setter + ....: def x(self, value): + ....: self._x = value + In [28]: c = C() + In [30]: c.x = 5 + In [31]: print(c.x) + 5 + +Now the interface is like simple attribute access! + +Decorators +---------- + +What's up with the "@" symbols? + +Those are "decorations". It is a syntax for wrapping functions up with something special. + +We will cover decorators in detail in another part of the program, but for now just copy the syntax. + +.. code-block:: python + + @property + def x(self): + +means: make a property called x with this as the "getter". + +.. code-block:: python + + @x.setter + def x(self, value): + +means: make the "setter" of the 'x' property this new function. + +Read Only Attributes +-------------------- + +You do not need to define a setter. If you don't, you get a "read only" attribute: + +.. code-block:: ipython + + In [11]: class D(): + ....: def __init__(self, x=5): + ....: self._x = x + ....: @property + ....: def x(self): + ....: """I am read only""" + ....: return self._x + ....: + In [12]: d = D() + In [13]: d.x + Out[13]: 5 + In [14]: d.x = 6 + --------------------------------------------------------------------------- + AttributeError Traceback (most recent call last) + in () + ----> 1 d.x = 6 + AttributeError: can't set attribute + +Deleters +-------- + +If you want to do something special when a property is deleted, you can define a deleter as well: + +.. code-block:: ipython + + In [11]: class D(): + ....: def __init__(self, x=5): + ....: self._x = 5 + ....: @property + ....: def x(self): + ....: return self._x + ....: @x.deleter + ....: def x(self): + ....: del self._x + +If you leave this out, the property can't be deleted, which is usually +what you want. + +Play around with some properties code: + +:download:`properties_example.py <../examples/properties/properties_example.py>` diff --git a/_sources/modules/Py2vsPy3.rst.txt b/_sources/modules/Py2vsPy3.rst.txt new file mode 100644 index 0000000..0c1241d --- /dev/null +++ b/_sources/modules/Py2vsPy3.rst.txt @@ -0,0 +1,101 @@ +.. _py2_vs_py3: + +######################## +Python 2 versus Python 3 +######################## + +now that Python version 2 has officially reached end of life, most recent information is in/about Python 3. + +But you will still find a lot of older example code online in Python2, rather than Python3 + +For the most part, they are the same -- so you can use those examples to learn from. + +There are a lot of subtle differences that you don't need to concern yourself with just yet. + +But a couple that you'll need to know right off the bat: + +print() +------- + +In Python2, ``print`` is a "statement", rather than a function. That means it didn't require parentheses around what you want printed: + +.. code-block:: python + + print something, something_else + +This made it a bit less flexible and powerful. + +But -- if you try to use it that way in Python3, you'll get an error: + +.. code-block:: python + + In [15]: print "this" + File "", line 1 + print "this" + ^ + SyntaxError: Missing parentheses in call to 'print' + +So -- if you get this error, simply add the parentheses: + +.. code-block:: ipython + + In [16]: print("this") + this + +Division +-------- + +In python 3, the division operator is "smart" when you divide integers: + +.. code-block:: ipython + + In [17]: 1 / 2 + Out[17]: 0.5 + +However in Python2, integer division, will give you an integer result: + +.. code-block:: ipython + + In [1]: 1/2 + Out[1]: 0 + +In both versions, you can get "integer division" if you want it with a double slash: + +.. code-block:: ipython + + In [1]: 1//2 + Out[1]: 0 + +And in Python2, you can get the behavior of Python3 with "true division": + +.. code-block:: ipython + + In [2]: from __future__ import division + + In [3]: 1/2 + Out[3]: 0.5 + +For the most part, you just need to be a bit careful with the rare cases where Python2 code counts on integer division. + +Iterators vs Lists +------------------ + +In Python2, a number of functions returned a full list of the contents. But most of the time, you didn't need a list -- you only needed a way to loop through all the items returned. Such an object is called an "iterable" -- more about that later in the class. But for now, if you get an error like:: + + TypeError: 'dict_keys' object does not support indexing + +Then you likely got an iterator, rather than a "proper" list. You can fix this by making a list out of it:: + + list(an_iterator) + +the list constructor will make a list out of any iterable. So you can now index it, etc. + +Other Python2 / Python3 differences +----------------------------------- + +The most drastic difference (improvement!) is better Unicode support, and better bytes / Unicode separation. + +Most of the other differences are essentially implementation details, like getting iterators instead of sequences -- we'll talk about that more when it comes up in a later lesson. + +There are also a few syntax differences with more advanced topics: Exceptions, ``super()``, etc. + diff --git a/_sources/modules/PythonClasses.rst.txt b/_sources/modules/PythonClasses.rst.txt new file mode 100644 index 0000000..71acfd8 --- /dev/null +++ b/_sources/modules/PythonClasses.rst.txt @@ -0,0 +1,390 @@ +.. _python_classes: + +############## +Python Classes +############## + +"Classes" are the core of Object Oriented Programming. + +They provide the tools for encapsulation (keeping the data with the functions) and subclassing (making custom versions of certain types) and polymorphism (making different classes reusable in the same context). + + +How are classes made in Python? +=============================== + +The ``class`` statement +----------------------- + +The ``class`` statement creates a new type object: + +.. code-block:: ipython + + In [4]: class C: + pass + ...: + In [5]: type(C) + Out[5]: type + +.. note:: A class is a type -- interesting! Remember how everything is an object in Python? well, every object is of a certain type. There are the built-in types that you are used to: integers, strings, lists, .... When you create a new class, you are creating a new type, and it is exactly the same as the built in ones. + +The class is created when the ``class`` statement is run -- much like ``def`` creates a function object. + +So we now have a new type, or class -- it doesn't have any actual functionality yet, though by default all classes "inherit" from ``object``. In doing so they get some minimal functionality from that: + +.. code-block:: ipython + + In [3]: issubclass(C, object) + Out[3]: True + +We can print it: + +.. code-block:: ipython + + In [4]: print(C) + + +And look at all the methods it has! + +.. code-block:: ipython + + In [5]: dir(C) + Out[5]: + ['__class__', + '__delattr__', + '__dict__', + '__dir__', + '__doc__', + '__eq__', + '__format__', + '__ge__', + '__getattribute__', + '__gt__', + '__hash__', + '__init__', + '__init_subclass__', + '__le__', + '__lt__', + '__module__', + '__ne__', + '__new__', + '__reduce__', + '__reduce_ex__', + '__repr__', + '__setattr__', + '__sizeof__', + '__str__', + '__subclasshook__', + '__weakref__'] + +Most of those don't do anything -- but they are there, so every class is guaranteed to have all the "stuff" Python expects objects to have. + +In order for the class to do anything useful, it needs to be given attributes and methods. + + +A simple ``class`` +------------------ + +About the simplest class you can write that is still useful: + +.. code-block:: python + + >>> class Point: + ... x = 1 + ... y = 2 + >>> Point + + >>> Point.x + 1 + >>> p = Point() + >>> p + <__main__.Point instance at 0x2de918> + >>> p.x + 1 + +This looks a lot like a "struct" in C -- Python doesn't have structures, so yes, a class with no methods (functions) is essentially a struct. + +.. note:: In practice, it is very common to use a simple class like this to store related data, even if there are no functions involved. So common, in fact, that in Python 3.8, a standard library package called ``dataclasses`` was added that generates classes for storing data like this -- without having to write hardly any code. I encourage you to check it out for real use, but for now, we'll build things from scratch so that you can learn how it all works. + + +Basic Structure of a class +-------------------------- + +.. code-block:: python + + class Point: + # everything defined in here is in the class namespace + def __init__(self, x, y): + self.x = x + self.y = y + +So this class has a method called "__init__" -- which is a Python special method. Almost all classes have an ``__init__`` method + +see: :download:`simple_classes.py <../examples/classes/simple_classes.py>` + +The Initializer +--------------- + +The ``__init__`` special method is known as the initializer. It is automatically called when a new instance of a class is created. + +You can use it to do any set-up you need: + +.. code-block:: python + + class Point(object): + def __init__(self, x, y): + self.x = x + self.y = y + + +It gets the arguments passed when you call the class object: + +.. code-block:: python + + Point(x, y) + +Once you have defined an __init__, you can create "instances" of the class: + +.. code-block:: python + + p = Point(3,4) + +And access the attributes: + +.. code-block:: python + + print("p.x is:", p.x) + print("p.y is:", p.y) + + +Self +---- + +What is this ``self`` thing? + +The instance of the class is passed as the first parameter for every method. + +The name "``self``" is only a convention -- but you *DO* want to use it. + +.. code-block:: python + + class Point: + def a_function(self, x, y): + ... + +Does this look familiar from C-style procedural programming? + +Anything assigned to a ``self.`` attribute is kept in the instance +name space -- ``self`` *is* the instance. + +That's where all the instance-specific data is. + + +Class Attributes +---------------- + +In the above example, we assigned two attributes to ``self`` -- these are going to be different for each instance, or copy of this class. But what if you want all the instances of a class to share the same values? + +.. code-block:: python + + class Point(object): + size = 4 + color= "red" + def __init__(self, x, y): + self.x = x + self.y = y + +Anything assigned in the class scope is a class attribute -- every +instance of the class shares the same one. + +Note: the methods defined by ``def`` are class attributes as well. + +The class is one namespace, the instance is another. + +.. code-block:: python + + class Point: + size = 4 + color = "red" + ... + def get_color(self): + return self.color + >>> p3.get_color() + 'red' + +So in this case, ``size`` and ``color`` are class attributes. + +But note in ``get_color`` -- it accesses color from ``self``: + +class attributes are accessed with ``self`` also. + +So what is the difference? + + * class attributes are shared by ALL the instances of the class. + * instance attributes are unique to each instance -- each one has its own copy. + +Example: + +.. code-block:: ipython + + In [6]: class C: + ...: x = [1,2,3] # class attribute + ...: def __init__(self): + ...: self.y = [4,5,6] # instance attribute + ...: + + In [7]: c1 = C() + + In [8]: c2 = C() + + In [9]: c1.x is c2.x # does each instance see the same x? + Out[9]: True + + In [10]: c1.y is c2.y # does each instance see the same y? + Out[10]: False + +But what are the consequences of this? It's a **very** important distinction. watch what happens if we change something in these objects, adding a new item to both the lists in ``c1``: + +.. code-block:: ipython + + # add an item to c1's x list + In [5]: c1.x.append(100) + + In [6]: c1.x + Out[6]: [1, 2, 3, 100] + + In [7]: c2.x + Out[7]: [1, 2, 3, 100] + +Note that adding something to ``c1.x`` also changed ``c2.x`` that is because they are the *same* list -- ``.x`` is a *class attribute* -- c1 and c2 share the same class, so they share the same class attributes. + +But if we change ``y``, an instance attribute: + +.. code-block:: ipython + + In [8]: c1.y.append(200) + + In [9]: c1.y + Out[9]: [4, 5, 6, 200] + + In [10]: c2.y + Out[10]: [4, 5, 6] + +appending to ``c1.y`` did not change ``c2.y`` -- ``y`` in this case is a an *instance* attribute -- each instance has its own version -- changing one will not affect the others. + +So when you are deciding where to "put" something, you need to think about whether all instances need the same thing, or if they each need their own version of the attribute. + +As a class attribute, you can access it from the class namespace as well, and it will affect all instances of that class: + +.. code-block:: python + + In [11]: C.x.append(2222) + + In [12]: c1.x + Out[12]: [1, 2, 3, 100, 2222] + + In [13]: c2.x + Out[13]: [1, 2, 3, 100, 2222] + +So here we changed ``x`` on the *class* object, ``C``, and the change showed up in all the instances, ``c1`` and ``c2``. + +Instance attributes are far more common than class attributes. After all, the whole point of classes it to have instances with their own data. + +Typical methods +--------------- + +.. code-block:: python + + import math + + class Circle: + color = "red" + + def __init__(self, diameter): + self.diameter = diameter + + def expand(self, factor=2): + self.diameter = self.diameter * factor + return None # note that if you leave that off, it will still return None + + def area(self): + area = (self.diameter / 2)**2 * math.pi + return area + + +Methods take some parameters, and possibly manipulate the attributes in ``self``. + +Remember that classes are about encapsulating the data and the functions that act on that data -- the methods are the functions that act on the data. + +They may or may not return something useful. + +.. note:: + + It is a convention in Python that methods that change the internal state of an object return ``None``, whereas methods that return a new object, or some calculated result without changing the state return that value. + + You can see examples of this in the python built ins -- methods of lists like ``append`` or ``sort`` return None -- indicating that they have mutated the instance. + + +Gotcha ! +-------- + +.. code-block:: python + + ... + def grow(self, factor=2): + self.diameter = self.diameter * factor + ... + In [205]: C = Circle(5) + In [206]: C.grow(2,3) + + TypeError: grow() takes at most 2 arguments (3 given) + +Huh???? I only gave two arguments! + +``self`` is implicitly passed in for you by Python. so it actually *did* get three! + + +Functions (methods) are First Class Objects +------------------------------------------- + +Note that in Python, functions are first class objects, so a method *is* an attribute. + +All the same rules apply about attribute access: note that the methods are defined in the class -- so they are class attributes. + +All the instances share the same methods -- there is only one copy of each method. + +But each method gets its own namespace when it is actually called, so there is no confusion -- just like when you call a regular function multiple times. + +Manipulating Attributes +----------------------- + +Python makes it very easy to manipulate object's attributes -- you can access them with the "dot" notation, and simply set them like any other variable. With the Circle class above: + +.. code-block:: python + + In [15]: c = Circle(2) + + In [16]: c.area() + Out[16]: 3.141592653589793 + + In [17]: c.diameter = 4 + + In [18]: c.area() + Out[18]: 12.566370614359172 + +Note that after I changed the diameter attribute, when I called the ``area()`` method it used the new diameter. Simple attribute access changed the state of the object. + +So you now know how to: + + * Define a class + * Give the class shared (class) attributes + * Add an initializer to set up its initial state + * Add methods to manipulate that state. + * Add methods that return the results of calculations of the current state + +You can do a lot with this simple functionality. Frankly, all creating classes like this has done is put everything together in a neat package -- which is very useful, but hasn't given you much new power. + +But it's a good idea to get the hang of using classes, and methods, and ``self`` for a bit before moving on to the more powerful feature of subclassing. + + + + diff --git a/_sources/modules/Recursion.rst.txt b/_sources/modules/Recursion.rst.txt new file mode 100644 index 0000000..b3e91ec --- /dev/null +++ b/_sources/modules/Recursion.rst.txt @@ -0,0 +1,160 @@ +######### +Recursion +######### + +You've seen functions that call other functions. + +If a function calls *itself*, we call that **recursion** + +Like with other functions, a call within a call establishes a *call stack* + +With recursion, if you are not careful, this stack can get *very* deep. + +Python has a maximum limit to how much it can recurse. This is intended to +save your machine from running out of RAM. + +Recursion is especially useful for a particular set of problems. + +For example, take the case of the *factorial* function. + +In mathematics, the *factorial* of an integer is the result of multiplying that +integer by every integer smaller than it down to 1. + +:: + + 5! == 5 * 4 * 3 * 2 * 1 + +We can use a recursive function nicely to model this mathematical function: + +:: + + 1! = 1 + 2! = 2 * 1 = 2 * 1! + 3! = 3 * 2 * 1 = 3 * 2! + +So we have a pattern here -- each value can be defined in terms of the previous value. + +So generically:: + + 1! = 1 + n! = n * (n-1)! + +How would we put that in code? Pretty straightforward translation: + +.. code-block:: python + + def factorial(n): + return n * factorial(n-1) + +That was pretty easy -- what happens when we run it? + +.. code-block:: ipython + + In [2]: factorial(3) + --------------------------------------------------------------------------- + RecursionError Traceback (most recent call last) + in () + ----> 1 factorial(3) + + in factorial(n) + 1 def factorial(n): + ----> 2 return n * factorial(n-1) + + ... last 1 frames repeated, from the frame below ... + + in factorial(n) + 1 def factorial(n): + ----> 2 return n * factorial(n-1) + + RecursionError: maximum recursion depth exceeded + +OOPS! that didn't work -- why not? Let's add a print... + +.. code-block:: python + + def factorial(n): + print("factorial called with", n) + return n * factorial(n-1) + +And call it: + +.. code-block:: ipython + + In [5]: factorial(3) + factorial called with 3 + factorial called with 2 + factorial called with 1 + factorial called with 0 + factorial called with -1 + factorial called with -2 + factorial called with -3 + factorial called with -4 + factorial called with -5 + ... + in factorial(n) + 1 def factorial(n): + 2 print("factorial called with", n) + ----> 3 return n * factorial(n-1) + + RecursionError: maximum recursion depth exceeded while calling a Python object + +Now it's clear what's going on -- each time you call the function, it calls itself with a value one less -- but then it just keeps going into the deep negative numbers, and only stops because Python reaches its recursion limit. + +This makes clear a core requirement of recursive functions: + + **Recursive functions must have a termination criteria!** + +That is, there must be a case (or more than one) for which they return a direct value. What should that be for factorial? Well, it's part of the definition that 1! == 1 -- so let's put that in our function: + +.. code-block:: python + + def factorial(n): + print("factorial called with", n) + if n == 1: + return 1 + return n * factorial(n-1) + +and try that: + +.. code-block:: ipython + + In [7]: factorial(3) + factorial called with 3 + factorial called with 2 + factorial called with 1 + Out[7]: 6 + +Much better! Try it out now with various values, and maybe without the print: + +.. code-block:: ipython + + In [14]: factorial(1) + Out[14]: 1 + + In [15]: factorial(2) + Out[15]: 2 + + In [16]: factorial(3) + Out[16]: 6 + + In [17]: factorial(4) + Out[17]: 24 + +Looking good! + +Exercise for the reader: What happens if you pass in a negative number? +Think about it first, before you try it. Hint -- it won't work! +How would you change your code to make it more robust? + +Summary +------- + +* Whenever you have a function that can be defined in terms of itself, you have a use case for recursion. It can make for nice compact, clear code. + +* Python will create a new "stack frame" for each call to the function -- so each call is kept separate, with separate local variables. + +But: + +* Python has a limited recursion depth -- so it can't be used for "big" problems. + +* You do need to make sure the calls will terminate. diff --git a/_sources/modules/Sequences.rst.txt b/_sources/modules/Sequences.rst.txt new file mode 100644 index 0000000..12dc20b --- /dev/null +++ b/_sources/modules/Sequences.rst.txt @@ -0,0 +1,1211 @@ +:orphan: + +.. _sequences: + +################ +Python Sequences +################ + + +Ordered collections of objects + + +What is a Sequence? +=================== + +A sequence is an ordered collection of objects. + +They are analogous to what are often called "arrays" or "lists" in other programming languages. + +But in Python, there are number of types that all fit this description, each with special customization. But any object that has the behavior expected of a sequence can be treated the same way in Python: + +Remember Duck Typing? + +If it looks like a duck and quacks like a duck... + +OR: If it looks and acts like a sequence -- it **is** a sequence. + +Technically, if it satisfies the "Sequence Protocol", it is a sequence. + +Python is all about these protocols -- we will see more of them. + +The Sequence Protocol +--------------------- + +A *sequence* can be considered as anything that supports *at least* these operations: + +* Indexing +* Slicing +* Membership +* Concatenation +* Length +* Iteration + +I'll get into all of those as we go along. + +Sequence Types +-------------- + +There are eight built in types in Python that are *sequences*: + +* string +* list +* tuple +* bytes +* bytearray +* buffer +* array.array +* range object (almost) + +For this lesson, you won't see much beyond strings, lists, and tuples -- +the rest are pretty special purpose. + +But what we learn in this lesson applies to all sequences (with minor caveats). + +I'll use lists, strings and tuples in the examples. + +So let's take a look at the key parts of the sequence protocol: + +Indexing +======== + +Items in a sequence may be looked up by *index* using the indexing +operator: ``[]`` + +Indexing in Python always starts at zero. + +Here is an example with a string -- a string is a sequence of characters. + +.. code-block:: ipython + + In [98]: s = "this is a string" + In [99]: s[0] + Out[99]: 't' + In [100]: s[5] + Out[100]: 'i' + +Note that the first character is indexed with zero -- I sometimes call that the "zeroth" item in the sequence. + +Zero indexing may seem odd at first (if you are not already a programming geek), but it turns out to make a lot of things easier. More on that later. + +You can use negative indexes to count from the end: + +.. code-block:: ipython + + In [2]: a_list = [34, 56, 19, 23, 55] + + In [3]: a_list[-1] + Out[3]: 55 + + In [4]: a_list[-2] + Out[4]: 23 + + In [5]: a_list[-4] + Out[5]: 56 + + +Indexing beyond the end of a sequence causes an IndexError: + +.. code-block:: ipython + + In [6]: a_list + Out[6]: [34, 56, 19, 23, 55] + + In [7]: a_list[5] + --------------------------------------------------------------------------- + IndexError Traceback (most recent call last) + in () + ----> 1 a_list[5] + + IndexError: list index out of range + +Pretty straight forward so far... + +Slicing +------- + +Slicing is a real "power tool" of Python -- it can allow very short code. + +Slicing a sequence creates a new sequence with a range of objects from the +original sequence. + +It also uses the indexing operator (``[]``), but with a twist. + +``sequence[start:finish]`` returns all `sequence[i]` for which `start <= i < finish` + +That's a fancy way to say that it's all the items from start to finish -- including start, but NOT including finish. + +This also may be a bit unintuitive -- but it's very practical. + +.. code-block:: ipython + + In [121]: s = "a bunch of words" + In [122]: s[2] + Out[122]: 'b' + In [123]: s[6] + Out[123]: 'h' + In [124]: s[2:6] + Out[124]: 'bunc' + In [125]: s[2:7] + Out[125]: 'bunch' + +Helpful Hint +------------ + +It can really help if you think about slicing this way: + +(write this out!) + +Think of the indexes as pointing to the spaces between the items:: + + a b u n c h o f w o r d s + | | | | | | | | | | | | | | | | + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + +Slicing +------- + +Python has some other slicing shortcuts... + +You do not have to provide both ``start`` and ``finish``: + +.. code-block:: ipython + + In [6]: s = "a bunch of words" + In [7]: s[:5] + Out[7]: 'a bun' + In [8]: s[5:] + Out[8]: 'ch of words' + +Either ``0`` or ``len(s)`` will be assumed, respectively. + +You can combine this with the negative index to get the end of a sequence: + +.. code-block:: ipython + + In [4]: s = 'this_could_be_a_filename.txt' + In [5]: s[:-4] + Out[5]: 'this_could_be_a_filename' + In [6]: s[-4:] + Out[6]: '.txt' + +**That** is a real-world example I use all the time. + +Why start from zero? +-------------------- + +Python indexing feels 'weird' to some folks -- particularly those that don't come with a background in the C family of languages. + +Why is the "first" item indexed with **zero**? + +Why is the last item in the slice **not** included? + +*Because* these lead to some nifty properties:: + + len(seq[a:b]) == b-a + + seq[:b] + seq[b:] == seq + + len(seq[:b]) == b + + len(seq[-b:]) == b + +There are very many fewer "off by one" errors as a result. + +More on Slicing +--------------- + +Slicing takes a third argument: ``step`` which controls which items are +returned: + +.. code-block:: ipython + + In [18]: a_tuple + Out[18]: (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19) + + In [19]: a_tuple[0:15] + Out[19]: (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14) + + In [20]: a_tuple[0:15:2] + Out[20]: (0, 2, 4, 6, 8, 10, 12, 14) + + In [21]: a_tuple[0:15:3] + Out[21]: (0, 3, 6, 9, 12) + + In [22]: a_tuple[::-1] + Out[22]: (19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0) + +Very cool -- a negative step reverses the results! + +Slicing vs. Indexing +-------------------- + +Though they share an operator, slicing and indexing have a few important +differences: + +* Indexing will always return one single object (a scalar), whereas slicing will return a sequence of objects. + +So if you start with, say, a list of numbers, indexing will return a single number. Slicing, on the other hand, will return list of numbers -- even if that list only has one number in it -- or zero! + +Note that strings are a bit of an exception -- there is no character type in Python -- so a single character is a string -- a sequence of length-1. + +* Indexing past the end of a sequence will raise an error, slicing will not: + +.. code-block:: ipython + + In [129]: s = "a bunch of words" + In [130]: s[17] + ----> 1 s[17] + IndexError: string index out of range + In [131]: s[10:20] + Out[131]: ' words' + In [132]: s[20:30] + Out[132]: '' + +(try it yourself....) + +Membership +========== + +All sequences support the ``in`` and ``not in`` membership operators: + +.. code-block:: ipython + + In [15]: s = [1, 2, 3, 4, 5, 6] + In [16]: 5 in s + Out[16]: True + In [17]: 42 in s + Out[17]: False + In [18]: 42 not in s + Out[18]: True + + +For strings, the membership operations are like ``substring`` operations in +other languages: + +.. code-block:: ipython + + In [20]: s = "This is a long string" + In [21]: "long" in s + Out[21]: True + +This does not work for sub-sequences of other types (can you think of why?): + +.. code-block:: ipython + + In [22]: s = [1, 2, 3, 4] + In [23]: [2, 3] in s + Out[23]: False + + +Concatenation +============= + +Using ``+`` or ``*`` on sequences will *concatenate* them: + +.. code-block:: ipython + + In [18]: l1 = [1,2,3,4] + In [19]: l2 = [5,6,7,8] + In [20]: l1 + l2 + Out[20]: [1, 2, 3, 4, 5, 6, 7, 8] + In [21]: (l1+l2) * 2 + Out[21]: [1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8] + +Multiplying and Slicing +----------------------- + +You can apply this concatenation to slices as well, leading to some nicely +concise code: + +from CodingBat: Warmup-1 -- front3 + +.. code-block:: python + + def front3(str): + if len(str) < 3: + return str+str+str + else: + return str[:3]+str[:3]+str[:3] + +This non-pythonic solution can also be expressed like so: + +.. code-block:: python + + def front3(str): + return str[:3] * 3 + +Length +====== + +All sequences have a length. You can get it with the ``len`` builtin: + +.. code-block:: ipython + + In [36]: s = "how long is this, anyway?" + In [37]: len(s) + Out[37]: 25 + +Remember: Sequences are 0-indexed, so the last index is ``len(s)-1``: + +.. code-block:: ipython + + In [38]: count = len(s) + In [39]: s[count] + ------------------------------------------------------------ + IndexError Traceback (most recent call last) + in () + ----> 1 s[count] + IndexError: string index out of range + +Better to use ``s[-1]`` + + +Miscellaneous +============= + +There are a bunch more operations supported by most sequences. +Min and Max +----------- + +All sequences also support the ``min`` and ``max`` builtins: + +.. code-block:: ipython + + In [42]: all_letters = "thequickbrownfoxjumpedoverthelazydog" + + In [43]: min(all_letters) + Out[43]: 'a' + + In [44]: max(all_letters) + Out[44]: 'z' + +Why are those the answers you get? (hint: ``ord('a')``) + +Of course this works with numbers, too! + +.. code-block:: ipython + + In [1]: seq = [4,2,8,3,5,8,5,7] + + In [2]: min(seq) + Out[2]: 2 + + In [3]: max(seq) + Out[3]: 8 + + +Index +----- + +All sequences also support the ``index`` method, which returns the index of the first occurrence of an item in the sequence: + +.. code-block:: ipython + + In [46]: all_letters.index('d') + Out[46]: 21 + +This causes a ``ValueError`` if the item is not in the sequence: + +.. code-block:: ipython + + In [47]: all_letters.index('A') + --------------------------------------------------------------------------- + ValueError Traceback (most recent call last) + in () + ----> 1 all_letters.index('A') + + ValueError: substring not found + +Count +----- + +A sequence can also be queried for the number of times a particular item +appears: + +.. code-block:: ipython + + In [52]: all_letters.count('o') + Out[52]: 4 + In [53]: all_letters.count('the') + Out[53]: 2 + +This does not raise an error if the item you seek is not present: + +.. code-block:: ipython + + In [54]: all_letters.count('A') + Out[54]: 0 + + +Iteration +========= + +All sequences are "iterables". + +You can iterate over a sequence with ``for``: + +.. code-block:: python + + for element in sequence: + do_something(element) + +Which is what we mean when we say a sequence is an "iterable". + +There are some complexities about that -- but more on that in another lesson. + + +Lists, Tuples... +================ + + +The *primary* sequence types. + +Lists +----- + +Lists can be constructed using list literals (``[]``): + +.. code-block:: ipython + + In [1]: [] + Out[1]: [] + In [2]: [1,2,3] + Out[2]: [1, 2, 3] + In [3]: [1, 'a', 7.34] + Out[3]: [1, 'a', 7.34] + +Or by using the ``list`` type object as a constructor: + +.. code-block:: ipython + + In [6]: list() + Out[6]: [] + In [7]: list(range(4)) + Out[7]: [0, 1, 2, 3] + In [8]: list('abc') + Out[8]: ['a', 'b', 'c'] + +It will take any "iterable" (which means any sequence automatically -- remember that all sequences are iterable?) + +List Elements +------------- + +The elements contained in a list need not be of a single type. + +Lists are *heterogenous*, *ordered* collections. + +Each element in a list is a value, and can be in multiple lists and have +multiple names (or no name): + +.. code-block:: ipython + + In [9]: name = 'Brian' + In [10]: a = [1, 2, name] + In [11]: b = [3, 4, name] + In [12]: a[2] + Out[12]: 'Brian' + In [13]: b[2] + Out[13]: 'Brian' + In [14]: a[2] is b[2] + Out[14]: True + +Notice that even with a "literal" -- the elements don't need to be literals as well -- they can be names. + +They can even be function calls: + +.. code-block:: ipython + + In [4]: def fun(n): + ...: return n * 2 + ...: + + In [5]: l = [3, 'four', fun(3), fun(9)] + + In [6]: l + Out[6]: [3, 'four', 6, 18] + + +Tuples +------ + +Tuples can be constructed using tuple literals (``()``): + +.. code-block:: ipython + + In [15]: () + Out[15]: () + In [16]: (1, 2) + Out[16]: (1, 2) + In [17]: (1, 'a', 7.65) + Out[17]: (1, 'a', 7.65) + In [18]: (1,) + Out[18]: (1,) + +Tuples and Commas... +-------------------- + +Tuples don't NEED parentheses... + +.. code-block:: ipython + + In [161]: t = (1,2,3) + In [162]: t + Out[162]: (1, 2, 3) + In [163]: t = 1,2,3 + In [164]: t + Out[164]: (1, 2, 3) + In [165]: type(t) + Out[165]: tuple + + +But they *do* need commas...! + +.. code-block:: ipython + + In [156]: t = ( 3 ) + In [157]: type(t) + Out[157]: int + In [158]: t = ( 3, ) + In [160]: type(t) + Out[160]: tuple + +This is a Python "gotcha" -- some folks on my team recently had a weird bug that two of them could not figure out. They were getting a type error -- something like: + +TypeError: unsupported operand type(s) for /: 'tuple' and 'float' + +which made no sense -- there were no tuples involved -- in this case, the value was being pulled from a list -- and it WAS a float. They even put type checking code in there, and it was, indeed, a float. + +After poking at the code a bit, I suddenly spotted an extra comma -- BINGO! that was it. + +The code was more involved, and thus harder to see, but it was pretty much like this: + +.. code-block:: python + + In [16]: l = [3, 4, 5, 6] + + In [17]: x = l[3], + +then a bit further down, x was used: + +.. code-block:: python + + In [18]: y = x / 2.0 + --------------------------------------------------------------------------- + TypeError Traceback (most recent call last) + in () + ----> 1 y = x / 2.0 + + TypeError: unsupported operand type(s) for /: 'tuple' and 'float' + +Would you have seen that? + +Converting something to a Tuple +------------------------------- + +You can also use the ``tuple`` type object to convert any iterable (sequence) into a tuple: + +.. code-block:: ipython + + In [20]: tuple() + Out[20]: () + In [21]: tuple(range(4)) + Out[21]: (0, 1, 2, 3) + In [22]: tuple('garbanzo') + Out[22]: ('g', 'a', 'r', 'b', 'a', 'n', 'z', 'o') + + +Tuple Elements +-------------- + +The elements contained in a tuple need not be of a single type. + +Tuples are *heterogenous*, *ordered* collections. + +Each element in a tuple is a value, and can be in multiple tuples and have +multiple names (or no name): + +.. code-block:: ipython + + In [23]: name = 'Brian' + In [24]: other = name + In [25]: a = (1, 2, name) + In [26]: b = (3, 4, other) + In [27]: for i in range(3): + ....: print(a[i] is b[i], end=' ') + ....: + False False True + +Look familiar from lists?? + +Lists vs. Tuples +---------------- + + + So why have both? + +Mutability +========== + +.. image:: /_static/transmogrifier.jpg + :align: center + :width: 35% + :alt: Presto change-o + + +image from flickr by `illuminaut`_, (CC by-nc-sa) + +.. _illuminaut: https://www.flickr.com/photos/illuminaut/3595530403 + + +Mutability in Python +==================== + +All objects in Python fall into one of two camps: + +* Mutable +* Immutable + +Objects which are mutable may be *changed in place*. + +Objects which are immutable may not be changed. + +Ever. + +The Types We Know +----------------- + +========= =========== +Immutable Mutable +========= =========== +String List +Integer Dictionary +Float +Tuple +========= =========== + +This may make it look like the Mutables are rare -- but in fact, most "container types", and most custom objects are mutable. + +Immutable types are the exception + +Lists Are Mutable +----------------- + +Try this out: + +.. code-block:: ipython + + In [28]: food = ['spam', 'eggs', 'ham'] + In [29]: food + Out[29]: ['spam', 'eggs', 'ham'] + In [30]: food[1] = 'raspberries' + In [31]: food + Out[31]: ['spam', 'raspberries', 'ham'] + + + +We repeat the exercise with a Tuple: + +.. code-block:: ipython + + In [32]: food = ('spam', 'eggs', 'ham') + In [33]: food + Out[33]: ('spam', 'eggs', 'ham') + In [34]: food[1] = 'raspberries' + --------------------------------------------------------------------------- + TypeError Traceback (most recent call last) + in () + ----> 1 food[1] = 'raspberries' + + TypeError: 'tuple' object does not support item assignment + + +Watch Out when name binding +--------------------------- + +This property means you need to be aware of what you are doing with your lists: + +.. code-block:: ipython + + In [36]: original = [1, 2, 3] + In [37]: altered = original + In [38]: for i in range(len(original)): + ....: if True: + ....: altered[i] += 1 + ....: + +Perhaps we want to check to see if altered has been updated, as a flag for +whatever condition caused it to be updated. + +What is the result of this code? + +Perhaps Not What You Expect +--------------------------- + +Our ``altered`` list has been updated as we'd expect: + +.. code-block:: ipython + + In [39]: altered + Out[39]: [2, 3, 4] + +But so has the ``original`` list: + +.. code-block:: ipython + + In [40]: original + Out[40]: [2, 3, 4] + +Why? + +Let's look at that code again. + +What does the line: ``altered = original`` do? + +It binds the name: "altered" to the same object that "original" is bound to. + +That is, there is only one list, even though it is referred to by two names. So when you mutate (or change) that list from *either* name, the changes show up when you refer to it by the other name. + +Other Gotchas +------------- + +Easy container setup, or deadly trap? + +Say you want something like sort of like a 2D array -- one way to do that is to nest lists -- make a list of lists. + +ONe seemingobvious way to create an empty list of lists would be to use multiplcation of lists -- make a list with one list in it, and then multiply it by the number of lists you want: + +.. code-block:: ipython + + In [12]: bins = [ [] ] * 5 + + In [13]: bins + Out[13]: [[], [], [], [], []] + +OK -- that worked -- you have a list with five empty lists in it. So let's try using that. This is a very contrived example, but say you have list of words: + +.. code-block:: ipython + + In [14]: words = ['one', 'three', 'rough', 'sad', 'goof'] + +and you want to put one in each of the "inside" lists: + +.. code-block:: ipython + + + In [15]: # loop five times + ...: for i in range(5): + ...: # add a word to the corresponding bin + ...: bins[i].append(words[i]) + +So, what is going to be in ``bins`` now? Think for a bit first -- you added one word to each bin, yes? But are those "sublists" independent? + +There is only **One** bin +------------------------- + +.. code-block:: ipython + + In [16]: bins + Out[16]: + [['one', 'three', 'rough', 'sad', 'goof'], + ['one', 'three', 'rough', 'sad', 'goof'], + ['one', 'three', 'rough', 'sad', 'goof'], + ['one', 'three', 'rough', 'sad', 'goof'], + ['one', 'three', 'rough', 'sad', 'goof']] + +Whoa! So we don't have 5 lists -- we have five *references* to the same list. Remember that in Python you can have any number of names "bound" to any object -- and any object can be contained in any number of containers, or multiple times in one container. + +So when we multiplied a sequence containing a single *mutable* object. We got a list containing five references to a single *mutable* object. + +Since it's mutable -- you can change it "in place", and when you change it -- the change shows everywhere that list is referenced. + +So how to make a list of independent lists? You need to loop and call that code that makes an empty list each time in the loop, something like this: + +.. code-block:: ipython + + In [21]: bins = [] + + In [22]: for i in range(5): + ...: bins.append([]) + ...: + + In [23]: bins + Out[23]: [[], [], [], [], []] + + In [24]: # loop five times + ...: for i in range(5): + ...: # add a word to the corresponding bin + ...: bins[i].append(words[i]) + ...: + + In [25]: bins + Out[25]: [['one'], ['three'], ['rough'], ['sad'], ['goof']] + + +Mutable Default Argument +------------------------ + +Watch out especially for passing mutable objects as default values for function parameters: + +.. code-block:: ipython + + In [71]: def accumulator(count, ac_list=[]): + ....: for i in range(count): + ....: ac_list.append(i) + ....: return ac_list + ....: + In [72]: accumulator(5) + Out[72]: [0, 1, 2, 3, 4] + In [73]: accumulator(7) + Out[73]: [0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 5, 6] + +What is going on here??? + +It turns out that that code: ``ac_list=[]`` is evaluated *when the function is defined* -- **not** when the function is called. + +So the name "ac_list" in the local scope of that function always refers to the same list. So every time the function is called, more is added to that same list. + +The moral of the story here is: + +**Do not use mutable objects for default arguments!** + +It turns out that this early evaluation can be useful -- but for now, just remember not to use mutables as default arguments. + +By the way --this is how you *should* write that code: + +.. code-block:: ipython + + In [21]: def accumulator(count, ac_list=None): + ...: if ac_list is None: + ...: ac_list = [] + ...: for i in range(count): + ...: ac_list.append(i) + ...: return ac_list + + In [22]: accumulator(5) + Out[22]: [0, 1, 2, 3, 4] + + In [23]: accumulator(7) + Out[23]: [0, 1, 2, 3, 4, 5, 6] + +This will ensure that a new list will be created if one is not passed-in. + + +Mutable Sequence Methods +======================== + +In addition to all the methods supported by sequences we've seen above, mutable sequences (the list), have a number of other methods that are used to change it in place. + +You can find all these in the Standard Library Documentation: + +https://docs.python.org/3/library/stdtypes.html#typesseq-mutable + + +Assignment +----------- + +You've already seen changing a single element of a list by assignment. + +Pretty much the same as "arrays" in most languages: + +.. code-block:: ipython + + In [100]: my_list = [1, 2, 3] + In [101]: my_list[2] = 10 + In [102]: my_list + Out[102]: [1, 2, 10] + + +Growing the List +---------------- + +``.append()``, ``.insert()``, ``.extend()`` + +.. code-block:: ipython + + In [74]: food = ['spam', 'eggs', 'ham'] + In [75]: food.append('sushi') + In [76]: food + Out[76]: ['spam', 'eggs', 'ham', 'sushi'] + In [77]: food.insert(0, 'beans') + In [78]: food + Out[78]: ['beans', 'spam', 'eggs', 'ham', 'sushi'] + In [79]: food.extend(['bread', 'water']) + In [80]: food + Out[80]: ['beans', 'spam', 'eggs', 'ham', 'sushi', 'bread', 'water'] + + +More on Extend +-------------- + +You can pass any sequence to ``.extend()``: + +.. code-block:: ipython + + In [85]: food + Out[85]: ['beans', 'spam', 'eggs', 'ham', 'sushi', 'bread', 'water'] + In [86]: food.extend('spaghetti') + In [87]: food + Out[87]: + ['beans', 'spam', 'eggs', 'ham', 'sushi', 'bread', 'water', + 's', 'p', 'a', 'g', 'h', 'e', 't', 't', 'i'] + +So be careful -- a string is a single object -- but also a sequence of characters (technically a sequence of length-1 strings). + + +Shrinking a list +---------------- + +``.pop()``, ``.remove()`` + +.. code-block:: ipython + + In [203]: food = ['spam', 'eggs', 'ham', 'toast'] + In [204]: food.pop() + Out[204]: 'toast' + In [205]: food.pop(0) + Out[205]: 'spam' + In [206]: food + Out[206]: ['eggs', 'ham'] + In [207]: food.remove('ham') + In [208]: food + Out[208]: ['eggs'] + + +You can also delete *slices* of a list with the ``del`` keyword: + +.. code-block:: ipython + + In [92]: nums = range(10) + In [93]: nums + Out[93]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + In [94]: del nums[1:6:2] + In [95]: nums + Out[95]: [0, 2, 4, 6, 7, 8, 9] + In [96]: del nums[-3:] + In [97]: nums + Out[97]: [0, 2, 4, 6] + + +Copying Lists +------------- + +You can make copies of part of a list using *slicing*: + +.. code-block:: ipython + + In [227]: food = ['spam', 'eggs', 'ham', 'sushi'] + In [228]: some_food = food[1:3] + In [229]: some_food[1] = 'bacon' + In [230]: food + Out[230]: ['spam', 'eggs', 'ham', 'sushi'] + In [231]: some_food + Out[231]: ['eggs', 'bacon'] + +If you provide *no* arguments to the slice, it makes a copy of the entire list: + +.. code-block:: ipython + + In [232]: food + Out[232]: ['spam', 'eggs', 'ham', 'sushi'] + In [233]: food2 = food[:] + In [234]: food is food2 + Out[234]: False + + +Shallow Copies +-------------- + +The copy of a list made this way is a *shallow copy*. + +The list is itself a new object, but the objects it contains are not. + +*Mutable* objects in the list can be mutated in both copies: + +.. code-block:: ipython + + In [249]: food = ['spam', ['eggs', 'ham']] + In [251]: food_copy = food[:] + In [252]: food[1].pop() + Out[252]: 'ham' + In [253]: food + Out[253]: ['spam', ['eggs']] + In [256]: food.pop(0) + Out[256]: 'spam' + In [257]: food + Out[257]: [['eggs']] + In [258]: food_copy + Out[258]: ['spam', ['eggs']] + + +Copies can solve problems +------------------------- + +Consider this common pattern: + +.. code-block:: python + + for x in somelist: + if should_be_removed(x): + somelist.remove(x) + +This looks benign enough, but changing a list while you are iterating over it can be the cause of some pernicious bugs. + +The Problem +----------- + +For example: + +.. code-block:: ipython + + In [27]: l = list(range(10)) + In [28]: l + Out[28]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + In [29]: for item in l: + ....: l.remove(item) + ....: + In [30]: l + Out[30]: [1, 3, 5, 7, 9] + +Was that what you expected? + +The Solution +------------ + +Iterate over a copy, and mutate the original: + +.. code-block:: ipython + + In [33]: l = list(range(10)) + + In [34]: for item in l[:]: + ....: l.remove(item) + ....: + In [35]: l + Out[35]: [] + + +Miscellaneous List Methods +========================== + +These methods change a list in place and are not available on immutable sequence types. + +``.reverse()`` + +.. code-block:: ipython + + In [129]: food = ['spam', 'eggs', 'ham'] + In [130]: food.reverse() + In [131]: food + Out[131]: ['ham', 'eggs', 'spam'] + +``.sort()`` + +.. code-block:: ipython + + In [132]: food.sort() + In [133]: food + Out[133]: ['eggs', 'ham', 'spam'] + +Because these methods mutate the list in place, they have a return value of ``None`` + + +Custom Sorting +-------------- + +``.sort()`` can take an optional ``key`` parameter. + +It should be a function that takes one parameter (list items one at a time) and returns something that can be used for sorting: + +.. code-block:: ipython + + In [137]: def third_letter(string): + .....: return string[2] + .....: + In [138]: food.sort(key=third_letter) + In [139]: food + Out[139]: ['spam', 'eggs', 'ham'] + +You end up with the list sorted by the third letter in each element. + +List Performance +---------------- + +* indexing is fast and constant time: O(1) +* ``x in l`` is proportional to n: O(n) +* visiting all is proportional to n: O(n) +* operating on the end of list is fast and constant time: O(1) + + * append(), pop() + +* operating on the front (or middle) of the list depends on n: O(n) + + * ``pop(0)``, ``insert(0, v)`` + * But, reversing is fast. ``Also, collections.deque`` + +What the heck does this O() thing mean? That is known as "big O" notation for time complexity. What it does is provide an indication of how much more time an operation will take depending on how many items the operation is acting on. + +Check out the Python wiki entry on Time Complexity for more info: + +http://wiki.python.org/moin/TimeComplexity + + +Choosing Lists or Tuples +======================== + +Here are a few guidelines on when to choose a list or a tuple: + +* If it needs to be mutable: list + +* If it needs to be immutable: tuple + + * provides safety when passing to a function (and as a key in a dict) + +Otherwise ... taste and convention. + + +Convention +---------- + +Lists are typically homogeneous collections: +-- they always contain values of the same type +-- they simplify iterating, sorting, etc + +Tuples are mixed types: +-- they group multiple values into one logical thing +-- they are similar to simple C structs. + + +Other Considerations +-------------------- + +* Do you need to do the same operation to each element? + + * list + +* Is there a small collection of values which make a single logical item? + + * tuple + +* Do you want to document that these values won't change? + + * tuple + +* Do you want to build it iteratively? + + * list + +* Do you need to transform, filter, etc? + + * list + + +More Documentation +------------------ + +For more information, read the list docs: + +https://docs.python.org/3.6/library/stdtypes.html#mutable-sequence-types + +(actually any mutable sequence....) + diff --git a/_sources/modules/SpecialMethodsAndProtocols.rst.txt b/_sources/modules/SpecialMethodsAndProtocols.rst.txt new file mode 100644 index 0000000..33ebb96 --- /dev/null +++ b/_sources/modules/SpecialMethodsAndProtocols.rst.txt @@ -0,0 +1,429 @@ +.. _special_methods: + +########################### +Special Methods & Protocols +########################### + + +Special methods (also called *magic* methods) are the secret sauce to Python's duck typing. + +Defining the appropriate special methods in your classes is how you make your class act like the standard classes. + + +What's in a Name? +----------------- + +We've seen at least one special method so far:: + + __init__ + +It's all in the double underscores... + +Pronounced "dunder" + + +try: ``dir(2)`` or ``dir(list)`` + + +Generally Useful Special Methods +-------------------------------- + +Most classes should at least have these special methods: + +``object.__str__``: + Called by the str() built-in function and by the print function to compute + the *informal* string representation of an object. + +``object.__repr__``: + Called by the repr() built-in function to compute the *official* string representation of an object. + + Ideally: ``eval( repr(something) ) == something`` + + This means that the "repr" is what you type to create the object. In practice, this is impractical for complex objects... but it is still a more "formal" form. + + Note that if you don't define a ``__str__`` method, then the ``__repr__`` will be used. And the base class (``object``) has a ``__repr__`` defined, so every class automatically gets one -- but it's ugly :-) + + +Protocols +---------- + +The set of special methods needed to emulate a particular type of Python object is called a *protocol*. + +Your classes can "become" like Python built-in classes by implementing the methods in a given protocol. + +Remember, these are more *guidelines* than laws. Implement only what you need. + + +The Numerics Protocol +--------------------- + +Do you want your class to behave like a number? Implement these methods: + +.. code-block:: python + + object.__add__(self, other) + object.__sub__(self, other) + object.__mul__(self, other) + object.__matmul__(self, other) + object.__truediv__(self, other) + object.__floordiv__(self, other) + object.__mod__(self, other) + object.__divmod__(self, other) + object.__pow__(self, other[, modulo]) + object.__lshift__(self, other) + object.__rshift__(self, other) + object.__and__(self, other) + object.__xor__(self, other) + object.__or__(self, other) + +(or the fraction you actually need). + + +Operator Overloading +-------------------- + +Most of the previous examples map to "operators": ``+, - , *, //, /, %`` etc. This is often known as "operator overloading", as you are redefining what the operators mean for that specific type. + +Note that you can define these operators to do ANYTHING you want -- but it is a really good idea to only define them to mean something that makes sense in the usual way. + +One interesting exception to this rule is the ``pathlib.Path`` class, that has defined ``__truediv__`` to mean path concatenation: + +.. code-block:: ipython + + In [19]: import pathlib + + In [20]: p1 = pathlib.Path.cwd() + + In [21]: p1 + Out[21]: PosixPath('/Users/Chris/PythonStuff/UWPCE/PythonCertDevel') + + In [22]: p1 / "a_filename" + Out[22]: PosixPath('/Users/Chris/PythonStuff/UWPCE/PythonCertDevel/a_filename') + +While this is not division in any sense, the slash *is* used as a path separator -- so this does make intuitive sense. At least to me -- I think it's pretty cool. + + +Comparing +--------- + +If you want your objects to be comparable:: + + A > B + A < B + A >= B + +etc... + +There is a full set of magic methods you can use to override the "comparison operators" :: + + __lt__ : < (less than) + __le__ : <= (less than or equal) + __eq__ : == (equal) + __ge__ : >= (greater than or equal) + __gt__ : > (greater than) + __ne__ : != (not equal) + +These are known as the "rich comparison" operators, as they allow fuller featured comparisons. In particular, they are used by numpy to provide "element-wise" comparison -- that is, comparing two arrays yields an array of results, rather than a single result: + +.. code-block:: ipython + + In [26]: import numpy as np + + In [27]: arr1 = np.array([3,4,5,6,7,8,9]) + + In [28]: arr2 = np.array([9,2,6,2,6,3,9]) + + In [29]: arr1 > arr2 + Out[29]: array([False, True, False, True, True, True, False], dtype=bool) + + In [30]: arr1 == arr2 + Out[30]: array([False, False, False, False, False, False, True], dtype=bool) + +This is just one example -- the point is that for your particular class, you can define these comparisons however you want. + +Total Ordering +-------------- + +You may notice that those operators are kind of redundant -- if ``A > B is True`` then we know that ``A < B is False`` and ``A <= B is False``. + +In fact, there is a mathematical / computer science concept known as "Total Order": (https://en.wikipedia.org/wiki/Total_order), which strictly defines "well behaved" objects in this regard. + +There may be some special cases, where these rules may not apply for your classes (though I can't think of any :-) ), but for the most part, you want your classes, if they support comparisons at all, to be well behaved, or "total ordered". + +Because this is the common case, Python comes with a nifty utility that implements total ordering for you: + +https://docs.python.org/3.6/library/functools.html#functools.total_ordering + +It can be found in the functools module, and allows you to specify __eq__ and only one of: ``__lt__()``, ``__le__()``, ``__gt__()``, or ``__ge__()``. It will then fill in the others for you. + +Note: if you define only one, it should be ``__lt__``, because this is the one used for sorting (see below for more about that). + +Here is the truncated example from the docs: + +.. code-block:: python + + @total_ordering + class Student: + def __eq__(self, other): + return ((self.lastname.lower(), self.firstname.lower()) == + (other.lastname.lower(), other.firstname.lower())) + def __lt__(self, other): + return ((self.lastname.lower(), self.firstname.lower()) < + (other.lastname.lower(), other.firstname.lower())) + +Note that this makes it a lot easier than implementing all six comparison operators. However, if you read the doc, it lets you know that ``total_ordering`` has poor performance -- it is doing extra method call re-direction when the operators are used. If performance matters to your use case (and it probably doesn't), you need to write all six comparison dunders. + +Sorting +------- + +Python has a handful of sorting methods built in: + +``list.sort()`` -- for sorting a list in place. +``sorted(iterable)`` -- for creating a sorted copy of an iterable (sequence). + +And a couple of more obscure ones. + +In order for your custom objects to be sortable, they need the ``__lt__`` (less than) magic method defined -- that's about it. + +So if you are using the ``total_ordering`` decorator, it's best to define ``__eq__`` and ``__lt__`` -- that way sorting will be able to use a "native" method for sorting, and maybe get better performance. + +Sort key methods +---------------- + +By default, the sorting methods use ``__lt__`` for comparison, and that algorithm calls ``__lt__`` O(n log(n)) times. But if you pass a "key" function in to the sort call: + +``a_list.sort(key=key_fun)`` + +then the key_fun is only called n times, and if the key returns a simple type, like an integer or float, then the sorting will be faster. + +So it often helps to provide a sort_key() method on your class, so it can be passed in to the sort methods: + +.. code-block:: python + + class Simple: + """ + simple class to demonstrate a simple sorting key method + """ + + def __init__(self, val): + self.val = val + + def sort_key(self): + return self.val + +And to use it: + +.. code-block:: python + + list_of_Simple_objects.sort(key=Simple.sort_key) + +See: :download:`sort_key.py <../examples/sort_key.py>` for a complete example with timing. Here is an example of running it:: + + Timing for 10000 items + regular sort took: 0.04288s + key sort took: 0.004779s + performance improvement factor: 8.9726 + +So almost 9 times faster for a 10,000 item list. Pretty good, eh? + +An Example +---------- + +Each of these methods supports a common Python operation. + +For example, to make '+' work with a sequence type in a vector-like fashion, +implement ``__add__``: + +.. code-block:: python + + def __add__(self, v): + """return the element-wise vector sum of self and v + """ + assert len(self) == len(v) + return vector([x1 + x2 for x1, x2 in zip(self, v)]) + + +[a slightly more complete example may be seen here :download:`vector.py <../examples/object_oriented/vector.py>`] + +Emulating Standard types +========================= + +Making your classes behave like the built-ins. + + +The Container Protocol +---------------------- + +Want to make a container type? Here's what you need: + +.. code-block:: python + + object.__len__(self) + object.__getitem__(self, key) + object.__setitem__(self, key, value) + object.__delitem__(self, key) + object.__iter__(self) + object.__reversed__(self) + object.__contains__(self, item) + + object.__index__(self) + +``__len__`` is called when len(object) is called. + +``__reversed__`` is called when reversed(object) is called. + +``__contains__`` is called with ``in`` is used: ``something in object`` + +``__iter__`` is used for iteration -- called when in a for loop. + +``__index__`` is used to convert the object into an integer for indexing. So you don't define this in a container type but rather define it for a type so it can be used as an index. If you have a class that could reasonably be interpreted as an index, you should define this. It should return an integer. This was added to support multiple integer types for numpy. + + +Indexing and Slicing +-------------------- + +``__getitem__`` and ``set__item__`` are used when indexing: + +``x = object[i]`` calls ``__getitem__``, and ``object[i] = something`` calls ``__setitem__``. + +But indexing is pretty complex in python. There is simple indexing: ``object[i]``, but there is also slicing: ``object[i:j:skip]`` + +When you implement ``__getitem__(self, index)``, ``index`` will simply be the index if it's a simple index, but if it's slicing, it will be a ``slice`` object. Python also supports multiple slices: + +``object[a:b,c:d]`` + +These are used in numpy to support multi-dimensional arrays, for instance. + +In this case, a tuple of slice objects is passed in. + +See: :download:`index_slicing.py<../examples/object_oriented/index_slicing.py>` + +Callable classes +----------------- + +We've been using functions a lot: + +.. code-block:: python + + def my_fun(something): + do_something + ... + return something + +And then we can call it: + +.. code-block:: python + + result = my_fun(some_arguments) + + +But what if we need to store some data to know how to evaluate that function? + +Example: a function that computes a quadratic function: + +.. math:: + + y = a x^2 + bx + c + +You could pass in a, b and c each time: + +.. code-block:: python + + def quadratic(x, a, b, c): + return a * x**2 + b * x + c + +But what if you are using the same a, b, and c numerous times? + +Or what if you need to pass this in to something +(like map) that requires a function that takes a single argument? + +"Callables" +----------- + +Various places in Python expect a "callable" -- something that you can +call like a function: + +.. code-block:: python + + a_result = something(some_arguments) + +"Something" in this case is often a function, but can be anything else +that is "callable". + +What have we been introduced to recently that is "callable", but not a +function object? + +Custom callable objects +------------------------ + +The trick is one of Python's "magic methods" + +.. code-block:: python + + __call__(*args, **kwargs) + +If you define a ``__call__`` method in your class, it will be used when +code "calls" an instance of your class: + +.. code-block:: python + + class Callable: + def __init__(self, .....) + some_initilization + def __call__(self, some_parameters) + +Then you can do: + +.. code-block:: python + + callable_instance = Callable(some_arguments) + + result = callable_instance(some_arguments) + + +Callable example +---------------- + +An example of writing a callable class: + +Write a class for a quadratic equation. + +* The initializer for that class should take the parameters: ``a, b, c`` + +* It should store those parameters as attributes. + +* The resulting instance should evaluate the function when called, and return the result: + + +.. code-block:: python + + my_quad = Quadratic(a=2, b=3, c=1) + + my_quad(0) + +Here's one way to do that: +:download:`quadratic.py <../examples/quadratic/quadratic.py>` + +Protocols in Summary +-------------------- + +Use special methods when you want your class to act like a "standard" type or class in some way. + +Look up the special methods you need and define them (and only the ones you need). + +There's more to read about the details of implementing these methods: + +* https://docs.python.org/3.8/reference/datamodel.html#special-method-names + + +References +---------- + +Here is a good reference for magic methods: + +http://minhhh.github.io/posts/a-guide-to-pythons-magic-methods + +And with a bit more explanation: + +https://www.python-course.eu/python3_magic_methods.php + diff --git a/_sources/modules/StaticAndClassMethods.rst.txt b/_sources/modules/StaticAndClassMethods.rst.txt new file mode 100644 index 0000000..1ea8919 --- /dev/null +++ b/_sources/modules/StaticAndClassMethods.rst.txt @@ -0,0 +1,159 @@ +.. _static_and_class_methods: + + +######################## +Static and Class Methods +######################## + +You've seen how methods of a class are *bound* to an instance when it is +created. + +And you've seen how the argument ``self`` is then automatically passed to the method when it is called. + +And you've seen how you can call *unbound* methods on a class object so +long as you pass an instance of that class as the first argument. + + +**But what if you don't want or need an instance?** + + +Static Methods +-------------- + +A *static method* is a method that doesn't get self: + +.. code-block:: ipython + + In [36]: class StaticAdder: + + ....: @staticmethod + ....: def add(a, b): + ....: return a + b + ....: + + In [37]: StaticAdder.add(3, 6) + Out[37]: 9 + + +Where are static methods useful? + +Usually they aren't. It is often better just to write a module-level function. + +An example from the Standard Library (tarfile.py): + +.. code-block:: python + + class TarInfo: + # ... + @staticmethod + def _create_payload(payload): + """Return the string payload filled with zero bytes + up to the next 512 byte border. + """ + blocks, remainder = divmod(len(payload), BLOCKSIZE) + if remainder > 0: + payload += (BLOCKSIZE - remainder) * NUL + return payload + + +Class Methods +------------- + +A class method gets the class object, rather than an instance, as the first +argument + +.. code-block:: ipython + + In [41]: class Classy: + ....: x = 2 + ....: @classmethod + ....: def a_class_method(cls, y): + ....: print("in a class method: ", cls) + ....: return y ** cls.x + ....: + In [42]: Classy.a_class_method(4) + in a class method: + Out[42]: 16 + + +Why? +---- + +Unlike static methods, class methods are quite common. + +They have the advantage of being friendly to subclassing. + +Consider this: + +.. code-block:: ipython + + In [44]: class SubClassy(Classy): + ....: x = 3 + ....: + + In [45]: SubClassy.a_class_method(4) + in a class method: + Out[45]: 64 + +``a_class_method`` is defined in the ``Classy`` class (see above). +And it prints the class that it is called on. But despite being defined in ``Classy``, it gets the ``SubClassy`` class object as the first parameter (``cls``). +So a classmethod will "do the right thing" when used in a subclass. + +Alternate Constructors +----------------------- + +Because of this friendliness to subclassing, class methods are often used to +build alternate constructors. + +Consider the case of wanting to build a dictionary with a given iterable of +keys: + +.. code-block:: ipython + + In [57]: d = dict([1,2,3]) + --------------------------------------------------------------------------- + TypeError Traceback (most recent call last) + in () + ----> 1 d = dict([1,2,3]) + + TypeError: cannot convert dictionary update sequence element #0 to a sequence + + +The stock constructor for a dictionary won't work this way. So the dict object +implements an alternate constructor that *can*. + +.. code-block:: python + + @classmethod + def fromkeys(cls, iterable, value=None): + '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S. + If not specified, the value defaults to None. + ''' + self = cls() + for key in iterable: + self[key] = value + return self + +(This is actually from the ``OrderedDict`` implementation in ``collections.py``). + +See also ``datetime.datetime.now()``, etc.... + + +Properties, Static Methods and Class Methods are powerful features of Python's OO model. + +They are implemented using an underlying structure called *descriptors* + +`Here is a low level look`_ at how the descriptor protocol works. + +The cool part is that this mechanism is available to you, the programmer, as +well. + +.. _Here is a low level look: https://docs.python.org/2/howto/descriptor.html + + +For the Circle Excercise: use a class method to make an alternate constructor that takes the diameter instead. + +Ultimately, make a subclass of ``Circle``, called ``Sphere``. Check and see if the ``.from_diameter`` alternate consructor still works! + + + diff --git a/_sources/modules/Strings.rst.txt b/_sources/modules/Strings.rst.txt new file mode 100644 index 0000000..bab586e --- /dev/null +++ b/_sources/modules/Strings.rst.txt @@ -0,0 +1,605 @@ +.. _strings: + +####### +Strings +####### + + Fun with Strings + +Strings +======= + +.. admonition:: Joke + + A piece of string is new in town, and looking for a drink. He sees a local bar, walks in, sits down, and orders a beer. The bartender looks at him askance, and says: "wait a minute, are you a piece of string?". "Why yes", the string replies. The bartender growls back: "We don't serve your kind in here -- you get out!". + + Disappointed, the piece of string leaves and heads down the street to the next bar, but this time, he barely gets to a seat before being yelled at -- "we don't want any string in here -- you get out!" + + A third bar, and he has a similar encounter. Now he's getting pretty distraught and confused -- "what have they got against string in this town?", he asks himself. But he's also tired and thirsty, so he gets an idea. + + The piece of string twists himself all up, winding himself around and around. Then he reaches up and fluffs up the top of his head. + + Thus prepared, he heads into yet another bar. This time is different. He walks in, sits down, the bartender takes his order -- all good. But then just as the bartender is putting his beer down he stops, and looks hard at him: "wait a minute! you're a piece of string, aren't you?". + + Full of confidence, the string replies: "Nope, I'm a frayed knot." + +A "String" is a computerese word for a piece of text -- a "string" of characters. + +Why "string"? + +"String" can be used to mean "a linear sequence -- as of characters, words, proteins, etc." + +`Definition of string `_ + +So a string is a sequence of individual letters or characters. + +In Python, each character can be a `Unicode `_ character -- that is, any character in any language in the world. +Having this built in by default in Python(3) means that you can get very far simply ignoring it -- anything you can type on your computer can be used in strings in Python. +If you do need to work with non-English characters, or data encoded in non-utf-8, particularly on Python 2, here are some notes about that: :ref:`unicode`. +But for the most part, in Python3 -- strings are text, and text is strings, and that's that. If you know how to type characters (accented, etc.) that are not used in English on your computer, they should "just work". + +Creating strings: +----------------- + +A string literal creates a string type. + +(we've seen this already...) + +A literal can be delineated with single or double quotes, alone or in triples. + +:: + + "this is a string" + + 'So is this' + + """and this also""" + + '''and even this. + Triple quotes preserve newlines + so this is three lines''' + +You can also call the string object (``str()``) to "make" a string out of other data types. + +.. code-block:: ipython + + In [256]: str(34) + Out[256]: '34' + +Strings can also be read from files or other sources of I/O. + + +String Methods +=============== + +The python string object is very powerful with lots of methods for common text manipulation. "methods" are functions defined on an object itself (more on that when we get to OO programming). But it means that you have many ways to manipulate text built right into the string objects themselves. + +Note that strings are "immutable" -- they can not be changed once they have been created. So the string methods all return new strings, rather than change the string in place. Which is kind of handy if you want to string multiple operations together (pun intended ...). + +Here are just a few of the more common string methods: + + +Splitting and Joining Strings +----------------------------- + +``split`` and ``join`` can be used to break up a string into pieces, or make one big string out of multiple smaller pieces: + +.. code-block:: ipython + + In [167]: csv = "comma,separated,values" + + In [168]: csv.split(',') + Out[168]: ['comma', 'separated', 'values'] + + In [169]: psv = '|'.join(csv.split(',')) + + In [170]: psv + Out[170]: 'comma|separated|values' + +It may seem odd at first that ``.join()`` is a string method, rather than, say, a method on lists. But in fact, it makes a lot of sense. Lists (and tuples, and other sequences) can hold any type of data -- and "joining" arbitrary data types doesn't make any sense. Joining is strictly a string activity. + +And you need a string so you can join the parts with something (e.g. a space, or a comma, or ...) -- therefore, we need a string object in there somewhere anyway. + +Lastly, having join() be a string method means that it can join strings in ANY iterable object -- not just lists or other built-in sequence types. + +So it does make sense. But even if doesn't make sense to you, that's the way it is -- so remember that you call ``.join()`` on the string you want to join things with, not on the sequence. + +So to be clear: if you have a bunch of strings in a sequence and you want to put them together, you create a string with the character (or characters) you want to join them with, and call join() on that object: + +.. code-block:: python + + In [20]: # comma separated: + + In [21]: ",".join(["these", "are", "some", "strings"]) + Out[21]: 'these,are,some,strings' + + In [22]: # you can concatenate by joining with the empty string: + + In [23]: "".join(["these", "are", "some", "strings"]) + Out[23]: 'thesearesomestrings' + +Maybe not very common, but you can join with a longer string as well: + +.. code-block:: ipython + + In [5]: " --#-- ".join(["these", "are", "some", "strings"]) + Out[5]: 'these --#-- are --#-- some --#-- strings' + + +Building up a Long String. +-------------------------- + +An obvious thing to do is something like: + +.. code-block:: python + + msg = "" + for piece in list_of_stuff: + msg += piece + +But: strings are immutable -- Python needs to create a new string each time you add a piece, which is not very efficient. So it's better to gather all the pieces together in a list, and then join them together: + +.. code-block:: python + + msg = [] + for piece in list_of_stuff: + msg.append(piece) + " ".join(msg) + +appending to lists is efficient -- and so is the ``join()`` method of strings. In fact the `+=` approach is so inefficient that the `sum()` function explicitly forbids summing strings: + +.. code-block:: ipython + + In [2]: sum(stuff_to_join, "") + --------------------------------------------------------------------------- + TypeError Traceback (most recent call last) + in + ----> 1 sum(stuff_to_join, "") + + TypeError: sum() can't sum strings [use ''.join(seq) instead] + + +Case Switching +-------------- + +.. code-block:: ipython + + In [171]: sample = 'A long string of words' + + In [172]: sample.upper() + Out[172]: 'A LONG STRING OF WORDS' + + In [173]: sample.lower() + Out[173]: 'a long string of words' + + In [174]: sample.swapcase() + Out[174]: 'a LONG STRING OF WORDS' + + In [175]: sample.title() + Out[175]: 'A Long String Of Words' + + +Testing for certain classes of characters +----------------------------------------- + +.. code-block:: ipython + + In [181]: number = "12345" + + In [182]: number.isnumeric() + Out[182]: True + + In [183]: number.isalnum() + Out[183]: True + + In [184]: number.isalpha() + Out[184]: False + + In [185]: fancy = "Th!$ $tr!ng h@$ $ymb0l$" + + In [186]: fancy.isalnum() + Out[186]: False + + +String Literals +----------------- + +Sometimes when you are creating a string, you want to put an non-normal character in there -- one that isn't strictly a letter or symbol, such as newlines, etc. + +To do that, python support a set of "escape" sequences -- when a character follows a backslash, it gets interpreted as having a particular meaning. + +Common Escape Sequences:: + + \\ Backslash (\) + \a ASCII Bell (BEL) + \b ASCII Backspace (BS) + \n ASCII Linefeed (LF) + \r ASCII Carriage Return (CR) + \t ASCII Horizontal Tab (TAB) + \ooo Character with octal value ooo + \xhh Character with hex value hh + \uxxxx Character with Unicode code point value xxxx + \N{char-name} Character with Unicdoe name char_name + +For example -- for tab-separated values: + +.. code-block:: ipython + + In [25]: s = "these\tare\tseparated\tby\ttabs" + + In [12]: print(s) + these are separated by tabs + +https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals + +https://docs.python.org/3/library/stdtypes.html#string-methods + +Raw Strings +------------ + +There are times when you want a literal backslash in your string: Windows file paths, regular expressions. To make this easy, Python support "raw" strings -- string literals where the backslash does not have special meaning: + +Add an ``r`` in front of the string literal: + +**Escape Sequences Are Ignored** + +.. code-block:: ipython + + In [408]: print("this\nthat") + this + that + + In [409]: print(r"this\nthat") + this\nthat + +**Gotcha** + +.. code-block:: ipython + + In [415]: r"\" + SyntaxError: EOL while scanning string literal + +Putting a backslash right before the end quote confuses the interpreter! + +Raw strings can be very handy for things like regular expressions that need embedded backslashes. + +Building Long String Literals +----------------------------- + +If you put two string literals next to each other in the code, Python will join them into one when compiling: + +.. code-block:: ipython + + In [6]: "this" "that" + Out[6]: 'thisthat' + +(note: no comma in between!) +This may not look useful, but when combined with the fact that Python joins together lines when inside a parentheses, it can be a nice way to make larger string literals: + +.. code-block:: ipython + + In [7]: print("This is the first line\n" + ...: "And here is another line\n" + ...: "If I don't put in a newline " + ...: "I can get a very long line in, without making the" + ...: "line of code too long.") + This is the first line + And here is another line + If I don't put in a newline I can get a very long line in, without making the line of code too long. + +Ordinal values +-------------- + +Characters in strings are stored as numeric values: + +* "ASCII" values: 1-127 + +* Unicode "code points" -- 1 - 1,114,111 (!!!) + +Unicode supports a LOT of characters -- every character in every language known to man -- and then some :-). The Unicode code points for the characters in the ASCII character set are the same as ASCII -- so handy for us English speakers. + +To get the code point value, use ``ord()``: + +.. code-block:: ipython + + In [109]: for i in 'Chris': + .....: print(ord(i), end=' ') + 67 104 114 105 115 + +To get the character from the code point, use ``chr()``: + +.. code-block:: ipython + + In [110]: for i in (67,104,114,105,115): + .....: print(chr(i), end='') + Chris + +For the English language, stick with ASCII, otherwise use full Unicode: it's easy with Python3 + + +Building Strings from Data +-------------------------- + +We often have some data in Python variables -- maybe strings, maybe numbers -- and we often want to combine that data with text to make a custom message of some sort. + +You could, but please don't(!), do this: + +.. code-block:: python + + 'Hello ' + name + '!' + +(I know -- we did that in the grid_printing exercise) + +Why not? It's slow and not very flexible. Python provides a few ways to "format" text, so you can do this instead: + +.. code-block:: ipython + + In [11]: 'Hello {}!'.format(name) + Out[11]: 'Hello Chris!' + +It's much faster and safer, and easier to modify as code gets complicated. + +https://docs.python.org/3/library/string.html#string-formatting + + +Old and New string formatting +----------------------------- + +Back in early Python days, there was the string formatting operator: ``%`` + +.. code-block:: python + + "a string: %s and a number: %i "%("text", 45) + +This is very similar to C-style string formatting (`sprintf`). + +It's still around, and handy --- but ... + +The "new" ``format()`` method is more powerful and flexible, so we'll focus on that in this class. And there is now the newer "f-strings" (see below) which provide a lot of that "quick and dirty" convenience, while using the same formatting codes as ``.format()``. So there really isn't a reason to use the "old style" anymore. + + +String Formatting +----------------- + +The string ``.format()`` method: + +.. code-block:: ipython + + In [62]: "A decimal integer is: {:d}".format(34) + Out[62]: 'A decimal integer is: 34' + + In [63]: "a floating point is: {:f}".format(34.5) + Out[63]: 'a floating point is: 34.500000' + + In [64]: "a string is the default: {}".format("anything") + Out[64]: 'a string is the default: anything' + + +Multiple placeholders: +----------------------- + +.. code-block:: ipython + + In [65]: "the number is {} is {}".format('five', 5) + Out[65]: 'the number is five is 5' + + In [66]: "the first 3 numbers are {}, {}, {}".format(1,2,3) + Out[66]: 'the first 3 numbers are 1, 2, 3' + +The counts must agree: + +.. code-block:: ipython + + In [67]: "string with {} formatting {}".format(1) + --------------------------------------------------------------------------- + IndexError Traceback (most recent call last) + in () + ----> 1 "string with {} formatting {}".format(1) + + IndexError: tuple index out of range + + +Named Placeholders: +------------------- + +.. code-block:: ipython + + + In [69]: "Hello, {name}, whadaya know?".format(name="Joe") + Out[69]: 'Hello, Joe, whadaya know?' + +You can use values more than once, and skip values: + +.. code-block:: ipython + + In [73]: "Hi, {name}. Howzit, {name}?".format(name='Bob') + Out[73]: 'Hi, Bob. Howzit, Bob?' + + +The format operator works with string variables, too: + +.. code-block:: ipython + + In [80]: s = "{:d} / {:d} = {:f}" + + In [81]: a, b = 12, 3 + + In [82]: s.format(a, b, a/b) + Out[82]: '12 / 3 = 4.000000' + +So you can save a format string, or even built it up dynamically, and then use it in multiple places in the code. + + +Complex Formatting +------------------ + +There is a complete syntax for specifying all sorts of options. + +It's well worth your while to spend some time getting to know this +`formatting language`_. You can accomplish a great deal just with this. + +.. _formatting language: https://docs.python.org/3/library/string.html#format-specification-mini-language + +Here is a nice tutorial: + +https://pyformat.info/ + +And a nice formatting cookbook: + +https://mkaz.blog/code/python-string-format-cookbook/ + + +Literal String Interpolation +============================ + +In Python 3.6, yet another string formatting method was introduced. + +Known at "f-strings", or more formally, "Literal String Interpolation", they provide a concise, readable way to include the value of Python expressions inside strings. In particular, they make it easy to include names in the current namespace without having to type them multiple times. + +For example: + +.. code-block:: ipython + + In [24]: first = "Chris" + + In [25]: last = "Barker" + + In [26]: f"My name is {first} {last}" + Out[26]: 'My name is Chris Barker' + +Note that they are called "f-strings" because they are created by putting and "f" before the string -- "f" is for format. + +All the other ways to do this required a lot more typing: + +.. code-block:: ipython + + In [28]: "My name is {first} {last}".format(first=first, last=last) + Out[28]: 'My name is Chris Barker' + + In [29]: "My name is {} {}".format(first, last) + Out[29]: 'My name is Chris Barker' + + In [30]: "My name is %s %s" % (first, last) + Out[30]: 'My name is Chris Barker' + +Even more than the typing, it required keeping the string and the input data in sync when you changed things: maybe adding or removing an interpolated item. + +f-string basics +--------------- + +f-strings are actually pretty simple concept: + +You can interpolate the stringifcation of any expression into a string at run time. Variables are all evaluated at the current scope. + +The expression is put inside curly brackets: {}, the same as for the ``.format`` method. + +So what does that all mean? + +For this most simple example:: + + f"some text: {expression}" + +`expression` is any valid python expression (remember that an expression is a combination of values and operators and names that produces a value). + +The expression is evaluated, and then, if it is not a string, it is converted to one, so it's really:: + + f"some text: {str(expression)}" + +Let's see how this works in practice: + +.. code-block:: ipython + + In [32]: # define a couple of names: + + In [33]: x = 5 + + In [34]: y = 12 + + In [35]: name = "fred" + + In [36]: # a simple string: + + In [37]: f"some text: {name}" + Out[37]: 'some text: fred' + + In [38]: # if it's not a string, it will be turned into one: + + In [39]: f"some text: {x}" + Out[39]: 'some text: 5' + + In [40]: # but you can do a more complex expression as well: + + In [41]: f"some text: {x + y}" + Out[41]: 'some text: 17' + + In [42]: # and call methods: + + In [43]: f"some text: {name.capitalize()}" + Out[43]: 'some text: Fred' + + In [45]: # even boolean expressions: + + In [46]: f"some text: {name if x < 5 else name2}" + Out[46]: 'some text: bob' + +You can put ANY expression in there -- no matter how complex. But do be careful, if it's too complex, it will just make the code harder to read! + +And it has to be an expression, not a statement -- so you can't put a for loop or anything like that in there. + +You can see how this can be a very powerful and quick way to get things done. + +Formatting codes with f-strings +------------------------------- + +We've seen that f-strings will automatically "stringify" the results of the expression used. +And that's often what you want. +But if you do want to control how that is done, you can use all the same formatting codes used with the ``.format()`` method: + + +.. code-block:: ipython + + In [15]: f"One third with 4 digits is: {1/3:.3f}" + Out[15]: 'One third with 4 digits is: 0.333' + +you put the format specifier after a colon. + + + +f-string Use +------------ + +f-strings are a fairly new Python feature. They will cause a syntax error in any Python version older than 3.6 -- 3.6 was first released on December 23, 2016 -- only a couple years from this writing. + +So there is still not much out there in the wild, and it it still rare in production code. + +They are not currently used in many of the examples in this course. + +Nevertheless, they are a nifty feature that could be very useful, so feel free to use them where it makes you code cleaner and clearer. + +More on f-strings +----------------- + +To read all about the justification and syntax, read PEP 498: + +https://www.python.org/dev/peps/pep-0498/ + + +Other resources for f-strings +----------------------------- + +f-strings have been around a while now, so there are a number of good introductions out there: + +A short introduction: + +https://cito.github.io/blog/f-strings/ + +Another intro: + +https://www.pydanny.com/python-f-strings-are-fun.html + +http://zetcode.com/python/fstring/ + +One that gets into the technical details (bytecode! -- for the real geeks): + +https://hackernoon.com/a-closer-look-at-how-python-f-strings-work-f197736b3bdb + diff --git a/_sources/modules/SubclassingAndInheritance.rst.txt b/_sources/modules/SubclassingAndInheritance.rst.txt new file mode 100644 index 0000000..220b19a --- /dev/null +++ b/_sources/modules/SubclassingAndInheritance.rst.txt @@ -0,0 +1,327 @@ +.. _subclassing_inheritance: + +########################### +Subclassing and Inheritance +########################### + +How to put the pieces together to build a complex system without repeating code. + +Inheritance +=========== + +In object-oriented programming (OOP), inheritance is a way to reuse the code +of existing objects, or to establish a subtype from an existing object. + +Objects are defined by classes. Classes can inherit attributes and behavior +from pre-existing classes called base classes or super classes. + +The resulting classes are known as derived classes or subclasses. + +(http://en.wikipedia.org/wiki/Inheritance_%28object-oriented_programming%29) + +Subclassing +----------- + +A subclass "inherits" all the attributes (methods, etc) of the parent class. This means that a subclass will have everything that its "parents" have. + +You can then change ("override") some or all of the attributes to change the behavior. You can also add new attributes to extend the behavior. + +You create a subclass by passing the superclass to the ``class`` statement. + +The simplest subclass in Python: + +.. code-block:: python + + class A_subclass(The_superclass): + pass + +``A_subclass`` now has exactly the same behavior as ``The_superclass`` -- all the same attributes and methods. + +Overriding attributes +--------------------- + +Overriding is as simple as creating a new attribute with the same name: + +.. code-block:: ipython + + In [1]: class Circle: + ...: color = "red" + ...: + +We now have a class with a class attribute, ``color``, with the value: "red". All instances of ``Circle`` will be red: + +.. code-block:: ipython + + In [2]: c = Circle() + + In [3]: c.color + Out[3]: 'red' + +If we create a subclass of Circle, and set that same class attribute: + +.. code-block:: ipython + + In [4]: class NewCircle(Circle): + ...: color = "blue" + ...: + + In [5]: nc = NewCircle() + + In [6]: nc.color + Out[6]: 'blue' + +We now have a class that is all the same, except that its instances have the color blue. + +Note that any methods that refer to that attribute, will get the new value, even if the methods themselves have not changed: + +.. code-block:: ipython + + In [10]: class Circle: + ...: color = "red" + ...: + ...: def describe(self): + ...: return f"I am a {self.color} circle" + ...: + + In [11]: class NewCircle(Circle): + ...: color = "blue" + ...: + + In [12]: c = Circle() + + In [13]: c.describe() + Out[13]: 'I am a red circle' + + In [14]: nc = NewCircle() + + In [15]: nc.describe() + Out[15]: 'I am a blue circle' + +Note that this is *why* self is passed in to every method -- when you write the method, you don't know exactly what class ``self`` will be -- it is an instance of the class at the time the method is called. + +Overriding methods +------------------ + +Overriding methods is exactly the same thing, but with methods (remember, a method *is* an attribute in Python -- one that happens to be a function) + +.. code-block:: python + + class Circle: + ... + def grow(self, factor=2): + """grows the circle's diameter by factor""" + self.diameter = self.diameter * factor + ... + + class NewCircle(Circle): + ... + def grow(self, factor=2): + """grows the area by factor...""" + self.diameter = self.diameter * math.sqrt(2) + + +all the instances of the new class will have the new method -- similar, but different, behavior. Note that both these methods are requiring that the class instance has a ``diameter`` attribute. + + +**Here's a program design suggestion:** + + Whenever you override a method, the interface of the new method should be the same as the old. It should take the same parameters, return the same type, and obey the same preconditions and postconditions. + + If you obey this rule, you will find that any function designed to work with an instance of a superclass, like a Deck, will also work with instances of subclasses like a Hand or PokerHand. If you violate this rule, your code will collapse like (sorry) a house of cards. + +-- from *Think Python* + + +Overriding ``__init__`` +----------------------- + +``__init__`` is a common method to override. + +You often need to call the super class ``__init__`` as well, so that any initialization required is performed: + +.. code-block:: python + + class Circle: + color = "red" + def __init__(self, diameter): + self.diameter = diameter + ... + class CircleR(Circle): + def __init__(self, radius): + diameter = radius*2 + Circle.__init__(self, diameter) + + +Exception to: "don't change the method signature" rule. + +Often when you override ``__init__``, the new class may take an extra parameter or two. In this case, you will want to keep the signature as similar as possible, and cleanly define what is part of the subclass. A common idiom in this case is this: + +.. code-block:: python + + class A_Subclass(A_Superclass): + + def __init__(self, param1, param2, *args, **kwargs): + self.param1 = param1 + self.init_something(param2) + super().__init__(*args, **kwargs) + +That is: + + * Put the extra parameters in the beginning of the list -- usually as required positional parameters. + + * Accept ``*args`` and ``**kwargs`` + + * Pass everything else on to the superclass' __init__ + +Using ``*args`` and ``**kwargs`` is a way to make it clear that the rest is simply the signature of the superclass. It is also flexible if the superclass (or others up in the hierarchy) changes -- it could completely change its signature, and this subclass would still work. + + +Using the superclass' methods +------------------------------- + +In a subclass, you can access everything in the superclass: all attributes and other methods: + +.. code-block:: python + + class Circle: + ... + def get_area(self, diameter): + return math.pi * (diameter/2.0)**2 + + + class CircleR2(Circle): + ... + def get_area(self): + return Circle.get_area(self, self.radius*2) + + +Note that there is nothing special about ``__init__`` except that it gets called automatically when you instantiate an instance. Otherwise, it is the same as any other method -- it gets ``self`` as the first argument, it can or can not call the superclass' methods, etc. + + +"Favor Object Composition Over Class Inheritance" +------------------------------------------------- + +That is a quotation from the "Design Patterns" book -- one of the gospels of OO programming. + +But what does it mean? + +There are essentially two ways to add multiple functionalities to a class: + +Subclassing + +and + +Composition + +As we have just learned about subclassing, you might be tempted to do it a lot. But you need to be careful of over-using subclassing: + +https://en.wikipedia.org/wiki/Composition_over_inheritance + +Composition is when your classes have attributes of various types that they use to gain functionality -- "delegate" functionality to -- "Delegation" is a related concept in OO. + + +"Is a" vs "Has a" +................. + +Thinking about "is a" vs "has a" can help you sort this out. + +For example, you may have a class that needs to accumulate an arbitrary number of objects. + +A list can do that -- so maybe you should subclass list? + +To help decide -- Ask yourself: + +-- **Is** your class a list (with some extra functionality)? + +or + +-- Does you class **have** a list? + +You only want to subclass list if your class could be used anywhere a list can be used. In fact this is a really good way to think about subclassing in general -- subclasses should be specialized versions of the superclass. "Kind of" the same, but with a little different functionality. + + +Attribute Resolution Order +-------------------------- + +Once there is a potentially large hierarchy of subclasses, how do you know which one will be used? + +When you access an attribute: + +``an_instance.something`` + +Python looks for it in this order: + + * Is it an instance attribute ? + * Is it a class attribute ? + * Is it a superclass attribute ? + * Is it a super-superclass attribute ? + * ... + +It can get more complicated, particularly when there are multiple superclasses (multiple inheritance), but when there is a simple inheritance structure (the usual case) -- it's fairly straightforward. + +This is often referred to as "method resolution order" (MRO), because it's more complicated with methods, and in some languages, methods and attributes are more distinct than in Python. In Python, it can be thought of as "name resolution" -- everything in Python is about names and namespaces. + +If you want to know more of the gory details -- here's some reading: + +https://www.python.org/download/releases/2.3/mro/ + +http://python-history.blogspot.com/2010/06/method-resolution-order.html + + +What are Python classes, really? +-------------------------------- + +Putting aside the OO theory... + +Python classes feature: + + * Namespaces + + * One for the class object + * One for each instance + + * Attribute resolution order -- how do you find an attribute. + * Auto tacking-on of ``self`` when methods are called + * automatically calling ``__init__`` when the class object is called. + +That's about it -- really! + +(Well, not really, there is more fancy stuff going on under the hood -- but this basic structure will get you far). + +Type-Based Dispatch +------------------- + +Occasionally you'll see code that looks like this: + +.. code-block:: python + + if isinstance(other, A_Class): + Do_something_with_other + else: + Do_something_else + +When it's called for, Python provides these utilities: + + * ``isinstance()`` + * ``issubclass()`` + +But it is *very* rarely called for! Between Duck Typing, polymorphism, and EAFP, you rarely need to check for type directly. + +Wrap Up +------- + +Thinking OO in Python: + +Think about what makes sense for your code: + +* Code re-use +* Clean APIs +* Separation of Concerns +* ... + +OO can be a very powerful approach, but don't be a slave to what OO is *supposed* to look like. + +Let OO work for you, not *create* work for you. + +And the biggest way to do that is to support code re-use. + diff --git a/_sources/modules/SubmittingCode.rst.txt b/_sources/modules/SubmittingCode.rst.txt new file mode 100644 index 0000000..7afb9f4 --- /dev/null +++ b/_sources/modules/SubmittingCode.rst.txt @@ -0,0 +1,127 @@ +############################# +Working with GitHub Classroom +############################# + + +Why GitHub Classroom? +===================== + +A software development project is all about continuous improvement: + +1. An opportunity is identified. + +2. Some initial code is written to address that opportunity. + +3. Feedback is provided for that code. + +4. The code is modified to create that feedback. + +5. A final version of the code is released. + +Steps 3-4 will be repeated multiple times until the development team (which could even be a single developer) deems it is ready for release. + +In this course, you will not only learn about Python but also about the development process that most Python projects go through. GitHub Classroom allows for the steps indicated above to be completed in an academic environment. + +Initial setup +============= + +You will need an account on GitHub to participate in this course. +If you don't have already have a GitHub account or if you would prefer to use create a new one for this course, make sure you setup a new account on `GitHub `_. Always keep in mind that your account name will be part of the private repositories that will be created for each of your assignments and it will be visible to both your instructors and your classmates. + +You will need to have git setup on the computer you will use for developing your code for this course. +You can find instructions for setting up git (and the rest of your development environment) here: +:ref:`installing_python`_ + +Once you have all the tools set up, you will need to create a folder (directory) within your development system for keeping your work. +This is the folder where all your assignment repositories will reside. +Open Git Bash (if using Git for Windows) or a terminal (Linux and Mac OS), go to the folder (using the `cd` command) you have created for this class and run:: + + git init + +This will setup your folder to house git repositories. + +Accepting an assignment +======================= + +Each assignment page will contain a section named "Accepting your Assignment". Click on the corresponding link, which will take you to GitHub Classroom to accept the corresponding assignment. + +Some things to consider: +------------------------ + +* You will need to accept each assignment separately. + +* Accepting an assignment will trigger the creation of a private repository for you to work on your assignment. + +* This repository is only assigned to you. + +* Any work you do there will not affect the work of your classmates. + +* The name of the new repository will include your GitHub user name at the end. + +Once your repository has been created, go to its link and clone it on your development system, under the folder you selected for this purpose (here (Links to an external site.) is GitHub's official guide on how to clone a repository). + + +Before you start working on your assignment +=========================================== + +Once your repository is setup, it's good to get familiar with your repository view. +You should see a tab there called "Pull Requests": they indicate code changes that are desired to be pulled into a main repository; +by default you should see one already there called "Feedback". Go ahead and click on it, and take a look at Files Changed tab - as of now it should show "No changes to show". As you start committing your code you will see your changes there. + +Committing your code +==================== + +A "commit" is snapshot of your code (and any other files included in your project). +You are encouraged to make frequent commits, as this will make it easier for you to restore your code to an earlier state if things go wrong. + +To create a new commit: +----------------------- + +Type the following to add all files and subdirectories in the folder to your commit (note the command includes a dot, make sure you include it as well): +``git add``. + +Commit your code by typing the following: +``git commit -m "Commit message"`` + +Note that the commit message should be replaced with something descriptive of what that commit includes ("added new functionality", "fixed floating point error", "ready for review", etc.) that will later help you remember what that particular commit was about. + +Pushing your code +================= + +"Pushing" refers to the process of synchronizing the commits you have made on your development system with your GitHub repository. +This is an important process, since it is needed before you can submit your code for review. +Also, it makes a copy of your code in your GitHub account that you can later use to restore it if your local development system fails. + +You can push your code immediately after every commit or do it once a day (in which case, several commits will be included in a single push). To do it, simply type:: + + git push + +The first time you push your code to a repository, GitHub will ask you to select the remote repository (i.e., your GitHub repository). Just copy the suggested push command (you will only need to do this once per assignment). + +Asking Coding Questions +======================= + +While working on your code, you might run into a situation in which you would like one of the instructors to look at it and provide some feedback before actually reviewing and grading it. +In order to do that, go to "Feedback" pull request and write a comment about your question or issue. You should make sure to tag your instructor in your comment, to assure that they ar noticfied of your comment. This is done by writing `@the_instrudtors_github_handle`, e.g. `@natasha-aleksandrova`. + +For example:: + + @natasha-aleksandrova: I need some help on line 20 + +When you submit a comment with a tag, the instructor will be notified by GitHub and will be able to review your question. + +Submitting your assignment +========================== + +Once your assignment is ready for review, copy the link of your Feedback pull request and submit it in the submission form. Here is an example of a submission link (yours will look a little different but will end with `/pull/1`):: + + https://github.com/UWPCE-Py210-SelfPaced-2021/lesson-02-fizzbuzz-exercise-uw-test-student-natasha/pull/1 + +As per UW's requirements, you also need to submit a zip file with your code on EdX or Canvas. Note that only the code included in your pull request will be reviewed. + +9. Resubmitting your assignment +=============================== + +On occasion, your instructor will provide feedback on elements in your assignment that need to be modified in order to get the full grade for the assignment. In those cases, follow the process outlined in the Asking Coding Questions section above. Let us know that you would like another review for grade adjustment and make sure to tag your instructor. + +Happy coding! diff --git a/_sources/modules/SubmittingCodeGithubClassroom.rst.txt b/_sources/modules/SubmittingCodeGithubClassroom.rst.txt new file mode 100644 index 0000000..3c9d068 --- /dev/null +++ b/_sources/modules/SubmittingCodeGithubClassroom.rst.txt @@ -0,0 +1,128 @@ +############################# +Working with GitHub Classroom +############################# + +The Python Certificate program uses `GitHub Classroom `_ to manage the submission and review of your coding assignments. + +Why GitHub Classroom? +===================== + +A software development project is all about continuous improvement: + +1. An opportunity is identified. + +2. Some initial code is written to address that opportunity. + +3. Feedback is provided for that code. + +4. The code is modified to create that feedback. + +5. A final version of the code is released. + +Steps 3-4 will be repeated multiple times until the development team (which could even be a single developer) deems it is ready for release. + +In this course, you will not only learn about Python but also about the development process that most Python projects go through. GitHub Classroom allows for the steps indicated above to be completed in an academic environment. + +Initial setup +============= + +You will need an account on GitHub to participate in this course. +If you don't have already have a GitHub account or if you would prefer to use create a new one for this course, make sure you setup a new account on `GitHub `_. Always keep in mind that your account name will be part of the private repositories that will be created for each of your assignments and it will be visible to both your instructors and your classmates. + +You will need to have git setup on the computer you will use for developing your code for this course. +You can find instructions for setting up git (and the rest of your development environment) here: +:ref:`installing_python` + +Once you have all the tools set up, you will need to create a folder (directory) within your development system for keeping your work. +This is the folder where all your assignment repositories will reside. +Open Git Bash (if using Git for Windows) or a terminal (Linux and Mac OS), go to the folder (using the `cd` command) you have created for this class and run:: + + git init + +This will setup your folder to house git repositories. + +Accepting an assignment +======================= + +Each assignment page will contain a section named "Accepting your Assignment". Click on the corresponding link, which will take you to GitHub Classroom to accept the corresponding assignment. + +Some things to consider: +------------------------ + +* You will need to accept each assignment separately. + +* Accepting an assignment will trigger the creation of a private repository for you to work on your assignment. + +* This repository is only assigned to you. + +* Any work you do there will not affect the work of your classmates. + +* The name of the new repository will include your GitHub user name at the end. + +Once your repository has been created, go to its link and clone it on your development system, under the folder you selected for this purpose (here (Links to an external site.) is GitHub's official guide on how to clone a repository). + + +Before you start working on your assignment +=========================================== + +Once your repository is setup, it's good to get familiar with your repository view. +You should see a tab there called "Pull Requests": they indicate code changes that are desired to be pulled into a main repository; +by default you should see one already there called "Feedback". Go ahead and click on it, and take a look at Files Changed tab - as of now it should show "No changes to show". As you start committing your code you will see your changes there. + +Committing your code +==================== + +A "commit" is snapshot of your code (and any other files included in your project). +You are encouraged to make frequent commits, as this will make it easier for you to restore your code to an earlier state if things go wrong. + +To create a new commit: +----------------------- + +Type the following to add all files and subdirectories in the folder to your commit (note the command includes a dot, make sure you include it as well): +``git add``. + +Commit your code by typing the following: +``git commit -m "Commit message"`` + +Note that the commit message should be replaced with something descriptive of what that commit includes ("added new functionality", "fixed floating point error", "ready for review", etc.) that will later help you remember what that particular commit was about. + +Pushing your code +================= + +"Pushing" refers to the process of synchronizing the commits you have made on your development system with your GitHub repository. +This is an important process, since it is needed before you can submit your code for review. +Also, it makes a copy of your code in your GitHub account that you can later use to restore it if your local development system fails. + +You can push your code immediately after every commit or do it once a day (in which case, several commits will be included in a single push). To do it, simply type:: + + git push + +The first time you push your code to a repository, GitHub will ask you to select the remote repository (i.e., your GitHub repository). Just copy the suggested push command (you will only need to do this once per assignment). + +Asking Coding Questions +======================= + +While working on your code, you might run into a situation in which you would like one of the instructors to look at it and provide some feedback before actually reviewing and grading it. +In order to do that, go to "Feedback" pull request and write a comment about your question or issue. You should make sure to tag your instructor in your comment, to assure that they ar noticfied of your comment. This is done by writing `@the_instrudtors_github_handle`, e.g. `@natasha-aleksandrova`. + +For example:: + + @natasha-aleksandrova: I need some help on line 20 + +When you submit a comment with a tag, the instructor will be notified by GitHub and will be able to review your question. + +Submitting your assignment +========================== + +Once your assignment is ready for review, copy the link of your Feedback pull request and submit it in the submission form. Here is an example of a submission link (yours will look a little different but will end with `/pull/1`):: + + https://github.com/UWPCE-Py210-SelfPaced-2021/lesson-02-fizzbuzz-exercise-uw-test-student-natasha/pull/1 + +As per UW's requirements, you also need to submit a zip file with your code on EdX or Canvas. Note that only the code included in your pull request will be reviewed. + +9. Resubmitting your assignment +=============================== + +On occasion, your instructor will provide feedback on elements in your assignment that need to be modified in order to get the full grade for the assignment. In those cases, follow the process outlined in the Asking Coding Questions section above. Let us know that you would like another review for grade adjustment and make sure to tag your instructor. + +Happy coding! diff --git a/_sources/modules/Submitting_to_github.rst.txt b/_sources/modules/Submitting_to_github.rst.txt new file mode 100644 index 0000000..9ff8c06 --- /dev/null +++ b/_sources/modules/Submitting_to_github.rst.txt @@ -0,0 +1,95 @@ +:orphan: + +.. _submitting_to_gitHub: + + +**NOTE:** This is the "old" way -- it has been replaced with gitHUb classroom. But there may be some helpful hints here that should be included in the new docs. + +#################################### +Submitting your work to gitHub (old) +#################################### + +In this program, we are using the gitHub source code management system to manage each student's code, as well as examples and solutions. + +gitHub is designed as a system to develop collaborative software projects. As such, it is a great tool to learn for your future programming endeavors. + +It also has great interface for code review, so provides a system with which the instructors can review and provide feedback on your code. + +Starting a New Exercise +======================= + +Most Exercise will begin with creating a new python file. + +1) Start by creating a folder for the current lesson ("lesson02" or "lesson03", or....) + +2) Create your python file and save it into the lesson folder you just created. + +3) Once the file exists, it can be added to your local git "repo" to be managed:: + + git add the_file.py + +4) If the exercise requires more than one file, create them and add each to the manged by git in the same way. + +5) As you work, when you have something working that you might want to go back to, commit the changes to your repo. You have two options now -- you can commit everything that you have changed (-a means "all") :: + + git commit -a + + or you can commit only those files that you want to. To do that, you need to "stage" the files you want to commit:: + + git add one_file.py another_file.py + + (Yes, it is confusing -- "add" means: "add this file to the ones git is managing" if it's a new file, but "add this file to the ones I want to commit (staged)" if git is already managing the file. In fact, for a new file, you need to do "git add the_file.py" twice!) + + Now you can do:: + + git commit + + and the files that are "staged for commit" will be committed to your repo. + + **reminder** -- you can (and frequently should) run:: + + git status + + To see what is going on -- it will tell you which files are staged for commit, which files have been modified, and which are not being managed by git at all. + +6) Recall that all this git adding and committing is only effecting your local repo -- the one on your local machine. Once you have everything at a point where you want to share it with others (i.e. the instructors, classmates, or yourself on another machine!), you want to "push" it to your gitHub account:: + + git push + + Should do it. + + If you are working from multiple machines, and pushing to gitHub, you will need to do:: + + git pull + + To get everything that is up on gitHub down to your local machine. + +7) Once you have completed the assignment, and are ready to "turn it in" (that is, have the instructors review your work), you need to: + + a) make sure the latest version is in your gitHub account:: + + git push + + b) Go to *your* repo in gitHub in a browser. + + c) Submit a "Pull Request" to the class repo: + + (more detail here) + + Make sure to add a note to the PR letting the instructors know which code is ready for review, or of you have any specific questions. + +When you submit you PR on gitHub, the instructors will automatically get an email letting them know that you have submitted something. + + + + + + + + + + + + + + diff --git a/_sources/modules/TestDrivenDevelopment.rst.txt b/_sources/modules/TestDrivenDevelopment.rst.txt new file mode 100644 index 0000000..b8a76b4 --- /dev/null +++ b/_sources/modules/TestDrivenDevelopment.rst.txt @@ -0,0 +1,1841 @@ + +.. _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. + + +"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. + +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 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. `__ + + +(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). + + +The Rules for Roman Numerals +---------------------------- + +In Roman numerals, there are seven characters that are repeated and combined in various ways to represent numbers. + +| I = 1 +| V = 5 +| X = 10 +| L = 50 +| C = 100 +| D = 500 +| M = 1000 + +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 ... 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: + +#. 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. + +.. 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):: + + $ python -m pip install pytest + + 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. + +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.) + +A Single Question +----------------- + +.. 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... + +- 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: + +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>`. + +.. code-block:: python + :linenos: + + """ + roman.py + + A Roman numeral to Arabic numeral (and back!) converter + + complete with tests + + 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'), + ) + + + def test_to_roman_known_values(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + 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. + +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. + +.. 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. + +* 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. + +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. + +.. 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. + +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 + + assert numeral == result + +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. + + +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. + +You can actually try your tests out before even writing any code! + +To run tests with pytest, you pass in the test file on the command line: + +.. code-block:: + + $ pytest roman.py + =========================== test session starts =========================== + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.8.2, pluggy-0.13.1 + rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/examples/test_driven_development + collected 1 item + + roman.py F [100%] + + ================================ FAILURES ================================= + _______________________ test_to_roman_known_values ________________________ + + def test_to_roman_known_values(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + > result = to_roman(integer) + E NameError: name 'to_roman' is not defined + + roman.py:75: NameError + ========================= short test summary info ========================= + 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. + +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>`) + +.. code-block:: python + + # roman1.py + + def to_roman(n): + '''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. + +Now run pytest again, with the function defined: + +.. code-block:: + + $ pytest roman1.py + =========================== test session starts =========================== + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.8.2, pluggy-0.13.1 + rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/examples/test_driven_development + collected 1 item + + roman1.py F [100%] + + ================================ FAILURES ================================= + _______________________ test_to_roman_known_values ________________________ + + def test_to_roman_known_values(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + result = to_roman(integer) + > assert numeral == result + E AssertionError: assert 'I' == None + + roman1.py:84: AssertionError + ========================= short test summary info ========================= + 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. + +.. 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. + +*Now*, finally, you can write the ``to_roman()`` function. + +:download:`roman2.py <../examples/test_driven_development/roman2.py>` + +.. code-block:: python + :linenos: + + """ + roman.py + + A Roman numeral to arabic numeral (and back!) converter + + complete with tests + + tests are expected to be able to be run with the pytest system + """ + + roman_numeral_map = (('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1)) + + + def to_roman(n): + '''convert integer to Roman numeral''' + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + return result + + + ## 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(): + """ + to_roman should give known result with known input + """ + for integer, numeral in KNOWN_VALUES: + 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: + +.. code-block:: python + + while n >= integer: + result += numeral + n -= integer + print(f'subtracting {integer} from input, adding {numeral} to output') + +With the debug ``print()`` statements, the output looks like this: + +.. code-block:: ipython + + In [3]: run roman2.py + + In [4]: to_roman(1424) + subtracting 1000 from input, adding M to output + subtracting 400 from input, adding CD to output + subtracting 10 from input, adding X to output + subtracting 10 from input, adding X to output + subtracting 4 from input, adding IV to output + 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? + +.. code-block:: + + In [7]: ! pytest roman2.py + ========================= test session starts ========================= + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 + rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/examples/test_driven_development + collected 1 item + + roman2.py . [100%] + + ========================== 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. + +“Good” input? Hmm. What about bad input? + + +“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. + +.. code-block:: ipython + + In [10]: to_roman(3000) + Out[10]: 'MMM' + + In [11]: to_roman(4000) + Out[11]: 'MMMM' + + In [12]: to_roman(5000) + Out[12]: 'MMMMM' + + 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. + +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``. + +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. + +So how do we test for an exception? What would that test look like? + +:download:`roman.py <../examples/test_driven_development/roman3.py>`. + +.. code-block:: python + + import pytest + + def test_too_large(): + """ + to_roman should raise an ValueError when passed + values over 3999 + """ + 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. + +.. 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: + +.. code-block:: ipython + + In [18]: ! pytest roman3.py + ========================= test session starts ========================= + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 + rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/examples/test_driven_development + collected 2 items + + roman3.py .F [100%] + + ============================== FAILURES =============================== + ___________________________ test_too_large ____________________________ + + def test_too_large(): + """ + to_roman should raise an ValueError when passed + values over 3999 + """ + with pytest.raises(ValueError): + > to_roman(4000) + E Failed: DID NOT RAISE + + roman3.py:115: Failed + ======================= short test summary info ======================= + FAILED roman3.py::test_too_large - Failed: DID NOT RAISE `. + +.. code-block:: python + + def to_roman(n): + '''convert integer to Roman numeral''' + if n > 3999: + raise ValueError("number out of range (must be less than 4000)") + + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + return result + +This is straightforward: if the given input (``n``) is greater than +``3999``, raise a ``ValueError`` exception. +The unit test does not check the human-readable string that accompanies the exception, +although you could write another test that did check it if you wanted to be sure +(but watch out for internationalization issues for strings that vary by the user’s language or environment). + +Does this make the test pass? Let’s find out. + +.. code-block:: ipython + + In [19]: ! pytest roman4.py + ========================= test session starts ========================= + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 + rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/examples/test_driven_development + collected 2 items + + roman4.py .. [100%] + + ========================== 2 passed in 0.01s ========================== + +Hooray! Both tests pass. Because you worked iteratively, bouncing +back and forth between testing and coding, you can be sure that the +two lines of code you just wrote were the cause of that one test +going from “fail” to “pass.” That kind of confidence doesn’t come +cheap, but it will pay for itself over the lifetime of your code. + + +More Halting, More Fire +----------------------- + +Along with testing numbers that are too large, you need to test numbers +that are too small. +As we noted in our functional requirements, Roman numerals cannot express zero or negative numbers. + +.. code-block:: ipython + + In [20]: run roman4.py + + In [21]: to_roman(-1) + Out[21]: '' + + In [22]: to_roman(0) + Out[22]: '' + +Well *that’s* not good -- it happily accepted the input and returned an empty string. Now let’s add tests for each of these conditions, to make sure they raise an exception instead of silently giving an non-answer. + +:download:`roman5.py <../examples/test_driven_development/roman5.py>`. + +.. code-block:: python + + def test_zero(): + """to_roman should raise an ValueError with 0 input""" + 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 ``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: + +.. code-block:: ipython + + In [24]: ! pytest roman5.py + ========================= test session starts ========================= + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 + rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/examples/test_driven_development + collected 4 items + + roman5.py ..FF [100%] + + ============================== FAILURES =============================== + ______________________________ test_zero ______________________________ + + def test_zero(): + """to_roman should raise an ValueError with 0 input""" + with pytest.raises(ValueError): + > to_roman(0) + E Failed: DID NOT RAISE + + roman5.py:123: Failed + ____________________________ test_negative ____________________________ + + def test_negative(): + """to_roman should raise an ValueError with negative input""" + with pytest.raises(ValueError): + > to_roman(-1) + E Failed: DID NOT RAISE + + roman5.py:129: Failed + ======================= short test summary info ======================= + FAILED roman5.py::test_zero - Failed: DID NOT RAISE `. + +.. code-block:: + + def to_roman(n): + """convert integer to Roman numeral""" + if not (0 < n < 4000): + raise ValueError("number out of range (must be 1..3999)") + + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + 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. + +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. + +.. code-block:: ipython + + In [26]: ! pytest roman6.py + ========================= test session starts ========================= + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 + rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/examples/test_driven_development + collected 4 items + + roman6.py .... [100%] + + ========================== 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. + + +And One More Thing ... +---------------------- + +There was one more functional requirement for converting numbers to Roman numerals: dealing with non-integers. + +.. code-block:: ipython + + In [30]: run roman6.py + + In [31]: to_roman(0.5) + Out[31]: '' + +Oh, that’s bad. + +.. code-block:: ipython + + In [32]: to_roman(1.0) + Out[32]: 'I' + +What about that? technically, 1.0 is a float type, not an integer. But it does have an integer value, and Python considers them equal: + +.. code-block:: ipython + + In [35]: 1 == 1.0 + Out[35]: True + +So I'd say that we want 1.0 to be convertible, but not 0.5 (or 1.00000001 for that matter) + +Testing for non-integers is not difficult. Simply write a test case that checks that a ``ValueError`` is raised if you pass in a non-integer value. + +:download:`roman7.py <../examples/test_driven_development/roman7.py>`. + +.. code-block:: python + + def test_non_integer(): + """to_roman should raise an ValueError with non-integer input""" + with pytest.raises(ValueError): + to_roman(0.5) + +And while we are at it, test a float type that happens to be an integer. + +.. code-block:: python + + def test_float_with_integer_value(): + """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. + +Now check that the test fails properly. + +.. code-block:: ipython + + In [36]: ! pytest roman7.py + ========================= test session starts ========================= + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 + rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/examples/test_driven_development + collected 6 items + + roman7.py ....F. [100%] + + ============================== FAILURES =============================== + __________________________ test_non_integer ___________________________ + + def test_non_integer(): + """to_roman should raise an ValueError with non-integer input""" + with pytest.raises(ValueError): + > to_roman(0.5) + E Failed: DID NOT RAISE + + roman7.py:135: Failed + ======================= short test summary info ======================= + FAILED roman7.py::test_non_integer - Failed: DID NOT RAISE `. + +.. code-block:: + + def to_roman(n): + """convert integer to Roman numeral""" + if not (0 < n < 4000): + raise ValueError("number out of range (must be 1..3999)") + + if int(n) != n: + raise ValueError("Only integers can be converted to Roman numerals") + + result = '' + for numeral, integer in roman_numeral_map: + while n >= integer: + result += numeral + n -= integer + return result + +``int(n) != n`` is checking that when you convert the value to an integer, it doesn't change. We need to do that, because simply checking if you can convert to an integer isn't enough -- when a float is converted to an integer, the fractional part is truncated: + +.. code-block:: ipython + + In [37]: int(1.00001) + Out[37]: 1 + +If the result of converting to an integer is equal to the original, then it had an integral value. Note that this will work with all the built numerical types: + +.. code-block:: ipython + + In [42]: int(Decimal(3)) == 3 + Out[42]: True + + In [43]: int(Decimal(3.5)) == 3.5 + Out[43]: False + +Finally, check that the code does indeed make the test pass. + +.. code-block:: ipython + + In [44]: ! pytest roman8.py + ========================= test session starts ========================= + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 + rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/examples/test_driven_development + collected 6 items + + roman8.py ...... [100%] + + ========================== 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()``. + + +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. + +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. + +.. code-block:: python + + def test_from_roman_known_values(): + """from_roman should give known result with known input""" + for integer, numeral in KNOWN_VALUES: + 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. + +.. code-block:: python + + 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. + +.. code-block:: python + + + def test_roundtrip(): + '''from_roman(to_roman(n))==n for all n''' + for integer in range(1, 4000): + numeral = to_roman(integer) + 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. + +.. code-block:: ipython + + In [48]: ! pytest roman9.py + ========================= test session starts ========================= + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 + rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/examples/test_driven_development + collected 8 items + + roman9.py ......FF [100%] + + ============================== FAILURES =============================== + ____________________ test_from_roman_known_values _____________________ + + def test_from_roman_known_values(): + """from_roman should give known result with known input""" + for integer, numeral in KNOWN_VALUES: + > result = from_roman(numeral) + E NameError: name 'from_roman' is not defined + + roman9.py:152: NameError + ___________________________ test_roundtrip ____________________________ + + def test_roundtrip(): + '''from_roman(to_roman(n))==n for all n''' + for integer in range(1, 4000): + numeral = to_roman(integer) + > result = from_roman(numeral) + E NameError: name 'from_roman' is not defined + + roman9.py:160: NameError + ======================= short test summary info ======================= + FAILED roman9.py::test_from_roman_known_values - NameError: name 'fr... + FAILED roman9.py::test_roundtrip - NameError: name 'from_roman' is n... + ===================== 2 failed, 6 passed in 0.10s ===================== + +A quick stub function will solve that problem. + +.. code-block:: python + + # roman10.py + 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 properly fail. + +.. code-block:: ipython + + In [50]: ! pytest roman10.py + ========================= test session starts ========================= + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 + rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/examples/test_driven_development + collected 8 items + + roman10.py ......FF [100%] + + ============================== FAILURES =============================== + ____________________ test_from_roman_known_values _____________________ + + def test_from_roman_known_values(): + """from_roman should give known result with known input""" + for integer, numeral in KNOWN_VALUES: + result = from_roman(numeral) + > assert integer == result + E assert 1 == None + + roman10.py:157: AssertionError + ___________________________ test_roundtrip ____________________________ + + def test_roundtrip(): + """from_roman(to_roman(n))==n for all n""" + for integer in range(1, 4000): + numeral = to_roman(integer) + result = from_roman(numeral) + > assert integer == result + E assert 1 == None + + roman10.py:165: AssertionError + ======================= short test summary info ======================= + FAILED roman10.py::test_from_roman_known_values - assert 1 == None + 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. + +.. code-block:: + + def from_roman(s): + """convert Roman numeral to integer""" + result = 0 + index = 0 + for numeral, integer in roman_numeral_map: + while s[index:index + len(numeral)] == numeral: + result += integer + 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. + +If you're not clear how ``from_roman()`` works, add a ``print`` +call to the end of the ``while`` loop: + +.. code-block:: ipython + + def from_roman(s): + """convert Roman numeral to integer""" + result = 0 + index = 0 + for numeral, integer in roman_numeral_map: + while s[index:index + len(numeral)] == numeral: + result += integer + index += len(numeral) + print(f'found, {numeral} of length, {len(numeral)} adding {integer}') + return result + +.. code-block:: ipython + + In [52]: run roman10.py + + In [53]: from_roman('MCMLXXII') + found, M of length, 1 adding 1000 + found, CM of length, 2 adding 900 + found, L of length, 1 adding 50 + found, X of length, 1 adding 10 + found, X of length, 1 adding 10 + found, I of length, 1 adding 1 + found, I of length, 1 adding 1 + Out[53]: 1972 + +Time to re-run the tests. + +.. code-block:: ipython + + In [54]: ! pytest roman10.py + ========================= test session starts ========================= + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 + rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/examples/test_driven_development + collected 8 items + + roman10.py ........ [100%] + + ========================== 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.) + +.. 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. + + +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. + +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``\ ”). + +Roman numerals can only use certain characters, so we should test to make sure there aren't any other characters in the input: + +.. code-block:: python + + def test_invalid_character(): + """ + Roman numerals can only use these characters: + + M, D, C, L, X, V, I + + This tests that other characters will cause a failure + """ + for s in ['Z', 'XXIIIQ', 'QXXIII', 'XXYIII']: + with pytest.raises(ValueError): + 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. + +.. code-block:: python + + def test_too_many_repeated_numerals(): + '''from_roman should fail with too many repeated numerals''' + for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'): + with pytest.raises(ValueError): + 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. + +.. code-block:: python + + def test_repeated_pairs(): + '''from_roman should fail with repeated pairs of numerals''' + for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'): + with pytest.raises(ValueError): + 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. + +.. code-block:: python + + def test_malformed_antecedents(): + '''from_roman should fail with malformed antecedents''' + for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV', + 'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'): + 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?) + +.. code-block:: + + In [61]: ! pytest roman11.py + ============================ test session starts ============================ + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 + rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/examples/test_driven_development + collected 12 items + + roman11.py ........FFFF [100%] + + ================================= FAILURES ================================== + __________________________ test_invalid_character ___________________________ + + def test_invalid_character(): + """ + Roman numerals can only use these characters: + + M, D, C, L, X, V, I + + This tests that other characters will cause a failure + """ + for s in ['Z', 'XXIIIQ', 'QXXIII', 'XXYIII']: + with pytest.raises(ValueError): + > from_roman(s) + E Failed: DID NOT RAISE + + roman11.py:191: Failed + ______________________ test_too_many_repeated_numerals ______________________ + + def test_too_many_repeated_numerals(): + '''from_roman should fail with too many repeated numerals''' + for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'): + with pytest.raises(ValueError): + > from_roman(s) + E Failed: DID NOT RAISE + + roman11.py:198: Failed + ____________________________ test_repeated_pairs ____________________________ + + def test_repeated_pairs(): + '''from_roman should fail with repeated pairs of numerals''' + for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'): + with pytest.raises(ValueError): + > from_roman(s) + E Failed: DID NOT RAISE + + roman11.py:205: Failed + ________________________ test_malformed_antecedents _________________________ + + def test_malformed_antecedents(): + '''from_roman should fail with malformed antecedents''' + for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV', + 'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'): + with pytest.raises(ValueError): + > from_roman(s) + E Failed: DID NOT RAISE + + roman11.py:213: Failed + ========================== short test summary info ========================== + FAILED roman11.py::test_invalid_character - 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 + :lineno-start: 44 + + def is_valid_roman_numeral(s): + """ + parse a Roman numeral as a human would: left to right, + looking for valid characters and removing them to determine + if this is, indeed, a valid Roman numeral + """ + # first check if uses only valid characters + for c in s: + if c not in "MDCLXVI": + return False + + print("starting to parse") + print("the thousands") + print(f"{s = }") + # first look for the thousands -- up to three Ms + for _ in range(3): + if s[:1] == "M": + s = s[1:] + # then look for the hundreds: + print("the hundreds") + print(f"{s = }") + # there can be ony one of CM, CD, or D: + if s[:2] == "CM": # 900 + s = s[2:] + elif s[:2] == "CD": # 400 + s = s[2:] + elif s[:1] == "D": # 500 + s = s[1:] + # there can be from 1 to 3 Cs + for _ in range(3): + if s[:1] == "C": + s = s[1:] + # now the tens + print("the tens") + print(f"{s = }") + # There can be one of either XC, XL or L + if s[:2] == "XC": # 90 + s = s[2:] + elif s[:2] == "XL": # 40 + s = s[2:] + elif s[:1] == "L": # 50 + s = s[1:] + # there can be up to three Xs + for _ in range(3): + if s[:1] == "X": + s = s[1:] + # and the ones + print("the ones") + print(f"{s = }") + # There can be one of IX, IV or V + if s[:2] == "IX": # 9 + s = s[2:] + elif s[:2] == "IV": # 4 + s = s[2:] + elif s[:1] == "V": # 5 + s = s[1:] + print("looking for the Is") + print(f"{s = }") + # There can be up to three Is + for _ in range(3): + if s[:1] == "I": # 1 + s = s[1:] + # if there is anything left, it's not a valid Roman numeral + print("done") + print(f"{s = }") + if s: + return False + 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. + +So let's see how well that worked: + +.. code-block:: ipython + + In [8]: ! pytest roman13.py + ======================== test session starts ========================= + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.8.2, pluggy-0.13.1 + rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/examples/test_driven_development + collected 12 items + + roman13.py ...........F [100%] + + ============================== FAILURES ============================== + _____________________ test_malformed_antecedents _____________________ + + def test_malformed_antecedents(): + '''from_roman should fail with malformed antecedents''' + for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV', + 'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'): + with pytest.raises(ValueError): + print(f"trying: {s}") + > from_roman(s) + E Failed: DID NOT RAISE + + roman13.py:289: Failed + ------------------------ Captured stdout call ------------------------ + + ... + +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. + +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. + +In this case, we want to look at the output starting from the bottom. See the line at the top of the output:: + + trying: MCMC + +That's the result of the print call inside the test:: + + with pytest.raises(ValueError): + print(f"trying: {s}") + from_roman(s) + +That was the last one tried, so we know that the test failed when trying "MCMC", somewhere in the middle of all the tests. So what's wrong with the code? Well, it's heavily instrumented with print() calls, so we can look at the rest of the output from the failed test, and try to see what's going on. + +MCMC not a legal Roman numeral, because there is an C (100) after the first CM (900) you can't have both a 900 and a 100. + +So why didn't that get picked up? Looking at the output:: + + trying: MCMC + starting to parse + the thousands + s = 'MCMC' + the hundreds + s = 'CMC' + +After parsing the thousands, the first M has been removed -- all good. Now it's trying to parse the hundreds, starting with 'CMC'. But once it gets past the hundreds to the tens, there's nothing left -- the final C was removed:: + + the tens + s = '' + +Why was that? Time to look at the code. + +.. code-block:: python + :lineno-start: 63 + + print("the hundreds") + print(f"{s = }") + # there can be only one of CM, CD, or D: + if s[:2] == "CM": # 900 + s = s[2:] + elif s[:2] == "CD": # 400 + s = s[2:] + elif s[:1] == "D": # 500 + s = s[1:] + # there can be from 1 to 3 Cs + for _ in range(3): + if s[:1] == "C": + s = s[1:] + # now the tens + +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: + +.. code-block:: python + :lineno-start: 62 + + # then look for the hundreds: + print("the hundreds") + print(f"{s = }") + # there can be only one of CM, CD, or D: + if s[:2] == "CM": # 900 + s = s[2:] + elif s[:2] == "CD": # 400 + s = s[2:] + else: + if s[:1] == "D": # 500 + s = s[1:] + # there can be from 1 to 3 Cs + for _ in range(3): + if s[:1] == "C": + s = s[1:] + +We put the check for D inside the else as well, as the D is 500 and it can't be after the "CM" (900) or "CD" (400). After a D, you need up to three Cs to make 600, 700, 800. Now to run the tests and see how it works: + +.. code-block:: ipython + + In [12]: ! pytest roman14.py + ============================= test session starts ============================= + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.8.2, pluggy-0.13.1 + rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/examples/test_driven_development + collected 12 items + + roman14.py ...........F [100%] + + ================================== FAILURES =================================== + _________________________ test_malformed_antecedents __________________________ + + def test_malformed_antecedents(): + '''from_roman should fail with malformed antecedents''' + for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV', + 'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'): + with pytest.raises(ValueError): + print(f"trying: {s}") + > from_roman(s) + E Failed: DID NOT RAISE + + roman14.py:290: Failed + + +Still a failure in the same test. But let's look at the end of the output:: + + trying: XCX + starting to parse + the thousands + s = 'XCX' + the hundreds + s = 'XCX' + the tens + s = 'XCX' + the ones + s = '' + looking for the Is + s = '' + 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? + +.. code-block:: python + :lineno-start: 77 + + # now the tens + print("the tens") + print(f"{s = }") + # There can be one of either XC, XL or L + if s[:2] == "XC": # 90 + s = s[2:] + elif s[:2] == "XL": # 40 + s = s[2:] + elif s[:1] == "L": # 50 + s = s[1:] + # there can be up to three Xs + for _ in range(3): + if s[:1] == "X": + s = s[1:] + +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 + ============================= test session starts ============================= + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.8.2, pluggy-0.13.1 + rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/examples/test_driven_development + collected 12 items + + roman15.py ...........F [100%] + + ================================== FAILURES =================================== + _________________________ test_malformed_antecedents __________________________ + + def test_malformed_antecedents(): + '''from_roman should fail with malformed antecedents''' + for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV', + 'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'): + with pytest.raises(ValueError): + print(f"trying: {s}") + > from_roman(s) + E Failed: DID NOT RAISE + + roman15.py:291: Failed + ---------------------------- Captured stdout call ----------------------------- + + ... + + trying: IVI + starting to parse + the thousands + s = 'IVI' + the hundreds + s = 'IVI' + the tens + s = 'IVI' + the ones + s = 'IVI' + looking for the Is + s = 'I' + done + s = '' + =========================== short test summary info =========================== + FAILED roman15.py::test_malformed_antecedents - Failed: DID NOT RAISE `. + +.. code-block:: bash + + $ pytest roman17.py + ====================== test session starts ======================= + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.8.2, pluggy-0.13.1 + rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/examples/test_driven_development + collected 12 items + + roman17.py ............ [100%] + + ======================= 12 passed in 0.66s ======================= + +Nice! less code is better code, as long as it still works! + +Any other changes you can think of? Go ahead and try them, if the tests still pass, you are good to go! + + + +© 2001–11 `Mark Pilgrim`, 2020 `Christopher Barker` + + diff --git a/_sources/modules/Testing.rst.txt b/_sources/modules/Testing.rst.txt new file mode 100644 index 0000000..a9ea6d9 --- /dev/null +++ b/_sources/modules/Testing.rst.txt @@ -0,0 +1,520 @@ + +.. _unit_testing: + +####### +Testing +####### + +This page is a quick overview of testing in Python. It provides some background on testing, and the tools available. Later on, we'll get to the details of how to actually do it. + +Testing your code is an absolute necessity -- you need to have *some* way to know it's doing what it should. + +Having your testing done in an automated way is really a good idea. + +You've already seen a very basic testing strategy: putting some ``assert`` statements in the ``__name__ == "__main__"`` block. + +You've written some tests using that strategy. + +These tests were pretty basic, and a bit awkward in places (testing error +conditions in particular). + +.. centered:: **It gets better** + +Test Frameworks +--------------- + +So far our tests have been limited to code in an ``if __name__ == "__main__":`` +block. + + +* They are run *only* when the file is executed +* They are *always* run when the file is executed +* You can't do anything else when the file is executed without running tests. + + +This is not optimal. + +You really want ways to structure your tests, and run your tests, that can be controlled and provide nifty features. You do want tests to be run often while you are developing, but they should be a specific test that is done *during* development, not when the code is running operationally. + + +Standard Library: ``unittest`` +------------------------------ + +Python comes with the ``unittest`` package that provides a number of nifty features. It was introduced in version 2.1 -- so it's been around a long time. + +It is more or less a port of `JUnit `_ from Java, which shows. It has a style and structure that fits Java better than Python: + +It is a bit verbose: you have to write classes & methods (And we haven't covered that yet!) + +But you will see it used in others' code, so it's good to be familiar with it. +And seeing how verbose it can be will help you appreciate other options. + +So here's a bit of an introduction -- if the class stuff confuses you, don't worry about it -- you don't need to actually DO this yourself at this point. + + +Using ``unittest`` +------------------ + +To use ``unittest``, you need to write subclasses of the ``unittest.TestCase`` class (after importing the package, of course): + +.. code-block:: python + + # in test.py + import unittest + + class MyTests(unittest.TestCase): + + def test_tautology(self): + self.assertEqual(1, 1) + +Then you run the tests by using the ``main`` function from the ``unittest`` +module: + +.. code-block:: python + + # in test.py + if __name__ == '__main__': + unittest.main() + +``unittest.main()`` is called in the module where the tests are. Which means that they can be, but do not have to be, in the same file as your code. + +NOTE: tests can also be run by "test runners" for more features. + + +Testing Your Code +----------------- + +You can write your code in one file and test it from another -- and for all but the smallest projects, you want to do that. + +in ``my_mod.py``: + +.. code-block:: python + + def my_func(val1, val2): + return val1 * val2 + +in ``test_my_mod.py``: + +.. code-block:: python + + import unittest + from my_mod import my_func + + + class MyFuncTestCase(unittest.TestCase): + def test_my_func(self): + test_val1, test_val2 = 2, 3 + expected = 6 + actual = my_func(test_val1, test_val2) + self.assertEqual(expected, actual) + + if __name__ == '__main__': + unittest.main() + +So this is pretty straightforward, but it's kind of a lot of code for just one test, yes? + + +Advantages of ``unittest`` +-------------------------- + +The ``unittest`` module is pretty full featured + +It comes with the standard Python distribution, no installation required. + +It provides a wide variety of assertions for testing many types of results. + +It allows for a "set up" and "tear down" work flow both before and after all tests and before and after each test. + +It's well known and well understood. + + +Disadvantages of ``unittest`` +----------------------------- + +It's Object Oriented, and quite "heavyweight". + + - modeled after Java's ``JUnit``. + +It uses the Framework design pattern, so knowing how to use the features means learning what to override. + +Needing to override means you have to be cautious. + +Test discovery is both inflexible and brittle. + +It doesn't really take advantage of Python's introspection capabilities: + - There are explicit "assert" methods for each type of test + - The available assertions are not the least bit complete + - All the assertions really do is provide pretty printing of errors + +Testing for Exceptions is awkward + +Test discovery is limited + +And there is no built-in parameterized testing. + + +Other Options +------------- + +Due to these limitations, folks in the Python community have developed other options for testing in Python: + +* **Nose2**: https://github.com/nose-devs/nose2 + +* **pytest**: http://pytest.org/latest/ + +* ... (many frameworks supply their own test runners: e.g. Django) + +Nose was the most common test runner when I first started learning testing, but it has been in maintenance mode for a while. Even the nose2 site recommends that you consider pytest. + +pytest has become the defacto standard testing system for those that want a more "pythonic" and robust test framework. + +pytest is very capable and widely used. + +For a great description of the strengths of pytest, see: + +`The Cleaning Hand of Pytest `_ + +If you look above, pytest provided every feature of ``unittest`` except being in the standard library. And none of the disadvantages. It also can run ``unittest`` tests, so if you already have ``unittest`` tests, or like some of its features, you can still use pytest. + +So we will use pytest for the rest of this class. + + +Installing ``pytest`` +--------------------- + +pytest is very easy to install these day: + +.. code-block:: bash + + $ python -m pip install pytest + +Once this is complete, you should have a ``pytest`` command you can run +at the command line: + +.. code-block:: bash + + $ pytest + +If you have any tests in your repository, that command will find and run them (If you have followed the proper naming conventions). + + **Do you have any tests?** + + +Pre-existing Tests +------------------ + +Let's take a look at some examples. + +Create a directory to try this out, and download: + +:download:`test_random_unitest.py <../examples/testing/test_random_unitest.py>` + +In the directory you created for that file, run: + +.. code-block:: bash + + $ pytest + +It should find that test file and run it. + +You can also run pytest on a particular test file: + +.. code-block:: bash + + $ pytest test_random_unitest.py + ====================== test session starts ======================= + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.8.2, pluggy-0.13.1 + rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/examples/testing + collected 3 items + + test_random_unitest.py ... [100%] + + ======================= 3 passed in 0.03s ======================== + + +You should have gotten similar results when you ran ``pytest`` yourself. + +Take a few minutes to look this file over. + +``test_random_unitest.py`` contains the tests for some of the functions in the built in``random`` module. You really don't need to test Python's built in modules -- they are already tested! This is just to demonstrate the process. + + +What is Happening Here? +----------------------- + +Let's look again at the results of running pytest on this file: + +.. code-block:: bash + + $ pytest + ====================== test session starts ======================= + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.8.2, pluggy-0.13.1 + rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/examples/testing + collected 3 items + + test_random_unitest.py ... [100%] + + ======================= 3 passed in 0.03s ======================== + + +When you run the ``pytest`` command, ``pytest`` starts in your current +working directory and searches the file system for things that might be tests. + +It follows some simple rules: + +* Any python file that starts with ``test_`` or ``_test`` is imported. + +* Any functions in them that start with ``test_`` are run as tests. + +* Any classes that start with ``Test`` are treated similarly, with methods that begin with ``test_`` treated as tests. + +( don't worry about "classes" part just yet ;-) ) + +* Any ``unittest`` test cases are run. + +So in this case, pytest found the ``test_random_unitest.py`` file and in that file, found the ``TestSequenceFunctions`` TestCase class, and ran the tests defined in that class. In this case, there were three of them, and they all passed. + +pytest +------ + +The pytest test framework is simple, flexible and configurable. + +Read the documentation for more information: + +https://docs.pytest.org + +Those docs are a bit intimidating, but with pytest, as they say: + +.. centered:: "The easy stuff is easy, and the hard stuff is possible" + +-- and you can get very far with the easy stuff. + +In addition to finding and running tests, it makes writing tests simple, and provides a bunch of nifty utilities to support more complex testing. + +To give this a try, download this file: + +:download:`test_random_pytest.py <../examples/testing/test_random_pytest.py>` + +And run pytest again on this file: + +.. code-block:: bash + + $ pytest test_random_pytest.py + ====================== test session starts ======================= + platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.8.2, pluggy-0.13.1 + rootdir: /Users/chris.barker/Personal/UWPCE/Python210CourseMaterials/source/examples/testing + collected 5 items + + test_random_pytest.py ..... [100%] + + ======================= 5 passed in 0.02s ======================== + +Note that if you had not passed in the filename, it would have run the tests in both the test files. + + +pytest tests +------------ + +Now take a look at ``test_random_pytest.py`` -- It is essentially the same tests -- but written in native pytest style -- simple test functions, rather than classes and special assertions. + +The beauty of pytest is that it takes advantage of Python's dynamic nature -- you don't need to use any particular structure to write tests, and you don't need to use special assertions to get good reporting. + +* Any function named appropriately is a test. + +* If the function doesn't raise an Exception or fail an assertion, the test passes. + +It's that simple. + +Look at ``test_random_pytest.py`` to see how this works. + +.. code-block:: python + + import random + import pytest + +The ``random`` module is imported because that's what we are testing. +``pytest`` only needs to be imported if you are using its utilities -- more on this in a moment. + +.. code-block:: python + + example_seq = list(range(10)) + +Here we create a simple little sequence to use for testing. We put it in the global namespace so other functions can access it. + +Now the first tests -- simply by naming it ``test_something``, pytest will run it as a test: + +.. code-block:: python + + def test_choice(): + """ + A choice selected should be in the sequence + """ + element = random.choice(example_seq) + assert (element in example_seq) + +This is pretty straightforward. We make a random choice from the sequence, +and then assert that the selected element is, indeed, in the original sequence. + +.. code-block:: python + + def test_sample(): + """ + All the items in a sample should be in the sequence + """ + for element in random.sample(example_seq, 5): + assert element in example_seq + +And this is pretty much the same thing, except that it loops to make sure that every item returned by ``.sample()`` is in the original sequence. + +Note that this will result in 5 separate assertions -- that is fine, you can have as many assertions as you like in one test function. But the test will fail on the first failed assertion -- so you only want to have closely related assertions in each test function. + +.. code-block:: python + + def test_shuffle(): + """ + Make sure a shuffled sequence does not lose any elements + """ + seq = list(range(10)) + random.shuffle(seq) + seq.sort() # If you comment this out, it will fail, so you can see output + print("seq:", seq) # only see output if it fails + assert seq == list(range(10)) + +This test is designed to make sure that ``random.shuffle`` only re-arranges the items, but doesn't add or lose any. + +In this case, the global ``example_seq`` isn't used, because ``shuffle()`` will change the sequence -- tests should never rely on or alter global state. So a new sequence is created for the test. This also allows the test to know exactly what the results should be at the end. + +Then the "real work": calling ``random.shuffle`` on the sequence. This should re-arrange the elements without adding or losing any. + +Calling ``.sort()`` again should put the elements back in the order they started + +So we can then test that after shuffling and re-sorting, we have the same sequence back: + +.. code-block:: python + + assert seq == list(range(10)) + +If that assertion passes, the test will pass. + +``print()`` and test failures +............................. + +Try commenting out the sort line: + +.. code-block:: python + + # seq.sort() # If you comment this out, it will fail, so you can see output + +And run again to see what happens. This is what I got: + +.. code-block:: bash + + $ pytest test_random_pytest.py + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.10.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/PythonStuff/UWPCE/PythonCertDevel/source/examples/testing, inifile: + plugins: cov-2.6.0 + collected 5 items + + test_random_pytest.py F.... [100%] + + =================================== FAILURES =================================== + _________________________________ test_shuffle _________________________________ + + def test_shuffle(): + """ + Make sure a shuffled sequence does not lose any elements + """ + seq = list(range(10)) + random.shuffle(seq) + # seq.sort() # If you comment this out, it will fail, so you can see output + print("seq:", seq) # only see output if it fails + > assert seq == list(range(10)) + E assert [4, 8, 9, 3, 2, 0, ...] == [0, 1, 2, 3, 4, 5, ...] + E At index 0 diff: 4 != 0 + E Use -v to get the full diff + + test_random_pytest.py:22: AssertionError + ----------------------------- Captured stdout call ----------------------------- + seq: [4, 8, 9, 3, 2, 0, 7, 5, 6, 1] + ====================== 1 failed, 4 passed in 0.40 seconds ====================== + +You get a lot of information when test fails. It's usually enough to tell you what went wrong. + +Note that pytest didn't print out the results of the print statement when the test passed, but when it failed, it printed it (under "Captured stdout call"). This means you can put diagnostic print calls in your tests, and they will not clutter up the output when they are not needed. This is *very* helpful! + + +Testing for Exceptions +...................... + +One of the things you might want to test about your code is that it raises an Exception when it should -- and that the Exception it raises is the correct one. + +In this example, if you try to call ``random.shuffle`` with an immutable sequence, such as a tuple, it should raise a ``TypeError``. Since raising an Exception will generally stop the code (and cause a test to fail), we can't use an assertion to test for this. + +pytest provides a "context manager", ``pytest.raises()``, that can be used to test for Exceptions. The test will pass if and only if the specified Exception is raised by the enclosed code. You use it like so: + +.. code-block:: python + + def test_shuffle_immutable(): + """ + Trying to shuffle an immutable sequence raises an Exception + """ + with pytest.raises(TypeError): + random.shuffle((1, 2, 3)) + +The ``with`` block is how you use a context manager -- it will run the code in the following block, +and perform various actions at the end of the code, or when an Exception is raised. +This is the same ``with`` as used to open files. In that case, it is used to assure that the file is properly closed when you are done with it. +In this case, the ``pytest.raises()`` context manager captures any Exceptions, and raises an ``AssertionError`` if no Exception is raised, or if the wrong Exception is raised. + +In this case, the test will only pass if a ``TypeError`` is raised by the call to ``random.shuffle()`` with a tuple as an argument. + +Try changing that to a different Exception and see what happens: + +.. code-block:: python + + def test_shuffle_immutable(): + """ + Trying to shuffle an immutable sequence raises an Exception + """ + with pytest.raises(ValueError): + random.shuffle((1, 2, 3)) + +I get a lot of context information, concluding with:: + + ==================== short test summary info ===================== + FAILED test_random_pytest.py::test_shuffle_immutable - TypeErro... + ================== 1 failed, 4 passed in 0.18s =================== + +So you got an Exception -- but not the one expected -- so the test failed. + + +The next test: + +.. code-block:: python + + def test_sample_too_large(): + """ + Trying to sample more than exist should raise an error + """ + with pytest.raises(ValueError): + random.sample(example_seq, 20) + +is very similar, except that this time, a ``ValueError`` has to be raised for the test to pass. + +pytest provides a number of other features for fixtures, parameterized tests, test classes, configuration, shared resources, etc. If you want to learn more about that, read the pytest documentation, and this introduction to more advanced concepts: :ref:`advanced_testing` + + +But simple test functions like this will get you very far. + + +Test Driven Development +----------------------- + +Test Driven Development or "TDD", is a development process where you write tests to assure that your code works, *before* you write the actual code. + +This is a very powerful approach, as it forces you to think carefully about exactly what your code should do before you start to write it. It also means that you know when you code is working, and you can refactor it in the future with assurance that you haven't broken it. + +Give this exercise a try to get the idea: + +:ref:`exercise_unit_testing` diff --git a/_sources/modules/Testing_advanced.rst.txt b/_sources/modules/Testing_advanced.rst.txt new file mode 100644 index 0000000..8df9285 --- /dev/null +++ b/_sources/modules/Testing_advanced.rst.txt @@ -0,0 +1,975 @@ +:orphan: + +.. _advanced_testing: + +################ +Advanced Testing +################ + +Testing in Python + + +What is testing? +================ + + +Code which runs your application in as close to a real environment as +feasible and validates its behavior + + +Terminology of testing +---------------------- + +- Unit tests +- Integration tests +- High level system tests +- Acceptance tests +- Black box / White box testing + + +"V" model and tests levels +-------------------------- +.. image:: /_static/test_v_model.png + + +Unit testing +------------ + +- Test smallest discrete units of source code +- Tests should be independent of each other +- Can separate tests from required resources through fixtures and + mocking +- Automatable +- Integrates with the development process + + +What should be tested? +---------------------- + +The percentage of code which gets run in a test is known as the +"coverage". + +100% coverage is an ideal to strive for. But the decision on when and +what to test should take into account the volatility of the project. + +**NOTE** Even if every line of code is run during tests (100% coverage), +they may not be comprehensive! It is very hard to anticipate every weird +input some code may get. + + +Unit Testing tools +------------------ + +- unittest, the test framework that ships with Python. Port of Java jUnit + + http://docs.python.org/3/library/unittest.html + +- pytest, a test runner, and also an alternative to unittest, which you should be pretty familiar with now + + http://pytest.org/latest/ + +- mock, an object mocking library. Ships with Python 3.3+ + + https://docs.python.org/dev/library/unittest.mock.html + +Note that while mock is in the ``unittest`` package, you do not need to be using ``unittest`` tests to use it. + + +About Unit Testing +------------------ + +1. Tests should be independent. +2. Tests do not run in order, which shouldn't matter, see point 1. +3. Test fixtures are available to do any setup / teardown needed for tests. +4. Test behavior not implementation dependent. +5. Mocking is available to fake stuff you may not want to run in your tests. + +This all applies regardless of your test framework + +unittest +-------- + +The unittest framework comes with the standard library + +Unittest is ported from Java's jUnit -- it is therefore OO-heavy, and +requires a lot of boilerplate code. + +Many projects built custom testing Frameworks on top of it -- e.g. Django + +Therefore you will encounter it + +So it's good to be familiar with it. + +Key missing features: + + * A test runner + + - many people use nose or pytest to run unittest tests. + + * Parameterized tests + + - there are kludges and some third-party tools for this. + + +unittest.TestCase anatomy +------------------------- + +* create a new subclass of ``unittest.TestCase`` +* name test methods ``test_foo`` so the test runner finds them +* make calls to the ``self.assert*`` family of methods to validate results + +.. code-block:: python + + import unittest + class TestMyStuff(unittest.TestCase): + + def setUp(self): + self.x = 2 + + def test_add(self): + self.assertEqual(self.x+2, 4) + + def test_len(self): + self.assertEqual(len('foo'), 3) + + if __name__ == '__main__': + unittest.main() + + +Assert Methods +--------------- + +TestCase contains a number of methods named ``assert*`` which can be used +for validation, here are a few common ones: + +.. code-block:: python + + assertEqual(first, second, msg=None) + assertNotEqual(first, second, msg=None) + assertTrue(expr, msg=None) + assertFalse(expr, msg=None) + assertIn(first, second) + assertRaises(exc, fun, msg=None, *args, **kwargs) + +See a full list at: + +http://docs.python.org/3/library/unittest.html#assert-methods or + +``dir(unittest.TestCase)`` or to get really fancy + +.. code-block:: python + + [print(i) for i in dir(unittest.TestCase) if i.startswith('assert')] + + +Running your tests +================== + +How do you actually run your tests? + + +running tests in a single module +-------------------------------- + +Call unittest.main() right in your module + +.. code-block:: python + + if __name__ == "__main__": + unittest.main() + +or from the command line: + +.. code-block:: bash + + python -m unittest test_my_module # with or without .py on end + + python -m unittest test_my_module.TestClass # particular class in a module + + python -m unittest test_my_module.TestClass.test_method # particular test + +If it gets cumbersome with many TestCases, organize the tests into a +test suite (or use a test runner, which we get to soon). + +Test Suites +----------- + +Test suites group test cases into a single testable unit + +.. code-block:: python + + import unittest + + from calculator_test import TestCalculatorFunctions + + suite = unittest.TestLoader().loadTestsFromTestCase(TestCalculatorFunctions) + + unittest.TextTestRunner(verbosity=2).run(suite) + +Tests can also be organized into suites in the + +``if __name__ == "__main__":`` + +block + + +TestRunners: pytest and Nose2 +----------------------------- + +Nose2 is the new nose. Nose is no longer maintained, and directs users to nose2. + +Both pytest and Nose2 are test runners: they auto-discover test cases. + +They will find tests for you so you can focus on writing tests, not +maintaining test suites. + +To find tests, pytest and nose look for modules (such as python files) +whose names start with ‘test’. In those modules, they will load tests +from all unittest.TestCase subclasses, as well as functions whose names +start with ‘test’. + +So running your tests is as easy as + +.. code-block:: bash + + $ pytest + +or + +.. code-block:: bash + + $ nose2 + +http://nose2.readthedocs.org/en/latest/getting_started.html#running-tests + +https://docs.pytest.org/en/latest/index.html + +A number of projects use nose -- so you may encounter it, but we'll focus +on pytest for now. + + +Fixtures: Setting up your tests for success +------------------------------------------- + +(or failure!) + +Test fixtures are a fixed baseline for tests to run from consistently, +also known as test context. + +Fixtures can (and should) be set up fresh before each test, once before each test +case, or before an entire test suite. + + +Fixtures in unittest +-------------------- + +unittest provides fixture support via these methods: + +- setUp / tearDown - these are run before and after each test method +- setUpClass / tearDownClass - these are run before/after each TestCase +- setUpModule / tearDownModule - run before/after each TestSuite +- addCleanup / doCleanups - called after tearDown, + in case a test throws an exception + +Fixtures in pytest +------------------ + +pytest provides a fixture system that is powerful and flexible: + +https://docs.pytest.org/en/latest/fixture.html#fixture + +You use a decorator to create a fixture: + +.. code-block:: python + + import pytest + + @pytest.fixture + def smtp(): + import smtplib + return smtplib.SMTP("smtp.gmail.com") + +A fixture is simply a function that will get run when it is used, and +returns *something* that your tests need: + +To use a fixture, you add it as a parameter to your test function: + +.. code-block:: python + + def test_ehlo(smtp): + response, msg = smtp.ehlo() + assert response == 250 + assert 0 # for demo purposes + +The parameter gets set to the value returned by the fixture function. +The fixture function is automatically run before each test. + +Let's see this in action: + +:download:`pytest_fixtures.py <../examples/testing/pytest_fixtures.py>` + +.. code-block:: bash + + $ pytest -s -v pytest_fixtures.py + +The ``-s`` tells pytest not to capture stdout -- so we can see print statements. + +The ``-v`` is verbose mode -- so we can see a bit more what is going on. + +"Teardown" +----------- + +If your fixture needs to clean itself up after its done, this is known as +"teardown" + +To accomplish this in pytest, you use "yield", rather than "return". + +The teardown code will run after the yield + +.. code-block:: python + + @pytest.fixture + def smtp(request): + smtp = smtplib.SMTP("smtp.gmail.com") + yield smtp # provide the fixture value + print("teardown smtp") + smtp.close() + +Remember that putting a yield in a function makes it a generator function -- which provides a way to halt execution of the function, return a value, and then pick up where it left off. So in this case, you use whatever code you want to generate your object -- then after the yield, all those variables will be there, so you can do whatever clean up you need to do. + +See the example again for this... + +Testing floating point values +============================= + +Why can't we just test if .5 == 1/2 ? + +.. code-block:: ipython + + In [1]: 3 * .15 == .45 + Out[1]: False + + In [2]: 3 * .15 + Out[2]: 0.44999999999999996 + + In [3]: 3 * .15 * 10 / 10 == .45 + Out[3]: True + +There are an infinite number of real numbers, so they are +stored as an approximation in computing hardware. + +https://docs.python.org/3/tutorial/floatingpoint.html + + +Levels of precision of floating point +------------------------------------- + +Python floating point numbers are stored in `IEEE 754 `_ 64-bit double precision format, so 1 bit for the sign, 11 bits for the exponent, and the remaining 52 for the fraction. + +So we can count on up to about 16 digits of precision in decimal: + +.. code-block:: ipython + + In [39]: len(str(2**52)) + Out[39]: 16 + + In [40]: .1+.2 + Out[40]: 0.30000000000000004 + + In [41]: len('3000000000000000') + Out[41]: 16 + + # with repeated operations, the errors eventually build up: + # here's multiplying by "1" 10 million times: + In [64]: x=1 + In [69]: for i in range(10000000): x *= (.1 + .2)/.3 + Out [69]: 1.000000002220446 + + +assertAlmostEqual +----------------- + +assertAlmostEqual is a custom assert in ``unittest`` that verifies that two floating point values are close enough to each other. + +Add a places keyword argument to specify the number of decimal places. + +.. code-block:: python + + import unittest + + class TestAlmostEqual(unittest.TestCase): + + def setUp(self): + pass + + def test_floating_point(self): + self.assertEqual(3*.15, .45) + + def test_almost_equal(self): + self.assertAlmostEqual(3*.15, .45, places=7) + + +What is close enough? +--------------------- + +**Warning** + +``assertAlmostEqual`` lets you specify *decimal places*, i.e. the number of digits after the decimal point. + +This works great for numbers that are about magnitude 1.0 (as above) + +But what if you have numbers that are very large? (or small): + + - ``1.0e22`` + - ``1.0000000000001e22`` + +are they almost equal? + +Remember that python floating point numbers store the exponent and up +to 16 decimal digits. + +So those two are almost as close as you can get. But: + +.. code-block:: ipython + + In [30]: x = 1e22 + + In [31]: y = 1.0000000000001e22 + + In [32]: '%g'%(y - x) + Out[32]: '1.00034e+09' + +They are different by about a billion! + +In general, we don't want to compare floating point numbers to within a +certain number of decimal places. + +Anyone remember "significant figures" from science classes? + +``isclose()`` +------------- + +Python 3.5 introduced the ``isclose()`` function in the ``math`` module: + +https://www.python.org/dev/peps/pep-0485/ + +.. code-block:: ipython + + In [39]: import math + + In [40]: x + Out[40]: 1e+22 + + In [41]: y + Out[41]: 1.0000000000001e+22 + + In [42]: math.isclose(x, y) + Out[42]: True + +So this works for any magnitude number. + +.. code-block:: python + + is_close(a, b, *, rel_tol=1e-09, abs_tol=0.0) -> bool + + Determine whether two floating point numbers are close in value. + + rel_tol + maximum difference for being considered "close", relative to the + magnitude of the input values + abs_tol + maximum difference for being considered "close", regardless of the + magnitude of the input values + + Return True if a is close in value to b, and False otherwise. + +``rel_tol`` essentially specifies how many significant figures you want: +``1e-09`` is 9 significant figures: about half of what floats can store. + +``abs_tol`` is required for comparisons to zero -- nothing is +"relatively close" to zero + + +Using ``isclose()`` with ``unittest`` +------------------------------------- + +Ideally, ``TestCase`` would have an ``assertIsClose`` method. +But you can use: + +.. code-block:: python + + import unittest + from math import isclose + + class TestAlmostEqual(unittest.TestCase): + + def test_floating_point(self): + self.assertEqual(3*.15, .45) + + def test_almost_equal(self): + self.assertTrue( isclose( 3*.15, .45, rel_tol=7) ) + +**NOTE** This is one of the key flaws with the unittest module: while +it can test anything with ``assertTrue`` and the like -- if there is no +nifty ``assert*`` method for your use-case, you lose the advantages of +the ``assert*`` methods. + +What are those advantages? -- mostly a prettier printing of information +in the error:: + + FAIL: test_floating_point (__main__.TestAlmostEqual) + ---------------------------------------------------------------------- + Traceback (most recent call last): + File "/Users/Chris/PythonStuff/UWPCE/Py300-Spring2017/Examples/testing/test_floats.py", line 17, in test_floating_point + self.assertEqual(3 * .15, .45) + AssertionError: 0.44999999999999996 != 0.45 + +But when you use assertTrue:: + + FAIL: test_isclose_tiny (__main__.TestAlmostEqual) + ---------------------------------------------------------------------- + Traceback (most recent call last): + File "/Users/Chris/PythonStuff/UWPCE/Py300-Spring2017/Examples/testing/test_floats.py", line 32, in test_isclose_tiny + self.assertTrue(math.isclose(4 * .15e-30, .45e-30)) + AssertionError: False is not true + +Not that helpful -- is it? I thikn we all already know that False is not true ;-) + +``pytest`` give you nice informative messages when tests fail -- without special asserts. + + +Parameterized Tests +=================== + +Often you want to run exactly the same tests, but with different outputs and inputs. + +You can do this a really naive way, by putting multiple asserts into one test: + +.. code-block:: python + + def test_multiply(): + assert multiply(2, 2) == 4 + assert multiply(2, -1) == -4 + assert multiply(-2, -3) == 6 + assert multiply(3, 0) == 0 + assert multiply(0, 3) == 0 + +If they all pass, fine, but if not, it will fail on the first one, +and you'll have no idea if the others pass. + +Plus, it gets a bit tedious to write, particularly if the code is more +complex than a single function call. + +You can write a separate test for each case: + +.. code-block:: python + + def test_multiply_both_positive(): + assert multiply(2, 2) == 4 + + def test_multiply_one_negative): + assert multiply(2, -1) == -4 + + def test_multiply_both_negative(): + assert multiply(-2, -3) == 6 + + def test_multiply_second_zero(): + assert multiply(3, 0) == 0 + + def test_multiply_first_zero(): + assert multiply(0, 3) == 0 + +But talk about tedious!!! + +Unfortunately, ``unittest`` does not have a built-in way to solve this problem. +There is a nifty library called parameterized, which does solve it (and they spell parameterize correctly). +It works with nose, unittest, and pytest. + +https://pypi.python.org/pypi/parameterized + +.. code-block:: python + + @parameterized([ + (2, 2, 4), + (2, 3, 8), + (1, 9, 1), + (0, 9, 0),]) + def test_pow(base, exponent, expected): + assert_equal(math.pow(base, exponent), expected) + + +You will find many more examples on their website. + + +``pytest.mark.parametrize`` +--------------------------- + +With pytest, you don't need a third party library: as it provides a nifty built-in way to do it: + +https://docs.pytest.org/en/latest/parametrize.html#parametrize-basics + +.. code-block:: python + + param_names = "arg1, arg2, result" + params = [(2, 2, 4), + (2, -1, -2), + (-2, -2, 4), + ] + @pytest.mark.parametrize(param_names, params) + def test_multiply(arg1, arg2, result): + assert multiply(arg1, arg2) == result + +I find this very, very, useful. + +See :download:`test_calculator_pytest.py ` + + +Code Coverage +------------- + +"Coverage" is the fraction of your code that is run by your tests. +That is, how much code is "covered" by the tests. + +It's usually reported as a percentage of lines of code that were run. + +If a line of code is *not* run in your tests -- you can be pretty +sure it hasn't been tested -- so how do you know it works? + +So 100% coverage is a good goal (though harder to achieve than you might think!) + +Keep in mind that 100% coverage does **NOT** mean that your code is *fully* tested -- you have no idea how many corner cases may not have been checked. + +But it's a good start. + + +The coverage tool +----------------- + +``coverage.py`` is a tool (written by Ned Batchelder) for checking code testing +coverage in python: + +https://coverage.readthedocs.io + +It can be installed with ``pip``: + +.. code-block:: bash + + $ python -m pip install coverage + +To run coverage on your test suite: + +.. code-block:: bash + + $ coverage run my_program.py arg1 arg2 + +This generates a .coverage file. To analyze it on the console: + +.. code-block:: bash + + $ coverage report + +Or you can generate an HTML report in the current directory: + +.. code-block:: bash + + $ coverage html + +To find out coverage across the standard library, add -L: + +:: + + -L, --pylib Measure coverage even inside the Python installed + library, which isn't done by default. + + +Branch Coverage +--------------- + +consider the following code: + +.. code-block:: python + + x = False # 1 + if x: # 2 + print("in branch") # 3 + print("out of branch") # 4 + +We want to make sure the branch is being bypassed correctly in the False +case + +Track which branch destinations were not visited with the --branch +option to run: + +.. code-block:: bash + + coverage run --branch myprog.py + +http://nedbatchelder.com/code/coverage/branch.html + + +Using coverage with pytest +-------------------------- + +There is a plug-in for pytest that will run coverage for you when you run your tests: + +.. code-block:: bash + + $ pip install pytest-cov + + # now it can be used + $ pytest --cov code_module test_module.py + +https://pypi.python.org/pypi/pytest-cov + +There are a number of ways to invoke it and get different reports: + +To get a nifty html report: + +.. code-block:: bash + + $ pytest --cov code_module --cov-report html test_module.py + + +Doctests +======== + +Tests placed in docstrings to demonstrate usage of a component to a +human in a machine testable way + +.. code-block:: python + + def square(x): + """ + Squares x. + + >>> square(2) + 4 + >>> square(-2) + 4 + """ + return x * x + +.. code-block:: bash + + python -m doctest -v example.py + +Now generate documentation, using epydoc for example: + +.. code-block:: bash + + $ epydoc example.py + + +http://docs.python.org/3/library/doctest.html + +http://www.python.org/dev/peps/pep-0257/ + +http://epydoc.sourceforge.net/ + +These days, most Python projects use Sphinx to do their documentation: + +http://sphinx-doc.org/ + +Well worth checking out -- and you can have Sphinx run your doctests for you. + +My Take: +-------- + +doctests are really cool -- but they are more a way to test your documentation, than a way to test your code. Which is great -- you can have examples in your docs, and know that they are still correct. + + +Test Driven Development (TDD) +============================= + +In TDD, the tests are written to meet the requirements before the code +exists. + +Once the collection of tests passes, the requirement is considered met. + +We've been trying to get you to do this from the beginning of this class :-) + +We don't always want to run the entire test suite. In order to run a +single test with pytest: + +.. code-block:: bash + + $ pytest -k "test_divide" + +The -k means: + + only run tests which match the given substring expression. An expression is a python evaluatable expression where all names are substring-matched against test names and their parent classes. + +So you can pretty easily select a subset of your tests if they have consistent naming scheme. + +Exercises +========= + +- Add unit tests for each method in calculator_functions.py +- Add fixtures via setUp/tearDown methods and setUpClass/tearDownClass + class methods. Are they behaving how you expect? + +or + +- Use pytest fixtures instead. +- Add additional unit tests for floating point calculations +- Fix any failures in the code +- Add doctests to calculator_functions.py + +Here are the files you'll need: + +:download:`calculator.py <../examples/testing/calculator/calculator.py>` + +:download:`calculator_functions.py <../examples/testing/calculator/calculator_functions.py>` + +:download:`calculator_test.sh <../examples/testing/calculator/calculator_test.sh>` + +:download:`test_calculator_pytest.py <../examples/testing/calculator/test_calculator_pytest.py>` + +:download:`calculator_test_suite.py <../examples/testing/calculator/calculator_test_suite.py>` + +:download:`test_calculator.py <../examples/testing/calculator/test_calculator.py>` + + +Mocking +======= + +Now we've got the tools to really test +-------------------------------------- + +Consider the application in: + +:download:`wikidef.zip <../examples/wikidef.zip>` + +Give the command line utility a subject, and it will return a definition. + +.. code-block:: bash + + ./define.py Robot + +How can we test our application code without abusing (and waiting for) +Wikipedia? + + +Using Mock objects +------------------ + +Using Mock objects to test an application with service dependencies + +Mock objects replace real objects in your code at runtime during test + +This allows you to test code which calls these objects without having +their actual code run + +Useful for testing objects which depend on unimplemented code, resources +which are expensive, or resources which are unavailable during test +execution + +https://docs.python.org/3/library/unittest.mock-examples.html + + +Mocks +----- + +The MagicMock class will keep track of calls to it so we can verify +that the class is being called correctly, without having to execute the +code underneath + +.. code-block:: python + + from unittest import mock + + mock_object = mock.MagicMock() + mock_object.foo.return_value = "foo return" + print(mock_object.foo.call_count) + print(mock_object.foo()) + print(mock_object.foo.call_count) + # raise an exception by assigning to the side_effect attribute + mock_object.foo.side_effect = Exception + mock_object.foo() + + +Easy mocking with mock.patch +---------------------------- + +patch acts as a function decorator, class decorator, or a context +manager + +Inside the body of the function or with statement, the target is patched +with a new object. When the function/with statement exits the patch is +undone + + +Using patch +----------- + +:: + + # patch with a decorator + @patch.object(Wikipedia, 'article') + def test_article_success_decorator_mocked(self, mock_method): + article = Definitions.article("Robot") + mock_method.assert_called_once_with("Robot") + + # patch with a context manager + def test_article_success_context_manager_mocked(self): + with patch.object(Wikipedia, 'article') as mock_method: + article = Definitions.article("Robot") + mock_method.assert_called_once_with("Robot") + +There are a number of ways to use ``mock.patch`` -- this is a nice discussion of that: `The Many Flavors of mock.patch `_ + + +mocking with pytest +------------------- + +pytest uses the same mock library, but has a little different syntax. + +Here is an example of mocking ``input()`` with pytest: + +:download:`test_mock_input.py ` + +``pytest-mock`` is a utility that makes it easier to mock +with pytest. + +.. code-block:: bash + + $ pip install pytest-mock + +Here is a nice blog post about using it: + +https://medium.com/@bfortuner/python-unit-testing-with-pytest-and-mock-197499c4623c + +Exercise +........ + +When ``define.py`` is given the name of a non-existent article, an exception +is thrown. This exception causes another exception to occur, and the whole thing is not very readable. Why does this happen? + +Use what you know about exceptions to throw a better exception, and +then add a new test that confirms this behavior. Use mock for your test, so you are not hammering Wikipedia. + + +Mocking a builtin +----------------- + +Say you would like to mock input in this function in a file called mock_input.py: + +.. code-block:: python + + def get_input(): + color = input("What is your favorite color? ") + return color + + +In your test file, you would do this: + +.. code-block:: python + + @mock.patch('builtins.input') + def test_get_input(self, new_mocked_input): + new_mocked_input.return_value = 'blue' + self.assertEqual(mock_input.get_input(), 'blue') + + + + diff --git a/_sources/modules/ThreadingMultiprocessing.rst.txt b/_sources/modules/ThreadingMultiprocessing.rst.txt new file mode 100644 index 0000000..113e1ef --- /dev/null +++ b/_sources/modules/ThreadingMultiprocessing.rst.txt @@ -0,0 +1,713 @@ + +.. _threading: + +############################# +Threading and multiprocessing +############################# + +Threading / multiprocessing +=========================== + +How to actually DO threading and multiprocessing: + +- ``threading`` module +- ``multiprocessing`` module + +Parallel programming can be hard! + +If your problem can be solved sequentially, consider the costs and +benefits before going parallel. + + +Parallelization Strategy for Performance +---------------------------------------- + +| 1. Break problem down into chunks +| 2. Execute chunks in parallel +| 3. Reassemble output of chunks into result + +.. image:: /_static/OPP.0108.gif + :align: right + :height: 450px + :alt: multitasking flow diagram + +| +| + +- Not every problem is parallelizable +- There is an optimal number of threads for each problem in each + environment, so make it tunable +- Working concurrently opens up synchronization issues +- Methods for synchronizing threads: + + - locks + - queues + - signaling/messaging mechanisms + +The mechanics: how do you use threads and/or processes +====================================================== + +Python provides the `threading` and `multiprocessing` modules to facility concurrency. + +They have similar APIs -- so you can use them in similar ways. + +Key points: + + - There is no Python thread scheduler, it is up to the host OS. yes these are "true" threads. + - Works well for I/O bound problems, can use literally thousands of threads + - Limit CPU-bound processing to C extensions (that release the GIL) + - Do not use for CPU bound problems, will go slower than no threads, especially on multiple cores!!! (see David Beazley's talk referenced above) + +Starting threads is relatively simple, but there are many potential issues. + +We already talked about shared data, this can lead to a "race condition". + + - May produce slightly different results every run + - May just flake out mysteriously every once in a while + - May run fine when testing, but fail when run on: + - a slower system + - a heavily loaded system + - a larger dataset + - Thus you *must* synchronize threads! + +Example: A CPU bound problem +---------------------------- + +Numerically integrate the function +:math:`y =x^2` from 0 to 10. + +http://www.wolframalpha.com/input/?i=x%5E2 + +.. image:: /_static/x2.png + :height: 400px + +`Solution `_ + +Parallel execution example +-------------------------- + +Consider the following code in: +:download:`integrate.py <../examples/threading-multiprocessing/integrate.py>` + +.. code-block:: python + + def f(x): + return x**2 + + def integrate(f, a, b, N): + s = 0 + dx = (b-a)/N + for i in xrange(N): + s += f(a+i*dx) + return s * dx + +We can do better than this + +Break down the problem into parallelizable chunks, then add the results +together: + +The threading module +-------------------- + +Starting threads doesn't take much: + +.. code-block:: python + + import sys + import threading + import time + + def func(): + for i in xrange(5): + print("hello from thread %s" % threading.current_thread().name) + time.sleep(1) + + threads = [] + for i in xrange(3): + thread = threading.Thread(target=func, args=()) + thread.start() + threads.append(thread) + + +- The process will exit when the last non-daemon thread exits. +- A thread can be specified as a daemon thread by setting its daemon + attribute: ``thread.daemon = True`` +- daemon threads get cut off at program exit, without any opportunity + for cleanup. But you don't have to track and manage them. Useful for + things like garbage collection, network keepalives, .. +- You can block and wait for a thread to exit with thread.join() + + +Subclassing Thread +------------------ + +You can add threading capability to your own classes + +Subclass Thread and implement the run method + + +.. code-block:: python + + import threading + + class MyThread(threading.Thread): + + def run(self): + print("hello from %s" % threading.current_thread().name) + + thread = MyThread() + thread.start() + + +Race Conditions +--------------- + +In the last example we saw threads competing for access to stdout. + +Worse, if competing threads try to update the same value, we might get +an unexpected race condition + +Race conditions occur when multiple statements need to execute +atomically, but get interrupted midway + +:download:`race_condition.py <../examples/threading-multiprocessing/race_condition.py>` + +No race condition +------------------ + ++--------------------+--------------------+--------------------+--------------------+ +| Thread 1 | Thread 2 | | Integer value | ++====================+====================+====================+====================+ +| | | | 0 | ++--------------------+--------------------+--------------------+--------------------+ +| read value | | ← | 0 | ++--------------------+--------------------+--------------------+--------------------+ +| increase value | | | 0 | ++--------------------+--------------------+--------------------+--------------------+ +| write back | | → | 1 | ++--------------------+--------------------+--------------------+--------------------+ +| | read value | ← | 1 | ++--------------------+--------------------+--------------------+--------------------+ +| | increase value | | 1 | ++--------------------+--------------------+--------------------+--------------------+ +| | write back | → | 2 | ++--------------------+--------------------+--------------------+--------------------+ + +Race Condition! +--------------- + ++--------------------+--------------------+--------------------+--------------------+ +| Thread 1 | Thread 2 | | Integer value | ++====================+====================+====================+====================+ +| | | | 0 | ++--------------------+--------------------+--------------------+--------------------+ +| read value | | ← | 0 | ++--------------------+--------------------+--------------------+--------------------+ +| | read value | ← | 0 | ++--------------------+--------------------+--------------------+--------------------+ +| increase value | | | 0 | ++--------------------+--------------------+--------------------+--------------------+ +| | increase value | | 0 | ++--------------------+--------------------+--------------------+--------------------+ +| write back | | → | 1 | ++--------------------+--------------------+--------------------+--------------------+ +| | write back | → | 1 | ++--------------------+--------------------+--------------------+--------------------+ + +http://en.wikipedia.org/wiki/Race_condition + +Deadlocks +--------- + +Synchronization and Critical Sections are used to control race +conditions + +But they introduce other potential problems... + +like: http://en.wikipedia.org/wiki/Deadlock + +"A deadlock is a situation in which two or more competing actions are +each waiting for the other to finish, and thus neither ever does." + +*When two trains approach each other at a crossing, both shall come to a +full stop and neither shall start up again until the other has gone* + +See also *Livelock*: + +*Two people meet in a narrow corridor, and each +tries to be polite by moving aside to let the other pass, but they end +up swaying from side to side without making any progress because they +both repeatedly move the same way at the same time.* + + +Locks +----- + +Lock objects allow threads to control access to a resource until they're done with it + +This is known as mutual exclusion, often called "mutex". + +A Lock has two states: locked and unlocked + +If multiple threads have access to the same Lock, they can police +themselves by calling its ``.acquire()`` and ``.release()`` methods + +If a Lock is locked, .acquire will block until it becomes unlocked + +These threads will wait in line politely for access to the statements in f() + +Mutex locks (``threading.Lock``) +-------------------------------- + + - Probably most common + - Only one thread can modify shared data at any given time + - Thread determines when unlocked + - Must put lock/unlock around critical code in ALL threads + - Difficult to manage + +Easiest with context manager: + +.. code-block:: python + + x = 0 + x_lock = threading.Lock() + + # Example critical section + with x_lock: + # statements using x + + +Only one lock per thread! (or risk mysterious deadlocks) + +Or use RLock for code-based locking (locking function/method execution rather than data access) + + +.. code-block:: python + + import threading + import time + + lock = threading.Lock() + + def f(): + lock.acquire() + print("%s got lock" % threading.current_thread().name) + time.sleep(1) + lock.release() + + threading.Thread(target=f).start() + threading.Thread(target=f).start() + threading.Thread(target=f).start() + + +Nonblocking Locking +------------------- + +``.acquire()`` will return True if it successfully acquires a lock + +Its first argument is a boolean which specifies whether a lock should +block or not. The default is ``True`` + +.. code-block:: python + + import threading + lock = threading.Lock() + lock.acquire() + if not lock.acquire(False): + print("couldn't get lock") + lock.release() + if lock.acquire(False): + print("got lock") + + +``threading.RLock`` - Reentrant Lock +------------------------------------ + +Useful for recursive algorithms, a thread-specific count of the locks is +maintained + +A reentrant lock can be acquired multiple times by the same thread + +``Lock.release()`` must be called the same number of times as ``Lock.acquire()`` +by that thread + + +``threading.Semaphore`` +----------------------- + +Like an ``RLock``, but in reverse + +A Semaphore is given an initial counter value, defaulting to 1 + +Each call to ``acquire()`` decrements the counter, ``release()`` increments it + +If ``acquire()`` is called on a Semaphore with a counter of 0, it will block +until the Semaphore counter is greater than 0. + +Useful for controlling the maximum number of threads allowed to access a +resource simultaneously + +`Semaphore `_ + +.. image:: /_static/flags.jpg + :height: 250px + +Events (``threading.Event``) +---------------------------- + + - Threads can wait for particular event + - Setting an event unblocks all waiting threads + +Common use: barriers, notification + + +Condition (``threading.Condition``) +----------------------------------- + + - Combination of locking/signaling + - lock protects code that establishes a "condition" (e.g., data available) + - signal notifies threads that "condition" has changed + +Common use: producer/consumer patterns + + +Locking Exercise +---------------- + +:download:`lock_exercise.zip <../examples/threading-multiprocessing/lock_exercise.zip>` + +In: ``lock/stdout_writer.py`` + +Multiple threads in the script write to stdout, and their output gets +jumbled + +1. Add a locking mechanism to give each thread exclusive access to + stdout + +2. Try adding a Semaphore to allow 2 threads access at once + + +Managing thread results +----------------------- + +We need a thread safe way of storing results from multiple threads of +execution. That is provided by the Queue module. + +Queues allow multiple producers and multiple consumers to exchange data +safely + +Size of the queue is managed with the maxsize kwarg + +It will block consumers if empty and block producers if full + +If maxsize is less than or equal to zero, the queue size is infinite + +.. code-block:: python + + from Queue import Queue + q = Queue(maxsize=10) + q.put(37337) + block = True + timeout = 2 + print(q.get(block, timeout)) + +- http://docs.python.org/3/library/threading.html +- http://docs.python.org/3/library/queue.html + +Queues (``queue``) +------------------ + + - Easier to use than many of above + - Do not need locks + - Has signaling + +Common use: producer/consumer patterns + + +.. code-block:: python + + + from Queue import Queue + data_q = Queue() + + Producer thread: + for item in produce_items(): + data_q.put(item) + + Consumer thread: + while True: + item = q.get() + consume_item(item) + + + +Scheduling (``sched``) +---------------------- + + - Schedules based on time, either absolute or delay + - Low level, so has many of the traps of the threading synchronization primitives. + +Timed events (``threading.timer``) +---------------------------------- + +Run a function at some time in the future: + +.. code-block:: python + + import threading + + def called_once(): + """ + this function is designed to be called once in the future + """ + print("I just got called! It's now: {}".format(time.asctime())) + + # setting it up to be called + t = Timer(interval=3, function=called_once) + t.start() + + # you can cancel it if you want: + t.cancel() + +:download:`simple_timer.py ` + +Other Queue types +----------------- + +``Queue.LifoQueue`` + + - Last In, First Out + +``Queue.PriorityQueue`` + + - Lowest valued entries are retrieved first + +One pattern for ``PriorityQueue`` is to insert entries of form data by +inserting the tuple: + +``(priority_number, data)`` + +Threading example with a queue +------------------------------ + +:download:`integrate_main.py <../examples/threading-multiprocessing/integrate_threads.py>` + +.. code-block:: python + + #!/usr/bin/env python + + import threading + import queue + + # from integrate.integrate import integrate, f + from integrate import f, integrate_numpy as integrate + from decorators import timer + + + @timer + def threading_integrate(f, a, b, N, thread_count=2): + """break work into N chunks""" + N_chunk = int(float(N) / thread_count) + dx = float(b - a) / thread_count + + results = queue.Queue() + + def worker(*args): + results.put(integrate(*args)) + + for i in range(thread_count): + x0 = dx * i + x1 = x0 + dx + thread = threading.Thread(target=worker, args=(f, x0, x1, N_chunk)) + thread.start() + print("Thread %s started" % thread.name) + + return sum((results.get() for i in range(thread_count))) + + + if __name__ == "__main__": + + # parameters of the integration + a = 0.0 + b = 10.0 + N = 10**8 + thread_count = 8 + + print("Numerical solution with N=%(N)d : %(x)f" % + {'N': N, 'x': threading_integrate(f, a, b, N, thread_count=thread_count)}) + + +Threading on a CPU bound problem +-------------------------------- + +Try running the code in :download:`integrate_threads.py ` + +It has a couple of tunable parameters: + +.. code-block:: python + + a = 0.0 # the start of the integration + b = 10.0 # the end point of the integration + N = 10**8 # the number of steps to use in the integration + thread_count = 8 # the number of threads to use + +What happens when you change the thread count? What thread count gives the maximum speed? + + +Multiprocessing +--------------- + + - processes are completely isolated + - no locking :) (and no GIL!) + - instead of locking: messaging + +``multiprocessing`` provides an API very similar to ``threading``, so the transition is easy + +use ``multiprocessing.Process`` instead of ``threading.Thread`` + +.. code-block:: python + + import multiprocessing + import os + import time + + def func(): + print "hello from process %s" % os.getpid() + time.sleep(1) + + proc = multiprocessing.Process(target=func, args=()) + proc.start() + proc = multiprocessing.Process(target=func, args=()) + proc.start() + + +Differences with Threading +-------------------------- + +Multiprocessing has its own ``multiprocessing.Queue`` which handles +interprocess communication + +Also has its own versions of ``Lock``, ``RLock``, ``Semaphore`` + +.. code-block:: python + + from multiprocessing import Queue, Lock + +``multiprocessing.Pipe`` for 2-way process communication: + +.. code-block:: python + + from multiprocessing import Pipe + parent_conn, child_conn = Pipe() + child_conn.send("foo") + print parent_conn.recv() + +Messaging +--------- + +Pipes (``multiprocessing.Pipe``) +................................ + + - Returns a pair of connected objects + - Largely mimics Unix pipes, but higher level + - send pickled objects or buffers + + +Queues (``multiprocessing.Queue``) +.................................. + + - same interface as ``queue.Queue`` + - implemented on top of pipes + - means you can pretty easily port threaded programs using queues to multiprocessing + - queue is the only shared data + - data is all pickled and unpickled to pass between processes -- significant overhead. + + +Other features of the multiprocessing package +............................................. + + - Pools + - Shared objects and arrays + - Synchronization primitives + - Managed objects + - Connections + + +Pooling +------- + +A processing pool contains worker processes with only a configured +number running at one time + +.. code-block:: python + + from multiprocessing import Pool + pool = Pool(processes=4) + +The Pool module has several methods for adding jobs to the pool + +``apply_async(func[, args[, kwargs[, callback]]])`` + +``map_async(func, iterable[, chunksize[, callback]])`` + + +Pooling example +--------------- + +.. code-block:: python + + from multiprocessing import Pool + def f(x): + return x*x + if __name__ == '__main__': + pool = Pool(processes=4) + + result = pool.apply_async(f, (10,)) + print(result.get(timeout=1)) + print(pool.map(f, range(10))) + + it = pool.imap(f, range(10)) + print(it.next()) + print(it.next()) + print(it.next(timeout=1)) + + import time + result = pool.apply_async(time.sleep, (10,)) + print(result.get(timeout=1)) + +http://docs.python.org/3/library/multiprocessing.html#module-multiprocessing.pool + + +ThreadPool +---------- + +Threading also has a pool + +Confusingly, it lives in the multiprocessing module + +.. code-block:: python + + from multiprocessing.pool import ThreadPool + pool = ThreadPool(processes=4) + + +.. Threading versus multiprocessing, networking edition +.. ---------------------------------------------------- + +.. :download:`server.zip <../examples/threading-multiprocessing/server.zip>` + +.. We're going to test making concurrent connections to a web service in: + +.. ``server/app.py`` + +.. It is a WSGI application which can be run with Green Unicorn or another WSGI server + +.. ``$ gunicorn app:app --bind 0.0.0.0:37337`` + +.. ``client-threading.py`` makes 100 threads to contact the web service + +.. ``client-mp.py`` makes 100 processes to contact the web service + +.. ``client-pooled.py`` creates a ThreadPool + +.. ``client-pooled.py`` contains a results Queue, but doesn't use it. Can you collect all the output from the pool into a single data structure using this Queue? diff --git a/_sources/modules/Tutorial.rst.txt b/_sources/modules/Tutorial.rst.txt new file mode 100644 index 0000000..a1562ba --- /dev/null +++ b/_sources/modules/Tutorial.rst.txt @@ -0,0 +1,334 @@ +:orphan: + +NOTE: this is only the start of a tutorial -- it would be nice to complete it some day! + +=============== +Python Tutorial +=============== + +This is a tutorial to get you started. Not a lot of explanation, +but enough to get you going with writing basic python code. + +This tutorial uses exclusively Python3. + +You may want to also check out some other tutorial options: + +* **Jessica McKeller's beginning tutorial** (Video) + https://www.youtube.com/watch?v=MirG-vJOg04 + +* **The Python Tutorial** + (https://docs.python.org/3/tutorial/): This is the + official tutorial from the Python website. No more authoritative source is + available. + +* **Code Academy Python Track** + (http://www.codecademy.com/tracks/python): Often + cited as a great resource, this site offers an entertaining and engaging + approach and in-browser work. + +* **Learn Python the Hard Way** + (https://learnpythonthehardway.org/python3/): Solid and gradual. + This course offers a great foundation for folks who have never + programmed in any language before. It used be be fully available + for free online -- but now you can only get the first bits. But still + a good way to get started. + + +Running Your Code +================= + +There are a number of ways to run python code: + +- At the interpreter, often referred to as a REPL (Read, Evaluate, Print Loop) +- At an enhanced interpreter such as iPython +- In a browser-based interactive system such as the Jupyter Notebook +- From an IDE, such as IDLE or PyCharm +- Calling python from the command line to run a file. + +While working with an interactive interpreter can be an excellent way to explore Python (and I highly recommend it), For this tutorial, to get you used to "real" production development, you will write, edit, and save your code in a programmer's text editor, and run it from the command line. + + +A Programmer's Text Editor +-------------------------- + +See These notes for getting set up with an editor and Python itself: :ref:`setting_up_dev_environment` + + + +The Python Interpreter +---------------------- + +Python is a "byte compiled, interpreted" language. What this means to you is that you need a Python interpreter to run your Python code. It also means that that is all you need -- write some code, and run it with Python. That's it. + +There are a few Python interpreters (or run-times) available: + +- cPython +- PyPy +- Jython +- Iron Python +- MicroPython + +These each have their own special uses for interaction the the Java VM or CLR, or running on micro controllers. But most production Python is run with the cPython interpreter, and most people mean cPython when they say "Python". + + +For this tutorial, you will need cPython version 3.7 or 3.8, installed and running so that when you type "python" at your command line, it starts up: + +.. code-block:: bash + + MacBook-Pro:~ Chris$ python + Python 3.7.4 (v3.6.2:5fd33b5926, Jul 16 2017, 20:11:06) + [GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin + Type "help", "copyright", "credits" or "license" for more information. + >>> + +Your result may be slightly different, but it should say Python 3. *something* there at the top, and give you the command prompt (``>>>``) at the end. + +You can get out of it by typing ctrl+D (on OS-X and Linux) or ctrl+Z (On Windows), or typing ``exit()`` and then . + +Your first "program" +-------------------- + +Create your first program by typing this into your text editor:: + + print("this worked!") + +Type it exactly as above, with no extra space at the beginning, and no other characters. + +Save the file as ``first.py``. Make sure to save it somewhere that makes sense, maybe a directory you create for this purpose, called "tutorial". + +Start up the command line ("Terminal" on OS-X, "Command Prompt" on Windows), and "Navigate" to the directory where you just saved that file:: + + cd tutorial + +Now run your little program, by typing ``python first.py`` and hitting . You should get something like this: + +.. code-block:: bash + + MacBook-Pro:tutorial Chris$ python first.py + this worked! + MacBook-Pro:tutorial Chris$ + +If this is *NOT* what you got then something went wrong. Some things to check: + + - Did you save the file? + - Is your command prompt "in" the same directory as the file? + - you can check this by typing ``ls`` on \*nix, and ``dir`` on Windows, to see what files are in the dir that the command prompt is in. + - Did you type *exactly* the same line as above? + +What did you just do? +..................... + +The "python" command starts up the python interpreter. If you "pass in" a file name, by typing the name of the file after "python", then the interpreter will read that file and run any code that is in it. + +In this case, python ran the one line of code you put in that file, which told it to print the text: "this worked!" -- and that is what it did. + +The print function +------------------ + +You can display just about anything in Python with the ``print()`` function. Simply type:: + + print(what you want to print) + +examples: + + print(45) + print("this is a bit of text") + +you can print more than one thing by separating them with commas, inside the parenthesis:: + + print("the value of pi is:", 3.1459, "to four decimal places") + + +Text in Python +-------------- + +Text in python is supported by the "str" datatype, which is short for "string". The text datatype is often referred to as "strings" in computer science because it is a series, or string, of characters. + +In Python3, strings can be any length, and contain any character (in virtually any language). This is because they support "Unicode" which is a system for representing all the characters of virtually all the languages used on earth. + +There are many complications to full support of Unicode, but for the most part, in Python it "just works". Any text you can put in your text editor should work fine. + +.. note:: With Unicode, the actual characters can be stored in multiple ways in the files themselves. If you've heard of "plain text", there really is no such thing anymore with Unicode. The exact way the text is stored is known as the "encoding". These days, an encoding known and "utf-8" is the mast commonly used. Python assumes that you are using utf-8 encoded files, but if strange things happen, make sure your editor is using utf-8. (or ASCII, which is an older encoding that does not support multiple languages -- but ASCII is subset of utf-8, so it still works.) + +To create a str, you simply type what you want surrounded by either double or single quotes (the apostrophe). + +Type this in a new file, called ``strings.py``: + +.. code-block:: python + + print("This is a basic string") + + print('This is exactly the same string') + + print("You want to use double quotes if there's an apostrophe, like this: ' in the string") + + print('You can use single quotes if you want to "quote" a word') + +run the file, and you should get something like this:: + + MacBook-Pro:tutorial Chris$ python strings.py + This is a basic string + This is exactly the same string + You want to use double quotes if there's an apostrophe, like this: ' in the string + You can use single quotes if you want to "quote" a word + +Numbers in Python +----------------- + +Python supports two types of numbers: integers (int) -- or "whole numbers", with no fractional part: + +.. code-block:: python + + 3, 123, -345, 23473948 + +integers can be negative or positive and as large as you want: + +.. code-block:: python + +>>> print(12345678987654321234567890987654321234567898765) +12345678987654321234567890987654321234567898765 + +"real numbers" are called "floating point" (float) numbers. They are internally stored as binary, but you write them as regular decimal (base 10) numbers: + +.. code-block:: python + + 2.3, 3.0, 3.2459, -23.21 + +Note that while the integer`3` and the float `3.0` have the same value, they are different types of numbers. But for the most part, Python will convert from integer to floating point numbers for you (and back again), so this distinction is rarely important. + +Math +---- + +Being a computer language, Python, of course, supports the regular math functions. Type the following into a file named math.py and run it: + +.. code-block:: python + + print(3) + print(3 * 4) + print(3 * 4 + 10 - 2) + print("twelve divided by 5 is:") + print(12 / 5) + + print("twelve divided by 5 is:") + print(12 // 5) + +What is the difference between ``12 / 5`` and ``12 // 5`` ? Run your this code and find out. + +Order of Operations +------------------- + +Python follows the standard rules of "operator precedence" from algebra -- which operations are performed first when there are a bunch in a row: + +https://en.wikipedia.org/wiki/Order_of_operations + +Add this to the ``math.py`` file: + +.. code-block:: python + + print(3 + 4 / 2) + +run the file, and see if you get the answer you expect. The result should be 5.0, not 6.0. + +That is because multiplication and division are a higher priority than addition, so Python divided 4 by 2 to get 2.0, and then added 3 + 2.0 to get 5.0. + +Always keep that in mind when you do math expressions in Python. If you want to change the order of operations, you can group them with parentheses. Try adding this to the ``math.py`` file and run it: + +.. code-block:: python + + print(3 + (4 / 2)) + print((3 + 4) / 2) + +Python will always evaluate what is in parentheses first. + +Variables +--------- + +Directly printing things is not all that useful -- though Python does make a good calculator! + +To do anything more complicated, you need to store values to be used later. We do this by "assigning" them to a "variable", essentially giving them a name. Save the following in a ``variables.py`` file: + +.. code-block:: python + + x = 5 + y = 20 + z = x + y + + print("the value of z is: ", z) + +The equals sign: ``=`` is the "assignment operator". It assigns a value to a name, and then when you use the name in the future, Python will replace it with the value it is assigned to when it is used. + +Names can (and generally should) be long and descriptive, and can contain letters, numbers (but not at the beginning) and only a few symbols, like the underscore character: + +.. code-block:: python + + rectangle_width = 200 + rectangle_height = 23 + rectangle_area = rectangle_width * rectangle_height + +Comments +-------- + +Try running this code: + +.. code-block:: python + + print("this") + # print ("that") + print("the other") + +What does it print? + +"that" didn't print because the "#" symbol (the hash) tells python not to run any code after it on that line. + +How about this? + +.. code-block:: python + + print("this") + print ("that") I think we need this line too + print("the other") + +And this? + +.. code-block:: python + + # Here we are printing useless stuff: + print("this") + print ("that") # I think we need this line too + print("the other") + +comments can come after running code on a line as well. Using the hash to "comment out" parts of code is used in two ways: + +1) To add a little extra description to some code, to explain what it doing. + +2) To temporarily disable some code + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_sources/modules/Unicode.rst.txt b/_sources/modules/Unicode.rst.txt new file mode 100644 index 0000000..881cfef --- /dev/null +++ b/_sources/modules/Unicode.rst.txt @@ -0,0 +1,581 @@ +:orphan: + +.. _unicode: + +================= +Unicode in Python +================= + + +A quick run-down of Unicode, + +Its use in Python 2 and 3, + +and some of the gotchas that arise. + + +History +======= + +A bit about where all this mess came from... + + +What the heck is Unicode anyway? +--------------------------------- + +* First there was chaos... + + * Different machines used different encodings -- different ways of mapping + the binary data that the computer stores to letters. + +* Then there was ASCII -- and all was good (7 bit), 127 characters + + * (for English speakers, anyway) + +* But each vendor used the top half of 8bit bytes (127-255) for different things. + + * MacRoman, Windows 1252, etc... + + * There is now "latin-1", a 1-byte encoding suitable for European languages -- but still a lot of old files around that use the old ones. + +* Non-Western European languages required totally incompatible 1-byte encodings + +* This means there was no way to mix languages with different alphabets in the same document (web page, etc.) + + +Enter Unicode +-------------- + +The Unicode idea is pretty simple: + * One "code point" for all characters in all languages + +But how do you express that in bytes? + * Early days: we can fit all the code points in a two byte integer (65536 characters) + + * Turns out that didn't work -- 65536 is not enough for all languages. So we now need 32 bit integer to hold all of Unicode "raw" (UTC-4). + * But it's a waste of space to use 4 full bytes for each character, when so many don't require that much space. + +Enter "encodings": + * An encoding is a way to map specific bytes to a code point. + + * Each code point can be represented by one or more bytes. + + * Each encoding is different -- if you don't know the encoding, you don't know how to interpret the bytes! (though maybe you can guess) + + +Unicode +------- + +A good start: + +The Absolute Minimum Every Software Developer Absolutely, +Positively Must Know About Unicode and Character Sets (No Excuses!) + +http://www.joelonsoftware.com/articles/Unicode.html + + +**Everything is Bytes** + +* If it's on disk or on a network, it's bytes + +* Python provides some abstractions to make it easier to deal with bytes + +**Unicode is a Biggie** + +Actually, dealing with numbers rather than bytes is big + +-- but we take that for granted. + + +Mechanics +========= + +What are strings? +----------------- + +Py2 strings were simply sequences of bytes. When text was one per character that worked fine. + +Py3 strings (or Unicode strings in py2) are sequences of "platonic characters". + +It's almost one code point per character -- there are complications +with combined characters: accents, etc -- but we can mostly ignore those -- you will get far thinking of a code point as a character. + +Platonic characters cannot be written to disk or network! + +(ANSI: one character == one byte -- it was so easy!) + + +Strings vs Unicode +------------------ + +Python 2 had two types that let you work with text: + +* ``str`` + +* ``unicode`` + +And two ways to work with binary data: + +* ``str`` + +* ``bytes()`` (and ``bytearray``) + +**but:** + +.. code-block:: ipython + + In [86]: str is bytes + Out[86]: True + +``bytes`` is there in py2 for py3 compatibility -- but it's good for making your intentions clear, too. + +py3 is more clear: + + ``str`` for text + ``bytes`` for binary data + +Unicode +-------- + +The py3 string (py2 ``Unicode``) object lets you work with characters, instead of bytes. + +It has all the same methods you'd expect a string object to have. + +Encoding / Decoding +------------------- + +If you need to deal with the actual bytes for some reason, you may need to convert between a string object and a particular set of bytes. + +**"encoding"** is converting from a string object to bytes + +**"decoding"** is converting from bytes to a string object + +(sometimes this feels backwards...) + +And can get even more confusing with py2 strings being *both* text and bytes! + +This is actually one of the biggest differences between Python 2 and Python 3. As an ordinary user (particularly one that used English...), you may not notice -- text is text, and things generally "just work", but under the hood it is very different, and folks writting libraries for things like Internet protocols struggled with the differences. + +Using Unicode in Py2 +-------------------- + +If you do need to write Python2 code, you really should use Unicode. If you don't -- skip ahead to "Unicode Literals". + +Here are the basics: + +Built in functions +.................. + +.. code-block:: python + + ord() + chr() + unichr() + str() + unicode() + +The codecs module +................. + +.. code-block:: python + + import codecs + codecs.encode() + codecs.decode() + codecs.open() # better to use ``io.open`` + + +Encoding and Decoding +---------------------- + +(Python 2!) + +**Encoding:** text to bytes -- you get a bytes (str) object + +.. code-block:: ipython + + In [17]: u"this".encode('utf-8') + Out[17]: 'this' + + In [18]: u"this".encode('utf-16') + Out[18]: '\xff\xfet\x00h\x00i\x00s\x00' + +**Decoding** bytes to text -- you get a unicode object + +.. code-block:: ipython + + In [2]: text = '\xff\xfe."+"x\x00\xb2\x00'.decode('utf-16') + + In [3]: type(text) + Out[3]: unicode + + In [4]: print text + ∮∫x² + + +Unicode Literals +---------------- + +How do you get text that isn't plain English text? + +1) Use Unicode in your source files: + +.. code-block:: python + + # -*- coding: utf-8 -*- + +(This is only required on Py2 -- the UTF-8 encoding is default for Python 3) + +2) Escape the Unicode characters: + +Either by its hexadecimal value, or its name: + +.. code-block:: python + + print u"The integral sign: \u222B" + print u"The integral sign: \N{integral}" + +Lots of tables of code points are available online: + +One example: http://inamidst.com/stuff/unidata/ + +:download:`hello_unicode.py <../examples/unicode/hello_unicode.py>`. + + +Using Unicode +------------- + +Use ``unicode`` objects in all your code + +**Decode on input** + +**Encode on output** + +Many packages do this for you: *XML processing, databases, ...* + +**Gotcha:** + +Python has a default encoding. On Mac and Unix systems, it's usually utf-8 -- Windows? not nearly as consistent. + +.. code-block:: ipython + + In [7]: sys.getdefaultencoding() + Out[7]: 'utf-8' + +Try this on your machine, and see what you get. + +The default encoding will get used in unexpected places! +Notably in text files by default. + +Using Unicode Everywhere +------------------------- + +Python 2.6 and above have a nice feature to make it easier to use Unicode everywhere + +.. code-block:: python + + from __future__ import unicode_literals + +After running that line, the ``u''`` is assumed + +.. code-block:: ipython + + In [1]: s = "this is a regular py2 string" + In [2]: print type(s) + + + In [3]: from __future__ import unicode_literals + In [4]: s = "this is now a unicode string" + In [5]: type(s) + Out[5]: unicode + +NOTE: You can still get py2 strings from other sources! So you still need to think about ``str`` vs ``unicdode`` + +This is a really good idea if you want to write code compatible with Python2 and 3. + +Encodings +---------- + +What encoding should I use??? + +There are a lot: + +http://en.wikipedia.org/wiki/Comparison_of_Unicode_encodings + +But only a couple you are likely to need: + +* utf-8 (``*nix``) +* utf-16 (Windows) + +and of course, still the one-bytes ones. + +* ASCII +* Latin-1 + +UTF-8 +----- + +Probably the one you'll use most -- most common in Internet protocols (xml, JSON, etc.) + +Nice properties: + +* ASCII compatible: First 127 characters are the same as ASCII + +* Any ascii string is a utf-8 string + +* Compact for mostly-English text. + +Gotchas: + +* "higher" code points may use more than one byte: up to 4 for one character + +* ASCII compatible means in may work with default encoding in tests -- but then blow up with real data... + +UTF-16 +------ + +Kind of like UTF-8, except it uses at least 16bits (2 bytes) for each character: NOT ASCII compatible. + +But it still needs more than two bytes for some code points, so you still can't simply process it as two bytes per character. + +In C/C++, it is held in a "wide char" or "wide string". + +MS Windows uses UTF-16, as does (I think) Java. + + +UTF-16 criticism +----------------- + +There is a lot of criticism on the net about UTF-16 -- it's kind of the worst of both worlds: + +* You can't assume every character is the same number of bytes +* It takes up more memory than UTF-8 + +`UTF-16 Considered Harmful `_ + +But to be fair: + +Early versions of Unicode: everything fit into two bytes (65536 code points). MS and Java were fairly early adopters, and it seemed simple enough to just use 2 bytes per character. + +When it turned out that 4 bytes were really needed, they were kind of stuck in the middle. + +Latin-1 +-------- + +**NOT Unicode**: + +A 1-byte per char encoding. + +* Superset of ASCII suitable for Western European languages. + +* The most common one-byte per char encoding for European text. + +* Nice property -- every byte value from 1 to 255 is a valid character ( at least in Python ) + +* You will never get an UnicodeDecodeError if you try to decode arbitrary bytes with latin-1. + +* And it can "round-trip" through a unicode object. + +* Useful if you don't know the encoding -- at least it won't raise an Exception + +* Useful if you need to work with combined text+binary data. + +:download:`latin1_test.py <../examples/unicode/latin1_test.py>`. + + +Unicode Docs +------------ + +Python Docs Unicode HowTo: + +http://docs.python.org/howto/unicode.html + +"Reading Unicode from a file is therefore simple" + +use plain old open: + +.. code-block:: python + + open('unicode.rst', encoding='utf-8') + for line in f: + print repr(line) + + +Encodings Built-in to Python: + http://docs.python.org/3/library/codecs.html#standard-encodings + + +Gotchas in Python 2 +------------------- + +(Probaly do'nt need to worry about this anymore!) + +File names, etc: + +If you pass in unicode, you get unicode + +.. code-block:: ipython + + In [9]: os.listdir('./') + Out[9]: ['hello_unicode.py', 'text.utf16', 'text.utf32'] + + In [10]: os.listdir(u'./') + Out[10]: [u'hello_unicode.py', u'text.utf16', u'text.utf32'] + +Python deals with the file system encoding for you... + +But: some more obscure calls don't support unicode filenames: + +``os.statvfs()`` (http://bugs.python.org/issue18695) + + +Exception messages: + + * Py2 Exceptions use str when they print messages. + + * But what if you pass in a unicode object? + + * It is encoded with the default encoding. + + * ``UnicodeDecodeError`` Inside an Exception???? + + NOPE: it swallows it instead. + +:download:`exception_test.py <../examples/unicode/exception_test.py>`. + +Unicode in Python 3 +------------------- + +The "string" object **is** Unicode (always). + +Py3 has two distinct concepts: + +* "text" -- uses the str object (which is always Unicode!) +* "binary data" -- uses bytes or bytearray + +Everything that's about text is Unicode. + +Everything that requires binary data uses bytes. + +It's all much cleaner. + +(by the way, the recent implementations are very efficient...) + +So you can pretty much ignore encodings and all that for most basic text processing. +If you do find yourself needing to deal with binary data, you ay need to encode/decode stuff yourself. +In which case, Python provides an ``.encode()`` method on strings that encode the string to a bytes object with the encoding you select: + +.. code-block:: ipython + + In [3]: this_in_utf16 = "this".encode('utf-16') + + In [4]: this_in_utf16 + Out[4]: b'\xff\xfet\x00h\x00i\x00s\x00' + +And bytes objects have a ``.decode`` method that decodes the bytes and makes a string object: + + In [5]: this_in_utf16.decode('utf-16') + Out[5]: 'this' + +It's all quite simple an robust. + +.. note:: + During the long and painful transition from Python2 to Python3, the Unicode-always string type was a major source of complaints. There are many rants and `well thought out posts `_ about it still available on the internet. It was enough to think that Python had made a huge mistake. + + But there are a couple key points to remember: + + * The primary people struggling were those that wrote (or worked with) libraries that had to deal with protocols that used both binary and text data in the same data stream. + + * As of Python 3.4 or so, the python string object had grown the features it needed to support even those ugly binary+text use cases. + + For a typical user, the Python3 text model is MUCH easier to deal with and less error prone. + + +Exercises +========= + +Basic Unicode LAB +------------------- + +* Find some nifty non-ascii characters you might use. + + - Create a unicode object with them in two different ways. + - :download:`here <../examples/unicode/hello_unicode.py>` is one example + +* Read the contents into unicode objects: + + - :download:`ICanEatGlass.utf8.txt <../examples/unicode/ICanEatGlass.utf8.txt>` + - :download:`ICanEatGlass.utf16.txt <../examples/unicode/ICanEatGlass.utf16.txt>` + +and / or + + - :download:`text.utf8 <../examples/unicode/text.utf8>` + - :download:`text.utf16 <../examples/unicode/text.utf16>` + - :download:`text.utf32 <../examples/unicode/text.utf32>` + +* write some of the text from the first exercise to file -- read that file back in. + +Some Help +--------- + +reference: http://inamidst.com/stuff/unidata/ + +NOTE: if your terminal does not support unicode -- you'll get an error trying to print. +Try a different terminal or IDE, or google for a solution. + +Challenge Unicode LAB +---------------------- + +Here is an error in Python2: + +.. code-block:: ipython + + In [38]: u'to \N{INFINITY} and beyond!'.decode('utf-8') + --------------------------------------------------------------------------- + UnicodeEncodeError Traceback (most recent call last) + in () + ----> 1 u'to \N{INFINITY} and beyond!'.decode('utf-8') + + /Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/encodings/utf_8.pyc in decode(input, errors) + 14 + 15 def decode(input, errors='strict'): + ---> 16 return codecs.utf_8_decode(input, errors, True) + 17 + 18 class IncrementalEncoder(codecs.IncrementalEncoder): + + UnicodeEncodeError: 'ascii' codec can't encode character u'\u221e' in position 3: ordinal not in range(128) + + +But why would you **decode** a unicode object? + +And it should be a no-op -- why the exception? + +And why 'ascii'? I specified 'utf-8'! + +It's there for backward compatibility + +What's happening under the hood + +.. code-block:: python + + u'to \N{INFINITY} and beyond!'.encode().decode('utf-8') + +It encodes with the default encoding (ascii), then decodes + +In this case, it barfs on attempting to encode to 'ascii' + +So never call decode on a unicode object! + +But what if someone passes one into a function of yours that's expecting a py2 string? + +Type checking and converting -- yeach! + +Read: + +http://axialcorps.com/2014/03/20/unicode-str/ + +See if you can figure out the decorators: + +:download:`unicodify.py <../examples/unicode/unicodify.py>`. + +(This is advanced Python JuJu: Aren't you glad I didn't ask you to write that yourself?) diff --git a/_sources/modules/dev_environment/atom_as_ide.rst.txt b/_sources/modules/dev_environment/atom_as_ide.rst.txt new file mode 100644 index 0000000..d4c0c70 --- /dev/null +++ b/_sources/modules/dev_environment/atom_as_ide.rst.txt @@ -0,0 +1,143 @@ +.. _atom_as_ide: + +########################################## +Turning Atom Into a Lightweight Python IDE +########################################## + +Atom is the self-proclaimed "hackable text editor for the 21st Century." It has a nice modern interface, and is highly customizable yet can also be used productively with minimal setup and configuration. + + +Requirements +============ + +Any IDE should ease your development experience by providing the following: + +* It should provide excellent, configurable syntax colorization. +* It should allow for robust tab completion. +* It should offer the ability to jump to the definition of symbols in other files. +* It should perform automatic code linting to help avoid silly mistakes. +* It should be able to interact with a Python interpreter such that when debugging, the editor will follow along with the debugger. + +Atom does all this and more, but some functionality requires you to select and install packages. + + +Which Version? +============== + +The latest version is the best version. Atom is regularly maintained, so the latest +version will have the latest bug fixes and updates. + + +Installation +============ + +Go to the Atom website_. + +.. _website: https://atom.io/ + +On the main page, click the big red button to download the installer, then run the installer. + +If it is not offering the correct platform: on the main page, below the big red button, click Other Platforms and find the installer for your operating system. + +If you already have Atom installed, but want to check for a newer version, go to +``Help`` -> ``Check for Update``. + + +Basic Settings +============== + +Atom can be used out of the box with no setup as a text editor. It automatically +recognizes file types and helpfully highlights text accordingly. To use in this manner, +write your Python files in Atom, then run them in your Python command prompt. + + +Extending the Editor +==================== + +When you first open Atom, a Welcome Guide appears. This provides some quick and helpful information on +how to open projects, install packages, and customize your themes and styling. + +Atom has great documentation_ on how to hack and configure it. Read the Flight Manual_ for tons of information on +everything you can do. You can also watch a Getting Started video_. + +.. _documentation: https://atom.io/docs +.. _Manual: http://flight-manual.atom.io/ +.. _video: https://www.youtube.com/watch?v=U5POoGSrtGg + +Atom has a configuration file which you can modify called config.cson. +Access it via ``File`` -> ``Config...`` + +:: + + "*": + core: + themes: [ + "atom-dark-ui" + "solarized-light-syntax" + ] + editor: + fontSize: 19 + "exception-reporting": + userId: "6e2a9c3f-7ddb-7deb-b5f7-b58f2f87ac0d" + "tree-view": + hideVcsIgnoredFiles: true + +Here you can quickly change the theme or font size. Some packages will require you to add configs +or make adjustments here. Read the documentation carefully when installing packages. + +In general, you can extend Atom by installing packages, and then accessing their functionality from the Packages +drop-down menu. Access the Install Packages page from the Welcome Guide page. If the Welcome Guide is not open, +you can open it via ``Help`` -> ``Welcome Guide``. + +Keyboard shortcuts are specified in the packages menus if available. + +The Useful Packages presented below are only a few options of many. + + +Useful Packages +=============== + +Running Scripts +--------------- + +To run scripts within Atom, you will need to install the Script_ package. The Script package supports a ton of languages, +including Python! + +.. _Script: https://atom.io/packages/script + +Autocompletion +-------------- + +By default, Atom knows which Python packages you have imported, variables you have created +and so on. Autocomplete_ ships with Atom and requires no setup. + +.. _Autocomplete: http://flight-manual.atom.io/using-atom/sections/autocomplete/ + +Code Linting +------------ + +To get code linting functionality in Atom, you will need to install a linting package +of which there are many to choose from. linter-pylint_ works well, and requires minimal +setup. + +.. _linter-pylint: https://atom.io/packages/linter-pylint + +White Space Management +---------------------- + +Atom knows when you are writing Python and helps you out by dealing with spaces and tabs +in the same way. When in a Python file, if you type 4 spaces, then hit delete, you are +taken back a tab. + +The Whitespace_ package ships with Atom and requires no setup. Under the ``Packages`` -> ``Whitespace`` menu, +you will find tools to turn all tabs into spaces, all spaces into tabs, among other whitespace-related options. + +.. _Whitespace: https://atom.io/packages/whitespace + +Debugging +--------- + +To use a Python debugger in Atom, you will need to install the python-debugger_ package. Once installed, turn on the +debugger by going to ``Packages`` -> ``python-debugger`` -> ``Toggle``. + +.. _python-debugger: https://atom.io/packages/python-debugger diff --git a/_sources/modules/dev_environment/command_line.rst.txt b/_sources/modules/dev_environment/command_line.rst.txt new file mode 100644 index 0000000..c0b7617 --- /dev/null +++ b/_sources/modules/dev_environment/command_line.rst.txt @@ -0,0 +1,38 @@ +.. _command_line_basics: + +=================== +Command line basics +=================== + +Windows +------- + +Tutorials +......... + +I honestly haven't found a really good tutorial for the Windows command line, but these will get you started. Please let us know if you find a better option! + +http://www.digitalcitizen.life/command-prompt-how-use-basic-commands + +http://www.bleepingcomputer.com/tutorials/windows-command-prompt-introduction/ + +Zed Shaw's tutorial in "Learn Code the Hard way" is excellent, but apparently is no longer available for free. However there is a tutorial in an appendix to Learn Python the Hard way: + +`Command Line Crash Course `_ + + +Linux and OS-X +-------------- + +Tutorials +......... + +* Software Carpentry had a nice introduction to the command line: + + https://swcarpentry.github.io/shell-novice/01-intro/ + +* http://conqueringthecommandline.com/book + +* Zed Shaw's tutorial in "Learn Code the Hard way" is excellent, but apparently no longer available for free. However there is one as an appendix to Learn Python the Hard way: + + https://learnpythonthehardway.org/book/appendixa.html diff --git a/_sources/modules/dev_environment/feature_branching.rst.txt b/_sources/modules/dev_environment/feature_branching.rst.txt new file mode 100644 index 0000000..4b9ae68 --- /dev/null +++ b/_sources/modules/dev_environment/feature_branching.rst.txt @@ -0,0 +1,220 @@ +***************** +Feature Branching +***************** + +Why use Feature Branches? +========================= + +The idea is to give a clear, highly focused purpose to each branch, +which is why we call them feature branches. They represent specific +feature being added or in our case a single assignment. + +When using feature branches you isolate your code from everything else, +and Pull Request becomes easier to review as they only contain one +thing. This also mean that you can work on multiple features (multiple +assignments) at the same time without effecting each other and being +able to have two separate clean PRs along side. + +Keep in mind that in a more professional setting the "master" branch +represents production code and will be the cleanest most stable branch. + +Below is what's called "Network Graph" of git activity. This displays +best what happens with feature branches. + +The black line represents master, the green represents my 1st feature +branch that got merged into master (you can see arrow goes to black +line), then I created second feature branch called "test2" (blue line) +and that also got merged into master. + +So feature branches get branched off the main branch (master) at some +point in time, then you do some isolated work, then merge it back into +main branch. No commits to master will affect your work in your feature +branch while you're in it. + +.. image:: images/feature_branching_img1.png + :scale: 75 + + +Workflow +======== + +1. Feature / Assignment Branch +------------------------------ + +Below explains the process for creating your feature branch. You can +either use the command line or the UI. + +Creating feature branch via Command Line +........................................ + +First make sure you're on master and make sure you have the latest +changes + +.. code-block:: bash + + git checkout master + + git pull + +Next you will create a new branch (-b flag) based of master + +.. code-block:: bash + + git checkout -b + +Here is the full example: + +.. code-block:: bash + + $ git checkout master + + Already on 'master' + + Your branch is up to date with 'origin/master'. + + $ git pull + + $ git checkout -b lesson1/assignment1 + + Switched to a new branch 'lesson1/assignment1' + +.. code-block:: bash + + $ git branch + branch_name1 + another_branch + * lesson1/assignment1 <------- The asterisk here indicates your current branch + master + + +That's it. You are now on an isolated feature branch, do your work and +make commits to this branch. There should be no difference.. when you're +ready to push your changes you would now push to your feature branch +instead of master: + +.. code-block:: bash + + git push origin + + +Creating feature branch via GitHub UI +..................................... + +Navigate to your forked GitHub repo and identify Branch dropdown: + +.. image:: images/feature_branching_img2.png + :scale: 50 + +Initially you should only see master branch listed. Within that dropdown +type the name of the new feature branch, for this example we will call +it ``lesson1/assignment1`` + +.. image:: images/feature_branching_img3.png + :scale: 50 + +Once you start typing the name of the branch, and if it does not +currently exist, you will get a prompt to create new branch. Click into +highlighted area (colors may differ based on browser or theme) and now +you have a new branch! + +Go into command line and check out this new branch: + +.. code-block:: bash + + $ git pull + + $ git checkout lesson1/assignment1 + + Switched to branch 'lesson1/assignment1' + + $ git branch + + feature_branch + + * lesson1/assignment1 <------- asterisk here indicates your current branch + + master + + $ + +2. When Feature Work is Complete +-------------------------------- + +When you're done working on your feature branch it is time to create a +Pull Request to get your changes into master branch (both main class +repo and fork) + +Create PR into main repo +........................ + +In GitHub UI navigate to Pull Requests and select New pull request +button, you should now see options for source/target and branches + +.. image:: images/feature_branching_img4.png + :scale: 50 + +In the far right dropdown you will want to select your feature branch. +Create the PR. + +Merge your feature branch into *your forked* master branch +.......................................................... + +Command Line +^^^^^^^^^^^^ + +note that you can always verify you are in your forked version by +running the command below which should show URL of origin + +.. code-block:: bash + + git remote show origin + +Now check out master and make sure it is up to date + +.. code-block:: bash + + git checkout master + + git pull + +Next merge your feature branch (in our example ``lesson1/assignment1`` +into master + +.. code-block:: bash + + git merge --no-ff lesson1/assignment1 + +Next you will be prompted to commit with pre-populated commit message, +then save and close. + +Finish up with + +.. code-block:: bash + + git push origin master + +GitHub UI +^^^^^^^^^ + +If you're not comfortable with command line, it is very easy to do in +the GitHub UI! + +Navigate to Pull Requests again, and press New Pull request button, you +will now select your fork on the far left with master branch, and your +feature branch on the far right. + +.. image:: images/feature_branching_img5.png + :scale: 50 + +Create your pull request, then merge it and delete feature branch. + +Extensive Explanation of Feature Branching +========================================== + +It has been said that git is not a Revision Control System, but rather, a tool you can use to make a Revision Control System. What this means is that git provides a huge number of features for managing your source code, but you still need to decide how to use it for your particular project. This is often referred to as the git "workflow". The "feature-branch" workflow is one such approach to managing a project. + +Here are a couple nice references that explain the feature branching workflow: + +https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow + +https://blog.landscape.io/use-feature-branches-for-everything.html diff --git a/_sources/modules/dev_environment/git_editor_windows.rst.txt b/_sources/modules/dev_environment/git_editor_windows.rst.txt new file mode 100644 index 0000000..80a0838 --- /dev/null +++ b/_sources/modules/dev_environment/git_editor_windows.rst.txt @@ -0,0 +1,48 @@ +.. _install_nano_win: + +Making your text Editor work with git on Windows +================================================ + +By default, git will dump you into the "vim" editor when you make a commit. + +"vim" is a venerable old Unix tool that no one but Unix geeks can make any sense of. + +So you are likely to want to use something else. + +git can be configured to use any editor you like. + +Here are some options: + +Notepad: +-------- + +https://github.com/github/GitPad/ + +Sublime Text: +------------- + +http://stackoverflow.com/questions/32282847/opening-sublime-text-from-windows-git-bash + +Notepad++ +--------- + +https://danlimerick.wordpress.com/2011/06/12/git-for-windows-tip-setting-an-editor/ + +Nano +---- + +This was a nice way to go -- but unfortunately, there no longer seems to be a Windows binary available for nano. + +For all Windows installations, download the WinNT/9x binary from here: + +http://www.nano-editor.org/download.php + +Unzip the file and move the files into the git bin directory: C:\Program Files\Git\bin + +That's it! You should now be able to use nano from git bash: + +.. code-block:: bash + + $ nano test.txt + +Command shortcuts are helpfully written in the editor! \ No newline at end of file diff --git a/_sources/modules/dev_environment/git_hints.rst.txt b/_sources/modules/dev_environment/git_hints.rst.txt new file mode 100644 index 0000000..7221e9c --- /dev/null +++ b/_sources/modules/dev_environment/git_hints.rst.txt @@ -0,0 +1,293 @@ +.. _git_hints: + +######### +git Hints +######### + +git is a very complex system, and can be used in many ways. Because of this, it can be hard to find answers to seemingly simple questions, even though the Internet is full of discussions of how to use git. + +Every group using git has to establish a standard "work flow". If you google "git workflow" you find a LOT of discussion, and they are not all the same. And depending on the workflow you are using, the problems you'll have and the solutions to them will be different. + +We are using a very simplified workflow for this class, and this page seeks to provide solutions to problems that you might encounter specifically with this workflow. + +"origin" and "upstream" +======================= + +git is a "distributed version control system". That means that git repositories are self contained, and can be "connected" with multiple other, remote repositories -- that is, repos on other machines elsewhere on the internet. + +This facilitates collaboration with widely dispersed groups, but as it allows essentially arbitrary complexity, some conventions have emerged. + +a git repo can be "connected" with virtually any number of remote repositories you can see what yours is connected to with: + +.. code-block:: bash + + git remote -v + +After cloning a repository (from gitHub, for instance) on your machine, is will look something like this: + +.. code-block:: bash + + $ git remote -v + origin https://github.com/PythonCHB/Sp2018-Accelerated.git (fetch) + origin https://github.com/PythonCHB/Sp2018-Accelerated.git (push) + +so I have one remote repository, on gitHub. It is listed twice, as I am both fetching from (pulling) and pushing to the same repository. "origin" is created when you do a clone, and it is the one that is pushed to and pulled from by default. git is so flexible that you could set it up to push and pull be default to two different repos, but I've never seen that done. + +In case of the PythonCertclass, we need *another* remote repository: + +There is the central class repository, which only the instructors have permissions to change. But you want to be able to get updated materials from that repository as well. Since this repo is the one your personal repo was "forked" from, the convention is to call it the "upstream" repository. You should have set that up with a command like this: + +.. code-block:: bash + + $ git remote add upstream https://github.com/UWPCE-PythonCert-ClassRepos/Sp2018-Accelerated.git + +And it should look something like this when you check your remotes: + +.. code-block:: bash + + $ git remote -v + origin https://github.com/PythonCHB/Sp2018-Accelerated.git (fetch) + origin https://github.com/PythonCHB/Sp2018-Accelerated.git (push) + upstream https://github.com/UWPCE-PythonCert-ClassRepos/Sp2018-Accelerated.git (fetch) + upstream https://github.com/UWPCE-PythonCert-ClassRepos/Sp2018-Accelerated.git (push) + +So I now have two more remotes, one for pushing and one for pulling (fetching). + +Do this now on your own machine, and make sure that "origin" points to your repo on gitHub, and "upstream" is pointed to the one in the "UWPCE-PythonCert-ClassRepos" gitHub organization. + +Changing a remote +----------------- + +If your remotes are not set up right, you can reset them, but removing one: + +.. code-block:: bash + + git remote remove upstream + +and then adding it back correctly: + + $ git remote add upstream https://github.com/UWPCE-PythonCert-ClassRepos/Sp2018-Accelerated.git + +**make sure to adjust that command for your particular class!** + +Working with "upstream" +----------------------- + +If you were to try to push to the upstream one, it would fail, as you do not have permissions to do so. But when you do: + +.. code-block:: bash + + $ git pull upstream master + +you are telling git to pull all the latest changes from the "upstream" repository into your local one. Note that all those changes will only get into your repo on github (origin) when you push: + +.. code-block:: bash + + $ git push + +Note that "origin" is the default remote, and "master" is the default branch, so that command is the same as: + +.. code-block:: bash + + $ git pull origin master + +And when you pull from your gitHub repo (``git pull``) that is shorthand for: + + $ git pull origin master + +Note that you may not have a reason to pull from your origin repo. But if you were to work on two different machines -- say a personal laptop at home, and a work machine at the office, you could push stuff to your gitHub repo from both, and use ``git pull`` to keep your changes in sync. + +In fact, I highly recommend using git and gitHub as a way to coordinate your personal work if you have multiple machines (or multiple OSs, or...). You also get a backup essentially for free that way. + + +Backing out a change +==================== + +If you change a file in your repo, and you decide that you simply want to put it back the way it was the last time you committed it -- that's easy:: + + git checkout the_name_of_the_file + +Backing out a change that has been committed. +--------------------------------------------- + +Here's the situation: + +I accidentally changed a file in the examples dir in my fork of the repo. + +Then I committed it, and pushed that commit to gitHub and did a PR. + +So how do I back this out? + +What you want to do is "checkout" the file from a previous commit. + +So the first step is to find a commit that has the correct version of the file. + +In this example, the file in question is: + +``examples/Session05/maillroom_test.py`` + +I can use ``git log`` to figure out when the file was last touched:: + + $ git log examples/Session05/maillroom_test.py + +That means: "show me the log of that particular file". ``git log`` by itself will show you the history of the entire repo -- less useful in this case. + +In this case, I got:: + + $ git log examples/Session05/maillroom_test.py + commit 87d27a12bcae5c1bdc565e05e954e7c94bfa27e0 (HEAD -> master, origin/master, origin/HEAD) + Author: Chris Barker + Date: Sat Dec 9 16:18:22 2017 -0800 + + adding a bit just to test... + + commit 8e5908a37d7df90263057644fef7138e77838107 + Author: Chris Barker + Date: Sun Nov 5 11:12:06 2017 -0800 + + some updates + + commit 4795ddf41f20cfc4346f02319ab61699e8a469f2 + Author: Chris Barker + Date: Tue Oct 31 18:59:31 2017 -0700 + + added mailroom review + +The entry at the top, from Dec 9th, is the one I want to get rid of, so I want to checkout the version of the file back to the one before that top entry. + +Each "commit" is essentially a snapshot of the entire repo when "git commit" was run. Each one is identified by a unique "hash" -- that long string of characters. + +To restore a file back to the state in a previous commit, we do:: + + git checkout 8e5908a37d7d examples/Session05/maillroom_test.py + +And that puts it back to the state it was in at that previous commit, identified by that "hash". + +Note that the full hash for each commit is really long, but git will if you use enough characters to uniquely identify it -- ten or so is usually plenty. + + +git blame +========= + +``git blame`` is a handy utility for examining the history of a particular part of a particular file. For example: + +``git blame -L 2,6 examples/Session05/maillroom_test.py`` + +That means: "show me the changes to lines 2--6 of this file". + +It's called *"blame"* because you can use it to figure out who to blame for a change in a file. + +Here's what I got with that example:: + + 4795ddf4 (Chris Barker 2017-10-31 18:59:31 -0700 2) from os import system + 4795ddf4 (Chris Barker 2017-10-31 18:59:31 -0700 3) + 87d27a12 (Chris Barker 2017-12-09 16:18:22 -0800 4) # some extra in here just to test git + 87d27a12 (Chris Barker 2017-12-09 16:18:22 -0800 5) + 4795ddf4 (Chris Barker 2017-10-31 18:59:31 -0700 6) + +So this shows me that it was changed on 12-09, and before that on 10-31. IN this case, I'm the only one that has messed with that file, so no one to shift the blame too :-) + + +.. _git_branching: + +Branching +========= + +A really quick intro to branching. + +You may want to start with this tutorial to familiarize yourself with the idea: + +https://www.atlassian.com/git/tutorials/using-branches + + +quick tutorial +-------------- + +You create a new "branch" with git with the branch command:: + + git branch the_name_of_the_branch + +where ``the_name_of_the_branch`` is the name of the branch, naturally. To see all the branches you have, you can simply do:: + + git branch + +The "current" branch or "HEAD" will be marked with an asterix. + +To switch to another branch, you can checkout the branch: + + git checkout the_name_of_the_branch + +You are now working in the new branch. Anything you commit will be comited to that branch, and no longer effect the master branch. + +IF you do a ``git push`` -- you will get a message from git telling you that the branch you are now on is not set up to push to "origin" (your giotHub repo), but it will show you the command you need to set that up -- set-upstream:: + + git push --set-upstream origin the_name_of_the_branch + +Now it will push to gitHub, and you can see it there. + +You can create Pull Requests from that new branch, as well as the old, master, branch. + +merging +------- + +When you are happy with your work in the new branch, you may want to merge it back into the "master" branch. + +Yu can do this by switching to the master branch:: + + git checkout master + +And then merging your new work into it:: + + git merge the_name_of_the_branch + +And there you go! + +There is a saying in the git world: + + "Branch early, merge often" + +It's a good way to work -- branching and merging is easy enough it git that it pays off to do it often. + +"detached HEAD" +--------------- + +Above, we talked about using ``git checkout`` to restore a file to the state it was in in a previous commit, like so:: + + git checkout 8e5908a37d7d examples/Session05/maillroom_test.py + +But what happens if you do a checkout with a commit, and no specific file? + +It does what you might expect -- puts ALL the files back the way they were at that commit. But there is a hitch ... let's see what happens when I do that:: + + $ git checkout c03bb5b2c401c + Note: checking out 'c03bb5b2c401c'. + + You are in 'detached HEAD' state. You can look around, make experimental + changes and commit them, and you can discard any commits you make in this + state without impacting any branches by performing another checkout. + + If you want to create a new branch to retain commits you create, you may + do so (now or later) by using -b with the checkout command again. Example: + + git checkout -b + + HEAD is now at c03bb5b adding print_grid from class + +So the files are set to the old state -- but now there is that note about "detached HEAD" -- this means that changes you make, even commits, will not effect the git repo. IF you want to start from here and make changes that will stick, you need to do what it says, and make a new branch. But what it DOESN'T tell you is how to simpel "re-attach" the HEAD. Turns out there is an easy way:: + + $ git checkout - + Previous HEAD position was c03bb5b adding print_grid from class + Switched to branch 'master' + Your branch is up to date with 'origin/master'. + +the dash means "the branch or commit you were on before your last checkout command". + +For more info about "detached HEAD", see: + +https://howtogit.net/recipes/getting-out-of-detached-head-state.html + + + + + + diff --git a/_sources/modules/dev_environment/git_overview.rst.txt b/_sources/modules/dev_environment/git_overview.rst.txt new file mode 100644 index 0000000..f3d811f --- /dev/null +++ b/_sources/modules/dev_environment/git_overview.rst.txt @@ -0,0 +1,198 @@ +.. _git_overview: + +============ +git Overview +============ + +git is a very complex and powerful system. However, it can be very useful even if you only use a small portion of its functionality. This page should be bring you up to speed enough to make good use of git for the Python Certificate class. + +Note that in the certificate program we will be using git in conjunction with gitHub, a cloud-based service that provides a collaboration environment for software development based on the git version control system. + +gitHub provides a web service and web interface that hosts projects and supports collaboration among teams and the open source community. It adds other features, but is very much built on the source control system, git. So a basic understanding of git is required to make proper use of gitHub. + + +Learning Resources +================== + +* Introduction to Git and GitHub for Python Developers + + https://realpython.com/python-git-github-intro/ + +* "Official" gitHub tutorials: + +https://guides.github.com/activities/hello-world/ + +https://guides.github.com/introduction/flow/ + +* Other suggested readings: + +http://rogerdudler.github.io/git-guide/ + +https://try.github.io/levels/1/challenges/1 + +* Pro git: The complete semi-official documentation -- the first few chapters are worth going through: + +https://git-scm.com/book/en + +* git Branching: Interactive tutorial about branching -- try it right in the browser! + +http://pcottle.github.io/learnGitBranching/ + + +A Graphical Tutorial +==================== + +A Picture of git +---------------- + +.. figure:: /_static/git_simple_timeline.png + :width: 80% + :class: center + +A git repository is a set of points in time, with history showing where +you've been. + +Each point has a *name* (here *A*, *B*, *C*) that uniquely identifies it, +called a *hash*. + +Note: To those computer geeks among us -- yes, this an actual hash of ALL the files in the repo at that point in time -- so it uniquely identifies the *exact* state. That is why it's a long ugly set of seemingly random characters. But when using git, all you need to know is that it is a name that identifies that unique state. + +The path from one point to the previous is represented by the *difference* between the two points. + + +.. figure:: /_static/git_head.png + :width: 75% + :class: center + +Each point in time can also have a label that points to it. + +One of these is *HEAD*, which always points to the place in the timeline that you are currently looking at. + + +.. figure:: /_static/git_master_branch.png + :width: 75% + :class: center + +You may also be familiar with the label "master". + +This is the name that git automatically gives to the first *branch* in a repository. + +A *branch* is actually just a label for a certain set of points in time. + + +.. figure:: /_static/git_new_commit.png + :width: 75% + :class: center + +When you make a *commit* in git, you add a new point to the timeline. + +The HEAD label moves to this new point. + +So does the label for the *branch* you are on. + +A lot of terms in git are "overloaded" - used in multiple ways. For instance, the verb "commit" is the act of committing the state of your files to git -- saving that state so you can go back to it later. + +The noun "commit" is a particular state of the repository -- it has been saved and has particular name (hash) -- it is one if the points on that timeline. + + +.. figure:: /_static/git_new_branch.png + :width: 75% + :class: center + + +You can make a new *branch* with the ``branch`` command. + +This adds a new label to the current commit. + +Notice that it *does not* check out that branch -- you will still be working in the current branch. + + +.. figure:: /_static/git_checkout_branch.png + :width: 75% + :class: center + + +You can use the ``checkout`` command to switch to the new branch. + +This associates the HEAD label with the *session01* label. + +Use ``git branch`` to see which branch is *active*:: + + $ git branch + master + * session01 + + +.. figure:: /_static/git_commit_on_branch.png + :width: 75% + :class: center + +While it is checked out, new commits move the *session01* label. + +Notice that HEAD is *always* the same as "where you are now" + + +You can use this to switch between branches and make changes in isolation. + + +.. figure:: /_static/git_checkout_master.png + :width: 75% + :class: center + +.. figure:: /_static/git_new_commit_on_master.png + :width: 75% + :class: center + + +Branching allows you to keep related sets of work separate from each-other. + +In our lessons, you can use it to do each of your exercises. + +Simply create a new branch for each session from your repository master +branch. + +Do your work on that branch, and then you can issue a **pull request** in +github to have your work evaluated. + +This is very much like how teams work in the "real world" so learning it +here will help you. + +The final step in the process is merging your work. + + +The ``merge`` command allows you to *combine* your work on one branch with the +work on another. + + +It creates a new commit which reconciles the differences: + +.. figure:: /_static/git_merge_commit.png + :width: 75% + :class: center + +Notice that this commit has **two** parents. + + +Sometimes when you ``merge`` two branches, you get *conflicts*. + +This happens when the same file was changed in about the same place in two different ways. + +Often, git can work these types of things out on its own, but if not, you'll need to manually edit files to fix the problem. + +You'll be helped by the fact that git will tell you which files are in conflict. + +Just open those files and look for conflict markers: + + * <<<<<<<<< *hash1* (stuff from the current branch) + * ========= (the pivot point between two branches' content) + * >>>>>>>>> *hash2* (stuff from the branch being merged) + + +Your job in fixing a conflict is to decide exactly what to keep. + +You can (and should) communicate with others on your team when doing this. + +Always remember to remove the conflict markers too. They are not syntactic code in any language and will cause errors. + +Once a conflict is resolved, you can ``git add`` the file back and then commit the merge. + diff --git a/_sources/modules/dev_environment/index.rst.txt b/_sources/modules/dev_environment/index.rst.txt new file mode 100644 index 0000000..a040d0c --- /dev/null +++ b/_sources/modules/dev_environment/index.rst.txt @@ -0,0 +1,437 @@ +.. _installing_python: + + +================================ +Installing Python and core tools +================================ + +.. toctree:: + :maxdepth: 1 + + python_for_mac + python_for_windows + python_for_linux + vagrant + + git_editor_windows + +.. _setting_up_dev_environment: + +############################################### +Setting Up A Development Environment For Python +############################################### + +The following is the recommended setup for the UWPCE Python Certificate Program. It is not necessary to have exactly this same setup, but if you choose to use a different setup, we will be less able to support you if you need help. + +Minimal Setup +============= + +Although it is OK to use different tools, there are some requirements to successfully do the work the program requires: + +#. cPython version 3.6.* or 3.7.* +#. A way to edit Python files (Programmers Text Editor) +#. A way to run your code -- command line, IDE, etc. +#. A way to use the "git" source code version control system + +You can be successful in the program as long as you have the above. If you don't already have a setup that fulfills those requirements -- read on. + +Platforms +--------- + +Python is a very platform independent system, it can run on all major operating systems, including micro controllers even. It is most commonly used in production on Windows, Linux or OS-X systems. + +For this program, we feel it is best for students to work in an environment in which they are comfortable, and which they will ultimately use to do production work. + +We have included instructions for Windows, Linux and OS-X systems -- any of these are fine. + +Python Itself +------------- + +Python is a "byte compiled, interpreted" language. What this means to you is that you need a Python interpreter to run your Python code. It also means that that is all you need -- write some code, and run it with Python. That's it. + +There are a number of different Python interpreters (or run-time environments) available: + +- cPython +- PyPy +- Jython +- Iron Python +- MicroPython + +These each have their own special uses. For example, for interaction with the Java VM or Microsoft CLR, or running on micro controllers. But most production Python is run with the cPython interpreter, and most people mean cPython when they say "Python". + +For this program, you will need cPython version 3.6, installed and running so that when you type "python" at your command line, it starts up. + +cPython itself is available from a number of sources, or "distributions". We recommend the version available from python.org. + +Python is also available as part of the Anaconda data analysis environment, as well as a few other sources. These Python distributions will work fine for this class, but when we get to advanced topics like virtual environments, there are some differences -- and you will be responsible for adapting to these differences. + +Your Development Environment +============================ + +There are three basic elements to your environment when working with Python: + +* The Command Line +* The Interpreter +* The Editor +* The source code version control system + +Some folks use an Integrated Development Environment (IDE), which combines some or all of these functions. For this class, we don't recommend using an IDE, because while it can make some things easier, it can also hide things that will be good for you to understand. + +Minimal Requirements +-------------------- + +In order to be productive in this program, you need to be able to do the following: + +* Manipulate files and write and save Python code in files. + You really, really want a "real" programmer's editor for this. + +* Run your code with Python 3.6 or 3.7 + +* Run the iPython interactive interpreter + +* Install new packages with pip + +* Use the git source code management system (with GitHub) + +If you are not set up and comfortable with doing all that, read and follow these instructions: + +:ref:`installing_python` + +Then come back and follow the rest of this review. + +The Command Line (cli) +====================== + +Having some familiarity with the command line is important + +Familiarity with basic use of the command line is a prerequisite for the program, so we won't cover this much in class. If you are not comfortable with the command line, please bone up on your own. + +We have some resources here: :ref:`command_line_basics` + +We suggest running through the **cli** tutorial at "learn code the hard way": + +`Command Line Crash Course `_ + +Or, for Linux and OS-X users: + +`Linux command line for you and me! `_ + + +Windows: +-------- + +Most of the demos in lessons will be done using the "bash" command line shell on OS-X. This is identical to the bash shell on Linux. + +Windows provides the "DOS" command line, which is OK, but pretty old and limited, or "Power Shell" -- a more modern, powerful, flexible command shell. + +If you are comfortable with either of these -- go for it. + +If not, you can use the "git Bash" shell -- which is much like the bash shell +on OS-X and Linux: :ref:`git_bash` + +Or, on Windows 10, look into the "bash shell for Windows" otherwise known as the "Linux subsystem for Windows" - - more info here: :ref:`windows_bash` + +OS-X +---- + +OS-X comes out of the box with a bash command line. You can access it by running the "Terminal" application, which you can find under: + +Applications => Utilities => Terminal.app + +Drag and Drop it into the dock for easy access. + +The Terminal app can be interfaced with the Finder, making it easy to open it up with the working dir set to the current folder in the finder: + +See: `launch an OS-X terminal in a folder `_ + +for how to set that up. + +Linux +----- + +On Linux, the terminal is usually very accessible -- each Desktop System has a different way to access it -- figure out how on your machine. + +The Python Interpreter +====================== + +Python comes with a built-in interpreter. + +You see it when you type ``python`` at the command line: + +.. code-block:: bash + + $ python + Python 3.6.1 (v3.5.2:4def2a2901a5, Jun 26 2016, 10:47:25) + [GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin + Type "help", "copyright", "credits" or "license" for more information. + >>> + +That last thing you see, ``>>>`` is the "Python prompt". + +This is where you type code. + + +Python in the Interpreter +------------------------- + +Try it out: + +.. code-block:: python + + >>> print("hello world!") + hello world! + >>> 4 + 5 + 9 + >>> 2 ** 8 - 1 + 255 + >>> print ("one string" + " plus another") + one string plus another + >>> + +To get out of the interpreter, you can type:: + + exit() + +Or hit `ctrl+D` on Linux and OS-X or `ctrl+Z` On Windows. + + +Tools in the Interpreter +------------------------ + +When you are in the interpreter, there are a number of tools available to +you. + +There is a help system: + +.. code-block:: python + + >>> help(str) + Help on class str in module __builtin__: + + class str(basestring) + | str(object='') -> string + | + | Return a nice string representation of the object. + | If the argument is a string, the return value is the same object. + ... + +You can type ``q`` to exit the help viewer. + + +You can also use the ``dir`` builtin to find out about the attributes of a +given object: + +.. code-block:: python + + >>> bob = "this is a string" + >>> dir(bob) + ['__add__', '__class__', '__contains__', '__delattr__', + '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', + '__getitem__', '__getnewargs__', '__getslice__', '__gt__', + ... + 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', + 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', + 'zfill'] + >>> help(bob.rpartition) + +This allows you quite a bit of latitude in exploring what Python is. + +Advanced Interpreters +--------------------- + +In addition to the built-in interpreter, there are several more advanced +interpreters available to you. + +We'll be using one in this course called ``iPython`` + +Some information about iPython can be found here: :ref:`ipython_resources` + +The Editor +========== + +Typing code in an interpreter is great for exploring. + +But for anything "real", you'll want to save the work you are doing in a more permanent fashion. + +This is where an Editor fits in. + +.. _editor_for_python: + +Text Editors Only +----------------- + +Any good programmers text editor will do. + +MS Word is **not** a text editor. + +Nor is *TextEdit* on a Mac. + +``Notepad`` on Windows is a text editor -- but a poor one. + +You need a real "programmers text editor" + +A text editor saves only what it shows you, with no special formatting +characters hidden behind the scenes. + +Minimum Requirements +-------------------- + +At a minimum, your editor should have: + +* Syntax Colorization +* Automatic Indentation + +In addition, great features to add include: + +* Tab completion +* Code linting +* Jump-to-definition + +Have an editor that does all this? Feel free to use it. + +If not, we recommend ``SublimeText``: + +http://www.sublimetext.com/ + +:ref:`sublime_as_ide` + +"Atom" is another good open source option. + +https://atom.io/ + +:ref:`atom_as_ide` + +"Visual Studio Code" is a relatively new cross platfrom offering from Microsoft -- a lot of folks seem to like it: + +:ref:`vsc_as_ide` + +And, of course, vim or Emacs on Linux, if you are familiar with those. + +Why No IDE? +----------- + +An IDE does not give you much that you can't get with a good editor plus a good interpreter. + +An IDE often weighs a great deal. + +Setting up IDEs to work with different projects can be challenging and time-consuming. + +Particularly when you are first learning, you don't want too much done for you. + + +Why Not an IDE? +--------------- + +That said ... + +You may want to go get the educational edition of PyCharm: + +https://www.jetbrains.com/pycharm-edu/ + +Which is awesome. + +Here are a number of pages to help you get started: + +Version Control System +======================== + +While not strictly necessary to develop code, it is a very, very good idea to manage your code in a Version Control System: + +https://en.wikipedia.org/wiki/Version_control + +This is such a critical software development practice the we use it in the program for you to mange your projects and turn in assignments, so that you can gain familiarity with the practice. + +git +--- + +git (https://en.wikipedia.org/wiki/Git) is an open-source version control system that has become an industry standard -- very widely used in both commercial and open-source development. + +We will be using git and the web service GitHub for collaboration in this program. + +Make sure you are set up to use git on your machine. If you have using a command line client, you should be able to type:: + + git --version + +and get something like this as a response:: + + git version 2.17.2 (Apple Git-81) + +Am I ready to go? +================= + +To see if you have Python ready to start class, try the following: + +Create and run a Python "program": +---------------------------------- + +Create a file called ``install_test.py``, with the following content: + +.. code-block:: python + + import sys + print("This is my first python program") + + version = sys.version_info + version_string = "{}.{}".format(version.major, version.minor) + if version.major == 3: + if version.minor not in (6, 7): + print("You should be running version 3.6 or 3.7") + else: + print("You are running python{} -- all good!".format(version_string)) + else: + print("You need to run Python 3!") + print("This is version: {}".format(version_string)) + +Run it with your version of python. It should result in:: + + This is my first python program + You are running python3.7 -- all good! + +If you get something else -- figure out why and fix it! + +Run git +------- + +You should be able to run git on the command line: + +.. code-block:: bash + + $ git --version + git version 2.20.1 (Apple Git-117) + +It should be version >= 2 + +iPython +------- + +``iPython`` is not critical, but it is very nice. You should be able to run it with:: + + $ ipython + Python 3.6.2 (v3.6.2:5fd33b5926, Jul 16 2017, 20:11:06) + Type 'copyright', 'credits' or 'license' for more information + IPython 6.1.0 -- An enhanced Interactive Python. Type '?' for help. + +And get something like that. + +``ipython`` can be quit by typing ``quit`` + + +Specific Documentation +====================== + +.. toctree:: + :maxdepth: 2 + + sublime_as_ide + atom_as_ide + vsc_as_ide + command_line + shell + ipython + windows_bash + + git_overview + feature_branching + git_hints + + virtualenv diff --git a/_sources/modules/dev_environment/ipython.rst.txt b/_sources/modules/dev_environment/ipython.rst.txt new file mode 100644 index 0000000..8c5fb93 --- /dev/null +++ b/_sources/modules/dev_environment/ipython.rst.txt @@ -0,0 +1,18 @@ +.. _ipython_resources: + +******************* +iPython Interpreter +******************* + +iPython is an enhanced interpreter that makes interactive experimentation at the command line much more pleasant and powerful. + +* **The iPython tutorial** + (http://ipython.readthedocs.io/en/stable/interactive/tutorial.html) + +* **Using IPython for interactive work** + (http://ipython.readthedocs.io/en/stable/interactive/index.html) + Learn about the abilities iPython provides for interactive sessions. + +* **The iPython Documentation** + (http://ipython.readthedocs.io/en/stable/) + Use this to learn more about iPython's amazing capabilities. diff --git a/_sources/modules/dev_environment/python_for_linux.rst.txt b/_sources/modules/dev_environment/python_for_linux.rst.txt new file mode 100644 index 0000000..a8ce4ef --- /dev/null +++ b/_sources/modules/dev_environment/python_for_linux.rst.txt @@ -0,0 +1,296 @@ +.. _python_for_linux: + +########################### +Setting Up Linux for Python +########################### + + +Debian and Related Distros (Ubuntu, Linux Mint) +=============================================== + +Python +------- + +For this program, you need Python3.6.* or 3.7.* + +Debian distros already have the stable python2 and python3 releases preinstalled (`Debian Wiki `_). + +Try the following command: + +.. code-block:: bash + + $ python3 + Python 3.6.3 (default, March 26 2017, 15:33:32) + [GCC 4.9.2 on linux] + >>> + +I'm pretty sure that 18.4 (the most recent long term support release) has 3.6, if so, you are set. + +That's nice, which one is the default version? Just type ``python`` to see. It's probably python2 still: + +.. code-block:: bash + + $ python + Python 2.7.9 (default, April 2 2015, 15:33:32) + [GCC 4.9.2 on linux2] + >>> + +If you want to make ``python3.6`` the default version then add the line ``alias python=python3`` to your user's ``/home/{user}/.bashrc`` file like so: + +.. code-block:: bash + + $ # before the change + $ python + Python 2.7.9 (default, April 2 2015, 15:33:32) + [GCC 4.9.2 on linux2] + >>> + + $ echo "alias python=python3" >> ~/.bashrc + $ echo "alias pip=pip3" >> ~/.bashrc + $ echo "alias ipython=ipython3" >> ~/.bashrc + $ source ~/.bashrc + + $ # after the change + $ python + Python 3.6.3 (default, March 26 2017, 15:33:32) + [GCC 4.9.2 on linux] + >>> + +Alternatively, you can always remember to type ``python3`` whenever you want Python. + +Note: your version number may vary, but it needs to be ``3.6.*`` or ``3.7.*`` + +You may not have pip and ipython installed yet, but you will as you follow the instructions below. + +If you don't have the version you want installed then use the package manager to find and install it: + +.. code-block:: bash + + $ # search the package manager for it + $ sudo apt-cache search python | grep '^python3.7\ -' + python3.7 - Interactive high-level object-oriented language (version 3.7) + $ # install it + $ sudo apt-get install python3.7 + +(If you cant find 3.7, try 3.6 instead) + +Terminal +--------- + +Every Linux box has a terminal emulator -- find and use it. + + + +pip +--- + +``pip`` is the Python package installer. + +Many Python packages are also available directly from your distro -- but you'll get the latest and greatest if you use ``pip`` to install it instead. + +To get pip, the first option is to use your system package manager, something like: + +.. code-block:: bash + + $ sudo apt-get install python3-pip + +If that doesn't work, then try "ensure-pip": + +.. code-block:: bash + + $ python3 -m ensurepip --upgrade + + + $ python3 -m ensurepip --upgrade + +You can now use pip to install other packages. The first thing you may want to do is update pip itself: + +.. code-block:: bash + + $ python3 -m pip install --upgrade pip + +Using pip: +---------- + +To use pip to install a package, you invoke it with this command:: + + python3 -m pip install the_name_of_the_package + +Where ``python3`` is the command you use to invoke the Python you want to use (could be ``python3``) + +**NOTE:** You will frequently see advice to use pip like so:: + + $ pip install something_or_other + +Which often works, but also can invoke the *wrong* version of pip. The above command:: + + $ python3 -m pip install something_or_other + +calls Python, and tells it to run the ``pip`` module. It is exactly the same as calling pip directly, except that you are assured that you are getting the version of pip connected the version of python that you are running (in this case python3). + + +iPython +-------- + +One extra package we are going to use in class is ``iPython``:: + + $ sudo python3 -m pip install ipython + +You should now be able to run ``iPython``:: + + $ ipython3 + Python 3.6.4 () + Type "copyright", "credits" or "license" for more information. + + IPython 2.0.0 -- An enhanced Interactive Python. + ? -> Introduction and overview of IPython's features. + %quickref -> Quick reference. + help -> Python's own help system. + object? -> Details about 'object', use 'object??' for extra details. + +git +---- + +Git is likely to be there on your system already, but if not: + +.. code-block:: bash + + $ sudo apt-get install git + +================================================== +Fedora and Red Hat Related Distros (CentOS) +================================================== + +.. warning:: + + CentOS is probably the most popular distro of these related flavors. However, getting Python3 on it can be a pain. You have been warned! (but there are lots of tutorials on the web -- google "install python3 on CentOS") + + +Python +------- + +Fedora distros already have the stable python2 and python3 releases preinstalled `[2] `_. However, CentOS, the most popular distro only has the stable python2 release. Try the following commands: + +.. code-block:: bash + + [centos@ip-172-31-21-5 ~]$ python2 + Python 2.7.5 (default, Jun 17 2014, 18:11:42) + [GCC 4.8.2 20140120 (Red Hat 4.8.2-16)] on linux2 + Type "help", "copyright", "credits" or "license" for more information. + >>> + + [centos@ip-172-31-21-5 ~]$ python3 + -bash: python3: command not found + + +Let's install python3 using the package manager. Step one install "Software Collections" to help us: + +.. code-block:: bash + + $ sudo yum -y install scl-utils + +Then go to the `software collections listing `_ and click on the Python collection version you want to install. + +Probably this one: + +https://www.softwarecollections.org/en/scls/rhscl/rh-python35/ + + +Note, you also need to know which version of CentOS you are using (probably 6 or 7). For example, we care about `python version 3.5` so let's go the `.rpm` i want to install `here `_: + +.. code-block:: bash + + $ # add this package to the rpm package manager + $ sudo rpm -Uvh https://www.softwarecollections.org/repos/rhscl/rh-python34/epel-7-x86_64/noarch/rhscl-rh-python35-epel-7-x86_64.noarch.rpm + + $ # install the right python version + $ sudo yum install rh-python35 + +When you want to use python3 run this command: + +.. code-block:: bash + + [centos@ip-172-31-21-5 ~]$ scl enable rh-python35 bash + + +Terminal +--------- + +Every Linux box has a terminal emulator -- find and use it. + + +git +---- + +Git is likely to be there on your system already, but if not: + +.. code-block:: bash + + $ sudo yum install git + +pip +--- + +``pip`` is the Python package installer. + +Many Python packages are also available directly from your distro -- but you'll get the latest and greatest if you use ``pip`` to install it instead. + +In CentOS, if you used the above technique to install Python3, then it comes with pip. Try: + +.. code-block:: bash + + [centos@ip-172-31-21-5 ~]$ python -m pip -V + pip 8.1.2 from /opt/rh/rh-python35/root/usr/lib/python3.5/site-packages (python 3.5) + +Using pip: +---------- + +To use pip to install a package, you invoke it with this command:: + + python -m pip install the_name_of_the_package + +Where ``python`` is the command you use to invoke the Python you want to use (could be `python3`) + +**NOTE:** You will frequently see advice to use pip like so:: + + $ pip install something_or_other + +Which often works, but also can invoke the *wrong* version of pip. The above command:: + + $ python -m pip install something_or_other + +calls Python, and tells it to run the ``pip`` module. It is exactly the same as calling pip directly, except that you are assured that you are getting the version of pip connected the version of python that you are running. + +iPython +-------- + +One we are going to use in class is ``iPython``:: + + $ sudo pip install ipython[all] + +You should now be able to run ``iPython``:: + + $ ipython3 + Python 3.5.2 () + Type "copyright", "credits" or "license" for more information. + + IPython 5.1.0 -- An enhanced Interactive Python. + ? -> Introduction and overview of IPython's features. + %quickref -> Quick reference. + help -> Python's own help system. + object? -> Details about 'object', use 'object??' for extra details. + + +Footnotes: +========== + +Debian Wiki +=========== + +https://wiki.debian.org/Python + +Fedora Wiki +============= + +https://fedoraproject.org/wiki/Packaging:Python + diff --git a/_sources/modules/dev_environment/python_for_mac.rst.txt b/_sources/modules/dev_environment/python_for_mac.rst.txt new file mode 100644 index 0000000..6c6da20 --- /dev/null +++ b/_sources/modules/dev_environment/python_for_mac.rst.txt @@ -0,0 +1,221 @@ +.. _python_for_mac: + +************************** +Setting up OS-X for Python +************************** + +================== +Getting The Tools +================== + + +OS-X comes with Python out of the box, but not the full setup you'll need for development or this class. It also doesn't have the latest version(s), and no version of Python 3. + +So we recommend installing a new version. + + +**Note**: + + +If you use ``macports`` or ``homebrew`` to manage \*nix software on your machine, feel free to use those for ``python``, ``git``, etc, as well. But make sure you have Python 3.7.* + +If not, then read on. + +Terminal +--------- + +You will need a command line terminal. The built-in "terminal" application works fine. Find it in:: + + /Applications/Utilities/Terminal + +Drag it to the dock to easily access. + +Python +------ + +While OS-X does provide Python out of the box, it tends not to have the +latest version, and you really don't want to mess with the system +installation. So we recommend installing an independent installation from +``python.org``: + +Download the latest realease of Python (currently 3.7.0) installer from Python.org: + +https://www.python.org/downloads/ + +Double click the installer and follow the prompts. The defaults are your best bet. Simple as that. + +Oddly, this does NOT install a ``python`` command, but rather a ``python3`` command. If you want to be able to simply type ``python`` and get python3, then you can add a symlink to the install. Type this at a terminal prompt: + +.. code-block:: bash + + $ cd /Library/Frameworks/Python.framework/Versions/3.7/bin + $ ln -s python3.7 python + +Or an add an alias in your shell by adding the following line:: + + alias python='python3' + +to your ``.bash_profile`` file. + +Once you have done that, you should be able to type ``python`` at the command prompt, and get something like: + +.. code-block:: bash + + $ python + Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 26 2018, 23:26:24) + [Clang 6.0 (clang-600.0.57)] on darwin + Type "help", "copyright", "credits" or "license" for more information. + >>> + +This is the Python interpreter. + +Type ``ctrl+D`` to get out (or ``exit()``) + +.. note:: If all this is confusing to you -- take heart -- you will get used it it. And in the meantime, you can simply type ``python3`` when you want to run python. + +pip +--- + +``pip`` is the Python package installer. It is updated more frequently than Python itself, so once you have Python, you want to get the latest version of pip working:: + + $ python3 -m ensurepip --upgrade + +[``python`` may work too, if you set things up correctly above, but ``python3`` should always work.] + +It should download and install the latest ``pip``. Or let you know that you already have it. + +You can now use pip to install other packages. The first thing you may want to do is update pip itself: + +.. code-block:: bash + + $ python3 -m pip install --upgrade pip + +Using pip: +---------- + +To use pip to install a package, you invoke it with this command:: + + python3 -m pip install the_name_of_the_package + +Where ``python3`` is the command you use to invoke the Python you want to use. + +**NOTE:** You will frequently see advice to use pip like so:: + + $ pip install something_or_other + +This often works, but also can invoke the *wrong* version of pip. This command:: + + $ python3 -m pip install something_or_other + +calls Python, and tells it to run the ``pip`` module. It is exactly the same as calling pip directly, except that you are assured that you are getting the version of pip connected the version of Python that you are running. + +iPython +-------- + +One package we are going to use in the program from the beginning is ``iPython``. You can install it with ``pip`` like so:: + + $ python3 -m pip install ipython + +(It will install a LOT...). + +Now you should now be able to run ``iPython``: + +.. code-block:: ipython + + $ ipython + Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 26 2018, 23:26:24) + Type 'copyright', 'credits' or 'license' for more information + IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help. + + In [1]: + +Which you can also get out of with ``ctrl+D`` or ``exit()`` + +git +---- + +git is a source code version control system. It is not strictly related to Python, but it (or a similar system) is a critical tool for software development in general, and it is very widely used in the Python community. We will be using it, along with the gitHub service, in the program to hand in assignments and support code review. + +You need a git client. The gitHub GUI client may be nice; I honestly don't know. However, we will be using the command line client in class. + +There are a couple of options for a command line client. + +This option is a big download and install, but has everything you need out of the box: + +http://sourceforge.net/projects/git-osx-installer/ + +NOTE: if you get a warning like: + +"... can't be opened because it is from an untrusted developer" + +you'll need to go to system preferences: + + "Security and Privacy" + + Depending on the OS-X version, you will need to check the box saying "Open Anyway," or perhaps the box saying you can install untrusted packages. + +This option works great, but you need the XCode command line tools to run it: + +http://git-scm.com/download/mac + +You can get XCode from the Apple App Store. If you try running "git" on the command line after installing, it should send you there. Warning: XCode is a BIG download. Once installed, run it so it can initialize itself. + +After either of these is installed, the ``git`` command should work: + +.. code-block:: bash + + $ git --version + git version 2.11.0 (Apple Git-81) + +Testing it out +-------------- + +To be ready for the program, you need to have, all available from the command line: + - python + - pip + - iPython + - git + +To try it out, you should be able to run all of the following commands, and get something like the results shown: + +(recall that you can get out of the Python or iPython command lines with ``ctrl+D``) + +For Python: +........... + +.. code-block:: bash + + $ python3 + Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 26 2018, 23:26:24) + [Clang 6.0 (clang-600.0.57)] on darwin + Type "help", "copyright", "credits" or "license" for more information. + >>> + + +For iPython: +............ + +.. code-block:: bash + + $ ipython + Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 26 2018, 23:26:24) + Type 'copyright', 'credits' or 'license' for more information + IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help. + +For pip: +........ + +.. code-block:: bash + + $ python3 -m pip --version + pip 18.0 from /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/pip (python 3.7) + +Note that when you ask pip for ``--version`` it tells you which version of python it is "connected" to. + +For git: +........ + +.. code-block:: bash + + $ git --version + git version 2.15.2 (Apple Git-101.1) diff --git a/_sources/modules/dev_environment/python_for_windows.rst.txt b/_sources/modules/dev_environment/python_for_windows.rst.txt new file mode 100644 index 0000000..1dca6fd --- /dev/null +++ b/_sources/modules/dev_environment/python_for_windows.rst.txt @@ -0,0 +1,233 @@ +.. _python_for_windows: + +############################# +Setting up Windows for Python +############################# + +Getting The Tools +================== + +Python +------- + +There are a number of Python distributions available -- many designed for easier support of scientific programming: + +- Anaconda +- Enthought Canopy +- Python(x,y) +- etc.... + +But for basic use, the installer from python.org is the way to go, and that is what we will be using in this program. + +https://www.python.org/downloads/ + +You want the installer for Python 3.7 -- probably 64 bit, though if you have a 32 bit system, you can get that. + +There is essentially no difference for the purposes of this course. + +Double click and install. + +Ensure that the Install launcher for all users (recommended) and the Add Python 3.7 to PATH checkboxes at the bottom are checked. + +**Add Python 3.7 to PATH step is important!** If this is not checked then when you try to run your python code it won't be able to find the executable. + +See: `Installing Python on Windows `_ + +.. _git_bash: + +Terminal +--------- + +If you are confident in your use of the "DOS Box" or "powershell", command lines, feel free to use one of those. However, your life may be easier if you install "Git Bash", as then you can follow unix-style terminal instructions exactly, and do not have to translate. Also, your instructors are more experienced with Bash. + +From now on, if you hear the terms "bash", "shell" or "terminal", or "command line" know that this is the application that is being referred to. We will use those terms interchangeably to mean ANY command line. + +When you install Git Bash, you are installing git (and a git gui) as well, thus killing two birds with one stone! + +https://git-for-windows.github.io/ + +Select the download button on the page and launch downloaded executable, then follow the prompts. On "Choosing default editor used by Git" step it is best to select Notepad++ (which you need to have installed first) unless you are comfortable with non-graphical editors like vim. +You can go through the rest of the prompts using default values. Once you are done, a terminal window should pop up - try out some commands like ``ls`` or ``git help``. + +You can use this git with the DOS box or Powershell as well. + +This is also a good bet for running Python -- If you use the Git Bash shell, you can use the same commands as Linux and OS-X users. Regardless of which shell you choose, you will need to add Python to your environment. It is possible that this was done during the installation of Python. If you type 'which python' into your terminal, and get back the answer '/c/python37/python', then you are good to go, otherwise (which shouldn't happen if you checked the "Add Python 3.7 to PATH" checkbox when you installed Python above), follow the instructions here: + +http://www.computerhope.com/issues/ch000549.htm + +You will want to add: + +``C:\Users\YourUserName\AppData\Local\Programs\Python\Python37`` + +and + +``C:\Users\YourUserName\AppData\Local\Programs\Python\Python37\Scripts`` + +to ``PATH`` + +Here are steps for updating path: + +:: + + cd + touch .bash_profile + +You can edit this file using Notepad, locate this file in File Explorer in This PC > Local Disk > Users > YourUsername + +Add to the file (file should be empty): + +:: + + PATH=$PATH:/C/Users/YourUserName/AppData/Local/Programs/Python/Python37:/C/Users/YourUserName/AppData/Local/Programs/Python/Scripts + +Note: your python executable may be located in a different path, to check the path you need to start windows shell (``cmd``) and type ``where python`` - this command will output where python is currently installed. + +Save the file and start a new gitbash shell. + +Once you have done that, you should be able to type ``python`` at the command prompt, and get something like: + +:: + + Python 3.7.0 (v3.6.2:5fd33b5926, Jul 16 2017, 20:11:06) + [GCC 4.2.1 (Windows build 7584) (dot 3)] on win64 + Type "help", "copyright", "credits" or "license" for more information. + >>> + +This is the Python interpreter. + +Type ``ctrl+Z`` to get out (or ``exit()``) + +Note: if you have trouble running ``python`` command in your gitbash (it hangs), try running this instead: ``winpty python``. To avoid having to type ``winpty python`` all the time, it's strongly recommended that you create an alias like below: + +:: + + $ echo "alias python='winpty python'" >> ~/.bash_profile + +You will need to close the current bash window and restart a new one to get this alias. Then from now on, you can just type ``python`` and it should work on git bash (no more hanging) as well. + +git +--- + +If you installed Git Bash, you will already have git, both usable in the terminal and as a gui, and can safely skip this section. If not, you still need a git client. You can use the above link and install git (it will install the bash shell as well, of course, but you can use your shell of choice instead). + +There is also TortoiseGit: + +https://code.google.com/p/tortoisegit/ + +Which integrates git with the file manager. Feel free to use this if you already have an understanding of how git works, but for the purposes of learning, it may be better to use a command line client (git Bash above). + + +pip +--- + +``pip`` is the Python package installer. It is updated faster than Python itself, so once you have Python you want to get the latest version of pip working:: + + $ python -m ensurepip --upgrade + +It should download and install the latest ``pip``. + +You can now use pip to install other packages. + +The first thing you may want to do is update pip itself: + +.. code-block:: bash + + $ python -m pip install --upgrade pip + +Using pip: +---------- + +To use pip to install a package, you invoke it with this command:: + + python -m pip install the_name_of_the_package + +Where ``python`` is the command you use to invoke the Python you want to use . + +**NOTE:** You will frequently see advice to use pip like so:: + + $ pip install something_or_other + +Which often works, but also can invoke the *wrong* version of pip. The above command:: + + $ python -m pip install something_or_other + +calls Python, and tells it to run the ``pip`` module. It is exactly the same as calling pip directly, except that you are assured that you are getting the version of pip connected the version of Python that you are running. + + +iPython +-------- + +One extra package we are going to use from the beginning in the program is ``iPython``:: + + $ python -m pip install ipython + +(It will install a LOT) + +You should now be able to run ``iPython`` from the git bash shell or "DOS Box" or PowerShell:: + + $ ipython + Python 3.7.0 (v3.6.2:5fd33b5926, Jul 16 2017, 20:11:06) + Type 'copyright', 'credits' or 'license' for more information + IPython 6.1.0 -- An enhanced Interactive Python. Type '?' for help. + (or from the DOS box or PowerShell prompt) + +We will use this in class as our default Python interpreter. + + +Testing it out +-------------- + +To be ready for the program, you need to have: + - python + - pip + - iPython + - git + +All available from the command line. + +To try it out, you should be able to run all of these commands, and get something like the following results: + +(recall that you can get out of the python or iPython command lines with ``ctrl+Z`` (if that doesn't work, try ``ctrl+D`` -- the \*nix version)) + +For Python: + +:: + + hosun@DESKTOP-GJT06Q0 MINGW64 ~ + $ python + Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 27 2018, 04:59:51) [MSC v.1914 64 bit (AMD64)] on win32 + Type "help", "copyright", "credits" or "license" for more information. + >>> ^Z + + +For iPython: + +:: + + hosun@DESKTOP-GJT06Q0 MINGW64 ~ + $ ipython + Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 27 2018, 04:59:51) [MSC v.1914 64 bit (AMD64)] + Type 'copyright', 'credits' or 'license' for more information + IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help. + + In [1]: + Do you really want to exit ([y]/n)? y + + +For pip: + +:: + + hosun@DESKTOP-GJT06Q0 MINGW64 ~ + $ python -m pip --version + pip 18.0 from C:\Python37\lib\site-packages\pip (python 3.7) + + +For git: + +:: + + hosun@DESKTOP-GJT06Q0 MINGW64 ~ + $ git --version + git version 2.18.3.windows.1 + diff --git a/_sources/modules/dev_environment/shell.rst.txt b/_sources/modules/dev_environment/shell.rst.txt new file mode 100644 index 0000000..3dec9ad --- /dev/null +++ b/_sources/modules/dev_environment/shell.rst.txt @@ -0,0 +1,243 @@ +.. _shell_customization: + +******************************************* +Shell Customizations for Python Development +******************************************* + +The command line is your home as a developer. You must be comfortable there. +In order to improve your comfort there are a number of enhancements you can +make to improve your experience, especially with non-standard software like +``git`` and ``virtualenv`` + +What was that name, again? +========================== + +For example, ``bash`` offers tab completion. But that doesn't extend to +interactions with ``git``. Considering how many branches, tags and remotes you +end up interacting with, and how many long-winded commands there are in +``git``, having a similar autocompletion for them would be very nice. + +The folks who create such things have been kind enough to provide a shell +script that sets this up. And it's not hard to install. + +`The script`_ is called ``git-completion`` and it's available in ``bash``, +``tcsh`` and ``zsh`` flavors. + +.. _The script: https://github.com/git/git/tree/master/contrib/completion + +To use it, download the version of the script that corresponds to your +preferred shell from the tag of the git repo that corresponds to the version of +git you are using. I've got git 1.8.4.2 installed on my machine, so +`this is the version for me`_. Put it in your home directory: + +.. code-block:: bash + + $ cd + $ curl https://raw.github.com/git/git/v1.8.4.2/contrib/completion/git-completion.bash -o .git-completion.bash + +Then source it from your shell startup file: + +.. code-block:: bash + + source ~/.git-completion.bash + +There's even a nifty gist that `does this automatically`_ for OS X. + +.. _this is the version for me: https://raw.github.com/git/git/v1.8.4.2/contrib/completion/git-completion.bash +.. _does this automatically: https://gist.github.com/johngibb/972430 + +Once installed, you should be able to visit any repository you have on your +machine and get tab completion of branch names, remotes and all git commands. + +Where am I, what am I doing? +============================ + +As a working developer, you end up with a *lot* of projects. Even with tab +completion its a chore to remember which branch is checked out, how far ahead +or behind the remote you are, and so on. + +Enter `git-prompt`_. Again, you place this code in your home directory, and +then source it from your shell startup file: + +.. code-block:: bash + + source ~/.git-prompt.sh + +Once you do this you can use the ``__git_ps1`` shell command and a number of +shell variables to configure ``PS1`` and change your shell prompt. You can show +the name of the current branch of a repository when you are in one. You can +get information about the status of HEAD, modified files, stashes, untracked +files and more. + +.. _git-prompt: https://github.com/git/git/blob/master/contrib/completion/git-prompt.sh + +There's two ways to do this. The first is to use ``__git_ps1`` as a command +directly in a ``PS1`` expression in your shell startup file: + +.. code-block:: bash + + export PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ ' + +The result looks like this: + +.. image:: /_static/simple_prompt.png + :width: 600px + :alt: Overriding PS1 provides a customized shell prompt + + +That's not bad, but a bit of color would be nice, and perhaps breaking things +onto more than one line so you can parse what you're seeing more easily would +be helpful. + +For that, you'll need to change strategies. The ``__git_ps1`` command can be +used as a single element in the expression for ``PS1``. But it can also be +used itself as the ``PROMPT_COMMAND`` env variable (this command is for +``bash``, there's different one for ``zsh``). If defined, this command will be +used to form ``PS1`` dynamically. + +When you use ``__git_ps1`` in this way, a couple of things happen. First, +instead of taking only one optional argument (a format string), you can provide +two or optionally three arguments: + +* The first will be prepended to the output of the command +* The second will be appended after +* The optional third argumment will be used as a format string for the output + of the command itself. If there is no output, it will not appear at all. + +Combining these three elements can be very expressive. For example, a standard +OS X command prompt can be expressed like so: ``\h:\W \u\\\$``. If you use this +expression as the second argument, leave the first empty and provide a simple format +ending in a newline for the ``__git_ps1`` output, you get some nice results. + +Enter this in your shell startup file: + +.. code-block:: bash + + PROMPT_COMMAND='__git_ps1 "" "\h:\W \u\\\$ " "[%s]\n"' + +That produces a nice two-line prompt that appears when you're in a git repo, and +disappears when you're not: + +.. image:: /_static/two_line_prompt.png + :width: 600px + :alt: A two-line prompt showing current git repository + +You can also play with setting a few environment variables in your shell +startup file to expand this further. For example, colorizing the output and +providing information about the state of a repo: + +.. code-block:: bash + + GIT_PS1_SHOWDIRTYSTATE=1 + GIT_PS1_SHOWCOLORHINTS=1 + GIT_PS1_SHOWSTASHSTATE=1 + GIT_PS1_SHOWUPSTREAM="auto" + PROMPT_COMMAND='__git_ps1 "" "\h:\W \u\\\$ " "[%s]\n"' + +.. image:: /_static/color_git_prompt.png + :width: 600px + :alt: A colorized git prompt + +Not half bad at all. + +But wait, there's more. +======================= + +The problem with this is that it doesn't play well with another incredibly +useful tool, `virtualenv`_. When you activate a virtualenv, it prepends the name +of the environment you are working on to the shell prompt. + +But it uses the standard ``PS1`` shell variable to do this. Since you've now +used the ``PROMPT_COMMAND`` to create your prompt, ``PS1`` is ignored, and +this nice feature of virtualenv is lost. + +.. _virtualenv: http://virtualenv.org + +Luckily, there is a way out. Bash shell scripting offers `parameter expansion`_ +and a trick of that syntax can help. Normally, a shell parameter is +referenced like so: + +.. code-block:: bash + + $ PARAM='foobar' + $ echo $PARAM + foobar + +In complicated situations, you can wrap the name of the paramter in curly +braces to avoid confusion with following characters: + +.. code-block:: bash + + $ echo ${PARAM}andthennotparam + foobarandthennotparam + +What is not as well known is that this curly-brace syntax has a lot of +interesting variations. For example, you can use ``PARAM`` as a test and +actually print something else entirely: + +.. code-block:: bash + + $ echo ${PARAM:+'foo'} + foo + $ echo ${PARAM:+'bar'} + + $ + +The key here is the ``:`` bit immediately after ``PARAM``. If the ``+`` +char is present, then if ``PARAM`` is unset or null, what comes after is not +printed, otherwise it is. + +If you look at the script that `activates a virtualenv in bash`_ you'll notice +that it exports ``VIRTUAL_ENV``. This means that so long as a virtualenv is +active, this environmental variable will be set. And it will be unset when no +environment is active. + +.. _parameter expansion: http://www.gnu.org/software/bash/manual/bash.html#Shell-Parameter-Expansion +.. _activates a virtualenv in bash: https://github.com/pypa/virtualenv/blob/develop/virtualenv_embedded/activate.sh + +You can use that! + +Armed with this knowledge, you can construct a shell expression that will either +print the name of the active virtualenv in square brackets, or print nothing if +no virtualenv was active: + +.. code-block:: bash + + $ echo ${VIRTUAL_ENV:+[`basename $VIRTUAL_ENV`]} + + $ source /path/to/someenv/bin/activate + $ echo ${VIRTUAL_ENV:+[`basename $VIRTUAL_ENV`]} + someenv + + +Roll that into your shell startup file. You'll have everything you want. You +can even throw in a little more color for good measure: + +.. code-block:: bash + + source ~/.git-prompt.sh + # PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ ' + GIT_PS1_SHOWDIRTYSTATE=1 + GIT_PS1_SHOWCOLORHINTS=1 + GIT_PS1_SHOWSTASHSTATE=1 + GIT_PS1_SHOWUPSTREAM="auto" + Color_Off="\[\033[0m\]" + Yellow="\[\033[0;33m\]" + PROMPT_COMMAND='__git_ps1 "${VIRTUAL_ENV:+[$Yellow`basename $VIRTUAL_ENV`$Color_Off]\n}" "\h:\W \u\\\$ " "[%s]\n"' + +And voilà! You've got a shell prompt that informs about all the things you'll +need to know when working on a daily basis: + +.. image:: /_static/virtualenv_prompt.png + :width: 600px + :alt: A shell session showing the prompt with both virtualenv and git information + +Wrap-Up +======= + +There is still a great deal more that you could do with your shell, but this +will suffice for now. If you are interested in reading further, there is +`a lot to learn`_. + +.. _a lot to learn: http://www.gnu.org/software/bash/manual/bash.html + diff --git a/_sources/modules/dev_environment/sublime_as_ide.rst.txt b/_sources/modules/dev_environment/sublime_as_ide.rst.txt new file mode 100644 index 0000000..1f96594 --- /dev/null +++ b/_sources/modules/dev_environment/sublime_as_ide.rst.txt @@ -0,0 +1,182 @@ +.. _sublime_as_ide: + +************************************************** +Turning Sublime Text Into a Lightweight Python IDE +************************************************** + +A solid text editor is a developer's best friend. You use it constantly and it +becomes like a second pair of hands. The keyboard commands you use daily +become so ingrained in your muscle memory that you stop thinking about them +entirely. + +With Sublime Text, it's possible to turn your text editor into the functional +equivalent of a Python IDE. The best part is you don't have to install an IDE +to do it. + +http://www.sublimetext.com/ + +Requirements +============ + +Here are *my* requirements for an 'IDE': + +* It should provide excellent, configurable syntax colorization. +* It should allow for robust tab completion. +* It should perform automatic code linting to help avoid silly mistakes and conform to good style. + +And some more advanced features that you may want later: + +* It should offer the ability to jump to the definition of symbols in other + files. +* It should be able to interact with a Python interpreter such that when + debugging, the editor will follow along with the debugger. + + +Which Version? +============== + +Use version 3 -- it is updated now and again, so make sure to get the latest. + +*Use Sublime Version 3* + + +Basic Settings +============== + +All configuration in Sublime Text is done via `JSON `_. It's simple to learn. Go and read that link then return here. + +.. note:: JSON is very similar to Python dict and list literals. Though it has its root in Javascript, it is also used in a wide variety of applications, and is well supported by Python. But a key difference is that it does not allow trailing commas after items in lists -- so be careful. + +There are a number of `different levels of configuration `_ in Sublime Text. You will most often work on settings at the user level. + +Open ``Preferences`` -> ``Settings`` + +On a Mac, you can find it under: + +``Sublime Text`` -> ``Preferences`` -> ``Settings`` + +Sublime will open up a split window -- "default" and "user" -- you can look through the default ones to see all the setting you might want to override, and then copy things from default by putting them in the user file. + +The preferences file is simply a JSON text file you edit like any other text file. + +Here's a reasonable set of preliminary settings (theme, color scheme and font are quite personal, find ones that suit you.): + +.. code-block:: javascript + + { + "auto_indent": true, + "color_scheme": "Packages/User/Monokai (Flake8Lint).tmTheme", + "draw_white_space": "all", + "font_face": "inconsolata", + "font_size": 23, + "indent_to_bracket": true, + "rulers": + [ + 79, + 95 + ], + "theme": "Adaptive.sublime-theme", + "smart_indent": true, + "tab_size": 4, + "translate_tabs_to_spaces": true, + "use_tab_stops": true, + "trailing_spaces_modified_lines_only": true, + "trim_automatic_white_space": true, + "trim_trailing_white_space_on_save": true, + "word_wrap": false, + "wrap_width": 95 + } + +**NOTE:** Especially important is the setting ``translate_tabs_to_spaces``, which ensures that any time you hit a tab key, the single character is replaced by four characters. In Python this is **vital**! + +If you do nothing else, add ``translate_tabs_to_spaces`` to your config! + +Extending the Editor +==================== + +Most of the requirements above go beyond basic editor functionality. So we'll use Plugins. + +Sublime Text comes with a great system for `Package Control `_. +It handles installing and uninstalling plugins, and even updates installed plugins for you. +You can also manually install plugins that haven't made it to the big-time yet, including `ones you write yourself `_. +Happily, the plugin system is based on Python! + + +To install a plugin using Package Control, open the ``command palette`` with ``shift-super-P`` (``ctrl-shift-P`` on Windows/Linux). +The ``super`` key is ``command`` or ``⌘`` on OS X. +When the palette opens, typing ``install`` will bring up the ``Package Control: Install Package`` command. +Hit ``enter`` to select it. + +.. image:: /_static/pc_menu.png + :width: 600px + :align: center + :alt: The package control command in the command palette. + +After you select the command, Sublime Text fetches an updated list of packages from the network. It might take a second or two for the list to appear. When it does, start to type the name of the package you want. Sublime Text filters the list and shows you what you want to see. To install a plugin, select it with the mouse, or use arrow keys to navigate the list and hit ``enter`` when your plugin is highlighted. + +.. image:: /_static/plugin_list.png + :width: 600px + :align: center + +Useful Plugins +============== + +Here are the plugins I've installed to achieve the requirements above. + +Anaconda +-------- + +There are a bunch of Python-related plugins available. However, Anaconda is a nice package that provides most of the features you want, so plan on using just that one. + +Not to be confused with the scientific Python distribution -- the Anaconda sublime plugin is a full featured package to turn Sublime into a pretty full IDE: + +http://damnwidget.github.io/anaconda/ + +There are nifty instructions on that page. + +By default, Anaconda uses the python interpreter that is in your ``PATH`` environment variable. So, the most important configuration option is the python_interpreter option that allows you to use a different Python interpreter, for example, one that resides in a virtual environment, or python3 vs python2. + +If you get the right Python when you type "python" at a raw command line, then you are OK. But if not you may need to re-configure it. + +{"python_interpreter": "~/.virtualenvs/myproject/bin/python"} + + Note: for detailed information about how to properly configure Anaconda to get the maximum of it, follow `"Configure Anaconda the Right Way" `_. + +A few settings you'll want to change +------------------------------------ + +There are a few setting you may want to change: + +* max line length for the linter: default is 72, which is pretty short these day. I use 95:: + + "pep8_max_line_length": 95, + + +White Space Management +---------------------- + +One of the issues highlighted by code linters is trailing spaces. Sublime Text provides a setting that allows you to remove them every time you save a file: + +.. code-block:: json + + { + "trim_trailing_whitespace_on_save": true + } + +This is a useful setting, but be careful if you are working with existing code: removing trailing whitespace by default causes a *ton* of noise in git commits. + +But if you use it from the start with your code, it will keep it clean from the beginning. + +It is suggested in the above settings. + +Debugging Support +----------------- + +You'll probably want to wait on this until you start using a debugger, but it's a nifty feature when you get there. + +The final requirement for a reasonable IDE experience is to be able to follow a debugging session in the file where the code exists. + + +This: https://packagecontrol.io/packages/Python%20Debugger + +Looks promising as a debugger plugin for sublime. diff --git a/_sources/modules/dev_environment/vagrant.rst.txt b/_sources/modules/dev_environment/vagrant.rst.txt new file mode 100644 index 0000000..e40d8d3 --- /dev/null +++ b/_sources/modules/dev_environment/vagrant.rst.txt @@ -0,0 +1,21 @@ +.. _vagrant-notes: + +******************************** +Setting up Python via a Linux VM +******************************** + +One of the challenges we face as developers on a team, or as students in a classroom, is getting everyone quickly up and running with a full fledged, feature rich, functional, consistent, and homogeneous Python development environment. + +Python is robust platform-independent programming language, and a full fledged development environment can be set up on any platform. However, there are still system differences, particularly between Windows and the \*nix family of Operating Systems. + +Also -- particularly if you are working on a web service of some sort, the odds are good that it will be deployed on a Linux system. In this case, developing on Linux makes things easier. + +So if you want an already setup environment in which to do your python programming that matches the environment used by the instructors and other students well (and many professional python developers too), a virtual machine running Linux can provide you that environment, regardless of the host operating system. + +We recommend `VirtualBox `_ for providing the VM, and `Vagrant `_ for deploying a pre-configured system. + +VirtualBox and Vagrant allow us to quickly build a virtual machine with everything we'll need. + +Complete instructions and installation resources can be found here: + +https://github.com/rriehle/uwpce-vagrant diff --git a/_sources/modules/dev_environment/virtualenv.rst.txt b/_sources/modules/dev_environment/virtualenv.rst.txt new file mode 100644 index 0000000..524bf1c --- /dev/null +++ b/_sources/modules/dev_environment/virtualenv.rst.txt @@ -0,0 +1,421 @@ +.. _virtualenv_section: + +####################### +Working with Virtualenv +####################### + + +"For every non-standard package installed in a system Python, the gods kill a kitten" + + - me + +Reasons Why +============ + +* As a working developer you will need to install packages that aren't in the + Python standard Library + +* As a working developer you often need to install *different* versions of the + *same* library for different projects + +* Conflicts arising from having the wrong version of a dependency installed can + cause long-term nightmares + +* Use `virtualenv`_ ... + +* **Always** + + +Installing Virtualenv +--------------------- + +The best way is to install directly in your system Python (one exception to the rule). + +To do so you will have to have `pip`_ installed. + +Try the following command: + +.. code-block:: bash + + $ which pip + /usr/local/bin/pip + +If the ``which`` command returns no value for you, then ``pip`` is not +installed in your system. To fix this, follow `the instructions here`_. + +Once you have ``pip`` installed in your system, you can use it to install +`virtualenv`_. Because you are installing it into your system Python, you will +most likely need ``superuser`` privileges to do so: + +.. code-block:: bash + + $ sudo pip install virtualenv + Downloading/unpacking virtualenv + Downloading virtualenv-1.11.2-py2.py3-none-any.whl (2.8MB): 2.8MB downloaded + Installing collected packages: virtualenv + Successfully installed virtualenv + Cleaning up... + +Great. Once that's done, you should find that you have a ``virtualenv`` +command available to you from your shell: + +:: + + $ virtualenv --help + Usage: virtualenv [OPTIONS] DEST_DIR + + Options: + --version show program's version number and exit + -h, --help ... + + +.. _pip: http://www.pip-installer.org +.. _the instructions here: http://www.pip-installer.org/en/latest/installing.html + +Using Virtualenv +================ + +Creating a new virtualenv is very very simple: + +.. code-block:: bash + + $ virtualenv [options] + + +```` is just the name of the environment you want to create. It's +arbitrary. Let's make one for demonstration purposes: + +.. code-block:: bash + + $ virtualenv demoenv + New python executable in demoenv/bin/python + Installing setuptools, pip...done. + + +What Happened? +-------------- + +When you ran that command, a few things took place: + +* A new directory with your requested name was created +* A new Python executable was created in /bin (/Scripts on Windows) +* The new Python was cloned from your system Python (where virtualenv was + installed) +* The new Python was isolated from any libraries installed in the old Python +* Setuptools was installed so you have ``easy_install`` for this new Python +* Pip was installed so you have ``pip`` for this new python + +Activation +---------- + +The virtual environment you just created, ``demoenv`` contains an executable +Python command, but if you do a quick check to see which Python executable is +found by your terminal, you'll see that it is not the one: + +.. code-block:: bash + + $ which python + /usr/bin/python + +You can execute the new Python by explicitly pointing to it: + +.. code-block:: bash + + $ ./demoenv/bin/python -V + Python 2.7.5 + +but that's tedious and hard to remember. Instead, ``activate`` your virtualenv +using the ``source`` command: + +.. code-block:: bash + + $ source demoenv/bin/activate + (demoenv)$ which python + /Users/cewing/demoenv/bin/python + +On Windows, the *activate* script is in the ``Scripts`` folder: + +``> \path\to\env\Scripts\activate`` + +There. That's better. Now whenever you run the ``python`` command, the +executable that will be used will be the new one in your ``demoenv``. + +Notice also that the your shell prompt has changed. It indicates which +``virtualenv`` is currently active. Little clues like that really help you to +keep things straight when you've got a lot of projects going on, so it's nice +the makers of virtualenv thought of it. + +Installing Packages +------------------- + +Now that your virtualenv is active, not only has your ``python`` executable been +hijacked, so have ``pip`` and ``easy_install``: + +.. code-block:: bash + + (demoenv)$ which pip + /Users/cewing/demoenv/bin/pip + (demoenv)$ which easy_install + /Users/cewing/demoenv/bin/easy_install + +This means that using these tools to install packages will install them *into +your virtual environment only* and not into the system Python. Let's see this +in action. We'll install a package called ``docutils`` that provides support +for converting ReStructuredText documents into other formats like HTML, LaTeX +and more: + +.. code-block:: bash + + (demoenv)$ pip install docutils + Downloading/unpacking docutils + Downloading docutils-0.11.tar.gz (1.6MB): 1.6MB downloaded + Running setup.py (path:/Users/cewing/demoenv/build/docutils/setup.py) egg_info for package docutils + ... + changing mode of /Users/cewing/demoenv/bin/rst2xml.py to 755 + changing mode of /Users/cewing/demoenv/bin/rstpep2html.py to 755 + Successfully installed docutils + Cleaning up... + +And now, when we fire up our Python interpreter, the docutils package is +available to us: + +.. code-block:: bash + + (demoenv)$ python + Python 2.7.5 (default, Aug 25 2013, 00:04:04) + [GCC 4.2.1 Compatible Apple LLVM 5.0 (clang-500.0.68)] on darwin + Type "help", "copyright", "credits" or "license" for more information. + >>> import docutils + >>> docutils.__path__ + ['/Users/cewing/demoenv/lib/python2.7/site-packages/docutils'] + >>> ^d + (demoenv)$ + +There's one other interesting side-effect of installing software with +``virtualenv``. The ``docutils`` package provides a number of executable +scripts when it is installed: ``rst2html.py``, ``rst2latex.py`` and so on. +These scripts are set up to execute using the Python with which they were +built. What this means is that running these scripts will use the Python +executable in your virtualenv, *even if that virtualenv is not active*! + +Deactivation +------------ + +So you've got a virtual environment created. And you've activated it so that +you can install packages and use them. Eventually you'll need to move on to +some other project. This likely means that you'll need to stop working with +this ``virtualenv`` and switch to another (it's a good idea to keep a separate +``virtualenv`` for every project you work on). + +When a ``virtualenv`` is active, all you have to do is use the ``deactivate`` +command: + +.. code-block:: bash + + (demoenv)$ deactivate + $ which python + /usr/bin/python + +Note that your shell prompt returns to normal, and now the executable Python +found when you check ``python`` is the system one again. + +Cleaning Up +----------- + +The final great advantage that ``virtualenv`` confers on you as a developer is +the ability to easily remove a batch of installed Python software from your +system. Consider a situation where you installed a library that breaks your +Python (it happens). If you are working in your system Python, you now have to +figure out what that package installed, where, and go clean it out manually. +With ``virtualenv`` the process is as simple as removing the directory that +virtualenv created when you started out. Let's do that with our ``demoenv``: + +.. code-block:: bash + + $ rm -rf demoenv + +And that's it. The entire environment and all the packages you installed into +it are now gone. There's no traces left to pollute your world. + +VirtualenvWrapper +================= + +So you have this great tool that allows you to build isolated environments in +which you can install Python software. Several questions arise when considering +this. + +* Where should such environments be placed? +* How can the environments be tied to the projects you are working on? +* Once you have more than a trivial number of projects, how can you keep track + of all these virtualenvs? + +Like any good tool, ``virtualenv`` does not impose on you any particular way of +working. You can place your environments into the directories where you are +building the project to which they apply. You can keep them all in a single +global location. You can build a random path generator that drops them +wherever. + +But any of these methods lead inevetably to chaos. They require too much from +you. It would be better if you could manage your virtual environments easily +and intuitively. + +With `virtualenvwrapper`_ you can. + +Installation +------------ + +Let's start by installing the package in our system Python, alongside +``virtualenv`` (again, you'll need ``superuser`` to do this): + +.. code-block:: bash + + $ sudo pip install virtualenvwrapper + Downloading/unpacking virtualenvwrapper + Downloading virtualenvwrapper-4.2.tar.gz (125kB): 125kB downloaded + Running setup.py (path:/private/tmp/pip_build_root/virtualenvwrapper/setup.py) egg_info for package virtualenvwrapper + ... + Successfully installed virtualenvwrapper virtualenv-clone stevedore + Cleaning up... + $ + +Once that's finished, you'll need to wire the system up by letting your shell +know that the commands it provides are present. Add the following lines to your +shell startup file (``.profile``, ``.bash-profile``, ...): + +.. code-block:: bash + + export WORKON_HOME=~/.virtualenvs + source /usr/local/bin/virtualenvwrapper.sh + +This will create a new environmental variable, ``WORKON_HOME``, that determines +where new virtual environments will be created. The actual name is completely +arbitrary. + +You'll need to be sure that the location you set exists: + +.. code-block:: bash + + $ mkdir ~/.virtualenvs + +Using ``mkvirtualenv`` +---------------------- + +When you've done that, start a new terminal and you'll have access to the +``mkvirtualenv`` command: + +.. code-block:: bash + + $ mkvirtualenv testenv + New python executable in testenv/bin/python + Installing setuptools, pip...done. + (testenv)$ ls ~/.virtualenvs + testenv + (testenv)$ which python + /Users/cewing/.virtualenvs/testenv/bin/python + (testenv)$ + +Notice a couple of things: + +* The new environment you asked for was created in ``WORKON_HOME`` +* The new environment was *immedately* activated for you + +That's a nice feature, eh? No more needing to remember to ``activate`` the env +you just created to install packages. + +Using ``workon`` +---------------- + +In addition to this nice little feature, you can also use the ``workon`` +command to see which environments you have, and to switch from one to another: + +.. code-block:: bash + + (testenv)$ workon + testenv + (testenv)$ mkvirtualenv number2 + New python executable in number2/bin/python + Installing setuptools, pip...done. + (number2)$ workon + number2 + testenv + (number2)$ workon testenv + (testenv)$ + +Sweet! + +The same ``deactivate`` command can get you back to your system environment: + +.. code-block:: bash + + (testenv)$ deactivate + $ + +Using ``mkproject`` +------------------- + +That takes care of deciding where to put new environments. It also clears up +the question of how to remember which ones you have and how to start them up +and switch between them. But we still have to figure out how to remember which +environment goes with which project. + +That's what the ``mkproject`` command is for. + +First, go back to your shell startup file and add a new environmental variable: + +.. code-block:: bash + + export PROJECT_HOME=~/projects #<- this line here is new + export WORKON_HOME=~/.virtualenvs + source /usr/local/bin/virtualenvwrapper.sh + +Then, make sure the directory you named exists: + +.. code-block:: bash + + $ mkdir ~/projects + +After all that, fire up a new shell to pick up the changes and try this: + +.. code-block:: bash + + $ mkproject foo + New python executable in foo/bin/python + Installing setuptools, pip...done. + Creating /Users/cewing/projects/foo + Setting project for foo to /Users/cewing/projects/foo + (foo)$ which python + /Users/cewing/.virtualenvs/foo/bin/python + (foo)$ pwd + /Users/cewing/projects/foo + (foo)$ ls -a $VIRTUAL_ENV + . .Python bin lib + .. .project include + (foo)$ more $VIRTUAL_ENV/.project + /Users/cewing/projects/foo + +Whoa! That command did a lot: + +* Created a new ``virtualenv`` in your ``$WORKON_HOME`` +* Created a new project directory in your ``$PROJECT_HOME`` +* Placed a ``.project`` file in your home directory with a path leading to the + associated project directory +* Activated the new virtualenv for you +* Automatically moved your present working directory to the new project + directory. + +And now, you can begin working on your ``foo`` project, secure that you will be +installing packages into the right environment. + +A Few Last Words +================ + +This quick introduction is **by no means** an exhaustive manual for either of +the packages we've talked about. There is a great deal more that they can do. +In particular, ``virtualenvwrapper`` is highly customizable, with support for +custom scripts to be hooked into every stage of the ``virtualenv`` workflow. + +I urge you to read the documentation for `virtualenv`_ and `virtualenvwrapper`_ +yourself to find out more. + +.. _virtualenv: http://www.virtualenv.org/ +.. _virtualenvwrapper: http://virtualenvwrapper.readthedocs.org diff --git a/_sources/modules/dev_environment/vsc_as_ide.rst.txt b/_sources/modules/dev_environment/vsc_as_ide.rst.txt new file mode 100644 index 0000000..98a0826 --- /dev/null +++ b/_sources/modules/dev_environment/vsc_as_ide.rst.txt @@ -0,0 +1,78 @@ +.. _vsc_as_ide: + +#################################################### +Using Visual Studio Code as a lightweight Python IDE +#################################################### + +Visual Studio Code is an extensible and customizable text editor from Microsoft that provides a very minimal layout with additional tooling such as an excellent built-in terminal. + + +Requirements +============ + +Any IDE should ease your development experience by providing the following: + +* It should provide excellent, configurable syntax colorization. +* It should allow for robust tab completion. +* It should offer the ability to jump to the definition of symbols in other files. +* It should perform automatic code linting to help avoid silly mistakes. +* It should be able to interact with a Python interpreter such that when debugging, the editor will follow along with the debugger. + +Visual Studio Code requires that you perform some setup out of the box (see below for details). + + +Which Version? +============== + +There's just the latest version available to download. + +This ensures that all recent bug fixes and updates have been made. + +Visual Studio Code runs on Macs, Windows, and Linux flavors like Ubuntu, Debian, Red Hat, etc. + +Also, Visual Studio Code performs updates on itself, so there's no need to download newer versions of the app... you should already have it. + +Installation +============ + +Check out this solid video_ that will walk you through the process of setting up Visual Studio Code for Python in detail. + +.. _video: https://www.youtube.com/watch?v=TILIcrrVABg/ + +Go to the Visual Studio Code website_. + +.. _website: https://code.visualstudio.com/ + +Scroll down to the bottom of the page and you'll see links for installers to all the major OS platforms. + +Download your flavor and run the installer. + + +Basic Settings +============== + +Visual Studio Code can be used out of the box with no setup as a text editor. It automatically +recognizes file types and helpfully highlights text accordingly. To use in this manner, +write your Python files in Visual Studio Code, then run them in your Python command prompt +or Visual Studio Code's own built in terminal: Ctrl + \` (control-backtick) + + +Extending the Editor +==================== + +After you've install Visual Studio Code, there are many ways to extend it for working with Python. + +The video linked above goes into this much deeper. + +There is also a great tutorial for setting up Python here_. + +.. _here: https://code.visualstudio.com/docs/python/python-tutorial + +If you're on a Mac, be sure to set up your path_ for easy integration with the terminal. + +.. _path: https://code.visualstudio.com/docs/setup/mac + +I also recommend setting up Visual Studio Code as your default_ Git editor. + +.. _default: https://stackoverflow.com/questions/30024353/how-to-use-visual-studio-code-as-default-editor-for-git + diff --git a/_sources/modules/dev_environment/windows_bash.rst.txt b/_sources/modules/dev_environment/windows_bash.rst.txt new file mode 100644 index 0000000..c624712 --- /dev/null +++ b/_sources/modules/dev_environment/windows_bash.rst.txt @@ -0,0 +1,37 @@ +.. _windows_bash: + +***************************************** +Using Windows Bash for Python Development +***************************************** + +**CAUTION** -- none of the instructors in the program use this -- so this is experimental -- proceed at your own risk! + +With Windows 10, Microsoft has introduced the "Windows Subsystem for Linux" (WSL). Technically, it's not Linux at all (it is not running the Linux kernel), but it does provide an actual bash shell, and access to much (all) of the packages available in Ubuntu Linux, providing a very Linux-like environment. + +If you run Windows 10, but want to be able to work in an environment that is very much like Linux (and the OS-X command line) Windows-bash may be a good options for you. + +Installing the System +===================== + +Here are MS's docs: + +https://msdn.microsoft.com/commandline/wsl/about + +and the install guide: + +https://msdn.microsoft.com/en-us/commandline/wsl/install_guide + + +Installing / running Python +=========================== + +To use Python from within the bash shell, you probably want a "linux" Python, rather than the native Windows installer. Unfortunately, the Ubuntu version that Windows Bash is hooked up to does not natively have the latest Python version -:( + +Using the environment +===================== + +The WSL provides a full linux experience -- but, most importantly, it does not suport any GUI environment. If you are comfortable with a command line editor like vim or Emacas, then you can work entirely in the bash shell. However, you may very well want to work with a nice modern GUI editor like Sublime Text or Notepad++. This is quite possible, as the systems share a file system. From the FAQ: + + One of the benefits of WSL is being able to use the same file with both Windows and Linux apps or tools. + +So you can manipulate and edit your files, as well as use the browser, etc, in regular Windows, and still run Python and git, etc in the bash shell. diff --git a/_sources/modules/index.rst.txt b/_sources/modules/index.rst.txt new file mode 100644 index 0000000..579bd51 --- /dev/null +++ b/_sources/modules/index.rst.txt @@ -0,0 +1,227 @@ + + +################################## +Introduction to Python Topic Notes +################################## + +These are assorted topic notes used in the Python certificate program. + + +Setting up your Environment +--------------------------- + +.. toctree:: + :maxdepth: 1 + + ./dev_environment/index + EnvironmentOverview + IPythonIntroduction + Py2vsPy3 + Git + GitWorkflow + Submitting_to_github + SubmittingCodeGithubClassroom + HowToRunAPythonFile + Learning + +Basic Python +------------ + +.. toctree:: + :maxdepth: 1 + + BasicPython + Functions + +Recursion +--------- + +.. toctree:: + :maxdepth: 1 + + Recursion + +Booleans +-------- + +.. toctree:: + :maxdepth: 1 + + Booleans + +Sequences and Iteration +----------------------- + +.. toctree:: + :maxdepth: 1 + + Sequences + Iteration + +Basic Text Handling +------------------- + + Strings + +Exception Handling +------------------ + +.. toctree:: + :maxdepth: 1 + + Exceptions + + +Unit Testing +------------ + +.. toctree:: + :maxdepth: 1 + + Testing + TestDrivenDevelopment + + +Dictionaries and Sets +--------------------- + +.. toctree:: + :maxdepth: 1 + + DictsAndSets + DictionaryAsSwitch + +Files +----- + +.. toctree:: + :maxdepth: 1 + + Files + +Modules and Packages +-------------------- + +.. toctree:: + :maxdepth: 1 + + NamingThings + Modules + Documentation + Packaging + + +Comprehensions +-------------- + +.. toctree:: + :maxdepth: 1 + + Comprehensions + CollectionsModule + + +Advanced Argument Passing +------------------------- + +.. toctree:: + :maxdepth: 1 + + AdvancedArgumentPassing + MoreOnMutability + + +Intro to Object Oriented Programing +----------------------------------- + +.. toctree:: + :maxdepth: 1 + + ObjectOrientationOverview + PythonClasses + +Properties and Magic Methods +---------------------------- + +.. toctree:: + :maxdepth: 1 + + Properties + StaticAndClassMethods + SpecialMethodsAndProtocols + +Subclassing +----------- + +.. toctree:: + :maxdepth: 1 + + SubclassingAndInheritance + + +Multiple Inheritance +-------------------- + +.. toctree:: + :maxdepth: 1 + + MultipleInheritance + + +Introduction to Functional Programming +-------------------------------------- + +.. toctree:: + :maxdepth: 1 + + OO_vs_functional + Lambda + MapFilterReduce + IPythonParallel + +Advanced Testing +---------------- + +.. toctree:: + :maxdepth: 1 + + Testing_advanced + + +Extras +------ + +The following are some extra topics that might be of interest + +.. toctree:: + :maxdepth: 1 + + Pep8 + CodeReviews + PersistanceAndSerialization + Unicode + + Lambda + IteratorsAndGenerators + + Closures + + Decorators + ContextManagers + + + MetaProgramming + + Logging + Debugging + + NoSQL + GraphDatabases + + Concurrency + ThreadingMultiprocessing + Coroutines + Async + + Profiling + diff --git a/_sources/modules/learning.rst.txt b/_sources/modules/learning.rst.txt new file mode 100644 index 0000000..f66ebbd --- /dev/null +++ b/_sources/modules/learning.rst.txt @@ -0,0 +1,133 @@ +.. _python_learning_resources: + +================================ +Useful Python Learning Resources +================================ + +In addition to the material we cover in class, there are numerous online +resources to help a newcomer get to know Python. The following list represents +the best-known and best-regarded of the breed. If you are itching for a bit +more work on your Python chops, you should try these out. + +Python Language Resources +========================== + +As a Python programmer, you'll want to keep a bookmark pointed at the +official Python documentation (https://docs.python.org/3/), especially +the documentation for the standard library +(https://docs.python.org/3/library/index.html). However, there are a +number of additional resources you can (and should) use to help build +your Python chops. + +For the beginner +----------------- + +Getting started tutorials +......................... + +* **Jessica McKeller's begining tutorial** (Video) + https://www.youtube.com/watch?v=MirG-vJOg04 + +* **The Python Tutorial** + (https://docs.python.org/3/tutorial/): This is the + official tutorial from the Python website. No more authoritative source is + available. + +* **Code Academy Python Track** + (http://www.codecademy.com/tracks/python): Often + cited as a great resource, this site offers an entertaining and engaging + approach and in-browser work. + +* **Learn Python the Hard Way** + (https://learnpythonthehardway.org/python3/): Solid + and gradual. This course offers a great foundation for folks who have never + programmed in any language before. + + +Complete Books / Series +....................... + +* **Dive Into Python 3** + (https://www.apress.com/us/book/9781430224150): The updated version + of a classic. This book offers an introduction to Python aimed at the student + who has experience programming in another language. It used to be available free online -- you can still find PDF copies on the web. + +* **Python for You and Me** + (http://pymbook.readthedocs.org/en/latest/): Simple + and clear. This is a great book for absolute newcomers, or to keep as a quick + reference as you get used to the language. + +* **Think Python** + (http://greenteapress.com/thinkpython2/html/index.html): Methodical and + complete. This book offers a very "computer science-style" introduction to + Python. It is really an intro to Python *in the service of* Computer Science, + though, so while helpful for the absolute newcomer, it isn't quite as + "pythonic" as it might be. + +* **Core Python Programming** + (http://corepython.com/): Only available as a dead trees version, but + if you like to have book to hold in your hands anyway, this + is the best textbook style introduction out there. It starts from the + beginning, but gets into the full language. Published in 2009, but still in + print, with updated appendixes available for new language features. + +* **Python 101** + (https://leanpub.com/python_101) + Available as a reasonably priced ebook. This is a new one from a popular blogger + about Python. Lots of practical examples. Also available as a Kindle book: + http://www.amazon.com/Python-101-Michael-Driscoll-ebook/dp/B00KQTFHNK + +Python Tutor +............ + +Python Tutor is a really nifty site that lets you write and run Python code, and visualize what is going on as it runs. Really gives you idea what is going on under the hood, with the stack, etc. + +http://pythontutor.com/ + + +Next Steps +---------- + +Web sites / services: +..................... + +* **New Coder** + (http://newcoder.io): advertised as "Five lifejackets to throw to + the new coder", this site offers five very interesting tutorials written in + an engaging style. Not an introduction. More a second step. + +* **OpenHatch** + (https://openhatch.org/wiki/Intermediate_Python_Workshop/Projects): + The Open Hatch project offers a number of workshops with well-paced + intermediate tutorials for Python programming. A great place to go once you + have the basics down and are ready for more challenging work. + +Advanced Books +.............. + +* **The Python Cookbook** + (http://chimera.labs.oreilly.com/books/1230000000393): + The Python Cookbook provides practical solutions to various programming tasks, along with excellent explanations of how they work. Written by David Beazley -- Python author and instructor extraordinaire. Also available fully online: (http://chimera.labs.oreilly.com/books/1230000000393/index.html) + +* **Fluent Python** + (http://shop.oreilly.com/product/0636920032519.do): Fluent Python is an excellent deeper dive into the inner workings of Python. Written for folks that already understand the basics of Python, it goes provides explainations of the more advanced topics in Python. If we were going to use a textbook for an advanced Python class, this would be it. + +Evaluating Your Options +----------------------------- + +The blurbs above are short descriptions of the material in each resource. We've +drawn them both from our own usage of the various tools, and from a wonderful +set of online reviews: + +(http://planningadinner.blogspot.com/search/label/So%20you%20want%20to%20learn%20Python.%20What%27s%20next%3F) + +done by Marta Maria Casetti on her blog, "Planning a Dinner" +(http://planningadinner.blogspot.com/). + +The poster she presented at PyCon 2014 +(http://planningadinner.blogspot.com/2014/04/the-poster.html) +as a result of that research offers some great hints about the aspects of +Python programming best covered by each resource. I would urge any new student +of Python to take the time to look over this poster to help determine the best +path forward for themselves. + diff --git a/_sources/solutions/mailroom/mailroom_json_save/README.rst.txt b/_sources/solutions/mailroom/mailroom_json_save/README.rst.txt new file mode 100644 index 0000000..2c08e9a --- /dev/null +++ b/_sources/solutions/mailroom/mailroom_json_save/README.rst.txt @@ -0,0 +1,15 @@ +:orphan: + +######## +Mailroom +######## + +Mailroom is a multi-class assignment in UWPCE Python Certificate Program. + +This version is set up as a complete python package, and is using mocking to test the interactive UI code. + + + + + + diff --git a/_sources/solutions/mailroom/mailroom_meta/README.rst.txt b/_sources/solutions/mailroom/mailroom_meta/README.rst.txt new file mode 100644 index 0000000..c129cfb --- /dev/null +++ b/_sources/solutions/mailroom/mailroom_meta/README.rst.txt @@ -0,0 +1,15 @@ +:orphan: + +This is a solution to the mailroom program with data saving via the json_save package. + +Two solutions -- The metaclass version and the decorator solution. Not that different, really. + +The json_save package is included as well. It must be installed in order for the the mailroom packages to use it: + +:: + + python -m pip install -e ./ + + + + diff --git a/_sources/topics/00-Template_Topic/index.rst.txt b/_sources/topics/00-Template_Topic/index.rst.txt new file mode 100644 index 0000000..3bb94dc --- /dev/null +++ b/_sources/topics/00-Template_Topic/index.rst.txt @@ -0,0 +1,28 @@ +:orphan: + +Topic Name +========== + + +To Read +------- + +.. toctree:: + :maxdepth: 1 + +.. ../../modules/xxxxx + + +Activities +---------- + +Exercises +--------- + +.. toctree:: + :maxdepth: 1 + +.. ../../exercises/xxxxx + + + diff --git a/_sources/topics/01-setting_up/Py2vsPy3.rst.txt b/_sources/topics/01-setting_up/Py2vsPy3.rst.txt new file mode 100644 index 0000000..555b38d --- /dev/null +++ b/_sources/topics/01-setting_up/Py2vsPy3.rst.txt @@ -0,0 +1,101 @@ +.. _py2_vs_py3: + +######################## +Python 2 versus Python 3 +######################## + + +Much of the example code you'll find online is Python2, rather than Python3 + +For the most part, they are the same -- so you can use those examples to learn from. + +There are a lot of subtle differences that you don't need to concern yourself with just yet. + +But a couple that you'll need to know right off the bat: + +print() +------- + +In Python2, ``print`` is a "statement", rather than a function. That means it didn't require parentheses around what you want printed: + +.. code-block:: python + + print something, something_else + +This made it a bit less flexible and powerful. + +But -- if you try to use it that way in Python3, you'll get an error: + +.. code-block:: python + + In [15]: print "this" + File "", line 1 + print "this" + ^ + SyntaxError: Missing parentheses in call to 'print' + +So -- if you get this error, simply add the parentheses: + +.. code-block:: ipython + + In [16]: print("this") + this + +Division +-------- + +In python 3, the division operator is "smart" when you divide integers: + +.. code-block:: ipython + + In [17]: 1 / 2 + Out[17]: 0.5 + +However in Python2, integer division, will give you an integer result: + +.. code-block:: ipython + + In [1]: 1/2 + Out[1]: 0 + +In both versions, you can get "integer division" if you want it with a double slash: + +.. code-block:: ipython + + In [1]: 1//2 + Out[1]: 0 + +And in Python2, you can get the behavior of Python3 with "true division": + +.. code-block:: ipython + + In [2]: from __future__ import division + + In [3]: 1/2 + Out[3]: 0.5 + +For the most part, you just need to be a bit careful with the rare cases where Python2 code counts on integer division. + +Iterators vs Lists +------------------ + +In Python2, a number of functions returned a full list of the contents. But most of the time, you didn't need a list -- you only needed a way to loop through all the items returned. Such an object is called an "iterable" -- more about that later in the class. But for now, if you get an error like:: + + TypeError: 'dict_keys' object does not support indexing + +Then you likely got an iterator, rather than a "proper" list. You can fix this by making a list out of it:: + + list(an_iterator) + +the list constructor will make a list out of any iterable. So you can now index it, etc. + +Other Python2 / Python3 differences +----------------------------------- + +The most drastic difference (improvement!) is better Unicode support, and better bytes / Unicode separation. + +Most of the other differences are essentially implementation details, like getting iterators instead of sequences -- we'll talk about that more when it comes up in a later lesson. + +There are also a few syntax differences with more advanced topics: Exceptions, ``super()``, etc. + +We'll talk about all that when we cover those topics as well. diff --git a/_sources/topics/01-setting_up/advanced_setup.rst.txt b/_sources/topics/01-setting_up/advanced_setup.rst.txt new file mode 100644 index 0000000..8807277 --- /dev/null +++ b/_sources/topics/01-setting_up/advanced_setup.rst.txt @@ -0,0 +1,32 @@ +.. _advanced_setup: + +============== +Advanced Setup +============== + +Once you have the core tools running, there are a number of things you can do to really get your environment working well. It is well worth doing some of this customization right up front. + + +Creating an IDE +--------------- + +.. toctree:: + :maxdepth: 1 + + sublime_as_ide.rst + atom_as_ide.rst + vsc_as_ide.rst + +Command Shells +-------------- + +.. toctree:: + :maxdepth: 1 + + shell.rst + ipython.rst + windows_bash.rst + + + + diff --git a/_sources/topics/01-setting_up/atom_as_ide.rst.txt b/_sources/topics/01-setting_up/atom_as_ide.rst.txt new file mode 100644 index 0000000..d4c0c70 --- /dev/null +++ b/_sources/topics/01-setting_up/atom_as_ide.rst.txt @@ -0,0 +1,143 @@ +.. _atom_as_ide: + +########################################## +Turning Atom Into a Lightweight Python IDE +########################################## + +Atom is the self-proclaimed "hackable text editor for the 21st Century." It has a nice modern interface, and is highly customizable yet can also be used productively with minimal setup and configuration. + + +Requirements +============ + +Any IDE should ease your development experience by providing the following: + +* It should provide excellent, configurable syntax colorization. +* It should allow for robust tab completion. +* It should offer the ability to jump to the definition of symbols in other files. +* It should perform automatic code linting to help avoid silly mistakes. +* It should be able to interact with a Python interpreter such that when debugging, the editor will follow along with the debugger. + +Atom does all this and more, but some functionality requires you to select and install packages. + + +Which Version? +============== + +The latest version is the best version. Atom is regularly maintained, so the latest +version will have the latest bug fixes and updates. + + +Installation +============ + +Go to the Atom website_. + +.. _website: https://atom.io/ + +On the main page, click the big red button to download the installer, then run the installer. + +If it is not offering the correct platform: on the main page, below the big red button, click Other Platforms and find the installer for your operating system. + +If you already have Atom installed, but want to check for a newer version, go to +``Help`` -> ``Check for Update``. + + +Basic Settings +============== + +Atom can be used out of the box with no setup as a text editor. It automatically +recognizes file types and helpfully highlights text accordingly. To use in this manner, +write your Python files in Atom, then run them in your Python command prompt. + + +Extending the Editor +==================== + +When you first open Atom, a Welcome Guide appears. This provides some quick and helpful information on +how to open projects, install packages, and customize your themes and styling. + +Atom has great documentation_ on how to hack and configure it. Read the Flight Manual_ for tons of information on +everything you can do. You can also watch a Getting Started video_. + +.. _documentation: https://atom.io/docs +.. _Manual: http://flight-manual.atom.io/ +.. _video: https://www.youtube.com/watch?v=U5POoGSrtGg + +Atom has a configuration file which you can modify called config.cson. +Access it via ``File`` -> ``Config...`` + +:: + + "*": + core: + themes: [ + "atom-dark-ui" + "solarized-light-syntax" + ] + editor: + fontSize: 19 + "exception-reporting": + userId: "6e2a9c3f-7ddb-7deb-b5f7-b58f2f87ac0d" + "tree-view": + hideVcsIgnoredFiles: true + +Here you can quickly change the theme or font size. Some packages will require you to add configs +or make adjustments here. Read the documentation carefully when installing packages. + +In general, you can extend Atom by installing packages, and then accessing their functionality from the Packages +drop-down menu. Access the Install Packages page from the Welcome Guide page. If the Welcome Guide is not open, +you can open it via ``Help`` -> ``Welcome Guide``. + +Keyboard shortcuts are specified in the packages menus if available. + +The Useful Packages presented below are only a few options of many. + + +Useful Packages +=============== + +Running Scripts +--------------- + +To run scripts within Atom, you will need to install the Script_ package. The Script package supports a ton of languages, +including Python! + +.. _Script: https://atom.io/packages/script + +Autocompletion +-------------- + +By default, Atom knows which Python packages you have imported, variables you have created +and so on. Autocomplete_ ships with Atom and requires no setup. + +.. _Autocomplete: http://flight-manual.atom.io/using-atom/sections/autocomplete/ + +Code Linting +------------ + +To get code linting functionality in Atom, you will need to install a linting package +of which there are many to choose from. linter-pylint_ works well, and requires minimal +setup. + +.. _linter-pylint: https://atom.io/packages/linter-pylint + +White Space Management +---------------------- + +Atom knows when you are writing Python and helps you out by dealing with spaces and tabs +in the same way. When in a Python file, if you type 4 spaces, then hit delete, you are +taken back a tab. + +The Whitespace_ package ships with Atom and requires no setup. Under the ``Packages`` -> ``Whitespace`` menu, +you will find tools to turn all tabs into spaces, all spaces into tabs, among other whitespace-related options. + +.. _Whitespace: https://atom.io/packages/whitespace + +Debugging +--------- + +To use a Python debugger in Atom, you will need to install the python-debugger_ package. Once installed, turn on the +debugger by going to ``Packages`` -> ``python-debugger`` -> ``Toggle``. + +.. _python-debugger: https://atom.io/packages/python-debugger diff --git a/_sources/topics/01-setting_up/command_line.rst.txt b/_sources/topics/01-setting_up/command_line.rst.txt new file mode 100644 index 0000000..c0b7617 --- /dev/null +++ b/_sources/topics/01-setting_up/command_line.rst.txt @@ -0,0 +1,38 @@ +.. _command_line_basics: + +=================== +Command line basics +=================== + +Windows +------- + +Tutorials +......... + +I honestly haven't found a really good tutorial for the Windows command line, but these will get you started. Please let us know if you find a better option! + +http://www.digitalcitizen.life/command-prompt-how-use-basic-commands + +http://www.bleepingcomputer.com/tutorials/windows-command-prompt-introduction/ + +Zed Shaw's tutorial in "Learn Code the Hard way" is excellent, but apparently is no longer available for free. However there is a tutorial in an appendix to Learn Python the Hard way: + +`Command Line Crash Course `_ + + +Linux and OS-X +-------------- + +Tutorials +......... + +* Software Carpentry had a nice introduction to the command line: + + https://swcarpentry.github.io/shell-novice/01-intro/ + +* http://conqueringthecommandline.com/book + +* Zed Shaw's tutorial in "Learn Code the Hard way" is excellent, but apparently no longer available for free. However there is one as an appendix to Learn Python the Hard way: + + https://learnpythonthehardway.org/book/appendixa.html diff --git a/_sources/topics/01-setting_up/environment_overview.rst.txt b/_sources/topics/01-setting_up/environment_overview.rst.txt new file mode 100644 index 0000000..922cafe --- /dev/null +++ b/_sources/topics/01-setting_up/environment_overview.rst.txt @@ -0,0 +1,41 @@ +############################################ +Introduction to your Programming Environment +############################################ + +This program is a course of instruction in developing with the Python programming language. + +Python is a language with multiple implementations, and many different ways to edit and run code. + +One's development environment is a personal thing. What is most productive for you depends on what platform you use, how you like to work, what the people you work with are using, etc. It is a very personal choice. + +Each of you starting this program comes with a different background and experience. So we do not require that you use a particular development environment. Indeed, each of the instructors in the program uses their own tools and approach. + +However, there are some core requirements, and we provide advice for what to do if you are just getting started. + +Core Requirements for Python Development +======================================== + +There are three basic elements to your environment when working with Python for this class: + +* A way to run your code, add packages to Python, use git, etc. + + - You really need at least a little familiarity with the command line for this. + +* A Python interpreter + + - We use "cPython" version 3.8 or greater for this class. + +* A way to edit your code. + + - Any good programmer's text editor with a Python mode will work well. + +If you are already set up with all this, then go straight here: + +:ref:`testing_your_setup` + +and give it a try. + +If you are not sure, then read on ... + + + diff --git a/_sources/topics/01-setting_up/feature_branching.rst.txt b/_sources/topics/01-setting_up/feature_branching.rst.txt new file mode 100644 index 0000000..922bacf --- /dev/null +++ b/_sources/topics/01-setting_up/feature_branching.rst.txt @@ -0,0 +1,223 @@ +***************** +Feature Branching +***************** + +.. note:: Feature branching is an important topic, but not one you need to know about right off -- if you are brand new to git -- feel free to ignore this, with gitHub classroom, it should handle it for you. + + +Why use Feature Branches? +========================= + +The idea is to give a clear, highly focused purpose to each branch, +which is why we call them feature branches. They represent specific +feature being added or in our case a single assignment. + +When using feature branches you isolate your code from everything else, +and Pull Request becomes easier to review as they only contain one +thing. This also mean that you can work on multiple features (multiple +assignments) at the same time without effecting each other and being +able to have two separate clean PRs along side. + +Keep in mind that in a more professional setting the "master" branch +represents production code and will be the cleanest most stable branch. + +Below is what's called "Network Graph" of git activity. This displays +best what happens with feature branches. + +The black line represents master, the green represents my 1st feature +branch that got merged into master (you can see arrow goes to black +line), then I created second feature branch called "test2" (blue line) +and that also got merged into master. + +So feature branches get branched off the main branch (master) at some +point in time, then you do some isolated work, then merge it back into +main branch. No commits to master will affect your work in your feature +branch while you're in it. + +.. image:: images/feature_branching_img1.png + :scale: 75 + + +Workflow +======== + +1. Feature / Assignment Branch +------------------------------ + +Below explains the process for creating your feature branch. You can +either use the command line or the UI. + +Creating feature branch via Command Line +........................................ + +First make sure you're on master and make sure you have the latest +changes + +.. code-block:: bash + + git checkout master + + git pull + +Next you will create a new branch (-b flag) based of master + +.. code-block:: bash + + git checkout -b + +Here is the full example: + +.. code-block:: bash + + $ git checkout master + + Already on 'master' + + Your branch is up to date with 'origin/master'. + + $ git pull + + $ git checkout -b lesson1/assignment1 + + Switched to a new branch 'lesson1/assignment1' + +.. code-block:: bash + + $ git branch + branch_name1 + another_branch + * lesson1/assignment1 <------- The asterisk here indicates your current branch + master + + +That's it. You are now on an isolated feature branch, do your work and +make commits to this branch. There should be no difference.. when you're +ready to push your changes you would now push to your feature branch +instead of master: + +.. code-block:: bash + + git push origin + + +Creating feature branch via GitHub UI +..................................... + +Navigate to your forked GitHub repo and identify Branch dropdown: + +.. image:: images/feature_branching_img2.png + :scale: 50 + +Initially you should only see master branch listed. Within that dropdown +type the name of the new feature branch, for this example we will call +it ``lesson1/assignment1`` + +.. image:: images/feature_branching_img3.png + :scale: 50 + +Once you start typing the name of the branch, and if it does not +currently exist, you will get a prompt to create new branch. Click into +highlighted area (colors may differ based on browser or theme) and now +you have a new branch! + +Go into command line and check out this new branch: + +.. code-block:: bash + + $ git pull + + $ git checkout lesson1/assignment1 + + Switched to branch 'lesson1/assignment1' + + $ git branch + + feature_branch + + * lesson1/assignment1 <------- asterisk here indicates your current branch + + master + + $ + +2. When Feature Work is Complete +-------------------------------- + +When you're done working on your feature branch it is time to create a +Pull Request to get your changes into master branch (both main class +repo and fork) + +Create PR into main repo +........................ + +In GitHub UI navigate to Pull Requests and select New pull request +button, you should now see options for source/target and branches + +.. image:: images/feature_branching_img4.png + :scale: 50 + +In the far right dropdown you will want to select your feature branch. +Create the PR. + +Merge your feature branch into *your forked* master branch +.......................................................... + +Command Line +^^^^^^^^^^^^ + +note that you can always verify you are in your forked version by +running the command below which should show URL of origin + +.. code-block:: bash + + git remote show origin + +Now check out master and make sure it is up to date + +.. code-block:: bash + + git checkout master + + git pull + +Next merge your feature branch (in our example ``lesson1/assignment1`` +into master + +.. code-block:: bash + + git merge --no-ff lesson1/assignment1 + +Next you will be prompted to commit with pre-populated commit message, +then save and close. + +Finish up with + +.. code-block:: bash + + git push origin master + +GitHub UI +^^^^^^^^^ + +If you're not comfortable with command line, it is very easy to do in +the GitHub UI! + +Navigate to Pull Requests again, and press New Pull request button, you +will now select your fork on the far left with master branch, and your +feature branch on the far right. + +.. image:: images/feature_branching_img5.png + :scale: 50 + +Create your pull request, then merge it and delete feature branch. + +Extensive Explanation of Feature Branching +========================================== + +It has been said that git is not a Revision Control System, but rather, a tool you can use to make a Revision Control System. What this means is that git provides a huge number of features for managing your source code, but you still need to decide how to use it for your particular project. This is often referred to as the git "workflow". The "feature-branch" workflow is one such approach to managing a project. + +Here are a couple nice references that explain the feature branching workflow: + +https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow + +https://blog.landscape.io/use-feature-branches-for-everything.html diff --git a/_sources/topics/01-setting_up/git.rst.txt b/_sources/topics/01-setting_up/git.rst.txt new file mode 100644 index 0000000..5e03d19 --- /dev/null +++ b/_sources/topics/01-setting_up/git.rst.txt @@ -0,0 +1,14 @@ +=== +Git +=== + +Git is a very powerful and useful tool. We only use a few of its features for this program, but it is well worth getting more familiar with it. + +.. toctree:: + :maxdepth: 1 + + intro_to_git + github_classroom + git_overview + git_hints + feature_branching diff --git a/_sources/topics/01-setting_up/git_editor_windows.rst.txt b/_sources/topics/01-setting_up/git_editor_windows.rst.txt new file mode 100644 index 0000000..80a0838 --- /dev/null +++ b/_sources/topics/01-setting_up/git_editor_windows.rst.txt @@ -0,0 +1,48 @@ +.. _install_nano_win: + +Making your text Editor work with git on Windows +================================================ + +By default, git will dump you into the "vim" editor when you make a commit. + +"vim" is a venerable old Unix tool that no one but Unix geeks can make any sense of. + +So you are likely to want to use something else. + +git can be configured to use any editor you like. + +Here are some options: + +Notepad: +-------- + +https://github.com/github/GitPad/ + +Sublime Text: +------------- + +http://stackoverflow.com/questions/32282847/opening-sublime-text-from-windows-git-bash + +Notepad++ +--------- + +https://danlimerick.wordpress.com/2011/06/12/git-for-windows-tip-setting-an-editor/ + +Nano +---- + +This was a nice way to go -- but unfortunately, there no longer seems to be a Windows binary available for nano. + +For all Windows installations, download the WinNT/9x binary from here: + +http://www.nano-editor.org/download.php + +Unzip the file and move the files into the git bin directory: C:\Program Files\Git\bin + +That's it! You should now be able to use nano from git bash: + +.. code-block:: bash + + $ nano test.txt + +Command shortcuts are helpfully written in the editor! \ No newline at end of file diff --git a/_sources/topics/01-setting_up/git_hints.rst.txt b/_sources/topics/01-setting_up/git_hints.rst.txt new file mode 100644 index 0000000..5beff97 --- /dev/null +++ b/_sources/topics/01-setting_up/git_hints.rst.txt @@ -0,0 +1,286 @@ +.. _git_hints: + +######### +git Hints +######### + +git is a very complex system, and can be used in many ways. Because of this, it can be hard to find answers to seemingly simple questions, even though the Internet is full of discussions of how to use git. + +Every group using git has to establish a standard "work flow". If you google "git workflow" you find a LOT of discussion, and they are not all the same. And depending on the workflow you are using, the problems you'll have and the solutions to them will be different. + +We are using a very simplified workflow with gitHub classroom for this class, and this page seeks to provide solutions to problems that you might encounter specifically with this workflow. + +.. note:: This is a page for reference. It is a bit outdated, and should not be required right off. But do remember that it's here if you get tangled up in git as we move along. + + +"origin" and "upstream" +======================= + +git is a "distributed version control system". That means that git repositories are self contained, and can be "connected" with multiple other, remote repositories -- that is, repos on other machines elsewhere on the internet. + +This facilitates collaboration with widely dispersed groups, but as it allows essentially arbitrary complexity, some conventions have emerged. + +a git repo can be "connected" with virtually any number of remote repositories you can see what yours is connected to with: + +.. code-block:: bash + + git remote -v + +After cloning a repository (from gitHub, for instance) on your machine, is will look something like this: + +.. code-block:: bash + + $ git remote -v + origin https://github.com/PythonCHB/Sp2018-Accelerated.git (fetch) + origin https://github.com/PythonCHB/Sp2018-Accelerated.git (push) + +so I have one remote repository, on gitHub. It is listed twice, as I am both fetching from (pulling) and pushing to the same repository. "origin" is created when you do a clone, and it is the one that is pushed to and pulled from by default. git is so flexible that you could set it up to push and pull be default to two different repos, but I've never seen that done. + +There is often use for having more than one remote repository, to keep various workflows in sync. But with gitHub classroom, you will have one: the "origin" remote that was created when you cloned your gitHub repo. + +Adding a remote +--------------- + +If you do need to add a remote, you it's pretty easy: + +.. code-block:: bash + + git remote add name_of_remote https://the_long_url_to_the_remote_repo.git + +"upstream" is a common name for antoher remote you need to talk to. + +Changing a remote +----------------- + +If your remotes are not set up right, you can reset them, but removing one: + +.. code-block:: bash + + git remote remove upstream + +and then adding it back correctly: + + $ git remote add upstream https://the_long_url_to_the_remote_repo.git + + +Working with "upstream" +----------------------- + +If you were to try to push to the upstream one, it would fail, as you do not have permissions to do so. But when you do: + +.. code-block:: bash + + $ git pull upstream master + +you are telling git to pull all the latest changes from the "upstream" repository into your local one. Note that all those changes will only get into your repo on github (origin) when you push: + +.. code-block:: bash + + $ git push + +Note that "origin" is the default remote, and "master" is the default branch, so that command is the same as: + +.. code-block:: bash + + $ git pull origin master + +And when you pull from your gitHub repo (``git pull``) that is shorthand for: + + $ git pull origin master + +Note that you may not have a reason to pull from your origin repo. But if you were to work on two different machines -- say a personal laptop at home, and a work machine at the office, you could push stuff to your gitHub repo from both, and use ``git pull`` to keep your changes in sync. + +In fact, I highly recommend using git and gitHub as a way to coordinate your personal work if you have multiple machines (or multiple OSs, or...). You also get a backup essentially for free that way. + + +Backing out a change +==================== + +If you change a file in your repo, and you decide that you simply want to put it back the way it was the last time you committed it -- that's easy:: + + git checkout the_name_of_the_file + +Backing out a change that has been committed. +--------------------------------------------- + +Here's the situation: + +I accidentally changed a file in the examples dir in my fork of the repo. + +Then I committed it, and pushed that commit to gitHub and did a PR. + +So how do I back this out? + +What you want to do is "checkout" the file from a previous commit. + +So the first step is to find a commit that has the correct version of the file. + +In this example, the file in question is: + +``examples/Session05/maillroom_test.py`` + +I can use ``git log`` to figure out when the file was last touched:: + + $ git log examples/Session05/maillroom_test.py + +That means: "show me the log of that particular file". ``git log`` by itself will show you the history of the entire repo -- less useful in this case. + +In this case, I got:: + + $ git log examples/Session05/maillroom_test.py + commit 87d27a12bcae5c1bdc565e05e954e7c94bfa27e0 (HEAD -> master, origin/master, origin/HEAD) + Author: Chris Barker + Date: Sat Dec 9 16:18:22 2017 -0800 + + adding a bit just to test... + + commit 8e5908a37d7df90263057644fef7138e77838107 + Author: Chris Barker + Date: Sun Nov 5 11:12:06 2017 -0800 + + some updates + + commit 4795ddf41f20cfc4346f02319ab61699e8a469f2 + Author: Chris Barker + Date: Tue Oct 31 18:59:31 2017 -0700 + + added mailroom review + +The entry at the top, from Dec 9th, is the one I want to get rid of, so I want to checkout the version of the file back to the one before that top entry. + +Each "commit" is essentially a snapshot of the entire repo when "git commit" was run. Each one is identified by a unique "hash" -- that long string of characters. + +To restore a file back to the state in a previous commit, we do:: + + git checkout 8e5908a37d7d examples/Session05/maillroom_test.py + +And that puts it back to the state it was in at that previous commit, identified by that "hash". + +Note that the full hash for each commit is really long, but git will if you use enough characters to uniquely identify it -- ten or so is usually plenty. + + +git blame +========= + +``git blame`` is a handy utility for examining the history of a particular part of a particular file. For example: + +``git blame -L 2,6 examples/Session05/maillroom_test.py`` + +That means: "show me the changes to lines 2--6 of this file". + +It's called *"blame"* because you can use it to figure out who to blame for a change in a file. + +Here's what I got with that example:: + + 4795ddf4 (Chris Barker 2017-10-31 18:59:31 -0700 2) from os import system + 4795ddf4 (Chris Barker 2017-10-31 18:59:31 -0700 3) + 87d27a12 (Chris Barker 2017-12-09 16:18:22 -0800 4) # some extra in here just to test git + 87d27a12 (Chris Barker 2017-12-09 16:18:22 -0800 5) + 4795ddf4 (Chris Barker 2017-10-31 18:59:31 -0700 6) + +So this shows me that it was changed on 12-09, and before that on 10-31. IN this case, I'm the only one that has messed with that file, so no one to shift the blame too :-) + + +.. _git_branching: + +Branching +========= + +A really quick intro to branching. + +You may want to start with this tutorial to familiarize yourself with the idea: + +https://www.atlassian.com/git/tutorials/using-branches + + +quick tutorial +-------------- + +You create a new "branch" with git with the branch command:: + + git branch the_name_of_the_branch + +where ``the_name_of_the_branch`` is the name of the branch, naturally. To see all the branches you have, you can simply do:: + + git branch + +The "current" branch or "HEAD" will be marked with an asterix. + +To switch to another branch, you can checkout the branch: + + git checkout the_name_of_the_branch + +You are now working in the new branch. Anything you commit will be comited to that branch, and no longer effect the master branch. + +IF you do a ``git push`` -- you will get a message from git telling you that the branch you are now on is not set up to push to "origin" (your giotHub repo), but it will show you the command you need to set that up -- set-upstream:: + + git push --set-upstream origin the_name_of_the_branch + +Now it will push to gitHub, and you can see it there. + +You can create Pull Requests from that new branch, as well as the old, master, branch. + +merging +------- + +When you are happy with your work in the new branch, you may want to merge it back into the "master" branch. + +Yu can do this by switching to the master branch:: + + git checkout master + +And then merging your new work into it:: + + git merge the_name_of_the_branch + +And there you go! + +There is a saying in the git world: + + "Branch early, merge often" + +It's a good way to work -- branching and merging is easy enough it git that it pays off to do it often. + +"detached HEAD" +--------------- + +Above, we talked about using ``git checkout`` to restore a file to the state it was in in a previous commit, like so:: + + git checkout 8e5908a37d7d examples/Session05/mailroom_test.py + +But what happens if you do a checkout with a commit, and no specific file? + +It does what you might expect -- puts ALL the files back the way they were at that commit. But there is a hitch ... let's see what happens when I do that:: + + $ git checkout c03bb5b2c401c + Note: checking out 'c03bb5b2c401c'. + + You are in 'detached HEAD' state. You can look around, make experimental + changes and commit them, and you can discard any commits you make in this + state without impacting any branches by performing another checkout. + + If you want to create a new branch to retain commits you create, you may + do so (now or later) by using -b with the checkout command again. Example: + + git checkout -b + + HEAD is now at c03bb5b adding print_grid from class + +So the files are set to the old state -- but now there is that note about "detached HEAD" -- this means that changes you make, even commits, will not effect the git repo. If you want to start from here and make changes that will stick, you need to do what it says, and make a new branch. But what it DOESN'T tell you is how to simply "re-attach" the HEAD. Turns out there is an easy way:: + + $ git checkout - + Previous HEAD position was c03bb5b adding print_grid from class + Switched to branch 'master' + Your branch is up to date with 'origin/master'. + +the dash means "the branch or commit you were on before your last checkout command". + +For more info about "detached HEAD", see: + +https://www.cloudbees.com/blog/git-detached-head + + + + + + diff --git a/_sources/topics/01-setting_up/git_overview.rst.txt b/_sources/topics/01-setting_up/git_overview.rst.txt new file mode 100644 index 0000000..14870ec --- /dev/null +++ b/_sources/topics/01-setting_up/git_overview.rst.txt @@ -0,0 +1,198 @@ +.. _git_overview: + +============ +git Overview +============ + +git is a very complex and powerful system. However, it can be very useful even if you only use a small portion of its functionality. This page should be bring you up to speed enough to make good use of git for the Python Certificate class. + +Note that in the certificate program we will be using git in conjunction with gitHub (in particular gitHub Classroom), a cloud-based service that provides a collaboration environment for software development based on the git version control system. + +gitHub provides a web service and web interface that hosts projects and supports collaboration among teams and the open source community. It adds other features, but is very much built on the source control system, git. So a basic understanding of git is required to make proper use of gitHub. + + +Learning Resources +================== + +* Introduction to Git and GitHub for Python Developers + + https://realpython.com/python-git-github-intro/ + +* "Official" gitHub tutorials: + +https://guides.github.com/activities/hello-world/ + +https://guides.github.com/introduction/flow/ + +* Other suggested readings: + +http://rogerdudler.github.io/git-guide/ + +https://try.github.io/levels/1/challenges/1 + +* Pro git: The complete semi-official documentation -- the first few chapters are worth going through: + +https://git-scm.com/book/en + +* git Branching: Interactive tutorial about branching -- try it right in the browser! + +http://pcottle.github.io/learnGitBranching/ + + +A Graphical Tutorial +==================== + +A Picture of git +---------------- + +.. figure:: /_static/git_simple_timeline.png + :width: 80% + :class: center + +A git repository is a set of points in time, with history showing where +you've been. + +Each point has a *name* (here *A*, *B*, *C*) that uniquely identifies it, +called a *hash*. + +Note: To those computer geeks among us -- yes, this an actual hash of ALL the files in the repo at that point in time -- so it uniquely identifies the *exact* state. That is why it's a long ugly set of seemingly random characters. But when using git, all you need to know is that it is a name that identifies that unique state. + +The path from one point to the previous is represented by the *difference* between the two points. + + +.. figure:: /_static/git_head.png + :width: 75% + :class: center + +Each point in time can also have a label that points to it. + +One of these is *HEAD*, which always points to the place in the timeline that you are currently looking at. + + +.. figure:: /_static/git_master_branch.png + :width: 75% + :class: center + +You may also be familiar with the label "master". + +This is the name that git automatically gives to the first *branch* in a repository. + +A *branch* is actually just a label for a certain set of points in time. + + +.. figure:: /_static/git_new_commit.png + :width: 75% + :class: center + +When you make a *commit* in git, you add a new point to the timeline. + +The HEAD label moves to this new point. + +So does the label for the *branch* you are on. + +A lot of terms in git are "overloaded" - used in multiple ways. For instance, the verb "commit" is the act of committing the state of your files to git -- saving that state so you can go back to it later. + +The noun "commit" is a particular state of the repository -- it has been saved and has particular name (hash) -- it is one if the points on that timeline. + + +.. figure:: /_static/git_new_branch.png + :width: 75% + :class: center + + +You can make a new *branch* with the ``branch`` command. + +This adds a new label to the current commit. + +Notice that it *does not* check out that branch -- you will still be working in the current branch. + + +.. figure:: /_static/git_checkout_branch.png + :width: 75% + :class: center + + +You can use the ``checkout`` command to switch to the new branch. + +This associates the HEAD label with the *session01* label. + +Use ``git branch`` to see which branch is *active*:: + + $ git branch + master + * session01 + + +.. figure:: /_static/git_commit_on_branch.png + :width: 75% + :class: center + +While it is checked out, new commits move the *session01* label. + +Notice that HEAD is *always* the same as "where you are now" + + +You can use this to switch between branches and make changes in isolation. + + +.. figure:: /_static/git_checkout_master.png + :width: 75% + :class: center + +.. figure:: /_static/git_new_commit_on_master.png + :width: 75% + :class: center + + +Branching allows you to keep related sets of work separate from each-other. + +In our lessons, you can use it to do each of your exercises. + +Simply create a new branch for each session from your repository master +branch. + +Do your work on that branch, and then you can issue a **pull request** in +github to have your work evaluated. + +This is very much like how teams work in the "real world" so learning it +here will help you. + +The final step in the process is merging your work. + + +The ``merge`` command allows you to *combine* your work on one branch with the +work on another. + + +It creates a new commit which reconciles the differences: + +.. figure:: /_static/git_merge_commit.png + :width: 75% + :class: center + +Notice that this commit has **two** parents. + + +Sometimes when you ``merge`` two branches, you get *conflicts*. + +This happens when the same file was changed in about the same place in two different ways. + +Often, git can work these types of things out on its own, but if not, you'll need to manually edit files to fix the problem. + +You'll be helped by the fact that git will tell you which files are in conflict. + +Just open those files and look for conflict markers: + + * <<<<<<<<< *hash1* (stuff from the current branch) + * ========= (the pivot point between two branches' content) + * >>>>>>>>> *hash2* (stuff from the branch being merged) + + +Your job in fixing a conflict is to decide exactly what to keep. + +You can (and should) communicate with others on your team when doing this. + +Always remember to remove the conflict markers too. They are not syntactic code in any language and will cause errors. + +Once a conflict is resolved, you can ``git add`` the file back and then commit the merge. + diff --git a/_sources/topics/01-setting_up/git_workflow.rst.txt b/_sources/topics/01-setting_up/git_workflow.rst.txt new file mode 100644 index 0000000..763dead --- /dev/null +++ b/_sources/topics/01-setting_up/git_workflow.rst.txt @@ -0,0 +1,207 @@ +:orphan: + +.. NOTE: This is the "old" version, before we adopted gitHub Classroom +.. It is not currently published. + +.. _git_workflow: + +git Workflow +============ + +Git is a very flexible system that can be used in a lot of different ways to manage code development. This page describes the workflow we are using for this class -- about as simple a workflow as you can have with git. + +We start with an overview of the usual process. This overview may be all you need for future work, once you have set up git on your workstation. + +The instructions following the overview are very explicit for those new to git and the command line. + +The usual process +----------------- + +This is the usual series of steps you will want to go through when you do a new project / assignment. + + +First make sure you are on the command line "in" your copy of the class repo. + +Remember that ``git status`` is your friend -- when in doubt, run that command to see what's going on in your repo. + +1. Make sure you are on the correct branch -- though if you never branch, you'll only need to do this once: + + ``$ git checkout master`` + +2. Get any changes from class repository -- good to do this whenever you start on something new + + ``$ git pull upstream master`` + +3. If there are changes merged in from upstream, you want to push them to your repository on GitHub. + + ``$ git push`` + +4. Make sure you are in your student directory, do some work -- create new files, edit then, etc. Verify you are happy with changes. Check the status of your repo: + + ``$ git status`` + + Make sure you add any new files: + + ``$ git add the_name_of_new_file`` + + you may want to do something like ``git add *.py`` if there are multiple files to add. + +5. Commit the all the changes -- make sure to add a good commit message. + + ``$ git commit -m 'my message here'`` + +6. Push your changes to your remote github account. + + ``$ git push`` + +7. If your are ready to submit your work, make a pull request on the gitHub website. + +Note that when you are working, you may want to do steps 2-6 far more often than step 7. (Don't go too crazy here, we don't want you to wait until the end of the quarter to get to step 7. ;-)) + +Now put that to work to get you set up for class: + +Required Initial Setup +----------------------- + +The first step in getting ready for the class is to create an individual directory for yourself inside the class repository, initiated with a README file. This step is necessary to ensure you have everything setup correctly and understand the process for future assignment submissions. Your instructor can review the workflow and give you early feedback before you start working on your first assignment submission. + +When you start a new class project or exercise, you should create a folder within this folder for that particular project (ex. lesson1). You should only ever add things inside your OWN directory -- don't add or change anything anywhere else. + +Note that when you start doing projects on your own (outside of classwork), you will want to create a whole new repository for each project. + +Create Your Own Working Directory +................................. + +The first step is to ``cd`` to the students directory: + +``$ cd students`` + +Then create a directory for yourself. You can use your first name, your gitHub handle (username), or any nickname you like -- just make sure your instructor knows who you are so you can get credit for your work. + +``$ mkdir marie_curie`` + +Switch into that dir: + +``$ cd marie_curie`` + +Adding a new file +................. + +Note that git does not track directories -- you do not have to add a new dir to git -- when you add a new file in that dir, git will track where it is. + +Now you can do your coding. For this example, that is simply adding a readme file. You can do that with your text editor, or directly on the command line:: + + cat > README.rst + Python code for UWPCE-PythonCert class, written by Marie Curie + ctrl+D + +Now tell git to track that new file: + +``git add README.rst`` + +Once you are done coding, always a good idea to look at what you have done. + +``$ git status`` + +Carefully observe new files or files that you have changed to ensure no other files are being committed outside of your student directory. + +Committing your changes +....................... + +Commit the changes with a summary of what you have done: + +``$ git commit -a -m 'added a readme file'`` + +Push your changes to your repo on gitHub: + +``$ git push origin master`` + +"origin" is the default name given by git referring to the server you cloned (in this case your github repository) + +"master" is the branch that you are currently pushing to that server. + +Since these are the default, you can usually simply do: + +``git push`` + +Make a PR +......... + +In high level overview, pull request provides a view to see the difference between a source branch (your fork) and a target branch (the main class repo), this view is used for code reviews and to provide feedback to the author. Keep in mind that this view is not static, meaning any subsequent commits to the source branch will show in this diff view. + +Now go onto GitHub, and make your first pull request (PR)! + +Here is some gitHub help for that: + +https://help.github.com/articles/creating-a-pull-request-from-a-fork/ + +You've pushed your own changes to that fork, and then issued pull requests to have that work merged back to the main class repo in (UWPCE-PythonCert-ClassRepos). An instructor will look at your code, make comments and approve your pull request if your work is satisfactory. + +Do that now with just the README file, so we can get the class repo all set up, and so that both you and your instructors know you have your gitHub repo all set up correctly. + +Starting a new Exercise +----------------------- + +Once you have created your directory, and are starting a new project, the process will look very much the same. This example is for marie_curie working on her mailroom exercise: + +Make sure you are "in" your copy of the class repo on your machine: + +``$ cd students/marie_curie`` + +Regardless of what you are working on, first make sure you don't have anything in your repository that you forgot to commit: + +``$ git status`` + +Note that when git status tells you that 'Your branch is up-to-date with 'origin/master', that does NOT mean that you are up-to-date with stuff that has been pushed to the github repository, only, confusingly, with what your local machine currently knows about. + +So, your next step is to make sure you have any changes that other people have made recently to the *remote* repository. + +``$ git pull upstream master`` + +"upstream" is the name we gave to the repository as it sits in the UWPCE github site. If you get an error message, check with the :ref:`git` documentation to make sure you set up the upstream shortcut correctly. + +"master" is the branch that you are currently pulling from that server, for the purpose of this class, we will always use master. + +If there are changes upstream that you did not have, it is a good idea to go ahead and push these changes to your github account right away so they don't confuse things: + +``$ git push`` + +Now you can begin your work: + +create a dir to do the Exercise in: + +``$ mkdir mailroom`` + +(remember to make sure you are creating this new dir in *your own working directory*) + +Create your new python file(s) in that new directory. Then add it to git before you start writing any real code -- just to make sure you don't forget: + +``$ git add mailroom.py`` + +Then as you work, each time you get to a good saving point, make a commit: + +``git commit -a -m "added the donation listing feature"`` + +And when you are done, push it to gitHub: + +``$ git push`` + +If you are ready for an instructor to review it, go to your repo on the gitHub website and make a pull request. + +Final Thoughts +-------------- + +We are using gitHub to submit and review your work because it provides a nice interface for code review. But more importantly, because the git revision control system, and the gitHub collaborative code development platform are industry standard tools for developing code. + +Learning git is a great skill -- we are only requiring the very basics for this class, but do take the opportunity to explore git a bit more -- making branches, reverting to older versions, etc. + +Also -- by doing it this way, you are getting an automatic back up of your work. Each time you "push", a copy of your work is getting backed up on gitHub. And you can also use it to coordinate your work among multiple computers -- you can have as many clones of your repo on gitHub as you like -- say one on a computer at work, and one at home. If you push a change from one computer, then running: + +``$ git pull`` + +on the other will bring that change down. This makes it really easy to do your classwork (or any work) in multiple places. + + + + + diff --git a/_sources/topics/01-setting_up/github_classroom.rst.txt b/_sources/topics/01-setting_up/github_classroom.rst.txt new file mode 100644 index 0000000..36274ee --- /dev/null +++ b/_sources/topics/01-setting_up/github_classroom.rst.txt @@ -0,0 +1,476 @@ +.. _gitHub_classroom: + +############################# +Working with gitHub Classroom +############################# + +The Python Certificate program uses `gitHub Classroom `_ to manage the submission and review of your coding assignments. + + +Why gitHub Classroom? +===================== + +A software development project is all about continuous improvement: + +1. An opportunity is identified. + +2. Some initial code is written to address that opportunity. + +3. Feedback is provided for that code. + +4. The code is modified to create that feedback. + +5. A final version of the code is released. + +Steps 3-4 will be repeated multiple times until the development team (which could even be a single developer) deems it is ready for release. + +In this course, you will not only learn about Python but also about the development process that most Python projects (and virtually any other programming language) go through. gitHub Classroom allows for the steps indicated above to be completed in an academic environment. + +In short: you will be using real professional tools and workflow when doing the work for this program. Be patient -- it is a lot to learn, but the goal is for you to learn useful skills, not to complete the coursework as easily as possible. + + +Initial Setup +============= + +You will need an account on gitHub to participate in this course. +If you don't already have a gitHub account, or if you would prefer to create a new one for this course, make sure you setup a new account on `gitHub `_. +Always keep in mind that your account name will be part of the private repositories that will be created for each of your assignments and it will be visible to both your instructors and your classmates. +Make sure you let your instructors know what your gitHub handle is -- it's not always obvious! + +You will need to have git setup on the computer you will use for developing your code for this course. +You can find instructions for setting up git (and the rest of your development environment) here: + +:ref:`setup_details` + +Once you have all the tools set up, you will need to create a folder (directory) within your development system for keeping your work. +This is the folder where all your assignment repositories will reside. It will be helpful to keep them all together in one place. +This folder (directory) should be somewhere in your "user" or "home" directory. + + +Accepting an Assignment +----------------------- + +On each assignment page in your LMS (Canvas or Edx), there should be a link to the assignment on gitHub classroom. Click on this link, and it should take you to gitHub classroom, and allow you to "Accept this Assignment". + + +Possible Confusions: +.................... + +The first time you accept an assignment, gitHub will "invite" you to join the class organization. You will need to click the link to accept the invitation. Once you have done that the first time, you shouldn't need to do it again. However, gitHub seems to get confused, and may continue to tell you about the invitation. You can ignore it if the invitation is working. + + +When accepting the assignment, gitHub will take a little while to set it up. after waiting a minute or two, you can refresh the browser window, and you should get a link to your assignment repo. If you get an error you can do what it suggests, and go back and try to accept the assignment again. It usually works after a try or two. + + +Some Things to Consider +....................... + +* You will need to accept each assignment separately. + +* Accepting an assignment will trigger the creation of a new gitHub repository for you to work on your assignment. That repository will be in your gitHub account. By default it is "private", so that only you and the instructors will be able to see it. + +* This repository is only assigned to you. + +* Any work you do there will not affect the work of your classmates. + +* The name of the new repository will include your gitHub user name at the end. + +Once your repository has been created, go to its link (provided by gitHub) and clone it on your development system, under the folder you selected for this purpose. + +Here: `Cloning a repo `_ +is gitHub's official guide on how to clone a repository. + +.. _gitHub_classroom_workflow: + +gitHub Classroom Assignment Workflow +==================================== + +The following is the workflow you will need to follow for each individual assignment. + + +1) Accept the Assignment +------------------------ + +The first step is to click on the link for the assignment in your LMS -- that will take you to gitHub classroom, where you can accept the assignment. + +When you accept, gitHub will create a new repository for the assignment in your gitHub account. + + +2) Clone the Repo +----------------- + +Once the repository has been created on gitHub, you need to make a copy, or "clone" of it on your local workstation, where you will be writing your code. + + +a) Click on the "Code" button in gitHub: + +.. image:: images/gitHubclassroom/code_button.png +.. :width: 600 + +b) Copy the "https: " url that shows up -- you can click the little clipboard icon to copy -- or highlight and copy the url + +.. image:: images/gitHubclassroom/clone_url.png +.. :width: 600 + +c) Go to your command line in the terminal application (Terminal, git bash, CMD prompt, etc). Make sure you are "in" the directory that you have set up for this class. ``ls``, ``dir`` and ``pwd`` can be helpful to make sure. + +d) Clone the repo + +:: + + git clone https://github.com/UWPCE-Py310-Fall2020/the_assignment_url.git + +(you should be able to type ``git clone`` and then paste the url you copied from gitHub) + +This will create a new directory for the repository, named by the assignment and your gitHub handle -- this is where you will put all the work for that assignment. + + +3) Create a develop branch for your work +---------------------------------------- + +Create and check out a new branch for your work. + +a) Change the working directory to the repo just created by the clone: + +:: + + cd the_name_of_the_assignment_repo + +b) Make a new branch: + +:: + + git checkout -b develop + +After that command, git will be "in" the develop branch -- anything you change will only be reflected in that branch. + +.. note:: A git "branch" is an independent "version" of your code where you can write and change code, create and delete files, etc, and it will be kept separate from the main code. When you are happy with this version, it can be merged into the main branch. For the purposed of this course, it will not be merged into the main branch until it has been reviewed, and both you and your instructors think it's done. + +If you get an error from this command that says:: + + fatal: A branch named 'develop' already exists + +That means one of two things: + + 1) You have already created a develop branch. In which case you should already be using it, or you can "check it out" again: `git checkout develop` + +or + + 2) That branch was created already by gitHub classroom. Which you'd think would be nice, but it turns out that the way it's created doesn't allow the next steps: the Pull Request. The solution in this case is to use a different name for your working branch, e.g. + +:: + + git checkout -b develop2 + +Then be sure to use the "develop2" branch when you make your PR to accept the assignment (see step 7). It doesn't really matter what you call this branch, as long as it's a new one you create. + +c) Check the git status + +:: + + $ git status + On branch develop + nothing to commit, working tree clean + +That lets you know that you are on the develop branch, and that you haven't made any changes to your files (the "working tree" is the dir and files on your machine) + +4) Start the Assignment +----------------------- + +a) Add some files. Create a new file or files for the assignment with your text editor. Once they are there, it's a good idea to add them before you do much work on them, but you can add them at any time. + +:: + + git add a_new_file.py + +You can also add all the files in the directory with:: + + git add . + +But be careful -- only do that if you really want everything added to git. + +b) Commit your work. When you have gotten to a good "pause point" in your work: the first feature works, you need help from the instructors, etc, you can "commit" the current state of your project. It's a good idea to check the status first. + +:: + + $ git status + On branch develop + Changes to be committed: + (use "git restore --staged ..." to unstage) + new file: a_simple_script.py + new file: another_file.py + new file: install_test.py + + Changes not staged for commit: + (use "git add ..." to update what will be committed) + (use "git restore ..." to discard changes in working directory) + modified: install_test.py + +Note that in this case, I edited the ``install_test.py`` file after adding it. When you edit a file, git will not track those changes unless you tell it to, which you can do by running ``git add`` again. So ``git add`` tells git that you want it to keep track of that file -- called "staging for commit":: + + $ git add install_test.py + + $ git status + On branch develop + Changes to be committed: + (use "git restore --staged ..." to unstage) + new file: a_simple_script.py + new file: another_file.py + new file: install_test.py + +(there is no harm done running ``git add`` any number of times on the same file) + +Note that after adding the file (again!) it is now ready to be committed:: + + git commit -m "adding the initial files" + +``-m`` means "message" -- you always need to provide a commit message. + +There is a trick to save a step -- you can ask git to commit all changes you've made since the last commit:: + + $ git commit -a -m "initial files added" + + [develop 4985f9d] initial files added + 3 files changed, 17 insertions(+) + create mode 100644 a_simple_script.py + create mode 100644 another_file.py + create mode 100644 install_test.py + +The ``-a`` means "all". Note that you still need to use ``git add`` to ask git to track a new file that it is not already managing. And be sure to run ``git status`` first to make sure you haven't accidentally added things you didn't want to. + +5) Push your work to gitHub +--------------------------- + +All this adding and committing has only affected the repository on your own machine -- the one on gitHub has not been changed. +In order to get your changes up to gitHub you need to "push" them. It's always a good idea to check the status before you push -- to make sure you're ready. + +:: + + $ git status + On branch develop + nothing to commit, working tree clean + +Note that I am on the "develop" branch, which is what's wanted, and nothing new to commit -- all my changes have been committed -- it's time to push. + +:: + + $ git push + fatal: The current branch develop has no upstream branch. + To push the current branch and set the remote as upstream, use + + git push --set-upstream origin develop + +Hmm -- **fatal** -- I don't like the look of that! +But it's pretty simple, really. git is telling you that it doesn't know where to push the code to -- your gitHub version of the repo doesn't have a develop branch. But it tells you want to do to create that branch on gitHub (origin), so do that: + +:: + + $ git push --set-upstream origin develop + Enumerating objects: 4, done. + Counting objects: 100% (4/4), done. + Delta compression using up to 4 threads + Compressing objects: 100% (3/3), done. + Writing objects: 100% (3/3), 639 bytes | 319.00 KiB/s, done. + Total 3 (delta 0), reused 0 (delta 0) + remote: + remote: Create a pull request for 'develop' on gitHub by visiting: + remote: https://github.com/UWPCE-Py310-Fall2020/initial-setup-PythonCHB/pull/new/develop + remote: + To https://github.com/UWPCE-Py310-Fall2020/initial-setup-PythonCHB.git + * [new branch] develop -> develop + Branch 'develop' set up to track remote branch 'develop' from 'origin'. + +Good -- now the local develop branch is hooked up to a develop branch on gitHub. And it even tells you what to do next -- see the "Create a pull request for 'develop' on gitHub by visiting:" -- that's exactly what you need to do! + +6) Complete the Assignment +-------------------------- + +Now it's time to write your code! As you work on it, make commits as you go along. Making a commit is essentially saving the state of your project -- so do it at each good "break point" -- when you have a feature working, or have fixed a bug. Do a ``git push`` every once in a while, to save your work to gitHub. + +.. note:: One of the really nice things about using gitHub for this (and your own work) is that now your work is all "in the cloud" -- you can make a clone on any other machine (say one at home and one at work), do work on that machine, push it to gitHub, and then retrieve it from somewhere else. If you want to get changes from gitHub that you don't have locally, you need to "pull" them (opposite of push): ``git pull`` should do it. + +7) Make a Pull Request +---------------------- + +When you are done with the assignment, or are at a state where you need some help, it's time to make a Pull Request (PR). +A PR is a request to "pull" the code you've just written into another branch -- usually the main branch. +In "real" development, this means that you have added a feature or fixed a bug, and want that code to be deployed. +But if you are not the primary developer, or if you work on a team, then the code may need to be reviewed before it's merged into the main branch. +For this class, we are mimicking that workflow, but it is the instructors that will review your code. When the code has been reviewed, we will "Merge" the PR into main, indicating that you have completed the assignment. + +You should make the PR when you have finished the assignment, or when you are stuck and need some help. In essence, the PR is a request for review. + +Go to the assignment gitHub repo in your browser. It should have a note that you have pushed a develop branch, and a button to click to create a PR: + +.. image:: images/gitHubclassroom/compare_and_pr.png + +Click the "compare and pull request" button to start making your PR! + +After you click that -- scroll down and you can see what has changed -- it will show you the files added or removed, and the individual lines that have changed in each file. Review that, to make sure the changes are what you expect. + +.. image:: images/gitHubclassroom/make_pr.png + +If so -- put a message in the "leave a comment box", and click "Create Pull Request". + +Note that this message is where you can start communicating with the instructors -- it should let them know why you are making the PR. +If you are all done with the assignment, say so. +If you are partially done, but have a question -- put your question in this comment box. + +Once you create the PR, gitHub will show you the PR view: + +.. image:: images/gitHubclassroom/pr_header.png + +This is the same view that your instructors will see. +If you click on the "conversation" tab, you can see your initial comment and any comments made after the initial PR creation. + +If you click on the "files changed" tab, you will see all the changes in this PR. For this class, that should be your entire assignment. + +Put a link to the PR in the LMS, to let us know that you have "turned in" the assignment. + +8) Wait for review +------------------ + +Once you make your PR, your instructors will be notified by gitHub (and the LMS), and will review your code. They can make general comments, or comment line by line. When a review is done, you should get an email from gitHub. But you can always go and check the PR yourself and see if anything new is there. + +At this point, two things might happen. + +* If the work is complete and well done, your instructors will make comments, and merge the PR. This is an indication that you are done. + +* If there is still more room for improvement, then your instructors will leave the PR open, and wait for you to push more changes. + + +14) Update your Code +-------------------- + +If the instructors request a change, or you just want to improve the code, you can make those changes, commit them, and push them to gitHub. +As long as the PR remains open, any new changes you push to the develop branch will show up in the PR. +Please ping your instructors if you have something new to review, by "tagging" them in a PR comment. +(you need to use their gitHub handle to tag them -- make sure you know what it is. +You can figure out what it is, because they will have been commenting on your PRs). You tag with a ``@`` symbol, like so: + +:: + + @PythonCHB: I've fixed that issue. Please review again. And I'm a little unclear on line 64 -- why doesn't ``name.upper()`` change the name? + +15) After the merge +------------------- + +When the assignment is complete and reviewed, your instructors will merge the PR. Then all that code will be in the "main" branch. If you do a ``git pull`` on your machine, and check out the main branch (``git checkout main``) you will see it there. + +16) Want to improve it after it's been accepted? +------------------------------------------------ + +If your instructors approve your code, and merge the PR, but you still want to work on it, do that work in the develop branch, and then push and make a new PR. + + +.. _gitHub_classroom_workflow_summary: + +Workflow Summary +================ + +I'm sure this seems like a lot, but it will get to be a habit, Here are the steps for each assignment: + + 1) Accept the assignment from the gitHub classroom link + + 2) Clone the resulting repo onto your work machine (``git clone``) + + 3) Make a develop branch (``git checkout -b develop``) + + 4) Do the assignment in the develop branch, committing and pushing as you go. (``git add``; ``git commit -a -m "a message"``; ``git push``) + + 5) When complete or when you would like some help, make a PR on gitHub, and post a link to the PR in the LMS (Canvas or EdX) + + 6) Read and respond to the comments on gitHub from your instructors + + 7) Continue working, committing and pushing changes as you go. + + 8) When the PR is accepted -- you are done! + +Is that so bad? + +Remember: this seems like a lot -- but it *does* reflect he real workflow when doing real coding. Even if you work alone, a version control system is a really good idea. + + +General Advice for working with git and gitHub +============================================== + +Committing your code +-------------------- + +A "commit" is snapshot of your code (and any other files included in your project). +You are encouraged to make frequent commits, as this will make it easier for you to restore your code to an earlier state if things go wrong. + + +Creating a New Commit: +---------------------- + +Type the following to add all files and subdirectories in the folder to your commit (note the command includes a dot, make sure you include it as well: the dot means "the current working directory"):: + + git add . + +.. note:: Using the "." (dot) can be a bit dangerous, as it will add everything in that directory! It's usually a bit safer to specifically add the file(s) you want to add: ``git add some_code.py`` + +After adding the file(s), you can commit your code by typing the following:: + + git commit -m "Commit message" + +Note that the commit message should be replaced with something descriptive of what that commit includes ("added new functionality", "fixed floating point error", "ready for review", etc.) that will later help you remember what that particular commit was about. + +.. note:: If you omit the message, git will bring up a text editor to let you write one. If you have not configured git to use another editor, it will be "vi", a venerable old Unix editor that is a real challenge for some. To get out of vi, hit the key, the a colon and an x: ``:x``. You can configure git to use an editor you are familiar with. See: :ref:`install_nano_win` for how to do that on Windows. + +After every change to the file, you will need to "commit" the changes. Keep in mind that git will not commit all the changes you have made, only the ones that are "staged for commit". You can stage them with the ``git add`` command again. So ``add`` means either "add this file" or "stage this file for committing", depending on whether it's already been added or not. + +Alternatively, you can tell git to commit any changes you have made, since the last commit, with the "-a" (all) flag:: + + git commit -a -m "your message" + + +You can always know what state git is in by using the "git status" command:: + + git status + +It's a good idea to do that before committing, so you know what will happen. + + +Pushing Your Code +----------------- + +"Pushing" refers to the process of synchronizing the commits you have made on your development system with your gitHub repository. +This is an important process, since it is needed before you can submit your code for review. +Also, it makes a copy of your code in your gitHub account that you can later use to restore it if your local development system fails, or access it from another system. + +You can push your code immediately after every commit or do it once a day (in which case, several commits will be included in a single push). To do it, simply type:: + + git push + +The first time you push your code to a repository, gitHub may ask you to select the remote repository (i.e., your gitHub repository). Just copy the suggested push command (you will only need to do this once per assignment). + +git will also ask you for your gitHub username and password the first time -- it should remember them after that -- until you try on a new machine. + +Asking Coding Questions +======================= + +While working on your code, you might run into a situation in which you would like one of the instructors to look at it and provide some feedback before actually reviewing and grading it. +In order to do that, go to PR you've created and write a comment about your question or issue. You should make sure to tag your instructor in your comment, to assure that they are notified of your comment. This is done by writing `@the_instructors_gitHub_handle`, e.g. `@natasha-aleksandrova`. + +For example:: + + @natasha-aleksandrova: I need some help on line 20 + +When you submit a comment with a tag, the instructor will be notified by gitHub and will be able to review your question. + + +Submitting your assignment +-------------------------- + +Once your assignment is ready for review, copy the link of your Feedback Pull Request and submit it in the submission form. Here is an example of a submission link (yours will look a little different but will end with `/pull/1`):: + + https://github.com/UWPCE-Py210-SelfPaced-2021/lesson-02-fizzbuzz-exercise-uw-test-student-natasha/pull/1 + + +Resubmitting your Assignment +============================ + +On occasion, your instructor will provide feedback on elements in your assignment that need to be modified in order to get the full grade for the assignment. In those cases, follow the process outlined in the Asking Coding Questions section above. Let us know that you would like another review for grade adjustment and make sure to tag your instructor. + +Happy coding! diff --git a/_sources/topics/01-setting_up/index.rst.txt b/_sources/topics/01-setting_up/index.rst.txt new file mode 100644 index 0000000..698bfca --- /dev/null +++ b/_sources/topics/01-setting_up/index.rst.txt @@ -0,0 +1,29 @@ + .. _setting_up_dev_environment: + +Setting up your Environment +=========================== + +Before you can begin programming in Python, or work on this program, +you need to have your computer all set up with the tools you need to do the job. + +Here are some resources to getting yourself set up. + +.. toctree:: + :maxdepth: 1 + + environment_overview + python_and_core_tools + setup_details + advanced_setup + git + resources + +.. toctree:: + :caption: Activities + :maxdepth: 1 + + testing_your_setup + ../../exercises/python_pushups + + + diff --git a/_sources/topics/01-setting_up/intro_to_git.rst.txt b/_sources/topics/01-setting_up/intro_to_git.rst.txt new file mode 100644 index 0000000..60bd592 --- /dev/null +++ b/_sources/topics/01-setting_up/intro_to_git.rst.txt @@ -0,0 +1,152 @@ +.. _git: + +############ +Intro to Git +############ + +What is Git? +------------ + +A "version control system" + +Why Version Control? +-------------------- + +.. figure:: http://phdcomics.com/comics/archive/phd101212s.gif + + "Piled Higher and Deeper" by Jorge Cham: www.phdcomics.com + +A history of everything everyone does to 'your' code + +A graph of "states" in which the code has existed + +That last one is a bit tricky, and is not necessary to understand right out of the gate. When you are ready, you can look at this supplement to gain a better understanding: + +:ref:`git_overview` + +There are other versioning systems, such as Mercurial and Subversion (and commercial offerings), but Git is the most popular. + +It is incredibly important to learn and understand version control to work as a developer today, so we have incorporated Git into our work flow for submitting students' work in this class. + + +Setting up Git +-------------- + +You should have git installed on your machine and accessible from the command line. If you don't have git working on the command line, revisit the appropriate instructions for your platform here: :ref:`installing_python`. + +Once git is installed and working, there is a little bit of setup for git that you should only have to do once: + +Letting git know your identity +.............................. + +.. code-block:: bash + + $ git config --global user.name "Marie Curie" + $ git config --global user.email "marie@radioactive.com" + +(using your email and name, of course) + +Editor +...... + +* git needs an editor occasionally +* default is VI, which is not very intuitive to non-Unix Geeks +* Nano is simple, easy solution for Macs and Linux +* Nano no longer available for windows, use Sublime Text or Notepad++ or Atom + +For windows users: :ref:`install_nano_win` + +Once you have chosen/installed an editor, configure git to use it: + +(full notes here: `GitHub help on Editors `_) + +**nano:** + +``$ git config --global core.editor "nano -w"`` + +**Sublime Text (mac):** + +``$ git config --global core.editor "subl -n -w"`` + +**Sublime Text(win):** + +``$ git config --global core.editor "'c:/program files/sublime text 2/sublime_text.exe' -w"`` + +**Notepad++ (Win):** + +``$ git config --global core.editor "'c:/program files (x86)/Notepad++/notepad++.exe' -multiInst -notabbar -nosession -noPlugin"`` + +**Atom:** + +``git config --global core.editor "atom --wait"`` + +Repositories +------------ + +A repository is just a collection of files that 'belong together'. + +Since ``git`` is a *distributed* versioning system, there is no **primary** +repository that serves as the one to rule them all. This simply means that all repositories on each users machine should look the same. + +However, to keep things sane, there is generally one "central" repository chosen that users check with for changes. For us this is the one hosted on GitHub in the UWPCE-PythonCert-ClassRepos organization. + + +.. Working with Remotes +.. -------------------- + +.. With git, you work with *local* repositories and the *remotes* that they are connected to. + +.. Git uses shortcuts to address *remotes*. When you *clone* a repository from its remote location to your local machine, you get an *origin* shortcut for free: + +.. .. code-block:: bash + +.. $ git remote -v +.. origin https://github.com/UWPCE-PythonCert-ClassRepos/ExampleRepo.git (fetch) +.. origin https://github.com/UWPCE-PythonCert-ClassRepos/ExampleRepo.git (push) + +.. This shows that the local repo on my machine *originated* from one in +.. the UWPCE-PythonCert-ClassRepos GitHub account (it shows up twice, because there is a shortcut for both ``fetch`` from and ``push`` to this remote). + +GitHub forks +------------ + +You can work on any project you wish to that has a public repository on GitHub. However, since you won't have permission to edit most projects directly, there is such a thing as *forking* a project. + +When you *fork* a repository, you make a copy of that repository in your own (GitHub) account. + +When you have made changes that you believe the rest of the community will want to adopt, you make a *pull request* to the original project. The maintainer(s) of that project than have the option of accepting your changes, in which case your changes will become part of that project. + +For this class, we are using gitHub classroom -- whihc does the creating and forkin go repos for you, so you proably dont need to use this feature now. + +With one exception: your instructor may use a gitHub repository to manage note3s, examples, and solutions for the class -- if so, it may be helpful to make a fork of that repo, particularly if you want to make suggestions etc. + +Another possiblity is if you notice an error, or can suggest a claification in these very pages. They are managed on gitUb as well, in this repo: + +https://github.com/UWPCE-PythonCert/ProgrammingInPython + +So you may want to fork that repo in order to make suggestions. + + +Structure of multiple git repos +------------------------------- + +Each repository will have a directory called ``.git`` that is normally +not seen. This directory is how git keeps track of everything. Leave it alone. :) + +Please do not set up a git repository inside another git repository, this can lead to heartache. + +Absolutely, do NOT set up a git repository in your home root directory. +This will put everything in your home directory up on GitHub, and you do not want that. + +Setting up new repositories can be confusing because when you clone a git repository it creates the directory that will be the repository, but when you are creating a new repository, you need to first be **IN** the directory in which you want the repository to be rooted. Please ask if this does not make sense. + +It’s also important to note that you do not run the ``$ git init`` command at any point in the process of cloning and configuring your local copy of a remote repo. The ``init`` git command is used to initialize a git repository on your local machine and is not necessary in our case because the cloned repository has already been initialized. + +Additional Resources: + +git tutorial: +https://try.github.io/levels/1/challenges/1 + +basic git commands: +https://confluence.atlassian.com/bitbucketserver/basic-git-commands-776639767.html + diff --git a/_sources/topics/01-setting_up/ipython.rst.txt b/_sources/topics/01-setting_up/ipython.rst.txt new file mode 100644 index 0000000..9ba1855 --- /dev/null +++ b/_sources/topics/01-setting_up/ipython.rst.txt @@ -0,0 +1,77 @@ +.. _ipython_resources: + +******************* +iPython Interpreter +******************* + +iPython is an enhanced interpreter that makes interactive experimentation at the command line much more pleasant and powerful. + +The very basics of IPython +-------------------------- + +IPython can do a lot for you, but for starters, here are the key pieces +you'll want to know: + +Start it up + +.. code-block:: bash + + $ ipython + Python 3.5.0 (v3.5.0:374f501f4567, Sep 12 2015, 11:00:19) + Type "copyright", "credits" or "license" for more information. + + IPython 4.0.0 -- An enhanced Interactive Python. + ? -> Introduction and overview of IPython's features. + %quickref -> Quick reference. + help -> Python's own help system. + object? -> Details about 'object', use 'object??' for extra details. + + +This is the stuff I use every day: + +* command line recall: + + - hit the "up arrow" key + - if you have typed a bit, it will find the last command that starts the same way. + +* basic shell commands: + + - ``ls``, ``cd``, ``pwd`` + +* any shell command: + + - ``! the_shell_command`` + +* pasting from the clipboard: + + - ``%paste`` (this keeps whitespace cleaner for you) + +* getting help: + + - ``something?`` + +* tab completion: + + - ``something.`` + +* running a python file: + + - ``run the_name_of_the_file.py`` + + +That's it -- you can get a lot done with those. + +iPython references +------------------ + + +* **The iPython tutorial** + (http://ipython.readthedocs.io/en/stable/interactive/tutorial.html) + +* **Using IPython for interactive work** + (http://ipython.readthedocs.io/en/stable/interactive/index.html) + Learn about the abilities iPython provides for interactive sessions. + +* **The iPython Documentation** + (http://ipython.readthedocs.io/en/stable/) + Use this to learn more about iPython's amazing capabilities. diff --git a/_sources/topics/01-setting_up/python_and_core_tools.rst.txt b/_sources/topics/01-setting_up/python_and_core_tools.rst.txt new file mode 100644 index 0000000..ff53c41 --- /dev/null +++ b/_sources/topics/01-setting_up/python_and_core_tools.rst.txt @@ -0,0 +1,364 @@ +.. _installing_python: + + +================================ +Installing Python and core tools +================================ + + +The following is the recommended setup for the UWPCE Python Certificate Program. It is not necessary to have exactly this same setup, but if you choose to use a different setup, we will be less able to support you if you need help. + +Minimal Setup +============= + +Although it is OK to use different tools, there are some requirements to successfully do the work the program requires: + +#. cPython version 3.8.* +#. A way to edit Python files (Programmers Text Editor) +#. A way to run your code -- command line, IDE, etc. +#. A way to use the "git" source code version control system + +You can be successful in the program as long as you have the above. If you don't already have a setup that fulfills those requirements: read on. + +If you are not sure -- you can go here to test your setup out: :ref:`testing_your_setup` -- if you get stuck, then come back here, and read on. + +Platforms +--------- + +Python is a very platform independent system, it can run on all major operating systems, including micro controllers even. It is most commonly used in production on Windows, Linux or OS-X systems. + +For this program, we feel it is best for students to work in an environment in which they are comfortable, and which they will ultimately use to do production work. + +We have included instructions for Windows, Linux and OS-X systems -- any of these are fine. + +Python Itself +------------- + +Python is a "byte compiled, interpreted" language. What this means to you is that you need a Python interpreter to run your Python code. It also means that that is all you need -- write some code, and run it with Python. That's it. + +There are a number of different Python interpreters (or run-time environments) available: + +- cPython +- PyPy +- Jython +- Iron Python +- MicroPython + +These each have their own special uses. For example, for interaction with the Java VM or Microsoft CLR, or running on micro controllers. But most production Python is run with the cPython interpreter, and most people mean cPython when they say "Python". + +For this program, you will need cPython version 3.8, installed and running so that when you type "python" at your command line, it starts up. + +cPython itself is available from a number of sources, or "distributions". We recommend the version available from `python.org`. + +Python is also available as part of the Anaconda data analysis environment, as well as a few other sources. These Python distributions will work fine for this class, but when we get to advanced topics like virtual environments, there are some differences -- and you will be responsible for adapting to these differences. + + +Your Development Environment +============================ + +There are four elements to your environment when working with Python: + +* The Command Line +* The Interpreter +* The Editor +* The source code version control system + +Some folks use an Integrated Development Environment (IDE), which combines some or all of these functions. For this class, we don't recommend using an IDE, because while it can make some things easier, it can also hide things that will be good for you to understand. But it's your call -- you should use tools you are comfortable with. + +Minimal Requirements +-------------------- + +In order to be productive in this program, you need to be able to do the following: + +* Manipulate files and write and save Python code in files. + You really, really want a "real" programmer's editor (not Notepad!) for this. + +* Run your code with Python 3.8 or higher + +* Run the iPython interactive interpreter + +* Install new packages with pip + +* Use the git source code management system (with GitHub Classroom) + +If you are not set up and comfortable with doing all that, read and follow these instructions: + +:ref:`setup_details` + +Then come back and follow the rest of this review. + + +The Command Line (CLI) +====================== + +Having some familiarity with the command line is important + +Familiarity with basic use of the command line is a prerequisite for the program, so we won't cover this much in class. If you are not comfortable with the command line, please bone up on your own. + +We have some resources here: :ref:`command_line_basics` + +We suggest running through the **CLI** tutorial at "learn code the hard way": + +`Command Line Crash Course `_ + +Or, for Linux and OS-X users: + +`Linux command line for you and me! `_ + + +Windows: +-------- + +Most of the demos in lessons will be done using the "bash" command line shell on OS-X. This is identical to the bash shell on Linux. + +Windows provides the "DOS" command line, which is OK, but pretty old and limited, or "Power Shell" -- a more modern, powerful, flexible command shell. + +If you are comfortable with either of these -- go for it. + +If not, you can use the "git Bash" shell -- which is much like the bash shell +on OS-X and Linux: :ref:`git_bash` + +Or, on Windows 10, look into the "bash shell for Windows" otherwise known as the "Linux subsystem for Windows" - - more info here: :ref:`windows_bash` + +OS-X +---- + +OS-X comes out of the box with a bash command line. You can access it by running the "Terminal" application, which you can find under: + +Applications => Utilities => Terminal.app + +Drag and Drop it into the dock for easy access. + +The Terminal app can be interfaced with the Finder, making it easy to open it up with the working dir set to the current folder in the finder: + +On The Mac, you may have a "New Terminal at Folder" right-click (or command click -- "secondary click") menu item already -- try it! If not, you can turn it on by following these instructions: + + Head into System Preferences and select Keyboard => Shortcuts => Services. Find "New Terminal at Folder" in the settings and click the box. Now, when you're in Finder, just right-click a folder and you're shown the open to open Terminal. When you do, it'll start right in the folder you're in. + +See: `launch an OS-X terminal in a folder `_ + +for more detail. + +Linux +----- + +Whether you use the KDE or GNOME Desktop (or anything else), there should be a way to open a shell from the file manager. Find it, it's very handy. + + +The Python Interpreter +====================== + +If you haven't already, install everything you need following these instructions: +:ref:`setup_details` + +Python comes with a built-in interpreter. + +You see it when you type ``python`` at the command line: + +.. code-block:: bash + + $ python + Python 3.8.2 (v3.5.2:4def2a2901a5, Jun 26 2016, 10:47:25) + [GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin + Type "help", "copyright", "credits" or "license" for more information. + >>> + +That last thing you see, ``>>>`` is the "Python prompt". + +This is where you can type code. + + +Python in the Interpreter +------------------------- + +Try it out: + +.. code-block:: python + + >>> print("hello world!") + hello world! + >>> 4 + 5 + 9 + >>> 2 ** 8 - 1 + 255 + >>> print ("one string" + " plus another") + one string plus another + >>> + +To get out of the interpreter, you can type:: + + exit() + +Or hit `ctrl+D` on Linux and OS-X or `ctrl+Z` On Windows. + + +Tools in the Interpreter +------------------------ + +When you are in the interpreter, there are a number of tools available to +you. + +There is a help system: + +.. code-block:: python + + >>> help(str) + Help on class str in module __builtin__: + + class str(basestring) + | str(object='') -> string + | + | Return a nice string representation of the object. + | If the argument is a string, the return value is the same object. + ... + +You can type ``q`` to exit the help viewer. + + +You can also use the ``dir`` builtin to find out about the attributes of a +given object: + +.. code-block:: python + + >>> bob = "this is a string" + >>> dir(bob) + ['__add__', '__class__', '__contains__', '__delattr__', + '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', + '__getitem__', '__getnewargs__', '__getslice__', '__gt__', + ... + 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', + 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', + 'zfill'] + >>> help(bob.rpartition) + +This allows you quite a bit of latitude in exploring what Python is. + +Advanced Interpreters +--------------------- + +In addition to the built-in interpreter, there are several more advanced +interpreters available to you. + +We'll be using one in this course called ``iPython`` + +Some information about iPython can be found here: :ref:`ipython_resources` + +The Editor +========== + +Typing code in an interpreter is great for exploring. + +But for anything "real", you'll want to save the work you are doing in a more permanent fashion. + +This is where an editor fits in. + +.. _editor_for_python: + +Text Editors Only +----------------- + +Any good programmer's text editor will do. + +MS Word is **not** a text editor. + +Nor is *TextEdit* on a Mac. + +``Notepad`` on Windows is a text editor -- but a poor one. + +You need a real "Programmer's Text Editor" + +A text editor saves only what it shows you, with no special formatting +characters hidden behind the scenes. + +Minimum Requirements +-------------------- + +At a minimum, your editor should have: + +* Syntax Colorization +* Automatic Indentation + +In addition, great features to add include: + +* Tab completion +* Code linting +* Jump-to-definition + +Have an editor that does all this? Feel free to use it. + +If not, we recommend "Sublime Text": + +http://www.sublimetext.com/ + +:ref:`sublime_as_ide` + +"Atom" is another good open source option. + +https://atom.io/ + +:ref:`atom_as_ide` + +"Visual Studio Code" is a relatively new cross platform offering from Microsoft -- a lot of folks seem to like it: + +:ref:`vsc_as_ide` + +And, of course, vim or Emacs on Linux, if you are familiar with one of those. + + +Why No Full IDE? +---------------- + +An IDE does not give you much that you can't get with a good editor plus a good interpreter. + +An IDE often weighs a great deal. + +Setting up IDEs to work with different projects can be challenging and time-consuming. + +Particularly when you are first learning, you don't want too much done for you. + + +Why an IDE? +----------- + +That said ... + +You may want to go get the educational edition of PyCharm: + +https://www.jetbrains.com/pycharm-edu/ + +Which a lot of people like a lot. + +VSCode from MS is apretty full featured IDE as well. + +But do make sure, when you set up and IDE, that you know what its doing when you click "run", and that it is using the version of Python that you expect. (cPython 3.8 in this case) + +Version Control System +====================== + +While not strictly necessary to develop code, it is a very, very, good idea to manage your code in a Version Control System: + +https://en.wikipedia.org/wiki/Version_control + +This is such a critical software development practice the we use it in the program for you to mange your projects and turn in assignments (via gitHub classroom), so that you can gain familiarity with the practice. + +git +--- + +git (https://en.wikipedia.org/wiki/Git) is an open-source version control system that has become an industry standard -- very widely used in both commercial and open-source development. + +We will be using git and the web service GitHub for collaboration in this program. + +Make sure you are set up to use git on your machine. If you have using a command line client, you should be able to type:: + + git --version + +and get something like this as a response:: + + git version 2.17.2 (Apple Git-81) + +Am I ready to go? +================= + +To see if you have Python ready to start class, try this: + +:ref:`testing_your_setup` diff --git a/_sources/topics/01-setting_up/python_for_linux.rst.txt b/_sources/topics/01-setting_up/python_for_linux.rst.txt new file mode 100644 index 0000000..a8ce4ef --- /dev/null +++ b/_sources/topics/01-setting_up/python_for_linux.rst.txt @@ -0,0 +1,296 @@ +.. _python_for_linux: + +########################### +Setting Up Linux for Python +########################### + + +Debian and Related Distros (Ubuntu, Linux Mint) +=============================================== + +Python +------- + +For this program, you need Python3.6.* or 3.7.* + +Debian distros already have the stable python2 and python3 releases preinstalled (`Debian Wiki `_). + +Try the following command: + +.. code-block:: bash + + $ python3 + Python 3.6.3 (default, March 26 2017, 15:33:32) + [GCC 4.9.2 on linux] + >>> + +I'm pretty sure that 18.4 (the most recent long term support release) has 3.6, if so, you are set. + +That's nice, which one is the default version? Just type ``python`` to see. It's probably python2 still: + +.. code-block:: bash + + $ python + Python 2.7.9 (default, April 2 2015, 15:33:32) + [GCC 4.9.2 on linux2] + >>> + +If you want to make ``python3.6`` the default version then add the line ``alias python=python3`` to your user's ``/home/{user}/.bashrc`` file like so: + +.. code-block:: bash + + $ # before the change + $ python + Python 2.7.9 (default, April 2 2015, 15:33:32) + [GCC 4.9.2 on linux2] + >>> + + $ echo "alias python=python3" >> ~/.bashrc + $ echo "alias pip=pip3" >> ~/.bashrc + $ echo "alias ipython=ipython3" >> ~/.bashrc + $ source ~/.bashrc + + $ # after the change + $ python + Python 3.6.3 (default, March 26 2017, 15:33:32) + [GCC 4.9.2 on linux] + >>> + +Alternatively, you can always remember to type ``python3`` whenever you want Python. + +Note: your version number may vary, but it needs to be ``3.6.*`` or ``3.7.*`` + +You may not have pip and ipython installed yet, but you will as you follow the instructions below. + +If you don't have the version you want installed then use the package manager to find and install it: + +.. code-block:: bash + + $ # search the package manager for it + $ sudo apt-cache search python | grep '^python3.7\ -' + python3.7 - Interactive high-level object-oriented language (version 3.7) + $ # install it + $ sudo apt-get install python3.7 + +(If you cant find 3.7, try 3.6 instead) + +Terminal +--------- + +Every Linux box has a terminal emulator -- find and use it. + + + +pip +--- + +``pip`` is the Python package installer. + +Many Python packages are also available directly from your distro -- but you'll get the latest and greatest if you use ``pip`` to install it instead. + +To get pip, the first option is to use your system package manager, something like: + +.. code-block:: bash + + $ sudo apt-get install python3-pip + +If that doesn't work, then try "ensure-pip": + +.. code-block:: bash + + $ python3 -m ensurepip --upgrade + + + $ python3 -m ensurepip --upgrade + +You can now use pip to install other packages. The first thing you may want to do is update pip itself: + +.. code-block:: bash + + $ python3 -m pip install --upgrade pip + +Using pip: +---------- + +To use pip to install a package, you invoke it with this command:: + + python3 -m pip install the_name_of_the_package + +Where ``python3`` is the command you use to invoke the Python you want to use (could be ``python3``) + +**NOTE:** You will frequently see advice to use pip like so:: + + $ pip install something_or_other + +Which often works, but also can invoke the *wrong* version of pip. The above command:: + + $ python3 -m pip install something_or_other + +calls Python, and tells it to run the ``pip`` module. It is exactly the same as calling pip directly, except that you are assured that you are getting the version of pip connected the version of python that you are running (in this case python3). + + +iPython +-------- + +One extra package we are going to use in class is ``iPython``:: + + $ sudo python3 -m pip install ipython + +You should now be able to run ``iPython``:: + + $ ipython3 + Python 3.6.4 () + Type "copyright", "credits" or "license" for more information. + + IPython 2.0.0 -- An enhanced Interactive Python. + ? -> Introduction and overview of IPython's features. + %quickref -> Quick reference. + help -> Python's own help system. + object? -> Details about 'object', use 'object??' for extra details. + +git +---- + +Git is likely to be there on your system already, but if not: + +.. code-block:: bash + + $ sudo apt-get install git + +================================================== +Fedora and Red Hat Related Distros (CentOS) +================================================== + +.. warning:: + + CentOS is probably the most popular distro of these related flavors. However, getting Python3 on it can be a pain. You have been warned! (but there are lots of tutorials on the web -- google "install python3 on CentOS") + + +Python +------- + +Fedora distros already have the stable python2 and python3 releases preinstalled `[2] `_. However, CentOS, the most popular distro only has the stable python2 release. Try the following commands: + +.. code-block:: bash + + [centos@ip-172-31-21-5 ~]$ python2 + Python 2.7.5 (default, Jun 17 2014, 18:11:42) + [GCC 4.8.2 20140120 (Red Hat 4.8.2-16)] on linux2 + Type "help", "copyright", "credits" or "license" for more information. + >>> + + [centos@ip-172-31-21-5 ~]$ python3 + -bash: python3: command not found + + +Let's install python3 using the package manager. Step one install "Software Collections" to help us: + +.. code-block:: bash + + $ sudo yum -y install scl-utils + +Then go to the `software collections listing `_ and click on the Python collection version you want to install. + +Probably this one: + +https://www.softwarecollections.org/en/scls/rhscl/rh-python35/ + + +Note, you also need to know which version of CentOS you are using (probably 6 or 7). For example, we care about `python version 3.5` so let's go the `.rpm` i want to install `here `_: + +.. code-block:: bash + + $ # add this package to the rpm package manager + $ sudo rpm -Uvh https://www.softwarecollections.org/repos/rhscl/rh-python34/epel-7-x86_64/noarch/rhscl-rh-python35-epel-7-x86_64.noarch.rpm + + $ # install the right python version + $ sudo yum install rh-python35 + +When you want to use python3 run this command: + +.. code-block:: bash + + [centos@ip-172-31-21-5 ~]$ scl enable rh-python35 bash + + +Terminal +--------- + +Every Linux box has a terminal emulator -- find and use it. + + +git +---- + +Git is likely to be there on your system already, but if not: + +.. code-block:: bash + + $ sudo yum install git + +pip +--- + +``pip`` is the Python package installer. + +Many Python packages are also available directly from your distro -- but you'll get the latest and greatest if you use ``pip`` to install it instead. + +In CentOS, if you used the above technique to install Python3, then it comes with pip. Try: + +.. code-block:: bash + + [centos@ip-172-31-21-5 ~]$ python -m pip -V + pip 8.1.2 from /opt/rh/rh-python35/root/usr/lib/python3.5/site-packages (python 3.5) + +Using pip: +---------- + +To use pip to install a package, you invoke it with this command:: + + python -m pip install the_name_of_the_package + +Where ``python`` is the command you use to invoke the Python you want to use (could be `python3`) + +**NOTE:** You will frequently see advice to use pip like so:: + + $ pip install something_or_other + +Which often works, but also can invoke the *wrong* version of pip. The above command:: + + $ python -m pip install something_or_other + +calls Python, and tells it to run the ``pip`` module. It is exactly the same as calling pip directly, except that you are assured that you are getting the version of pip connected the version of python that you are running. + +iPython +-------- + +One we are going to use in class is ``iPython``:: + + $ sudo pip install ipython[all] + +You should now be able to run ``iPython``:: + + $ ipython3 + Python 3.5.2 () + Type "copyright", "credits" or "license" for more information. + + IPython 5.1.0 -- An enhanced Interactive Python. + ? -> Introduction and overview of IPython's features. + %quickref -> Quick reference. + help -> Python's own help system. + object? -> Details about 'object', use 'object??' for extra details. + + +Footnotes: +========== + +Debian Wiki +=========== + +https://wiki.debian.org/Python + +Fedora Wiki +============= + +https://fedoraproject.org/wiki/Packaging:Python + diff --git a/_sources/topics/01-setting_up/python_for_mac.rst.txt b/_sources/topics/01-setting_up/python_for_mac.rst.txt new file mode 100644 index 0000000..25d53f4 --- /dev/null +++ b/_sources/topics/01-setting_up/python_for_mac.rst.txt @@ -0,0 +1,231 @@ +.. _python_for_mac: + +************************** +Setting up OS-X for Python +************************** + +================== +Getting The Tools +================== + + +OS-X comes with Python out of the box, but not the full setup you'll need for development or this class. It also doesn't have the latest version(s), and no version of Python 3. + +So we recommend installing a new version. + + +**Note**: + + +If you use ``macports`` or ``homebrew`` to manage \*nix software on your machine, feel free to use those for ``python``, ``git``, etc, as well. But make sure you have Python 3.8.* + +If not, then read on. + +Terminal +--------- + +You will need a command line terminal. The built-in "terminal" application works fine. Find it in:: + + /Applications/Utilities/Terminal + +Drag it to the dock to easily access. + +Python +------ + +While OS-X does provide Python out of the box, it tends not to have the +latest version, and you really don't want to mess with the system +installation. So we recommend installing an independent installation from +``python.org``: + +Download the latest release of Python installer from Python.org: + +https://www.python.org/downloads/ + +NOTE: As of this writting, version 3.9.0 was jsut released -- it will work fine. But we willnot be using any 3.9 features in this course, and examples will generally be in 3.8 -- so 3.8 or 3.9 is fine. + +Double click the installer and follow the prompts. The defaults are your best bet. Simple as that. + +Oddly, this does NOT install a ``python`` command, but rather a ``python3`` command. If you want to be able to simply type ``python`` and get python3, then you can add a symlink to the install. Type this at a terminal prompt: + +.. code-block:: bash + + $ cd /Library/Frameworks/Python.framework/Versions/3.8/bin + $ ln -s python3.8 python + +(replace the 3.8 above with 3.9 if you have installed 3.9) + +Or an add an alias in your shell by adding the following line:: + + alias python='python3' + +to your ``.bash_profile`` file. + +Once you have done that, you should be able to type ``python`` at the command prompt, and get something like: + +.. code-block:: bash + + $ python + Python 3.8.6 (v3.7.0:1bf9cc5093, Jun 26 2018, 23:26:24) + [Clang 6.0 (clang-600.0.57)] on darwin + Type "help", "copyright", "credits" or "license" for more information. + >>> + +This is the Python interpreter. + +Type ``ctrl+D`` to get out (or ``exit()``) + +.. note:: If all this is confusing to you -- take heart -- you will get used it it. And in the meantime, you can simply type ``python3`` when you want to run python. + +pip +--- + +``pip`` is the Python package installer. It is updated more frequently than Python itself, so once you have Python, you want to get the latest version of pip working:: + + $ python3 -m ensurepip --upgrade + +[``python`` may work too, if you set things up correctly above, but ``python3`` should always work.] + +It should download and install the latest ``pip``. Or let you know that you already have it. + +You can now use pip to install other packages. The first thing you may want to do is update pip itself: + +.. code-block:: bash + + $ python3 -m pip install --upgrade pip + +Using pip: +---------- + +To use pip to install a package, you invoke it with this command:: + + python3 -m pip install the_name_of_the_package + +Where ``python3`` is the command you use to invoke the Python you want to use. + +**NOTE:** You will frequently see advice to use pip like so:: + + $ pip install something_or_other + +This often works, but also can invoke the *wrong* version of pip. This command:: + + $ python3 -m pip install something_or_other + +calls Python, and tells it to run the ``pip`` module. It is exactly the same as calling pip directly, except that you are assured that you are getting the version of pip connected the version of Python that you are running. + +iPython +-------- + +One package we are going to use in the program from the beginning is ``iPython``. You can install it with ``pip`` like so:: + + $ python3 -m pip install ipython + +(It will install a LOT...). + +Now you should now be able to run ``iPython``: + +.. code-block:: ipython + + $ ipython + Python 3.8.6 (v3.8.6:db455296be, Sep 23 2020, 13:31:39) + Type 'copyright', 'credits' or 'license' for more information + IPython 7.18.1 -- An enhanced Interactive Python. Type '?' for help. + + In [1]: + +Which you can also get out of with ``ctrl+D`` or ``exit()`` + +git +---- + +git is a source code version control system. It is not strictly related to Python, but it (or a similar system) is a critical tool for software development in general, and it is very widely used in the Python community. +We will be using it, along with the gitHub classroom service, in the program to hand in assignments and support code review. + +You will need a git client. The gitHub GUI client may be nice; I honestly don't know. However, we will be using the command line client in class. + +There are a couple of options for a command line client. + +https://git-scm.com/download/mac + +Perhpas the easiest, particularly if you need a compiler for any other reason, is to get git as part of the XCode command line tools, You can install XCode from the App Store. But be forwarned -- it is a VERY big download: 11.2GB! + +Ifyou only need git, this has everything you need out of the box, and is a 500(!) times smaller download. + +https://sourceforge.net/projects/git-osx-installer/ + +NOTE: if you get a warning like: + +"... can't be opened because it is from an untrusted developer" + +you'll need to go to system preferences: + + "Security and Privacy" + +Depending on the OS-X version, you will need to check the box saying "Open Anyway," or perhaps the box saying you can install untrusted packages. + +After either of these is installed, the ``git`` command should work: + +.. code-block:: bash + + $ git --version + git version 2.24.3 (Apple Git-128) + + +Testing it out +-------------- + +To be ready for this course, you need to have, all available from the command line: + - python + - pip + - iPython + - git + +To try it out, you should be able to run all of the following commands, and get something like the results shown: + +(recall that you can get out of the Python or iPython command lines with ``ctrl+D`` or ``exit()`` + +For Python: +........... + +.. code-block:: bash + + $ python3 + Python 3.8.6 (v3.8.6:db455296be, Sep 23 2020, 13:31:39) + [Clang 6.0 (clang-600.0.57)] on darwin + Type "help", "copyright", "credits" or "license" for more information. + >>> + + +For iPython: +............ + +.. code-block:: bash + + $ ipython + Python 3.8.6 (v3.8.6:db455296be, Sep 23 2020, 13:31:39) + Type 'copyright', 'credits' or 'license' for more information + IPython 7.18.1 -- An enhanced Interactive Python. Type '?' for help. + + In [1]: + +For pip: +........ + +.. code-block:: bash + + $ python3 -m pip --version + pip 20.2.3 from /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/pip (python 3.8) + +Note that when you ask pip for ``--version`` it tells you which version of python it is "connected" to. +Make sure that's the one you expect! + +For git: +........ + +.. code-block:: bash + + $ git --version + git version 2.24.3 (Apple Git-128) + + +If those commands all run -- you are all set! diff --git a/_sources/topics/01-setting_up/python_for_windows.rst.txt b/_sources/topics/01-setting_up/python_for_windows.rst.txt new file mode 100644 index 0000000..1dca6fd --- /dev/null +++ b/_sources/topics/01-setting_up/python_for_windows.rst.txt @@ -0,0 +1,233 @@ +.. _python_for_windows: + +############################# +Setting up Windows for Python +############################# + +Getting The Tools +================== + +Python +------- + +There are a number of Python distributions available -- many designed for easier support of scientific programming: + +- Anaconda +- Enthought Canopy +- Python(x,y) +- etc.... + +But for basic use, the installer from python.org is the way to go, and that is what we will be using in this program. + +https://www.python.org/downloads/ + +You want the installer for Python 3.7 -- probably 64 bit, though if you have a 32 bit system, you can get that. + +There is essentially no difference for the purposes of this course. + +Double click and install. + +Ensure that the Install launcher for all users (recommended) and the Add Python 3.7 to PATH checkboxes at the bottom are checked. + +**Add Python 3.7 to PATH step is important!** If this is not checked then when you try to run your python code it won't be able to find the executable. + +See: `Installing Python on Windows `_ + +.. _git_bash: + +Terminal +--------- + +If you are confident in your use of the "DOS Box" or "powershell", command lines, feel free to use one of those. However, your life may be easier if you install "Git Bash", as then you can follow unix-style terminal instructions exactly, and do not have to translate. Also, your instructors are more experienced with Bash. + +From now on, if you hear the terms "bash", "shell" or "terminal", or "command line" know that this is the application that is being referred to. We will use those terms interchangeably to mean ANY command line. + +When you install Git Bash, you are installing git (and a git gui) as well, thus killing two birds with one stone! + +https://git-for-windows.github.io/ + +Select the download button on the page and launch downloaded executable, then follow the prompts. On "Choosing default editor used by Git" step it is best to select Notepad++ (which you need to have installed first) unless you are comfortable with non-graphical editors like vim. +You can go through the rest of the prompts using default values. Once you are done, a terminal window should pop up - try out some commands like ``ls`` or ``git help``. + +You can use this git with the DOS box or Powershell as well. + +This is also a good bet for running Python -- If you use the Git Bash shell, you can use the same commands as Linux and OS-X users. Regardless of which shell you choose, you will need to add Python to your environment. It is possible that this was done during the installation of Python. If you type 'which python' into your terminal, and get back the answer '/c/python37/python', then you are good to go, otherwise (which shouldn't happen if you checked the "Add Python 3.7 to PATH" checkbox when you installed Python above), follow the instructions here: + +http://www.computerhope.com/issues/ch000549.htm + +You will want to add: + +``C:\Users\YourUserName\AppData\Local\Programs\Python\Python37`` + +and + +``C:\Users\YourUserName\AppData\Local\Programs\Python\Python37\Scripts`` + +to ``PATH`` + +Here are steps for updating path: + +:: + + cd + touch .bash_profile + +You can edit this file using Notepad, locate this file in File Explorer in This PC > Local Disk > Users > YourUsername + +Add to the file (file should be empty): + +:: + + PATH=$PATH:/C/Users/YourUserName/AppData/Local/Programs/Python/Python37:/C/Users/YourUserName/AppData/Local/Programs/Python/Scripts + +Note: your python executable may be located in a different path, to check the path you need to start windows shell (``cmd``) and type ``where python`` - this command will output where python is currently installed. + +Save the file and start a new gitbash shell. + +Once you have done that, you should be able to type ``python`` at the command prompt, and get something like: + +:: + + Python 3.7.0 (v3.6.2:5fd33b5926, Jul 16 2017, 20:11:06) + [GCC 4.2.1 (Windows build 7584) (dot 3)] on win64 + Type "help", "copyright", "credits" or "license" for more information. + >>> + +This is the Python interpreter. + +Type ``ctrl+Z`` to get out (or ``exit()``) + +Note: if you have trouble running ``python`` command in your gitbash (it hangs), try running this instead: ``winpty python``. To avoid having to type ``winpty python`` all the time, it's strongly recommended that you create an alias like below: + +:: + + $ echo "alias python='winpty python'" >> ~/.bash_profile + +You will need to close the current bash window and restart a new one to get this alias. Then from now on, you can just type ``python`` and it should work on git bash (no more hanging) as well. + +git +--- + +If you installed Git Bash, you will already have git, both usable in the terminal and as a gui, and can safely skip this section. If not, you still need a git client. You can use the above link and install git (it will install the bash shell as well, of course, but you can use your shell of choice instead). + +There is also TortoiseGit: + +https://code.google.com/p/tortoisegit/ + +Which integrates git with the file manager. Feel free to use this if you already have an understanding of how git works, but for the purposes of learning, it may be better to use a command line client (git Bash above). + + +pip +--- + +``pip`` is the Python package installer. It is updated faster than Python itself, so once you have Python you want to get the latest version of pip working:: + + $ python -m ensurepip --upgrade + +It should download and install the latest ``pip``. + +You can now use pip to install other packages. + +The first thing you may want to do is update pip itself: + +.. code-block:: bash + + $ python -m pip install --upgrade pip + +Using pip: +---------- + +To use pip to install a package, you invoke it with this command:: + + python -m pip install the_name_of_the_package + +Where ``python`` is the command you use to invoke the Python you want to use . + +**NOTE:** You will frequently see advice to use pip like so:: + + $ pip install something_or_other + +Which often works, but also can invoke the *wrong* version of pip. The above command:: + + $ python -m pip install something_or_other + +calls Python, and tells it to run the ``pip`` module. It is exactly the same as calling pip directly, except that you are assured that you are getting the version of pip connected the version of Python that you are running. + + +iPython +-------- + +One extra package we are going to use from the beginning in the program is ``iPython``:: + + $ python -m pip install ipython + +(It will install a LOT) + +You should now be able to run ``iPython`` from the git bash shell or "DOS Box" or PowerShell:: + + $ ipython + Python 3.7.0 (v3.6.2:5fd33b5926, Jul 16 2017, 20:11:06) + Type 'copyright', 'credits' or 'license' for more information + IPython 6.1.0 -- An enhanced Interactive Python. Type '?' for help. + (or from the DOS box or PowerShell prompt) + +We will use this in class as our default Python interpreter. + + +Testing it out +-------------- + +To be ready for the program, you need to have: + - python + - pip + - iPython + - git + +All available from the command line. + +To try it out, you should be able to run all of these commands, and get something like the following results: + +(recall that you can get out of the python or iPython command lines with ``ctrl+Z`` (if that doesn't work, try ``ctrl+D`` -- the \*nix version)) + +For Python: + +:: + + hosun@DESKTOP-GJT06Q0 MINGW64 ~ + $ python + Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 27 2018, 04:59:51) [MSC v.1914 64 bit (AMD64)] on win32 + Type "help", "copyright", "credits" or "license" for more information. + >>> ^Z + + +For iPython: + +:: + + hosun@DESKTOP-GJT06Q0 MINGW64 ~ + $ ipython + Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 27 2018, 04:59:51) [MSC v.1914 64 bit (AMD64)] + Type 'copyright', 'credits' or 'license' for more information + IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help. + + In [1]: + Do you really want to exit ([y]/n)? y + + +For pip: + +:: + + hosun@DESKTOP-GJT06Q0 MINGW64 ~ + $ python -m pip --version + pip 18.0 from C:\Python37\lib\site-packages\pip (python 3.7) + + +For git: + +:: + + hosun@DESKTOP-GJT06Q0 MINGW64 ~ + $ git --version + git version 2.18.3.windows.1 + diff --git a/_sources/topics/01-setting_up/resources.rst.txt b/_sources/topics/01-setting_up/resources.rst.txt new file mode 100644 index 0000000..eac3361 --- /dev/null +++ b/_sources/topics/01-setting_up/resources.rst.txt @@ -0,0 +1,12 @@ + +========= +Resources +========= + +.. toctree:: + :maxdepth: 1 + + command_line + virtualenv + ../../modules/Py2vsPy3 + diff --git a/_sources/topics/01-setting_up/setup_details.rst.txt b/_sources/topics/01-setting_up/setup_details.rst.txt new file mode 100644 index 0000000..d48e1a0 --- /dev/null +++ b/_sources/topics/01-setting_up/setup_details.rst.txt @@ -0,0 +1,18 @@ +.. _setup_details: + +============= +Setup Details +============= + +Python itself is very platform independent. The code itself runs the same on all supported operating systems. But getting it installed and running is a bit different for each platform. Choose the page for your platform to get started: + +.. toctree:: + :maxdepth: 1 + + python_for_mac + python_for_windows + python_for_linux + vagrant + + git_editor_windows + diff --git a/_sources/topics/01-setting_up/shell.rst.txt b/_sources/topics/01-setting_up/shell.rst.txt new file mode 100644 index 0000000..3dec9ad --- /dev/null +++ b/_sources/topics/01-setting_up/shell.rst.txt @@ -0,0 +1,243 @@ +.. _shell_customization: + +******************************************* +Shell Customizations for Python Development +******************************************* + +The command line is your home as a developer. You must be comfortable there. +In order to improve your comfort there are a number of enhancements you can +make to improve your experience, especially with non-standard software like +``git`` and ``virtualenv`` + +What was that name, again? +========================== + +For example, ``bash`` offers tab completion. But that doesn't extend to +interactions with ``git``. Considering how many branches, tags and remotes you +end up interacting with, and how many long-winded commands there are in +``git``, having a similar autocompletion for them would be very nice. + +The folks who create such things have been kind enough to provide a shell +script that sets this up. And it's not hard to install. + +`The script`_ is called ``git-completion`` and it's available in ``bash``, +``tcsh`` and ``zsh`` flavors. + +.. _The script: https://github.com/git/git/tree/master/contrib/completion + +To use it, download the version of the script that corresponds to your +preferred shell from the tag of the git repo that corresponds to the version of +git you are using. I've got git 1.8.4.2 installed on my machine, so +`this is the version for me`_. Put it in your home directory: + +.. code-block:: bash + + $ cd + $ curl https://raw.github.com/git/git/v1.8.4.2/contrib/completion/git-completion.bash -o .git-completion.bash + +Then source it from your shell startup file: + +.. code-block:: bash + + source ~/.git-completion.bash + +There's even a nifty gist that `does this automatically`_ for OS X. + +.. _this is the version for me: https://raw.github.com/git/git/v1.8.4.2/contrib/completion/git-completion.bash +.. _does this automatically: https://gist.github.com/johngibb/972430 + +Once installed, you should be able to visit any repository you have on your +machine and get tab completion of branch names, remotes and all git commands. + +Where am I, what am I doing? +============================ + +As a working developer, you end up with a *lot* of projects. Even with tab +completion its a chore to remember which branch is checked out, how far ahead +or behind the remote you are, and so on. + +Enter `git-prompt`_. Again, you place this code in your home directory, and +then source it from your shell startup file: + +.. code-block:: bash + + source ~/.git-prompt.sh + +Once you do this you can use the ``__git_ps1`` shell command and a number of +shell variables to configure ``PS1`` and change your shell prompt. You can show +the name of the current branch of a repository when you are in one. You can +get information about the status of HEAD, modified files, stashes, untracked +files and more. + +.. _git-prompt: https://github.com/git/git/blob/master/contrib/completion/git-prompt.sh + +There's two ways to do this. The first is to use ``__git_ps1`` as a command +directly in a ``PS1`` expression in your shell startup file: + +.. code-block:: bash + + export PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ ' + +The result looks like this: + +.. image:: /_static/simple_prompt.png + :width: 600px + :alt: Overriding PS1 provides a customized shell prompt + + +That's not bad, but a bit of color would be nice, and perhaps breaking things +onto more than one line so you can parse what you're seeing more easily would +be helpful. + +For that, you'll need to change strategies. The ``__git_ps1`` command can be +used as a single element in the expression for ``PS1``. But it can also be +used itself as the ``PROMPT_COMMAND`` env variable (this command is for +``bash``, there's different one for ``zsh``). If defined, this command will be +used to form ``PS1`` dynamically. + +When you use ``__git_ps1`` in this way, a couple of things happen. First, +instead of taking only one optional argument (a format string), you can provide +two or optionally three arguments: + +* The first will be prepended to the output of the command +* The second will be appended after +* The optional third argumment will be used as a format string for the output + of the command itself. If there is no output, it will not appear at all. + +Combining these three elements can be very expressive. For example, a standard +OS X command prompt can be expressed like so: ``\h:\W \u\\\$``. If you use this +expression as the second argument, leave the first empty and provide a simple format +ending in a newline for the ``__git_ps1`` output, you get some nice results. + +Enter this in your shell startup file: + +.. code-block:: bash + + PROMPT_COMMAND='__git_ps1 "" "\h:\W \u\\\$ " "[%s]\n"' + +That produces a nice two-line prompt that appears when you're in a git repo, and +disappears when you're not: + +.. image:: /_static/two_line_prompt.png + :width: 600px + :alt: A two-line prompt showing current git repository + +You can also play with setting a few environment variables in your shell +startup file to expand this further. For example, colorizing the output and +providing information about the state of a repo: + +.. code-block:: bash + + GIT_PS1_SHOWDIRTYSTATE=1 + GIT_PS1_SHOWCOLORHINTS=1 + GIT_PS1_SHOWSTASHSTATE=1 + GIT_PS1_SHOWUPSTREAM="auto" + PROMPT_COMMAND='__git_ps1 "" "\h:\W \u\\\$ " "[%s]\n"' + +.. image:: /_static/color_git_prompt.png + :width: 600px + :alt: A colorized git prompt + +Not half bad at all. + +But wait, there's more. +======================= + +The problem with this is that it doesn't play well with another incredibly +useful tool, `virtualenv`_. When you activate a virtualenv, it prepends the name +of the environment you are working on to the shell prompt. + +But it uses the standard ``PS1`` shell variable to do this. Since you've now +used the ``PROMPT_COMMAND`` to create your prompt, ``PS1`` is ignored, and +this nice feature of virtualenv is lost. + +.. _virtualenv: http://virtualenv.org + +Luckily, there is a way out. Bash shell scripting offers `parameter expansion`_ +and a trick of that syntax can help. Normally, a shell parameter is +referenced like so: + +.. code-block:: bash + + $ PARAM='foobar' + $ echo $PARAM + foobar + +In complicated situations, you can wrap the name of the paramter in curly +braces to avoid confusion with following characters: + +.. code-block:: bash + + $ echo ${PARAM}andthennotparam + foobarandthennotparam + +What is not as well known is that this curly-brace syntax has a lot of +interesting variations. For example, you can use ``PARAM`` as a test and +actually print something else entirely: + +.. code-block:: bash + + $ echo ${PARAM:+'foo'} + foo + $ echo ${PARAM:+'bar'} + + $ + +The key here is the ``:`` bit immediately after ``PARAM``. If the ``+`` +char is present, then if ``PARAM`` is unset or null, what comes after is not +printed, otherwise it is. + +If you look at the script that `activates a virtualenv in bash`_ you'll notice +that it exports ``VIRTUAL_ENV``. This means that so long as a virtualenv is +active, this environmental variable will be set. And it will be unset when no +environment is active. + +.. _parameter expansion: http://www.gnu.org/software/bash/manual/bash.html#Shell-Parameter-Expansion +.. _activates a virtualenv in bash: https://github.com/pypa/virtualenv/blob/develop/virtualenv_embedded/activate.sh + +You can use that! + +Armed with this knowledge, you can construct a shell expression that will either +print the name of the active virtualenv in square brackets, or print nothing if +no virtualenv was active: + +.. code-block:: bash + + $ echo ${VIRTUAL_ENV:+[`basename $VIRTUAL_ENV`]} + + $ source /path/to/someenv/bin/activate + $ echo ${VIRTUAL_ENV:+[`basename $VIRTUAL_ENV`]} + someenv + + +Roll that into your shell startup file. You'll have everything you want. You +can even throw in a little more color for good measure: + +.. code-block:: bash + + source ~/.git-prompt.sh + # PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ ' + GIT_PS1_SHOWDIRTYSTATE=1 + GIT_PS1_SHOWCOLORHINTS=1 + GIT_PS1_SHOWSTASHSTATE=1 + GIT_PS1_SHOWUPSTREAM="auto" + Color_Off="\[\033[0m\]" + Yellow="\[\033[0;33m\]" + PROMPT_COMMAND='__git_ps1 "${VIRTUAL_ENV:+[$Yellow`basename $VIRTUAL_ENV`$Color_Off]\n}" "\h:\W \u\\\$ " "[%s]\n"' + +And voilà! You've got a shell prompt that informs about all the things you'll +need to know when working on a daily basis: + +.. image:: /_static/virtualenv_prompt.png + :width: 600px + :alt: A shell session showing the prompt with both virtualenv and git information + +Wrap-Up +======= + +There is still a great deal more that you could do with your shell, but this +will suffice for now. If you are interested in reading further, there is +`a lot to learn`_. + +.. _a lot to learn: http://www.gnu.org/software/bash/manual/bash.html + diff --git a/_sources/topics/01-setting_up/sublime_as_ide.rst.txt b/_sources/topics/01-setting_up/sublime_as_ide.rst.txt new file mode 100644 index 0000000..1f96594 --- /dev/null +++ b/_sources/topics/01-setting_up/sublime_as_ide.rst.txt @@ -0,0 +1,182 @@ +.. _sublime_as_ide: + +************************************************** +Turning Sublime Text Into a Lightweight Python IDE +************************************************** + +A solid text editor is a developer's best friend. You use it constantly and it +becomes like a second pair of hands. The keyboard commands you use daily +become so ingrained in your muscle memory that you stop thinking about them +entirely. + +With Sublime Text, it's possible to turn your text editor into the functional +equivalent of a Python IDE. The best part is you don't have to install an IDE +to do it. + +http://www.sublimetext.com/ + +Requirements +============ + +Here are *my* requirements for an 'IDE': + +* It should provide excellent, configurable syntax colorization. +* It should allow for robust tab completion. +* It should perform automatic code linting to help avoid silly mistakes and conform to good style. + +And some more advanced features that you may want later: + +* It should offer the ability to jump to the definition of symbols in other + files. +* It should be able to interact with a Python interpreter such that when + debugging, the editor will follow along with the debugger. + + +Which Version? +============== + +Use version 3 -- it is updated now and again, so make sure to get the latest. + +*Use Sublime Version 3* + + +Basic Settings +============== + +All configuration in Sublime Text is done via `JSON `_. It's simple to learn. Go and read that link then return here. + +.. note:: JSON is very similar to Python dict and list literals. Though it has its root in Javascript, it is also used in a wide variety of applications, and is well supported by Python. But a key difference is that it does not allow trailing commas after items in lists -- so be careful. + +There are a number of `different levels of configuration `_ in Sublime Text. You will most often work on settings at the user level. + +Open ``Preferences`` -> ``Settings`` + +On a Mac, you can find it under: + +``Sublime Text`` -> ``Preferences`` -> ``Settings`` + +Sublime will open up a split window -- "default" and "user" -- you can look through the default ones to see all the setting you might want to override, and then copy things from default by putting them in the user file. + +The preferences file is simply a JSON text file you edit like any other text file. + +Here's a reasonable set of preliminary settings (theme, color scheme and font are quite personal, find ones that suit you.): + +.. code-block:: javascript + + { + "auto_indent": true, + "color_scheme": "Packages/User/Monokai (Flake8Lint).tmTheme", + "draw_white_space": "all", + "font_face": "inconsolata", + "font_size": 23, + "indent_to_bracket": true, + "rulers": + [ + 79, + 95 + ], + "theme": "Adaptive.sublime-theme", + "smart_indent": true, + "tab_size": 4, + "translate_tabs_to_spaces": true, + "use_tab_stops": true, + "trailing_spaces_modified_lines_only": true, + "trim_automatic_white_space": true, + "trim_trailing_white_space_on_save": true, + "word_wrap": false, + "wrap_width": 95 + } + +**NOTE:** Especially important is the setting ``translate_tabs_to_spaces``, which ensures that any time you hit a tab key, the single character is replaced by four characters. In Python this is **vital**! + +If you do nothing else, add ``translate_tabs_to_spaces`` to your config! + +Extending the Editor +==================== + +Most of the requirements above go beyond basic editor functionality. So we'll use Plugins. + +Sublime Text comes with a great system for `Package Control `_. +It handles installing and uninstalling plugins, and even updates installed plugins for you. +You can also manually install plugins that haven't made it to the big-time yet, including `ones you write yourself `_. +Happily, the plugin system is based on Python! + + +To install a plugin using Package Control, open the ``command palette`` with ``shift-super-P`` (``ctrl-shift-P`` on Windows/Linux). +The ``super`` key is ``command`` or ``⌘`` on OS X. +When the palette opens, typing ``install`` will bring up the ``Package Control: Install Package`` command. +Hit ``enter`` to select it. + +.. image:: /_static/pc_menu.png + :width: 600px + :align: center + :alt: The package control command in the command palette. + +After you select the command, Sublime Text fetches an updated list of packages from the network. It might take a second or two for the list to appear. When it does, start to type the name of the package you want. Sublime Text filters the list and shows you what you want to see. To install a plugin, select it with the mouse, or use arrow keys to navigate the list and hit ``enter`` when your plugin is highlighted. + +.. image:: /_static/plugin_list.png + :width: 600px + :align: center + +Useful Plugins +============== + +Here are the plugins I've installed to achieve the requirements above. + +Anaconda +-------- + +There are a bunch of Python-related plugins available. However, Anaconda is a nice package that provides most of the features you want, so plan on using just that one. + +Not to be confused with the scientific Python distribution -- the Anaconda sublime plugin is a full featured package to turn Sublime into a pretty full IDE: + +http://damnwidget.github.io/anaconda/ + +There are nifty instructions on that page. + +By default, Anaconda uses the python interpreter that is in your ``PATH`` environment variable. So, the most important configuration option is the python_interpreter option that allows you to use a different Python interpreter, for example, one that resides in a virtual environment, or python3 vs python2. + +If you get the right Python when you type "python" at a raw command line, then you are OK. But if not you may need to re-configure it. + +{"python_interpreter": "~/.virtualenvs/myproject/bin/python"} + + Note: for detailed information about how to properly configure Anaconda to get the maximum of it, follow `"Configure Anaconda the Right Way" `_. + +A few settings you'll want to change +------------------------------------ + +There are a few setting you may want to change: + +* max line length for the linter: default is 72, which is pretty short these day. I use 95:: + + "pep8_max_line_length": 95, + + +White Space Management +---------------------- + +One of the issues highlighted by code linters is trailing spaces. Sublime Text provides a setting that allows you to remove them every time you save a file: + +.. code-block:: json + + { + "trim_trailing_whitespace_on_save": true + } + +This is a useful setting, but be careful if you are working with existing code: removing trailing whitespace by default causes a *ton* of noise in git commits. + +But if you use it from the start with your code, it will keep it clean from the beginning. + +It is suggested in the above settings. + +Debugging Support +----------------- + +You'll probably want to wait on this until you start using a debugger, but it's a nifty feature when you get there. + +The final requirement for a reasonable IDE experience is to be able to follow a debugging session in the file where the code exists. + + +This: https://packagecontrol.io/packages/Python%20Debugger + +Looks promising as a debugger plugin for sublime. diff --git a/_sources/topics/01-setting_up/submitting_to_github.rst.txt b/_sources/topics/01-setting_up/submitting_to_github.rst.txt new file mode 100644 index 0000000..9ff8c06 --- /dev/null +++ b/_sources/topics/01-setting_up/submitting_to_github.rst.txt @@ -0,0 +1,95 @@ +:orphan: + +.. _submitting_to_gitHub: + + +**NOTE:** This is the "old" way -- it has been replaced with gitHUb classroom. But there may be some helpful hints here that should be included in the new docs. + +#################################### +Submitting your work to gitHub (old) +#################################### + +In this program, we are using the gitHub source code management system to manage each student's code, as well as examples and solutions. + +gitHub is designed as a system to develop collaborative software projects. As such, it is a great tool to learn for your future programming endeavors. + +It also has great interface for code review, so provides a system with which the instructors can review and provide feedback on your code. + +Starting a New Exercise +======================= + +Most Exercise will begin with creating a new python file. + +1) Start by creating a folder for the current lesson ("lesson02" or "lesson03", or....) + +2) Create your python file and save it into the lesson folder you just created. + +3) Once the file exists, it can be added to your local git "repo" to be managed:: + + git add the_file.py + +4) If the exercise requires more than one file, create them and add each to the manged by git in the same way. + +5) As you work, when you have something working that you might want to go back to, commit the changes to your repo. You have two options now -- you can commit everything that you have changed (-a means "all") :: + + git commit -a + + or you can commit only those files that you want to. To do that, you need to "stage" the files you want to commit:: + + git add one_file.py another_file.py + + (Yes, it is confusing -- "add" means: "add this file to the ones git is managing" if it's a new file, but "add this file to the ones I want to commit (staged)" if git is already managing the file. In fact, for a new file, you need to do "git add the_file.py" twice!) + + Now you can do:: + + git commit + + and the files that are "staged for commit" will be committed to your repo. + + **reminder** -- you can (and frequently should) run:: + + git status + + To see what is going on -- it will tell you which files are staged for commit, which files have been modified, and which are not being managed by git at all. + +6) Recall that all this git adding and committing is only effecting your local repo -- the one on your local machine. Once you have everything at a point where you want to share it with others (i.e. the instructors, classmates, or yourself on another machine!), you want to "push" it to your gitHub account:: + + git push + + Should do it. + + If you are working from multiple machines, and pushing to gitHub, you will need to do:: + + git pull + + To get everything that is up on gitHub down to your local machine. + +7) Once you have completed the assignment, and are ready to "turn it in" (that is, have the instructors review your work), you need to: + + a) make sure the latest version is in your gitHub account:: + + git push + + b) Go to *your* repo in gitHub in a browser. + + c) Submit a "Pull Request" to the class repo: + + (more detail here) + + Make sure to add a note to the PR letting the instructors know which code is ready for review, or of you have any specific questions. + +When you submit you PR on gitHub, the instructors will automatically get an email letting them know that you have submitted something. + + + + + + + + + + + + + + diff --git a/_sources/topics/01-setting_up/testing_your_setup.rst.txt b/_sources/topics/01-setting_up/testing_your_setup.rst.txt new file mode 100644 index 0000000..52fa97b --- /dev/null +++ b/_sources/topics/01-setting_up/testing_your_setup.rst.txt @@ -0,0 +1,77 @@ + +.. _testing_your_setup: + + +################## +Testing Your setup +################## + +If you have access to a command line, and Python installed, and a text editor or IDE ready to go, here's how you can make sure it's all working correctly. + +Python Interpreter +------------------ + +If you have Python installed and know how to run a python file, give this a try to make sure you're all setup: + +Create a file called ``install_test.py``, with the following content: + +.. code-block:: python + + #!/usr/bin/env python3 + + import sys + print("This is my first python program") + + version = sys.version_info + + if version.major == 3: + if version.minor < 8: + print("You should be running version 3.8 or higher") + else: + print("I am running python {}.{} -- all good!".format( + version.major, version.minor)) + + else: + print("You need to run Python 3!") + print("This is version: {}.{}".format(version.major, version.minor)) + +Run it with your version of python. It should result in something like this:: + + This is my first python program + I am running python 3.8 -- all good! + + +If you can't figure out how to run it, see: :ref:`how_to_run_a_python_file` + +If you can run, it but don't get that nice "all good" message, then you either do not have Python installed, or you have the wrong version. + +Go back to :ref:`setup_details` + +And try again. + +Run git +------- + +You should be able to run git on the command line: + +.. code-block:: bash + + $ git --version + git version 2.20.1 (Apple Git-117) + +It should be version >= 2 + +iPython +------- + +``iPython`` is not critical, but it is very nice. You should be able to run it with:: + + $ ipython + Python 3.6.2 (v3.6.2:5fd33b5926, Jul 16 2017, 20:11:06) + Type 'copyright', 'credits' or 'license' for more information + IPython 6.1.0 -- An enhanced Interactive Python. Type '?' for help. + +And get something like that. + +``ipython`` can be quit by typing ``quit`` + diff --git a/_sources/topics/01-setting_up/vagrant.rst.txt b/_sources/topics/01-setting_up/vagrant.rst.txt new file mode 100644 index 0000000..e40d8d3 --- /dev/null +++ b/_sources/topics/01-setting_up/vagrant.rst.txt @@ -0,0 +1,21 @@ +.. _vagrant-notes: + +******************************** +Setting up Python via a Linux VM +******************************** + +One of the challenges we face as developers on a team, or as students in a classroom, is getting everyone quickly up and running with a full fledged, feature rich, functional, consistent, and homogeneous Python development environment. + +Python is robust platform-independent programming language, and a full fledged development environment can be set up on any platform. However, there are still system differences, particularly between Windows and the \*nix family of Operating Systems. + +Also -- particularly if you are working on a web service of some sort, the odds are good that it will be deployed on a Linux system. In this case, developing on Linux makes things easier. + +So if you want an already setup environment in which to do your python programming that matches the environment used by the instructors and other students well (and many professional python developers too), a virtual machine running Linux can provide you that environment, regardless of the host operating system. + +We recommend `VirtualBox `_ for providing the VM, and `Vagrant `_ for deploying a pre-configured system. + +VirtualBox and Vagrant allow us to quickly build a virtual machine with everything we'll need. + +Complete instructions and installation resources can be found here: + +https://github.com/rriehle/uwpce-vagrant diff --git a/_sources/topics/01-setting_up/virtualenv.rst.txt b/_sources/topics/01-setting_up/virtualenv.rst.txt new file mode 100644 index 0000000..94e806e --- /dev/null +++ b/_sources/topics/01-setting_up/virtualenv.rst.txt @@ -0,0 +1,428 @@ +.. _virtualenv_section: + +####################### +Working with Virtualenv +####################### + +.. note:: Virtual environments are a critical tool for software development. However, When you are first learning Python, you can safetly ignore them. For the first class, Programming In Python, we make very little use of third-party packages -- it is OK to simply use your one system Python. come back to this page if you do find yourself getting tangled up in multiple "requirements" + + +"For every non-standard package installed in a system Python, the gods kill a kitten" + + - me + +Reasons Why +============ + +* As a working developer you will need to install packages that aren't in the + Python standard Library + +* As a working developer you often need to install *different* versions of the + *same* library for different projects + +* Conflicts arising from having the wrong version of a dependency installed can + cause long-term nightmares + +* Use `virtualenv`_ ... + +* **Always** + +conda ? +------- + +The above principles imply that you need to use *some* way to manage multiple environments. virtualenv is one (and there is a newer one called pipenv) And these are common and work well for a lot of developers, particulaly web developers. But there is antoher option. The "conda" system, as used by the Anaconda distribution provides another envrionment management system that has the advantage of managing non-python tools, too, like Fortran or C libaries, compilers, even the R statistical programming environment. So it is widely used in the data science world. + + +Installing Virtualenv +--------------------- + +The best way is to install directly in your system Python (one exception to the rule). + +To do so you will have to have `pip`_ installed. + +Try the following command: + +.. code-block:: bash + + $ which pip + /usr/local/bin/pip + +If the ``which`` command returns no value for you, then ``pip`` is not +installed in your system. To fix this, follow `the instructions here`_. + +Once you have ``pip`` installed in your system, you can use it to install +`virtualenv`_. Because you are installing it into your system Python, you will +most likely need ``superuser`` privileges to do so: + +.. code-block:: bash + + $ sudo pip install virtualenv + Downloading/unpacking virtualenv + Downloading virtualenv-1.11.2-py2.py3-none-any.whl (2.8MB): 2.8MB downloaded + Installing collected packages: virtualenv + Successfully installed virtualenv + Cleaning up... + +Great. Once that's done, you should find that you have a ``virtualenv`` +command available to you from your shell: + +:: + + $ virtualenv --help + Usage: virtualenv [OPTIONS] DEST_DIR + + Options: + --version show program's version number and exit + -h, --help ... + + +.. _pip: http://www.pip-installer.org +.. _the instructions here: http://www.pip-installer.org/en/latest/installing.html + +Using Virtualenv +================ + +Creating a new virtualenv is very very simple: + +.. code-block:: bash + + $ virtualenv [options] + + +```` is just the name of the environment you want to create. It's +arbitrary. Let's make one for demonstration purposes: + +.. code-block:: bash + + $ virtualenv demoenv + New python executable in demoenv/bin/python + Installing setuptools, pip...done. + + +What Happened? +-------------- + +When you ran that command, a few things took place: + +* A new directory with your requested name was created +* A new Python executable was created in /bin (/Scripts on Windows) +* The new Python was cloned from your system Python (where virtualenv was + installed) +* The new Python was isolated from any libraries installed in the old Python +* Setuptools was installed so you have ``easy_install`` for this new Python +* Pip was installed so you have ``pip`` for this new python + +Activation +---------- + +The virtual environment you just created, ``demoenv`` contains an executable +Python command, but if you do a quick check to see which Python executable is +found by your terminal, you'll see that it is not the one: + +.. code-block:: bash + + $ which python + /usr/bin/python + +You can execute the new Python by explicitly pointing to it: + +.. code-block:: bash + + $ ./demoenv/bin/python -V + Python 2.7.5 + +but that's tedious and hard to remember. Instead, ``activate`` your virtualenv +using the ``source`` command: + +.. code-block:: bash + + $ source demoenv/bin/activate + (demoenv)$ which python + /Users/cewing/demoenv/bin/python + +On Windows, the *activate* script is in the ``Scripts`` folder: + +``> \path\to\env\Scripts\activate`` + +There. That's better. Now whenever you run the ``python`` command, the +executable that will be used will be the new one in your ``demoenv``. + +Notice also that the your shell prompt has changed. It indicates which +``virtualenv`` is currently active. Little clues like that really help you to +keep things straight when you've got a lot of projects going on, so it's nice +the makers of virtualenv thought of it. + +Installing Packages +------------------- + +Now that your virtualenv is active, not only has your ``python`` executable been +hijacked, so have ``pip`` and ``easy_install``: + +.. code-block:: bash + + (demoenv)$ which pip + /Users/cewing/demoenv/bin/pip + (demoenv)$ which easy_install + /Users/cewing/demoenv/bin/easy_install + +This means that using these tools to install packages will install them *into +your virtual environment only* and not into the system Python. Let's see this +in action. We'll install a package called ``docutils`` that provides support +for converting ReStructuredText documents into other formats like HTML, LaTeX +and more: + +.. code-block:: bash + + (demoenv)$ pip install docutils + Downloading/unpacking docutils + Downloading docutils-0.11.tar.gz (1.6MB): 1.6MB downloaded + Running setup.py (path:/Users/cewing/demoenv/build/docutils/setup.py) egg_info for package docutils + ... + changing mode of /Users/cewing/demoenv/bin/rst2xml.py to 755 + changing mode of /Users/cewing/demoenv/bin/rstpep2html.py to 755 + Successfully installed docutils + Cleaning up... + +And now, when we fire up our Python interpreter, the docutils package is +available to us: + +.. code-block:: bash + + (demoenv)$ python + Python 2.7.5 (default, Aug 25 2013, 00:04:04) + [GCC 4.2.1 Compatible Apple LLVM 5.0 (clang-500.0.68)] on darwin + Type "help", "copyright", "credits" or "license" for more information. + >>> import docutils + >>> docutils.__path__ + ['/Users/cewing/demoenv/lib/python2.7/site-packages/docutils'] + >>> ^d + (demoenv)$ + +There's one other interesting side-effect of installing software with +``virtualenv``. The ``docutils`` package provides a number of executable +scripts when it is installed: ``rst2html.py``, ``rst2latex.py`` and so on. +These scripts are set up to execute using the Python with which they were +built. What this means is that running these scripts will use the Python +executable in your virtualenv, *even if that virtualenv is not active*! + +Deactivation +------------ + +So you've got a virtual environment created. And you've activated it so that +you can install packages and use them. Eventually you'll need to move on to +some other project. This likely means that you'll need to stop working with +this ``virtualenv`` and switch to another (it's a good idea to keep a separate +``virtualenv`` for every project you work on). + +When a ``virtualenv`` is active, all you have to do is use the ``deactivate`` +command: + +.. code-block:: bash + + (demoenv)$ deactivate + $ which python + /usr/bin/python + +Note that your shell prompt returns to normal, and now the executable Python +found when you check ``python`` is the system one again. + +Cleaning Up +----------- + +The final great advantage that ``virtualenv`` confers on you as a developer is +the ability to easily remove a batch of installed Python software from your +system. Consider a situation where you installed a library that breaks your +Python (it happens). If you are working in your system Python, you now have to +figure out what that package installed, where, and go clean it out manually. +With ``virtualenv`` the process is as simple as removing the directory that +virtualenv created when you started out. Let's do that with our ``demoenv``: + +.. code-block:: bash + + $ rm -rf demoenv + +And that's it. The entire environment and all the packages you installed into +it are now gone. There's no traces left to pollute your world. + +VirtualenvWrapper +================= + +So you have this great tool that allows you to build isolated environments in +which you can install Python software. Several questions arise when considering +this. + +* Where should such environments be placed? +* How can the environments be tied to the projects you are working on? +* Once you have more than a trivial number of projects, how can you keep track + of all these virtualenvs? + +Like any good tool, ``virtualenv`` does not impose on you any particular way of +working. You can place your environments into the directories where you are +building the project to which they apply. You can keep them all in a single +global location. You can build a random path generator that drops them +wherever. + +But any of these methods lead inevetably to chaos. They require too much from +you. It would be better if you could manage your virtual environments easily +and intuitively. + +With `virtualenvwrapper`_ you can. + +Installation +------------ + +Let's start by installing the package in our system Python, alongside +``virtualenv`` (again, you'll need ``superuser`` to do this): + +.. code-block:: bash + + $ sudo pip install virtualenvwrapper + Downloading/unpacking virtualenvwrapper + Downloading virtualenvwrapper-4.2.tar.gz (125kB): 125kB downloaded + Running setup.py (path:/private/tmp/pip_build_root/virtualenvwrapper/setup.py) egg_info for package virtualenvwrapper + ... + Successfully installed virtualenvwrapper virtualenv-clone stevedore + Cleaning up... + $ + +Once that's finished, you'll need to wire the system up by letting your shell +know that the commands it provides are present. Add the following lines to your +shell startup file (``.profile``, ``.bash-profile``, ...): + +.. code-block:: bash + + export WORKON_HOME=~/.virtualenvs + source /usr/local/bin/virtualenvwrapper.sh + +This will create a new environmental variable, ``WORKON_HOME``, that determines +where new virtual environments will be created. The actual name is completely +arbitrary. + +You'll need to be sure that the location you set exists: + +.. code-block:: bash + + $ mkdir ~/.virtualenvs + +Using ``mkvirtualenv`` +---------------------- + +When you've done that, start a new terminal and you'll have access to the +``mkvirtualenv`` command: + +.. code-block:: bash + + $ mkvirtualenv testenv + New python executable in testenv/bin/python + Installing setuptools, pip...done. + (testenv)$ ls ~/.virtualenvs + testenv + (testenv)$ which python + /Users/cewing/.virtualenvs/testenv/bin/python + (testenv)$ + +Notice a couple of things: + +* The new environment you asked for was created in ``WORKON_HOME`` +* The new environment was *immedately* activated for you + +That's a nice feature, eh? No more needing to remember to ``activate`` the env +you just created to install packages. + +Using ``workon`` +---------------- + +In addition to this nice little feature, you can also use the ``workon`` +command to see which environments you have, and to switch from one to another: + +.. code-block:: bash + + (testenv)$ workon + testenv + (testenv)$ mkvirtualenv number2 + New python executable in number2/bin/python + Installing setuptools, pip...done. + (number2)$ workon + number2 + testenv + (number2)$ workon testenv + (testenv)$ + +Sweet! + +The same ``deactivate`` command can get you back to your system environment: + +.. code-block:: bash + + (testenv)$ deactivate + $ + +Using ``mkproject`` +------------------- + +That takes care of deciding where to put new environments. It also clears up +the question of how to remember which ones you have and how to start them up +and switch between them. But we still have to figure out how to remember which +environment goes with which project. + +That's what the ``mkproject`` command is for. + +First, go back to your shell startup file and add a new environmental variable: + +.. code-block:: bash + + export PROJECT_HOME=~/projects #<- this line here is new + export WORKON_HOME=~/.virtualenvs + source /usr/local/bin/virtualenvwrapper.sh + +Then, make sure the directory you named exists: + +.. code-block:: bash + + $ mkdir ~/projects + +After all that, fire up a new shell to pick up the changes and try this: + +.. code-block:: bash + + $ mkproject foo + New python executable in foo/bin/python + Installing setuptools, pip...done. + Creating /Users/cewing/projects/foo + Setting project for foo to /Users/cewing/projects/foo + (foo)$ which python + /Users/cewing/.virtualenvs/foo/bin/python + (foo)$ pwd + /Users/cewing/projects/foo + (foo)$ ls -a $VIRTUAL_ENV + . .Python bin lib + .. .project include + (foo)$ more $VIRTUAL_ENV/.project + /Users/cewing/projects/foo + +Whoa! That command did a lot: + +* Created a new ``virtualenv`` in your ``$WORKON_HOME`` +* Created a new project directory in your ``$PROJECT_HOME`` +* Placed a ``.project`` file in your home directory with a path leading to the + associated project directory +* Activated the new virtualenv for you +* Automatically moved your present working directory to the new project + directory. + +And now, you can begin working on your ``foo`` project, secure that you will be +installing packages into the right environment. + +A Few Last Words +================ + +This quick introduction is **by no means** an exhaustive manual for either of +the packages we've talked about. There is a great deal more that they can do. +In particular, ``virtualenvwrapper`` is highly customizable, with support for +custom scripts to be hooked into every stage of the ``virtualenv`` workflow. + +I urge you to read the documentation for `virtualenv`_ and `virtualenvwrapper`_ +yourself to find out more. + +.. _virtualenv: http://www.virtualenv.org/ +.. _virtualenvwrapper: http://virtualenvwrapper.readthedocs.org diff --git a/_sources/topics/01-setting_up/vsc_as_ide.rst.txt b/_sources/topics/01-setting_up/vsc_as_ide.rst.txt new file mode 100644 index 0000000..98a0826 --- /dev/null +++ b/_sources/topics/01-setting_up/vsc_as_ide.rst.txt @@ -0,0 +1,78 @@ +.. _vsc_as_ide: + +#################################################### +Using Visual Studio Code as a lightweight Python IDE +#################################################### + +Visual Studio Code is an extensible and customizable text editor from Microsoft that provides a very minimal layout with additional tooling such as an excellent built-in terminal. + + +Requirements +============ + +Any IDE should ease your development experience by providing the following: + +* It should provide excellent, configurable syntax colorization. +* It should allow for robust tab completion. +* It should offer the ability to jump to the definition of symbols in other files. +* It should perform automatic code linting to help avoid silly mistakes. +* It should be able to interact with a Python interpreter such that when debugging, the editor will follow along with the debugger. + +Visual Studio Code requires that you perform some setup out of the box (see below for details). + + +Which Version? +============== + +There's just the latest version available to download. + +This ensures that all recent bug fixes and updates have been made. + +Visual Studio Code runs on Macs, Windows, and Linux flavors like Ubuntu, Debian, Red Hat, etc. + +Also, Visual Studio Code performs updates on itself, so there's no need to download newer versions of the app... you should already have it. + +Installation +============ + +Check out this solid video_ that will walk you through the process of setting up Visual Studio Code for Python in detail. + +.. _video: https://www.youtube.com/watch?v=TILIcrrVABg/ + +Go to the Visual Studio Code website_. + +.. _website: https://code.visualstudio.com/ + +Scroll down to the bottom of the page and you'll see links for installers to all the major OS platforms. + +Download your flavor and run the installer. + + +Basic Settings +============== + +Visual Studio Code can be used out of the box with no setup as a text editor. It automatically +recognizes file types and helpfully highlights text accordingly. To use in this manner, +write your Python files in Visual Studio Code, then run them in your Python command prompt +or Visual Studio Code's own built in terminal: Ctrl + \` (control-backtick) + + +Extending the Editor +==================== + +After you've install Visual Studio Code, there are many ways to extend it for working with Python. + +The video linked above goes into this much deeper. + +There is also a great tutorial for setting up Python here_. + +.. _here: https://code.visualstudio.com/docs/python/python-tutorial + +If you're on a Mac, be sure to set up your path_ for easy integration with the terminal. + +.. _path: https://code.visualstudio.com/docs/setup/mac + +I also recommend setting up Visual Studio Code as your default_ Git editor. + +.. _default: https://stackoverflow.com/questions/30024353/how-to-use-visual-studio-code-as-default-editor-for-git + diff --git a/_sources/topics/01-setting_up/windows_bash.rst.txt b/_sources/topics/01-setting_up/windows_bash.rst.txt new file mode 100644 index 0000000..f9c24f7 --- /dev/null +++ b/_sources/topics/01-setting_up/windows_bash.rst.txt @@ -0,0 +1,54 @@ +.. _windows_bash: + +***************************************** +Using Windows Bash for Python Development +***************************************** + +**CAUTION** -- none of the instructors in the program use this -- so this is experimental -- proceed at your own risk! + +With Windows 10, Microsoft has introduced the "Windows Subsystem for Linux" (WSL). Technically, it's not Linux at all (it is not running the Linux kernel), but it does provide an actual bash shell, and access to much (all) of the packages available in Ubuntu Linux, providing a very Linux-like environment. + +If you run Windows 10, but want to be able to work in an environment that is very much like Linux (and the OS-X command line) Windows-bash may be a good options for you. + +Using WSL to Build a Python Development Environment on Windows +============================================================== + +Here is a recent post about how to do Python with the "WSL": + +https://pbpython.com/wsl-python.html + +Let us know if it works for you! + + +Offical Docs +============ + +The following are some addition links, of that doesn't get it done: + + +Installing the System +--------------------- + +Here are MS's docs: + +https://msdn.microsoft.com/commandline/wsl/about + +and the install guide: + +https://msdn.microsoft.com/en-us/commandline/wsl/install_guide + + +Installing / running Python +--------------------------- + +To use Python from within the bash shell, you probably want a "linux" Python, rather than the native Windows installer. Unfortunately, the Ubuntu version that Windows Bash is hooked up to does not natively have the latest Python version -:( (though it may now) + +Using the environment +===================== + +The WSL provides a full linux experience -- but, most importantly, it does not suport any GUI environment. If you are comfortable with a command line editor like vim or Emacs, then you can work entirely in the bash shell. However, you may very well want to work with a nice modern GUI editor like Sublime Text or Notepad++. This is quite possible, as the systems share a file system. From the FAQ: + + One of the benefits of WSL is being able to use the same file with both Windows and Linux apps or tools. + +So you can manipulate and edit your files, as well as use the browser, etc, in regular Windows, and still run Python and git, etc in the bash shell. + diff --git a/_sources/topics/02-basic_python/index.rst.txt b/_sources/topics/02-basic_python/index.rst.txt new file mode 100644 index 0000000..f97fc0a --- /dev/null +++ b/_sources/topics/02-basic_python/index.rst.txt @@ -0,0 +1,28 @@ +Basic Python +============ + +Learning a programming language is a bit of a "bootstrap" problem: +the best way to learn is by doing, but you need the basics in order to do anything at all. So this topic jumps right in -- if you're confused at first, just keep going and it should start to make sense. + +.. toctree:: + :caption: To Read + :maxdepth: 1 + + ../../modules/HowToRunAPythonFile + ../../modules/Learning + ../../modules/BasicPython + ../../modules/Functions + +.. toctree:: + :caption: Activities + :maxdepth: 1 + + ../../exercises/codingbat.rst + +.. toctree:: + :caption: Exercises + :maxdepth: 1 + + ../../exercises/grid_printer.rst + ../../exercises/fizz_buzz.rst + diff --git a/_sources/topics/03-recursion_booleans/index.rst.txt b/_sources/topics/03-recursion_booleans/index.rst.txt new file mode 100644 index 0000000..4bf3c18 --- /dev/null +++ b/_sources/topics/03-recursion_booleans/index.rst.txt @@ -0,0 +1,20 @@ +Booleans and Recursion +====================== + +.. toctree:: + :caption: To Read + :maxdepth: 1 + + ../../modules/Booleans + ../../modules/Recursion + +.. toctree:: + :caption: Activities + :maxdepth: 1 + +.. toctree:: + :caption: Exercises + :maxdepth: 1 + + ../../exercises/series/fib_and_lucas.rst + diff --git a/_sources/topics/04-sequences_iteration/index.rst.txt b/_sources/topics/04-sequences_iteration/index.rst.txt new file mode 100644 index 0000000..8a4a790 --- /dev/null +++ b/_sources/topics/04-sequences_iteration/index.rst.txt @@ -0,0 +1,21 @@ +Sequences and Iteration +======================= + +.. toctree:: + :caption: To Read + :maxdepth: 1 + + ../../modules/Sequences + ../../modules/Iteration + +.. toctree:: + :caption: Activities + :maxdepth: 1 + + ../../exercises/slicing.rst + ../../exercises/list_lab.rst + +.. toctree:: + :caption: Exercises + :maxdepth: 1 + diff --git a/_sources/topics/05-text_handling/index.rst.txt b/_sources/topics/05-text_handling/index.rst.txt new file mode 100644 index 0000000..21517c5 --- /dev/null +++ b/_sources/topics/05-text_handling/index.rst.txt @@ -0,0 +1,23 @@ +Basic Text Handling +=================== + +.. toctree:: + :caption: To Read + :maxdepth: 1 + + ../../modules/Strings + +.. toctree:: + :caption: Activities + :maxdepth: 1 + + ../../exercises/string_formatting.rst + ../../exercises/rot13.rst + +.. toctree:: + :caption: Exercises + :maxdepth: 1 + + ../../exercises/mailroom/mailroom.rst + ../../exercises/mailroom/mailroom_tutorial.rst + diff --git a/_sources/topics/06-exceptions/index.rst.txt b/_sources/topics/06-exceptions/index.rst.txt new file mode 100644 index 0000000..2d3774a --- /dev/null +++ b/_sources/topics/06-exceptions/index.rst.txt @@ -0,0 +1,22 @@ +Exception Handling +================== + +.. toctree:: + :caption: To Read + :maxdepth: 1 + + ../../modules/Exceptions + +.. toctree:: + :caption: Activities + :maxdepth: 1 + + ../../exercises/exceptions/exceptions_lab.rst + +.. toctree:: + :caption: Exercises + :maxdepth: 1 + + ../../exercises/exceptions/except_exercise.rst + ../../exercises/mailroom/mailroom_with_exceptions.rst + diff --git a/_sources/topics/07-unit_testing/index.rst.txt b/_sources/topics/07-unit_testing/index.rst.txt new file mode 100644 index 0000000..f23f4ee --- /dev/null +++ b/_sources/topics/07-unit_testing/index.rst.txt @@ -0,0 +1,21 @@ +Unit Testing +============ + +.. toctree:: + :caption: To Read + :maxdepth: 1 + + ../../modules/Testing + ../../modules/TestDrivenDevelopment + +.. toctree:: + :caption: Activities + :maxdepth: 1 + + ../../exercises/unit_testing/unit_testing.rst + +.. toctree:: + :caption: Exercises + :maxdepth: 1 + + ../../exercises/mailroom/mailroom_with_tests.rst diff --git a/_sources/topics/08-dicts_sets/index.rst.txt b/_sources/topics/08-dicts_sets/index.rst.txt new file mode 100644 index 0000000..e8d6855 --- /dev/null +++ b/_sources/topics/08-dicts_sets/index.rst.txt @@ -0,0 +1,22 @@ +Dictionaries and Sets +===================== + +.. toctree:: + :caption: To Read + :maxdepth: 1 + + ../../modules/DictsAndSets + ../../modules/DictionaryAsSwitch + +.. toctree:: + :caption: Activities + :maxdepth: 1 + + ../../exercises/dict_lab.rst + +.. toctree:: + :caption: Exercises + :maxdepth: 1 + + ../../exercises/mailroom/mailroom_with_dicts.rst + diff --git a/_sources/topics/09-files/index.rst.txt b/_sources/topics/09-files/index.rst.txt new file mode 100644 index 0000000..8a67460 --- /dev/null +++ b/_sources/topics/09-files/index.rst.txt @@ -0,0 +1,23 @@ +File Handling +============= + +.. toctree:: + :caption: To Read + :maxdepth: 1 + + ../../modules/Files + +.. toctree:: + :caption: Activities + :maxdepth: 1 + + ../../exercises/file_processing/file_lab.rst + +.. toctree:: + :caption: Exercises + :maxdepth: 1 + + ../../exercises/file_processing/file_processing.rst + ../../exercises/mailroom/mailroom_with_files.rst + ../../exercises/trigrams/trigrams.rst + diff --git a/_sources/topics/10-modules_packages/index.rst.txt b/_sources/topics/10-modules_packages/index.rst.txt new file mode 100644 index 0000000..7fda297 --- /dev/null +++ b/_sources/topics/10-modules_packages/index.rst.txt @@ -0,0 +1,24 @@ +Modules and Packages +==================== + +.. toctree:: + :caption: To Read + :maxdepth: 1 + + ../../modules/NamingThings + ../../modules/Modules + ../../modules/Documentation + ../../modules/Packaging + +.. toctree:: + :caption: Activities + :maxdepth: 1 + + ../../exercises/packaging/package_lab + +.. toctree:: + :caption: Exercises + :maxdepth: 1 + + ../../exercises/mailroom/mailroom-pkg.rst + diff --git a/_sources/topics/11-argument_passing/index.rst.txt b/_sources/topics/11-argument_passing/index.rst.txt new file mode 100644 index 0000000..19af7cd --- /dev/null +++ b/_sources/topics/11-argument_passing/index.rst.txt @@ -0,0 +1,16 @@ +Advanced Argument Passing +========================= + +.. toctree:: + :caption: To Read + :maxdepth: 1 + + ../../modules/AdvancedArgumentPassing + ../../modules/MoreOnMutability + +.. toctree:: + :caption: Activities + :maxdepth: 1 + + ../../exercises/args_kwargs_lab + diff --git a/_sources/topics/12-comprehensions/index.rst.txt b/_sources/topics/12-comprehensions/index.rst.txt new file mode 100644 index 0000000..ec1fe1f --- /dev/null +++ b/_sources/topics/12-comprehensions/index.rst.txt @@ -0,0 +1,22 @@ +Comprehensions +============== + +.. toctree:: + :caption: To Read + :maxdepth: 1 + + ../../modules/Comprehensions + ../../modules/CollectionsModule + +.. toctree:: + :caption: Activities + :maxdepth: 1 + + ../../exercises/comprehensions_lab + +.. toctree:: + :caption: Exercises + :maxdepth: 1 + + ../../exercises/mailroom/mailroom_with_comprehensions + diff --git a/_sources/topics/13-intro_oo/index.rst.txt b/_sources/topics/13-intro_oo/index.rst.txt new file mode 100644 index 0000000..5f680de --- /dev/null +++ b/_sources/topics/13-intro_oo/index.rst.txt @@ -0,0 +1,21 @@ +Intro to Object Oriented Programing +=================================== + +.. toctree:: + :caption: To Read + :maxdepth: 1 + + ../../modules/ObjectOrientationOverview + ../../modules/PythonClasses + +.. toctree:: + :caption: Activities + :maxdepth: 1 + +.. toctree:: + :caption: Exercises + :maxdepth: 1 + + ../../exercises/oo_intro/oo_intro.rst + ../../exercises/mailroom/mailroom-oo.rst + diff --git a/_sources/topics/14-magic_methods/index.rst.txt b/_sources/topics/14-magic_methods/index.rst.txt new file mode 100644 index 0000000..3ea28d2 --- /dev/null +++ b/_sources/topics/14-magic_methods/index.rst.txt @@ -0,0 +1,23 @@ +Properties and Magic Methods +============================ + +.. toctree:: + :caption: To Read + :maxdepth: 1 + + ../../modules/Properties + ../../modules/StaticAndClassMethods + ../../modules/SpecialMethodsAndProtocols + +.. toctree:: + :caption: Exercises + :maxdepth: 1 + + ../../exercises/circle/circle_class + +.. toctree:: + :caption: Advanced Exercise + :maxdepth: 1 + + ../../exercises/sparse_array + diff --git a/_sources/topics/15-subclassing/index.rst.txt b/_sources/topics/15-subclassing/index.rst.txt new file mode 100644 index 0000000..f7a8ca8 --- /dev/null +++ b/_sources/topics/15-subclassing/index.rst.txt @@ -0,0 +1,20 @@ +Subclassing and Inheritance +=========================== + +.. toctree:: + :caption: To Read + :maxdepth: 1 + + ../../modules/SubclassingAndInheritance + +.. toctree:: + :caption: Activities + :maxdepth: 1 + +.. toctree:: + :caption: Exercises + :maxdepth: 1 + + ../../exercises/html_renderer/html_renderer.rst + ../../exercises/html_renderer/html_renderer_tutorial.rst + diff --git a/_sources/topics/16-multiple_inheritance/index.rst.txt b/_sources/topics/16-multiple_inheritance/index.rst.txt new file mode 100644 index 0000000..b0596af --- /dev/null +++ b/_sources/topics/16-multiple_inheritance/index.rst.txt @@ -0,0 +1,21 @@ +Multiple Inheritance +==================== + +.. toctree:: + :caption: To Read + :maxdepth: 1 + + ../../modules/MultipleInheritance + +.. toctree:: + :caption: Activities + :maxdepth: 1 + +.. toctree:: + :caption: Exercises + :maxdepth: 1 + +.. Add old excercise (updated) using mixins with PIL. + +.. ../../exercises/xxxxx + diff --git a/_sources/topics/17-functional_programming/index.rst.txt b/_sources/topics/17-functional_programming/index.rst.txt new file mode 100644 index 0000000..37f8243 --- /dev/null +++ b/_sources/topics/17-functional_programming/index.rst.txt @@ -0,0 +1,25 @@ +Introduction to Functional Programming +====================================== + +.. toctree:: + :caption: To Read + :maxdepth: 1 + + ../../modules/OO_vs_functional + ../../modules/Lambda + ../../modules/MapFilterReduce + ../../modules/IPythonParallel + ../../modules/Closures + +.. toctree:: + :caption: Activities + :maxdepth: 1 + + ../../exercises/lambda_magic + +.. toctree:: + :caption: Exercises + :maxdepth: 1 + + ../../exercises/trapezoid + diff --git a/_sources/topics/18-advanced_testing/index.rst.txt b/_sources/topics/18-advanced_testing/index.rst.txt new file mode 100644 index 0000000..b8435dd --- /dev/null +++ b/_sources/topics/18-advanced_testing/index.rst.txt @@ -0,0 +1,21 @@ +Advanced Testing +================ + +.. toctree:: + :caption: To Read + :maxdepth: 1 + + ../../modules/Testing_advanced + +.. toctree:: + :caption: Activities + :maxdepth: 1 + +.. Add an activity for mocking input() and doing a parameterized test. + +.. toctree:: + :caption: Exercises + :maxdepth: 1 + + ../../exercises/mailroom/mailroom-mock.rst + diff --git a/_sources/topics/18-advnaced_testing/index.rst.txt b/_sources/topics/18-advnaced_testing/index.rst.txt new file mode 100644 index 0000000..cd297c0 --- /dev/null +++ b/_sources/topics/18-advnaced_testing/index.rst.txt @@ -0,0 +1,28 @@ +Advanced Testing +================ + + +To Read +------- + +.. toctree:: + :maxdepth: 1 + + ../../modules/Testing_advanced + + +Activities +---------- + +.. Add an activity for mocking input() and doing a parameterized test. + +Exercises +--------- + +.. toctree:: + :maxdepth: 1 + + ../../exercises/mailroom/mailroom_with_full_tests.rst + + + diff --git a/_sources/topics/99-extras/index.rst.txt b/_sources/topics/99-extras/index.rst.txt new file mode 100644 index 0000000..97be441 --- /dev/null +++ b/_sources/topics/99-extras/index.rst.txt @@ -0,0 +1,37 @@ +Extra Topics +============ + +The following are some extra topics that might be of interest + +.. toctree:: + :maxdepth: 1 + + ../../modules/Pep8 + ../../modules/CodeReviews + ../../modules/PersistanceAndSerialization + ../../modules/Unicode + + ../../modules/IteratorsAndGenerators + + ../../modules/Decorators + ../../exercises/mailroom/mailroom-decorator + + ../../modules/ContextManagers + ../../exercises/context-managers-exercise + + ../../modules/MetaProgramming + ../../exercises/mailroom/mailroom-meta + + ../../modules/Logging + ../../modules/Debugging + + ../../modules/NoSQL + ../../modules/GraphDatabases + + ../../modules/Concurrency + ../../modules/Async + ../../modules/Coroutines + ../../modules/ThreadingMultiprocessing + ../../exercises/threaded_downloader + + ../../modules/Profiling diff --git a/_sources/topics/index.rst.txt b/_sources/topics/index.rst.txt new file mode 100644 index 0000000..e17c897 --- /dev/null +++ b/_sources/topics/index.rst.txt @@ -0,0 +1,31 @@ +##################### +Programming in Python +##################### + +Many of these topics can be useful on their own, but each assumes that you know concepts that were introduced earlier in the program, so working them in order can be helpful. + +.. toctree:: + :numbered: + :maxdepth: 1 + + 01-setting_up/index + 02-basic_python/index + 03-recursion_booleans/index + 04-sequences_iteration/index + 05-text_handling/index + 06-exceptions/index + 07-unit_testing/index + 08-dicts_sets/index + 09-files/index + 10-modules_packages/index + 11-argument_passing/index + 12-comprehensions/index + 13-intro_oo/index + 14-magic_methods/index + 15-subclassing/index + 16-multiple_inheritance/index + 17-functional_programming/index + 18-advanced_testing/index + 99-extras/index + + diff --git a/_static/OPP.0108.gif b/_static/OPP.0108.gif new file mode 100644 index 0000000..24e2b23 Binary files /dev/null and b/_static/OPP.0108.gif differ diff --git a/_static/UWPCE_logo_W.png b/_static/UWPCE_logo_W.png new file mode 100644 index 0000000..a6aa633 Binary files /dev/null and b/_static/UWPCE_logo_W.png differ diff --git a/_static/UWPCE_logo_full.png b/_static/UWPCE_logo_full.png new file mode 100644 index 0000000..1f728fc Binary files /dev/null and b/_static/UWPCE_logo_full.png differ diff --git a/_static/UWPCE_logo_full_W.png b/_static/UWPCE_logo_full_W.png new file mode 100644 index 0000000..a6aa633 Binary files /dev/null and b/_static/UWPCE_logo_full_W.png differ diff --git a/_static/alabaster.css b/_static/alabaster.css new file mode 100644 index 0000000..0eddaeb --- /dev/null +++ b/_static/alabaster.css @@ -0,0 +1,701 @@ +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: Georgia, serif; + font-size: 17px; + background-color: #fff; + color: #000; + margin: 0; + padding: 0; +} + + +div.document { + width: 940px; + margin: 30px auto 0 auto; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 220px; +} + +div.sphinxsidebar { + width: 220px; + font-size: 14px; + line-height: 1.5; +} + +hr { + border: 1px solid #B1B4B6; +} + +div.body { + background-color: #fff; + color: #3E4349; + padding: 0 30px 0 30px; +} + +div.body > .section { + text-align: left; +} + +div.footer { + width: 940px; + margin: 20px auto 30px auto; + font-size: 14px; + color: #888; + text-align: right; +} + +div.footer a { + color: #888; +} + +p.caption { + font-family: inherit; + font-size: inherit; +} + + +div.relations { + display: none; +} + + +div.sphinxsidebar a { + color: #444; + text-decoration: none; + border-bottom: 1px dotted #999; +} + +div.sphinxsidebar a:hover { + border-bottom: 1px solid #999; +} + +div.sphinxsidebarwrapper { + padding: 18px 10px; +} + +div.sphinxsidebarwrapper p.logo { + padding: 0; + margin: -10px 0 0 0px; + text-align: center; +} + +div.sphinxsidebarwrapper h1.logo { + margin-top: -10px; + text-align: center; + margin-bottom: 5px; + text-align: left; +} + +div.sphinxsidebarwrapper h1.logo-name { + margin-top: 0px; +} + +div.sphinxsidebarwrapper p.blurb { + margin-top: 0; + font-style: normal; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: Georgia, serif; + color: #444; + font-size: 24px; + font-weight: normal; + margin: 0 0 5px 0; + padding: 0; +} + +div.sphinxsidebar h4 { + font-size: 20px; +} + +div.sphinxsidebar h3 a { + color: #444; +} + +div.sphinxsidebar p.logo a, +div.sphinxsidebar h3 a, +div.sphinxsidebar p.logo a:hover, +div.sphinxsidebar h3 a:hover { + border: none; +} + +div.sphinxsidebar p { + color: #555; + margin: 10px 0; +} + +div.sphinxsidebar ul { + margin: 10px 0; + padding: 0; + color: #000; +} + +div.sphinxsidebar ul li.toctree-l1 > a { + font-size: 120%; +} + +div.sphinxsidebar ul li.toctree-l2 > a { + font-size: 110%; +} + +div.sphinxsidebar input { + border: 1px solid #CCC; + font-family: Georgia, serif; + font-size: 1em; +} + +div.sphinxsidebar hr { + border: none; + height: 1px; + color: #AAA; + background: #AAA; + + text-align: left; + margin-left: 0; + width: 50%; +} + +div.sphinxsidebar .badge { + border-bottom: none; +} + +div.sphinxsidebar .badge:hover { + border-bottom: none; +} + +/* To address an issue with donation coming after search */ +div.sphinxsidebar h3.donation { + margin-top: 10px; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #004B6B; + text-decoration: underline; +} + +a:hover { + color: #6D4100; + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: Georgia, serif; + font-weight: normal; + margin: 30px 0px 10px 0px; + padding: 0; +} + +div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } +div.body h2 { font-size: 180%; } +div.body h3 { font-size: 150%; } +div.body h4 { font-size: 130%; } +div.body h5 { font-size: 100%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: #DDD; + padding: 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + color: #444; + background: #EAEAEA; +} + +div.body p, div.body dd, div.body li { + line-height: 1.4em; +} + +div.admonition { + margin: 20px 0px; + padding: 10px 30px; + background-color: #EEE; + border: 1px solid #CCC; +} + +div.admonition tt.xref, div.admonition code.xref, div.admonition a tt { + background-color: #FBFBFB; + border-bottom: 1px solid #fafafa; +} + +div.admonition p.admonition-title { + font-family: Georgia, serif; + font-weight: normal; + font-size: 24px; + margin: 0 0 10px 0; + padding: 0; + line-height: 1; +} + +div.admonition p.last { + margin-bottom: 0; +} + +div.highlight { + background-color: #fff; +} + +dt:target, .highlight { + background: #FAF3E8; +} + +div.warning { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.danger { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.error { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.caution { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.attention { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.important { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.note { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.tip { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.hint { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.seealso { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.topic { + background-color: #EEE; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre, tt, code { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + font-size: 0.9em; +} + +.hll { + background-color: #FFC; + margin: 0 -12px; + padding: 0 12px; + display: block; +} + +img.screenshot { +} + +tt.descname, tt.descclassname, code.descname, code.descclassname { + font-size: 0.95em; +} + +tt.descname, code.descname { + padding-right: 0.08em; +} + +img.screenshot { + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; +} + +table.docutils { + border: 1px solid #888; + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; +} + +table.docutils td, table.docutils th { + border: 1px solid #888; + padding: 0.25em 0.7em; +} + +table.field-list, table.footnote { + border: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +table.footnote { + margin: 15px 0; + width: 100%; + border: 1px solid #EEE; + background: #FDFDFD; + font-size: 0.9em; +} + +table.footnote + table.footnote { + margin-top: -15px; + border-top: none; +} + +table.field-list th { + padding: 0 0.8em 0 0; +} + +table.field-list td { + padding: 0; +} + +table.field-list p { + margin-bottom: 0.8em; +} + +/* Cloned from + * https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68 + */ +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +table.footnote td.label { + width: .1px; + padding: 0.3em 0 0.3em 0.5em; +} + +table.footnote td { + padding: 0.3em 0.5em; +} + +dl { + margin: 0; + padding: 0; +} + +dl dd { + margin-left: 30px; +} + +blockquote { + margin: 0 0 0 30px; + padding: 0; +} + +ul, ol { + /* Matches the 30px from the narrow-screen "li > ul" selector below */ + margin: 10px 0 10px 30px; + padding: 0; +} + +pre { + background: #EEE; + padding: 7px 30px; + margin: 15px 0px; + line-height: 1.3em; +} + +div.viewcode-block:target { + background: #ffd; +} + +dl pre, blockquote pre, li pre { + margin-left: 0; + padding-left: 30px; +} + +tt, code { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ +} + +tt.xref, code.xref, a tt { + background-color: #FBFBFB; + border-bottom: 1px solid #fff; +} + +a.reference { + text-decoration: none; + border-bottom: 1px dotted #004B6B; +} + +/* Don't put an underline on images */ +a.image-reference, a.image-reference:hover { + border-bottom: none; +} + +a.reference:hover { + border-bottom: 1px solid #6D4100; +} + +a.footnote-reference { + text-decoration: none; + font-size: 0.7em; + vertical-align: top; + border-bottom: 1px dotted #004B6B; +} + +a.footnote-reference:hover { + border-bottom: 1px solid #6D4100; +} + +a:hover tt, a:hover code { + background: #EEE; +} + + +@media screen and (max-width: 870px) { + + div.sphinxsidebar { + display: none; + } + + div.document { + width: 100%; + + } + + div.documentwrapper { + margin-left: 0; + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + } + + div.bodywrapper { + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + margin-left: 0; + } + + ul { + margin-left: 0; + } + + li > ul { + /* Matches the 30px from the "ul, ol" selector above */ + margin-left: 30px; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .bodywrapper { + margin: 0; + } + + .footer { + width: auto; + } + + .github { + display: none; + } + + + +} + + + +@media screen and (max-width: 875px) { + + body { + margin: 0; + padding: 20px 30px; + } + + div.documentwrapper { + float: none; + background: #fff; + } + + div.sphinxsidebar { + display: block; + float: none; + width: 102.5%; + margin: 50px -30px -20px -30px; + padding: 10px 20px; + background: #333; + color: #FFF; + } + + div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, + div.sphinxsidebar h3 a { + color: #fff; + } + + div.sphinxsidebar a { + color: #AAA; + } + + div.sphinxsidebar p.logo { + display: none; + } + + div.document { + width: 100%; + margin: 0; + } + + div.footer { + display: none; + } + + div.bodywrapper { + margin: 0; + } + + div.body { + min-height: 0; + padding: 0; + } + + .rtd_doc_footer { + display: none; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .footer { + width: auto; + } + + .github { + display: none; + } +} + + +/* misc. */ + +.revsys-inline { + display: none!important; +} + +/* Make nested-list/multi-paragraph items look better in Releases changelog + * pages. Without this, docutils' magical list fuckery causes inconsistent + * formatting between different release sub-lists. + */ +div#changelog > div.section > ul > li > p:only-child { + margin-bottom: 0; +} + +/* Hide fugly table cell borders in ..bibliography:: directive output */ +table.docutils.citation, table.docutils.citation td, table.docutils.citation th { + border: none; + /* Below needed in some edge cases; if not applied, bottom shadows appear */ + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + + +/* relbar */ + +.related { + line-height: 30px; + width: 100%; + font-size: 0.9rem; +} + +.related.top { + border-bottom: 1px solid #EEE; + margin-bottom: 20px; +} + +.related.bottom { + border-top: 1px solid #EEE; +} + +.related ul { + padding: 0; + margin: 0; + list-style: none; +} + +.related li { + display: inline; +} + +nav#rellinks { + float: right; +} + +nav#rellinks li+li:before { + content: "|"; +} + +nav#breadcrumbs li+li:before { + content: "\00BB"; +} + +/* Hide certain items when printing */ +@media print { + div.related { + display: none; + } +} \ No newline at end of file diff --git a/_static/basic.css b/_static/basic.css new file mode 100644 index 0000000..603f6a8 --- /dev/null +++ b/_static/basic.css @@ -0,0 +1,905 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 450px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a.brackets:before, +span.brackets > a:before{ + content: "["; +} + +a.brackets:after, +span.brackets > a:after { + content: "]"; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +dl.footnote > dt, +dl.citation > dt { + float: left; + margin-right: 0.5em; +} + +dl.footnote > dd, +dl.citation > dd { + margin-bottom: 0em; +} + +dl.footnote > dd:after, +dl.citation > dd:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dt:after { + content: ":"; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/_static/big_o.png b/_static/big_o.png new file mode 100644 index 0000000..ee4e296 Binary files /dev/null and b/_static/big_o.png differ diff --git a/_static/color_git_prompt.png b/_static/color_git_prompt.png new file mode 100644 index 0000000..b85ab44 Binary files /dev/null and b/_static/color_git_prompt.png differ diff --git a/_static/coroutines_plot.png b/_static/coroutines_plot.png new file mode 100644 index 0000000..428b5a5 Binary files /dev/null and b/_static/coroutines_plot.png differ diff --git a/_static/css/badge_only.css b/_static/css/badge_only.css new file mode 100644 index 0000000..e380325 --- /dev/null +++ b/_static/css/badge_only.css @@ -0,0 +1 @@ +.fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} \ No newline at end of file diff --git a/_static/css/fonts/Roboto-Slab-Bold.woff b/_static/css/fonts/Roboto-Slab-Bold.woff new file mode 100644 index 0000000..6cb6000 Binary files /dev/null and b/_static/css/fonts/Roboto-Slab-Bold.woff differ diff --git a/_static/css/fonts/Roboto-Slab-Bold.woff2 b/_static/css/fonts/Roboto-Slab-Bold.woff2 new file mode 100644 index 0000000..7059e23 Binary files /dev/null and b/_static/css/fonts/Roboto-Slab-Bold.woff2 differ diff --git a/_static/css/fonts/Roboto-Slab-Regular.woff b/_static/css/fonts/Roboto-Slab-Regular.woff new file mode 100644 index 0000000..f815f63 Binary files /dev/null and b/_static/css/fonts/Roboto-Slab-Regular.woff differ diff --git a/_static/css/fonts/Roboto-Slab-Regular.woff2 b/_static/css/fonts/Roboto-Slab-Regular.woff2 new file mode 100644 index 0000000..f2c76e5 Binary files /dev/null and b/_static/css/fonts/Roboto-Slab-Regular.woff2 differ diff --git a/_static/css/fonts/fontawesome-webfont.eot b/_static/css/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000..e9f60ca Binary files /dev/null and b/_static/css/fonts/fontawesome-webfont.eot differ diff --git a/_static/css/fonts/fontawesome-webfont.svg b/_static/css/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000..855c845 --- /dev/null +++ b/_static/css/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_static/css/fonts/fontawesome-webfont.ttf b/_static/css/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..35acda2 Binary files /dev/null and b/_static/css/fonts/fontawesome-webfont.ttf differ diff --git a/_static/css/fonts/fontawesome-webfont.woff b/_static/css/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..400014a Binary files /dev/null and b/_static/css/fonts/fontawesome-webfont.woff differ diff --git a/_static/css/fonts/fontawesome-webfont.woff2 b/_static/css/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000..4d13fc6 Binary files /dev/null and b/_static/css/fonts/fontawesome-webfont.woff2 differ diff --git a/_static/css/fonts/lato-bold-italic.woff b/_static/css/fonts/lato-bold-italic.woff new file mode 100644 index 0000000..88ad05b Binary files /dev/null and b/_static/css/fonts/lato-bold-italic.woff differ diff --git a/_static/css/fonts/lato-bold-italic.woff2 b/_static/css/fonts/lato-bold-italic.woff2 new file mode 100644 index 0000000..c4e3d80 Binary files /dev/null and b/_static/css/fonts/lato-bold-italic.woff2 differ diff --git a/_static/css/fonts/lato-bold.woff b/_static/css/fonts/lato-bold.woff new file mode 100644 index 0000000..c6dff51 Binary files /dev/null and b/_static/css/fonts/lato-bold.woff differ diff --git a/_static/css/fonts/lato-bold.woff2 b/_static/css/fonts/lato-bold.woff2 new file mode 100644 index 0000000..bb19504 Binary files /dev/null and b/_static/css/fonts/lato-bold.woff2 differ diff --git a/_static/css/fonts/lato-normal-italic.woff b/_static/css/fonts/lato-normal-italic.woff new file mode 100644 index 0000000..76114bc Binary files /dev/null and b/_static/css/fonts/lato-normal-italic.woff differ diff --git a/_static/css/fonts/lato-normal-italic.woff2 b/_static/css/fonts/lato-normal-italic.woff2 new file mode 100644 index 0000000..3404f37 Binary files /dev/null and b/_static/css/fonts/lato-normal-italic.woff2 differ diff --git a/_static/css/fonts/lato-normal.woff b/_static/css/fonts/lato-normal.woff new file mode 100644 index 0000000..ae1307f Binary files /dev/null and b/_static/css/fonts/lato-normal.woff differ diff --git a/_static/css/fonts/lato-normal.woff2 b/_static/css/fonts/lato-normal.woff2 new file mode 100644 index 0000000..3bf9843 Binary files /dev/null and b/_static/css/fonts/lato-normal.woff2 differ diff --git a/_static/css/theme.css b/_static/css/theme.css new file mode 100644 index 0000000..0d9ae7e --- /dev/null +++ b/_static/css/theme.css @@ -0,0 +1,4 @@ +html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,.wy-nav-top a,.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs li{display:inline-block}.wy-breadcrumbs li.wy-breadcrumbs-aside{float:right}.wy-breadcrumbs li a{display:inline-block;padding:5px}.wy-breadcrumbs li a:first-child{padding-left:0}.rst-content .wy-breadcrumbs li tt,.wy-breadcrumbs li .rst-content tt,.wy-breadcrumbs li code{padding:5px;border:none;background:none}.rst-content .wy-breadcrumbs li tt.literal,.wy-breadcrumbs li .rst-content tt.literal,.wy-breadcrumbs li code.literal{color:#404040}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search>a:hover{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.field-list>dt:after,html.writer-html5 .rst-content dl.footnote>dt:after{content:":"}html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.footnote>dt>span.brackets{margin-right:.5rem}html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{font-style:italic}html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.footnote>dd p,html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{font-size:inherit;line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.field-list)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dl:not(.field-list)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.field-list)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dl:not(.field-list)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel{border:1px solid #7fbbe3;background:#e7f2fa;font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} \ No newline at end of file diff --git a/_static/custom.css b/_static/custom.css new file mode 100644 index 0000000..2a924f1 --- /dev/null +++ b/_static/custom.css @@ -0,0 +1 @@ +/* This file intentionally left blank. */ diff --git a/_static/doctools.js b/_static/doctools.js new file mode 100644 index 0000000..8cbf1b1 --- /dev/null +++ b/_static/doctools.js @@ -0,0 +1,323 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Sphinx JavaScript utilities for all documentation. + * + * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/** + * select a different prefix for underscore + */ +$u = _.noConflict(); + +/** + * make the code below compatible with browsers without + * an installed firebug like debugger +if (!window.console || !console.firebug) { + var names = ["log", "debug", "info", "warn", "error", "assert", "dir", + "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", + "profile", "profileEnd"]; + window.console = {}; + for (var i = 0; i < names.length; ++i) + window.console[names[i]] = function() {}; +} + */ + +/** + * small helper function to urldecode strings + * + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL + */ +jQuery.urldecode = function(x) { + if (!x) { + return x + } + return decodeURIComponent(x.replace(/\+/g, ' ')); +}; + +/** + * small helper function to urlencode strings + */ +jQuery.urlencode = encodeURIComponent; + +/** + * This function returns the parsed url parameters of the + * current request. Multiple values per key are supported, + * it will always return arrays of strings for the value parts. + */ +jQuery.getQueryParameters = function(s) { + if (typeof s === 'undefined') + s = document.location.search; + var parts = s.substr(s.indexOf('?') + 1).split('&'); + var result = {}; + for (var i = 0; i < parts.length; i++) { + var tmp = parts[i].split('=', 2); + var key = jQuery.urldecode(tmp[0]); + var value = jQuery.urldecode(tmp[1]); + if (key in result) + result[key].push(value); + else + result[key] = [value]; + } + return result; +}; + +/** + * highlight a given string on a jquery object by wrapping it in + * span elements with the given class name. + */ +jQuery.fn.highlightText = function(text, className) { + function highlight(node, addItems) { + if (node.nodeType === 3) { + var val = node.nodeValue; + var pos = val.toLowerCase().indexOf(text); + if (pos >= 0 && + !jQuery(node.parentNode).hasClass(className) && + !jQuery(node.parentNode).hasClass("nohighlight")) { + var span; + var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.className = className; + } + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + node.parentNode.insertBefore(span, node.parentNode.insertBefore( + document.createTextNode(val.substr(pos + text.length)), + node.nextSibling)); + node.nodeValue = val.substr(0, pos); + if (isInSVG) { + var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + var bbox = node.parentElement.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute('class', className); + addItems.push({ + "parent": node.parentNode, + "target": rect}); + } + } + } + else if (!jQuery(node).is("button, select, textarea")) { + jQuery.each(node.childNodes, function() { + highlight(this, addItems); + }); + } + } + var addItems = []; + var result = this.each(function() { + highlight(this, addItems); + }); + for (var i = 0; i < addItems.length; ++i) { + jQuery(addItems[i].parent).before(addItems[i].target); + } + return result; +}; + +/* + * backward compatibility for jQuery.browser + * This will be supported until firefox bug is fixed. + */ +if (!jQuery.browser) { + jQuery.uaMatch = function(ua) { + ua = ua.toLowerCase(); + + var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || + /(webkit)[ \/]([\w.]+)/.exec(ua) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || + /(msie) ([\w.]+)/.exec(ua) || + ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || + []; + + return { + browser: match[ 1 ] || "", + version: match[ 2 ] || "0" + }; + }; + jQuery.browser = {}; + jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; +} + +/** + * Small JavaScript module for the documentation. + */ +var Documentation = { + + init : function() { + this.fixFirefoxAnchorBug(); + this.highlightSearchWords(); + this.initIndexTable(); + if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) { + this.initOnKeyListeners(); + } + }, + + /** + * i18n support + */ + TRANSLATIONS : {}, + PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; }, + LOCALE : 'unknown', + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext : function(string) { + var translated = Documentation.TRANSLATIONS[string]; + if (typeof translated === 'undefined') + return string; + return (typeof translated === 'string') ? translated : translated[0]; + }, + + ngettext : function(singular, plural, n) { + var translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated === 'undefined') + return (n == 1) ? singular : plural; + return translated[Documentation.PLURALEXPR(n)]; + }, + + addTranslations : function(catalog) { + for (var key in catalog.messages) + this.TRANSLATIONS[key] = catalog.messages[key]; + this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); + this.LOCALE = catalog.locale; + }, + + /** + * add context elements like header anchor links + */ + addContextElements : function() { + $('div[id] > :header:first').each(function() { + $('\u00B6'). + attr('href', '#' + this.id). + attr('title', _('Permalink to this headline')). + appendTo(this); + }); + $('dt[id]').each(function() { + $('\u00B6'). + attr('href', '#' + this.id). + attr('title', _('Permalink to this definition')). + appendTo(this); + }); + }, + + /** + * workaround a firefox stupidity + * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075 + */ + fixFirefoxAnchorBug : function() { + if (document.location.hash && $.browser.mozilla) + window.setTimeout(function() { + document.location.href += ''; + }, 10); + }, + + /** + * highlight the search words provided in the url in the text + */ + highlightSearchWords : function() { + var params = $.getQueryParameters(); + var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; + if (terms.length) { + var body = $('div.body'); + if (!body.length) { + body = $('body'); + } + window.setTimeout(function() { + $.each(terms, function() { + body.highlightText(this.toLowerCase(), 'highlighted'); + }); + }, 10); + $('') + .appendTo($('#searchbox')); + } + }, + + /** + * init the domain index toggle buttons + */ + initIndexTable : function() { + var togglers = $('img.toggler').click(function() { + var src = $(this).attr('src'); + var idnum = $(this).attr('id').substr(7); + $('tr.cg-' + idnum).toggle(); + if (src.substr(-9) === 'minus.png') + $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); + else + $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); + }).css('display', ''); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { + togglers.click(); + } + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords : function() { + $('#searchbox .highlight-link').fadeOut(300); + $('span.highlighted').removeClass('highlighted'); + }, + + /** + * make the url absolute + */ + makeURL : function(relativeURL) { + return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; + }, + + /** + * get the current relative url + */ + getCurrentURL : function() { + var path = document.location.pathname; + var parts = path.split(/\//); + $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { + if (this === '..') + parts.pop(); + }); + var url = parts.join('/'); + return path.substring(url.lastIndexOf('/') + 1, path.length - 1); + }, + + initOnKeyListeners: function() { + $(document).keydown(function(event) { + var activeElementType = document.activeElement.tagName; + // don't navigate when in search box, textarea, dropdown or button + if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT' + && activeElementType !== 'BUTTON' && !event.altKey && !event.ctrlKey && !event.metaKey + && !event.shiftKey) { + switch (event.keyCode) { + case 37: // left + var prevHref = $('link[rel="prev"]').prop('href'); + if (prevHref) { + window.location.href = prevHref; + return false; + } + break; + case 39: // right + var nextHref = $('link[rel="next"]').prop('href'); + if (nextHref) { + window.location.href = nextHref; + return false; + } + break; + } + } + }); + } +}; + +// quick alias for translations +_ = Documentation.gettext; + +$(document).ready(function() { + Documentation.init(); +}); diff --git a/_static/documentation_options.js b/_static/documentation_options.js new file mode 100644 index 0000000..83fa208 --- /dev/null +++ b/_static/documentation_options.js @@ -0,0 +1,12 @@ +var DOCUMENTATION_OPTIONS = { + URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), + VERSION: '7.0', + LANGUAGE: 'None', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false +}; \ No newline at end of file diff --git a/_static/dummy b/_static/dummy new file mode 100644 index 0000000..e69de29 diff --git a/_static/file.png b/_static/file.png new file mode 100644 index 0000000..a858a41 Binary files /dev/null and b/_static/file.png differ diff --git a/_static/flags.jpg b/_static/flags.jpg new file mode 100644 index 0000000..061713d Binary files /dev/null and b/_static/flags.jpg differ diff --git a/_static/flake8_output.png b/_static/flake8_output.png new file mode 100644 index 0000000..dc44e48 Binary files /dev/null and b/_static/flake8_output.png differ diff --git a/_static/fonts/FontAwesome.otf b/_static/fonts/FontAwesome.otf new file mode 100644 index 0000000..401ec0f Binary files /dev/null and b/_static/fonts/FontAwesome.otf differ diff --git a/_static/fonts/Lato/lato-bold.eot b/_static/fonts/Lato/lato-bold.eot new file mode 100644 index 0000000..3361183 Binary files /dev/null and b/_static/fonts/Lato/lato-bold.eot differ diff --git a/_static/fonts/Lato/lato-bold.ttf b/_static/fonts/Lato/lato-bold.ttf new file mode 100644 index 0000000..29f691d Binary files /dev/null and b/_static/fonts/Lato/lato-bold.ttf differ diff --git a/_static/fonts/Lato/lato-bold.woff b/_static/fonts/Lato/lato-bold.woff new file mode 100644 index 0000000..c6dff51 Binary files /dev/null and b/_static/fonts/Lato/lato-bold.woff differ diff --git a/_static/fonts/Lato/lato-bold.woff2 b/_static/fonts/Lato/lato-bold.woff2 new file mode 100644 index 0000000..bb19504 Binary files /dev/null and b/_static/fonts/Lato/lato-bold.woff2 differ diff --git a/_static/fonts/Lato/lato-bolditalic.eot b/_static/fonts/Lato/lato-bolditalic.eot new file mode 100644 index 0000000..3d41549 Binary files /dev/null and b/_static/fonts/Lato/lato-bolditalic.eot differ diff --git a/_static/fonts/Lato/lato-bolditalic.ttf b/_static/fonts/Lato/lato-bolditalic.ttf new file mode 100644 index 0000000..f402040 Binary files /dev/null and b/_static/fonts/Lato/lato-bolditalic.ttf differ diff --git a/_static/fonts/Lato/lato-bolditalic.woff b/_static/fonts/Lato/lato-bolditalic.woff new file mode 100644 index 0000000..88ad05b Binary files /dev/null and b/_static/fonts/Lato/lato-bolditalic.woff differ diff --git a/_static/fonts/Lato/lato-bolditalic.woff2 b/_static/fonts/Lato/lato-bolditalic.woff2 new file mode 100644 index 0000000..c4e3d80 Binary files /dev/null and b/_static/fonts/Lato/lato-bolditalic.woff2 differ diff --git a/_static/fonts/Lato/lato-italic.eot b/_static/fonts/Lato/lato-italic.eot new file mode 100644 index 0000000..3f82642 Binary files /dev/null and b/_static/fonts/Lato/lato-italic.eot differ diff --git a/_static/fonts/Lato/lato-italic.ttf b/_static/fonts/Lato/lato-italic.ttf new file mode 100644 index 0000000..b4bfc9b Binary files /dev/null and b/_static/fonts/Lato/lato-italic.ttf differ diff --git a/_static/fonts/Lato/lato-italic.woff b/_static/fonts/Lato/lato-italic.woff new file mode 100644 index 0000000..76114bc Binary files /dev/null and b/_static/fonts/Lato/lato-italic.woff differ diff --git a/_static/fonts/Lato/lato-italic.woff2 b/_static/fonts/Lato/lato-italic.woff2 new file mode 100644 index 0000000..3404f37 Binary files /dev/null and b/_static/fonts/Lato/lato-italic.woff2 differ diff --git a/_static/fonts/Lato/lato-regular.eot b/_static/fonts/Lato/lato-regular.eot new file mode 100644 index 0000000..11e3f2a Binary files /dev/null and b/_static/fonts/Lato/lato-regular.eot differ diff --git a/_static/fonts/Lato/lato-regular.ttf b/_static/fonts/Lato/lato-regular.ttf new file mode 100644 index 0000000..74decd9 Binary files /dev/null and b/_static/fonts/Lato/lato-regular.ttf differ diff --git a/_static/fonts/Lato/lato-regular.woff b/_static/fonts/Lato/lato-regular.woff new file mode 100644 index 0000000..ae1307f Binary files /dev/null and b/_static/fonts/Lato/lato-regular.woff differ diff --git a/_static/fonts/Lato/lato-regular.woff2 b/_static/fonts/Lato/lato-regular.woff2 new file mode 100644 index 0000000..3bf9843 Binary files /dev/null and b/_static/fonts/Lato/lato-regular.woff2 differ diff --git a/_static/fonts/Roboto-Slab-Bold.woff b/_static/fonts/Roboto-Slab-Bold.woff new file mode 100644 index 0000000..6cb6000 Binary files /dev/null and b/_static/fonts/Roboto-Slab-Bold.woff differ diff --git a/_static/fonts/Roboto-Slab-Bold.woff2 b/_static/fonts/Roboto-Slab-Bold.woff2 new file mode 100644 index 0000000..7059e23 Binary files /dev/null and b/_static/fonts/Roboto-Slab-Bold.woff2 differ diff --git a/_static/fonts/Roboto-Slab-Light.woff b/_static/fonts/Roboto-Slab-Light.woff new file mode 100644 index 0000000..337d287 Binary files /dev/null and b/_static/fonts/Roboto-Slab-Light.woff differ diff --git a/_static/fonts/Roboto-Slab-Light.woff2 b/_static/fonts/Roboto-Slab-Light.woff2 new file mode 100644 index 0000000..20398af Binary files /dev/null and b/_static/fonts/Roboto-Slab-Light.woff2 differ diff --git a/_static/fonts/Roboto-Slab-Regular.woff b/_static/fonts/Roboto-Slab-Regular.woff new file mode 100644 index 0000000..f815f63 Binary files /dev/null and b/_static/fonts/Roboto-Slab-Regular.woff differ diff --git a/_static/fonts/Roboto-Slab-Regular.woff2 b/_static/fonts/Roboto-Slab-Regular.woff2 new file mode 100644 index 0000000..f2c76e5 Binary files /dev/null and b/_static/fonts/Roboto-Slab-Regular.woff2 differ diff --git a/_static/fonts/Roboto-Slab-Thin.woff b/_static/fonts/Roboto-Slab-Thin.woff new file mode 100644 index 0000000..6b30ea6 Binary files /dev/null and b/_static/fonts/Roboto-Slab-Thin.woff differ diff --git a/_static/fonts/Roboto-Slab-Thin.woff2 b/_static/fonts/Roboto-Slab-Thin.woff2 new file mode 100644 index 0000000..328f5bb Binary files /dev/null and b/_static/fonts/Roboto-Slab-Thin.woff2 differ diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot new file mode 100644 index 0000000..79dc8ef Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot differ diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf new file mode 100644 index 0000000..df5d1df Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf differ diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff new file mode 100644 index 0000000..6cb6000 Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff differ diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 new file mode 100644 index 0000000..7059e23 Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 differ diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot new file mode 100644 index 0000000..2f7ca78 Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot differ diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf new file mode 100644 index 0000000..eb52a79 Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf differ diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff new file mode 100644 index 0000000..f815f63 Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff differ diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 new file mode 100644 index 0000000..f2c76e5 Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 differ diff --git a/_static/fonts/fontawesome-webfont.eot b/_static/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000..e9f60ca Binary files /dev/null and b/_static/fonts/fontawesome-webfont.eot differ diff --git a/_static/fonts/fontawesome-webfont.svg b/_static/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000..855c845 --- /dev/null +++ b/_static/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_static/fonts/fontawesome-webfont.ttf b/_static/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..35acda2 Binary files /dev/null and b/_static/fonts/fontawesome-webfont.ttf differ diff --git a/_static/fonts/fontawesome-webfont.woff b/_static/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..400014a Binary files /dev/null and b/_static/fonts/fontawesome-webfont.woff differ diff --git a/_static/fonts/fontawesome-webfont.woff2 b/_static/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000..4d13fc6 Binary files /dev/null and b/_static/fonts/fontawesome-webfont.woff2 differ diff --git a/_static/fonts/lato-bold-italic.woff b/_static/fonts/lato-bold-italic.woff new file mode 100644 index 0000000..88ad05b Binary files /dev/null and b/_static/fonts/lato-bold-italic.woff differ diff --git a/_static/fonts/lato-bold-italic.woff2 b/_static/fonts/lato-bold-italic.woff2 new file mode 100644 index 0000000..c4e3d80 Binary files /dev/null and b/_static/fonts/lato-bold-italic.woff2 differ diff --git a/_static/fonts/lato-bold.woff b/_static/fonts/lato-bold.woff new file mode 100644 index 0000000..c6dff51 Binary files /dev/null and b/_static/fonts/lato-bold.woff differ diff --git a/_static/fonts/lato-bold.woff2 b/_static/fonts/lato-bold.woff2 new file mode 100644 index 0000000..bb19504 Binary files /dev/null and b/_static/fonts/lato-bold.woff2 differ diff --git a/_static/fonts/lato-normal-italic.woff b/_static/fonts/lato-normal-italic.woff new file mode 100644 index 0000000..76114bc Binary files /dev/null and b/_static/fonts/lato-normal-italic.woff differ diff --git a/_static/fonts/lato-normal-italic.woff2 b/_static/fonts/lato-normal-italic.woff2 new file mode 100644 index 0000000..3404f37 Binary files /dev/null and b/_static/fonts/lato-normal-italic.woff2 differ diff --git a/_static/fonts/lato-normal.woff b/_static/fonts/lato-normal.woff new file mode 100644 index 0000000..ae1307f Binary files /dev/null and b/_static/fonts/lato-normal.woff differ diff --git a/_static/fonts/lato-normal.woff2 b/_static/fonts/lato-normal.woff2 new file mode 100644 index 0000000..3bf9843 Binary files /dev/null and b/_static/fonts/lato-normal.woff2 differ diff --git a/_static/gil.png b/_static/gil.png new file mode 100644 index 0000000..301a39d Binary files /dev/null and b/_static/gil.png differ diff --git a/_static/git_another_commit_on_branch.png b/_static/git_another_commit_on_branch.png new file mode 100644 index 0000000..7ef6a06 Binary files /dev/null and b/_static/git_another_commit_on_branch.png differ diff --git a/_static/git_checkout_branch.png b/_static/git_checkout_branch.png new file mode 100644 index 0000000..dab12bd Binary files /dev/null and b/_static/git_checkout_branch.png differ diff --git a/_static/git_checkout_master.png b/_static/git_checkout_master.png new file mode 100644 index 0000000..dc245a7 Binary files /dev/null and b/_static/git_checkout_master.png differ diff --git a/_static/git_commit_on_branch.png b/_static/git_commit_on_branch.png new file mode 100644 index 0000000..23143d7 Binary files /dev/null and b/_static/git_commit_on_branch.png differ diff --git a/_static/git_head.png b/_static/git_head.png new file mode 100644 index 0000000..c48c40e Binary files /dev/null and b/_static/git_head.png differ diff --git a/_static/git_master_branch.png b/_static/git_master_branch.png new file mode 100644 index 0000000..9c4aeb8 Binary files /dev/null and b/_static/git_master_branch.png differ diff --git a/_static/git_merge_commit.png b/_static/git_merge_commit.png new file mode 100644 index 0000000..2df3d2d Binary files /dev/null and b/_static/git_merge_commit.png differ diff --git a/_static/git_new_branch.png b/_static/git_new_branch.png new file mode 100644 index 0000000..a0a4ef4 Binary files /dev/null and b/_static/git_new_branch.png differ diff --git a/_static/git_new_commit.png b/_static/git_new_commit.png new file mode 100644 index 0000000..012eb84 Binary files /dev/null and b/_static/git_new_commit.png differ diff --git a/_static/git_new_commit_on_master.png b/_static/git_new_commit_on_master.png new file mode 100644 index 0000000..6c25e51 Binary files /dev/null and b/_static/git_new_commit_on_master.png differ diff --git a/_static/git_simple_timeline.png b/_static/git_simple_timeline.png new file mode 100644 index 0000000..465f546 Binary files /dev/null and b/_static/git_simple_timeline.png differ diff --git a/_static/jquery-3.5.1.js b/_static/jquery-3.5.1.js new file mode 100644 index 0000000..5093733 --- /dev/null +++ b/_static/jquery-3.5.1.js @@ -0,0 +1,10872 @@ +/*! + * jQuery JavaScript Library v3.5.1 + * https://jquery.com/ + * + * Includes Sizzle.js + * https://sizzlejs.com/ + * + * Copyright JS Foundation and other contributors + * Released under the MIT license + * https://jquery.org/license + * + * Date: 2020-05-04T22:49Z + */ +( function( global, factory ) { + + "use strict"; + + if ( typeof module === "object" && typeof module.exports === "object" ) { + + // For CommonJS and CommonJS-like environments where a proper `window` + // is present, execute the factory and get jQuery. + // For environments that do not have a `window` with a `document` + // (such as Node.js), expose a factory as module.exports. + // This accentuates the need for the creation of a real `window`. + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info. + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1 +// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode +// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common +// enough that all such attempts are guarded in a try block. +"use strict"; + +var arr = []; + +var getProto = Object.getPrototypeOf; + +var slice = arr.slice; + +var flat = arr.flat ? function( array ) { + return arr.flat.call( array ); +} : function( array ) { + return arr.concat.apply( [], array ); +}; + + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var fnToString = hasOwn.toString; + +var ObjectFunctionString = fnToString.call( Object ); + +var support = {}; + +var isFunction = function isFunction( obj ) { + + // Support: Chrome <=57, Firefox <=52 + // In some browsers, typeof returns "function" for HTML elements + // (i.e., `typeof document.createElement( "object" ) === "function"`). + // We don't want to classify *any* DOM node as a function. + return typeof obj === "function" && typeof obj.nodeType !== "number"; + }; + + +var isWindow = function isWindow( obj ) { + return obj != null && obj === obj.window; + }; + + +var document = window.document; + + + + var preservedScriptAttributes = { + type: true, + src: true, + nonce: true, + noModule: true + }; + + function DOMEval( code, node, doc ) { + doc = doc || document; + + var i, val, + script = doc.createElement( "script" ); + + script.text = code; + if ( node ) { + for ( i in preservedScriptAttributes ) { + + // Support: Firefox 64+, Edge 18+ + // Some browsers don't support the "nonce" property on scripts. + // On the other hand, just using `getAttribute` is not enough as + // the `nonce` attribute is reset to an empty string whenever it + // becomes browsing-context connected. + // See https://github.com/whatwg/html/issues/2369 + // See https://html.spec.whatwg.org/#nonce-attributes + // The `node.getAttribute` check was added for the sake of + // `jQuery.globalEval` so that it can fake a nonce-containing node + // via an object. + val = node[ i ] || node.getAttribute && node.getAttribute( i ); + if ( val ) { + script.setAttribute( i, val ); + } + } + } + doc.head.appendChild( script ).parentNode.removeChild( script ); + } + + +function toType( obj ) { + if ( obj == null ) { + return obj + ""; + } + + // Support: Android <=2.3 only (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; +} +/* global Symbol */ +// Defining this global in .eslintrc.json would create a danger of using the global +// unguarded in another place, it seems safer to define global only for this module + + + +var + version = "3.5.1", + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + + // The jQuery object is actually just the init constructor 'enhanced' + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); + }; + +jQuery.fn = jQuery.prototype = { + + // The current version of jQuery being used + jquery: version, + + constructor: jQuery, + + // The default length of a jQuery object is 0 + length: 0, + + toArray: function() { + return slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + + // Return all the elements in a clean array + if ( num == null ) { + return slice.call( this ); + } + + // Return just the one element from the set + return num < 0 ? this[ num + this.length ] : this[ num ]; + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + each: function( callback ) { + return jQuery.each( this, callback ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map( this, function( elem, i ) { + return callback.call( elem, i, elem ); + } ) ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + even: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return ( i + 1 ) % 2; + } ) ); + }, + + odd: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return i % 2; + } ) ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: arr.sort, + splice: arr.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // Skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !isFunction( target ) ) { + target = {}; + } + + // Extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + + // Only deal with non-null/undefined values + if ( ( options = arguments[ i ] ) != null ) { + + // Extend the base object + for ( name in options ) { + copy = options[ name ]; + + // Prevent Object.prototype pollution + // Prevent never-ending loop + if ( name === "__proto__" || target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = Array.isArray( copy ) ) ) ) { + src = target[ name ]; + + // Ensure proper type for the source value + if ( copyIsArray && !Array.isArray( src ) ) { + clone = []; + } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { + clone = {}; + } else { + clone = src; + } + copyIsArray = false; + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend( { + + // Unique for each copy of jQuery on the page + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + // Assume jQuery is ready without the ready module + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + isPlainObject: function( obj ) { + var proto, Ctor; + + // Detect obvious negatives + // Use toString instead of jQuery.type to catch host objects + if ( !obj || toString.call( obj ) !== "[object Object]" ) { + return false; + } + + proto = getProto( obj ); + + // Objects with no prototype (e.g., `Object.create( null )`) are plain + if ( !proto ) { + return true; + } + + // Objects with prototype are plain iff they were constructed by a global Object function + Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; + return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; + }, + + isEmptyObject: function( obj ) { + var name; + + for ( name in obj ) { + return false; + } + return true; + }, + + // Evaluates a script in a provided context; falls back to the global one + // if not specified. + globalEval: function( code, options, doc ) { + DOMEval( code, { nonce: options && options.nonce }, doc ); + }, + + each: function( obj, callback ) { + var length, i = 0; + + if ( isArrayLike( obj ) ) { + length = obj.length; + for ( ; i < length; i++ ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } else { + for ( i in obj ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } + + return obj; + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArrayLike( Object( arr ) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + return arr == null ? -1 : indexOf.call( arr, elem, i ); + }, + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var length, value, + i = 0, + ret = []; + + // Go through the array, translating each of the items to their new values + if ( isArrayLike( elems ) ) { + length = elems.length; + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + // Flatten any nested arrays + return flat( ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +} ); + +if ( typeof Symbol === "function" ) { + jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; +} + +// Populate the class2type map +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), +function( _i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +} ); + +function isArrayLike( obj ) { + + // Support: real iOS 8.2 only (not reproducible in simulator) + // `in` check used to prevent JIT error (gh-2145) + // hasOwn isn't used here due to false negatives + // regarding Nodelist length in IE + var length = !!obj && "length" in obj && obj.length, + type = toType( obj ); + + if ( isFunction( obj ) || isWindow( obj ) ) { + return false; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} +var Sizzle = +/*! + * Sizzle CSS Selector Engine v2.3.5 + * https://sizzlejs.com/ + * + * Copyright JS Foundation and other contributors + * Released under the MIT license + * https://js.foundation/ + * + * Date: 2020-03-14 + */ +( function( window ) { +var i, + support, + Expr, + getText, + isXML, + tokenize, + compile, + select, + outermostContext, + sortInput, + hasDuplicate, + + // Local document vars + setDocument, + document, + docElem, + documentIsHTML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + + // Instance-specific data + expando = "sizzle" + 1 * new Date(), + preferredDoc = window.document, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + nonnativeSelectorCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + // Instance methods + hasOwn = ( {} ).hasOwnProperty, + arr = [], + pop = arr.pop, + pushNative = arr.push, + push = arr.push, + slice = arr.slice, + + // Use a stripped-down indexOf as it's faster than native + // https://jsperf.com/thor-indexof-vs-for/5 + indexOf = function( list, elem ) { + var i = 0, + len = list.length; + for ( ; i < len; i++ ) { + if ( list[ i ] === elem ) { + return i; + } + } + return -1; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|" + + "ismap|loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + + // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram + identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace + + "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+", + + // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + + // "Attribute values must be CSS identifiers [capture 5] + // or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + + whitespace + "*\\]", + + pseudos = ":(" + identifier + ")(?:\\((" + + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rwhitespace = new RegExp( whitespace + "+", "g" ), + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + + "*" ), + rdescend = new RegExp( whitespace + "|>" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + identifier + ")" ), + "CLASS": new RegExp( "^\\.(" + identifier + ")" ), + "TAG": new RegExp( "^(" + identifier + "|[*])" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + + whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + + whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), + + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rhtml = /HTML$/i, + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rnative = /^[^{]+\{\s*\[native \w/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + + // CSS escapes + // http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + "?|\\\\([^\\r\\n\\f])", "g" ), + funescape = function( escape, nonHex ) { + var high = "0x" + escape.slice( 1 ) - 0x10000; + + return nonHex ? + + // Strip the backslash prefix from a non-hex escape sequence + nonHex : + + // Replace a hexadecimal escape sequence with the encoded Unicode code point + // Support: IE <=11+ + // For values outside the Basic Multilingual Plane (BMP), manually construct a + // surrogate pair + high < 0 ? + String.fromCharCode( high + 0x10000 ) : + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }, + + // CSS string/identifier serialization + // https://drafts.csswg.org/cssom/#common-serializing-idioms + rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g, + fcssescape = function( ch, asCodePoint ) { + if ( asCodePoint ) { + + // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER + if ( ch === "\0" ) { + return "\uFFFD"; + } + + // Control characters and (dependent upon position) numbers get escaped as code points + return ch.slice( 0, -1 ) + "\\" + + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; + } + + // Other potentially-special ASCII characters get backslash-escaped + return "\\" + ch; + }, + + // Used for iframes + // See setDocument() + // Removing the function wrapper causes a "Permission Denied" + // error in IE + unloadHandler = function() { + setDocument(); + }, + + inDisabledFieldset = addCombinator( + function( elem ) { + return elem.disabled === true && elem.nodeName.toLowerCase() === "fieldset"; + }, + { dir: "parentNode", next: "legend" } + ); + +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + ( arr = slice.call( preferredDoc.childNodes ) ), + preferredDoc.childNodes + ); + + // Support: Android<4.0 + // Detect silently failing push.apply + // eslint-disable-next-line no-unused-expressions + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { apply: arr.length ? + + // Leverage slice if possible + function( target, els ) { + pushNative.apply( target, slice.call( els ) ); + } : + + // Support: IE<9 + // Otherwise append directly + function( target, els ) { + var j = target.length, + i = 0; + + // Can't trust NodeList.length + while ( ( target[ j++ ] = els[ i++ ] ) ) {} + target.length = j - 1; + } + }; +} + +function Sizzle( selector, context, results, seed ) { + var m, i, elem, nid, match, groups, newSelector, + newContext = context && context.ownerDocument, + + // nodeType defaults to 9, since context defaults to document + nodeType = context ? context.nodeType : 9; + + results = results || []; + + // Return early from calls with invalid selector or context + if ( typeof selector !== "string" || !selector || + nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { + + return results; + } + + // Try to shortcut find operations (as opposed to filters) in HTML documents + if ( !seed ) { + setDocument( context ); + context = context || document; + + if ( documentIsHTML ) { + + // If the selector is sufficiently simple, try using a "get*By*" DOM method + // (excepting DocumentFragment context, where the methods don't exist) + if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) { + + // ID selector + if ( ( m = match[ 1 ] ) ) { + + // Document context + if ( nodeType === 9 ) { + if ( ( elem = context.getElementById( m ) ) ) { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + + // Element context + } else { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( newContext && ( elem = newContext.getElementById( m ) ) && + contains( context, elem ) && + elem.id === m ) { + + results.push( elem ); + return results; + } + } + + // Type selector + } else if ( match[ 2 ] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + // Class selector + } else if ( ( m = match[ 3 ] ) && support.getElementsByClassName && + context.getElementsByClassName ) { + + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // Take advantage of querySelectorAll + if ( support.qsa && + !nonnativeSelectorCache[ selector + " " ] && + ( !rbuggyQSA || !rbuggyQSA.test( selector ) ) && + + // Support: IE 8 only + // Exclude object elements + ( nodeType !== 1 || context.nodeName.toLowerCase() !== "object" ) ) { + + newSelector = selector; + newContext = context; + + // qSA considers elements outside a scoping root when evaluating child or + // descendant combinators, which is not what we want. + // In such cases, we work around the behavior by prefixing every selector in the + // list with an ID selector referencing the scope context. + // The technique has to be used as well when a leading combinator is used + // as such selectors are not recognized by querySelectorAll. + // Thanks to Andrew Dupont for this technique. + if ( nodeType === 1 && + ( rdescend.test( selector ) || rcombinators.test( selector ) ) ) { + + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + + // We can use :scope instead of the ID hack if the browser + // supports it & if we're not changing the context. + if ( newContext !== context || !support.scope ) { + + // Capture the context ID, setting it first if necessary + if ( ( nid = context.getAttribute( "id" ) ) ) { + nid = nid.replace( rcssescape, fcssescape ); + } else { + context.setAttribute( "id", ( nid = expando ) ); + } + } + + // Prefix every selector in the list + groups = tokenize( selector ); + i = groups.length; + while ( i-- ) { + groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " + + toSelector( groups[ i ] ); + } + newSelector = groups.join( "," ); + } + + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + nonnativeSelectorCache( selector, true ); + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Create key-value caches of limited size + * @returns {function(string, object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key + " " ) > Expr.cacheLength ) { + + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return ( cache[ key + " " ] = value ); + } + return cache; +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created element and returns a boolean result + */ +function assert( fn ) { + var el = document.createElement( "fieldset" ); + + try { + return !!fn( el ); + } catch ( e ) { + return false; + } finally { + + // Remove from its parent by default + if ( el.parentNode ) { + el.parentNode.removeChild( el ); + } + + // release memory in IE + el = null; + } +} + +/** + * Adds the same handler for all of the specified attrs + * @param {String} attrs Pipe-separated list of attributes + * @param {Function} handler The method that will be applied + */ +function addHandle( attrs, handler ) { + var arr = attrs.split( "|" ), + i = arr.length; + + while ( i-- ) { + Expr.attrHandle[ arr[ i ] ] = handler; + } +} + +/** + * Checks document order of two siblings + * @param {Element} a + * @param {Element} b + * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b + */ +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && a.nodeType === 1 && b.nodeType === 1 && + a.sourceIndex - b.sourceIndex; + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( ( cur = cur.nextSibling ) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return ( name === "input" || name === "button" ) && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for :enabled/:disabled + * @param {Boolean} disabled true for :disabled; false for :enabled + */ +function createDisabledPseudo( disabled ) { + + // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable + return function( elem ) { + + // Only certain elements can match :enabled or :disabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled + if ( "form" in elem ) { + + // Check for inherited disabledness on relevant non-disabled elements: + // * listed form-associated elements in a disabled fieldset + // https://html.spec.whatwg.org/multipage/forms.html#category-listed + // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled + // * option elements in a disabled optgroup + // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled + // All such elements have a "form" property. + if ( elem.parentNode && elem.disabled === false ) { + + // Option elements defer to a parent optgroup if present + if ( "label" in elem ) { + if ( "label" in elem.parentNode ) { + return elem.parentNode.disabled === disabled; + } else { + return elem.disabled === disabled; + } + } + + // Support: IE 6 - 11 + // Use the isDisabled shortcut property to check for disabled fieldset ancestors + return elem.isDisabled === disabled || + + // Where there is no isDisabled, check manually + /* jshint -W018 */ + elem.isDisabled !== !disabled && + inDisabledFieldset( elem ) === disabled; + } + + return elem.disabled === disabled; + + // Try to winnow out elements that can't be disabled before trusting the disabled property. + // Some victims get caught in our net (label, legend, menu, track), but it shouldn't + // even exist on them, let alone have a boolean value. + } else if ( "label" in elem ) { + return elem.disabled === disabled; + } + + // Remaining elements are neither :enabled nor :disabled + return false; + }; +} + +/** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ +function createPositionalPseudo( fn ) { + return markFunction( function( argument ) { + argument = +argument; + return markFunction( function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ ( j = matchIndexes[ i ] ) ] ) { + seed[ j ] = !( matches[ j ] = seed[ j ] ); + } + } + } ); + } ); +} + +/** + * Checks a node for validity as a Sizzle context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +// Expose support vars for convenience +support = Sizzle.support = {}; + +/** + * Detects XML nodes + * @param {Element|Object} elem An element or a document + * @returns {Boolean} True iff elem is a non-HTML XML node + */ +isXML = Sizzle.isXML = function( elem ) { + var namespace = elem.namespaceURI, + docElem = ( elem.ownerDocument || elem ).documentElement; + + // Support: IE <=8 + // Assume HTML when documentElement doesn't yet exist, such as inside loading iframes + // https://bugs.jquery.com/ticket/4833 + return !rhtml.test( namespace || docElem && docElem.nodeName || "HTML" ); +}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var hasCompare, subWindow, + doc = node ? node.ownerDocument || node : preferredDoc; + + // Return early if doc is invalid or already selected + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( doc == document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Update global variables + document = doc; + docElem = document.documentElement; + documentIsHTML = !isXML( document ); + + // Support: IE 9 - 11+, Edge 12 - 18+ + // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936) + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( preferredDoc != document && + ( subWindow = document.defaultView ) && subWindow.top !== subWindow ) { + + // Support: IE 11, Edge + if ( subWindow.addEventListener ) { + subWindow.addEventListener( "unload", unloadHandler, false ); + + // Support: IE 9 - 10 only + } else if ( subWindow.attachEvent ) { + subWindow.attachEvent( "onunload", unloadHandler ); + } + } + + // Support: IE 8 - 11+, Edge 12 - 18+, Chrome <=16 - 25 only, Firefox <=3.6 - 31 only, + // Safari 4 - 5 only, Opera <=11.6 - 12.x only + // IE/Edge & older browsers don't support the :scope pseudo-class. + // Support: Safari 6.0 only + // Safari 6.0 supports :scope but it's an alias of :root there. + support.scope = assert( function( el ) { + docElem.appendChild( el ).appendChild( document.createElement( "div" ) ); + return typeof el.querySelectorAll !== "undefined" && + !el.querySelectorAll( ":scope fieldset div" ).length; + } ); + + /* Attributes + ---------------------------------------------------------------------- */ + + // Support: IE<8 + // Verify that getAttribute really returns attributes and not properties + // (excepting IE8 booleans) + support.attributes = assert( function( el ) { + el.className = "i"; + return !el.getAttribute( "className" ); + } ); + + /* getElement(s)By* + ---------------------------------------------------------------------- */ + + // Check if getElementsByTagName("*") returns only elements + support.getElementsByTagName = assert( function( el ) { + el.appendChild( document.createComment( "" ) ); + return !el.getElementsByTagName( "*" ).length; + } ); + + // Support: IE<9 + support.getElementsByClassName = rnative.test( document.getElementsByClassName ); + + // Support: IE<10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programmatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert( function( el ) { + docElem.appendChild( el ).id = expando; + return !document.getElementsByName || !document.getElementsByName( expando ).length; + } ); + + // ID filter and find + if ( support.getById ) { + Expr.filter[ "ID" ] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute( "id" ) === attrId; + }; + }; + Expr.find[ "ID" ] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var elem = context.getElementById( id ); + return elem ? [ elem ] : []; + } + }; + } else { + Expr.filter[ "ID" ] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== "undefined" && + elem.getAttributeNode( "id" ); + return node && node.value === attrId; + }; + }; + + // Support: IE 6 - 7 only + // getElementById is not reliable as a find shortcut + Expr.find[ "ID" ] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var node, i, elems, + elem = context.getElementById( id ); + + if ( elem ) { + + // Verify the id attribute + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + + // Fall back on getElementsByName + elems = context.getElementsByName( id ); + i = 0; + while ( ( elem = elems[ i++ ] ) ) { + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + } + } + + return []; + } + }; + } + + // Tag + Expr.find[ "TAG" ] = support.getElementsByTagName ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( tag ); + + // DocumentFragment nodes don't have gEBTN + } else if ( support.qsa ) { + return context.querySelectorAll( tag ); + } + } : + + function( tag, context ) { + var elem, + tmp = [], + i = 0, + + // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( ( elem = results[ i++ ] ) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Class + Expr.find[ "CLASS" ] = support.getElementsByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21) + // We allow this because of a bug in IE8/9 that throws an error + // whenever `document.activeElement` is accessed on an iframe + // So, we allow :focus to pass through QSA all the time to avoid the IE error + // See https://bugs.jquery.com/ticket/13378 + rbuggyQSA = []; + + if ( ( support.qsa = rnative.test( document.querySelectorAll ) ) ) { + + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert( function( el ) { + + var input; + + // Select is set to empty string on purpose + // This is to test IE's treatment of not explicitly + // setting a boolean content attribute, + // since its presence should be enough + // https://bugs.jquery.com/ticket/12359 + docElem.appendChild( el ).innerHTML = "" + + ""; + + // Support: IE8, Opera 11-12.16 + // Nothing should be selected when empty strings follow ^= or $= or *= + // The test attribute must be unknown in Opera but "safe" for WinRT + // https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section + if ( el.querySelectorAll( "[msallowcapture^='']" ).length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } + + // Support: IE8 + // Boolean attributes and "value" are not treated correctly + if ( !el.querySelectorAll( "[selected]" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+ + if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) { + rbuggyQSA.push( "~=" ); + } + + // Support: IE 11+, Edge 15 - 18+ + // IE 11/Edge don't find elements on a `[name='']` query in some cases. + // Adding a temporary attribute to the document before the selection works + // around the issue. + // Interestingly, IE 10 & older don't seem to have the issue. + input = document.createElement( "input" ); + input.setAttribute( "name", "" ); + el.appendChild( input ); + if ( !el.querySelectorAll( "[name='']" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*name" + whitespace + "*=" + + whitespace + "*(?:''|\"\")" ); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !el.querySelectorAll( ":checked" ).length ) { + rbuggyQSA.push( ":checked" ); + } + + // Support: Safari 8+, iOS 8+ + // https://bugs.webkit.org/show_bug.cgi?id=136851 + // In-page `selector#id sibling-combinator selector` fails + if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) { + rbuggyQSA.push( ".#.+[+~]" ); + } + + // Support: Firefox <=3.6 - 5 only + // Old Firefox doesn't throw on a badly-escaped identifier. + el.querySelectorAll( "\\\f" ); + rbuggyQSA.push( "[\\r\\n\\f]" ); + } ); + + assert( function( el ) { + el.innerHTML = "" + + ""; + + // Support: Windows 8 Native Apps + // The type and name attributes are restricted during .innerHTML assignment + var input = document.createElement( "input" ); + input.setAttribute( "type", "hidden" ); + el.appendChild( input ).setAttribute( "name", "D" ); + + // Support: IE8 + // Enforce case-sensitivity of name attribute + if ( el.querySelectorAll( "[name=d]" ).length ) { + rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( el.querySelectorAll( ":enabled" ).length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Support: IE9-11+ + // IE's :disabled selector does not pick up the children of disabled fieldsets + docElem.appendChild( el ).disabled = true; + if ( el.querySelectorAll( ":disabled" ).length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Support: Opera 10 - 11 only + // Opera 10-11 does not throw on post-comma invalid pseudos + el.querySelectorAll( "*,:x" ); + rbuggyQSA.push( ",.*:" ); + } ); + } + + if ( ( support.matchesSelector = rnative.test( ( matches = docElem.matches || + docElem.webkitMatchesSelector || + docElem.mozMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector ) ) ) ) { + + assert( function( el ) { + + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( el, "*" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( el, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + } ); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) ); + rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join( "|" ) ); + + /* Contains + ---------------------------------------------------------------------- */ + hasCompare = rnative.test( docElem.compareDocumentPosition ); + + // Element contains another + // Purposefully self-exclusive + // As in, an element does not contain itself + contains = hasCompare || rnative.test( docElem.contains ) ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + ) ); + } : + function( a, b ) { + if ( b ) { + while ( ( b = b.parentNode ) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + /* Sorting + ---------------------------------------------------------------------- */ + + // Document order sorting + sortOrder = hasCompare ? + function( a, b ) { + + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + // Calculate position if both inputs belong to the same document + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if ( compare & 1 || + ( !support.sortDetached && b.compareDocumentPosition( a ) === compare ) ) { + + // Choose the first element that is related to our preferred document + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( a == document || a.ownerDocument == preferredDoc && + contains( preferredDoc, a ) ) { + return -1; + } + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( b == document || b.ownerDocument == preferredDoc && + contains( preferredDoc, b ) ) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + } : + function( a, b ) { + + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Parentless nodes are either documents or disconnected + if ( !aup || !bup ) { + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + /* eslint-disable eqeqeq */ + return a == document ? -1 : + b == document ? 1 : + /* eslint-enable eqeqeq */ + aup ? -1 : + bup ? 1 : + sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( ( cur = cur.parentNode ) ) { + ap.unshift( cur ); + } + cur = b; + while ( ( cur = cur.parentNode ) ) { + bp.unshift( cur ); + } + + // Walk down the tree looking for a discrepancy + while ( ap[ i ] === bp[ i ] ) { + i++; + } + + return i ? + + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[ i ], bp[ i ] ) : + + // Otherwise nodes in our document sort first + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + /* eslint-disable eqeqeq */ + ap[ i ] == preferredDoc ? -1 : + bp[ i ] == preferredDoc ? 1 : + /* eslint-enable eqeqeq */ + 0; + }; + + return document; +}; + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + setDocument( elem ); + + if ( support.matchesSelector && documentIsHTML && + !nonnativeSelectorCache[ expr + " " ] && + ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { + + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch ( e ) { + nonnativeSelectorCache( expr, true ); + } + } + + return Sizzle( expr, document, null, [ elem ] ).length > 0; +}; + +Sizzle.contains = function( context, elem ) { + + // Set document vars if needed + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( ( context.ownerDocument || context ) != document ) { + setDocument( context ); + } + return contains( context, elem ); +}; + +Sizzle.attr = function( elem, name ) { + + // Set document vars if needed + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( ( elem.ownerDocument || elem ) != document ) { + setDocument( elem ); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + + // Don't get fooled by Object.prototype properties (jQuery #13807) + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; + + return val !== undefined ? + val : + support.attributes || !documentIsHTML ? + elem.getAttribute( name ) : + ( val = elem.getAttributeNode( name ) ) && val.specified ? + val.value : + null; +}; + +Sizzle.escape = function( sel ) { + return ( sel + "" ).replace( rcssescape, fcssescape ); +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + sortInput = !support.sortStable && results.slice( 0 ); + results.sort( sortOrder ); + + if ( hasDuplicate ) { + while ( ( elem = results[ i++ ] ) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + + return results; +}; + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + + // If no nodeType, this is expected to be an array + while ( ( node = elem[ i++ ] ) ) { + + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + + // Use textContent for elements + // innerText usage removed for consistency of new lines (jQuery #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + + // Do not include comment or processing instruction nodes + + return ret; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[ 1 ] = match[ 1 ].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[ 3 ] = ( match[ 3 ] || match[ 4 ] || + match[ 5 ] || "" ).replace( runescape, funescape ); + + if ( match[ 2 ] === "~=" ) { + match[ 3 ] = " " + match[ 3 ] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[ 1 ] = match[ 1 ].toLowerCase(); + + if ( match[ 1 ].slice( 0, 3 ) === "nth" ) { + + // nth-* requires argument + if ( !match[ 3 ] ) { + Sizzle.error( match[ 0 ] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[ 4 ] = +( match[ 4 ] ? + match[ 5 ] + ( match[ 6 ] || 1 ) : + 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) ); + match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" ); + + // other types prohibit arguments + } else if ( match[ 3 ] ) { + Sizzle.error( match[ 0 ] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var excess, + unquoted = !match[ 6 ] && match[ 2 ]; + + if ( matchExpr[ "CHILD" ].test( match[ 0 ] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[ 3 ] ) { + match[ 2 ] = match[ 4 ] || match[ 5 ] || ""; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + + // Get excess from tokenize (recursively) + ( excess = tokenize( unquoted, true ) ) && + + // advance to the next closing parenthesis + ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length ) ) { + + // excess is a negative index + match[ 0 ] = match[ 0 ].slice( 0, excess ); + match[ 2 ] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + "TAG": function( nodeNameSelector ) { + var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { + return true; + } : + function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + ( pattern = new RegExp( "(^|" + whitespace + + ")" + className + "(" + whitespace + "|$)" ) ) && classCache( + className, function( elem ) { + return pattern.test( + typeof elem.className === "string" && elem.className || + typeof elem.getAttribute !== "undefined" && + elem.getAttribute( "class" ) || + "" + ); + } ); + }, + + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + /* eslint-disable max-len */ + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.slice( -check.length ) === check : + operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : + false; + /* eslint-enable max-len */ + + }; + }, + + "CHILD": function( type, what, _argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, _context, xml ) { + var cache, uniqueCache, outerCache, node, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType, + diff = false; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( ( node = node[ dir ] ) ) { + if ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) { + + return false; + } + } + + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + + // Seek `elem` from a previously-cached index + + // ...in a gzip-friendly way + node = parent; + outerCache = node[ expando ] || ( node[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + ( outerCache[ node.uniqueID ] = {} ); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex && cache[ 2 ]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( ( node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + ( diff = nodeIndex = 0 ) || start.pop() ) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + uniqueCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + } else { + + // Use previously-cached element index if available + if ( useCache ) { + + // ...in a gzip-friendly way + node = elem; + outerCache = node[ expando ] || ( node[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + ( outerCache[ node.uniqueID ] = {} ); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex; + } + + // xml :nth-child(...) + // or :nth-last-child(...) or :nth(-last)?-of-type(...) + if ( diff === false ) { + + // Use the same loop as above to seek `elem` from the start + while ( ( node = ++nodeIndex && node && node[ dir ] || + ( diff = nodeIndex = 0 ) || start.pop() ) ) { + + if ( ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) && + ++diff ) { + + // Cache the index of each encountered element + if ( useCache ) { + outerCache = node[ expando ] || + ( node[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + ( outerCache[ node.uniqueID ] = {} ); + + uniqueCache[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction( function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf( seed, matched[ i ] ); + seed[ idx ] = !( matches[ idx ] = matched[ i ] ); + } + } ) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + + // Potentially complex pseudos + "not": markFunction( function( selector ) { + + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction( function( seed, matches, _context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( ( elem = unmatched[ i ] ) ) { + seed[ i ] = !( matches[ i ] = elem ); + } + } + } ) : + function( elem, _context, xml ) { + input[ 0 ] = elem; + matcher( input, null, xml, results ); + + // Don't keep the element (issue #299) + input[ 0 ] = null; + return !results.pop(); + }; + } ), + + "has": markFunction( function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + } ), + + "contains": markFunction( function( text ) { + text = text.replace( runescape, funescape ); + return function( elem ) { + return ( elem.textContent || getText( elem ) ).indexOf( text ) > -1; + }; + } ), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + + // lang value must be a valid identifier + if ( !ridentifier.test( lang || "" ) ) { + Sizzle.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( ( elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute( "xml:lang" ) || elem.getAttribute( "lang" ) ) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( ( elem = elem.parentNode ) && elem.nodeType === 1 ); + return false; + }; + } ), + + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + "root": function( elem ) { + return elem === docElem; + }, + + "focus": function( elem ) { + return elem === document.activeElement && + ( !document.hasFocus || document.hasFocus() ) && + !!( elem.type || elem.href || ~elem.tabIndex ); + }, + + // Boolean properties + "enabled": createDisabledPseudo( false ), + "disabled": createDisabledPseudo( true ), + + "checked": function( elem ) { + + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return ( nodeName === "input" && !!elem.checked ) || + ( nodeName === "option" && !!elem.selected ); + }, + + "selected": function( elem ) { + + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + // eslint-disable-next-line no-unused-expressions + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function( elem ) { + + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeType < 6 ) { + return false; + } + } + return true; + }, + + "parent": function( elem ) { + return !Expr.pseudos[ "empty" ]( elem ); + }, + + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function( elem ) { + var attr; + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + + // Support: IE<8 + // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" + ( ( attr = elem.getAttribute( "type" ) ) == null || + attr.toLowerCase() === "text" ); + }, + + // Position-in-collection + "first": createPositionalPseudo( function() { + return [ 0 ]; + } ), + + "last": createPositionalPseudo( function( _matchIndexes, length ) { + return [ length - 1 ]; + } ), + + "eq": createPositionalPseudo( function( _matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + } ), + + "even": createPositionalPseudo( function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + "odd": createPositionalPseudo( function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + "lt": createPositionalPseudo( function( matchIndexes, length, argument ) { + var i = argument < 0 ? + argument + length : + argument > length ? + length : + argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + "gt": createPositionalPseudo( function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ) + } +}; + +Expr.pseudos[ "nth" ] = Expr.pseudos[ "eq" ]; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +// Easy API for creating new setFilters +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); + +tokenize = Sizzle.tokenize = function( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || ( match = rcomma.exec( soFar ) ) ) { + if ( match ) { + + // Don't consume trailing commas as valid + soFar = soFar.slice( match[ 0 ].length ) || soFar; + } + groups.push( ( tokens = [] ) ); + } + + matched = false; + + // Combinators + if ( ( match = rcombinators.exec( soFar ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + + // Cast descendant combinators to space + type: match[ 0 ].replace( rtrim, " " ) + } ); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( ( match = matchExpr[ type ].exec( soFar ) ) && ( !preFilters[ type ] || + ( match = preFilters[ type ]( match ) ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + type: type, + matches: match + } ); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +}; + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[ i ].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + skip = combinator.next, + key = skip || dir, + checkNonElements = base && key === "parentNode", + doneName = done++; + + return combinator.first ? + + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + return false; + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var oldCache, uniqueCache, outerCache, + newCache = [ dirruns, doneName ]; + + // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching + if ( xml ) { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || ( elem[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ elem.uniqueID ] || + ( outerCache[ elem.uniqueID ] = {} ); + + if ( skip && skip === elem.nodeName.toLowerCase() ) { + elem = elem[ dir ] || elem; + } else if ( ( oldCache = uniqueCache[ key ] ) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return ( newCache[ 2 ] = oldCache[ 2 ] ); + } else { + + // Reuse newcache so results back-propagate to previous elements + uniqueCache[ key ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) { + return true; + } + } + } + } + } + return false; + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[ i ]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[ 0 ]; +} + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[ i ], results ); + } + return results; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( ( elem = unmatched[ i ] ) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction( function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( + selector || "*", + context.nodeType ? [ context ] : context, + [] + ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( ( elem = temp[ i ] ) ) { + matcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem ); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( ( elem = matcherOut[ i ] ) ) { + + // Restore matcherIn since elem is not yet a final match + temp.push( ( matcherIn[ i ] = elem ) ); + } + } + postFinder( null, ( matcherOut = [] ), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( ( elem = matcherOut[ i ] ) && + ( temp = postFinder ? indexOf( seed, elem ) : preMap[ i ] ) > -1 ) { + + seed[ temp ] = !( results[ temp ] = elem ); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + } ); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[ 0 ].type ], + implicitRelative = leadingRelative || Expr.relative[ " " ], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + ( checkContext = context ).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + + // Avoid hanging onto element (issue #299) + checkContext = null; + return ret; + } ]; + + for ( ; i < len; i++ ) { + if ( ( matcher = Expr.relative[ tokens[ i ].type ] ) ) { + matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; + } else { + matcher = Expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[ j ].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens + .slice( 0, i - 1 ) + .concat( { value: tokens[ i - 2 ].type === " " ? "*" : "" } ) + ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, outermost ) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find[ "TAG" ]( "*", outermost ), + + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ), + len = elems.length; + + if ( outermost ) { + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + outermostContext = context == document || context || outermost; + } + + // Add elements passing elementMatchers directly to results + // Support: IE<9, Safari + // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id + for ( ; i !== len && ( elem = elems[ i ] ) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( !context && elem.ownerDocument != document ) { + setDocument( elem ); + xml = !documentIsHTML; + } + while ( ( matcher = elementMatchers[ j++ ] ) ) { + if ( matcher( elem, context || document, xml ) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + + // They will have gone through all possible matchers + if ( ( elem = !matcher && elem ) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // `i` is now the count of elements visited above, and adding it to `matchedCount` + // makes the latter nonnegative. + matchedCount += i; + + // Apply set filters to unmatched elements + // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` + // equals `i`), unless we didn't visit _any_ elements in the above loop because we have + // no element matchers and no seed. + // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that + // case, which will result in a "00" `matchedCount` that differs from `i` but is also + // numerically zero. + if ( bySet && i !== matchedCount ) { + j = 0; + while ( ( matcher = setMatchers[ j++ ] ) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !( unmatched[ i ] || setMatched[ i ] ) ) { + setMatched[ i ] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + + // Generate a function of recursive functions that can be used to check each element + if ( !match ) { + match = tokenize( selector ); + } + i = match.length; + while ( i-- ) { + cached = matcherFromTokens( match[ i ] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( + selector, + matcherFromGroupMatchers( elementMatchers, setMatchers ) + ); + + // Save selector and tokenization + cached.selector = selector; + } + return cached; +}; + +/** + * A low-level selection function that works with Sizzle's compiled + * selector functions + * @param {String|Function} selector A selector or a pre-compiled + * selector function built with Sizzle.compile + * @param {Element} context + * @param {Array} [results] + * @param {Array} [seed] A set of elements to match against + */ +select = Sizzle.select = function( selector, context, results, seed ) { + var i, tokens, token, type, find, + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( ( selector = compiled.selector || selector ) ); + + results = results || []; + + // Try to minimize operations if there is only one selector in the list and no seed + // (the latter of which guarantees us context) + if ( match.length === 1 ) { + + // Reduce context if the leading compound selector is an ID + tokens = match[ 0 ] = match[ 0 ].slice( 0 ); + if ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === "ID" && + context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[ 1 ].type ] ) { + + context = ( Expr.find[ "ID" ]( token.matches[ 0 ] + .replace( runescape, funescape ), context ) || [] )[ 0 ]; + if ( !context ) { + return results; + + // Precompiled matchers will still verify ancestry, so step up a level + } else if ( compiled ) { + context = context.parentNode; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr[ "needsContext" ].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[ i ]; + + // Abort if we hit a combinator + if ( Expr.relative[ ( type = token.type ) ] ) { + break; + } + if ( ( find = Expr.find[ type ] ) ) { + + // Search, expanding context for leading sibling combinators + if ( ( seed = find( + token.matches[ 0 ].replace( runescape, funescape ), + rsibling.test( tokens[ 0 ].type ) && testContext( context.parentNode ) || + context + ) ) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; + } + + break; + } + } + } + } + + // Compile and execute a filtering function if one is not provided + // Provide `match` to avoid retokenization if we modified the selector above + ( compiled || compile( selector, match ) )( + seed, + context, + !documentIsHTML, + results, + !context || rsibling.test( selector ) && testContext( context.parentNode ) || context + ); + return results; +}; + +// One-time assignments + +// Sort stability +support.sortStable = expando.split( "" ).sort( sortOrder ).join( "" ) === expando; + +// Support: Chrome 14-35+ +// Always assume duplicates if they aren't passed to the comparison function +support.detectDuplicates = !!hasDuplicate; + +// Initialize against the default document +setDocument(); + +// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) +// Detached nodes confoundingly follow *each other* +support.sortDetached = assert( function( el ) { + + // Should return 1, but returns 4 (following) + return el.compareDocumentPosition( document.createElement( "fieldset" ) ) & 1; +} ); + +// Support: IE<8 +// Prevent attribute/property "interpolation" +// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !assert( function( el ) { + el.innerHTML = ""; + return el.firstChild.getAttribute( "href" ) === "#"; +} ) ) { + addHandle( "type|href|height|width", function( elem, name, isXML ) { + if ( !isXML ) { + return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); + } + } ); +} + +// Support: IE<9 +// Use defaultValue in place of getAttribute("value") +if ( !support.attributes || !assert( function( el ) { + el.innerHTML = ""; + el.firstChild.setAttribute( "value", "" ); + return el.firstChild.getAttribute( "value" ) === ""; +} ) ) { + addHandle( "value", function( elem, _name, isXML ) { + if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { + return elem.defaultValue; + } + } ); +} + +// Support: IE<9 +// Use getAttributeNode to fetch booleans when getAttribute lies +if ( !assert( function( el ) { + return el.getAttribute( "disabled" ) == null; +} ) ) { + addHandle( booleans, function( elem, name, isXML ) { + var val; + if ( !isXML ) { + return elem[ name ] === true ? name.toLowerCase() : + ( val = elem.getAttributeNode( name ) ) && val.specified ? + val.value : + null; + } + } ); +} + +return Sizzle; + +} )( window ); + + + +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; + +// Deprecated +jQuery.expr[ ":" ] = jQuery.expr.pseudos; +jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; +jQuery.escapeSelector = Sizzle.escape; + + + + +var dir = function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; +}; + + +var siblings = function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; +}; + + +var rneedsContext = jQuery.expr.match.needsContext; + + + +function nodeName( elem, name ) { + + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + +}; +var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i ); + + + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + return !!qualifier.call( elem, i, elem ) !== not; + } ); + } + + // Single element + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + } ); + } + + // Arraylike of elements (jQuery, arguments, Array) + if ( typeof qualifier !== "string" ) { + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) > -1 ) !== not; + } ); + } + + // Filtered directly for both simple and complex selectors + return jQuery.filter( qualifier, elements, not ); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + if ( elems.length === 1 && elem.nodeType === 1 ) { + return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : []; + } + + return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + } ) ); +}; + +jQuery.fn.extend( { + find: function( selector ) { + var i, ret, + len = this.length, + self = this; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter( function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + } ) ); + } + + ret = this.pushStack( [] ); + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + return len > 1 ? jQuery.uniqueSort( ret ) : ret; + }, + filter: function( selector ) { + return this.pushStack( winnow( this, selector || [], false ) ); + }, + not: function( selector ) { + return this.pushStack( winnow( this, selector || [], true ) ); + }, + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +} ); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) +var rootjQuery, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + // Shortcut simple #id case for speed + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/, + + init = jQuery.fn.init = function( selector, context, root ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Method init() accepts an alternate rootjQuery + // so migrate can support jQuery.sub (gh-2101) + root = root || rootjQuery; + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector[ 0 ] === "<" && + selector[ selector.length - 1 ] === ">" && + selector.length >= 3 ) { + + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && ( match[ 1 ] || !context ) ) { + + // HANDLE: $(html) -> $(array) + if ( match[ 1 ] ) { + context = context instanceof jQuery ? context[ 0 ] : context; + + // Option to run scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge( this, jQuery.parseHTML( + match[ 1 ], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + + // Properties of context are called as methods if possible + if ( isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[ 2 ] ); + + if ( elem ) { + + // Inject the element directly into the jQuery object + this[ 0 ] = elem; + this.length = 1; + } + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || root ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this[ 0 ] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( isFunction( selector ) ) { + return root.ready !== undefined ? + root.ready( selector ) : + + // Execute immediately if ready is not present + selector( jQuery ); + } + + return jQuery.makeArray( selector, this ); + }; + +// Give the init function the jQuery prototype for later instantiation +init.prototype = jQuery.fn; + +// Initialize central reference +rootjQuery = jQuery( document ); + + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + + // Methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend( { + has: function( target ) { + var targets = jQuery( target, this ), + l = targets.length; + + return this.filter( function() { + var i = 0; + for ( ; i < l; i++ ) { + if ( jQuery.contains( this, targets[ i ] ) ) { + return true; + } + } + } ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + targets = typeof selectors !== "string" && jQuery( selectors ); + + // Positional selectors never match, since there's no _selection_ context + if ( !rneedsContext.test( selectors ) ) { + for ( ; i < l; i++ ) { + for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { + + // Always skip document fragments + if ( cur.nodeType < 11 && ( targets ? + targets.index( cur ) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector( cur, selectors ) ) ) { + + matched.push( cur ); + break; + } + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); + }, + + // Determine the position of an element within the set + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + // Index in selector + if ( typeof elem === "string" ) { + return indexOf.call( jQuery( elem ), this[ 0 ] ); + } + + // Locate the position of the desired element + return indexOf.call( this, + + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.uniqueSort( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + } +} ); + +function sibling( cur, dir ) { + while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} + return cur; +} + +jQuery.each( { + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, _i, until ) { + return dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, _i, until ) { + return dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, _i, until ) { + return dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return siblings( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return siblings( elem.firstChild ); + }, + contents: function( elem ) { + if ( elem.contentDocument != null && + + // Support: IE 11+ + // elements with no `data` attribute has an object + // `contentDocument` with a `null` prototype. + getProto( elem.contentDocument ) ) { + + return elem.contentDocument; + } + + // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only + // Treat the template element as a regular one in browsers that + // don't support it. + if ( nodeName( elem, "template" ) ) { + elem = elem.content || elem; + } + + return jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var matched = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + matched = jQuery.filter( selector, matched ); + } + + if ( this.length > 1 ) { + + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + jQuery.uniqueSort( matched ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + matched.reverse(); + } + } + + return this.pushStack( matched ); + }; +} ); +var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g ); + + + +// Convert String-formatted options into Object-formatted ones +function createOptions( options ) { + var object = {}; + jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) { + object[ flag ] = true; + } ); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + createOptions( options ) : + jQuery.extend( {}, options ); + + var // Flag to know if list is currently firing + firing, + + // Last fire value for non-forgettable lists + memory, + + // Flag to know if list was already fired + fired, + + // Flag to prevent firing + locked, + + // Actual callback list + list = [], + + // Queue of execution data for repeatable lists + queue = [], + + // Index of currently firing callback (modified by add/remove as needed) + firingIndex = -1, + + // Fire callbacks + fire = function() { + + // Enforce single-firing + locked = locked || options.once; + + // Execute callbacks for all pending executions, + // respecting firingIndex overrides and runtime changes + fired = firing = true; + for ( ; queue.length; firingIndex = -1 ) { + memory = queue.shift(); + while ( ++firingIndex < list.length ) { + + // Run callback and check for early termination + if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && + options.stopOnFalse ) { + + // Jump to end and forget the data so .add doesn't re-fire + firingIndex = list.length; + memory = false; + } + } + } + + // Forget the data if we're done with it + if ( !options.memory ) { + memory = false; + } + + firing = false; + + // Clean up if we're done firing for good + if ( locked ) { + + // Keep an empty list if we have data for future add calls + if ( memory ) { + list = []; + + // Otherwise, this object is spent + } else { + list = ""; + } + } + }, + + // Actual Callbacks object + self = { + + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + + // If we have memory from a past run, we should fire after adding + if ( memory && !firing ) { + firingIndex = list.length - 1; + queue.push( memory ); + } + + ( function add( args ) { + jQuery.each( args, function( _, arg ) { + if ( isFunction( arg ) ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && toType( arg ) !== "string" ) { + + // Inspect recursively + add( arg ); + } + } ); + } )( arguments ); + + if ( memory && !firing ) { + fire(); + } + } + return this; + }, + + // Remove a callback from the list + remove: function() { + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + + // Handle firing indexes + if ( index <= firingIndex ) { + firingIndex--; + } + } + } ); + return this; + }, + + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? + jQuery.inArray( fn, list ) > -1 : + list.length > 0; + }, + + // Remove all callbacks from the list + empty: function() { + if ( list ) { + list = []; + } + return this; + }, + + // Disable .fire and .add + // Abort any current/pending executions + // Clear all callbacks and values + disable: function() { + locked = queue = []; + list = memory = ""; + return this; + }, + disabled: function() { + return !list; + }, + + // Disable .fire + // Also disable .add unless we have memory (since it would have no effect) + // Abort any pending executions + lock: function() { + locked = queue = []; + if ( !memory && !firing ) { + list = memory = ""; + } + return this; + }, + locked: function() { + return !!locked; + }, + + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( !locked ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + queue.push( args ); + if ( !firing ) { + fire(); + } + } + return this; + }, + + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + +function Identity( v ) { + return v; +} +function Thrower( ex ) { + throw ex; +} + +function adoptValue( value, resolve, reject, noValue ) { + var method; + + try { + + // Check for promise aspect first to privilege synchronous behavior + if ( value && isFunction( ( method = value.promise ) ) ) { + method.call( value ).done( resolve ).fail( reject ); + + // Other thenables + } else if ( value && isFunction( ( method = value.then ) ) ) { + method.call( value, resolve, reject ); + + // Other non-thenables + } else { + + // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer: + // * false: [ value ].slice( 0 ) => resolve( value ) + // * true: [ value ].slice( 1 ) => resolve() + resolve.apply( undefined, [ value ].slice( noValue ) ); + } + + // For Promises/A+, convert exceptions into rejections + // Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in + // Deferred#then to conditionally suppress rejection. + } catch ( value ) { + + // Support: Android 4.0 only + // Strict mode functions invoked without .call/.apply get global-object context + reject.apply( undefined, [ value ] ); + } +} + +jQuery.extend( { + + Deferred: function( func ) { + var tuples = [ + + // action, add listener, callbacks, + // ... .then handlers, argument index, [final state] + [ "notify", "progress", jQuery.Callbacks( "memory" ), + jQuery.Callbacks( "memory" ), 2 ], + [ "resolve", "done", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 0, "resolved" ], + [ "reject", "fail", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 1, "rejected" ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + "catch": function( fn ) { + return promise.then( null, fn ); + }, + + // Keep pipe for back-compat + pipe: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + + return jQuery.Deferred( function( newDefer ) { + jQuery.each( tuples, function( _i, tuple ) { + + // Map tuples (progress, done, fail) to arguments (done, fail, progress) + var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; + + // deferred.progress(function() { bind to newDefer or newDefer.notify }) + // deferred.done(function() { bind to newDefer or newDefer.resolve }) + // deferred.fail(function() { bind to newDefer or newDefer.reject }) + deferred[ tuple[ 1 ] ]( function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && isFunction( returned.promise ) ) { + returned.promise() + .progress( newDefer.notify ) + .done( newDefer.resolve ) + .fail( newDefer.reject ); + } else { + newDefer[ tuple[ 0 ] + "With" ]( + this, + fn ? [ returned ] : arguments + ); + } + } ); + } ); + fns = null; + } ).promise(); + }, + then: function( onFulfilled, onRejected, onProgress ) { + var maxDepth = 0; + function resolve( depth, deferred, handler, special ) { + return function() { + var that = this, + args = arguments, + mightThrow = function() { + var returned, then; + + // Support: Promises/A+ section 2.3.3.3.3 + // https://promisesaplus.com/#point-59 + // Ignore double-resolution attempts + if ( depth < maxDepth ) { + return; + } + + returned = handler.apply( that, args ); + + // Support: Promises/A+ section 2.3.1 + // https://promisesaplus.com/#point-48 + if ( returned === deferred.promise() ) { + throw new TypeError( "Thenable self-resolution" ); + } + + // Support: Promises/A+ sections 2.3.3.1, 3.5 + // https://promisesaplus.com/#point-54 + // https://promisesaplus.com/#point-75 + // Retrieve `then` only once + then = returned && + + // Support: Promises/A+ section 2.3.4 + // https://promisesaplus.com/#point-64 + // Only check objects and functions for thenability + ( typeof returned === "object" || + typeof returned === "function" ) && + returned.then; + + // Handle a returned thenable + if ( isFunction( then ) ) { + + // Special processors (notify) just wait for resolution + if ( special ) { + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ) + ); + + // Normal processors (resolve) also hook into progress + } else { + + // ...and disregard older resolution values + maxDepth++; + + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ), + resolve( maxDepth, deferred, Identity, + deferred.notifyWith ) + ); + } + + // Handle all other returned values + } else { + + // Only substitute handlers pass on context + // and multiple values (non-spec behavior) + if ( handler !== Identity ) { + that = undefined; + args = [ returned ]; + } + + // Process the value(s) + // Default process is resolve + ( special || deferred.resolveWith )( that, args ); + } + }, + + // Only normal processors (resolve) catch and reject exceptions + process = special ? + mightThrow : + function() { + try { + mightThrow(); + } catch ( e ) { + + if ( jQuery.Deferred.exceptionHook ) { + jQuery.Deferred.exceptionHook( e, + process.stackTrace ); + } + + // Support: Promises/A+ section 2.3.3.3.4.1 + // https://promisesaplus.com/#point-61 + // Ignore post-resolution exceptions + if ( depth + 1 >= maxDepth ) { + + // Only substitute handlers pass on context + // and multiple values (non-spec behavior) + if ( handler !== Thrower ) { + that = undefined; + args = [ e ]; + } + + deferred.rejectWith( that, args ); + } + } + }; + + // Support: Promises/A+ section 2.3.3.3.1 + // https://promisesaplus.com/#point-57 + // Re-resolve promises immediately to dodge false rejection from + // subsequent errors + if ( depth ) { + process(); + } else { + + // Call an optional hook to record the stack, in case of exception + // since it's otherwise lost when execution goes async + if ( jQuery.Deferred.getStackHook ) { + process.stackTrace = jQuery.Deferred.getStackHook(); + } + window.setTimeout( process ); + } + }; + } + + return jQuery.Deferred( function( newDefer ) { + + // progress_handlers.add( ... ) + tuples[ 0 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onProgress ) ? + onProgress : + Identity, + newDefer.notifyWith + ) + ); + + // fulfilled_handlers.add( ... ) + tuples[ 1 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onFulfilled ) ? + onFulfilled : + Identity + ) + ); + + // rejected_handlers.add( ... ) + tuples[ 2 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onRejected ) ? + onRejected : + Thrower + ) + ); + } ).promise(); + }, + + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 5 ]; + + // promise.progress = list.add + // promise.done = list.add + // promise.fail = list.add + promise[ tuple[ 1 ] ] = list.add; + + // Handle state + if ( stateString ) { + list.add( + function() { + + // state = "resolved" (i.e., fulfilled) + // state = "rejected" + state = stateString; + }, + + // rejected_callbacks.disable + // fulfilled_callbacks.disable + tuples[ 3 - i ][ 2 ].disable, + + // rejected_handlers.disable + // fulfilled_handlers.disable + tuples[ 3 - i ][ 3 ].disable, + + // progress_callbacks.lock + tuples[ 0 ][ 2 ].lock, + + // progress_handlers.lock + tuples[ 0 ][ 3 ].lock + ); + } + + // progress_handlers.fire + // fulfilled_handlers.fire + // rejected_handlers.fire + list.add( tuple[ 3 ].fire ); + + // deferred.notify = function() { deferred.notifyWith(...) } + // deferred.resolve = function() { deferred.resolveWith(...) } + // deferred.reject = function() { deferred.rejectWith(...) } + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments ); + return this; + }; + + // deferred.notifyWith = list.fireWith + // deferred.resolveWith = list.fireWith + // deferred.rejectWith = list.fireWith + deferred[ tuple[ 0 ] + "With" ] = list.fireWith; + } ); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( singleValue ) { + var + + // count of uncompleted subordinates + remaining = arguments.length, + + // count of unprocessed arguments + i = remaining, + + // subordinate fulfillment data + resolveContexts = Array( i ), + resolveValues = slice.call( arguments ), + + // the master Deferred + master = jQuery.Deferred(), + + // subordinate callback factory + updateFunc = function( i ) { + return function( value ) { + resolveContexts[ i ] = this; + resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( !( --remaining ) ) { + master.resolveWith( resolveContexts, resolveValues ); + } + }; + }; + + // Single- and empty arguments are adopted like Promise.resolve + if ( remaining <= 1 ) { + adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject, + !remaining ); + + // Use .then() to unwrap secondary thenables (cf. gh-3000) + if ( master.state() === "pending" || + isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { + + return master.then(); + } + } + + // Multiple arguments are aggregated like Promise.all array elements + while ( i-- ) { + adoptValue( resolveValues[ i ], updateFunc( i ), master.reject ); + } + + return master.promise(); + } +} ); + + +// These usually indicate a programmer mistake during development, +// warn about them ASAP rather than swallowing them by default. +var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/; + +jQuery.Deferred.exceptionHook = function( error, stack ) { + + // Support: IE 8 - 9 only + // Console exists when dev tools are open, which can happen at any time + if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) { + window.console.warn( "jQuery.Deferred exception: " + error.message, error.stack, stack ); + } +}; + + + + +jQuery.readyException = function( error ) { + window.setTimeout( function() { + throw error; + } ); +}; + + + + +// The deferred used on DOM ready +var readyList = jQuery.Deferred(); + +jQuery.fn.ready = function( fn ) { + + readyList + .then( fn ) + + // Wrap jQuery.readyException in a function so that the lookup + // happens at the time of error handling instead of callback + // registration. + .catch( function( error ) { + jQuery.readyException( error ); + } ); + + return this; +}; + +jQuery.extend( { + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + } +} ); + +jQuery.ready.then = readyList.then; + +// The ready event handler and self cleanup method +function completed() { + document.removeEventListener( "DOMContentLoaded", completed ); + window.removeEventListener( "load", completed ); + jQuery.ready(); +} + +// Catch cases where $(document).ready() is called +// after the browser event has already occurred. +// Support: IE <=9 - 10 only +// Older IE sometimes signals "interactive" too soon +if ( document.readyState === "complete" || + ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { + + // Handle it asynchronously to allow scripts the opportunity to delay ready + window.setTimeout( jQuery.ready ); + +} else { + + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed ); +} + + + + +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; + + // Sets many values + if ( toType( key ) === "object" ) { + chainable = true; + for ( i in key ) { + access( elems, fn, i, key[ i ], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, _key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < len; i++ ) { + fn( + elems[ i ], key, raw ? + value : + value.call( elems[ i ], i, fn( elems[ i ], key ) ) + ); + } + } + } + + if ( chainable ) { + return elems; + } + + // Gets + if ( bulk ) { + return fn.call( elems ); + } + + return len ? fn( elems[ 0 ], key ) : emptyGet; +}; + + +// Matches dashed string for camelizing +var rmsPrefix = /^-ms-/, + rdashAlpha = /-([a-z])/g; + +// Used by camelCase as callback to replace() +function fcamelCase( _all, letter ) { + return letter.toUpperCase(); +} + +// Convert dashed to camelCase; used by the css and data modules +// Support: IE <=9 - 11, Edge 12 - 15 +// Microsoft forgot to hump their vendor prefix (#9572) +function camelCase( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); +} +var acceptData = function( owner ) { + + // Accepts only: + // - Node + // - Node.ELEMENT_NODE + // - Node.DOCUMENT_NODE + // - Object + // - Any + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); +}; + + + + +function Data() { + this.expando = jQuery.expando + Data.uid++; +} + +Data.uid = 1; + +Data.prototype = { + + cache: function( owner ) { + + // Check if the owner object already has a cache + var value = owner[ this.expando ]; + + // If not, create one + if ( !value ) { + value = {}; + + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return an empty object. + if ( acceptData( owner ) ) { + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable property + // configurable must be true to allow the property to be + // deleted when data is removed + } else { + Object.defineProperty( owner, this.expando, { + value: value, + configurable: true + } ); + } + } + } + + return value; + }, + set: function( owner, data, value ) { + var prop, + cache = this.cache( owner ); + + // Handle: [ owner, key, value ] args + // Always use camelCase key (gh-2257) + if ( typeof data === "string" ) { + cache[ camelCase( data ) ] = value; + + // Handle: [ owner, { properties } ] args + } else { + + // Copy the properties one-by-one to the cache object + for ( prop in data ) { + cache[ camelCase( prop ) ] = data[ prop ]; + } + } + return cache; + }, + get: function( owner, key ) { + return key === undefined ? + this.cache( owner ) : + + // Always use camelCase key (gh-2257) + owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ]; + }, + access: function( owner, key, value ) { + + // In cases where either: + // + // 1. No key was specified + // 2. A string key was specified, but no value provided + // + // Take the "read" path and allow the get method to determine + // which value to return, respectively either: + // + // 1. The entire cache object + // 2. The data stored at the key + // + if ( key === undefined || + ( ( key && typeof key === "string" ) && value === undefined ) ) { + + return this.get( owner, key ); + } + + // When the key is not a string, or both a key and value + // are specified, set or extend (existing objects) with either: + // + // 1. An object of properties + // 2. A key and value + // + this.set( owner, key, value ); + + // Since the "set" path can have two possible entry points + // return the expected data based on which path was taken[*] + return value !== undefined ? value : key; + }, + remove: function( owner, key ) { + var i, + cache = owner[ this.expando ]; + + if ( cache === undefined ) { + return; + } + + if ( key !== undefined ) { + + // Support array or space separated string of keys + if ( Array.isArray( key ) ) { + + // If key is an array of keys... + // We always set camelCase keys, so remove that. + key = key.map( camelCase ); + } else { + key = camelCase( key ); + + // If a key with the spaces exists, use it. + // Otherwise, create an array by matching non-whitespace + key = key in cache ? + [ key ] : + ( key.match( rnothtmlwhite ) || [] ); + } + + i = key.length; + + while ( i-- ) { + delete cache[ key[ i ] ]; + } + } + + // Remove the expando if there's no more data + if ( key === undefined || jQuery.isEmptyObject( cache ) ) { + + // Support: Chrome <=35 - 45 + // Webkit & Blink performance suffers when deleting properties + // from DOM nodes, so set to undefined instead + // https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted) + if ( owner.nodeType ) { + owner[ this.expando ] = undefined; + } else { + delete owner[ this.expando ]; + } + } + }, + hasData: function( owner ) { + var cache = owner[ this.expando ]; + return cache !== undefined && !jQuery.isEmptyObject( cache ); + } +}; +var dataPriv = new Data(); + +var dataUser = new Data(); + + + +// Implementation Summary +// +// 1. Enforce API surface and semantic compatibility with 1.9.x branch +// 2. Improve the module's maintainability by reducing the storage +// paths to a single mechanism. +// 3. Use the same single mechanism to support "private" and "user" data. +// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) +// 5. Avoid exposing implementation details on user objects (eg. expando properties) +// 6. Provide a clear path for implementation upgrade to WeakMap in 2014 + +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /[A-Z]/g; + +function getData( data ) { + if ( data === "true" ) { + return true; + } + + if ( data === "false" ) { + return false; + } + + if ( data === "null" ) { + return null; + } + + // Only convert to a number if it doesn't change the string + if ( data === +data + "" ) { + return +data; + } + + if ( rbrace.test( data ) ) { + return JSON.parse( data ); + } + + return data; +} + +function dataAttr( elem, key, data ) { + var name; + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = getData( data ); + } catch ( e ) {} + + // Make sure we set the data so it isn't changed later + dataUser.set( elem, key, data ); + } else { + data = undefined; + } + } + return data; +} + +jQuery.extend( { + hasData: function( elem ) { + return dataUser.hasData( elem ) || dataPriv.hasData( elem ); + }, + + data: function( elem, name, data ) { + return dataUser.access( elem, name, data ); + }, + + removeData: function( elem, name ) { + dataUser.remove( elem, name ); + }, + + // TODO: Now that all calls to _data and _removeData have been replaced + // with direct calls to dataPriv methods, these can be deprecated. + _data: function( elem, name, data ) { + return dataPriv.access( elem, name, data ); + }, + + _removeData: function( elem, name ) { + dataPriv.remove( elem, name ); + } +} ); + +jQuery.fn.extend( { + data: function( key, value ) { + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = dataUser.get( elem ); + + if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { + i = attrs.length; + while ( i-- ) { + + // Support: IE 11 only + // The attrs elements can be null (#14894) + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = camelCase( name.slice( 5 ) ); + dataAttr( elem, name, data[ name ] ); + } + } + } + dataPriv.set( elem, "hasDataAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each( function() { + dataUser.set( this, key ); + } ); + } + + return access( this, function( value ) { + var data; + + // The calling jQuery object (element matches) is not empty + // (and therefore has an element appears at this[ 0 ]) and the + // `value` parameter was not undefined. An empty jQuery object + // will result in `undefined` for elem = this[ 0 ] which will + // throw an exception if an attempt to read a data cache is made. + if ( elem && value === undefined ) { + + // Attempt to get data from the cache + // The key will always be camelCased in Data + data = dataUser.get( elem, key ); + if ( data !== undefined ) { + return data; + } + + // Attempt to "discover" the data in + // HTML5 custom data-* attrs + data = dataAttr( elem, key ); + if ( data !== undefined ) { + return data; + } + + // We tried really hard, but the data doesn't exist. + return; + } + + // Set the data... + this.each( function() { + + // We always store the camelCased key + dataUser.set( this, key, value ); + } ); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each( function() { + dataUser.remove( this, key ); + } ); + } +} ); + + +jQuery.extend( { + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = dataPriv.get( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || Array.isArray( data ) ) { + queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // Clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // Not public - generate a queueHooks object, or return the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { + empty: jQuery.Callbacks( "once memory" ).add( function() { + dataPriv.remove( elem, [ type + "queue", key ] ); + } ) + } ); + } +} ); + +jQuery.fn.extend( { + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[ 0 ], type ); + } + + return data === undefined ? + this : + this.each( function() { + var queue = jQuery.queue( this, type, data ); + + // Ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + } ); + }, + dequeue: function( type ) { + return this.each( function() { + jQuery.dequeue( this, type ); + } ); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while ( i-- ) { + tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +} ); +var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; + +var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); + + +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + +var documentElement = document.documentElement; + + + + var isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ); + }, + composed = { composed: true }; + + // Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only + // Check attachment across shadow DOM boundaries when possible (gh-3504) + // Support: iOS 10.0-10.2 only + // Early iOS 10 versions support `attachShadow` but not `getRootNode`, + // leading to errors. We need to check for `getRootNode`. + if ( documentElement.getRootNode ) { + isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ) || + elem.getRootNode( composed ) === elem.ownerDocument; + }; + } +var isHiddenWithinTree = function( elem, el ) { + + // isHiddenWithinTree might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + + // Inline style trumps all + return elem.style.display === "none" || + elem.style.display === "" && + + // Otherwise, check computed style + // Support: Firefox <=43 - 45 + // Disconnected elements can have computed display: none, so first confirm that elem is + // in the document. + isAttached( elem ) && + + jQuery.css( elem, "display" ) === "none"; + }; + + + +function adjustCSS( elem, prop, valueParts, tween ) { + var adjusted, scale, + maxIterations = 20, + currentValue = tween ? + function() { + return tween.cur(); + } : + function() { + return jQuery.css( elem, prop, "" ); + }, + initial = currentValue(), + unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), + + // Starting value computation is required for potential unit mismatches + initialInUnit = elem.nodeType && + ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && + rcssNum.exec( jQuery.css( elem, prop ) ); + + if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { + + // Support: Firefox <=54 + // Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144) + initial = initial / 2; + + // Trust units reported by jQuery.css + unit = unit || initialInUnit[ 3 ]; + + // Iteratively approximate from a nonzero starting point + initialInUnit = +initial || 1; + + while ( maxIterations-- ) { + + // Evaluate and update our best guess (doubling guesses that zero out). + // Finish if the scale equals or crosses 1 (making the old*new product non-positive). + jQuery.style( elem, prop, initialInUnit + unit ); + if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) { + maxIterations = 0; + } + initialInUnit = initialInUnit / scale; + + } + + initialInUnit = initialInUnit * 2; + jQuery.style( elem, prop, initialInUnit + unit ); + + // Make sure we update the tween properties later on + valueParts = valueParts || []; + } + + if ( valueParts ) { + initialInUnit = +initialInUnit || +initial || 0; + + // Apply relative offset (+=/-=) if specified + adjusted = valueParts[ 1 ] ? + initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : + +valueParts[ 2 ]; + if ( tween ) { + tween.unit = unit; + tween.start = initialInUnit; + tween.end = adjusted; + } + } + return adjusted; +} + + +var defaultDisplayMap = {}; + +function getDefaultDisplay( elem ) { + var temp, + doc = elem.ownerDocument, + nodeName = elem.nodeName, + display = defaultDisplayMap[ nodeName ]; + + if ( display ) { + return display; + } + + temp = doc.body.appendChild( doc.createElement( nodeName ) ); + display = jQuery.css( temp, "display" ); + + temp.parentNode.removeChild( temp ); + + if ( display === "none" ) { + display = "block"; + } + defaultDisplayMap[ nodeName ] = display; + + return display; +} + +function showHide( elements, show ) { + var display, elem, + values = [], + index = 0, + length = elements.length; + + // Determine new display value for elements that need to change + for ( ; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + + display = elem.style.display; + if ( show ) { + + // Since we force visibility upon cascade-hidden elements, an immediate (and slow) + // check is required in this first loop unless we have a nonempty display value (either + // inline or about-to-be-restored) + if ( display === "none" ) { + values[ index ] = dataPriv.get( elem, "display" ) || null; + if ( !values[ index ] ) { + elem.style.display = ""; + } + } + if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) { + values[ index ] = getDefaultDisplay( elem ); + } + } else { + if ( display !== "none" ) { + values[ index ] = "none"; + + // Remember what we're overwriting + dataPriv.set( elem, "display", display ); + } + } + } + + // Set the display of the elements in a second loop to avoid constant reflow + for ( index = 0; index < length; index++ ) { + if ( values[ index ] != null ) { + elements[ index ].style.display = values[ index ]; + } + } + + return elements; +} + +jQuery.fn.extend( { + show: function() { + return showHide( this, true ); + }, + hide: function() { + return showHide( this ); + }, + toggle: function( state ) { + if ( typeof state === "boolean" ) { + return state ? this.show() : this.hide(); + } + + return this.each( function() { + if ( isHiddenWithinTree( this ) ) { + jQuery( this ).show(); + } else { + jQuery( this ).hide(); + } + } ); + } +} ); +var rcheckableType = ( /^(?:checkbox|radio)$/i ); + +var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i ); + +var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i ); + + + +( function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); + + // Support: Android 4.0 - 4.3 only + // Check state lost if the name is set (#11217) + // Support: Windows Web Apps (WWA) + // `name` and `type` must use .setAttribute for WWA (#14901) + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + + // Support: Android <=4.1 only + // Older WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE <=11 only + // Make sure textarea (and checkbox) defaultValue is properly cloned + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; + + // Support: IE <=9 only + // IE <=9 replaces "; + support.option = !!div.lastChild; +} )(); + + +// We have to close these tags to support XHTML (#13200) +var wrapMap = { + + // XHTML parsers do not magically insert elements in the + // same way that tag soup parsers do. So we cannot shorten + // this by omitting or other required elements. + thead: [ 1, "", "
      " ], + col: [ 2, "", "
      " ], + tr: [ 2, "", "
      " ], + td: [ 3, "", "
      " ], + + _default: [ 0, "", "" ] +}; + +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// Support: IE <=9 only +if ( !support.option ) { + wrapMap.optgroup = wrapMap.option = [ 1, "" ]; +} + + +function getAll( context, tag ) { + + // Support: IE <=9 - 11 only + // Use typeof to avoid zero-argument method invocation on host objects (#15151) + var ret; + + if ( typeof context.getElementsByTagName !== "undefined" ) { + ret = context.getElementsByTagName( tag || "*" ); + + } else if ( typeof context.querySelectorAll !== "undefined" ) { + ret = context.querySelectorAll( tag || "*" ); + + } else { + ret = []; + } + + if ( tag === undefined || tag && nodeName( context, tag ) ) { + return jQuery.merge( [ context ], ret ); + } + + return ret; +} + + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + dataPriv.set( + elems[ i ], + "globalEval", + !refElements || dataPriv.get( refElements[ i ], "globalEval" ) + ); + } +} + + +var rhtml = /<|&#?\w+;/; + +function buildFragment( elems, context, scripts, selection, ignored ) { + var elem, tmp, tag, wrap, attached, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( toType( elem ) === "object" ) { + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; + + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, tmp.childNodes ); + + // Remember the top-level container + tmp = fragment.firstChild; + + // Ensure the created nodes are orphaned (#12392) + tmp.textContent = ""; + } + } + } + + // Remove wrapper from fragment + fragment.textContent = ""; + + i = 0; + while ( ( elem = nodes[ i++ ] ) ) { + + // Skip elements already in the context collection (trac-4087) + if ( selection && jQuery.inArray( elem, selection ) > -1 ) { + if ( ignored ) { + ignored.push( elem ); + } + continue; + } + + attached = isAttached( elem ); + + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( attached ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( ( elem = tmp[ j++ ] ) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; +} + + +var + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +// Support: IE <=9 - 11+ +// focus() and blur() are asynchronous, except when they are no-op. +// So expect focus to be synchronous when the element is already active, +// and blur to be synchronous when the element is not already active. +// (focus and blur are always synchronous in other supported browsers, +// this just defines when we can count on it). +function expectSync( elem, type ) { + return ( elem === safeActiveElement() ) === ( type === "focus" ); +} + +// Support: IE <=9 only +// Accessing document.activeElement can throw unexpectedly +// https://bugs.jquery.com/ticket/13393 +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +function on( elem, types, selector, data, fn, one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + on( elem, type, selector, data, types[ type ], one ); + } + return elem; + } + + if ( data == null && fn == null ) { + + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return elem; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return elem.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + } ); +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.get( elem ); + + // Only attach events to objects that accept data + if ( !acceptData( elem ) ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Ensure that invalid selectors throw exceptions at attach time + // Evaluate against documentElement in case elem is a non-element node (e.g., document) + if ( selector ) { + jQuery.find.matchesSelector( documentElement, selector ); + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !( events = elemData.events ) ) { + events = elemData.events = Object.create( null ); + } + if ( !( eventHandle = elemData.handle ) ) { + eventHandle = elemData.handle = function( e ) { + + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply( elem, arguments ) : undefined; + }; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend( { + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join( "." ) + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !( handlers = events[ type ] ) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener if the special events handler returns false + if ( !special.setup || + special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); + + if ( !elemData || !( events = elemData.events ) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[ 2 ] && + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || + selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || + special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove data and the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + dataPriv.remove( elem, "handle events" ); + } + }, + + dispatch: function( nativeEvent ) { + + var i, j, ret, matched, handleObj, handlerQueue, + args = new Array( arguments.length ), + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( nativeEvent ), + + handlers = ( + dataPriv.get( this, "events" ) || Object.create( null ) + )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[ 0 ] = event; + + for ( i = 1; i < arguments.length; i++ ) { + args[ i ] = arguments[ i ]; + } + + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( ( handleObj = matched.handlers[ j++ ] ) && + !event.isImmediatePropagationStopped() ) { + + // If the event is namespaced, then each handler is only invoked if it is + // specially universal or its namespaces are a superset of the event's. + if ( !event.rnamespace || handleObj.namespace === false || + event.rnamespace.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || + handleObj.handler ).apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( ( event.result = ret ) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var i, handleObj, sel, matchedHandlers, matchedSelectors, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Find delegate handlers + if ( delegateCount && + + // Support: IE <=9 + // Black-hole SVG instance trees (trac-13180) + cur.nodeType && + + // Support: Firefox <=42 + // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861) + // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click + // Support: IE 11 only + // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343) + !( event.type === "click" && event.button >= 1 ) ) { + + for ( ; cur !== this; cur = cur.parentNode || this ) { + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) { + matchedHandlers = []; + matchedSelectors = {}; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matchedSelectors[ sel ] === undefined ) { + matchedSelectors[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) > -1 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matchedSelectors[ sel ] ) { + matchedHandlers.push( handleObj ); + } + } + if ( matchedHandlers.length ) { + handlerQueue.push( { elem: cur, handlers: matchedHandlers } ); + } + } + } + } + + // Add the remaining (directly-bound) handlers + cur = this; + if ( delegateCount < handlers.length ) { + handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } ); + } + + return handlerQueue; + }, + + addProp: function( name, hook ) { + Object.defineProperty( jQuery.Event.prototype, name, { + enumerable: true, + configurable: true, + + get: isFunction( hook ) ? + function() { + if ( this.originalEvent ) { + return hook( this.originalEvent ); + } + } : + function() { + if ( this.originalEvent ) { + return this.originalEvent[ name ]; + } + }, + + set: function( value ) { + Object.defineProperty( this, name, { + enumerable: true, + configurable: true, + writable: true, + value: value + } ); + } + } ); + }, + + fix: function( originalEvent ) { + return originalEvent[ jQuery.expando ] ? + originalEvent : + new jQuery.Event( originalEvent ); + }, + + special: { + load: { + + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + click: { + + // Utilize native event to ensure correct state for checkable inputs + setup: function( data ) { + + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Claim the first handler + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + // dataPriv.set( el, "click", ... ) + leverageNative( el, "click", returnTrue ); + } + + // Return false to allow normal processing in the caller + return false; + }, + trigger: function( data ) { + + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Force setup before triggering a click + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + leverageNative( el, "click" ); + } + + // Return non-false to allow normal event-path propagation + return true; + }, + + // For cross-browser consistency, suppress native .click() on links + // Also prevent it if we're currently inside a leveraged native-event stack + _default: function( event ) { + var target = event.target; + return rcheckableType.test( target.type ) && + target.click && nodeName( target, "input" ) && + dataPriv.get( target, "click" ) || + nodeName( target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + } +}; + +// Ensure the presence of an event listener that handles manually-triggered +// synthetic events by interrupting progress until reinvoked in response to +// *native* events that it fires directly, ensuring that state changes have +// already occurred before other listeners are invoked. +function leverageNative( el, type, expectSync ) { + + // Missing expectSync indicates a trigger call, which must force setup through jQuery.event.add + if ( !expectSync ) { + if ( dataPriv.get( el, type ) === undefined ) { + jQuery.event.add( el, type, returnTrue ); + } + return; + } + + // Register the controller as a special universal handler for all event namespaces + dataPriv.set( el, type, false ); + jQuery.event.add( el, type, { + namespace: false, + handler: function( event ) { + var notAsync, result, + saved = dataPriv.get( this, type ); + + if ( ( event.isTrigger & 1 ) && this[ type ] ) { + + // Interrupt processing of the outer synthetic .trigger()ed event + // Saved data should be false in such cases, but might be a leftover capture object + // from an async native handler (gh-4350) + if ( !saved.length ) { + + // Store arguments for use when handling the inner native event + // There will always be at least one argument (an event object), so this array + // will not be confused with a leftover capture object. + saved = slice.call( arguments ); + dataPriv.set( this, type, saved ); + + // Trigger the native event and capture its result + // Support: IE <=9 - 11+ + // focus() and blur() are asynchronous + notAsync = expectSync( this, type ); + this[ type ](); + result = dataPriv.get( this, type ); + if ( saved !== result || notAsync ) { + dataPriv.set( this, type, false ); + } else { + result = {}; + } + if ( saved !== result ) { + + // Cancel the outer synthetic event + event.stopImmediatePropagation(); + event.preventDefault(); + return result.value; + } + + // If this is an inner synthetic event for an event with a bubbling surrogate + // (focus or blur), assume that the surrogate already propagated from triggering the + // native event and prevent that from happening again here. + // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the + // bubbling surrogate propagates *after* the non-bubbling base), but that seems + // less bad than duplication. + } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) { + event.stopPropagation(); + } + + // If this is a native event triggered above, everything is now in order + // Fire an inner synthetic event with the original arguments + } else if ( saved.length ) { + + // ...and capture the result + dataPriv.set( this, type, { + value: jQuery.event.trigger( + + // Support: IE <=9 - 11+ + // Extend with the prototype to reset the above stopImmediatePropagation() + jQuery.extend( saved[ 0 ], jQuery.Event.prototype ), + saved.slice( 1 ), + this + ) + } ); + + // Abort handling of the native event + event.stopImmediatePropagation(); + } + } + } ); +} + +jQuery.removeEvent = function( elem, type, handle ) { + + // This "if" is needed for plain objects + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle ); + } +}; + +jQuery.Event = function( src, props ) { + + // Allow instantiation without the 'new' keyword + if ( !( this instanceof jQuery.Event ) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + + // Support: Android <=2.3 only + src.returnValue === false ? + returnTrue : + returnFalse; + + // Create target properties + // Support: Safari <=6 - 7 only + // Target should not be a text node (#504, #13143) + this.target = ( src.target && src.target.nodeType === 3 ) ? + src.target.parentNode : + src.target; + + this.currentTarget = src.currentTarget; + this.relatedTarget = src.relatedTarget; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || Date.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + constructor: jQuery.Event, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + isSimulated: false, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + + if ( e && !this.isSimulated ) { + e.preventDefault(); + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopPropagation(); + } + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +// Includes all common event props including KeyEvent and MouseEvent specific props +jQuery.each( { + altKey: true, + bubbles: true, + cancelable: true, + changedTouches: true, + ctrlKey: true, + detail: true, + eventPhase: true, + metaKey: true, + pageX: true, + pageY: true, + shiftKey: true, + view: true, + "char": true, + code: true, + charCode: true, + key: true, + keyCode: true, + button: true, + buttons: true, + clientX: true, + clientY: true, + offsetX: true, + offsetY: true, + pointerId: true, + pointerType: true, + screenX: true, + screenY: true, + targetTouches: true, + toElement: true, + touches: true, + + which: function( event ) { + var button = event.button; + + // Add which for key events + if ( event.which == null && rkeyEvent.test( event.type ) ) { + return event.charCode != null ? event.charCode : event.keyCode; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + if ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) { + if ( button & 1 ) { + return 1; + } + + if ( button & 2 ) { + return 3; + } + + if ( button & 4 ) { + return 2; + } + + return 0; + } + + return event.which; + } +}, jQuery.event.addProp ); + +jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { + jQuery.event.special[ type ] = { + + // Utilize native event if possible so blur/focus sequence is correct + setup: function() { + + // Claim the first handler + // dataPriv.set( this, "focus", ... ) + // dataPriv.set( this, "blur", ... ) + leverageNative( this, type, expectSync ); + + // Return false to allow normal processing in the caller + return false; + }, + trigger: function() { + + // Force setup before trigger + leverageNative( this, type ); + + // Return non-false to allow normal event-path propagation + return true; + }, + + delegateType: delegateType + }; +} ); + +// Create mouseenter/leave events using mouseover/out and event-time checks +// so that event delegation works in jQuery. +// Do the same for pointerenter/pointerleave and pointerover/pointerout +// +// Support: Safari 7 only +// Safari sends mouseenter too often; see: +// https://bugs.chromium.org/p/chromium/issues/detail?id=470258 +// for the description of the bug (it existed in older Chrome versions as well). +jQuery.each( { + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mouseenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +} ); + +jQuery.fn.extend( { + + on: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn ); + }, + one: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? + handleObj.origType + "." + handleObj.namespace : + handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each( function() { + jQuery.event.remove( this, types, fn, selector ); + } ); + } +} ); + + +var + + // Support: IE <=10 - 11, Edge 12 - 13 only + // In IE/Edge using regex groups here causes severe slowdowns. + // See https://connect.microsoft.com/IE/feedback/details/1736512/ + rnoInnerhtml = /\s*$/g; + +// Prefer a tbody over its parent table for containing new rows +function manipulationTarget( elem, content ) { + if ( nodeName( elem, "table" ) && + nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { + + return jQuery( elem ).children( "tbody" )[ 0 ] || elem; + } + + return elem; +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) { + elem.type = elem.type.slice( 5 ); + } else { + elem.removeAttribute( "type" ); + } + + return elem; +} + +function cloneCopyEvent( src, dest ) { + var i, l, type, pdataOld, udataOld, udataCur, events; + + if ( dest.nodeType !== 1 ) { + return; + } + + // 1. Copy private data: events, handlers, etc. + if ( dataPriv.hasData( src ) ) { + pdataOld = dataPriv.get( src ); + events = pdataOld.events; + + if ( events ) { + dataPriv.remove( dest, "handle events" ); + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + } + + // 2. Copy user data + if ( dataUser.hasData( src ) ) { + udataOld = dataUser.access( src ); + udataCur = jQuery.extend( {}, udataOld ); + + dataUser.set( dest, udataCur ); + } +} + +// Fix IE bugs, see support tests +function fixInput( src, dest ) { + var nodeName = dest.nodeName.toLowerCase(); + + // Fails to persist the checked state of a cloned checkbox or radio button. + if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + dest.checked = src.checked; + + // Fails to return the selected option to the default selected state when cloning options + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +function domManip( collection, args, callback, ignored ) { + + // Flatten any nested arrays + args = flat( args ); + + var fragment, first, scripts, hasScripts, node, doc, + i = 0, + l = collection.length, + iNoClone = l - 1, + value = args[ 0 ], + valueIsFunction = isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( valueIsFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { + return collection.each( function( index ) { + var self = collection.eq( index ); + if ( valueIsFunction ) { + args[ 0 ] = value.call( this, index, self.html() ); + } + domManip( self, args, callback, ignored ); + } ); + } + + if ( l ) { + fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + // Require either new content or an interest in ignored elements to invoke the callback + if ( first || ignored ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item + // instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( collection[ i ], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !dataPriv.access( node, "globalEval" ) && + jQuery.contains( doc, node ) ) { + + if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) { + + // Optional AJAX dependency, but won't run scripts if not present + if ( jQuery._evalUrl && !node.noModule ) { + jQuery._evalUrl( node.src, { + nonce: node.nonce || node.getAttribute( "nonce" ) + }, doc ); + } + } else { + DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc ); + } + } + } + } + } + } + + return collection; +} + +function remove( elem, selector, keepData ) { + var node, + nodes = selector ? jQuery.filter( selector, elem ) : elem, + i = 0; + + for ( ; ( node = nodes[ i ] ) != null; i++ ) { + if ( !keepData && node.nodeType === 1 ) { + jQuery.cleanData( getAll( node ) ); + } + + if ( node.parentNode ) { + if ( keepData && isAttached( node ) ) { + setGlobalEval( getAll( node, "script" ) ); + } + node.parentNode.removeChild( node ); + } + } + + return elem; +} + +jQuery.extend( { + htmlPrefilter: function( html ) { + return html; + }, + + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var i, l, srcElements, destElements, + clone = elem.cloneNode( true ), + inPage = isAttached( elem ); + + // Fix IE cloning issues + if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && + !jQuery.isXMLDoc( elem ) ) { + + // We eschew Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + fixInput( srcElements[ i ], destElements[ i ] ); + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + cloneCopyEvent( srcElements[ i ], destElements[ i ] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + // Return the cloned set + return clone; + }, + + cleanData: function( elems ) { + var data, elem, type, + special = jQuery.event.special, + i = 0; + + for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { + if ( acceptData( elem ) ) { + if ( ( data = elem[ dataPriv.expando ] ) ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Support: Chrome <=35 - 45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataPriv.expando ] = undefined; + } + if ( elem[ dataUser.expando ] ) { + + // Support: Chrome <=35 - 45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataUser.expando ] = undefined; + } + } + } + } +} ); + +jQuery.fn.extend( { + detach: function( selector ) { + return remove( this, selector, true ); + }, + + remove: function( selector ) { + return remove( this, selector ); + }, + + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().each( function() { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.textContent = value; + } + } ); + }, null, value, arguments.length ); + }, + + append: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + } ); + }, + + prepend: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + } ); + }, + + before: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + } ); + }, + + after: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + } ); + }, + + empty: function() { + var elem, + i = 0; + + for ( ; ( elem = this[ i ] ) != null; i++ ) { + if ( elem.nodeType === 1 ) { + + // Prevent memory leaks + jQuery.cleanData( getAll( elem, false ) ); + + // Remove any remaining nodes + elem.textContent = ""; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + } ); + }, + + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if ( value === undefined && elem.nodeType === 1 ) { + return elem.innerHTML; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { + + value = jQuery.htmlPrefilter( value ); + + try { + for ( ; i < l; i++ ) { + elem = this[ i ] || {}; + + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch ( e ) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var ignored = []; + + // Make the changes, replacing each non-ignored context element with the new content + return domManip( this, arguments, function( elem ) { + var parent = this.parentNode; + + if ( jQuery.inArray( this, ignored ) < 0 ) { + jQuery.cleanData( getAll( this ) ); + if ( parent ) { + parent.replaceChild( elem, this ); + } + } + + // Force callback invocation + }, ignored ); + } +} ); + +jQuery.each( { + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1, + i = 0; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone( true ); + jQuery( insert[ i ] )[ original ]( elems ); + + // Support: Android <=4.0 only, PhantomJS 1 only + // .get() because push.apply(_, arraylike) throws on ancient WebKit + push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +} ); +var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); + +var getStyles = function( elem ) { + + // Support: IE <=11 only, Firefox <=30 (#15098, #14150) + // IE throws on elements created in popups + // FF meanwhile throws on frame elements through "defaultView.getComputedStyle" + var view = elem.ownerDocument.defaultView; + + if ( !view || !view.opener ) { + view = window; + } + + return view.getComputedStyle( elem ); + }; + +var swap = function( elem, options, callback ) { + var ret, name, + old = {}; + + // Remember the old values, and insert the new ones + for ( name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + ret = callback.call( elem ); + + // Revert the old values + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } + + return ret; +}; + + +var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); + + + +( function() { + + // Executing both pixelPosition & boxSizingReliable tests require only one layout + // so they're executed at the same time to save the second computation. + function computeStyleTests() { + + // This is a singleton, we need to execute it only once + if ( !div ) { + return; + } + + container.style.cssText = "position:absolute;left:-11111px;width:60px;" + + "margin-top:1px;padding:0;border:0"; + div.style.cssText = + "position:relative;display:block;box-sizing:border-box;overflow:scroll;" + + "margin:auto;border:1px;padding:1px;" + + "width:60%;top:1%"; + documentElement.appendChild( container ).appendChild( div ); + + var divStyle = window.getComputedStyle( div ); + pixelPositionVal = divStyle.top !== "1%"; + + // Support: Android 4.0 - 4.3 only, Firefox <=3 - 44 + reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12; + + // Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3 + // Some styles come back with percentage values, even though they shouldn't + div.style.right = "60%"; + pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36; + + // Support: IE 9 - 11 only + // Detect misreporting of content dimensions for box-sizing:border-box elements + boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36; + + // Support: IE 9 only + // Detect overflow:scroll screwiness (gh-3699) + // Support: Chrome <=64 + // Don't get tricked when zoom affects offsetWidth (gh-4029) + div.style.position = "absolute"; + scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12; + + documentElement.removeChild( container ); + + // Nullify the div so it wouldn't be stored in the memory and + // it will also be a sign that checks already performed + div = null; + } + + function roundPixelMeasures( measure ) { + return Math.round( parseFloat( measure ) ); + } + + var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal, + reliableTrDimensionsVal, reliableMarginLeftVal, + container = document.createElement( "div" ), + div = document.createElement( "div" ); + + // Finish early in limited (non-browser) environments + if ( !div.style ) { + return; + } + + // Support: IE <=9 - 11 only + // Style of cloned element affects source element cloned (#8908) + div.style.backgroundClip = "content-box"; + div.cloneNode( true ).style.backgroundClip = ""; + support.clearCloneStyle = div.style.backgroundClip === "content-box"; + + jQuery.extend( support, { + boxSizingReliable: function() { + computeStyleTests(); + return boxSizingReliableVal; + }, + pixelBoxStyles: function() { + computeStyleTests(); + return pixelBoxStylesVal; + }, + pixelPosition: function() { + computeStyleTests(); + return pixelPositionVal; + }, + reliableMarginLeft: function() { + computeStyleTests(); + return reliableMarginLeftVal; + }, + scrollboxSize: function() { + computeStyleTests(); + return scrollboxSizeVal; + }, + + // Support: IE 9 - 11+, Edge 15 - 18+ + // IE/Edge misreport `getComputedStyle` of table rows with width/height + // set in CSS while `offset*` properties report correct values. + // Behavior in IE 9 is more subtle than in newer versions & it passes + // some versions of this test; make sure not to make it pass there! + reliableTrDimensions: function() { + var table, tr, trChild, trStyle; + if ( reliableTrDimensionsVal == null ) { + table = document.createElement( "table" ); + tr = document.createElement( "tr" ); + trChild = document.createElement( "div" ); + + table.style.cssText = "position:absolute;left:-11111px"; + tr.style.height = "1px"; + trChild.style.height = "9px"; + + documentElement + .appendChild( table ) + .appendChild( tr ) + .appendChild( trChild ); + + trStyle = window.getComputedStyle( tr ); + reliableTrDimensionsVal = parseInt( trStyle.height ) > 3; + + documentElement.removeChild( table ); + } + return reliableTrDimensionsVal; + } + } ); +} )(); + + +function curCSS( elem, name, computed ) { + var width, minWidth, maxWidth, ret, + + // Support: Firefox 51+ + // Retrieving style before computed somehow + // fixes an issue with getting wrong values + // on detached elements + style = elem.style; + + computed = computed || getStyles( elem ); + + // getPropertyValue is needed for: + // .css('filter') (IE 9 only, #12537) + // .css('--customProperty) (#3144) + if ( computed ) { + ret = computed.getPropertyValue( name ) || computed[ name ]; + + if ( ret === "" && !isAttached( elem ) ) { + ret = jQuery.style( elem, name ); + } + + // A tribute to the "awesome hack by Dean Edwards" + // Android Browser returns percentage for some values, + // but width seems to be reliably pixels. + // This is against the CSSOM draft spec: + // https://drafts.csswg.org/cssom/#resolved-values + if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) { + + // Remember the original values + width = style.width; + minWidth = style.minWidth; + maxWidth = style.maxWidth; + + // Put in the new values to get a computed value out + style.minWidth = style.maxWidth = style.width = ret; + ret = computed.width; + + // Revert the changed values + style.width = width; + style.minWidth = minWidth; + style.maxWidth = maxWidth; + } + } + + return ret !== undefined ? + + // Support: IE <=9 - 11 only + // IE returns zIndex value as an integer. + ret + "" : + ret; +} + + +function addGetHookIf( conditionFn, hookFn ) { + + // Define the hook, we'll check on the first run if it's really needed. + return { + get: function() { + if ( conditionFn() ) { + + // Hook not needed (or it's not possible to use it due + // to missing dependency), remove it. + delete this.get; + return; + } + + // Hook needed; redefine it so that the support test is not executed again. + return ( this.get = hookFn ).apply( this, arguments ); + } + }; +} + + +var cssPrefixes = [ "Webkit", "Moz", "ms" ], + emptyStyle = document.createElement( "div" ).style, + vendorProps = {}; + +// Return a vendor-prefixed property or undefined +function vendorPropName( name ) { + + // Check for vendor prefixed names + var capName = name[ 0 ].toUpperCase() + name.slice( 1 ), + i = cssPrefixes.length; + + while ( i-- ) { + name = cssPrefixes[ i ] + capName; + if ( name in emptyStyle ) { + return name; + } + } +} + +// Return a potentially-mapped jQuery.cssProps or vendor prefixed property +function finalPropName( name ) { + var final = jQuery.cssProps[ name ] || vendorProps[ name ]; + + if ( final ) { + return final; + } + if ( name in emptyStyle ) { + return name; + } + return vendorProps[ name ] = vendorPropName( name ) || name; +} + + +var + + // Swappable if display is none or starts with table + // except "table", "table-cell", or "table-caption" + // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + rcustomProp = /^--/, + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: "0", + fontWeight: "400" + }; + +function setPositiveNumber( _elem, value, subtract ) { + + // Any relative (+/-) values have already been + // normalized at this point + var matches = rcssNum.exec( value ); + return matches ? + + // Guard against undefined "subtract", e.g., when used as in cssHooks + Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) : + value; +} + +function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) { + var i = dimension === "width" ? 1 : 0, + extra = 0, + delta = 0; + + // Adjustment may not be necessary + if ( box === ( isBorderBox ? "border" : "content" ) ) { + return 0; + } + + for ( ; i < 4; i += 2 ) { + + // Both box models exclude margin + if ( box === "margin" ) { + delta += jQuery.css( elem, box + cssExpand[ i ], true, styles ); + } + + // If we get here with a content-box, we're seeking "padding" or "border" or "margin" + if ( !isBorderBox ) { + + // Add padding + delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + + // For "border" or "margin", add border + if ( box !== "padding" ) { + delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + + // But still keep track of it otherwise + } else { + extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + + // If we get here with a border-box (content + padding + border), we're seeking "content" or + // "padding" or "margin" + } else { + + // For "content", subtract padding + if ( box === "content" ) { + delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + } + + // For "content" or "padding", subtract border + if ( box !== "margin" ) { + delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } + } + + // Account for positive content-box scroll gutter when requested by providing computedVal + if ( !isBorderBox && computedVal >= 0 ) { + + // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border + // Assuming integer scroll gutter, subtract the rest and round down + delta += Math.max( 0, Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + computedVal - + delta - + extra - + 0.5 + + // If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter + // Use an explicit zero to avoid NaN (gh-3964) + ) ) || 0; + } + + return delta; +} + +function getWidthOrHeight( elem, dimension, extra ) { + + // Start with computed style + var styles = getStyles( elem ), + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322). + // Fake content-box until we know it's needed to know the true value. + boxSizingNeeded = !support.boxSizingReliable() || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + valueIsBorderBox = isBorderBox, + + val = curCSS( elem, dimension, styles ), + offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ); + + // Support: Firefox <=54 + // Return a confounding non-pixel value or feign ignorance, as appropriate. + if ( rnumnonpx.test( val ) ) { + if ( !extra ) { + return val; + } + val = "auto"; + } + + + // Support: IE 9 - 11 only + // Use offsetWidth/offsetHeight for when box sizing is unreliable. + // In those cases, the computed value can be trusted to be border-box. + if ( ( !support.boxSizingReliable() && isBorderBox || + + // Support: IE 10 - 11+, Edge 15 - 18+ + // IE/Edge misreport `getComputedStyle` of table rows with width/height + // set in CSS while `offset*` properties report correct values. + // Interestingly, in some cases IE 9 doesn't suffer from this issue. + !support.reliableTrDimensions() && nodeName( elem, "tr" ) || + + // Fall back to offsetWidth/offsetHeight when value is "auto" + // This happens for inline elements with no explicit setting (gh-3571) + val === "auto" || + + // Support: Android <=4.1 - 4.3 only + // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602) + !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) && + + // Make sure the element is visible & connected + elem.getClientRects().length ) { + + isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + + // Where available, offsetWidth/offsetHeight approximate border box dimensions. + // Where not available (e.g., SVG), assume unreliable box-sizing and interpret the + // retrieved value as a content box dimension. + valueIsBorderBox = offsetProp in elem; + if ( valueIsBorderBox ) { + val = elem[ offsetProp ]; + } + } + + // Normalize "" and auto + val = parseFloat( val ) || 0; + + // Adjust for the element's box model + return ( val + + boxModelAdjustment( + elem, + dimension, + extra || ( isBorderBox ? "border" : "content" ), + valueIsBorderBox, + styles, + + // Provide the current computed size to request scroll gutter calculation (gh-3589) + val + ) + ) + "px"; +} + +jQuery.extend( { + + // Add in style property hooks for overriding the default + // behavior of getting and setting a style property + cssHooks: { + opacity: { + get: function( elem, computed ) { + if ( computed ) { + + // We should always get a number back from opacity + var ret = curCSS( elem, "opacity" ); + return ret === "" ? "1" : ret; + } + } + } + }, + + // Don't automatically add "px" to these possibly-unitless properties + cssNumber: { + "animationIterationCount": true, + "columnCount": true, + "fillOpacity": true, + "flexGrow": true, + "flexShrink": true, + "fontWeight": true, + "gridArea": true, + "gridColumn": true, + "gridColumnEnd": true, + "gridColumnStart": true, + "gridRow": true, + "gridRowEnd": true, + "gridRowStart": true, + "lineHeight": true, + "opacity": true, + "order": true, + "orphans": true, + "widows": true, + "zIndex": true, + "zoom": true + }, + + // Add in properties whose names you wish to fix before + // setting or getting the value + cssProps: {}, + + // Get and set the style property on a DOM Node + style: function( elem, name, value, extra ) { + + // Don't set styles on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + // Make sure that we're working with the right name + var ret, type, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ), + style = elem.style; + + // Make sure that we're working with the right name. We don't + // want to query the value if it is a CSS custom property + // since they are user-defined. + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + // Gets hook for the prefixed version, then unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // Check if we're setting a value + if ( value !== undefined ) { + type = typeof value; + + // Convert "+=" or "-=" to relative numbers (#7345) + if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) { + value = adjustCSS( elem, name, ret ); + + // Fixes bug #9237 + type = "number"; + } + + // Make sure that null and NaN values aren't set (#7116) + if ( value == null || value !== value ) { + return; + } + + // If a number was passed in, add the unit (except for certain CSS properties) + // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append + // "px" to a few hardcoded values. + if ( type === "number" && !isCustomProp ) { + value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); + } + + // background-* props affect original clone's values + if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { + style[ name ] = "inherit"; + } + + // If a hook was provided, use that value, otherwise just set the specified value + if ( !hooks || !( "set" in hooks ) || + ( value = hooks.set( elem, value, extra ) ) !== undefined ) { + + if ( isCustomProp ) { + style.setProperty( name, value ); + } else { + style[ name ] = value; + } + } + + } else { + + // If a hook was provided get the non-computed value from there + if ( hooks && "get" in hooks && + ( ret = hooks.get( elem, false, extra ) ) !== undefined ) { + + return ret; + } + + // Otherwise just get the value from the style object + return style[ name ]; + } + }, + + css: function( elem, name, extra, styles ) { + var val, num, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ); + + // Make sure that we're working with the right name. We don't + // want to modify the value if it is a CSS custom property + // since they are user-defined. + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + // Try prefixed name followed by the unprefixed name + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // If a hook was provided get the computed value from there + if ( hooks && "get" in hooks ) { + val = hooks.get( elem, true, extra ); + } + + // Otherwise, if a way to get the computed value exists, use that + if ( val === undefined ) { + val = curCSS( elem, name, styles ); + } + + // Convert "normal" to computed value + if ( val === "normal" && name in cssNormalTransform ) { + val = cssNormalTransform[ name ]; + } + + // Make numeric if forced or a qualifier was provided and val looks numeric + if ( extra === "" || extra ) { + num = parseFloat( val ); + return extra === true || isFinite( num ) ? num || 0 : val; + } + + return val; + } +} ); + +jQuery.each( [ "height", "width" ], function( _i, dimension ) { + jQuery.cssHooks[ dimension ] = { + get: function( elem, computed, extra ) { + if ( computed ) { + + // Certain elements can have dimension info if we invisibly show them + // but it must have a current display style that would benefit + return rdisplayswap.test( jQuery.css( elem, "display" ) ) && + + // Support: Safari 8+ + // Table columns in Safari have non-zero offsetWidth & zero + // getBoundingClientRect().width unless display is changed. + // Support: IE <=11 only + // Running getBoundingClientRect on a disconnected node + // in IE throws an error. + ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? + swap( elem, cssShow, function() { + return getWidthOrHeight( elem, dimension, extra ); + } ) : + getWidthOrHeight( elem, dimension, extra ); + } + }, + + set: function( elem, value, extra ) { + var matches, + styles = getStyles( elem ), + + // Only read styles.position if the test has a chance to fail + // to avoid forcing a reflow. + scrollboxSizeBuggy = !support.scrollboxSize() && + styles.position === "absolute", + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991) + boxSizingNeeded = scrollboxSizeBuggy || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + subtract = extra ? + boxModelAdjustment( + elem, + dimension, + extra, + isBorderBox, + styles + ) : + 0; + + // Account for unreliable border-box dimensions by comparing offset* to computed and + // faking a content-box to get border and padding (gh-3699) + if ( isBorderBox && scrollboxSizeBuggy ) { + subtract -= Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + parseFloat( styles[ dimension ] ) - + boxModelAdjustment( elem, dimension, "border", false, styles ) - + 0.5 + ); + } + + // Convert to pixels if value adjustment is needed + if ( subtract && ( matches = rcssNum.exec( value ) ) && + ( matches[ 3 ] || "px" ) !== "px" ) { + + elem.style[ dimension ] = value; + value = jQuery.css( elem, dimension ); + } + + return setPositiveNumber( elem, value, subtract ); + } + }; +} ); + +jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, + function( elem, computed ) { + if ( computed ) { + return ( parseFloat( curCSS( elem, "marginLeft" ) ) || + elem.getBoundingClientRect().left - + swap( elem, { marginLeft: 0 }, function() { + return elem.getBoundingClientRect().left; + } ) + ) + "px"; + } + } +); + +// These hooks are used by animate to expand properties +jQuery.each( { + margin: "", + padding: "", + border: "Width" +}, function( prefix, suffix ) { + jQuery.cssHooks[ prefix + suffix ] = { + expand: function( value ) { + var i = 0, + expanded = {}, + + // Assumes a single number if not a string + parts = typeof value === "string" ? value.split( " " ) : [ value ]; + + for ( ; i < 4; i++ ) { + expanded[ prefix + cssExpand[ i ] + suffix ] = + parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; + } + + return expanded; + } + }; + + if ( prefix !== "margin" ) { + jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; + } +} ); + +jQuery.fn.extend( { + css: function( name, value ) { + return access( this, function( elem, name, value ) { + var styles, len, + map = {}, + i = 0; + + if ( Array.isArray( name ) ) { + styles = getStyles( elem ); + len = name.length; + + for ( ; i < len; i++ ) { + map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); + } + + return map; + } + + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }, name, value, arguments.length > 1 ); + } +} ); + + +function Tween( elem, options, prop, end, easing ) { + return new Tween.prototype.init( elem, options, prop, end, easing ); +} +jQuery.Tween = Tween; + +Tween.prototype = { + constructor: Tween, + init: function( elem, options, prop, end, easing, unit ) { + this.elem = elem; + this.prop = prop; + this.easing = easing || jQuery.easing._default; + this.options = options; + this.start = this.now = this.cur(); + this.end = end; + this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); + }, + cur: function() { + var hooks = Tween.propHooks[ this.prop ]; + + return hooks && hooks.get ? + hooks.get( this ) : + Tween.propHooks._default.get( this ); + }, + run: function( percent ) { + var eased, + hooks = Tween.propHooks[ this.prop ]; + + if ( this.options.duration ) { + this.pos = eased = jQuery.easing[ this.easing ]( + percent, this.options.duration * percent, 0, 1, this.options.duration + ); + } else { + this.pos = eased = percent; + } + this.now = ( this.end - this.start ) * eased + this.start; + + if ( this.options.step ) { + this.options.step.call( this.elem, this.now, this ); + } + + if ( hooks && hooks.set ) { + hooks.set( this ); + } else { + Tween.propHooks._default.set( this ); + } + return this; + } +}; + +Tween.prototype.init.prototype = Tween.prototype; + +Tween.propHooks = { + _default: { + get: function( tween ) { + var result; + + // Use a property on the element directly when it is not a DOM element, + // or when there is no matching style property that exists. + if ( tween.elem.nodeType !== 1 || + tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) { + return tween.elem[ tween.prop ]; + } + + // Passing an empty string as a 3rd parameter to .css will automatically + // attempt a parseFloat and fallback to a string if the parse fails. + // Simple values such as "10px" are parsed to Float; + // complex values such as "rotate(1rad)" are returned as-is. + result = jQuery.css( tween.elem, tween.prop, "" ); + + // Empty strings, null, undefined and "auto" are converted to 0. + return !result || result === "auto" ? 0 : result; + }, + set: function( tween ) { + + // Use step hook for back compat. + // Use cssHook if its there. + // Use .style if available and use plain properties where available. + if ( jQuery.fx.step[ tween.prop ] ) { + jQuery.fx.step[ tween.prop ]( tween ); + } else if ( tween.elem.nodeType === 1 && ( + jQuery.cssHooks[ tween.prop ] || + tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) { + jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); + } else { + tween.elem[ tween.prop ] = tween.now; + } + } + } +}; + +// Support: IE <=9 only +// Panic based approach to setting things on disconnected nodes +Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { + set: function( tween ) { + if ( tween.elem.nodeType && tween.elem.parentNode ) { + tween.elem[ tween.prop ] = tween.now; + } + } +}; + +jQuery.easing = { + linear: function( p ) { + return p; + }, + swing: function( p ) { + return 0.5 - Math.cos( p * Math.PI ) / 2; + }, + _default: "swing" +}; + +jQuery.fx = Tween.prototype.init; + +// Back compat <1.8 extension point +jQuery.fx.step = {}; + + + + +var + fxNow, inProgress, + rfxtypes = /^(?:toggle|show|hide)$/, + rrun = /queueHooks$/; + +function schedule() { + if ( inProgress ) { + if ( document.hidden === false && window.requestAnimationFrame ) { + window.requestAnimationFrame( schedule ); + } else { + window.setTimeout( schedule, jQuery.fx.interval ); + } + + jQuery.fx.tick(); + } +} + +// Animations created synchronously will run synchronously +function createFxNow() { + window.setTimeout( function() { + fxNow = undefined; + } ); + return ( fxNow = Date.now() ); +} + +// Generate parameters to create a standard animation +function genFx( type, includeWidth ) { + var which, + i = 0, + attrs = { height: type }; + + // If we include width, step value is 1 to do all cssExpand values, + // otherwise step value is 2 to skip over Left and Right + includeWidth = includeWidth ? 1 : 0; + for ( ; i < 4; i += 2 - includeWidth ) { + which = cssExpand[ i ]; + attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; + } + + if ( includeWidth ) { + attrs.opacity = attrs.width = type; + } + + return attrs; +} + +function createTween( value, prop, animation ) { + var tween, + collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ), + index = 0, + length = collection.length; + for ( ; index < length; index++ ) { + if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) { + + // We're done with this property + return tween; + } + } +} + +function defaultPrefilter( elem, props, opts ) { + var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display, + isBox = "width" in props || "height" in props, + anim = this, + orig = {}, + style = elem.style, + hidden = elem.nodeType && isHiddenWithinTree( elem ), + dataShow = dataPriv.get( elem, "fxshow" ); + + // Queue-skipping animations hijack the fx hooks + if ( !opts.queue ) { + hooks = jQuery._queueHooks( elem, "fx" ); + if ( hooks.unqueued == null ) { + hooks.unqueued = 0; + oldfire = hooks.empty.fire; + hooks.empty.fire = function() { + if ( !hooks.unqueued ) { + oldfire(); + } + }; + } + hooks.unqueued++; + + anim.always( function() { + + // Ensure the complete handler is called before this completes + anim.always( function() { + hooks.unqueued--; + if ( !jQuery.queue( elem, "fx" ).length ) { + hooks.empty.fire(); + } + } ); + } ); + } + + // Detect show/hide animations + for ( prop in props ) { + value = props[ prop ]; + if ( rfxtypes.test( value ) ) { + delete props[ prop ]; + toggle = toggle || value === "toggle"; + if ( value === ( hidden ? "hide" : "show" ) ) { + + // Pretend to be hidden if this is a "show" and + // there is still data from a stopped show/hide + if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { + hidden = true; + + // Ignore all other no-op show/hide data + } else { + continue; + } + } + orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); + } + } + + // Bail out if this is a no-op like .hide().hide() + propTween = !jQuery.isEmptyObject( props ); + if ( !propTween && jQuery.isEmptyObject( orig ) ) { + return; + } + + // Restrict "overflow" and "display" styles during box animations + if ( isBox && elem.nodeType === 1 ) { + + // Support: IE <=9 - 11, Edge 12 - 15 + // Record all 3 overflow attributes because IE does not infer the shorthand + // from identically-valued overflowX and overflowY and Edge just mirrors + // the overflowX value there. + opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; + + // Identify a display type, preferring old show/hide data over the CSS cascade + restoreDisplay = dataShow && dataShow.display; + if ( restoreDisplay == null ) { + restoreDisplay = dataPriv.get( elem, "display" ); + } + display = jQuery.css( elem, "display" ); + if ( display === "none" ) { + if ( restoreDisplay ) { + display = restoreDisplay; + } else { + + // Get nonempty value(s) by temporarily forcing visibility + showHide( [ elem ], true ); + restoreDisplay = elem.style.display || restoreDisplay; + display = jQuery.css( elem, "display" ); + showHide( [ elem ] ); + } + } + + // Animate inline elements as inline-block + if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) { + if ( jQuery.css( elem, "float" ) === "none" ) { + + // Restore the original display value at the end of pure show/hide animations + if ( !propTween ) { + anim.done( function() { + style.display = restoreDisplay; + } ); + if ( restoreDisplay == null ) { + display = style.display; + restoreDisplay = display === "none" ? "" : display; + } + } + style.display = "inline-block"; + } + } + } + + if ( opts.overflow ) { + style.overflow = "hidden"; + anim.always( function() { + style.overflow = opts.overflow[ 0 ]; + style.overflowX = opts.overflow[ 1 ]; + style.overflowY = opts.overflow[ 2 ]; + } ); + } + + // Implement show/hide animations + propTween = false; + for ( prop in orig ) { + + // General show/hide setup for this element animation + if ( !propTween ) { + if ( dataShow ) { + if ( "hidden" in dataShow ) { + hidden = dataShow.hidden; + } + } else { + dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } ); + } + + // Store hidden/visible for toggle so `.stop().toggle()` "reverses" + if ( toggle ) { + dataShow.hidden = !hidden; + } + + // Show elements before animating them + if ( hidden ) { + showHide( [ elem ], true ); + } + + /* eslint-disable no-loop-func */ + + anim.done( function() { + + /* eslint-enable no-loop-func */ + + // The final step of a "hide" animation is actually hiding the element + if ( !hidden ) { + showHide( [ elem ] ); + } + dataPriv.remove( elem, "fxshow" ); + for ( prop in orig ) { + jQuery.style( elem, prop, orig[ prop ] ); + } + } ); + } + + // Per-property setup + propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); + if ( !( prop in dataShow ) ) { + dataShow[ prop ] = propTween.start; + if ( hidden ) { + propTween.end = propTween.start; + propTween.start = 0; + } + } + } +} + +function propFilter( props, specialEasing ) { + var index, name, easing, value, hooks; + + // camelCase, specialEasing and expand cssHook pass + for ( index in props ) { + name = camelCase( index ); + easing = specialEasing[ name ]; + value = props[ index ]; + if ( Array.isArray( value ) ) { + easing = value[ 1 ]; + value = props[ index ] = value[ 0 ]; + } + + if ( index !== name ) { + props[ name ] = value; + delete props[ index ]; + } + + hooks = jQuery.cssHooks[ name ]; + if ( hooks && "expand" in hooks ) { + value = hooks.expand( value ); + delete props[ name ]; + + // Not quite $.extend, this won't overwrite existing keys. + // Reusing 'index' because we have the correct "name" + for ( index in value ) { + if ( !( index in props ) ) { + props[ index ] = value[ index ]; + specialEasing[ index ] = easing; + } + } + } else { + specialEasing[ name ] = easing; + } + } +} + +function Animation( elem, properties, options ) { + var result, + stopped, + index = 0, + length = Animation.prefilters.length, + deferred = jQuery.Deferred().always( function() { + + // Don't match elem in the :animated selector + delete tick.elem; + } ), + tick = function() { + if ( stopped ) { + return false; + } + var currentTime = fxNow || createFxNow(), + remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), + + // Support: Android 2.3 only + // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497) + temp = remaining / animation.duration || 0, + percent = 1 - temp, + index = 0, + length = animation.tweens.length; + + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( percent ); + } + + deferred.notifyWith( elem, [ animation, percent, remaining ] ); + + // If there's more to do, yield + if ( percent < 1 && length ) { + return remaining; + } + + // If this was an empty animation, synthesize a final progress notification + if ( !length ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + } + + // Resolve the animation and report its conclusion + deferred.resolveWith( elem, [ animation ] ); + return false; + }, + animation = deferred.promise( { + elem: elem, + props: jQuery.extend( {}, properties ), + opts: jQuery.extend( true, { + specialEasing: {}, + easing: jQuery.easing._default + }, options ), + originalProperties: properties, + originalOptions: options, + startTime: fxNow || createFxNow(), + duration: options.duration, + tweens: [], + createTween: function( prop, end ) { + var tween = jQuery.Tween( elem, animation.opts, prop, end, + animation.opts.specialEasing[ prop ] || animation.opts.easing ); + animation.tweens.push( tween ); + return tween; + }, + stop: function( gotoEnd ) { + var index = 0, + + // If we are going to the end, we want to run all the tweens + // otherwise we skip this part + length = gotoEnd ? animation.tweens.length : 0; + if ( stopped ) { + return this; + } + stopped = true; + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( 1 ); + } + + // Resolve when we played the last frame; otherwise, reject + if ( gotoEnd ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + deferred.resolveWith( elem, [ animation, gotoEnd ] ); + } else { + deferred.rejectWith( elem, [ animation, gotoEnd ] ); + } + return this; + } + } ), + props = animation.props; + + propFilter( props, animation.opts.specialEasing ); + + for ( ; index < length; index++ ) { + result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts ); + if ( result ) { + if ( isFunction( result.stop ) ) { + jQuery._queueHooks( animation.elem, animation.opts.queue ).stop = + result.stop.bind( result ); + } + return result; + } + } + + jQuery.map( props, createTween, animation ); + + if ( isFunction( animation.opts.start ) ) { + animation.opts.start.call( elem, animation ); + } + + // Attach callbacks from options + animation + .progress( animation.opts.progress ) + .done( animation.opts.done, animation.opts.complete ) + .fail( animation.opts.fail ) + .always( animation.opts.always ); + + jQuery.fx.timer( + jQuery.extend( tick, { + elem: elem, + anim: animation, + queue: animation.opts.queue + } ) + ); + + return animation; +} + +jQuery.Animation = jQuery.extend( Animation, { + + tweeners: { + "*": [ function( prop, value ) { + var tween = this.createTween( prop, value ); + adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween ); + return tween; + } ] + }, + + tweener: function( props, callback ) { + if ( isFunction( props ) ) { + callback = props; + props = [ "*" ]; + } else { + props = props.match( rnothtmlwhite ); + } + + var prop, + index = 0, + length = props.length; + + for ( ; index < length; index++ ) { + prop = props[ index ]; + Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || []; + Animation.tweeners[ prop ].unshift( callback ); + } + }, + + prefilters: [ defaultPrefilter ], + + prefilter: function( callback, prepend ) { + if ( prepend ) { + Animation.prefilters.unshift( callback ); + } else { + Animation.prefilters.push( callback ); + } + } +} ); + +jQuery.speed = function( speed, easing, fn ) { + var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { + complete: fn || !fn && easing || + isFunction( speed ) && speed, + duration: speed, + easing: fn && easing || easing && !isFunction( easing ) && easing + }; + + // Go to the end state if fx are off + if ( jQuery.fx.off ) { + opt.duration = 0; + + } else { + if ( typeof opt.duration !== "number" ) { + if ( opt.duration in jQuery.fx.speeds ) { + opt.duration = jQuery.fx.speeds[ opt.duration ]; + + } else { + opt.duration = jQuery.fx.speeds._default; + } + } + } + + // Normalize opt.queue - true/undefined/null -> "fx" + if ( opt.queue == null || opt.queue === true ) { + opt.queue = "fx"; + } + + // Queueing + opt.old = opt.complete; + + opt.complete = function() { + if ( isFunction( opt.old ) ) { + opt.old.call( this ); + } + + if ( opt.queue ) { + jQuery.dequeue( this, opt.queue ); + } + }; + + return opt; +}; + +jQuery.fn.extend( { + fadeTo: function( speed, to, easing, callback ) { + + // Show any hidden elements after setting opacity to 0 + return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show() + + // Animate to the value specified + .end().animate( { opacity: to }, speed, easing, callback ); + }, + animate: function( prop, speed, easing, callback ) { + var empty = jQuery.isEmptyObject( prop ), + optall = jQuery.speed( speed, easing, callback ), + doAnimation = function() { + + // Operate on a copy of prop so per-property easing won't be lost + var anim = Animation( this, jQuery.extend( {}, prop ), optall ); + + // Empty animations, or finishing resolves immediately + if ( empty || dataPriv.get( this, "finish" ) ) { + anim.stop( true ); + } + }; + doAnimation.finish = doAnimation; + + return empty || optall.queue === false ? + this.each( doAnimation ) : + this.queue( optall.queue, doAnimation ); + }, + stop: function( type, clearQueue, gotoEnd ) { + var stopQueue = function( hooks ) { + var stop = hooks.stop; + delete hooks.stop; + stop( gotoEnd ); + }; + + if ( typeof type !== "string" ) { + gotoEnd = clearQueue; + clearQueue = type; + type = undefined; + } + if ( clearQueue ) { + this.queue( type || "fx", [] ); + } + + return this.each( function() { + var dequeue = true, + index = type != null && type + "queueHooks", + timers = jQuery.timers, + data = dataPriv.get( this ); + + if ( index ) { + if ( data[ index ] && data[ index ].stop ) { + stopQueue( data[ index ] ); + } + } else { + for ( index in data ) { + if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { + stopQueue( data[ index ] ); + } + } + } + + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && + ( type == null || timers[ index ].queue === type ) ) { + + timers[ index ].anim.stop( gotoEnd ); + dequeue = false; + timers.splice( index, 1 ); + } + } + + // Start the next in the queue if the last step wasn't forced. + // Timers currently will call their complete callbacks, which + // will dequeue but only if they were gotoEnd. + if ( dequeue || !gotoEnd ) { + jQuery.dequeue( this, type ); + } + } ); + }, + finish: function( type ) { + if ( type !== false ) { + type = type || "fx"; + } + return this.each( function() { + var index, + data = dataPriv.get( this ), + queue = data[ type + "queue" ], + hooks = data[ type + "queueHooks" ], + timers = jQuery.timers, + length = queue ? queue.length : 0; + + // Enable finishing flag on private data + data.finish = true; + + // Empty the queue first + jQuery.queue( this, type, [] ); + + if ( hooks && hooks.stop ) { + hooks.stop.call( this, true ); + } + + // Look for any active animations, and finish them + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && timers[ index ].queue === type ) { + timers[ index ].anim.stop( true ); + timers.splice( index, 1 ); + } + } + + // Look for any animations in the old queue and finish them + for ( index = 0; index < length; index++ ) { + if ( queue[ index ] && queue[ index ].finish ) { + queue[ index ].finish.call( this ); + } + } + + // Turn off finishing flag + delete data.finish; + } ); + } +} ); + +jQuery.each( [ "toggle", "show", "hide" ], function( _i, name ) { + var cssFn = jQuery.fn[ name ]; + jQuery.fn[ name ] = function( speed, easing, callback ) { + return speed == null || typeof speed === "boolean" ? + cssFn.apply( this, arguments ) : + this.animate( genFx( name, true ), speed, easing, callback ); + }; +} ); + +// Generate shortcuts for custom animations +jQuery.each( { + slideDown: genFx( "show" ), + slideUp: genFx( "hide" ), + slideToggle: genFx( "toggle" ), + fadeIn: { opacity: "show" }, + fadeOut: { opacity: "hide" }, + fadeToggle: { opacity: "toggle" } +}, function( name, props ) { + jQuery.fn[ name ] = function( speed, easing, callback ) { + return this.animate( props, speed, easing, callback ); + }; +} ); + +jQuery.timers = []; +jQuery.fx.tick = function() { + var timer, + i = 0, + timers = jQuery.timers; + + fxNow = Date.now(); + + for ( ; i < timers.length; i++ ) { + timer = timers[ i ]; + + // Run the timer and safely remove it when done (allowing for external removal) + if ( !timer() && timers[ i ] === timer ) { + timers.splice( i--, 1 ); + } + } + + if ( !timers.length ) { + jQuery.fx.stop(); + } + fxNow = undefined; +}; + +jQuery.fx.timer = function( timer ) { + jQuery.timers.push( timer ); + jQuery.fx.start(); +}; + +jQuery.fx.interval = 13; +jQuery.fx.start = function() { + if ( inProgress ) { + return; + } + + inProgress = true; + schedule(); +}; + +jQuery.fx.stop = function() { + inProgress = null; +}; + +jQuery.fx.speeds = { + slow: 600, + fast: 200, + + // Default speed + _default: 400 +}; + + +// Based off of the plugin by Clint Helfers, with permission. +// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/ +jQuery.fn.delay = function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = window.setTimeout( next, time ); + hooks.stop = function() { + window.clearTimeout( timeout ); + }; + } ); +}; + + +( function() { + var input = document.createElement( "input" ), + select = document.createElement( "select" ), + opt = select.appendChild( document.createElement( "option" ) ); + + input.type = "checkbox"; + + // Support: Android <=4.3 only + // Default value for a checkbox should be "on" + support.checkOn = input.value !== ""; + + // Support: IE <=11 only + // Must access selectedIndex to make default options select + support.optSelected = opt.selected; + + // Support: IE <=11 only + // An input loses its value after becoming a radio + input = document.createElement( "input" ); + input.value = "t"; + input.type = "radio"; + support.radioValue = input.value === "t"; +} )(); + + +var boolHook, + attrHandle = jQuery.expr.attrHandle; + +jQuery.fn.extend( { + attr: function( name, value ) { + return access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each( function() { + jQuery.removeAttr( this, name ); + } ); + } +} ); + +jQuery.extend( { + attr: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + // Don't get/set attributes on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + // Attribute hooks are determined by the lowercase version + // Grab necessary hook if one is defined + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + hooks = jQuery.attrHooks[ name.toLowerCase() ] || + ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined ); + } + + if ( value !== undefined ) { + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + } + + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + elem.setAttribute( name, value + "" ); + return value; + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + ret = jQuery.find.attr( elem, name ); + + // Non-existent attributes return null, we normalize to undefined + return ret == null ? undefined : ret; + }, + + attrHooks: { + type: { + set: function( elem, value ) { + if ( !support.radioValue && value === "radio" && + nodeName( elem, "input" ) ) { + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + } + }, + + removeAttr: function( elem, value ) { + var name, + i = 0, + + // Attribute names can contain non-HTML whitespace characters + // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 + attrNames = value && value.match( rnothtmlwhite ); + + if ( attrNames && elem.nodeType === 1 ) { + while ( ( name = attrNames[ i++ ] ) ) { + elem.removeAttribute( name ); + } + } + } +} ); + +// Hooks for boolean attributes +boolHook = { + set: function( elem, value, name ) { + if ( value === false ) { + + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + elem.setAttribute( name, name ); + } + return name; + } +}; + +jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( _i, name ) { + var getter = attrHandle[ name ] || jQuery.find.attr; + + attrHandle[ name ] = function( elem, name, isXML ) { + var ret, handle, + lowercaseName = name.toLowerCase(); + + if ( !isXML ) { + + // Avoid an infinite loop by temporarily removing this function from the getter + handle = attrHandle[ lowercaseName ]; + attrHandle[ lowercaseName ] = ret; + ret = getter( elem, name, isXML ) != null ? + lowercaseName : + null; + attrHandle[ lowercaseName ] = handle; + } + return ret; + }; +} ); + + + + +var rfocusable = /^(?:input|select|textarea|button)$/i, + rclickable = /^(?:a|area)$/i; + +jQuery.fn.extend( { + prop: function( name, value ) { + return access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + return this.each( function() { + delete this[ jQuery.propFix[ name ] || name ]; + } ); + } +} ); + +jQuery.extend( { + prop: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + // Don't get/set properties on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + return ( elem[ name ] = value ); + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + return elem[ name ]; + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + + // Support: IE <=9 - 11 only + // elem.tabIndex doesn't always return the + // correct value when it hasn't been explicitly set + // https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + // Use proper attribute retrieval(#12072) + var tabindex = jQuery.find.attr( elem, "tabindex" ); + + if ( tabindex ) { + return parseInt( tabindex, 10 ); + } + + if ( + rfocusable.test( elem.nodeName ) || + rclickable.test( elem.nodeName ) && + elem.href + ) { + return 0; + } + + return -1; + } + } + }, + + propFix: { + "for": "htmlFor", + "class": "className" + } +} ); + +// Support: IE <=11 only +// Accessing the selectedIndex property +// forces the browser to respect setting selected +// on the option +// The getter ensures a default option is selected +// when in an optgroup +// eslint rule "no-unused-expressions" is disabled for this code +// since it considers such accessions noop +if ( !support.optSelected ) { + jQuery.propHooks.selected = { + get: function( elem ) { + + /* eslint no-unused-expressions: "off" */ + + var parent = elem.parentNode; + if ( parent && parent.parentNode ) { + parent.parentNode.selectedIndex; + } + return null; + }, + set: function( elem ) { + + /* eslint no-unused-expressions: "off" */ + + var parent = elem.parentNode; + if ( parent ) { + parent.selectedIndex; + + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + } + }; +} + +jQuery.each( [ + "tabIndex", + "readOnly", + "maxLength", + "cellSpacing", + "cellPadding", + "rowSpan", + "colSpan", + "useMap", + "frameBorder", + "contentEditable" +], function() { + jQuery.propFix[ this.toLowerCase() ] = this; +} ); + + + + + // Strip and collapse whitespace according to HTML spec + // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace + function stripAndCollapse( value ) { + var tokens = value.match( rnothtmlwhite ) || []; + return tokens.join( " " ); + } + + +function getClass( elem ) { + return elem.getAttribute && elem.getAttribute( "class" ) || ""; +} + +function classesToArray( value ) { + if ( Array.isArray( value ) ) { + return value; + } + if ( typeof value === "string" ) { + return value.match( rnothtmlwhite ) || []; + } + return []; +} + +jQuery.fn.extend( { + addClass: function( value ) { + var classes, elem, cur, curValue, clazz, j, finalValue, + i = 0; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + classes = classesToArray( value ); + + if ( classes.length ) { + while ( ( elem = this[ i++ ] ) ) { + curValue = getClass( elem ); + cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + j = 0; + while ( ( clazz = classes[ j++ ] ) ) { + if ( cur.indexOf( " " + clazz + " " ) < 0 ) { + cur += clazz + " "; + } + } + + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + elem.setAttribute( "class", finalValue ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classes, elem, cur, curValue, clazz, j, finalValue, + i = 0; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + if ( !arguments.length ) { + return this.attr( "class", "" ); + } + + classes = classesToArray( value ); + + if ( classes.length ) { + while ( ( elem = this[ i++ ] ) ) { + curValue = getClass( elem ); + + // This expression is here for better compressibility (see addClass) + cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + j = 0; + while ( ( clazz = classes[ j++ ] ) ) { + + // Remove *all* instances + while ( cur.indexOf( " " + clazz + " " ) > -1 ) { + cur = cur.replace( " " + clazz + " ", " " ); + } + } + + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + elem.setAttribute( "class", finalValue ); + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isValidValue = type === "string" || Array.isArray( value ); + + if ( typeof stateVal === "boolean" && isValidValue ) { + return stateVal ? this.addClass( value ) : this.removeClass( value ); + } + + if ( isFunction( value ) ) { + return this.each( function( i ) { + jQuery( this ).toggleClass( + value.call( this, i, getClass( this ), stateVal ), + stateVal + ); + } ); + } + + return this.each( function() { + var className, i, self, classNames; + + if ( isValidValue ) { + + // Toggle individual class names + i = 0; + self = jQuery( this ); + classNames = classesToArray( value ); + + while ( ( className = classNames[ i++ ] ) ) { + + // Check each className given, space separated list + if ( self.hasClass( className ) ) { + self.removeClass( className ); + } else { + self.addClass( className ); + } + } + + // Toggle whole class name + } else if ( value === undefined || type === "boolean" ) { + className = getClass( this ); + if ( className ) { + + // Store className if set + dataPriv.set( this, "__className__", className ); + } + + // If the element has a class name or if we're passed `false`, + // then remove the whole classname (if there was one, the above saved it). + // Otherwise bring back whatever was previously saved (if anything), + // falling back to the empty string if nothing was stored. + if ( this.setAttribute ) { + this.setAttribute( "class", + className || value === false ? + "" : + dataPriv.get( this, "__className__" ) || "" + ); + } + } + } ); + }, + + hasClass: function( selector ) { + var className, elem, + i = 0; + + className = " " + selector + " "; + while ( ( elem = this[ i++ ] ) ) { + if ( elem.nodeType === 1 && + ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { + return true; + } + } + + return false; + } +} ); + + + + +var rreturn = /\r/g; + +jQuery.fn.extend( { + val: function( value ) { + var hooks, ret, valueIsFunction, + elem = this[ 0 ]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || + jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && + "get" in hooks && + ( ret = hooks.get( elem, "value" ) ) !== undefined + ) { + return ret; + } + + ret = elem.value; + + // Handle most common string cases + if ( typeof ret === "string" ) { + return ret.replace( rreturn, "" ); + } + + // Handle cases where value is null/undef or number + return ret == null ? "" : ret; + } + + return; + } + + valueIsFunction = isFunction( value ); + + return this.each( function( i ) { + var val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( valueIsFunction ) { + val = value.call( this, i, jQuery( this ).val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + + } else if ( typeof val === "number" ) { + val += ""; + + } else if ( Array.isArray( val ) ) { + val = jQuery.map( val, function( value ) { + return value == null ? "" : value + ""; + } ); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + } ); + } +} ); + +jQuery.extend( { + valHooks: { + option: { + get: function( elem ) { + + var val = jQuery.find.attr( elem, "value" ); + return val != null ? + val : + + // Support: IE <=10 - 11 only + // option.text throws exceptions (#14686, #14858) + // Strip and collapse whitespace + // https://html.spec.whatwg.org/#strip-and-collapse-whitespace + stripAndCollapse( jQuery.text( elem ) ); + } + }, + select: { + get: function( elem ) { + var value, option, i, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one", + values = one ? null : [], + max = one ? index + 1 : options.length; + + if ( index < 0 ) { + i = max; + + } else { + i = one ? index : 0; + } + + // Loop through all the selected options + for ( ; i < max; i++ ) { + option = options[ i ]; + + // Support: IE <=9 only + // IE8-9 doesn't update selected after form reset (#2551) + if ( ( option.selected || i === index ) && + + // Don't return options that are disabled or in a disabled optgroup + !option.disabled && + ( !option.parentNode.disabled || + !nodeName( option.parentNode, "optgroup" ) ) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + }, + + set: function( elem, value ) { + var optionSet, option, + options = elem.options, + values = jQuery.makeArray( value ), + i = options.length; + + while ( i-- ) { + option = options[ i ]; + + /* eslint-disable no-cond-assign */ + + if ( option.selected = + jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1 + ) { + optionSet = true; + } + + /* eslint-enable no-cond-assign */ + } + + // Force browsers to behave consistently when non-matching value is set + if ( !optionSet ) { + elem.selectedIndex = -1; + } + return values; + } + } + } +} ); + +// Radios and checkboxes getter/setter +jQuery.each( [ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + set: function( elem, value ) { + if ( Array.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 ); + } + } + }; + if ( !support.checkOn ) { + jQuery.valHooks[ this ].get = function( elem ) { + return elem.getAttribute( "value" ) === null ? "on" : elem.value; + }; + } +} ); + + + + +// Return jQuery for attributes-only inclusion + + +support.focusin = "onfocusin" in window; + + +var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + stopPropagationCallback = function( e ) { + e.stopPropagation(); + }; + +jQuery.extend( jQuery.event, { + + trigger: function( event, data, elem, onlyHandlers ) { + + var i, cur, tmp, bubbleType, ontype, handle, special, lastElement, + eventPath = [ elem || document ], + type = hasOwn.call( event, "type" ) ? event.type : event, + namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; + + cur = lastElement = tmp = elem = elem || document; + + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "." ) > -1 ) { + + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split( "." ); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf( ":" ) < 0 && "on" + type; + + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join( "." ); + event.rnamespace = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === ( elem.ownerDocument || document ) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + // Fire handlers on the event path + i = 0; + while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { + lastElement = cur; + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( + dataPriv.get( cur, "events" ) || Object.create( null ) + )[ event.type ] && + dataPriv.get( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && handle.apply && acceptData( cur ) ) { + event.result = handle.apply( cur, data ); + if ( event.result === false ) { + event.preventDefault(); + } + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( ( !special._default || + special._default.apply( eventPath.pop(), data ) === false ) && + acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name as the event. + // Don't do default actions on window, that's where global variables be (#6170) + if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + + if ( event.isPropagationStopped() ) { + lastElement.addEventListener( type, stopPropagationCallback ); + } + + elem[ type ](); + + if ( event.isPropagationStopped() ) { + lastElement.removeEventListener( type, stopPropagationCallback ); + } + + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + // Piggyback on a donor event to simulate a different one + // Used only for `focus(in | out)` events + simulate: function( type, elem, event ) { + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true + } + ); + + jQuery.event.trigger( e, null, elem ); + } + +} ); + +jQuery.fn.extend( { + + trigger: function( type, data ) { + return this.each( function() { + jQuery.event.trigger( type, data, this ); + } ); + }, + triggerHandler: function( type, data ) { + var elem = this[ 0 ]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +} ); + + +// Support: Firefox <=44 +// Firefox doesn't have focus(in | out) events +// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 +// +// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1 +// focus(in | out) events fire after focus & blur events, +// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order +// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857 +if ( !support.focusin ) { + jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler on the document while someone wants focusin/focusout + var handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + + // Handle: regular nodes (via `this.ownerDocument`), window + // (via `this.document`) & document (via `this`). + var doc = this.ownerDocument || this.document || this, + attaches = dataPriv.access( doc, fix ); + + if ( !attaches ) { + doc.addEventListener( orig, handler, true ); + } + dataPriv.access( doc, fix, ( attaches || 0 ) + 1 ); + }, + teardown: function() { + var doc = this.ownerDocument || this.document || this, + attaches = dataPriv.access( doc, fix ) - 1; + + if ( !attaches ) { + doc.removeEventListener( orig, handler, true ); + dataPriv.remove( doc, fix ); + + } else { + dataPriv.access( doc, fix, attaches ); + } + } + }; + } ); +} +var location = window.location; + +var nonce = { guid: Date.now() }; + +var rquery = ( /\?/ ); + + + +// Cross-browser xml parsing +jQuery.parseXML = function( data ) { + var xml; + if ( !data || typeof data !== "string" ) { + return null; + } + + // Support: IE 9 - 11 only + // IE throws on parseFromString with invalid input. + try { + xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); + } catch ( e ) { + xml = undefined; + } + + if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; +}; + + +var + rbracket = /\[\]$/, + rCRLF = /\r?\n/g, + rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, + rsubmittable = /^(?:input|select|textarea|keygen)/i; + +function buildParams( prefix, obj, traditional, add ) { + var name; + + if ( Array.isArray( obj ) ) { + + // Serialize array item. + jQuery.each( obj, function( i, v ) { + if ( traditional || rbracket.test( prefix ) ) { + + // Treat each array item as a scalar. + add( prefix, v ); + + } else { + + // Item is non-scalar (array or object), encode its numeric index. + buildParams( + prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]", + v, + traditional, + add + ); + } + } ); + + } else if ( !traditional && toType( obj ) === "object" ) { + + // Serialize object item. + for ( name in obj ) { + buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); + } + + } else { + + // Serialize scalar item. + add( prefix, obj ); + } +} + +// Serialize an array of form elements or a set of +// key/values into a query string +jQuery.param = function( a, traditional ) { + var prefix, + s = [], + add = function( key, valueOrFunction ) { + + // If value is a function, invoke it and use its return value + var value = isFunction( valueOrFunction ) ? + valueOrFunction() : + valueOrFunction; + + s[ s.length ] = encodeURIComponent( key ) + "=" + + encodeURIComponent( value == null ? "" : value ); + }; + + if ( a == null ) { + return ""; + } + + // If an array was passed in, assume that it is an array of form elements. + if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { + + // Serialize the form elements + jQuery.each( a, function() { + add( this.name, this.value ); + } ); + + } else { + + // If traditional, encode the "old" way (the way 1.3.2 or older + // did it), otherwise encode params recursively. + for ( prefix in a ) { + buildParams( prefix, a[ prefix ], traditional, add ); + } + } + + // Return the resulting serialization + return s.join( "&" ); +}; + +jQuery.fn.extend( { + serialize: function() { + return jQuery.param( this.serializeArray() ); + }, + serializeArray: function() { + return this.map( function() { + + // Can add propHook for "elements" to filter or add form elements + var elements = jQuery.prop( this, "elements" ); + return elements ? jQuery.makeArray( elements ) : this; + } ) + .filter( function() { + var type = this.type; + + // Use .is( ":disabled" ) so that fieldset[disabled] works + return this.name && !jQuery( this ).is( ":disabled" ) && + rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && + ( this.checked || !rcheckableType.test( type ) ); + } ) + .map( function( _i, elem ) { + var val = jQuery( this ).val(); + + if ( val == null ) { + return null; + } + + if ( Array.isArray( val ) ) { + return jQuery.map( val, function( val ) { + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ); + } + + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ).get(); + } +} ); + + +var + r20 = /%20/g, + rhash = /#.*$/, + rantiCache = /([?&])_=[^&]*/, + rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg, + + // #7653, #8125, #8152: local protocol detection + rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, + rnoContent = /^(?:GET|HEAD)$/, + rprotocol = /^\/\//, + + /* Prefilters + * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) + * 2) These are called: + * - BEFORE asking for a transport + * - AFTER param serialization (s.data is a string if s.processData is true) + * 3) key is the dataType + * 4) the catchall symbol "*" can be used + * 5) execution will start with transport dataType and THEN continue down to "*" if needed + */ + prefilters = {}, + + /* Transports bindings + * 1) key is the dataType + * 2) the catchall symbol "*" can be used + * 3) selection will start with transport dataType and THEN go to "*" if needed + */ + transports = {}, + + // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression + allTypes = "*/".concat( "*" ), + + // Anchor tag for parsing the document origin + originAnchor = document.createElement( "a" ); + originAnchor.href = location.href; + +// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport +function addToPrefiltersOrTransports( structure ) { + + // dataTypeExpression is optional and defaults to "*" + return function( dataTypeExpression, func ) { + + if ( typeof dataTypeExpression !== "string" ) { + func = dataTypeExpression; + dataTypeExpression = "*"; + } + + var dataType, + i = 0, + dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || []; + + if ( isFunction( func ) ) { + + // For each dataType in the dataTypeExpression + while ( ( dataType = dataTypes[ i++ ] ) ) { + + // Prepend if requested + if ( dataType[ 0 ] === "+" ) { + dataType = dataType.slice( 1 ) || "*"; + ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func ); + + // Otherwise append + } else { + ( structure[ dataType ] = structure[ dataType ] || [] ).push( func ); + } + } + } + }; +} + +// Base inspection function for prefilters and transports +function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { + + var inspected = {}, + seekingTransport = ( structure === transports ); + + function inspect( dataType ) { + var selected; + inspected[ dataType ] = true; + jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { + var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); + if ( typeof dataTypeOrTransport === "string" && + !seekingTransport && !inspected[ dataTypeOrTransport ] ) { + + options.dataTypes.unshift( dataTypeOrTransport ); + inspect( dataTypeOrTransport ); + return false; + } else if ( seekingTransport ) { + return !( selected = dataTypeOrTransport ); + } + } ); + return selected; + } + + return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); +} + +// A special extend for ajax options +// that takes "flat" options (not to be deep extended) +// Fixes #9887 +function ajaxExtend( target, src ) { + var key, deep, + flatOptions = jQuery.ajaxSettings.flatOptions || {}; + + for ( key in src ) { + if ( src[ key ] !== undefined ) { + ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; + } + } + if ( deep ) { + jQuery.extend( true, target, deep ); + } + + return target; +} + +/* Handles responses to an ajax request: + * - finds the right dataType (mediates between content-type and expected dataType) + * - returns the corresponding response + */ +function ajaxHandleResponses( s, jqXHR, responses ) { + + var ct, type, finalDataType, firstDataType, + contents = s.contents, + dataTypes = s.dataTypes; + + // Remove auto dataType and get content-type in the process + while ( dataTypes[ 0 ] === "*" ) { + dataTypes.shift(); + if ( ct === undefined ) { + ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" ); + } + } + + // Check if we're dealing with a known content-type + if ( ct ) { + for ( type in contents ) { + if ( contents[ type ] && contents[ type ].test( ct ) ) { + dataTypes.unshift( type ); + break; + } + } + } + + // Check to see if we have a response for the expected dataType + if ( dataTypes[ 0 ] in responses ) { + finalDataType = dataTypes[ 0 ]; + } else { + + // Try convertible dataTypes + for ( type in responses ) { + if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) { + finalDataType = type; + break; + } + if ( !firstDataType ) { + firstDataType = type; + } + } + + // Or just use first one + finalDataType = finalDataType || firstDataType; + } + + // If we found a dataType + // We add the dataType to the list if needed + // and return the corresponding response + if ( finalDataType ) { + if ( finalDataType !== dataTypes[ 0 ] ) { + dataTypes.unshift( finalDataType ); + } + return responses[ finalDataType ]; + } +} + +/* Chain conversions given the request and the original response + * Also sets the responseXXX fields on the jqXHR instance + */ +function ajaxConvert( s, response, jqXHR, isSuccess ) { + var conv2, current, conv, tmp, prev, + converters = {}, + + // Work with a copy of dataTypes in case we need to modify it for conversion + dataTypes = s.dataTypes.slice(); + + // Create converters map with lowercased keys + if ( dataTypes[ 1 ] ) { + for ( conv in s.converters ) { + converters[ conv.toLowerCase() ] = s.converters[ conv ]; + } + } + + current = dataTypes.shift(); + + // Convert to each sequential dataType + while ( current ) { + + if ( s.responseFields[ current ] ) { + jqXHR[ s.responseFields[ current ] ] = response; + } + + // Apply the dataFilter if provided + if ( !prev && isSuccess && s.dataFilter ) { + response = s.dataFilter( response, s.dataType ); + } + + prev = current; + current = dataTypes.shift(); + + if ( current ) { + + // There's only work to do if current dataType is non-auto + if ( current === "*" ) { + + current = prev; + + // Convert response if prev dataType is non-auto and differs from current + } else if ( prev !== "*" && prev !== current ) { + + // Seek a direct converter + conv = converters[ prev + " " + current ] || converters[ "* " + current ]; + + // If none found, seek a pair + if ( !conv ) { + for ( conv2 in converters ) { + + // If conv2 outputs current + tmp = conv2.split( " " ); + if ( tmp[ 1 ] === current ) { + + // If prev can be converted to accepted input + conv = converters[ prev + " " + tmp[ 0 ] ] || + converters[ "* " + tmp[ 0 ] ]; + if ( conv ) { + + // Condense equivalence converters + if ( conv === true ) { + conv = converters[ conv2 ]; + + // Otherwise, insert the intermediate dataType + } else if ( converters[ conv2 ] !== true ) { + current = tmp[ 0 ]; + dataTypes.unshift( tmp[ 1 ] ); + } + break; + } + } + } + } + + // Apply converter (if not an equivalence) + if ( conv !== true ) { + + // Unless errors are allowed to bubble, catch and return them + if ( conv && s.throws ) { + response = conv( response ); + } else { + try { + response = conv( response ); + } catch ( e ) { + return { + state: "parsererror", + error: conv ? e : "No conversion from " + prev + " to " + current + }; + } + } + } + } + } + } + + return { state: "success", data: response }; +} + +jQuery.extend( { + + // Counter for holding the number of active queries + active: 0, + + // Last-Modified header cache for next request + lastModified: {}, + etag: {}, + + ajaxSettings: { + url: location.href, + type: "GET", + isLocal: rlocalProtocol.test( location.protocol ), + global: true, + processData: true, + async: true, + contentType: "application/x-www-form-urlencoded; charset=UTF-8", + + /* + timeout: 0, + data: null, + dataType: null, + username: null, + password: null, + cache: null, + throws: false, + traditional: false, + headers: {}, + */ + + accepts: { + "*": allTypes, + text: "text/plain", + html: "text/html", + xml: "application/xml, text/xml", + json: "application/json, text/javascript" + }, + + contents: { + xml: /\bxml\b/, + html: /\bhtml/, + json: /\bjson\b/ + }, + + responseFields: { + xml: "responseXML", + text: "responseText", + json: "responseJSON" + }, + + // Data converters + // Keys separate source (or catchall "*") and destination types with a single space + converters: { + + // Convert anything to text + "* text": String, + + // Text to html (true = no transformation) + "text html": true, + + // Evaluate text as a json expression + "text json": JSON.parse, + + // Parse text as xml + "text xml": jQuery.parseXML + }, + + // For options that shouldn't be deep extended: + // you can add your own custom options here if + // and when you create one that shouldn't be + // deep extended (see ajaxExtend) + flatOptions: { + url: true, + context: true + } + }, + + // Creates a full fledged settings object into target + // with both ajaxSettings and settings fields. + // If target is omitted, writes into ajaxSettings. + ajaxSetup: function( target, settings ) { + return settings ? + + // Building a settings object + ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : + + // Extending ajaxSettings + ajaxExtend( jQuery.ajaxSettings, target ); + }, + + ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), + ajaxTransport: addToPrefiltersOrTransports( transports ), + + // Main method + ajax: function( url, options ) { + + // If url is an object, simulate pre-1.5 signature + if ( typeof url === "object" ) { + options = url; + url = undefined; + } + + // Force options to be an object + options = options || {}; + + var transport, + + // URL without anti-cache param + cacheURL, + + // Response headers + responseHeadersString, + responseHeaders, + + // timeout handle + timeoutTimer, + + // Url cleanup var + urlAnchor, + + // Request state (becomes false upon send and true upon completion) + completed, + + // To know if global events are to be dispatched + fireGlobals, + + // Loop variable + i, + + // uncached part of the url + uncached, + + // Create the final options object + s = jQuery.ajaxSetup( {}, options ), + + // Callbacks context + callbackContext = s.context || s, + + // Context for global events is callbackContext if it is a DOM node or jQuery collection + globalEventContext = s.context && + ( callbackContext.nodeType || callbackContext.jquery ) ? + jQuery( callbackContext ) : + jQuery.event, + + // Deferreds + deferred = jQuery.Deferred(), + completeDeferred = jQuery.Callbacks( "once memory" ), + + // Status-dependent callbacks + statusCode = s.statusCode || {}, + + // Headers (they are sent all at once) + requestHeaders = {}, + requestHeadersNames = {}, + + // Default abort message + strAbort = "canceled", + + // Fake xhr + jqXHR = { + readyState: 0, + + // Builds headers hashtable if needed + getResponseHeader: function( key ) { + var match; + if ( completed ) { + if ( !responseHeaders ) { + responseHeaders = {}; + while ( ( match = rheaders.exec( responseHeadersString ) ) ) { + responseHeaders[ match[ 1 ].toLowerCase() + " " ] = + ( responseHeaders[ match[ 1 ].toLowerCase() + " " ] || [] ) + .concat( match[ 2 ] ); + } + } + match = responseHeaders[ key.toLowerCase() + " " ]; + } + return match == null ? null : match.join( ", " ); + }, + + // Raw string + getAllResponseHeaders: function() { + return completed ? responseHeadersString : null; + }, + + // Caches the header + setRequestHeader: function( name, value ) { + if ( completed == null ) { + name = requestHeadersNames[ name.toLowerCase() ] = + requestHeadersNames[ name.toLowerCase() ] || name; + requestHeaders[ name ] = value; + } + return this; + }, + + // Overrides response content-type header + overrideMimeType: function( type ) { + if ( completed == null ) { + s.mimeType = type; + } + return this; + }, + + // Status-dependent callbacks + statusCode: function( map ) { + var code; + if ( map ) { + if ( completed ) { + + // Execute the appropriate callbacks + jqXHR.always( map[ jqXHR.status ] ); + } else { + + // Lazy-add the new callbacks in a way that preserves old ones + for ( code in map ) { + statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; + } + } + } + return this; + }, + + // Cancel the request + abort: function( statusText ) { + var finalText = statusText || strAbort; + if ( transport ) { + transport.abort( finalText ); + } + done( 0, finalText ); + return this; + } + }; + + // Attach deferreds + deferred.promise( jqXHR ); + + // Add protocol if not provided (prefilters might expect it) + // Handle falsy url in the settings object (#10093: consistency with old signature) + // We also use the url parameter if available + s.url = ( ( url || s.url || location.href ) + "" ) + .replace( rprotocol, location.protocol + "//" ); + + // Alias method option to type as per ticket #12004 + s.type = options.method || options.type || s.method || s.type; + + // Extract dataTypes list + s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnothtmlwhite ) || [ "" ]; + + // A cross-domain request is in order when the origin doesn't match the current origin. + if ( s.crossDomain == null ) { + urlAnchor = document.createElement( "a" ); + + // Support: IE <=8 - 11, Edge 12 - 15 + // IE throws exception on accessing the href property if url is malformed, + // e.g. http://example.com:80x/ + try { + urlAnchor.href = s.url; + + // Support: IE <=8 - 11 only + // Anchor's host property isn't correctly set when s.url is relative + urlAnchor.href = urlAnchor.href; + s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !== + urlAnchor.protocol + "//" + urlAnchor.host; + } catch ( e ) { + + // If there is an error parsing the URL, assume it is crossDomain, + // it can be rejected by the transport if it is invalid + s.crossDomain = true; + } + } + + // Convert data if not already a string + if ( s.data && s.processData && typeof s.data !== "string" ) { + s.data = jQuery.param( s.data, s.traditional ); + } + + // Apply prefilters + inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); + + // If request was aborted inside a prefilter, stop there + if ( completed ) { + return jqXHR; + } + + // We can fire global events as of now if asked to + // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118) + fireGlobals = jQuery.event && s.global; + + // Watch for a new set of requests + if ( fireGlobals && jQuery.active++ === 0 ) { + jQuery.event.trigger( "ajaxStart" ); + } + + // Uppercase the type + s.type = s.type.toUpperCase(); + + // Determine if request has content + s.hasContent = !rnoContent.test( s.type ); + + // Save the URL in case we're toying with the If-Modified-Since + // and/or If-None-Match header later on + // Remove hash to simplify url manipulation + cacheURL = s.url.replace( rhash, "" ); + + // More options handling for requests with no content + if ( !s.hasContent ) { + + // Remember the hash so we can put it back + uncached = s.url.slice( cacheURL.length ); + + // If data is available and should be processed, append data to url + if ( s.data && ( s.processData || typeof s.data === "string" ) ) { + cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data; + + // #9682: remove data so that it's not used in an eventual retry + delete s.data; + } + + // Add or update anti-cache param if needed + if ( s.cache === false ) { + cacheURL = cacheURL.replace( rantiCache, "$1" ); + uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce.guid++ ) + + uncached; + } + + // Put hash and anti-cache on the URL that will be requested (gh-1732) + s.url = cacheURL + uncached; + + // Change '%20' to '+' if this is encoded form body content (gh-2658) + } else if ( s.data && s.processData && + ( s.contentType || "" ).indexOf( "application/x-www-form-urlencoded" ) === 0 ) { + s.data = s.data.replace( r20, "+" ); + } + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + if ( jQuery.lastModified[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); + } + if ( jQuery.etag[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); + } + } + + // Set the correct header, if data is being sent + if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { + jqXHR.setRequestHeader( "Content-Type", s.contentType ); + } + + // Set the Accepts header for the server, depending on the dataType + jqXHR.setRequestHeader( + "Accept", + s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ? + s.accepts[ s.dataTypes[ 0 ] ] + + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : + s.accepts[ "*" ] + ); + + // Check for headers option + for ( i in s.headers ) { + jqXHR.setRequestHeader( i, s.headers[ i ] ); + } + + // Allow custom headers/mimetypes and early abort + if ( s.beforeSend && + ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) { + + // Abort if not done already and return + return jqXHR.abort(); + } + + // Aborting is no longer a cancellation + strAbort = "abort"; + + // Install callbacks on deferreds + completeDeferred.add( s.complete ); + jqXHR.done( s.success ); + jqXHR.fail( s.error ); + + // Get transport + transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); + + // If no transport, we auto-abort + if ( !transport ) { + done( -1, "No Transport" ); + } else { + jqXHR.readyState = 1; + + // Send global event + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); + } + + // If request was aborted inside ajaxSend, stop there + if ( completed ) { + return jqXHR; + } + + // Timeout + if ( s.async && s.timeout > 0 ) { + timeoutTimer = window.setTimeout( function() { + jqXHR.abort( "timeout" ); + }, s.timeout ); + } + + try { + completed = false; + transport.send( requestHeaders, done ); + } catch ( e ) { + + // Rethrow post-completion exceptions + if ( completed ) { + throw e; + } + + // Propagate others as results + done( -1, e ); + } + } + + // Callback for when everything is done + function done( status, nativeStatusText, responses, headers ) { + var isSuccess, success, error, response, modified, + statusText = nativeStatusText; + + // Ignore repeat invocations + if ( completed ) { + return; + } + + completed = true; + + // Clear timeout if it exists + if ( timeoutTimer ) { + window.clearTimeout( timeoutTimer ); + } + + // Dereference transport for early garbage collection + // (no matter how long the jqXHR object will be used) + transport = undefined; + + // Cache response headers + responseHeadersString = headers || ""; + + // Set readyState + jqXHR.readyState = status > 0 ? 4 : 0; + + // Determine if successful + isSuccess = status >= 200 && status < 300 || status === 304; + + // Get response data + if ( responses ) { + response = ajaxHandleResponses( s, jqXHR, responses ); + } + + // Use a noop converter for missing script + if ( !isSuccess && jQuery.inArray( "script", s.dataTypes ) > -1 ) { + s.converters[ "text script" ] = function() {}; + } + + // Convert no matter what (that way responseXXX fields are always set) + response = ajaxConvert( s, response, jqXHR, isSuccess ); + + // If successful, handle type chaining + if ( isSuccess ) { + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + modified = jqXHR.getResponseHeader( "Last-Modified" ); + if ( modified ) { + jQuery.lastModified[ cacheURL ] = modified; + } + modified = jqXHR.getResponseHeader( "etag" ); + if ( modified ) { + jQuery.etag[ cacheURL ] = modified; + } + } + + // if no content + if ( status === 204 || s.type === "HEAD" ) { + statusText = "nocontent"; + + // if not modified + } else if ( status === 304 ) { + statusText = "notmodified"; + + // If we have data, let's convert it + } else { + statusText = response.state; + success = response.data; + error = response.error; + isSuccess = !error; + } + } else { + + // Extract error from statusText and normalize for non-aborts + error = statusText; + if ( status || !statusText ) { + statusText = "error"; + if ( status < 0 ) { + status = 0; + } + } + } + + // Set data for the fake xhr object + jqXHR.status = status; + jqXHR.statusText = ( nativeStatusText || statusText ) + ""; + + // Success/Error + if ( isSuccess ) { + deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); + } else { + deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); + } + + // Status-dependent callbacks + jqXHR.statusCode( statusCode ); + statusCode = undefined; + + if ( fireGlobals ) { + globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", + [ jqXHR, s, isSuccess ? success : error ] ); + } + + // Complete + completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); + + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); + + // Handle the global AJAX counter + if ( !( --jQuery.active ) ) { + jQuery.event.trigger( "ajaxStop" ); + } + } + } + + return jqXHR; + }, + + getJSON: function( url, data, callback ) { + return jQuery.get( url, data, callback, "json" ); + }, + + getScript: function( url, callback ) { + return jQuery.get( url, undefined, callback, "script" ); + } +} ); + +jQuery.each( [ "get", "post" ], function( _i, method ) { + jQuery[ method ] = function( url, data, callback, type ) { + + // Shift arguments if data argument was omitted + if ( isFunction( data ) ) { + type = type || callback; + callback = data; + data = undefined; + } + + // The url can be an options object (which then must have .url) + return jQuery.ajax( jQuery.extend( { + url: url, + type: method, + dataType: type, + data: data, + success: callback + }, jQuery.isPlainObject( url ) && url ) ); + }; +} ); + +jQuery.ajaxPrefilter( function( s ) { + var i; + for ( i in s.headers ) { + if ( i.toLowerCase() === "content-type" ) { + s.contentType = s.headers[ i ] || ""; + } + } +} ); + + +jQuery._evalUrl = function( url, options, doc ) { + return jQuery.ajax( { + url: url, + + // Make this explicit, since user can override this through ajaxSetup (#11264) + type: "GET", + dataType: "script", + cache: true, + async: false, + global: false, + + // Only evaluate the response if it is successful (gh-4126) + // dataFilter is not invoked for failure responses, so using it instead + // of the default converter is kludgy but it works. + converters: { + "text script": function() {} + }, + dataFilter: function( response ) { + jQuery.globalEval( response, options, doc ); + } + } ); +}; + + +jQuery.fn.extend( { + wrapAll: function( html ) { + var wrap; + + if ( this[ 0 ] ) { + if ( isFunction( html ) ) { + html = html.call( this[ 0 ] ); + } + + // The elements to wrap the target around + wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); + + if ( this[ 0 ].parentNode ) { + wrap.insertBefore( this[ 0 ] ); + } + + wrap.map( function() { + var elem = this; + + while ( elem.firstElementChild ) { + elem = elem.firstElementChild; + } + + return elem; + } ).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( isFunction( html ) ) { + return this.each( function( i ) { + jQuery( this ).wrapInner( html.call( this, i ) ); + } ); + } + + return this.each( function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + } ); + }, + + wrap: function( html ) { + var htmlIsFunction = isFunction( html ); + + return this.each( function( i ) { + jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html ); + } ); + }, + + unwrap: function( selector ) { + this.parent( selector ).not( "body" ).each( function() { + jQuery( this ).replaceWith( this.childNodes ); + } ); + return this; + } +} ); + + +jQuery.expr.pseudos.hidden = function( elem ) { + return !jQuery.expr.pseudos.visible( elem ); +}; +jQuery.expr.pseudos.visible = function( elem ) { + return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); +}; + + + + +jQuery.ajaxSettings.xhr = function() { + try { + return new window.XMLHttpRequest(); + } catch ( e ) {} +}; + +var xhrSuccessStatus = { + + // File protocol always yields status code 0, assume 200 + 0: 200, + + // Support: IE <=9 only + // #1450: sometimes IE returns 1223 when it should be 204 + 1223: 204 + }, + xhrSupported = jQuery.ajaxSettings.xhr(); + +support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); +support.ajax = xhrSupported = !!xhrSupported; + +jQuery.ajaxTransport( function( options ) { + var callback, errorCallback; + + // Cross domain only allowed if supported through XMLHttpRequest + if ( support.cors || xhrSupported && !options.crossDomain ) { + return { + send: function( headers, complete ) { + var i, + xhr = options.xhr(); + + xhr.open( + options.type, + options.url, + options.async, + options.username, + options.password + ); + + // Apply custom fields if provided + if ( options.xhrFields ) { + for ( i in options.xhrFields ) { + xhr[ i ] = options.xhrFields[ i ]; + } + } + + // Override mime type if needed + if ( options.mimeType && xhr.overrideMimeType ) { + xhr.overrideMimeType( options.mimeType ); + } + + // X-Requested-With header + // For cross-domain requests, seeing as conditions for a preflight are + // akin to a jigsaw puzzle, we simply never set it to be sure. + // (it can always be set on a per-request basis or even using ajaxSetup) + // For same-domain requests, won't change header if already provided. + if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) { + headers[ "X-Requested-With" ] = "XMLHttpRequest"; + } + + // Set headers + for ( i in headers ) { + xhr.setRequestHeader( i, headers[ i ] ); + } + + // Callback + callback = function( type ) { + return function() { + if ( callback ) { + callback = errorCallback = xhr.onload = + xhr.onerror = xhr.onabort = xhr.ontimeout = + xhr.onreadystatechange = null; + + if ( type === "abort" ) { + xhr.abort(); + } else if ( type === "error" ) { + + // Support: IE <=9 only + // On a manual native abort, IE9 throws + // errors on any property access that is not readyState + if ( typeof xhr.status !== "number" ) { + complete( 0, "error" ); + } else { + complete( + + // File: protocol always yields status 0; see #8605, #14207 + xhr.status, + xhr.statusText + ); + } + } else { + complete( + xhrSuccessStatus[ xhr.status ] || xhr.status, + xhr.statusText, + + // Support: IE <=9 only + // IE9 has no XHR2 but throws on binary (trac-11426) + // For XHR2 non-text, let the caller handle it (gh-2498) + ( xhr.responseType || "text" ) !== "text" || + typeof xhr.responseText !== "string" ? + { binary: xhr.response } : + { text: xhr.responseText }, + xhr.getAllResponseHeaders() + ); + } + } + }; + }; + + // Listen to events + xhr.onload = callback(); + errorCallback = xhr.onerror = xhr.ontimeout = callback( "error" ); + + // Support: IE 9 only + // Use onreadystatechange to replace onabort + // to handle uncaught aborts + if ( xhr.onabort !== undefined ) { + xhr.onabort = errorCallback; + } else { + xhr.onreadystatechange = function() { + + // Check readyState before timeout as it changes + if ( xhr.readyState === 4 ) { + + // Allow onerror to be called first, + // but that will not handle a native abort + // Also, save errorCallback to a variable + // as xhr.onerror cannot be accessed + window.setTimeout( function() { + if ( callback ) { + errorCallback(); + } + } ); + } + }; + } + + // Create the abort callback + callback = callback( "abort" ); + + try { + + // Do send the request (this may raise an exception) + xhr.send( options.hasContent && options.data || null ); + } catch ( e ) { + + // #14683: Only rethrow if this hasn't been notified as an error yet + if ( callback ) { + throw e; + } + } + }, + + abort: function() { + if ( callback ) { + callback(); + } + } + }; + } +} ); + + + + +// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432) +jQuery.ajaxPrefilter( function( s ) { + if ( s.crossDomain ) { + s.contents.script = false; + } +} ); + +// Install script dataType +jQuery.ajaxSetup( { + accepts: { + script: "text/javascript, application/javascript, " + + "application/ecmascript, application/x-ecmascript" + }, + contents: { + script: /\b(?:java|ecma)script\b/ + }, + converters: { + "text script": function( text ) { + jQuery.globalEval( text ); + return text; + } + } +} ); + +// Handle cache's special case and crossDomain +jQuery.ajaxPrefilter( "script", function( s ) { + if ( s.cache === undefined ) { + s.cache = false; + } + if ( s.crossDomain ) { + s.type = "GET"; + } +} ); + +// Bind script tag hack transport +jQuery.ajaxTransport( "script", function( s ) { + + // This transport only deals with cross domain or forced-by-attrs requests + if ( s.crossDomain || s.scriptAttrs ) { + var script, callback; + return { + send: function( _, complete ) { + script = jQuery( " + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/class_schedule/lesson01.html b/class_schedule/lesson01.html new file mode 100644 index 0000000..fe61bf5 --- /dev/null +++ b/class_schedule/lesson01.html @@ -0,0 +1,173 @@ + + + + + + Lesson 1: Setting up Your Environment — Programming in Python 7.0 documentation + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/class_schedule/lesson02.html b/class_schedule/lesson02.html new file mode 100644 index 0000000..a380f84 --- /dev/null +++ b/class_schedule/lesson02.html @@ -0,0 +1,173 @@ + + + + + + Lesson 2: Basic Python and Functions — Programming in Python 7.0 documentation + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/class_schedule/lesson03.html b/class_schedule/lesson03.html new file mode 100644 index 0000000..41e124e --- /dev/null +++ b/class_schedule/lesson03.html @@ -0,0 +1,156 @@ + + + + + + Session 3: Booleans, Sequences, Iteration, and Strings — Programming in Python 7.0 documentation + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/class_schedule/lesson04.html b/class_schedule/lesson04.html new file mode 100644 index 0000000..bd545cf --- /dev/null +++ b/class_schedule/lesson04.html @@ -0,0 +1,150 @@ + + + + + + Session 4: Dictionaries and Sets and Unit Testing — Programming in Python 7.0 documentation + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/class_schedule/lesson05.html b/class_schedule/lesson05.html new file mode 100644 index 0000000..7b28196 --- /dev/null +++ b/class_schedule/lesson05.html @@ -0,0 +1,143 @@ + + + + + + Lession 5: File Handling, Exceptions and Comprehensions — Programming in Python 7.0 documentation + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/class_schedule/lesson06.html b/class_schedule/lesson06.html new file mode 100644 index 0000000..0f62bb0 --- /dev/null +++ b/class_schedule/lesson06.html @@ -0,0 +1,148 @@ + + + + + + Session 6: Advanced Argument Passing and Modules — Programming in Python 7.0 documentation + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/class_schedule/lesson07.html b/class_schedule/lesson07.html new file mode 100644 index 0000000..25f403a --- /dev/null +++ b/class_schedule/lesson07.html @@ -0,0 +1,179 @@ + + + + + + Session 7: Object Oriented Programing — Programming in Python 7.0 documentation + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/class_schedule/lesson08.html b/class_schedule/lesson08.html new file mode 100644 index 0000000..5e6eda6 --- /dev/null +++ b/class_schedule/lesson08.html @@ -0,0 +1,156 @@ + + + + + + Session 8: Properties and Magic methods — Programming in Python 7.0 documentation + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/class_schedule/lesson09.html b/class_schedule/lesson09.html new file mode 100644 index 0000000..71e097f --- /dev/null +++ b/class_schedule/lesson09.html @@ -0,0 +1,147 @@ + + + + + + Session 9: Static and class methods: multiple inheritance — Programming in Python 7.0 documentation + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/class_schedule/lesson10.html b/class_schedule/lesson10.html new file mode 100644 index 0000000..a9053cb --- /dev/null +++ b/class_schedule/lesson10.html @@ -0,0 +1,134 @@ + + + + + + Session 10: Intro to Functional Programming: lambda and Map, Filter, Reduce — Programming in Python 7.0 documentation + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/class_schedule/orientation.html b/class_schedule/orientation.html new file mode 100644 index 0000000..4530936 --- /dev/null +++ b/class_schedule/orientation.html @@ -0,0 +1,137 @@ + + + + + + Orientation — Programming in Python 7.0 documentation + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/args_kwargs_lab.html b/exercises/args_kwargs_lab.html new file mode 100644 index 0000000..bf74069 --- /dev/null +++ b/exercises/args_kwargs_lab.html @@ -0,0 +1,209 @@ + + + + + + args and kwargs Lab — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/circle/circle_class.html b/exercises/circle/circle_class.html new file mode 100644 index 0000000..87d6f5b --- /dev/null +++ b/exercises/circle/circle_class.html @@ -0,0 +1,344 @@ + + + + + + Circle Class Exercise — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/circle_class.html b/exercises/circle_class.html new file mode 100644 index 0000000..4b19bdc --- /dev/null +++ b/exercises/circle_class.html @@ -0,0 +1,451 @@ + + + + + + + + + + + Circle Class Exercise — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/codingbat.html b/exercises/codingbat.html new file mode 100644 index 0000000..c8b9580 --- /dev/null +++ b/exercises/codingbat.html @@ -0,0 +1,144 @@ + + + + + + Practice with CodingBat — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/comprehensions_lab.html b/exercises/comprehensions_lab.html new file mode 100644 index 0000000..1cf9ec1 --- /dev/null +++ b/exercises/comprehensions_lab.html @@ -0,0 +1,318 @@ + + + + + + Comprehensions Lab — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/context-managers-exercise.html b/exercises/context-managers-exercise.html new file mode 100644 index 0000000..d56ee45 --- /dev/null +++ b/exercises/context-managers-exercise.html @@ -0,0 +1,233 @@ + + + + + + A Couple Handy Context Managers — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/dict_lab.html b/exercises/dict_lab.html new file mode 100644 index 0000000..5b86eb5 --- /dev/null +++ b/exercises/dict_lab.html @@ -0,0 +1,214 @@ + + + + + + Dictionary and Set Lab — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/except_exercise.html b/exercises/except_exercise.html new file mode 100644 index 0000000..2be7844 --- /dev/null +++ b/exercises/except_exercise.html @@ -0,0 +1,307 @@ + + + + + + + + + + Exceptions Exercise — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/exceptions/except_exercise.html b/exercises/exceptions/except_exercise.html new file mode 100644 index 0000000..399324d --- /dev/null +++ b/exercises/exceptions/except_exercise.html @@ -0,0 +1,191 @@ + + + + + + Exceptions Exercise — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/exceptions/exceptions_lab.html b/exercises/exceptions/exceptions_lab.html new file mode 100644 index 0000000..58738d4 --- /dev/null +++ b/exercises/exceptions/exceptions_lab.html @@ -0,0 +1,147 @@ + + + + + + Exceptions Lab — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/exceptions_lab.html b/exercises/exceptions_lab.html new file mode 100644 index 0000000..995568e --- /dev/null +++ b/exercises/exceptions_lab.html @@ -0,0 +1,264 @@ + + + + + + + + + + Exceptions Lab — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/fib_and_lucas.html b/exercises/fib_and_lucas.html new file mode 100644 index 0000000..b2828b9 --- /dev/null +++ b/exercises/fib_and_lucas.html @@ -0,0 +1,324 @@ + + + + + + + + + + Fibonacci Series Exercise — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/file_lab.html b/exercises/file_lab.html new file mode 100644 index 0000000..8e2f1f3 --- /dev/null +++ b/exercises/file_lab.html @@ -0,0 +1,283 @@ + + + + + + + + + + + 9.2. File Exercise — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/file_processing/file_lab.html b/exercises/file_processing/file_lab.html new file mode 100644 index 0000000..b750a63 --- /dev/null +++ b/exercises/file_processing/file_lab.html @@ -0,0 +1,157 @@ + + + + + + File Exercise — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/file_processing/file_processing.html b/exercises/file_processing/file_processing.html new file mode 100644 index 0000000..a8847e5 --- /dev/null +++ b/exercises/file_processing/file_processing.html @@ -0,0 +1,153 @@ + + + + + + File Processing — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/fizz_buzz.html b/exercises/fizz_buzz.html new file mode 100644 index 0000000..3e76fd7 --- /dev/null +++ b/exercises/fizz_buzz.html @@ -0,0 +1,190 @@ + + + + + + Fizz Buzz Exercise — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/grid_printer.html b/exercises/grid_printer.html new file mode 100644 index 0000000..9d22eca --- /dev/null +++ b/exercises/grid_printer.html @@ -0,0 +1,314 @@ + + + + + + Grid Printer Exercise — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/html_renderer.html b/exercises/html_renderer.html new file mode 100644 index 0000000..a42ef98 --- /dev/null +++ b/exercises/html_renderer.html @@ -0,0 +1,800 @@ + + + + + + + + + + HTML Renderer Exercise — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/html_renderer/html_renderer.html b/exercises/html_renderer/html_renderer.html new file mode 100644 index 0000000..efd124e --- /dev/null +++ b/exercises/html_renderer/html_renderer.html @@ -0,0 +1,683 @@ + + + + + + HTML Renderer Exercise — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/html_renderer/html_renderer_tutorial.html b/exercises/html_renderer/html_renderer_tutorial.html new file mode 100644 index 0000000..4896795 --- /dev/null +++ b/exercises/html_renderer/html_renderer_tutorial.html @@ -0,0 +1,1513 @@ + + + + + + Tutorial for the Html Render Assignment — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/html_renderer_tutorial.html b/exercises/html_renderer_tutorial.html new file mode 100644 index 0000000..cb1397d --- /dev/null +++ b/exercises/html_renderer_tutorial.html @@ -0,0 +1,1624 @@ + + + + + + + + + + Tutorial for the Html Render Assignment — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/index.html b/exercises/index.html new file mode 100644 index 0000000..9cd5f35 --- /dev/null +++ b/exercises/index.html @@ -0,0 +1,563 @@ + + + + + + + + + + + All Exercises — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/kata_fourteen.html b/exercises/kata_fourteen.html new file mode 100644 index 0000000..9303fde --- /dev/null +++ b/exercises/kata_fourteen.html @@ -0,0 +1,664 @@ + + + + + + + + + + + Trigrams – Simple Text Manipulation — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/lambda_magic.html b/exercises/lambda_magic.html new file mode 100644 index 0000000..c76eb00 --- /dev/null +++ b/exercises/lambda_magic.html @@ -0,0 +1,180 @@ + + + + + + lambda and keyword Magic — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/list_lab.html b/exercises/list_lab.html new file mode 100644 index 0000000..0904fe8 --- /dev/null +++ b/exercises/list_lab.html @@ -0,0 +1,226 @@ + + + + + + List Lab — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/mailroom-decorator.html b/exercises/mailroom-decorator.html new file mode 100644 index 0000000..b23903b --- /dev/null +++ b/exercises/mailroom-decorator.html @@ -0,0 +1,243 @@ + + + + + + + + + + + Mailroom – Decoratoring it — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/mailroom-fp.html b/exercises/mailroom-fp.html new file mode 100644 index 0000000..68203cc --- /dev/null +++ b/exercises/mailroom-fp.html @@ -0,0 +1,268 @@ + + + + + + + + + + + Mailroom - Functional — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/mailroom-meta.html b/exercises/mailroom-meta.html new file mode 100644 index 0000000..b0633c9 --- /dev/null +++ b/exercises/mailroom-meta.html @@ -0,0 +1,258 @@ + + + + + + + + + + + Mailroom – metaprogramming it! — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/mailroom-mock.html b/exercises/mailroom-mock.html new file mode 100644 index 0000000..e88d6ca --- /dev/null +++ b/exercises/mailroom-mock.html @@ -0,0 +1,272 @@ + + + + + + + + + + + Mocking Mailroom — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/mailroom-oo.html b/exercises/mailroom-oo.html new file mode 100644 index 0000000..8caf5e9 --- /dev/null +++ b/exercises/mailroom-oo.html @@ -0,0 +1,411 @@ + + + + + + + + + + + Mailroom - Object Oriented — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/mailroom-part1.html b/exercises/mailroom-part1.html new file mode 100644 index 0000000..b4a6293 --- /dev/null +++ b/exercises/mailroom-part1.html @@ -0,0 +1,321 @@ + + + + + + + + + + + Mailroom Part 1 — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/mailroom-part2.html b/exercises/mailroom-part2.html new file mode 100644 index 0000000..42c6295 --- /dev/null +++ b/exercises/mailroom-part2.html @@ -0,0 +1,321 @@ + + + + + + + + + + + Mailroom Part 2 — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/mailroom-part3.html b/exercises/mailroom-part3.html new file mode 100644 index 0000000..245dd38 --- /dev/null +++ b/exercises/mailroom-part3.html @@ -0,0 +1,261 @@ + + + + + + + + + + + Mailroom Part 3 — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/mailroom-part4.html b/exercises/mailroom-part4.html new file mode 100644 index 0000000..eb8ef32 --- /dev/null +++ b/exercises/mailroom-part4.html @@ -0,0 +1,294 @@ + + + + + + + + + + + Mailroom Part 4 — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/mailroom-pkg.html b/exercises/mailroom-pkg.html new file mode 100644 index 0000000..530ae76 --- /dev/null +++ b/exercises/mailroom-pkg.html @@ -0,0 +1,309 @@ + + + + + + + + + + + Mailroom – as a Python Package — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/mailroom/mailroom-decorator.html b/exercises/mailroom/mailroom-decorator.html new file mode 100644 index 0000000..c84ee3b --- /dev/null +++ b/exercises/mailroom/mailroom-decorator.html @@ -0,0 +1,154 @@ + + + + + + Mailroom – Decoratoring it — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/mailroom/mailroom-fp.html b/exercises/mailroom/mailroom-fp.html new file mode 100644 index 0000000..0f05b20 --- /dev/null +++ b/exercises/mailroom/mailroom-fp.html @@ -0,0 +1,189 @@ + + + + + + Mailroom - Functional — Programming in Python 7.0 documentation + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/mailroom/mailroom-meta.html b/exercises/mailroom/mailroom-meta.html new file mode 100644 index 0000000..a07aec8 --- /dev/null +++ b/exercises/mailroom/mailroom-meta.html @@ -0,0 +1,168 @@ + + + + + + Mailroom – metaprogramming it! — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/mailroom/mailroom-mock.html b/exercises/mailroom/mailroom-mock.html new file mode 100644 index 0000000..0397fb0 --- /dev/null +++ b/exercises/mailroom/mailroom-mock.html @@ -0,0 +1,162 @@ + + + + + + Mocking Mailroom — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/mailroom/mailroom-oo.html b/exercises/mailroom/mailroom-oo.html new file mode 100644 index 0000000..4ca47a7 --- /dev/null +++ b/exercises/mailroom/mailroom-oo.html @@ -0,0 +1,324 @@ + + + + + + Mailroom - Object Oriented — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/mailroom/mailroom-pkg.html b/exercises/mailroom/mailroom-pkg.html new file mode 100644 index 0000000..ea5eb04 --- /dev/null +++ b/exercises/mailroom/mailroom-pkg.html @@ -0,0 +1,235 @@ + + + + + + Mailroom – as a Python Package — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/mailroom/mailroom.html b/exercises/mailroom/mailroom.html new file mode 100644 index 0000000..0ab665c --- /dev/null +++ b/exercises/mailroom/mailroom.html @@ -0,0 +1,213 @@ + + + + + + Mailroom — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/mailroom/mailroom_tutorial.html b/exercises/mailroom/mailroom_tutorial.html new file mode 100644 index 0000000..a6b9e1d --- /dev/null +++ b/exercises/mailroom/mailroom_tutorial.html @@ -0,0 +1,291 @@ + + + + + + Mailroom Tutorial — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/mailroom/mailroom_with_comprehansions.html b/exercises/mailroom/mailroom_with_comprehansions.html new file mode 100644 index 0000000..341013c --- /dev/null +++ b/exercises/mailroom/mailroom_with_comprehansions.html @@ -0,0 +1,388 @@ + + + + + + + + + + + Mailroom With Comprehensions — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/mailroom/mailroom_with_comprehensions.html b/exercises/mailroom/mailroom_with_comprehensions.html new file mode 100644 index 0000000..a817009 --- /dev/null +++ b/exercises/mailroom/mailroom_with_comprehensions.html @@ -0,0 +1,165 @@ + + + + + + Mailroom With Comprehensions — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/mailroom/mailroom_with_dicts.html b/exercises/mailroom/mailroom_with_dicts.html new file mode 100644 index 0000000..b1f11bc --- /dev/null +++ b/exercises/mailroom/mailroom_with_dicts.html @@ -0,0 +1,158 @@ + + + + + + Mailroom With Dicts — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/mailroom/mailroom_with_exceptions.html b/exercises/mailroom/mailroom_with_exceptions.html new file mode 100644 index 0000000..308e57b --- /dev/null +++ b/exercises/mailroom/mailroom_with_exceptions.html @@ -0,0 +1,150 @@ + + + + + + Mailroom With Exceptions — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/mailroom/mailroom_with_files.html b/exercises/mailroom/mailroom_with_files.html new file mode 100644 index 0000000..47a5390 --- /dev/null +++ b/exercises/mailroom/mailroom_with_files.html @@ -0,0 +1,178 @@ + + + + + + Mailroom With Files — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/mailroom/mailroom_with_full_tests.html b/exercises/mailroom/mailroom_with_full_tests.html new file mode 100644 index 0000000..6bd84f1 --- /dev/null +++ b/exercises/mailroom/mailroom_with_full_tests.html @@ -0,0 +1,248 @@ + + + + + + + + + + + 17.3.1. Mailroom With Full Unit Tests — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/mailroom/mailroom_with_tests.html b/exercises/mailroom/mailroom_with_tests.html new file mode 100644 index 0000000..b139299 --- /dev/null +++ b/exercises/mailroom/mailroom_with_tests.html @@ -0,0 +1,213 @@ + + + + + + Mailroom With Unit Tests — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/mailroom_tutorial.html b/exercises/mailroom_tutorial.html new file mode 100644 index 0000000..bf74914 --- /dev/null +++ b/exercises/mailroom_tutorial.html @@ -0,0 +1,394 @@ + + + + + + + + + + + Mailroom Tutorial — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/oo_intro.html b/exercises/oo_intro.html new file mode 100644 index 0000000..8d12923 --- /dev/null +++ b/exercises/oo_intro.html @@ -0,0 +1,268 @@ + + + + + + + + + + OO Intro - Report Class — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/oo_intro/oo_intro.html b/exercises/oo_intro/oo_intro.html new file mode 100644 index 0000000..8c3bf09 --- /dev/null +++ b/exercises/oo_intro/oo_intro.html @@ -0,0 +1,157 @@ + + + + + + OO Intro - Report Class — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/packaging/package_lab.html b/exercises/packaging/package_lab.html new file mode 100644 index 0000000..b25cb19 --- /dev/null +++ b/exercises/packaging/package_lab.html @@ -0,0 +1,161 @@ + + + + + + A Small Example Package — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/python_pushups.html b/exercises/python_pushups.html new file mode 100644 index 0000000..4872c42 --- /dev/null +++ b/exercises/python_pushups.html @@ -0,0 +1,166 @@ + + + + + + Python Pushups — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/roman.html b/exercises/roman.html new file mode 100644 index 0000000..6396053 --- /dev/null +++ b/exercises/roman.html @@ -0,0 +1,245 @@ + + + + + + + + + + + Roman/Arabic Numeral Converter — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + +
      + + + + + + + + + + + + + + + + + +
      + + + + +
      +
      +
      +
      + +
      +

      Roman/Arabic Numeral Converter

      +

      Roman and Arabic numerals are different enough that a program to convert back and forth between the two systems would be useful assuming, of course, that you are a Roman trader living sometime in the Middle Ages with access to a computer with a Python interpreter.

      +

      Write a class-based program to convert between the two formats.

      +

      https://en.wikipedia.org/wiki/Roman_numerals

      +

      https://en.wikipedia.org/wiki/Arabic_numerals

      +
      + + +
      + +
      +
      + + + + +
      + +
      +

      + © Copyright 2020, University of Washington, Natasha Aleksandrova, Christopher Barker, Brian Dorsey, Cris Ewing, Christy Heaton, Jon Jacky, Maria McKinley, Andy Miles, Rick Riehle, Joseph Schilz, Joseph Sheedy, Hosung Song. Creative Commons Attribution-ShareAlike 4.0 license + +

      +
      + Built with Sphinx using a theme provided by Read the Docs. + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/rot13.html b/exercises/rot13.html new file mode 100644 index 0000000..2479931 --- /dev/null +++ b/exercises/rot13.html @@ -0,0 +1,189 @@ + + + + + + ROT13 — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/series/fib_and_lucas.html b/exercises/series/fib_and_lucas.html new file mode 100644 index 0000000..7de0a88 --- /dev/null +++ b/exercises/series/fib_and_lucas.html @@ -0,0 +1,207 @@ + + + + + + Fibonacci Series Exercise — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/slicing.html b/exercises/slicing.html new file mode 100644 index 0000000..a4dbf8c --- /dev/null +++ b/exercises/slicing.html @@ -0,0 +1,178 @@ + + + + + + Slicing Lab — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/sparse_array.html b/exercises/sparse_array.html new file mode 100644 index 0000000..6b9b5b5 --- /dev/null +++ b/exercises/sparse_array.html @@ -0,0 +1,195 @@ + + + + + + Sparse Array Exercise — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/string_formatting.html b/exercises/string_formatting.html new file mode 100644 index 0000000..47eb216 --- /dev/null +++ b/exercises/string_formatting.html @@ -0,0 +1,321 @@ + + + + + + String Formatting Exercise — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/threaded_downloader.html b/exercises/threaded_downloader.html new file mode 100644 index 0000000..bec2eb7 --- /dev/null +++ b/exercises/threaded_downloader.html @@ -0,0 +1,175 @@ + + + + + + Threaded Web Scraper — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/trapezoid.html b/exercises/trapezoid.html new file mode 100644 index 0000000..0fb757d --- /dev/null +++ b/exercises/trapezoid.html @@ -0,0 +1,323 @@ + + + + + + Trapezoidal Rule — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/trigrams/trigrams.html b/exercises/trigrams/trigrams.html new file mode 100644 index 0000000..a856034 --- /dev/null +++ b/exercises/trigrams/trigrams.html @@ -0,0 +1,629 @@ + + + + + + Trigrams – Simple Text Manipulation — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/exercises/unit_testing.html b/exercises/unit_testing.html new file mode 100644 index 0000000..3cd3213 --- /dev/null +++ b/exercises/unit_testing.html @@ -0,0 +1,454 @@ + + + + + + + + + + + Introduction To Unit Testing — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/unit_testing/unit_testing.html b/exercises/unit_testing/unit_testing.html new file mode 100644 index 0000000..8da1895 --- /dev/null +++ b/exercises/unit_testing/unit_testing.html @@ -0,0 +1,212 @@ + + + + + + Introduction To Unit Testing — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/genindex.html b/genindex.html new file mode 100644 index 0000000..80531bc --- /dev/null +++ b/genindex.html @@ -0,0 +1,119 @@ + + + + + + Index — Programming in Python 7.0 documentation + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..f432042 --- /dev/null +++ b/index.html @@ -0,0 +1,167 @@ + + + + + + Programming in Python — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/make.bat b/make.bat deleted file mode 100644 index 6247f7e..0000000 --- a/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/modules/AdvancedArgumentPassing.html b/modules/AdvancedArgumentPassing.html new file mode 100644 index 0000000..7b61741 --- /dev/null +++ b/modules/AdvancedArgumentPassing.html @@ -0,0 +1,365 @@ + + + + + + Advanced Argument Passing — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Async.html b/modules/Async.html new file mode 100644 index 0000000..4936f0e --- /dev/null +++ b/modules/Async.html @@ -0,0 +1,603 @@ + + + + + + Asychronous Programming — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/BasicPython.html b/modules/BasicPython.html new file mode 100644 index 0000000..1f1e814 --- /dev/null +++ b/modules/BasicPython.html @@ -0,0 +1,1069 @@ + + + + + + Basic Python — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Booleans.html b/modules/Booleans.html new file mode 100644 index 0000000..c52eda0 --- /dev/null +++ b/modules/Booleans.html @@ -0,0 +1,348 @@ + + + + + + Boolean Expressions — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/CallableClasses.html b/modules/CallableClasses.html new file mode 100644 index 0000000..f0a87e6 --- /dev/null +++ b/modules/CallableClasses.html @@ -0,0 +1,121 @@ + + + + + + Callable Classes — Programming in Python 7.0 documentation + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Class_introduction.html b/modules/Class_introduction.html new file mode 100644 index 0000000..b741da9 --- /dev/null +++ b/modules/Class_introduction.html @@ -0,0 +1,289 @@ + + + + + + + + + + + Introduction to the in-class version of the program — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/Closures.html b/modules/Closures.html new file mode 100644 index 0000000..7e0c022 --- /dev/null +++ b/modules/Closures.html @@ -0,0 +1,590 @@ + + + + + + Closures and Function Currying — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/CodeReviews.html b/modules/CodeReviews.html new file mode 100644 index 0000000..b3c2742 --- /dev/null +++ b/modules/CodeReviews.html @@ -0,0 +1,239 @@ + + + + + + Code Reviews — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/CollectionsModule.html b/modules/CollectionsModule.html new file mode 100644 index 0000000..36a7e2b --- /dev/null +++ b/modules/CollectionsModule.html @@ -0,0 +1,190 @@ + + + + + + The Collections Module — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Comprehensions.html b/modules/Comprehensions.html new file mode 100644 index 0000000..c941aea --- /dev/null +++ b/modules/Comprehensions.html @@ -0,0 +1,486 @@ + + + + + + Comprehensions — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Concurrency.html b/modules/Concurrency.html new file mode 100644 index 0000000..881742c --- /dev/null +++ b/modules/Concurrency.html @@ -0,0 +1,437 @@ + + + + + + Concurrent Programming — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/ContextManagers.html b/modules/ContextManagers.html new file mode 100644 index 0000000..3408d94 --- /dev/null +++ b/modules/ContextManagers.html @@ -0,0 +1,393 @@ + + + + + + Context Managers — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Coroutines.html b/modules/Coroutines.html new file mode 100644 index 0000000..3b1181c --- /dev/null +++ b/modules/Coroutines.html @@ -0,0 +1,226 @@ + + + + + + Notes on Coroutines — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/DIP-unit-testing.html b/modules/DIP-unit-testing.html new file mode 100644 index 0000000..b1ab181 --- /dev/null +++ b/modules/DIP-unit-testing.html @@ -0,0 +1,1341 @@ + + + + + + + + + + + Unit Testing — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + +
      + + + + + + + + + + + + + + + + + +
      + + + + +
      +
      +
      +
      + +

      Unit testing - Dive Into Python 3

      +

      You are here: HomeDive Into Python +3

      +
      +

      Unit Testing

      +
      +
      +
      ❝ Certitude is not the test of certainty. We have been cocksure of +many things that were not so. ❞
      + +
      +
      +
      +

      (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 chapter, you’re going to write and debug a set of utility +functions to convert to and from Roman numerals. You saw the mechanics +of constructing and validating Roman numerals in “Case study: roman +numerals”. Now step back and +consider what it would take to expand that into a two-way utility.

      +

      The rules for Roman +numerals lead to a number of +interesting observations:

      +
        +
      1. There is only one correct way to represent a particular number as a +Roman numeral.

      2. +
      3. 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).

      4. +
      5. 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 chapter, let’s +stipulate that Roman numerals go from 1 to 3999.

      6. +
      7. There is no way to represent 0 in Roman numerals.

      8. +
      9. There is no way to represent negative numbers in Roman numerals.

      10. +
      11. There is no way to represent fractions or non-integer numbers in +Roman numerals.

      12. +
      +

      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 +imports them. Python has a framework for unit testing, the +appropriately-named unittest module.

      +

      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 in this chapter. +But unit tests are beneficial 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.)

      • +
      +

      +
      +
      +

      A Single Question

      +

      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…

      +
        +
      • …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:

      +
        +
      1. The to_roman() function should return the Roman numeral +representation for all integers 1 to 3999.

      2. +
      +

      It is not immediately obvious how this code does… well, anything. It +defines a class which has no __init__() method. The class does +have another method, but it is never called. The entire script has a +__main__ block, but it doesn’t reference the class or its method. +But it does do something, I promise.

      +

      [download ``romantest1.py` <examples/romantest1.py>`__]

      +
      import roman1
      +import unittest
      +
      +class KnownValues(unittest.TestCase):               ①
      +    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(self):           ③
      +        '''to_roman should give known result with known input'''
      +        for integer, numeral in self.known_values:
      +            result = roman1.to_roman(integer)       ④
      +            self.assertEqual(numeral, result)       ⑤
      +
      +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. +
      3. This is a tuple 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. +
      5. Every individual test is its own method. A test method takes no +parameters, returns no value, and must have a name beginning with the +four letters test. If a test method exits normally without +raising an exception, the test is considered passed; if the method +raises an exception, the test is considered failed.

      6. +
      7. Here 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. 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.

      8. +
      9. 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.

      10. +
      +

      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):
      +    '''convert integer to Roman numeral'''
      +    pass                                   ①
      +
      +
      +
        +
      1. At this stage, you want to define the API of the to_roman() +function, but you don’t want to code it yet. (Your test needs to fail +first.) To stub it out, use the Python reserved word pass, which +does precisely nothing.

      2. +
      +

      Execute romantest1.py on the command line to run the test. If you +call it with the -v command-line option, it will give more verbose +output so you can see exactly what’s going on as each test case runs. +With any luck, your output should look like this:

      +
      you@localhost:~/diveintopython3/examples$ python3 romantest1.py -v
      +test_to_roman_known_values (__main__.KnownValues)                      ①
      +to_roman should give known result with known input ... FAIL            ②
      +
      +======================================================================
      +FAIL: to_roman should give known result with known input
      +----------------------------------------------------------------------
      +Traceback (most recent call last):
      +  File "romantest1.py", line 73, in test_to_roman_known_values
      +    self.assertEqual(numeral, result)
      +AssertionError: 'I' != None                                            ③
      +
      +----------------------------------------------------------------------
      +Ran 1 test in 0.016s                                                   ④
      +
      +FAILED (failures=1)                                                    ⑤
      +
      +
      +
        +
      1. Running the script runs unittest.main(), which runs each test +case. Each test case is a method within a class in romantest.py. +There is no required organization of these test classes; they can +each contain a single test method, or you can have one class that +contains multiple test methods. The only requirement is that each +test class must inherit from unittest.TestCase.

      2. +
      3. 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.

      4. +
      5. 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.)

      6. +
      7. After the detail of each test, unittest displays a summary of how +many tests were performed and how long it took.

      8. +
      9. 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.

      10. +
      +

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

      +

      [download ``roman1.py` <examples/roman1.py>`__]

      +
      roman_numeral_map = (('M',  1000),
      +                     ('CM', 900),
      +                     ('D',  500),
      +                     ('CD', 400),
      +                     ('C',  100),
      +                     ('XC', 90),
      +                     ('L',  50),
      +                     ('XL', 40),
      +                     ('X',  10),
      +                     ('IX', 9),
      +                     ('V',  5),
      +                     ('IV', 4),
      +                     ('I',  1))                 ①
      +
      +def to_roman(n):
      +    '''convert integer to Roman numeral'''
      +    result = ''
      +    for numeral, integer in roman_numeral_map:
      +        while n >= integer:                     ②
      +            result += numeral
      +            n -= integer
      +    return result
      +
      +
      +
        +
      1. 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.

      2. +
      3. 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.

      4. +
      +

      If you’re still not clear how the to_roman() function works, add a +print() call to the end of the while loop:

      +
      while n >= integer:
      +    result += numeral
      +    n -= integer
      +    print('subtracting {0} from input, adding {1} to output'.format(integer, numeral))
      +
      +
      +

      With the debug print() statements, the output looks like this:

      +
      >>> import roman1
      +>>> roman1.to_roman(1424)
      +subtracting 1000 from input, adding M to output
      +subtracting 400 from input, adding CD to output
      +subtracting 10 from input, adding X to output
      +subtracting 10 from input, adding X to output
      +subtracting 4 from input, adding IV to output
      +'MCDXXIV'
      +
      +
      +

      So the to_roman() function appears to work, at least in this manual +spot check. But will it pass the test case you wrote?

      +
      you@localhost:~/diveintopython3/examples$ python3 romantest1.py -v
      +test_to_roman_known_values (__main__.KnownValues)
      +to_roman should give known result with known input ... ok               ①
      +
      +----------------------------------------------------------------------
      +Ran 1 test in 0.016s
      +
      +OK
      +
      +
      +
        +
      1. 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.

      2. +
      +

      “Good” input? Hmm. What about bad input?

      +

      +
      +
      +

      “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.

      +
      >>> import roman1
      +>>> roman1.to_roman(4000)
      +'MMMM'
      +>>> roman1.to_roman(5000)
      +'MMMMM'
      +>>> roman1.to_roman(9000)  ①
      +'MMMMMMMMM'
      +
      +
      +
        +
      1. That’s definitely not what you wanted — that’s not even a valid Roman +numeral! In fact, 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. The Pythonic way to halt and catch +fire is to raise an exception.

      2. +
      +

      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 OutOfRangeError when +given an integer greater than 3999.

      +
      +

      What would that test look like?

      +

      [download ``romantest2.py` <examples/romantest2.py>`__]

      +
      import unittest, roman2
      +class ToRomanBadInput(unittest.TestCase):                                 ①
      +    def test_too_large(self):                                             ②
      +        '''to_roman should fail with large input'''
      +        self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000)  ③
      +
      +
      +
        +
      1. Like the previous test case, you create a class that inherits from +unittest.TestCase. You can have more than one test per class (as +you’ll see later in this chapter), but I chose to create a new class +here because this test is something different than the last one. +We’ll keep all the good input tests together in one class, and all +the bad input tests together in another.

      2. +
      3. Like the previous test case, the test itself is a method of the +class, with a name starting with test.

      4. +
      5. The unittest.TestCase class provides the assertRaises method, +which takes the following arguments: the exception you’re expecting, +the function you’re testing, and the arguments you’re passing to that +function. (If the function you’re testing takes more than one +argument, pass them all to assertRaises, in order, and it will +pass them right along to the function you’re testing.)

      6. +
      +

      Pay close attention to this last line of code. Instead of calling +to_roman() directly and manually checking that it raises a +particular exception (by wrapping it in a ``try…except` +block <your-first-python-program.html#exceptions>`__), the +assertRaises method has encapsulated all of that for us. All you do +is tell it what exception you’re expecting (roman2.OutOfRangeError), +the function (to_roman()), and the function’s arguments (4000). +The assertRaises method takes care of calling to_roman() and +checking that it raises roman2.OutOfRangeError.

      +

      Also note that you’re passing the to_roman() function itself as an +argument; you’re not calling it, and you’re not passing the name of it +as a string. Have I mentioned recently how handy it is that everything +in Python is an +object?

      +

      So what happens when you run the test suite with this new test?

      +
      you@localhost:~/diveintopython3/examples$ python3 romantest2.py -v
      +test_to_roman_known_values (__main__.KnownValues)
      +to_roman should give known result with known input ... ok
      +test_too_large (__main__.ToRomanBadInput)
      +to_roman should fail with large input ... ERROR                         ①
      +
      +======================================================================
      +ERROR: to_roman should fail with large input
      +----------------------------------------------------------------------
      +Traceback (most recent call last):
      +  File "romantest2.py", line 78, in test_too_large
      +    self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000)
      +AttributeError: 'module' object has no attribute 'OutOfRangeError'      ②
      +
      +----------------------------------------------------------------------
      +Ran 2 tests in 0.000s
      +
      +FAILED (errors=1)
      +
      +
      +
        +
      1. You should have expected this to fail (since you haven’t written any +code to pass it yet), but… it didn’t actually “fail,” it had an +“error” instead. This is a subtle but important distinction. A unit +test actually has three return values: pass, fail, and error. Pass, +of course, means that the test passed — the code did what you +expected. “Fail” is what the previous test case did (until you wrote +code to make it pass) — it executed the code but the result was not +what you expected. “Error” means that the code didn’t even execute +properly.

      2. +
      3. Why didn’t the code execute properly? The traceback tells all. The +module you’re testing doesn’t have an exception called +OutOfRangeError. Remember, you passed this exception to the +assertRaises() method, because it’s the exception you want the +function to raise given an out-of-range input. But the exception +doesn’t exist, so the call to the assertRaises() method failed. +It never got a chance to test the to_roman() function; it didn’t +get that far.

      4. +
      +

      To solve this problem, you need to define the OutOfRangeError +exception in roman2.py.

      +
      class OutOfRangeError(ValueError):  ①
      +    pass                            ②
      +
      +
      +
        +
      1. Exceptions are classes. An “out of range” error is a kind of value +error — the argument value is out of its acceptable range. So this +exception inherits from the built-in ValueError exception. This +is not strictly necessary (it could just inherit from the base +Exception class), but it feels right.

      2. +
      3. Exceptions don’t actually do anything, but you need at least one line +of code to make a class. Calling pass does precisely nothing, but +it’s a line of Python code, so that makes it a class.

      4. +
      +

      Now run the test suite again.

      +
      you@localhost:~/diveintopython3/examples$ python3 romantest2.py -v
      +test_to_roman_known_values (__main__.KnownValues)
      +to_roman should give known result with known input ... ok
      +test_too_large (__main__.ToRomanBadInput)
      +to_roman should fail with large input ... FAIL                          ①
      +
      +======================================================================
      +FAIL: to_roman should fail with large input
      +----------------------------------------------------------------------
      +Traceback (most recent call last):
      +  File "romantest2.py", line 78, in test_too_large
      +    self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000)
      +AssertionError: OutOfRangeError not raised by to_roman                 ②
      +
      +----------------------------------------------------------------------
      +Ran 2 tests in 0.016s
      +
      +FAILED (failures=1)
      +
      +
      +
        +
      1. The new test is still not passing, but it’s not returning an error +either. Instead, the test is failing. That’s progress! It means the +call to the assertRaises() method succeeded this time, and the +unit test framework actually tested the to_roman() function.

      2. +
      3. Of course, the to_roman() function isn’t raising the +OutOfRangeError exception you just defined, because you haven’t +told it to do that yet. That’s excellent news! It means this is a +valid test case — it fails before you write the code to make it pass.

      4. +
      +

      Now you can write the code to make this test pass.

      +

      [download ``roman2.py` <examples/roman2.py>`__]

      +
      def to_roman(n):
      +    '''convert integer to Roman numeral'''
      +    if n > 3999:
      +        raise OutOfRangeError('number out of range (must be less than 4000)')  ①
      +
      +    result = ''
      +    for numeral, integer in roman_numeral_map:
      +        while n >= integer:
      +            result += numeral
      +            n -= integer
      +    return result
      +
      +
      +
        +
      1. This is straightforward: if the given input (n) is greater than +3999, raise an OutOfRangeError exception. The unit test does +not check the human-readable string that accompanies the exception, +although you could write another test that did check it (but watch +out for internationalization issues for strings that vary by the +user’s language or environment).

      2. +
      +

      Does this make the test pass? Let’s find out.

      +
      you@localhost:~/diveintopython3/examples$ python3 romantest2.py -v
      +test_to_roman_known_values (__main__.KnownValues)
      +to_roman should give known result with known input ... ok
      +test_too_large (__main__.ToRomanBadInput)
      +to_roman should fail with large input ... ok                            ①
      +
      +----------------------------------------------------------------------
      +Ran 2 tests in 0.000s
      +
      +OK
      +
      +
      +
        +
      1. Hooray! Both tests pass. Because you worked iteratively, bouncing +back and forth between testing and coding, you can be sure that the +two lines of code you just wrote were the cause of that one test +going from “fail” to “pass.” That kind of confidence doesn’t come +cheap, but it will pay for itself over the lifetime of your code.

      2. +
      +

      +
      +
      +

      More Halting, More Fire

      +

      Along with testing numbers that are too large, you need to test numbers +that are too small. As we noted in our functional +requirements, Roman numerals cannot express 0 or negative +numbers.

      +
      >>> import roman2
      +>>> roman2.to_roman(0)
      +''
      +>>> roman2.to_roman(-1)
      +''
      +
      +
      +

      Well that’s not good. Let’s add tests for each of these conditions.

      +

      [download ``romantest3.py` <examples/romantest3.py>`__]

      +
      class ToRomanBadInput(unittest.TestCase):
      +    def test_too_large(self):
      +        '''to_roman should fail with large input'''
      +        self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 4000)  ①
      +
      +    def test_zero(self):
      +        '''to_roman should fail with 0 input'''
      +        self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 0)     ②
      +
      +    def test_negative(self):
      +        '''to_roman should fail with negative input'''
      +        self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, -1)    ③
      +
      +
      +
        +
      1. The test_too_large() method has not changed since the previous +step. I’m including it here to show where the new code fits.

      2. +
      3. Here’s a new test: the test_zero() method. Like the +test_too_large() method, it tells the assertRaises() method +defined in unittest.TestCase to call our to_roman() function +with a parameter of 0, and check that it raises the appropriate +exception, OutOfRangeError.

      4. +
      5. The test_negative() method is almost identical, except it passes +-1 to the to_roman() function. If either of these new tests +does not raise an OutOfRangeError (either because the function +returns an actual value, or because it raises some other exception), +the test is considered failed.

      6. +
      +

      Now check that the tests fail:

      +
      you@localhost:~/diveintopython3/examples$ python3 romantest3.py -v
      +test_to_roman_known_values (__main__.KnownValues)
      +to_roman should give known result with known input ... ok
      +test_negative (__main__.ToRomanBadInput)
      +to_roman should fail with negative input ... FAIL
      +test_too_large (__main__.ToRomanBadInput)
      +to_roman should fail with large input ... ok
      +test_zero (__main__.ToRomanBadInput)
      +to_roman should fail with 0 input ... FAIL
      +
      +======================================================================
      +FAIL: to_roman should fail with negative input
      +----------------------------------------------------------------------
      +Traceback (most recent call last):
      +  File "romantest3.py", line 86, in test_negative
      +    self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, -1)
      +AssertionError: OutOfRangeError not raised by to_roman
      +
      +======================================================================
      +FAIL: to_roman should fail with 0 input
      +----------------------------------------------------------------------
      +Traceback (most recent call last):
      +  File "romantest3.py", line 82, in test_zero
      +    self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 0)
      +AssertionError: OutOfRangeError not raised by to_roman
      +
      +----------------------------------------------------------------------
      +Ran 4 tests in 0.000s
      +
      +FAILED (failures=2)
      +
      +
      +

      Excellent. Both tests failed, as expected. Now let’s switch over to the +code and see what we can do to make them pass.

      +

      [download ``roman3.py` <examples/roman3.py>`__]

      +
      def to_roman(n):
      +    '''convert integer to Roman numeral'''
      +    if not (0 < n < 4000):                                              ①
      +        raise OutOfRangeError('number out of range (must be 1..3999)')  ②
      +
      +    result = ''
      +    for numeral, integer in roman_numeral_map:
      +        while n >= integer:
      +            result += numeral
      +            n -= integer
      +    return result
      +
      +
      +
        +
      1. This is a nice Pythonic shortcut: multiple comparisons at once. This +is equivalent to if not ((0 < n) and (n < 4000)), but it’s much +easier to read. This one line of code should catch inputs that are +too large, negative, or zero.

      2. +
      3. If you change your conditions, make sure to update your +human-readable error strings to match. The unittest framework +won’t care, but it’ll make it difficult to do manual debugging if +your code is throwing incorrectly-described exceptions.

      4. +
      +

      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.

      +
      you@localhost:~/diveintopython3/examples$ python3 romantest3.py -v
      +test_to_roman_known_values (__main__.KnownValues)
      +to_roman should give known result with known input ... ok
      +test_negative (__main__.ToRomanBadInput)
      +to_roman should fail with negative input ... ok
      +test_too_large (__main__.ToRomanBadInput)
      +to_roman should fail with large input ... ok
      +test_zero (__main__.ToRomanBadInput)
      +to_roman should fail with 0 input ... ok
      +
      +----------------------------------------------------------------------
      +Ran 4 tests in 0.016s
      +
      +OK
      +
      +
      +

      +
      +
      +

      And One More Thing…

      +

      There was one more functional requirement for converting +numbers to Roman numerals: dealing with non-integers.

      +
      >>> import roman3
      +>>> roman3.to_roman(0.5)  ①
      +''
      +>>> roman3.to_roman(1.0)  ②
      +'I'
      +
      +
      +
        +
      1. Oh, that’s bad.

      2. +
      3. Oh, that’s even worse. Both of these cases should raise an exception. +Instead, they give bogus results.

      4. +
      +

      Testing for non-integers is not difficult. First, define a +NotIntegerError exception.

      +
      # roman4.py
      +class OutOfRangeError(ValueError): pass
      +class NotIntegerError(ValueError): pass
      +
      +
      +

      Next, write a test case that checks for the NotIntegerError +exception.

      +
      class ToRomanBadInput(unittest.TestCase):
      +    .
      +    .
      +    .
      +    def test_non_integer(self):
      +        '''to_roman should fail with non-integer input'''
      +        self.assertRaises(roman4.NotIntegerError, roman4.to_roman, 0.5)
      +
      +
      +

      Now check that the test fails properly.

      +
      you@localhost:~/diveintopython3/examples$ python3 romantest4.py -v
      +test_to_roman_known_values (__main__.KnownValues)
      +to_roman should give known result with known input ... ok
      +test_negative (__main__.ToRomanBadInput)
      +to_roman should fail with negative input ... ok
      +test_non_integer (__main__.ToRomanBadInput)
      +to_roman should fail with non-integer input ... FAIL
      +test_too_large (__main__.ToRomanBadInput)
      +to_roman should fail with large input ... ok
      +test_zero (__main__.ToRomanBadInput)
      +to_roman should fail with 0 input ... ok
      +
      +======================================================================
      +FAIL: to_roman should fail with non-integer input
      +----------------------------------------------------------------------
      +Traceback (most recent call last):
      +  File "romantest4.py", line 90, in test_non_integer
      +    self.assertRaises(roman4.NotIntegerError, roman4.to_roman, 0.5)
      +AssertionError: NotIntegerError not raised by to_roman
      +
      +----------------------------------------------------------------------
      +Ran 5 tests in 0.000s
      +
      +FAILED (failures=1)
      +
      +
      +

      Write the code that makes the test pass.

      +
      def to_roman(n):
      +    '''convert integer to Roman numeral'''
      +    if not (0 < n < 4000):
      +        raise OutOfRangeError('number out of range (must be 1..3999)')
      +    if not isinstance(n, int):                                          ①
      +        raise NotIntegerError('non-integers can not be converted')      ②
      +
      +    result = ''
      +    for numeral, integer in roman_numeral_map:
      +        while n >= integer:
      +            result += numeral
      +            n -= integer
      +    return result
      +
      +
      +
        +
      1. The built-in isinstance() function tests whether a variable is a +particular type (or, technically, any descendant type).

      2. +
      3. If the argument n is not an int, raise our newly minted +NotIntegerError exception.

      4. +
      +

      Finally, check that the code does indeed make the test pass.

      +
      you@localhost:~/diveintopython3/examples$ python3 romantest4.py -v
      +test_to_roman_known_values (__main__.KnownValues)
      +to_roman should give known result with known input ... ok
      +test_negative (__main__.ToRomanBadInput)
      +to_roman should fail with negative input ... ok
      +test_non_integer (__main__.ToRomanBadInput)
      +to_roman should fail with non-integer input ... ok
      +test_too_large (__main__.ToRomanBadInput)
      +to_roman should fail with large input ... ok
      +test_zero (__main__.ToRomanBadInput)
      +to_roman should fail with 0 input ... ok
      +
      +----------------------------------------------------------------------
      +Ran 5 tests in 0.000s
      +
      +OK
      +
      +
      +

      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 already constructed a regular expression to check for +Roman numerals, so that part +is done.

      +

      That leaves 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.

      +
      def test_from_roman_known_values(self):
      +    '''from_roman should give known result with known input'''
      +    for integer, numeral in self.known_values:
      +        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.

      +
      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.

      +
      class RoundtripCheck(unittest.TestCase):
      +    def test_roundtrip(self):
      +        '''from_roman(to_roman(n))==n for all n'''
      +        for integer in range(1, 4000):
      +            numeral = roman5.to_roman(integer)
      +            result = roman5.from_roman(numeral)
      +            self.assertEqual(integer, result)
      +
      +
      +

      These new tests won’t even fail yet. We haven’t defined a +from_roman() function at all, so they’ll just raise errors.

      +
      you@localhost:~/diveintopython3/examples$ python3 romantest5.py
      +E.E....
      +======================================================================
      +ERROR: test_from_roman_known_values (__main__.KnownValues)
      +from_roman should give known result with known input
      +----------------------------------------------------------------------
      +Traceback (most recent call last):
      +  File "romantest5.py", line 78, in test_from_roman_known_values
      +    result = roman5.from_roman(numeral)
      +AttributeError: 'module' object has no attribute 'from_roman'
      +
      +======================================================================
      +ERROR: test_roundtrip (__main__.RoundtripCheck)
      +from_roman(to_roman(n))==n for all n
      +----------------------------------------------------------------------
      +Traceback (most recent call last):
      +  File "romantest5.py", line 103, in test_roundtrip
      +    result = roman5.from_roman(numeral)
      +AttributeError: 'module' object has no attribute 'from_roman'
      +
      +----------------------------------------------------------------------
      +Ran 7 tests in 0.019s
      +
      +FAILED (errors=2)
      +
      +
      +

      A quick stub function will solve that problem.

      +
      # roman5.py
      +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.

      +
      you@localhost:~/diveintopython3/examples$ python3 romantest5.py
      +F.F....
      +======================================================================
      +FAIL: test_from_roman_known_values (__main__.KnownValues)
      +from_roman should give known result with known input
      +----------------------------------------------------------------------
      +Traceback (most recent call last):
      +  File "romantest5.py", line 79, in test_from_roman_known_values
      +    self.assertEqual(integer, result)
      +AssertionError: 1 != None
      +
      +======================================================================
      +FAIL: test_roundtrip (__main__.RoundtripCheck)
      +from_roman(to_roman(n))==n for all n
      +----------------------------------------------------------------------
      +Traceback (most recent call last):
      +  File "romantest5.py", line 104, in test_roundtrip
      +    self.assertEqual(integer, result)
      +AssertionError: 1 != None
      +
      +----------------------------------------------------------------------
      +Ran 7 tests in 0.002s
      +
      +FAILED (failures=2)
      +
      +
      +

      Now it’s time to write the from_roman() function.

      +
      def from_roman(s):
      +    """convert Roman numeral to integer"""
      +    result = 0
      +    index = 0
      +    for numeral, integer in roman_numeral_map:
      +        while s[index:index+len(numeral)] == numeral:  ①
      +            result += integer
      +            index += len(numeral)
      +    return result
      +
      +
      +
        +
      1. The pattern here is the same as the `to_roman() <#romantest1>`__ +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.

      2. +
      +

      If you’re not clear how from_roman() works, add a print +statement to the end of the while loop:

      +
      def from_roman(s):
      +    """convert Roman numeral to integer"""
      +    result = 0
      +    index = 0
      +    for numeral, integer in roman_numeral_map:
      +        while s[index:index+len(numeral)] == numeral:
      +            result += integer
      +            index += len(numeral)
      +            print('found', numeral, 'of length', len(numeral), ', adding', integer)
      +
      +
      +
      >>> import roman5
      +>>> roman5.from_roman('MCMLXXII')
      +found M of length 1, adding 1000
      +found CM of length 2, adding 900
      +found L of length 1, adding 50
      +found X of length 1, adding 10
      +found X of length 1, adding 10
      +found I of length 1, adding 1
      +found I of length 1, adding 1
      +1972
      +
      +
      +

      Time to re-run the tests.

      +
      you@localhost:~/diveintopython3/examples$ python3 romantest5.py
      +.......
      +----------------------------------------------------------------------
      +Ran 7 tests in 0.060s
      +
      +OK
      +
      +
      +

      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.)

      +

      +
      +
      +

      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 in the +to_roman() function, but you have a powerful tool at your disposal: +regular expressions. (If you’re not familiar with regular expressions, +now would be a good time to read the regular expressions +chapter.)

      +

      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:

      +
        +
      • 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”).

      • +
      +

      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.

      +
      class FromRomanBadInput(unittest.TestCase):
      +    def test_too_many_repeated_numerals(self):
      +        '''from_roman should fail with too many repeated numerals'''
      +        for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):
      +            self.assertRaises(roman6.InvalidRomanNumeralError, roman6.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.

      +
      def test_repeated_pairs(self):
      +    '''from_roman should fail with repeated pairs of numerals'''
      +    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. 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'''
      +    for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
      +              'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):
      +        self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
      +
      +
      +

      Each of these tests relies the from_roman() function raising a new +exception, InvalidRomanNumeralError, which we haven’t defined yet.

      +
      # roman6.py
      +class InvalidRomanNumeralError(ValueError): pass
      +
      +
      +

      All three 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?)

      +
      you@localhost:~/diveintopython3/examples$ python3 romantest6.py
      +FFF.......
      +======================================================================
      +FAIL: test_malformed_antecedents (__main__.FromRomanBadInput)
      +from_roman should fail with malformed antecedents
      +----------------------------------------------------------------------
      +Traceback (most recent call last):
      +  File "romantest6.py", line 113, in test_malformed_antecedents
      +    self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
      +AssertionError: InvalidRomanNumeralError not raised by from_roman
      +
      +======================================================================
      +FAIL: test_repeated_pairs (__main__.FromRomanBadInput)
      +from_roman should fail with repeated pairs of numerals
      +----------------------------------------------------------------------
      +Traceback (most recent call last):
      +  File "romantest6.py", line 107, in test_repeated_pairs
      +    self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
      +AssertionError: InvalidRomanNumeralError not raised by from_roman
      +
      +======================================================================
      +FAIL: test_too_many_repeated_numerals (__main__.FromRomanBadInput)
      +from_roman should fail with too many repeated numerals
      +----------------------------------------------------------------------
      +Traceback (most recent call last):
      +  File "romantest6.py", line 102, in test_too_many_repeated_numerals
      +    self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
      +AssertionError: InvalidRomanNumeralError not raised by from_roman
      +
      +----------------------------------------------------------------------
      +Ran 10 tests in 0.058s
      +
      +FAILED (failures=3)
      +
      +
      +

      Good deal. Now, all we need to do is add the regular expression to test +for valid Roman numerals +into the from_roman() function.

      +
      roman_numeral_pattern = re.compile('''
      +    ^                   # beginning of string
      +    M{0,3}              # thousands - 0 to 3 Ms
      +    (CM|CD|D?C{0,3})    # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 Cs),
      +                        #            or 500-800 (D, followed by 0 to 3 Cs)
      +    (XC|XL|L?X{0,3})    # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 Xs),
      +                        #        or 50-80 (L, followed by 0 to 3 Xs)
      +    (IX|IV|V?I{0,3})    # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 Is),
      +                        #        or 5-8 (V, followed by 0 to 3 Is)
      +    $                   # end of string
      +    ''', re.VERBOSE)
      +
      +def from_roman(s):
      +    '''convert Roman numeral to integer'''
      +    if not roman_numeral_pattern.search(s):
      +        raise InvalidRomanNumeralError('Invalid Roman numeral: {0}'.format(s))
      +
      +    result = 0
      +    index = 0
      +    for numeral, integer in roman_numeral_map:
      +        while s[index : index + len(numeral)] == numeral:
      +            result += integer
      +            index += len(numeral)
      +    return result
      +
      +
      +

      And re-run the tests…

      +
      you@localhost:~/diveintopython3/examples$ python3 romantest7.py
      +..........
      +----------------------------------------------------------------------
      +Ran 10 tests in 0.066s
      +
      +OK
      +
      +
      +

      And the anticlimax award of the year goes to… the word “OK”, +which is printed by the unittest module when all the tests pass.

      +

      +

      © 2001–11 Mark Pilgrim

      +
      +
      + + +
      + +
      +
      + + +
      + +
      +

      + © Copyright 2020, University of Washington, Natasha Aleksandrova, Christopher Barker, Brian Dorsey, Cris Ewing, Christy Heaton, Jon Jacky, Maria McKinley, Andy Miles, Rick Riehle, Joseph Schilz, Joseph Sheedy, Hosung Song. Creative Commons Attribution-ShareAlike 4.0 license + +

      +
      + Built with Sphinx using a theme provided by Read the Docs. + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/Debugging.html b/modules/Debugging.html new file mode 100644 index 0000000..f4f4983 --- /dev/null +++ b/modules/Debugging.html @@ -0,0 +1,595 @@ + + + + + + Debugging — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Decorators.html b/modules/Decorators.html new file mode 100644 index 0000000..3e43c01 --- /dev/null +++ b/modules/Decorators.html @@ -0,0 +1,581 @@ + + + + + + Decorators — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/DictionaryAsSwitch.html b/modules/DictionaryAsSwitch.html new file mode 100644 index 0000000..c7ee809 --- /dev/null +++ b/modules/DictionaryAsSwitch.html @@ -0,0 +1,237 @@ + + + + + + Using a Dictionary to switch — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/DictsAndSets.html b/modules/DictsAndSets.html new file mode 100644 index 0000000..6dab21d --- /dev/null +++ b/modules/DictsAndSets.html @@ -0,0 +1,695 @@ + + + + + + Dictionaries and Sets — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Documentation.html b/modules/Documentation.html new file mode 100644 index 0000000..73ac15a --- /dev/null +++ b/modules/Documentation.html @@ -0,0 +1,240 @@ + + + + + + Documentation — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/EnvironmentOverview.html b/modules/EnvironmentOverview.html new file mode 100644 index 0000000..68ca34c --- /dev/null +++ b/modules/EnvironmentOverview.html @@ -0,0 +1,412 @@ + + + + + + + + + + + Introduction to your Programming Environment — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/Exceptions.html b/modules/Exceptions.html new file mode 100644 index 0000000..7878115 --- /dev/null +++ b/modules/Exceptions.html @@ -0,0 +1,439 @@ + + + + + + Exception Handling — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Files.html b/modules/Files.html new file mode 100644 index 0000000..bf30f2f --- /dev/null +++ b/modules/Files.html @@ -0,0 +1,436 @@ + + + + + + File Reading and Writing — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Functions.html b/modules/Functions.html new file mode 100644 index 0000000..bed1ebc --- /dev/null +++ b/modules/Functions.html @@ -0,0 +1,320 @@ + + + + + + More on Functions — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Git.html b/modules/Git.html new file mode 100644 index 0000000..32c9d14 --- /dev/null +++ b/modules/Git.html @@ -0,0 +1,416 @@ + + + + + + + + + + + Intro to Git — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/GitWorkflow.html b/modules/GitWorkflow.html new file mode 100644 index 0000000..c8a3b6c --- /dev/null +++ b/modules/GitWorkflow.html @@ -0,0 +1,316 @@ + + + + + + + + + + + git Workflow — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/GraphDatabases.html b/modules/GraphDatabases.html new file mode 100644 index 0000000..5ee2909 --- /dev/null +++ b/modules/GraphDatabases.html @@ -0,0 +1,276 @@ + + + + + + Graph Databases — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/HowToRunAPythonFile.html b/modules/HowToRunAPythonFile.html new file mode 100644 index 0000000..0130572 --- /dev/null +++ b/modules/HowToRunAPythonFile.html @@ -0,0 +1,168 @@ + + + + + + How to run a python file — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/IPythonIntroduction.html b/modules/IPythonIntroduction.html new file mode 100644 index 0000000..7ed3bb6 --- /dev/null +++ b/modules/IPythonIntroduction.html @@ -0,0 +1,260 @@ + + + + + + + + + + + IPython Introduction — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/IPythonParallel.html b/modules/IPythonParallel.html new file mode 100644 index 0000000..d3d0a4d --- /dev/null +++ b/modules/IPythonParallel.html @@ -0,0 +1,155 @@ + + + + + + IPython Parallel Quickstart — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/IndividualProject.html b/modules/IndividualProject.html new file mode 100644 index 0000000..5f39155 --- /dev/null +++ b/modules/IndividualProject.html @@ -0,0 +1,140 @@ + + + + + + Individual Project — Programming in Python 7.0 documentation + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Iteration.html b/modules/Iteration.html new file mode 100644 index 0000000..851a418 --- /dev/null +++ b/modules/Iteration.html @@ -0,0 +1,392 @@ + + + + + + Iteration — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/IteratorsAndGenerators.html b/modules/IteratorsAndGenerators.html new file mode 100644 index 0000000..481625f --- /dev/null +++ b/modules/IteratorsAndGenerators.html @@ -0,0 +1,602 @@ + + + + + + Iterators and Generators — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Lambda.html b/modules/Lambda.html new file mode 100644 index 0000000..1cadf60 --- /dev/null +++ b/modules/Lambda.html @@ -0,0 +1,271 @@ + + + + + + Anonymous Functions: Lambda — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Learning.html b/modules/Learning.html new file mode 100644 index 0000000..7c3c003 --- /dev/null +++ b/modules/Learning.html @@ -0,0 +1,247 @@ + + + + + + Useful Python Learning Resources — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Logging.html b/modules/Logging.html new file mode 100644 index 0000000..2ad3606 --- /dev/null +++ b/modules/Logging.html @@ -0,0 +1,371 @@ + + + + + + Logging and the logging module — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/MapFilterReduce.html b/modules/MapFilterReduce.html new file mode 100644 index 0000000..d1741a6 --- /dev/null +++ b/modules/MapFilterReduce.html @@ -0,0 +1,233 @@ + + + + + + Map Filter and Reduce — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/MetaProgramming.html b/modules/MetaProgramming.html new file mode 100644 index 0000000..88c875d --- /dev/null +++ b/modules/MetaProgramming.html @@ -0,0 +1,782 @@ + + + + + + Metaprogramming — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Modules.html b/modules/Modules.html new file mode 100644 index 0000000..1d46375 --- /dev/null +++ b/modules/Modules.html @@ -0,0 +1,483 @@ + + + + + + Code Structure, Modules, and Namespaces — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/MoreOnMutability.html b/modules/MoreOnMutability.html new file mode 100644 index 0000000..d02d867 --- /dev/null +++ b/modules/MoreOnMutability.html @@ -0,0 +1,252 @@ + + + + + + A bit more on mutability (and copies) — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/MultipleInheritance.html b/modules/MultipleInheritance.html new file mode 100644 index 0000000..4b5959f --- /dev/null +++ b/modules/MultipleInheritance.html @@ -0,0 +1,430 @@ + + + + + + Multiple Inheritance — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/NamingThings.html b/modules/NamingThings.html new file mode 100644 index 0000000..2a07dde --- /dev/null +++ b/modules/NamingThings.html @@ -0,0 +1,226 @@ + + + + + + Style and Naming — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/NoSQL.html b/modules/NoSQL.html new file mode 100644 index 0000000..67d7714 --- /dev/null +++ b/modules/NoSQL.html @@ -0,0 +1,505 @@ + + + + + + No SQL Databases — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/OO_vs_functional.html b/modules/OO_vs_functional.html new file mode 100644 index 0000000..ce5b792 --- /dev/null +++ b/modules/OO_vs_functional.html @@ -0,0 +1,185 @@ + + + + + + Object Oriented vs Functional Programming — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/ObjectOrientationOverview.html b/modules/ObjectOrientationOverview.html new file mode 100644 index 0000000..4477a21 --- /dev/null +++ b/modules/ObjectOrientationOverview.html @@ -0,0 +1,268 @@ + + + + + + Object Orientation Overview — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Packaging.html b/modules/Packaging.html new file mode 100644 index 0000000..5495e2a --- /dev/null +++ b/modules/Packaging.html @@ -0,0 +1,1104 @@ + + + + + + Packages and Packaging — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Pep8.html b/modules/Pep8.html new file mode 100644 index 0000000..64f8836 --- /dev/null +++ b/modules/Pep8.html @@ -0,0 +1,428 @@ + + + + + + Coding Style and Linting — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/PersistanceAndSerialization.html b/modules/PersistanceAndSerialization.html new file mode 100644 index 0000000..b89dc3d --- /dev/null +++ b/modules/PersistanceAndSerialization.html @@ -0,0 +1,704 @@ + + + + + + Persistence and Serialization — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Profiling.html b/modules/Profiling.html new file mode 100644 index 0000000..33bed25 --- /dev/null +++ b/modules/Profiling.html @@ -0,0 +1,654 @@ + + + + + + Performance and Profiling — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Properties.html b/modules/Properties.html new file mode 100644 index 0000000..4241775 --- /dev/null +++ b/modules/Properties.html @@ -0,0 +1,254 @@ + + + + + + Properties — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Py2vsPy3.html b/modules/Py2vsPy3.html new file mode 100644 index 0000000..a11a533 --- /dev/null +++ b/modules/Py2vsPy3.html @@ -0,0 +1,207 @@ + + + + + + Python 2 versus Python 3 — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/PythonClasses.html b/modules/PythonClasses.html new file mode 100644 index 0000000..c4270d2 --- /dev/null +++ b/modules/PythonClasses.html @@ -0,0 +1,438 @@ + + + + + + Python Classes — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Recursion.html b/modules/Recursion.html new file mode 100644 index 0000000..2ba9932 --- /dev/null +++ b/modules/Recursion.html @@ -0,0 +1,258 @@ + + + + + + Recursion — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Sequences.html b/modules/Sequences.html new file mode 100644 index 0000000..c2db6fc --- /dev/null +++ b/modules/Sequences.html @@ -0,0 +1,1100 @@ + + + + + + Python Sequences — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/SpecialMethodsAndProtocols.html b/modules/SpecialMethodsAndProtocols.html new file mode 100644 index 0000000..8b006fb --- /dev/null +++ b/modules/SpecialMethodsAndProtocols.html @@ -0,0 +1,442 @@ + + + + + + Special Methods & Protocols — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/StaticAndClassMethods.html b/modules/StaticAndClassMethods.html new file mode 100644 index 0000000..1219184 --- /dev/null +++ b/modules/StaticAndClassMethods.html @@ -0,0 +1,243 @@ + + + + + + Static and Class Methods — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Strings.html b/modules/Strings.html new file mode 100644 index 0000000..692de98 --- /dev/null +++ b/modules/Strings.html @@ -0,0 +1,583 @@ + + + + + + Strings — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/SubclassingAndInheritance.html b/modules/SubclassingAndInheritance.html new file mode 100644 index 0000000..8e39575 --- /dev/null +++ b/modules/SubclassingAndInheritance.html @@ -0,0 +1,378 @@ + + + + + + Subclassing and Inheritance — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/SubmittingCode.html b/modules/SubmittingCode.html new file mode 100644 index 0000000..9d60495 --- /dev/null +++ b/modules/SubmittingCode.html @@ -0,0 +1,293 @@ + + + + + + + + + + + Working with GitHub Classroom — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + +
      + + + + + + + + + + + + + + + + + +
      + + + + +
      +
      +
      +
      + +
      +

      Working with GitHub Classroom

      +
      +

      Why GitHub Classroom?

      +

      A software development project is all about continuous improvement:

      +
        +
      1. An opportunity is identified.

      2. +
      3. Some initial code is written to address that opportunity.

      4. +
      5. Feedback is provided for that code.

      6. +
      7. The code is modified to create that feedback.

      8. +
      9. A final version of the code is released.

      10. +
      +

      Steps 3-4 will be repeated multiple times until the development team (which could even be a single developer) deems it is ready for release.

      +

      In this course, you will not only learn about Python but also about the development process that most Python projects go through. GitHub Classroom allows for the steps indicated above to be completed in an academic environment.

      +
      +
      +

      Initial setup

      +

      You will need an account on GitHub to participate in this course. +If you don’t have already have a GitHub account or if you would prefer to use create a new one for this course, make sure you setup a new account on GitHub. Always keep in mind that your account name will be part of the private repositories that will be created for each of your assignments and it will be visible to both your instructors and your classmates.

      +

      You will need to have git setup on the computer you will use for developing your code for this course. +You can find instructions for setting up git (and the rest of your development environment) here: +:ref:`installing_python`_

      +

      Once you have all the tools set up, you will need to create a folder (directory) within your development system for keeping your work. +This is the folder where all your assignment repositories will reside. +Open Git Bash (if using Git for Windows) or a terminal (Linux and Mac OS), go to the folder (using the cd command) you have created for this class and run:

      +
      git init
      +
      +
      +

      This will setup your folder to house git repositories.

      +
      +
      +

      Accepting an assignment

      +

      Each assignment page will contain a section named “Accepting your Assignment”. Click on the corresponding link, which will take you to GitHub Classroom to accept the corresponding assignment.

      +
      +

      Some things to consider:

      +
        +
      • You will need to accept each assignment separately.

      • +
      • Accepting an assignment will trigger the creation of a private repository for you to work on your assignment.

      • +
      • This repository is only assigned to you.

      • +
      • Any work you do there will not affect the work of your classmates.

      • +
      • The name of the new repository will include your GitHub user name at the end.

      • +
      +

      Once your repository has been created, go to its link and clone it on your development system, under the folder you selected for this purpose (here (Links to an external site.) is GitHub’s official guide on how to clone a repository).

      +
      +
      +
      +

      Before you start working on your assignment

      +

      Once your repository is setup, it’s good to get familiar with your repository view. +You should see a tab there called “Pull Requests”: they indicate code changes that are desired to be pulled into a main repository; +by default you should see one already there called “Feedback”. Go ahead and click on it, and take a look at Files Changed tab - as of now it should show “No changes to show”. As you start committing your code you will see your changes there.

      +
      +
      +

      Committing your code

      +

      A “commit” is snapshot of your code (and any other files included in your project). +You are encouraged to make frequent commits, as this will make it easier for you to restore your code to an earlier state if things go wrong.

      +
      +

      To create a new commit:

      +

      Type the following to add all files and subdirectories in the folder to your commit (note the command includes a dot, make sure you include it as well): +git add.

      +

      Commit your code by typing the following: +git commit -m "Commit message"

      +

      Note that the commit message should be replaced with something descriptive of what that commit includes (“added new functionality”, “fixed floating point error”, “ready for review”, etc.) that will later help you remember what that particular commit was about.

      +
      +
      +
      +

      Pushing your code

      +

      “Pushing” refers to the process of synchronizing the commits you have made on your development system with your GitHub repository. +This is an important process, since it is needed before you can submit your code for review. +Also, it makes a copy of your code in your GitHub account that you can later use to restore it if your local development system fails.

      +

      You can push your code immediately after every commit or do it once a day (in which case, several commits will be included in a single push). To do it, simply type:

      +
      git push
      +
      +
      +

      The first time you push your code to a repository, GitHub will ask you to select the remote repository (i.e., your GitHub repository). Just copy the suggested push command (you will only need to do this once per assignment).

      +
      +
      +

      Asking Coding Questions

      +

      While working on your code, you might run into a situation in which you would like one of the instructors to look at it and provide some feedback before actually reviewing and grading it. +In order to do that, go to “Feedback” pull request and write a comment about your question or issue. You should make sure to tag your instructor in your comment, to assure that they ar noticfied of your comment. This is done by writing @the_instrudtors_github_handle, e.g. @natasha-aleksandrova.

      +

      For example:

      +
      @natasha-aleksandrova: I need some help on line 20
      +
      +
      +

      When you submit a comment with a tag, the instructor will be notified by GitHub and will be able to review your question.

      +
      +
      +

      Submitting your assignment

      +

      Once your assignment is ready for review, copy the link of your Feedback pull request and submit it in the submission form. Here is an example of a submission link (yours will look a little different but will end with /pull/1):

      +
      https://github.com/UWPCE-Py210-SelfPaced-2021/lesson-02-fizzbuzz-exercise-uw-test-student-natasha/pull/1
      +
      +
      +

      As per UW’s requirements, you also need to submit a zip file with your code on EdX or Canvas. Note that only the code included in your pull request will be reviewed.

      +
      +
      +

      9. Resubmitting your assignment

      +

      On occasion, your instructor will provide feedback on elements in your assignment that need to be modified in order to get the full grade for the assignment. In those cases, follow the process outlined in the Asking Coding Questions section above. Let us know that you would like another review for grade adjustment and make sure to tag your instructor.

      +

      Happy coding!

      +
      +
      + + +
      + +
      +
      + + +
      + +
      +

      + © Copyright 2020, University of Washington, Natasha Aleksandrova, Christopher Barker, Brian Dorsey, Cris Ewing, Christy Heaton, Jon Jacky, Maria McKinley, Andy Miles, Rick Riehle, Joseph Schilz, Joseph Sheedy, Hosung Song. Creative Commons Attribution-ShareAlike 4.0 license + +

      +
      + Built with Sphinx using a theme provided by Read the Docs. + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/SubmittingCodeGithubClassroom.html b/modules/SubmittingCodeGithubClassroom.html new file mode 100644 index 0000000..f993002 --- /dev/null +++ b/modules/SubmittingCodeGithubClassroom.html @@ -0,0 +1,292 @@ + + + + + + + + + + + Working with GitHub Classroom — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/Submitting_to_github.html b/modules/Submitting_to_github.html new file mode 100644 index 0000000..26013e6 --- /dev/null +++ b/modules/Submitting_to_github.html @@ -0,0 +1,182 @@ + + + + + + Submitting your work to gitHub (old) — Programming in Python 7.0 documentation + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/TestDrivenDevelopment.html b/modules/TestDrivenDevelopment.html new file mode 100644 index 0000000..43009b8 --- /dev/null +++ b/modules/TestDrivenDevelopment.html @@ -0,0 +1,1725 @@ + + + + + + Test Driven Development — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Testing.html b/modules/Testing.html new file mode 100644 index 0000000..f2fb347 --- /dev/null +++ b/modules/Testing.html @@ -0,0 +1,508 @@ + + + + + + Testing — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Testing_advanced.html b/modules/Testing_advanced.html new file mode 100644 index 0000000..69a0ce4 --- /dev/null +++ b/modules/Testing_advanced.html @@ -0,0 +1,872 @@ + + + + + + Advanced Testing — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/ThreadingMultiprocessing.html b/modules/ThreadingMultiprocessing.html new file mode 100644 index 0000000..5b6a8be --- /dev/null +++ b/modules/ThreadingMultiprocessing.html @@ -0,0 +1,817 @@ + + + + + + Threading and multiprocessing — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Tutorial.html b/modules/Tutorial.html new file mode 100644 index 0000000..1073e1c --- /dev/null +++ b/modules/Tutorial.html @@ -0,0 +1,357 @@ + + + + + + Python Tutorial — Programming in Python 7.0 documentation + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/Unicode.html b/modules/Unicode.html new file mode 100644 index 0000000..a12a2bf --- /dev/null +++ b/modules/Unicode.html @@ -0,0 +1,595 @@ + + + + + + Unicode in Python — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/modules/dev_environment/atom_as_ide.html b/modules/dev_environment/atom_as_ide.html new file mode 100644 index 0000000..f5782aa --- /dev/null +++ b/modules/dev_environment/atom_as_ide.html @@ -0,0 +1,289 @@ + + + + + + + + + + + Turning Atom Into a Lightweight Python IDE — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/dev_environment/command_line.html b/modules/dev_environment/command_line.html new file mode 100644 index 0000000..f909635 --- /dev/null +++ b/modules/dev_environment/command_line.html @@ -0,0 +1,221 @@ + + + + + + + + + + + Command line basics — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/dev_environment/feature_branching.html b/modules/dev_environment/feature_branching.html new file mode 100644 index 0000000..fe174a6 --- /dev/null +++ b/modules/dev_environment/feature_branching.html @@ -0,0 +1,359 @@ + + + + + + + + + + + Feature Branching — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/dev_environment/git_editor_windows.html b/modules/dev_environment/git_editor_windows.html new file mode 100644 index 0000000..3b6c661 --- /dev/null +++ b/modules/dev_environment/git_editor_windows.html @@ -0,0 +1,224 @@ + + + + + + + + + + + Making your text Editor work with git on Windows — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/dev_environment/git_hints.html b/modules/dev_environment/git_hints.html new file mode 100644 index 0000000..2b70d05 --- /dev/null +++ b/modules/dev_environment/git_hints.html @@ -0,0 +1,411 @@ + + + + + + + + + + + git Hints — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/dev_environment/git_overview.html b/modules/dev_environment/git_overview.html new file mode 100644 index 0000000..365bae5 --- /dev/null +++ b/modules/dev_environment/git_overview.html @@ -0,0 +1,317 @@ + + + + + + + + + + + git Overview — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/dev_environment/index.html b/modules/dev_environment/index.html new file mode 100644 index 0000000..4d13ab2 --- /dev/null +++ b/modules/dev_environment/index.html @@ -0,0 +1,573 @@ + + + + + + + + + + + Installing Python and core tools — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/dev_environment/ipython.html b/modules/dev_environment/ipython.html new file mode 100644 index 0000000..4bdb6c4 --- /dev/null +++ b/modules/dev_environment/ipython.html @@ -0,0 +1,215 @@ + + + + + + + + + + + iPython Interpreter — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/dev_environment/python_for_linux.html b/modules/dev_environment/python_for_linux.html new file mode 100644 index 0000000..696f31a --- /dev/null +++ b/modules/dev_environment/python_for_linux.html @@ -0,0 +1,424 @@ + + + + + + + + + + + Setting Up Linux for Python — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/dev_environment/python_for_mac.html b/modules/dev_environment/python_for_mac.html new file mode 100644 index 0000000..ed0298e --- /dev/null +++ b/modules/dev_environment/python_for_mac.html @@ -0,0 +1,364 @@ + + + + + + + + + + + Setting up OS-X for Python — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/dev_environment/python_for_windows.html b/modules/dev_environment/python_for_windows.html new file mode 100644 index 0000000..931c885 --- /dev/null +++ b/modules/dev_environment/python_for_windows.html @@ -0,0 +1,359 @@ + + + + + + + + + + + Setting up Windows for Python — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/dev_environment/shell.html b/modules/dev_environment/shell.html new file mode 100644 index 0000000..19f4966 --- /dev/null +++ b/modules/dev_environment/shell.html @@ -0,0 +1,361 @@ + + + + + + + + + + + Shell Customizations for Python Development — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/dev_environment/sublime_as_ide.html b/modules/dev_environment/sublime_as_ide.html new file mode 100644 index 0000000..5b6ee17 --- /dev/null +++ b/modules/dev_environment/sublime_as_ide.html @@ -0,0 +1,328 @@ + + + + + + + + + + + Turning Sublime Text Into a Lightweight Python IDE — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/dev_environment/vagrant.html b/modules/dev_environment/vagrant.html new file mode 100644 index 0000000..b33ee86 --- /dev/null +++ b/modules/dev_environment/vagrant.html @@ -0,0 +1,203 @@ + + + + + + + + + + + Setting up Python via a Linux VM — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/dev_environment/virtualenv.html b/modules/dev_environment/virtualenv.html new file mode 100644 index 0000000..5e370c4 --- /dev/null +++ b/modules/dev_environment/virtualenv.html @@ -0,0 +1,533 @@ + + + + + + + + + + + Working with Virtualenv — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/dev_environment/vsc_as_ide.html b/modules/dev_environment/vsc_as_ide.html new file mode 100644 index 0000000..f7dbcee --- /dev/null +++ b/modules/dev_environment/vsc_as_ide.html @@ -0,0 +1,237 @@ + + + + + + + + + + + Using Visual Studio Code as a lightweight Python IDE — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/dev_environment/windows_bash.html b/modules/dev_environment/windows_bash.html new file mode 100644 index 0000000..e8987a9 --- /dev/null +++ b/modules/dev_environment/windows_bash.html @@ -0,0 +1,217 @@ + + + + + + + + + + + Using Windows Bash for Python Development — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/index.html b/modules/index.html new file mode 100644 index 0000000..97c8f1c --- /dev/null +++ b/modules/index.html @@ -0,0 +1,703 @@ + + + + + + + + + + + Introduction to Python Topic Notes — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/objects.inv b/objects.inv new file mode 100644 index 0000000..1787215 Binary files /dev/null and b/objects.inv differ diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index cbf1e36..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -sphinx -sphinx-rtd-theme diff --git a/search.html b/search.html new file mode 100644 index 0000000..d1a3694 --- /dev/null +++ b/search.html @@ -0,0 +1,134 @@ + + + + + + Search — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + + + + + + \ No newline at end of file diff --git a/searchindex.js b/searchindex.js new file mode 100644 index 0000000..e1ba7ec --- /dev/null +++ b/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({docnames:["class_schedule/index","class_schedule/lesson01","class_schedule/lesson02","class_schedule/lesson03","class_schedule/lesson04","class_schedule/lesson05","class_schedule/lesson06","class_schedule/lesson07","class_schedule/lesson08","class_schedule/lesson09","class_schedule/lesson10","class_schedule/orientation","exercises/args_kwargs_lab","exercises/circle/circle_class","exercises/codingbat","exercises/comprehensions_lab","exercises/context-managers-exercise","exercises/dict_lab","exercises/exceptions/except_exercise","exercises/exceptions/exceptions_lab","exercises/file_processing/file_lab","exercises/file_processing/file_processing","exercises/fizz_buzz","exercises/grid_printer","exercises/html_renderer/html_renderer","exercises/html_renderer/html_renderer_tutorial","exercises/lambda_magic","exercises/list_lab","exercises/mailroom/mailroom","exercises/mailroom/mailroom-decorator","exercises/mailroom/mailroom-fp","exercises/mailroom/mailroom-meta","exercises/mailroom/mailroom-mock","exercises/mailroom/mailroom-oo","exercises/mailroom/mailroom-pkg","exercises/mailroom/mailroom_tutorial","exercises/mailroom/mailroom_with_comprehensions","exercises/mailroom/mailroom_with_dicts","exercises/mailroom/mailroom_with_exceptions","exercises/mailroom/mailroom_with_files","exercises/mailroom/mailroom_with_tests","exercises/oo_intro/oo_intro","exercises/packaging/package_lab","exercises/python_pushups","exercises/rot13","exercises/series/fib_and_lucas","exercises/slicing","exercises/sparse_array","exercises/string_formatting","exercises/threaded_downloader","exercises/trapezoid","exercises/trigrams/trigrams","exercises/unit_testing/unit_testing","index","modules/AdvancedArgumentPassing","modules/Async","modules/BasicPython","modules/Booleans","modules/CallableClasses","modules/Closures","modules/CodeReviews","modules/CollectionsModule","modules/Comprehensions","modules/Concurrency","modules/ContextManagers","modules/Coroutines","modules/Debugging","modules/Decorators","modules/DictionaryAsSwitch","modules/DictsAndSets","modules/Documentation","modules/Exceptions","modules/Files","modules/Functions","modules/GraphDatabases","modules/HowToRunAPythonFile","modules/IPythonParallel","modules/IndividualProject","modules/Iteration","modules/IteratorsAndGenerators","modules/Lambda","modules/Learning","modules/Logging","modules/MapFilterReduce","modules/MetaProgramming","modules/Modules","modules/MoreOnMutability","modules/MultipleInheritance","modules/NamingThings","modules/NoSQL","modules/OO_vs_functional","modules/ObjectOrientationOverview","modules/Packaging","modules/Pep8","modules/PersistanceAndSerialization","modules/Profiling","modules/Properties","modules/Py2vsPy3","modules/PythonClasses","modules/Recursion","modules/Sequences","modules/SpecialMethodsAndProtocols","modules/StaticAndClassMethods","modules/Strings","modules/SubclassingAndInheritance","modules/Submitting_to_github","modules/TestDrivenDevelopment","modules/Testing","modules/Testing_advanced","modules/ThreadingMultiprocessing","modules/Tutorial","modules/Unicode","solutions/mailroom/mailroom_json_save/README","solutions/mailroom/mailroom_meta/README","topics/01-setting_up/advanced_setup","topics/01-setting_up/atom_as_ide","topics/01-setting_up/command_line","topics/01-setting_up/environment_overview","topics/01-setting_up/feature_branching","topics/01-setting_up/git","topics/01-setting_up/git_editor_windows","topics/01-setting_up/git_hints","topics/01-setting_up/git_overview","topics/01-setting_up/git_workflow","topics/01-setting_up/github_classroom","topics/01-setting_up/index","topics/01-setting_up/intro_to_git","topics/01-setting_up/ipython","topics/01-setting_up/python_and_core_tools","topics/01-setting_up/python_for_linux","topics/01-setting_up/python_for_mac","topics/01-setting_up/python_for_windows","topics/01-setting_up/resources","topics/01-setting_up/setup_details","topics/01-setting_up/shell","topics/01-setting_up/sublime_as_ide","topics/01-setting_up/submitting_to_github","topics/01-setting_up/testing_your_setup","topics/01-setting_up/vagrant","topics/01-setting_up/virtualenv","topics/01-setting_up/vsc_as_ide","topics/01-setting_up/windows_bash","topics/02-basic_python/index","topics/03-recursion_booleans/index","topics/04-sequences_iteration/index","topics/05-text_handling/index","topics/06-exceptions/index","topics/07-unit_testing/index","topics/08-dicts_sets/index","topics/09-files/index","topics/10-modules_packages/index","topics/11-argument_passing/index","topics/12-comprehensions/index","topics/13-intro_oo/index","topics/14-magic_methods/index","topics/15-subclassing/index","topics/16-multiple_inheritance/index","topics/17-functional_programming/index","topics/18-advanced_testing/index","topics/99-extras/index"],envversion:{"sphinx.domains.c":2,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":4,"sphinx.domains.index":1,"sphinx.domains.javascript":2,"sphinx.domains.math":2,"sphinx.domains.python":3,"sphinx.domains.rst":2,"sphinx.domains.std":2,sphinx:56},filenames:["class_schedule/index.rst","class_schedule/lesson01.rst","class_schedule/lesson02.rst","class_schedule/lesson03.rst","class_schedule/lesson04.rst","class_schedule/lesson05.rst","class_schedule/lesson06.rst","class_schedule/lesson07.rst","class_schedule/lesson08.rst","class_schedule/lesson09.rst","class_schedule/lesson10.rst","class_schedule/orientation.rst","exercises/args_kwargs_lab.rst","exercises/circle/circle_class.rst","exercises/codingbat.rst","exercises/comprehensions_lab.rst","exercises/context-managers-exercise.rst","exercises/dict_lab.rst","exercises/exceptions/except_exercise.rst","exercises/exceptions/exceptions_lab.rst","exercises/file_processing/file_lab.rst","exercises/file_processing/file_processing.rst","exercises/fizz_buzz.rst","exercises/grid_printer.rst","exercises/html_renderer/html_renderer.rst","exercises/html_renderer/html_renderer_tutorial.rst","exercises/lambda_magic.rst","exercises/list_lab.rst","exercises/mailroom/mailroom.rst","exercises/mailroom/mailroom-decorator.rst","exercises/mailroom/mailroom-fp.rst","exercises/mailroom/mailroom-meta.rst","exercises/mailroom/mailroom-mock.rst","exercises/mailroom/mailroom-oo.rst","exercises/mailroom/mailroom-pkg.rst","exercises/mailroom/mailroom_tutorial.rst","exercises/mailroom/mailroom_with_comprehensions.rst","exercises/mailroom/mailroom_with_dicts.rst","exercises/mailroom/mailroom_with_exceptions.rst","exercises/mailroom/mailroom_with_files.rst","exercises/mailroom/mailroom_with_tests.rst","exercises/oo_intro/oo_intro.rst","exercises/packaging/package_lab.rst","exercises/python_pushups.rst","exercises/rot13.rst","exercises/series/fib_and_lucas.rst","exercises/slicing.rst","exercises/sparse_array.rst","exercises/string_formatting.rst","exercises/threaded_downloader.rst","exercises/trapezoid.rst","exercises/trigrams/trigrams.rst","exercises/unit_testing/unit_testing.rst","index.rst","modules/AdvancedArgumentPassing.rst","modules/Async.rst","modules/BasicPython.rst","modules/Booleans.rst","modules/CallableClasses.rst","modules/Closures.rst","modules/CodeReviews.rst","modules/CollectionsModule.rst","modules/Comprehensions.rst","modules/Concurrency.rst","modules/ContextManagers.rst","modules/Coroutines.rst","modules/Debugging.rst","modules/Decorators.rst","modules/DictionaryAsSwitch.rst","modules/DictsAndSets.rst","modules/Documentation.rst","modules/Exceptions.rst","modules/Files.rst","modules/Functions.rst","modules/GraphDatabases.rst","modules/HowToRunAPythonFile.rst","modules/IPythonParallel.rst","modules/IndividualProject.rst","modules/Iteration.rst","modules/IteratorsAndGenerators.rst","modules/Lambda.rst","modules/Learning.rst","modules/Logging.rst","modules/MapFilterReduce.rst","modules/MetaProgramming.rst","modules/Modules.rst","modules/MoreOnMutability.rst","modules/MultipleInheritance.rst","modules/NamingThings.rst","modules/NoSQL.rst","modules/OO_vs_functional.rst","modules/ObjectOrientationOverview.rst","modules/Packaging.rst","modules/Pep8.rst","modules/PersistanceAndSerialization.rst","modules/Profiling.rst","modules/Properties.rst","modules/Py2vsPy3.rst","modules/PythonClasses.rst","modules/Recursion.rst","modules/Sequences.rst","modules/SpecialMethodsAndProtocols.rst","modules/StaticAndClassMethods.rst","modules/Strings.rst","modules/SubclassingAndInheritance.rst","modules/Submitting_to_github.rst","modules/TestDrivenDevelopment.rst","modules/Testing.rst","modules/Testing_advanced.rst","modules/ThreadingMultiprocessing.rst","modules/Tutorial.rst","modules/Unicode.rst","solutions/mailroom/mailroom_json_save/README.rst","solutions/mailroom/mailroom_meta/README.rst","topics/01-setting_up/advanced_setup.rst","topics/01-setting_up/atom_as_ide.rst","topics/01-setting_up/command_line.rst","topics/01-setting_up/environment_overview.rst","topics/01-setting_up/feature_branching.rst","topics/01-setting_up/git.rst","topics/01-setting_up/git_editor_windows.rst","topics/01-setting_up/git_hints.rst","topics/01-setting_up/git_overview.rst","topics/01-setting_up/git_workflow.rst","topics/01-setting_up/github_classroom.rst","topics/01-setting_up/index.rst","topics/01-setting_up/intro_to_git.rst","topics/01-setting_up/ipython.rst","topics/01-setting_up/python_and_core_tools.rst","topics/01-setting_up/python_for_linux.rst","topics/01-setting_up/python_for_mac.rst","topics/01-setting_up/python_for_windows.rst","topics/01-setting_up/resources.rst","topics/01-setting_up/setup_details.rst","topics/01-setting_up/shell.rst","topics/01-setting_up/sublime_as_ide.rst","topics/01-setting_up/submitting_to_github.rst","topics/01-setting_up/testing_your_setup.rst","topics/01-setting_up/vagrant.rst","topics/01-setting_up/virtualenv.rst","topics/01-setting_up/vsc_as_ide.rst","topics/01-setting_up/windows_bash.rst","topics/02-basic_python/index.rst","topics/03-recursion_booleans/index.rst","topics/04-sequences_iteration/index.rst","topics/05-text_handling/index.rst","topics/06-exceptions/index.rst","topics/07-unit_testing/index.rst","topics/08-dicts_sets/index.rst","topics/09-files/index.rst","topics/10-modules_packages/index.rst","topics/11-argument_passing/index.rst","topics/12-comprehensions/index.rst","topics/13-intro_oo/index.rst","topics/14-magic_methods/index.rst","topics/15-subclassing/index.rst","topics/16-multiple_inheritance/index.rst","topics/17-functional_programming/index.rst","topics/18-advanced_testing/index.rst","topics/99-extras/index.rst"],objects:{},objnames:{},objtypes:{},terms:{"0":[13,15,16,17,18,25,26,33,35,45,47,50,51,52,53,54,55,56,57,59,62,63,66,67,68,69,71,72,73,76,78,79,80,83,84,85,86,88,92,94,95,97,99,100,101,102,103,104,106,107,108,109,110,124,127,129,130,131,134,137,139],"00":[52,94,124,127,139],"000":[95,101],"000000":[13,103],"0000000000001e":108,"0000000000001e22":108,"000000002220446":108,"00000001":106,"00001":106,"00034e":108,"0008":[77,88,93],"004779":101,"00e":48,"01":[48,94,106,116],"015625":59,"02":[25,48,79,95,106,107,124],"022107":57,"0249":94,"0257":108,"03":[25,62,94,107,111],"0308":57,"03125":59,"0328":92,"033":134,"04":[6,48,67,81,94,131,139],"0427":92,"04288":101,"0485":[50,108],"0498":103,"05":[25,87,92,94],"0519":72,"05311584473e":67,"057c5c08ae41":54,"05e8dde3229c":63,"06":[9,25,67,87,94,104,110,120,121,131,137],"0625":59,"0636920032519":81,"07":25,"0700":121,"08":[6,12,25,82,84,106],"0800":121,"08b622ddf7eb":84,"09":[25,48,106,108,121],"0c3401794933":100,"0dfacfcc443":54,"0e22":108,"0ec059b9bfe1":73,"0j":57,"0m":134,"0th":78,"0x101e01090":79,"0x101e01350":79,"0x101e01710":79,"0x1020bb198":25,"0x102bbed00":62,"0x10325b5e8":25,"0x1032f8438":25,"0x10349e888":64,"0x103604400":24,"0x103967030":25,"0x104437fd0":79,"0x1046f2ac8":83,"0x1047873c8":64,"0x104d75620":55,"0x1063ef6d0":65,"0x10911bf50":79,"0x10b10dfd0":25,"0x10b123828":25,"0x10c4881f8":25,"0x10c4d5c88":25,"0x10e23fcf0":25,"0x1105e28e8":25,"0x2bf928":98,"0x2de918":98,"0xff":72,"1":[0,2,15,22,26,28,30,39,46,47,48,50,51,52,54,55,56,57,59,62,63,64,65,66,67,68,69,71,72,73,75,76,78,79,80,83,84,85,86,87,92,93,95,96,97,98,99,100,101,102,103,104,106,107,108,109,110,122,126,128,129,130,131,134,137,139],"10":[0,2,3,13,22,23,25,28,35,44,47,48,50,54,55,56,57,59,61,62,69,72,73,78,79,80,82,83,84,92,98,100,101,104,106,107,108,109,110,111,121,128,141],"100":[22,25,26,30,32,33,35,49,50,56,77,94,95,98,100,106,107,108,124],"1000":[88,94,95,106],"10000":[48,95,101],"100000":16,"1000000":95,"10000000":[67,108],"100644":124,"101":[78,81,100,106],"102":100,"104":103,"1043":106,"10432":35,"105":103,"1087":[24,25],"109":103,"11":[3,4,13,22,25,45,47,48,56,59,61,62,68,69,72,73,78,79,80,83,84,86,92,96,98,100,103,104,106,108,110,121,127,129,130,131,137,139],"110":103,"111":103,"1110":106,"11111110":95,"11111128":95,"114":103,"115":[103,106],"117":[25,137],"12":[3,13,25,46,47,48,55,56,59,61,62,68,69,71,72,78,79,80,83,85,86,92,96,98,100,103,104,106,110,120,121,127],"120":35,"121":100,"122":100,"1226":106,"123":[48,100,106,110],"1230000000393":81,"1234":51,"12345":[48,103],"12345678987654321234567890987654321234567898765":110,"124":[67,100],"125":[59,88,100],"1252":111,"125kb":139,"127":[103,111],"128":[111,130],"129":[25,100,106],"13":[4,13,18,22,44,45,46,47,51,52,55,56,59,61,62,68,69,72,79,80,83,86,92,96,98,100,104,106,107,130],"130":[51,100],"1301":106,"131":100,"132":100,"133":100,"135":106,"137":100,"138":100,"139":100,"14":[4,13,18,22,28,56,59,61,62,68,69,72,73,79,80,83,84,92,94,96,99,100,104,106,111],"140":78,"140553647884864":56,"140553647890984":56,"141":78,"1415":94,"141592653589793":98,"142":25,"1424":106,"143":78,"1459":110,"147":78,"148":[78,106],"1485":106,"15":[7,18,23,48,55,56,59,62,66,69,72,73,79,80,84,86,92,94,97,98,99,100,103,104,106,108,111,129],"150":[78,106],"1509":106,"151":54,"151283":69,"152":[54,106],"15210":74,"153":54,"154":54,"156":[54,78,100],"157":[54,78,100,106],"158":[54,78,100],"159":54,"15e":108,"16":[13,18,22,48,56,59,62,69,72,73,79,80,83,84,86,89,92,97,98,99,100,102,108,110,121,129,131,137],"160":[54,100,106],"1607":106,"161":[78,100],"162":100,"163":[25,100],"16396":28,"164":[79,100],"165":[79,100,106],"166":79,"1660":35,"167":[85,103],"168":[85,103],"169":103,"16bit":111,"17":[18,25,48,51,56,59,62,72,73,79,80,83,84,86,94,97,98,99,100,103,108,111,124,128,129],"170":[78,103],"171":[78,103],"172":[78,103,129],"173":[78,103],"174":[78,103],"175":103,"1754":106,"1767766952966369":59,"18":[2,7,13,18,25,45,48,51,56,59,72,73,80,83,98,100,106,107,111,121,129,130,131],"181":103,"182":103,"183":103,"1832":106,"184":103,"185":103,"186":[80,103],"187":80,"1888":106,"19":[13,18,25,56,62,69,73,79,83,84,94,100,101,106,115,127],"191":106,"1914":131,"1946":106,"1972":106,"197499c4623c":108,"198":106,"199":25,"1993":106,"1a":24,"1a7db9b70878":79,"1b":24,"1bf9cc5093":[130,131],"1butthisisnot":56,"1c":[24,25],"1c17e5fa4f59":103,"1dbbea504a9":56,"1e":108,"1e22":108,"1e6fdc328f08":56,"1st":[26,118],"1th":78,"2":[0,4,7,9,15,16,22,26,28,30,39,40,45,46,47,48,50,51,52,54,55,56,57,59,62,63,64,66,67,68,69,71,73,74,76,78,80,83,84,85,86,92,93,95,98,99,100,101,102,103,104,106,107,108,109,110,121,123,126,128,129,130,131,132,134,137,139],"20":[13,16,23,48,50,54,56,59,62,64,69,73,79,80,82,83,84,100,101,103,106,107,110,111,124,130,131,137],"200":[24,60,98,110],"2001":106,"2002":57,"2006":94,"2008":67,"2009":[79,81],"2010":[9,63,87,104],"2011":[84,87,120],"2012":82,"2013":[6,12,139],"2014":[81,92,111,129],"20140120":129,"2015":[62,67,127,129],"2016":[79,103,128],"20160":83,"2017":[48,69,74,110,121,129,131,137],"2018":[73,130,131],"2019":62,"2020":[106,130],"2021":124,"203":[25,100],"204":100,"205":[98,100,106],"206":[98,100],"206805":16,"207":100,"2074":106,"208":100,"20argument":80,"20callback":80,"20debugg":135,"20learn":81,"20next":81,"20python":81,"20to":[80,81],"20want":81,"20what":81,"20you":81,"21":[2,23,48,54,56,62,66,73,78,79,84,100,101,103,106,110,129],"213":106,"2152":106,"217":25,"21st":115,"22":[23,54,56,62,73,79,83,84,100,101,103,106,107,108,121],"2212":106,"2222":98,"2246467991473532e":92,"227":100,"228":100,"229":100,"23":[23,25,35,54,56,59,61,69,79,100,103,110,130,135],"230":100,"231":100,"232":100,"233":100,"234":100,"2343":106,"23473948":110,"236":28,"23e":48,"24":[23,25,28,54,59,69,73,79,83,99,100,103,106,130],"243":[25,56],"2459":110,"247":25,"249":[94,100],"2499":106,"25":[54,56,59,69,73,78,80,92,100,103,128,139],"250":108,"251":100,"252":100,"253":100,"255":[111,128],"256":[100,103],"257":[70,100],"2574":106,"258":100,"26":[25,35,44,54,56,66,69,73,78,82,83,87,100,101,103,106,128,129,130],"2646":106,"27":[25,48,56,69,73,78,79,81,83,96,100,101,131],"27017":89,"2723":106,"27s_law":63,"28":[56,65,66,69,73,78,83,86,94,96,100,101,103],"283":95,"289":106,"2892":106,"28object":104,"28program":96,"29":[23,44,45,54,56,73,78,86,96,100,101,103,104],"290":106,"291":106,"294":106,"297":25,"2975":106,"2d":[47,100],"2db728a46f78":100,"2f":50,"2gb":130,"2n":50,"2th":78,"3":[0,7,8,9,15,16,17,26,28,35,37,39,41,44,45,46,47,48,50,51,52,54,55,56,57,59,61,62,63,64,65,66,67,69,72,73,76,78,79,80,81,82,83,84,85,86,87,92,93,94,95,98,99,100,101,102,103,104,106,107,108,109,110,117,121,127,128,129,130,131,132,135,137],"30":[23,25,48,54,56,73,82,84,86,88,96,100,101,103,106,108],"300":66,"3000":106,"3000000000000000":108,"30000000000000004":108,"3051":106,"307":25,"308":57,"31":[23,56,62,86,96,100,106,108,121,129,130],"310":25,"3102":54,"3103":68,"312":106,"3132459951e4":56,"3185":106,"319":[72,124],"32":[35,46,54,56,66,73,79,83,86,92,94,100,103,106,108,111,129,131],"320":72,"321":72,"322":72,"32282847":120,"323":72,"32432432":95,"325":69,"3250":106,"326":69,"326892":28,"327":[69,94],"328":[69,92],"329":69,"33":[28,35,39,56,59,62,65,73,83,86,100,103,129],"3313":106,"333":103,"33m":134,"34":[48,54,56,57,59,62,65,71,73,79,83,86,100,103],"3408":106,"341":62,"342":62,"343":[35,62],"345":[57,110],"35":[56,57,59,62,65,66,71,84,86,94,100,103,106],"3501":106,"352":69,"353":69,"3535533905932738":59,"354":69,"36":[54,56,57,59,65,83,86,100,102,103,106],"3610":106,"3623577544766736":85,"37":[28,54,56,59,65,67,71,100,102,103,106],"37337":109,"3743":106,"374f501f4567":127,"38":[59,65,86,100,103,106,111],"3844":106,"3888":106,"389":54,"39":[35,54,57,59,65,86,100,103,108,130],"3940":106,"3975376":86,"3999":106,"3f":[81,103],"3fd9b1939623":99,"3rd":63,"3rdparti":79,"4":[0,3,15,17,22,23,26,35,39,45,46,47,48,50,51,52,53,54,55,56,57,59,61,62,64,67,69,73,74,76,78,79,80,83,84,85,86,92,93,94,95,98,99,100,101,102,103,104,106,107,108,109,110,111,115,121,127,128,129,131,134,135,139],"40":[35,57,59,82,86,100,103,106,107,108],"400":[24,25,60,88,106],"4000":106,"408":103,"409":103,"4096":72,"41":[59,67,78,86,102,103,106,108],"411":35,"415":103,"42":[13,28,54,56,59,69,78,100,102,103,106,108,129],"421":106,"43":[59,78,94,100,103,106],"4320":35,"4363b2b69f73":73,"44":[54,59,78,100,102,106],"444":56,"44999999999999996":108,"45":[54,78,94,102,103,108,110],"4529e5befb95":73,"456":[56,57],"4567":48,"45e":108,"46":[48,54,64,78,100,103],"47":[69,78,100,128],"4795ddf4":121,"4795ddf41f20cfc4346f02319ab61699e8a469f2":121,"48":[56,69,71,78,106],"49":[28,56,69,72,78],"49718712":63,"498":103,"4985f9d":124,"4def2a2901a5":128,"4f":95,"5":[0,15,16,23,26,37,45,46,47,48,50,51,52,54,55,56,57,59,62,64,65,66,67,69,72,73,78,79,82,83,84,85,86,92,93,94,95,96,97,98,99,100,101,103,104,106,107,108,109,110,111,121,127,128,129,131,139],"50":[30,50,56,66,69,72,78,82,106],"500":[94,106,130,139],"5000":106,"500000":103,"500l":55,"50c56a77d95f":102,"51":[56,64,69,72,78,94,131],"512":102,"515":71,"519":72,"52":[48,56,69,94,100,106,108],"528":106,"5289811a13ac":100,"53":[48,56,69,94,100,106],"534dcdcb5c84d28b596ad15":89,"54":[46,56,59,69,84,94,100,106],"5465":28,"54c2cb2bf478":99,"55":[56,80,100],"555":85,"557":84,"56":[48,56,67,80,84,100],"566370":13,"566370614359172":98,"5666":[110,128],"57":[25,56,67,80,84,102,130],"576169":87,"58":[56,67],"59":[56,84,121,131],"5a33b9d3e525":100,"5c468e6e2e8":55,"5c699bc20e80":72,"5e2":109,"5ef8442810a5":54,"5f315c5de15b":65,"5fd33b5926":[110,131,137],"5ff805d50ea6":99,"6":[0,2,4,15,26,44,46,48,51,54,56,57,59,62,67,69,72,73,78,79,83,84,85,86,87,92,93,94,96,98,99,100,101,102,103,104,106,107,108,110,111,121,123,129,130,131,137],"60":[66,84],"600":[106,130],"60b725f10c9c":56,"61":[84,106],"62":103,"621":106,"63":[103,106],"639":124,"64":[92,102,103,108,124,131],"641528ffa695":64,"65":[44,100,103],"653784":28,"65536":111,"66":[44,103,106],"663":35,"67":[48,103],"6731d4ac4476":56,"6760685":84,"68":[56,79,94,106,139],"685a01a77340":56,"69":[56,79,94,103,108],"6e2a9c3f":115,"6mb":139,"7":[0,3,4,22,26,41,44,45,48,51,54,56,59,62,66,67,69,71,72,73,78,79,80,83,84,85,86,94,95,96,98,99,100,101,103,106,107,108,110,111,123,129,130,131,139],"70":56,"700":106,"7071067811865476":59,"708":28,"70c8add5d16":97,"71":[56,67,94,100],"72":[25,56,67,94,100,135],"72be8e65d115":65,"73":[56,67,94,100,103,106],"74":[56,100],"75":[56,100,106],"754":108,"755":139,"7584":131,"76":[56,100],"77":[56,100],"776639767":126,"78":[56,100],"782":106,"79":[25,56,93,100,135],"7d9a6b30cd26":24,"7ddb":115,"7deb":115,"7f87d44dfcfa":111,"8":[0,14,22,26,44,45,48,51,52,55,56,59,61,62,67,69,72,73,78,79,80,84,85,95,96,98,100,101,103,106,107,108,109,110,117,128,129,130,134,137],"80":[56,100,103],"800":106,"8000":66,"81":[56,103,128],"82":[25,63,103,106],"83":[25,94],"84":[56,94,106],"8414709848078965":85,"85":[94,100],"86":[25,94,100,111],"87":[35,94,100],"870":106,"877":[28,35,39],"879":[7,95],"87d27a12":121,"87d27a12bcae5c1bdc565e05e954e7c94bfa27e0":121,"88":48,"880":7,"8837b3d7f123":64,"8b9269cdf0e5":85,"8bit":111,"8e5908a37d7d":121,"8e5908a37d7df90263057644fef7138e77838107":121,"8mb":139,"9":[0,23,44,48,56,59,61,62,66,67,69,72,78,80,84,96,98,100,101,102,106,107,108,111,121,128,129,130],"90":106,"900":106,"9000":106,"907616e55e2a":56,"92":100,"93":[94,100],"9320390859672263":85,"94":[94,100],"941":106,"95":[25,33,93,94,100,135],"96":[26,64,94,100],"97":[26,44,94,95,100],"9726":101,"9781430224150":81,"98":[26,94,100],"99":[48,100],"997071027756":67,"99999990000000":67,"9th":121,"9x":120,"abstract":[25,84,111],"boolean":[0,53,56,64,83,103,109],"break":[24,25,33,34,40,50,51,55,56,60,63,66,72,78,79,87,93,103,106,109,124,134,139],"byte":[72,94,97,100,102,103,110,111,124,128],"case":[12,13,17,18,24,25,27,28,33,34,35,36,38,39,40,48,49,50,51,54,55,57,59,62,66,67,69,71,72,73,74,77,79,80,82,84,85,86,87,90,91,92,95,97,98,99,100,101,102,104,106,107,108,110,111,118,121,123,124,126,128,129,138],"catch":[10,24,25,41,51,64,66],"char":[33,95,103,111,134],"class":[2,6,8,11,14,21,24,25,30,31,43,45,50,52,53,55,56,57,59,61,64,66,67,68,69,74,77,79,81,89,90,91,92,94,96,97,105,106,107,108,109,111,112,117,118,121,122,123,124,126,128,129,130,131,136,139,153,154],"default":[12,16,23,24,25,45,55,61,66,68,69,72,74,80,82,84,85,92,93,94,95,98,101,102,103,108,109,111,115,120,121,123,124,126,129,130,131,135,139,140],"do":[1,2,12,13,14,15,17,22,23,24,25,26,27,28,30,33,34,35,39,40,41,44,45,47,48,49,50,51,54,56,57,59,62,63,65,66,67,68,69,70,72,74,75,77,78,80,81,83,84,88,89,90,93,94,95,96,98,99,100,101,102,103,104,105,106,107,108,111,114,115,117,118,121,122,123,124,125,126,127,128,129,130,131,135,136,137,138,139,141,142],"export":[89,94,134,139],"final":[17,25,28,33,48,56,64,72,74,106,122,124,135,139],"float":[38,48,50,56,57,66,71,94,100,101,103,106,109,110,124],"function":[0,1,12,15,19,24,25,26,28,32,34,35,36,39,40,41,42,43,44,45,46,48,50,51,52,53,58,60,61,62,63,64,65,66,69,71,79,82,84,85,86,87,89,91,92,93,95,96,97,99,100,101,102,103,104,106,107,108,109,115,122,124,128,135,138,142],"goto":91,"import":[13,16,25,33,34,35,37,40,48,51,54,55,56,59,61,63,64,66,70,71,72,76,78,79,82,83,84,86,88,89,91,94,95,97,98,100,101,106,107,108,109,110,111,115,118,121,124,126,128,131,135,137,139],"int":[38,56,57,62,71,88,93,94,100,106,109,110],"long":[16,25,35,47,49,51,55,59,62,63,66,67,73,85,88,92,95,100,102,106,107,110,111,121,122,124,128,129,134,139],"new":[12,13,15,17,24,25,27,28,33,34,35,39,40,43,44,45,46,48,49,50,51,55,56,59,62,64,66,67,68,69,72,73,74,77,79,81,82,84,85,86,87,89,91,94,96,98,99,100,102,104,106,107,108,110,118,121,122,126,128,130,131,139],"null":[93,134],"public":126,"return":[12,13,15,16,19,26,27,28,30,33,35,36,40,44,45,46,48,49,50,51,55,59,62,64,65,66,67,68,69,70,71,73,79,80,82,83,84,87,90,92,93,95,96,97,98,99,100,101,102,103,104,106,107,108,109,110,111,128,135,139],"short":[25,33,44,48,51,55,60,66,67,80,81,85,88,92,93,100,103,106,107,110,124,135],"static":[0,8,33,84,91,123,154],"super":[25,84,97,104,135],"switch":[4,25,37,106,118,121,122,123,139,148],"throw":[81,93,106,108,134],"true":[13,17,34,35,55,56,57,59,63,64,66,69,72,78,79,83,84,86,91,92,93,94,97,98,100,101,103,106,108,109,111,115,135],"try":[2,15,18,22,23,24,25,30,33,38,40,42,44,47,50,51,55,56,59,61,64,66,67,69,71,72,73,78,79,81,84,85,87,88,89,90,92,93,94,95,97,99,100,101,106,107,108,109,110,111,117,121,122,124,126,128,129,130,131,137,139],"var":[56,62,66,78,84,92],"voil\u00e0":134,"while":[12,14,17,25,27,28,33,34,35,40,45,51,55,56,59,63,64,65,69,71,72,73,79,80,81,84,85,87,88,92,93,95,99,100,101,103,106,107,108,109,110,118,122,124,128,130],"x\u00b2":111,A:[6,8,13,21,26,33,34,41,44,46,48,51,52,55,56,59,63,64,71,72,75,80,81,84,85,87,89,91,93,94,100,101,102,103,104,107,108,111,117,121,124,126,128,150,151,159],And:[13,16,18,21,23,24,25,30,31,32,33,34,40,46,48,50,51,52,54,55,57,59,61,62,63,64,65,67,68,69,70,71,72,74,75,77,79,80,82,83,84,85,87,88,89,90,92,94,96,97,98,99,101,102,103,104,107,110,111,121,123,124,128,130,134,135,137,139],As:[15,18,24,33,40,44,51,55,60,62,64,69,70,71,72,81,82,84,90,91,92,95,98,104,105,106,111,124,130,134,136,139],At:[0,27,28,30,33,51,59,60,64,66,72,74,76,101,106,107,110,124,128],BUT:[23,51],BY:94,Be:[13,24,25,51,70,74,78,124],Being:[59,110],But:[6,16,18,22,24,25,32,33,34,40,46,47,48,49,50,51,52,54,55,56,57,59,62,63,64,65,66,67,68,69,71,72,73,74,75,78,79,80,82,83,84,85,86,87,88,89,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,121,122,123,124,128,130,131,133,135,136,139],By:[25,100,101,106,115,120,124,135],For:[12,13,22,23,24,25,27,30,33,38,40,48,50,51,55,60,62,63,66,67,70,71,72,73,74,77,79,84,85,86,88,89,93,95,97,99,100,101,102,103,104,106,107,110,111,120,121,122,123,124,126,128,129,131,134,139],IF:[30,72,121],IN:[121,126],INTO:94,IT:68,If:[1,2,7,9,12,13,14,15,23,24,25,26,27,28,29,30,33,34,35,38,39,40,44,49,50,51,52,54,55,56,57,59,62,63,64,66,67,68,69,70,71,72,73,74,75,77,78,79,80,81,82,83,84,85,86,87,89,90,92,93,94,95,96,99,100,101,102,103,104,105,106,107,108,109,110,111,115,117,118,121,123,124,126,128,129,130,131,134,135,136,137,139,140,141],In:[1,10,12,13,16,18,21,23,24,25,26,27,30,33,34,35,36,37,39,40,43,44,45,47,48,49,50,51,52,54,55,57,59,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,78,79,80,81,82,83,84,85,86,87,89,90,91,92,94,95,96,97,98,99,100,101,102,103,104,105,107,108,109,110,111,115,118,121,122,123,124,128,129,130,131,134,135,136,138,139],Into:[2,3,4,81,114,128],Is:[30,35,50,67,69,71,74,85,86,87,88,91,92,100,106,110,124],It:[2,10,12,14,18,22,24,25,28,30,31,32,33,34,35,39,40,45,47,48,51,53,55,56,57,62,63,66,67,68,69,70,71,73,74,75,77,78,79,80,81,82,83,84,85,87,88,89,90,91,92,93,94,95,96,98,99,100,101,102,103,104,105,106,107,108,109,110,111,113,114,115,117,118,121,122,124,126,128,129,130,131,135,136,137,139,140],Its:[24,25,109,111],NOT:[13,21,24,25,33,54,55,56,57,71,80,85,89,92,94,100,106,108,110,111,123,126,130],No:[25,51,63,69,71,73,81,84,92,94,110,111,118,139,159],Not:[18,24,26,34,35,55,63,64,69,79,81,84,87,88,89,92,94,108,109,110,113,134,135],OF:[51,103],ON:66,ONe:100,OR:100,Of:[19,25,28,50,84,100,103,106],On:[30,57,75,85,95,110,111,115,124,128,131,135,139],One:[23,24,25,32,33,34,35,51,52,58,77,84,88,90,91,92,95,96,101,103,104,107,109,111,117,122,124,129,130,131,135,138,141],Or:[15,24,30,32,41,55,60,63,64,71,73,84,85,88,89,92,94,100,101,108,109,128,130],Such:97,That:[12,17,18,24,25,27,32,33,34,36,40,45,46,48,49,51,52,55,56,59,62,66,67,68,71,72,73,74,78,79,82,83,84,87,90,91,92,95,97,98,99,100,104,106,108,109,110,118,120,121,122,124,126,127,128,129,134,139],The:[1,5,7,13,15,17,18,19,23,25,27,30,34,35,37,39,40,42,45,48,52,54,56,57,62,63,65,67,71,73,74,77,78,80,81,83,85,90,93,95,97,102,103,104,107,113,115,118,121,122,124,126,129,133,134,135,139,140,141,152,159],Then:[24,25,34,35,36,48,49,51,52,56,59,60,61,67,71,73,74,75,79,84,85,87,89,91,92,93,95,97,101,103,106,107,111,121,123,124,128,129,131,134,139],There:[14,18,24,25,28,33,34,35,38,39,41,44,51,55,56,59,62,63,64,67,68,69,70,71,74,78,80,82,84,87,89,90,91,92,93,94,95,97,98,101,103,104,106,107,108,109,110,111,117,118,121,124,126,128,130,131,134,135,139,140],These:[24,25,29,33,43,46,47,52,53,55,56,59,61,65,69,72,73,74,84,85,89,91,92,94,100,101,106,107,108,109,110,128,139],To:[4,14,17,23,24,30,34,39,40,44,48,51,55,56,57,60,61,62,63,66,74,76,77,79,83,89,103,104,105,106,107,108,110,115,121,122,124,128,129,130,131,134,135,136,139,140,141],WITH:56,Will:[24,29,95],With:[4,5,6,25,32,33,55,66,68,73,74,93,98,99,106,108,110,126,135,139,141,146,147,148,149,152],_:[25,59,106],__:[78,84,92],________________:51,____________________:106,_____________________:106,______________________:106,_______________________:106,________________________:106,_________________________:106,__________________________:106,___________________________:106,____________________________:106,_____________________________:25,______________________________:[25,106],_______________________________:25,________________________________:25,_________________________________:[25,107],__________________________________:25,___________________________________:25,____________________________________:25,__add__:[101,128],__all__:92,__and__:101,__await__:55,__bool__:57,__builtin__:[62,66,71,128],__builtins__:92,__cached__:92,__call__:[58,67,79,84,101],__class__:[84,98,128],__contains__:[101,128],__delattr__:[84,98,128],__delitem__:101,__dict__:98,__dir__:[84,98],__divmod__:101,__doc__:[70,84,92,98,128],__enter__:64,__eq__:[98,101,128],__file__:[34,92],__floordiv__:101,__format__:[98,128],__future__:[97,111],__ge__:[98,101,128],__getattribute__:[98,128],__getitem__:[79,101,128],__getnewargs__:128,__getslice__:128,__git_ps1:134,__gt__:[98,101,128],__hash__:98,__index__:101,__init__:[7,24,25,33,34,59,64,67,79,85,87,89,92,96,98,101],__init_subclass__:98,__iter__:[79,101],__le__:[98,101],__len__:[57,84,101],__loader__:92,__lshift__:101,__lt__:[98,101],__main__:[28,34,35,41,44,51,52,56,59,67,84,92,98,102,106,107,108,109],__matmul__:101,__metaclass__:84,__mod__:101,__module__:[59,84,98],__mul__:101,__my_paranoid_method:93,__name__:[28,34,35,44,51,52,59,67,80,92,107,108,109],__ne__:[98,101],__new__:98,__next__:79,__or__:101,__package__:92,__path__:139,__pow__:101,__qualname__:59,__reduce__:98,__reduce_ex__:98,__repr__:[13,94,98,101],__reversed__:101,__rshift__:101,__setattr__:98,__setitem__:101,__sizeof__:98,__spec__:92,__str__:[13,98,101],__sub__:101,__subclasshook__:[84,98],__traceback__:71,__truediv__:101,__version__:92,__weakref__:[84,98],__xor__:101,_asisthi:56,_buffer_decod:72,_close_tag:25,_codec:92,_create_payload:102,_del_x:67,_dict:84,_draw:87,_frozen_importlib:92,_frozen_importlib_extern:92,_id:89,_imp:92,_io:[25,92],_my_private_method:93,_open_tag:25,_set_x:67,_test:107,_thread:92,_warn:92,_weakref:92,_x:[67,96],a079bc472aca:103,a18e010ecdd0:71,a_boolean:94,a_circl:13,a_class:104,a_class_attribut:84,a_class_method:102,a_condit:78,a_coroutin:55,a_decor:84,a_default:93,a_dict:[62,69],a_dir:72,a_field:89,a_filenam:[72,101],a_fixtur:79,a_float:[56,94],a_funct:[56,62,67,85,98],a_gener:79,a_generator_funct:79,a_list2:62,a_list:[46,51,56,62,79,89,100,101],a_list_of_numb:50,a_list_of_str:56,a_modul:85,a_nam:[56,92],a_name_in_the_modul:85,a_new_code_block:56,a_new_fil:124,a_new_nam:85,a_new_sequ:46,a_packag:92,a_photo:72,a_result:101,a_script:92,a_sequ:[62,79,88],a_simple_script:124,a_stat:56,a_str:[46,62],a_sub_packag:92,a_subclass:104,a_superclass:104,a_tupl:[46,100],a_valu:57,a_vari:66,aaadfbdd293:56,aabbbcccc:15,aac:92,ab:72,abc:[69,100],abil:[13,33,84,89,92,115,127,135,139,140],abl:[1,2,12,13,17,23,24,25,27,28,32,33,34,35,40,47,51,55,56,60,64,65,68,69,70,77,84,85,87,91,101,106,115,118,120,124,128,129,130,131,134,135,137,140,141],about:[1,8,12,15,16,17,23,24,25,28,29,30,33,34,35,37,38,39,40,41,48,49,51,55,56,57,59,60,63,64,65,68,71,72,73,81,82,83,84,85,86,87,89,90,91,92,93,94,95,97,98,99,100,101,103,104,106,107,109,110,111,118,121,122,123,124,127,128,129,134,135,139,141],about_comprehens:15,abov:[12,13,24,25,27,28,30,35,39,40,46,48,50,51,55,59,62,64,66,67,68,69,71,72,74,81,82,84,85,87,92,98,100,102,107,108,109,110,111,121,124,128,129,130,131,135,139,140],abs_tol:108,absenc:57,absolut:[55,66,72,81,88,92,107,109,111,126],abspath:72,abstract_tag:25,abus:[62,108],ac_list:100,academ:[74,90,124],academi:[81,110],acceler:[95,121],accent:[103,111],accept:[24,25,35,50,51,87,88,104,106,108,126],access:[24,25,33,47,55,56,59,60,63,69,71,73,74,78,79,84,85,89,91,92,93,94,96,98,104,107,109,115,124,126,128,130,137,139,141],accid:92,accident:[74,92,106,121,124],accommod:48,accompani:106,accomplish:[17,24,25,27,28,30,35,41,44,57,67,103,106,108],accord:30,accordingli:[51,115,140],account:[30,72,95,105,108,123,124,126,136],accumul:[100,104],accur:50,accuraci:106,acer:35,achiev:[18,55,62,63,68,108,135],acid:89,acquir:109,across:[30,89,90,95,108],act:[15,50,72,82,91,95,98,100,101,108,122],action:[17,27,28,39,55,56,64,67,71,89,107,108,109,139],activ:[0,1,55,66,74,103,118,122,134],activepython:92,activest:92,actual:[13,15,18,24,25,32,35,40,41,42,51,54,55,56,57,59,62,63,65,67,68,69,70,71,72,74,79,82,84,87,88,89,90,91,92,95,98,100,101,102,103,106,107,108,109,110,111,122,124,134,139,141],ad:[13,25,27,28,30,31,33,35,36,38,40,51,52,54,55,58,59,60,61,62,65,67,68,71,79,89,92,95,98,100,101,103,105,106,107,109,110,118,124,130,136],adam:80,adapt:[12,23,51,52,89,93,106,128,135],add:[13,17,24,25,26,27,28,29,30,32,33,34,35,39,40,41,43,44,45,48,50,51,55,56,59,62,66,67,68,69,70,71,72,74,79,82,84,85,87,89,92,96,97,98,99,100,102,103,104,105,106,107,108,109,110,115,117,121,122,123,124,128,129,130,131,135,136,139],add_book_data:94,add_book_data_flat:94,add_done_callback:55,add_fruit:35,add_sect:94,add_tag:67,addcleanup:108,addhandl:82,addit:[13,24,25,33,34,35,39,41,48,50,51,54,55,56,65,67,74,77,81,92,100,106,107,108,110,126,128,139,140,141],addon:94,address:[1,24,25,33,63,66,89,90,94,124],address_book_model:89,address_book_mongo:89,address_book_zodb:89,addressbook:94,adequ:1,adjac:51,adjust:[115,124],admin:74,adopt:[92,93,111,126],adult:87,advanc:[0,12,20,33,40,53,56,64,66,71,84,89,92,97,107,111,125,135],advantag:[25,30,35,41,55,64,71,72,92,102,108,139],adventur:51,advertis:[12,81],advic:[60,117,129,130,131],advoc:106,aeiou:62,aesthet:90,af:74,affect:[29,98,118,124],afraid:87,after:[1,16,21,23,24,25,28,30,33,34,39,40,50,51,55,56,57,59,60,65,66,69,71,72,73,74,77,79,84,85,92,93,94,98,100,103,106,107,108,110,111,121,129,130,134,135,139,140],afterward:60,ag:[35,48,84],again:[12,14,18,24,25,27,28,30,33,51,52,62,65,66,68,69,71,79,80,84,85,87,92,95,100,106,107,108,109,110,118,121,124,135,137,139],against:[35,51,74,94,103,106,108],agnost:39,ago:[85,86,87,89],agre:[91,93,103],ahead:[13,25,62,106,111,123,134],ahh:106,aid:[62,106],aim:81,airpair:74,airship:51,ajax:55,aka:[91,93],akin:90,alabama:90,alchemi:94,aleksandrova:124,alert:78,alex:[71,86],algebra:[56,110],algorithm:[24,30,51,69,74,95,101,109],alia:[92,129,130,131],alias:92,align:[24,25,28,48],aliv:55,all:[1,8,12,13,14,15,16,20,21,24,25,26,27,28,30,33,34,35,36,39,40,41,42,43,45,46,49,50,51,52,53,54,55,56,59,60,61,62,63,64,66,67,68,69,72,73,74,78,79,80,82,83,84,85,87,88,90,91,92,93,94,95,97,98,100,101,103,104,105,106,107,108,109,110,111,115,117,120,121,122,123,124,125,126,128,129,130,131,133,134,135,136,137,139,140,141,142],all_cap:73,all_lett:100,all_the_nam:88,allen:[28,35],alloc:[36,95],allow:[16,24,25,30,33,34,40,51,54,55,56,57,63,64,66,69,70,73,74,78,82,84,88,91,92,94,100,101,106,107,108,109,115,121,122,124,128,135,138,139,140],allud:79,almarklein:66,almost:[15,49,51,71,78,80,83,89,91,92,95,98,100,101,106,108,111],alon:[25,103,124,126],along:[24,54,55,56,59,67,71,81,92,100,106,115,118,121,124,130,135,140],alongsid:139,aloud:66,alphabet:[44,111],alreadi:[2,13,16,24,25,32,33,34,40,48,51,52,55,59,62,66,67,69,71,72,73,78,81,84,85,88,92,95,100,103,105,106,107,108,109,115,117,118,124,126,128,129,130,131,136,138,140],also:[1,12,15,16,23,24,25,27,30,31,33,34,35,39,41,48,51,52,54,55,56,57,59,61,62,63,64,65,66,67,69,70,71,72,74,78,79,80,81,82,84,85,86,87,89,90,91,92,93,94,95,97,98,100,101,102,103,104,105,106,107,108,109,110,115,118,121,122,123,124,126,128,129,130,131,134,135,136,138,139,140],alter:[18,78,89,92,100,107],altern:[13,19,24,48,57,63,89,90,92,94,95,108,124,129],although:[69,84,106,128],alwai:[25,33,46,51,55,56,57,59,60,66,69,71,72,74,78,82,84,85,87,89,90,91,92,93,94,95,100,106,107,108,110,111,118,122,123,124,129,130,139],alwlai:82,am:[55,56,71,96,104,121,124,137],amaz:127,amazon:[81,94],amd64:131,amdahl:63,american:90,among:[90,115,122,123],amount:[24,25,28,30,38,40,44,55,95],amplitud:50,an:[1,2,10,12,13,14,16,17,18,19,22,23,24,25,26,27,28,30,32,33,34,35,36,37,38,39,40,42,43,44,45,46,47,48,50,51,54,56,57,59,61,62,63,65,66,69,70,72,73,75,77,78,80,81,82,83,85,86,87,88,90,91,93,94,97,98,99,100,102,103,104,105,106,107,108,109,110,111,116,118,120,121,122,123,126,127,129,130,131,135,136,137,138,139,140,141],an_await:65,an_el:24,an_inst:[84,104],an_instance_attribut:84,an_integ:[56,94],an_iter:[62,79,97],anaconda:[92,95,128,131,139],analog:100,analysi:[51,128],analyt:50,analyz:[66,84,108],anchor:[24,25],ancient:106,andi:48,andthennotparam:134,ani:[1,12,15,20,23,24,25,27,28,29,30,31,33,35,36,41,44,46,48,50,51,54,55,56,57,58,59,62,63,64,65,66,67,69,70,71,72,73,77,79,80,81,82,84,85,86,87,89,90,91,92,94,95,97,98,100,101,103,104,105,106,107,108,109,110,111,115,117,120,121,122,123,124,126,127,128,130,131,134,135,136,138,139,140,141],anim:87,anonym:[10,56,157],anoth:[2,18,23,24,25,27,30,33,35,36,41,49,50,51,52,55,56,57,59,62,63,66,67,68,69,71,73,79,81,82,83,84,85,86,90,92,94,96,98,100,103,105,106,107,108,121,122,124,126,128,134,136,139],another_branch:118,another_circl:13,another_field:89,another_fil:[105,124,136],another_list:46,another_logg:82,another_nam:[24,85],another_str:46,another_valu:57,ansi:111,answer:[27,29,50,51,57,69,86,90,100,106,110,121,131],anteat:87,anteced:106,anti:92,anticip:[71,106,108],antoh:[121,139],anydbm:89,anymor:[85,103,110,111],anyon:[24,48,52,56,74,80,85,92,95,108],anyth:[12,24,25,33,35,41,51,52,55,56,60,62,64,67,68,69,71,72,78,79,84,85,87,89,92,94,98,100,101,103,106,107,108,110,121,123,124,128,142],anytim:79,anywai:[25,55,71,81,91,93,94,100,103,106,130],anywher:[33,34,37,39,51,55,66,80,92,104,123],aosabook:55,apach:[89,94],api:[25,33,54,63,66,74,82,84,85,89,92,95,104,106,109],apostroph:[51,56,110],app:[33,66,82,92,94,128,130,140,141],appar:[22,93,116],appdata:131,appear:[100,106,115,134,135],append:[24,25,35,36,46,47,51,61,62,80,82,86,89,98,100,103,109,134],appendix:[81,116],appendixa:116,appl:[27,35,92,110,128,130,137,139],appleton:51,appli:[22,30,35,51,59,62,67,70,83,84,93,95,98,100,101,108,139],applic:[25,55,62,63,66,74,82,85,88,89,93,94,95,106,108,124,128,130,131,135],apply_async:109,apply_soap:70,apply_sync:[30,76],appreci:[67,107],approach:[24,25,31,33,40,49,51,55,70,75,81,83,89,90,91,92,103,104,106,107,109,110,117,118],appropri:[1,13,24,33,77,82,91,92,95,101,106,107,126],approv:[123,124],approxim:108,apress:81,apretti:128,april:[57,129],apt:[92,129],ar:[1,2,6,7,8,11,12,13,14,15,16,20,22,23,24,25,27,28,29,30,31,32,33,34,35,36,38,40,41,43,44,45,47,48,50,51,52,53,54,55,56,59,60,61,62,63,64,65,66,67,68,69,70,71,72,74,75,77,78,79,80,81,82,83,85,86,87,88,89,91,92,93,94,95,97,99,101,102,103,105,106,107,108,109,110,114,115,117,118,120,121,122,123,124,125,126,127,128,129,130,131,134,135,136,138,139,140,141,159],arab:106,arbitrari:[15,23,25,30,48,50,51,55,59,63,69,72,89,92,94,103,104,111,121,139],arbitrarili:[24,69,85,106],architectur:[30,89],archiv:92,area:[13,50,88,98,104,118],aren:[25,63,69,72,74,87,92,95,102,103,106,111,139],arg1:[67,70,84,93,108],arg2:[67,70,84,93,108],arg3:93,arg4:93,arg:[6,25,55,56,62,66,67,71,73,84,87,95,101,104,108,109,151],arg_dict:68,arguabl:[62,90],argum:134,argument:[0,16,24,25,26,34,37,45,46,53,55,59,62,64,66,67,68,69,73,83,84,90,92,95,98,101,102,104,106,107,108,109,128,134],argv:51,argw:87,aris:[111,139],arithmet:56,arithmeticerror:[56,62],arm:134,armstrong:91,arn:44,around:[24,30,44,50,55,59,61,63,64,65,67,72,80,82,87,89,90,92,95,96,97,103,107,109,111,121],arr1:101,arr2:101,arr:78,arrai:[8,56,66,69,78,90,95,100,101,109,154],arrang:[33,107],arrow:[118,127,135],art:7,articl:[59,66,108,111,123],ascii:[72,93,103,110,111],asctim:[82,109],asid:[76,104,109],ask:[1,24,25,27,29,33,35,38,39,40,56,60,62,66,69,71,82,103,104,106,111,126,130,139],askanc:103,asnyc:65,asp:24,aspect:[81,89],ass:106,assert:[16,25,40,44,45,46,51,52,66,84,101,106,107],assert_called_once_with:108,assert_equ:108,assertequ:[107,108],assertfals:108,assertin:108,assertionerror:[16,25,56,62,106,107,108],assertisclos:108,assertnotequ:108,assertrais:108,asserttru:108,assess:93,assid:54,assign:[7,13,23,24,28,35,37,40,41,51,54,59,62,67,72,73,80,84,85,92,98,105,106,108,110,112,123,128,130,136,155],assignment1:118,assist:91,associ:[44,62,69,75,78,84,87,122,139],assort:53,assum:[25,29,53,92,95,100,106,110,111],assur:[25,51,107,124,129,130,131],ast:94,asterisk:118,asterix:121,asychron:159,async:[49,65],async_executor:55,async_tim:55,asynchron:[49,63],asynchroni:63,asyncio:[49,55,65],asyncron:63,atla:89,atlassian:[118,121,126],atle:82,atom:[95,109,114,126,128],att:84,attach:[24,55,56,71,91,121],attempt:[71,95,111],attend:1,attent:30,attr:[24,84],attr_dict:84,attrgett:33,attri:51,attribut:[7,13,25,33,34,41,53,56,59,67,69,70,71,82,85,87,91,92,93,94,101,108,109,128],attribute:51,attributeerror:[13,24,25,43,51,56,62,69,85,96],audreyr:92,aug:139,augment:13,author:[51,81,84,92,93,121,123],author_email:92,authorit:[81,110],auto:[32,33,80,84,89,92,104,108,134],auto_ind:135,autocomplet:134,autom:[55,93,106,107],automat:[35,56,57,61,66,70,82,84,92,98,100,101,102,103,104,105,108,115,122,123,128,134,135,136,139,140],automaticali:82,avail:[2,33,34,44,48,66,81,84,85,89,92,94,100,102,106,107,108,109,110,111,115,116,120,126,128,129,130,131,134,135,139,140,141],availabl:89,avenu:30,averag:[28,95],avinash:67,avinashv:67,avoid:[25,39,71,78,85,92,93,115,131,134,135,140],awai:[15,25,32,34,44,55,62,63,65,93,106,123],await:65,awar:[35,100],awesom:92,awkward:107,ax:50,axialcorp:111,ay:111,azdwveidqji:71,b00kqtfhnk:81,b58f2f87ac0d:115,b5f7:115,b:[30,44,48,50,51,56,57,59,62,66,67,71,72,85,86,87,100,101,102,103,108,109,111,118,121,122,124],baaaaaaad:106,back:[7,14,24,25,27,30,38,40,44,45,51,55,56,59,63,65,66,79,89,92,94,95,103,105,106,107,109,110,111,115,118,122,123,124,128,131,136,137,139],back_color:12,backbon:55,backend:84,background:[2,50,100,107,117],backslash:103,backspac:103,backtick:140,backtrack:51,backup:121,backward:[15,57,92,106,111],bacon:100,bad:[24,25,34,38,56,64,71,79,88,93,94,124,134],bake:[59,80,94],balanc:88,balloon:70,banana:70,band:51,bang:27,bar:[56,73,84,92,94,103,106,134],bare:103,barf:111,barker:[23,37,51,52,54,67,80,103,106,107,121],barrier:109,bartend:103,base:[24,25,27,30,31,33,35,45,51,55,59,63,67,70,74,79,82,84,87,89,92,93,94,95,101,108,109,110,118,122,135],baseexcept:56,baselin:[93,108],basenam:[72,134],basestr:128,bash:[75,114,120,124,128,129,131,134,139],bash_profil:[89,130,131],basho:89,bashrc:129,basi:[27,29,134],basic:[0,1,17,19,25,27,28,35,37,46,51,53,59,61,66,69,70,73,81,84,94,104,106,107,108,110,117,122,123,126,128,131,132],bat:[14,15,57,97],batch:[56,139],batcheld:[93,108],baz:[73,94],bdist_wheel:92,bean:[94,100],bear:[7,24,71,78],beat:51,beauti:[18,24,52,93,107],beazlei:[55,81,109],becasu:79,becaus:[24,25,35,36,45,46,48,49,51,52,56,57,59,62,64,67,68,69,71,72,73,84,85,92,94,95,98,99,100,101,102,103,104,106,107,109,110,121,123,124,126,128,130,139],becom:[25,30,31,35,48,54,55,60,63,68,72,73,78,84,93,101,107,109,118,126,128,135],been:[2,16,21,23,25,28,30,51,52,55,59,62,63,64,66,71,73,74,82,83,84,85,88,89,90,92,100,101,103,105,106,107,108,118,122,123,126,129,134,136,139,140],beer:103,befor:[2,13,15,16,22,23,24,25,29,30,40,41,51,55,56,59,60,63,65,66,67,68,71,73,74,81,84,85,92,93,95,98,99,103,106,107,108,109,110,121,123,124,125,129],beforehand:30,began:[55,84],begin:[21,24,25,27,56,60,62,66,81,91,92,95,104,105,106,107,108,110,123,125,130,131,135,136,139],beginn:88,behav:[64,72,79,95,101,106,108],behavior:[57,69,71,91,96,97,100,104,108],behind:[48,55,56,59,87,128,134],being:[24,25,31,34,51,52,59,62,67,73,74,84,87,91,92,100,102,103,105,106,107,108,111,118,122,123,131,136,141],bel:103,believ:126,bell:103,belong:[24,33,52,91,126],below:[12,28,30,33,35,40,48,55,69,70,92,99,101,103,115,118,129,131,140],benchmark:95,benefici:106,benefit:[24,33,63,79,87,109,141],benign:100,best:[1,21,35,60,63,71,81,89,91,95,101,115,118,128,130,131,135,139,142],bet:[130,131],better:[23,24,25,33,38,40,51,57,60,62,65,67,71,73,79,83,84,85,88,90,91,92,93,94,97,99,100,101,102,103,106,107,108,109,111,116,126,131,139],between:[21,24,25,28,37,39,46,50,51,55,56,60,63,65,71,72,74,79,82,84,86,89,90,92,94,95,100,103,104,106,109,110,111,122,123,138,139],beyond:[47,82,93,94,100,111,135],bezo:[28,35,39],bfortun:108,bhgfvqr:44,bibliograph:106,big:[37,40,41,50,55,83,91,94,99,100,103,106,111,115,130,135],bigger:[13,15,25,60],biggest:[24,92,104,111],biggi:[25,111],billion:108,billionth:92,bin:[17,27,51,72,75,85,89,92,95,109,120,130,134,135,137,139],binari:[20,57,89,94,95,110,111,120],bind:[59,67,69,73,89,92],bingo:100,bird:[87,131],birthgiv:87,bisect:95,bit:[2,6,13,14,15,16,19,20,21,24,25,26,31,33,34,36,37,39,44,47,49,51,52,55,56,57,59,61,64,65,66,67,69,70,72,73,77,78,81,83,84,85,87,89,91,93,95,97,98,100,101,106,107,108,110,111,121,123,124,126,127,128,131,133,134,142,151],bitbucketserv:126,bitwis:56,black:[51,108,118],blah:[70,71],blank:93,bleepingcomput:116,blend:91,blob:[15,79,84,87,89],block:[16,18,25,28,34,35,41,44,45,51,52,57,62,63,64,66,68,69,70,71,72,73,78,85,102,106,107,108,109],blocksiz:102,blog:[24,48,55,63,66,67,73,81,82,89,91,92,103,108,118,121],blogger:[51,81],blogspot:[6,9,12,81,87,104],blow:111,blown:63,blue:[12,104,108,118],blurb:81,bna:95,bna_read:95,bob:[35,39,54,84,85,103,128],bodi:[24,25,55,67,70,78,79,84,85,95,106,108],bogu:[28,106],bohemia:51,boi:[8,51],boilerpl:[84,108],bolt:74,boltprotocol:74,bone:128,bonu:27,book:[8,51,63,79,89,91,94,104,116,122],bookkeep:64,bookmark:81,bool:[94,101,108],bootstrap:142,boranga:70,border:102,bore:[28,33],borrow:[31,55,84,90],both:[13,20,22,24,25,30,34,35,40,45,50,51,55,57,59,63,67,69,74,78,81,84,85,87,92,93,94,97,98,100,104,106,107,108,109,111,118,121,123,124,128,131,141],bother:[57,106],bottleneck:[63,95],bottom:[66,67,85,87,106,131,140],bounc:106,bound:[48,56,59,63,71,73,100,102],boundedsemaphor:63,box:[55,106,108,115,124,128,129,130,131,140],bpnumber:66,br:[24,25],brace:134,bracket:[25,56,62,103,134],branch:[30,71,119,122,123,134],branch_name1:118,brand:[84,118],brave:63,bread:100,break_m:43,breakfast:15,breakpoint:66,breed:81,brian:[69,100],brilliant:86,bring:[59,85,87,92,122,123,124,135],british:90,brittl:107,brnach:48,broadli:94,broke:[25,106],broken:[24,25,106,107],brows:62,browser:[24,31,32,55,81,84,94,105,110,118,122,124,136,141],bruce:94,brush:[1,87],bs:103,bsd:72,bson:[89,94],bubbl:[66,95],bucket:69,buffer:[72,100,109],buffererror:[56,62,71],bug:[24,33,35,57,60,66,71,85,92,93,100,106,111,115,124,140],buggi:59,bui:[92,94],build:[15,23,24,25,28,30,33,34,37,42,52,54,55,56,62,78,79,81,82,87,89,94,98,100,102,104,106,108,110,128,131,138,139],build_text:51,build_trigram:51,built:[6,25,30,37,43,47,49,50,51,53,55,56,59,61,62,65,66,69,72,82,83,92,94,98,100,101,103,106,107,108,122,128,130,139,140],builtin:[56,61,64,67,78,84,85,92,100,128],bullet:[24,93],bumpvers:92,bun:100,bunc:100,bunch:[13,21,24,25,33,37,48,49,50,51,52,56,59,62,68,69,84,87,89,92,100,103,106,107,110,135],bundl:[28,32,33,92],buri:85,burn:63,busi:[16,33,40,88,89],button:[55,74,115,118,124,131],buzz:[2,142],buzzword:89,bx:[50,101],bye:35,bypass:108,bytearrai:[100,111],bytecod:[63,85,103],c03bb5b2c401c:121,c03bb5b:121,c1:[13,59,98],c1f9ac3b6fe:100,c2:[13,22,59,95,98],c3:13,c816927c2fb8:56,c83386d97be3:96,c9a57e8c0d14:59,c:[13,15,19,50,55,56,57,59,63,67,68,76,84,85,86,87,89,92,94,96,98,100,101,103,104,106,109,111,120,122,126,131,139],ca:82,cabin:51,cach:[29,95,129],cake:[15,17,54],calcul:[13,30,56,70,95,96,98,108,110],calculator_funct:108,calculator_test:108,calculator_test_suit:108,call:[12,13,18,23,24,25,28,30,33,34,35,40,41,42,43,44,45,48,51,52,54,57,58,59,61,62,63,64,65,67,68,69,71,72,73,74,75,79,80,82,84,85,86,87,90,91,92,93,95,96,97,98,99,100,101,102,103,104,106,107,108,109,110,111,115,118,121,122,124,126,128,129,130,131,134,137,139],call_a_func:55,call_at:55,call_count:108,call_handl:55,call_lat:55,call_soon:55,call_soon_threadsaf:55,calla:33,callabl:[26,55,56,79,84,91],callable_inst:101,callback:[63,80,109],calle:87,called_onc:109,caller:87,callgrind:95,calltre:95,came:[51,59,71,82,92,111],camelcas:93,camp:[90,100],can:[12,13,14,15,16,18,19,20,21,22,23,24,25,27,28,30,31,32,33,34,35,36,38,39,40,41,42,43,45,46,47,48,50,51,52,53,55,56,57,58,59,60,61,62,63,64,65,66,67,68,70,71,72,74,75,76,77,78,80,81,82,83,84,85,86,87,89,90,92,93,94,95,96,97,98,99,101,102,103,104,105,106,107,108,109,110,111,114,115,118,120,121,122,123,124,125,126,127,128,129,130,131,134,135,136,137,138,139,140,141],canada:90,cancel:[19,55,109],candid:[30,33,36],cannont:79,cannot:[46,56,62,102,106,111],canopi:[92,131],cant:129,canva:[87,124],cap:93,cap_data:42,cap_script:42,capabl:[30,41,107,109,127],capit:[15,33,42,44,50,51,88,92,103],capital_mod:42,captur:[25,51,57,71,95,106,107,108],car:51,card:104,care:[69,73,78,85,86,94,97,99,100,103,104,106,124,129,135,139],carefulli:[25,59,66,74,78,107,115,123],cargo_weight:88,carpent:1,carpentri:[1,116],carri:[15,56,59],carriag:[51,103],casetti:81,cassandra:89,cat:[87,123],catagor:82,catagori:74,categori:[89,91],caught:[24,66,71],caus:[25,30,43,46,56,63,66,72,73,78,87,100,103,106,107,108,122,135,139],caution:[24,69,94,106,141],cautiou:107,caveat:[92,100],cc:[84,100],cccc:106,cccxii:106,ccxciv:106,cd:[43,93,94,106,110,123,124,127,130,131,134],cdcd:106,cdll:95,cdxxi:106,ce:66,cef0e50bb96d:57,cell:23,cell_phon:89,center:[24,25],central:[82,95,126],centric:106,centuri:115,cereal:15,certain:[18,55,56,66,74,90,98,106,108,122],certainli:[18,50,51,106],certainti:106,certif:[53,112,122,124,128],certitud:106,cew:139,cfg:94,cgi:[22,95],ch000549:131,ch:100,chain:82,chainmap:61,challeng:[8,21,25,30,47,50,55,81,106,122,124,126,128,138],cham:126,chanc:[12,15,33,55,77,106],chang:[13,17,18,24,25,27,29,33,35,40,45,48,51,52,56,59,60,66,68,69,73,74,82,84,85,86,89,92,93,94,95,98,99,100,103,104,105,106,107,109,110,115,118,122,124,126,129,134,136,139],channel:[55,82],chao:[111,139],chapter:[2,3,4,51,67,79,122],char_nam:103,charact:[44,46,48,51,56,72,93,95,100,106,110,111,121,122,128,134,135],charactor:106,charfield:84,charg:7,chariti:28,charset:[24,25],chartreus:12,chdir:72,cheap:[63,92,106],cheat:55,check:[13,25,33,45,50,51,56,57,59,64,66,69,71,73,74,84,87,92,93,96,98,100,102,104,106,108,110,111,115,118,121,122,123,124,126,130,131,134,139,140],checkbox:131,checkout:[118,121,122,123,124],chees:18,chicken:63,child_conn:109,childb:87,chimera:81,chmod:[17,27,75],chocol:[15,17,24],choic:[1,30,51,78,94,107,117,131],choos:[20,28,33,39,51,63,70,82,91,92,115,128,131,133],chop:81,chore:134,chose:51,chosen:[95,106,126],chr:[44,103,111],chri:[15,17,23,25,37,51,52,54,67,72,80,88,101,103,106,107,108,110,121],christoph:106,chronolog:30,chunk:[50,56,60,63,73,79,109],chunksiz:109,ci:[93,106],circl:[8,44,68,88,91,94,98,102,104,154],circler2:104,circler:104,circumst:[1,18],cite:[81,110],citi:[15,17,54],cito:103,cl:[67,84,93,102,106],cla:[24,106],claific:126,clang:[130,139],clarifi:[45,64],clariti:96,classi:102,classic:[16,28,51,55,57,79,81,83],classmat:[60,105,124,136],classmethod:[13,67,102],classrepo:[123,126],classroom:[18,27,44,105,118,119,121,122,126,128,130,136,138],classwork:123,claus:[24,66,71],clean:[18,19,32,36,51,56,59,64,71,79,82,87,91,92,93,104,107,108,118,124,135],cleaner:[25,32,52,54,55,57,80,103,111,127],cleanest:118,cleanli:[28,33,35,104],cleanup:[64,71,109],clear:[24,25,26,33,35,41,48,52,55,60,62,66,72,78,80,81,85,87,99,103,104,106,111,118,139],clearer:[60,84,103],clearli:[12,62,90],cli:[33,34],click:[32,55,74,115,118,124,128,129,130,131],client:[25,49,74,76,89,92,128,130,131],clipboard:[124,127],clobber:82,clock:16,clone:[17,27,44,121,123,126,139],close:[16,24,25,32,50,59,63,71,72,89,94,107,118,131],closer:[25,103],closur:[67,84,157],cloud:[30,89,90,122,124],cloudbe:121,clr:[110,128],clsname:84,clue:139,clunkier:75,cluster:[30,76],clutter:[25,59,82,85,96,106,107],cm:106,cmc:106,cmcm:106,cmd:[124,131],cmm:106,cmu:74,cmxli:106,co:[8,50,85],coalesc:90,coars:95,cocksur:106,code:[1,2,6,7,12,13,14,15,16,18,19,20,21,22,23,24,25,31,32,33,35,36,38,39,41,42,44,45,47,48,49,50,52,57,59,62,63,64,65,66,67,68,69,70,71,72,73,74,75,77,78,79,80,81,82,84,87,88,89,91,92,94,95,96,97,98,99,100,101,104,105,109,111,112,114,116,117,118,122,123,126,128,130,131,133,134,135,136,150,159],code_modul:108,codec:[72,92],codecademi:[81,110],codekata:[51,79],coder:[81,92],codingbat:[15,52,57,100,142],coef:50,coeffici:50,cogsci:95,colbert:57,collabor:[1,105,121,122,123,128,136],collaps:104,collect:[5,24,25,33,35,51,52,55,62,69,78,79,85,88,89,90,91,92,95,100,102,106,107,108,109,126,129,139,152],colon:[21,56,66,85,103,124],color:[1,12,24,62,98,104,108,115,118,128,134,135,140],color_off:134,color_schem:135,columbia:90,column:[23,28,48],com:[2,3,6,7,9,12,14,15,22,24,25,41,51,52,53,55,61,62,63,64,66,71,72,73,74,79,81,82,84,86,87,89,92,94,95,103,104,107,108,109,110,111,116,118,120,121,122,123,124,126,128,130,131,134,135,138,141],combin:[12,23,35,62,83,87,93,100,103,106,109,111,122,128,134],come:[2,14,16,18,25,30,33,40,56,59,64,66,74,75,82,84,90,93,94,95,97,100,101,106,107,108,109,110,117,128,129,130,134,135,139],comfort:[1,2,118,128,131,134,141],comit:121,comma:[56,94,103,110,135],command:[1,17,20,27,28,32,34,51,52,63,66,70,72,74,75,106,107,108,110,115,117,120,121,122,123,124,126,127,129,130,131,132,134,135,137,139,140,141],commandlin:141,commenc:[51,74],comment:[24,30,43,45,51,56,60,82,84,93,94,107,123,124],commerc:90,commerci:[74,92,126,128],commit:[17,27,44,45,48,94,105,118,120,122,135,136],common:[13,18,24,31,43,51,53,55,57,59,62,63,65,67,68,69,71,73,74,78,79,82,84,85,87,90,92,94,98,100,101,102,103,104,107,108,109,111,121,139],commonli:[67,72,84,92,94,110,128],commun:[24,31,34,55,63,66,82,84,88,89,92,93,107,109,122,124,126,130],comp:[15,62,86],compact:[50,57,59,62,67,80,99,111],compani:88,companion:51,compar:[13,40,69,106,108,118,124],comparison:[35,56,95,101,106,108],comparison_of_unicode_encod:111,compat:[57,66,72,89,92,94,111,139],compel:93,compet:109,compex:87,compil:[59,84,85,103,110,128,130,139],complain:25,complaint:111,complet:[1,10,25,32,33,34,41,44,48,51,52,55,61,63,69,70,73,77,79,84,93,94,101,103,104,105,106,107,109,110,112,115,122,127,128,134,135,136,138,139,140],complex:[23,25,33,35,56,61,62,70,74,84,85,93,94,100,101,104,106,107,108,121,122],complex_funct:70,complic:[31,34,35,66,71,72,87,92,103,104,110,111,134],compon:[24,35,40,55,56,63,71,108],compos:[28,84],composition_over_inherit:104,comprehens:[0,24,25,26,50,53,95,106,108],compress:[57,124],compromis:89,comput:[13,15,17,22,24,30,33,47,49,51,55,59,62,67,72,73,74,81,82,84,92,94,95,101,103,106,108,110,111,122,123,124,125],computation:[47,63],computeres:103,computerhop:131,computersciencewiki:69,con:66,concaten:[23,46,101,103],conceiv:25,concentr:59,concept:[1,24,30,40,48,53,57,59,62,74,83,88,94,101,103,104,107,111],conceptu:[30,50,79],concern:[33,40,91,93,97,104],concis:[44,45,57,100,103],conclud:107,conclus:18,concur:63,concurr:[55,109,159],conda:[89,92,106],condit:[16,56,59,62,63,66,78,100,106,107],conf:92,confer:139,confess:64,confid:[103,106,131],config:[74,94,115,126,135],configur:[27,34,74,82,84,88,92,93,94,107,109,115,120,124,126,134,135,138,140],configuration_kei:73,confirm:[13,108],conflict:[85,122,139],confluenc:126,conform:[33,79,93,94,135],confus:[12,46,50,54,55,56,68,72,85,86,92,93,98,103,105,107,111,123,126,130,134,135,136,142],confusingli:[109,123],congrat:25,congratul:67,conjunct:122,conn:94,connect:[49,55,64,71,74,89,94,109,121,129,130,131],conqueringthecommandlin:116,consecut:48,consensu:83,consequ:98,consid:[17,30,33,35,48,51,54,55,57,59,62,63,64,67,69,79,83,84,85,87,88,90,91,100,102,106,107,108,109,111,134,139],consider:87,consist:[24,33,51,87,88,92,93,95,108,111,138],consol:[24,33,34,55,82,92,106,108],console_script:[34,92],consructor:102,constant:[33,51,69,73,92,93,95,100],constantli:135,constel:90,constrain:[51,91],construct:[36,56,62,68,89,90,94,100,106,134],constructor:[13,24,25,57,62,71,84,97,100],consum:[72,95,109,128],consume_item:109,contain:[15,17,24,25,26,27,33,35,40,41,53,55,56,61,62,63,66,69,71,72,73,74,79,80,84,85,86,94,95,100,106,107,108,109,110,118,121,139],contamin:59,contemporari:55,contenet:24,content:[20,24,25,27,39,40,62,64,66,72,86,97,106,111,122,137],context:[25,56,62,63,65,71,72,98,106,107,108,109,159],contextlib:[16,64],contextmanag:16,continu:[35,52,53,55,56,66,71,78,85,92,124],continuum:92,contract:[41,51],contradictori:7,contrari:51,contrast:[55,56,74,78],contrib:134,contribut:30,contriv:[87,100],control:[28,30,57,63,65,66,70,71,76,77,80,84,92,94,100,103,106,107,109,110,118,121,122,123,124,130,135,140],contruct:84,conveni:[48,94,103],convent:[25,30,52,59,70,72,73,78,92,98,107,121],convention:48,convers:[56,95,106,124],convert:[28,37,56,69,71,88,94,95,101,102,103,106,110,111,139],cookbook:[48,81,82,103],cooki:[55,92],cookiecutt:92,cool:[13,25,56,59,84,87,94,100,101,102,108],cool_meta:84,coolclass:84,coolmeta:84,cooper:[55,63,87],coordin:[121,123],copi:[6,15,20,24,25,27,30,34,46,48,51,63,66,69,77,79,81,83,84,89,92,94,96,98,101,106,123,124,126,135,151],copyright:[106,110,127,128,129,130,131,137,139],core:[1,11,24,31,55,63,74,81,82,84,91,92,95,98,99,109,114,115,125,126],corepython:81,corner:108,corout:65,coroutin:[63,159],correct:[21,24,25,27,33,40,51,57,60,84,95,106,107,108,115,121,123],correctli:[12,25,27,35,40,51,94,106,108,121,123,130,137],correspond:[27,55,92,100,106,134],corridor:109,cosin:85,cost:[48,63,74,95,109],couchdb:94,could:[13,15,16,24,25,29,30,33,44,45,48,49,50,51,52,55,57,59,61,63,64,67,71,72,79,80,82,84,85,87,92,94,95,100,101,103,104,106,121,124,129,134,139],couldn:[25,64,71,72,83,87,109],count:[25,56,59,61,63,79,93,95,97,103,108,109,124],count_even:15,counter:[59,61,109],countless:24,countri:90,coupl:[12,13,23,24,25,32,35,51,56,59,60,62,64,69,75,77,84,91,92,95,97,101,103,109,111,118,130,134,139,159],cours:[8,24,25,28,30,33,35,40,50,52,53,55,56,59,62,63,65,66,81,82,84,100,101,103,106,107,110,111,116,117,124,126,128,130,131],coursework:124,cov:[32,107,108],cover:[25,53,56,62,73,79,81,96,106,107,108,128],coverag:[33,77,93,106],cp35:95,cpu:[63,95],cpython:[63,69,95,110,117,128],cr:[55,65,103],crack:106,crap:82,crash:[18,28,38,46,66,71,116,128],crawler:55,crazi:[71,123],creat:[1,7,13,15,16,17,19,24,25,27,30,33,34,36,39,41,42,43,44,45,48,50,51,55,56,59,61,62,63,64,65,66,67,69,71,72,74,77,78,79,80,82,83,85,86,91,92,94,95,98,99,100,101,102,104,105,106,107,108,110,111,115,121,122,126,131,134,136,137,139],create_task:55,creation:[1,40,84,124],creativ:[53,62],credenti:74,credit:[15,26,110,123,127,128,129,130,131,137,139],crew:86,criteria:99,critic:[35,40,63,73,74,82,84,92,106,109,128,130,137,139],cross:[63,90,106,109,128],cruft:59,crufti:92,crunch:63,cs:[59,74,106],cshop:92,cson:115,css:24,csv:103,ctrl:[110,123,128,130,131,135,140],ctype:95,cube:[59,90],cultur:93,cumbersom:108,cumtim:95,cumul:95,cupcak:70,cur_ind:[24,25],curat:92,curi:[123,126],curio:55,curiou:106,curl:134,curli:[62,103,134],current:[20,23,25,28,30,32,39,52,59,63,66,72,75,79,80,85,89,95,98,103,105,106,107,108,118,121,122,123,124,128,131,134,136,139],current_fund_pric:88,current_thread:109,currentfram:66,curri:[24,157],cursor:64,curtain:56,curv:[50,66],custom:[1,18,23,25,35,50,61,71,80,82,84,87,92,94,98,103,108,114,115,139],customari:106,customiz:[115,139,140],cut:[44,66,67,90,109],cutter:92,cwd:101,cxlviii:106,cyclomat:93,cypher:44,cython:95,cywkf4rbgf2swtkp:74,d8100c70edef:56,d:[13,19,25,37,48,51,54,55,56,57,59,62,67,69,72,78,79,82,86,87,93,94,96,100,101,102,103,106,109,110,111,123,124,128,130,131,139],dabeaz:55,daemon:109,dai:[24,29,60,62,72,83,84,89,92,94,103,106,107,108,110,111,124,127,135],daili:[134,135],damnwidget:135,danc:106,danger:[85,94,124],danlimerick:120,dark:115,darn:[2,25,61,87,106],darwin:[25,51,52,106,107,110,128,130,139],dash:121,data2:94,data:[21,24,25,28,30,31,32,33,37,38,39,40,41,42,47,48,49,51,55,56,57,62,63,66,69,71,72,74,79,82,83,84,88,91,94,96,98,101,106,109,111,113,128,139],data_aggreg:95,data_dir:92,data_fil:71,data_q:109,databas:[29,30,31,32,34,39,55,63,64,71,83,84,95,111,159],dataclass:[41,98],datafile2:92,datafram:90,datamodel:[84,101],dataset:[31,94,109],datastor:89,datatfile1:92,datatyp:[4,31,84,110],date:[39,55,62,70,83,92,94,106,118,121,123],datetim:[55,82,102],dave:51,david:[55,81,109],db455296be:130,db:[33,84],dbader:67,dbpath:89,dc4f30fe2ac4:59,dc:106,dccclxx:106,dcclxxxii:106,dcm:106,dct:84,dcxxi:106,dd:[61,106],de:[31,84],dead:[8,51,81],deadli:100,deadlock:63,deal:[24,33,50,55,63,64,68,72,82,91,94,103,106,111,115,128,134,139],dear:39,death:18,debat:[91,92],debian:140,deborah:91,debug:[25,35,56,62,67,71,82,92,95,106,140,159],debugg:[115,135,140],dec:[69,121],decad:[89,91],decai:59,decemb:[69,103],decid:[18,23,24,25,28,30,33,38,51,67,84,87,88,98,104,118,121,122,139],decim:[48,95,103,106,108,110],decis:[35,92,108],deck:104,declar:[59,67,69,85,92],decod:[72,94],decor:[16,31,95,101,108,109,111,113,159],decorator_on:67,decorator_two:67,decreas:106,decrement:109,decrypt:44,dedic:106,deem:124,deep:[24,25,66,73,85,86,99],deepcopi:86,deeper:[18,35,73,81,126,140],deepli:[24,85],def:[15,18,24,25,33,35,40,46,48,50,51,52,54,55,57,59,62,64,65,66,67,68,70,71,73,79,80,83,84,85,86,87,89,92,93,95,96,98,99,100,101,102,104,106,107,108,109,111],defacto:107,defaultdict:61,defect:56,defens:64,defin:[2,7,12,13,18,24,25,33,34,35,41,45,49,50,51,54,55,56,57,59,62,64,66,67,69,70,72,73,80,82,84,85,86,87,89,90,91,92,93,94,95,96,98,99,100,101,102,103,104,106,107,108,134],definit:[25,33,50,56,59,65,66,69,70,73,80,84,90,92,99,103,106,108,115,128,135,140],definiton:12,defint:93,degrad:63,del:[47,56,67,80,94,96,100],delai:[55,83,109],delattr:84,deleg:[55,87,104],delet:[17,27,47,66,67,69,71,80,82,84,94,100,115,118,124],deliber:[25,51],delicaci:15,delimit:[56,94],delin:[56,103],delta:124,delx:67,demand:51,demo:[55,84,108,128],demoenv:139,demonstr:[25,44,45,56,77,101,106,107,108,139],depend:[16,24,50,55,59,68,71,74,84,87,92,93,95,100,106,108,117,121,124,130,139],deploi:[92,94,124,138],deploy:82,deprec:92,depth:[2,56,99],dequ:[61,100],deriv:[25,66,104],descend:[87,106],describ:[1,24,34,66,70,92,95,104,106,123],descrintro:84,descript:[41,44,51,70,74,79,81,89,90,92,100,107,110,124],descriptor:102,design:[2,16,33,36,41,51,55,59,63,73,74,79,84,94,104,105,107,109,131,136],desktop:[33,55,128,131],despit:[7,63,102],dest:72,dest_dir:139,destin:[20,92,108],detail:[2,33,37,51,69,72,82,84,85,91,92,93,96,97,101,103,104,105,106,107,124,125,127,128,129,135,136,137,140],detect:66,determin:[25,35,45,50,56,57,59,63,67,73,74,81,82,84,85,87,93,95,106,108,109,139],determinist:[63,90],dev:[39,50,54,57,68,69,72,77,88,89,92,93,94,103,107,108],devel:92,develop2:124,develop:[1,4,7,18,22,24,25,28,30,34,41,42,48,50,53,55,56,60,61,66,68,74,82,85,91,96,105,110,111,114,115,122,123,126,130,135,136,138,139,140,147],development:30,devot:15,diagnost:107,dialect:[24,94],diamet:[13,98,102,104],diamond_sup:87,dict:[4,24,25,33,34,47,57,59,61,79,80,84,86,89,92,94,95,100,102,135,148],dict_comprehens:15,dict_item:69,dict_kei:[51,59,69,92,97],dict_lab:17,dict_of_weapon:15,dict_valu:69,dictionari:[0,31,33,34,37,41,51,53,59,61,62,92,94,100,102],did:[25,30,33,49,51,54,57,62,63,64,66,73,79,84,85,98,103,106,121,123,139],didn:[25,38,65,69,71,78,84,93,97,99,106,107,110,111,124],diederich:[7,84,88],diff:[107,123],differ:[2,12,21,24,25,29,30,33,39,45,46,50,51,52,55,56,59,62,63,65,66,67,68,69,71,72,78,79,80,82,84,85,86,87,88,89,90,91,92,94,95,98,100,103,104,106,107,108,110,111,113,117,118,121,122,123,124,128,131,133,134,135,138,139],differenti:[79,90],differnc:86,difficult:[25,63,106,109],difficulti:60,dig:73,digit:[48,95,103,108],digitalcitizen:116,dimension:[47,90,101],dinner:81,dir:[24,31,32,34,43,51,56,62,64,66,71,72,79,85,89,92,98,101,108,110,121,123,124,128],direct:[69,84,89,99,101,108],directli:[12,13,17,24,25,27,31,33,53,55,56,62,63,65,69,71,74,75,84,85,94,104,106,110,123,126,129,130,131,134,139],directori:[1,20,24,25,30,32,39,43,51,52,64,71,74,85,92,107,108,110,120,124,126,134,139],dirnam:72,dirti:103,disabl:[66,93,110],disallow:93,disappear:134,disappoint:103,discard:[121,124],disciplin:106,discourag:[59,66],discov:[87,106,108],discoveri:[92,107],discreet:28,discret:108,discuss:[2,62,84,86,87,90,92,106,108,121],disk:[39,40,63,72,94,95,111,131],disorgan:30,disp:66,dispatch:[67,68],dispers:121,displai:[17,27,48,66,69,85,95,110,118],display_d:55,display_report:40,distinct:[34,69,75,90,94,98,104,110,111],distinguish:106,distraught:103,distribut:[85,89,107,121,126,128,131,135,139],district:18,distro:92,distutil:92,div:67,dive:[2,3,4,7,81,82],diveintopython3:[2,3,4,7,106],diverg:95,divid:[50,71,89,97,110],divis:[13,15,17,56,57,66,72,79,101,110],divisor:15,divmod:102,django:[67,74,84,89,92,93,94,107,108],djangoproject:94,dlhcuy23h:55,do_noth:85,do_other_stuff:72,do_some_clean:71,do_some_mor:72,do_someth:[35,57,71,88,100,101],do_somethign_with_grid:68,do_something_bad:56,do_something_els:104,do_something_here_to_make_a_format_str:48,do_something_to:36,do_something_with:[71,79,88],do_something_with_item:79,do_something_with_lin:72,do_something_with_oth:104,do_something_with_til:68,do_stuff:79,do_your_stuff:87,doabl:106,doc:[6,16,24,33,41,48,51,55,57,59,61,62,63,64,66,67,69,71,72,74,79,81,82,84,87,89,92,94,95,100,101,103,105,107,108,109,110,136],dock:[128,130],docleanup:108,docstr:[33,41,45,62,84,106,108],doctyp:[24,25],document:[2,5,24,25,33,44,51,53,55,59,61,72,74,76,81,82,87,89,92,93,94,106,107,108,111,115,122,123,127,139,150],docutil:[92,139],doe:[1,12,15,23,24,25,33,35,36,40,42,46,49,51,52,55,56,57,59,62,63,65,66,67,68,69,70,71,73,75,78,83,86,91,92,93,95,97,98,100,101,103,104,106,107,108,109,110,111,115,118,121,122,123,124,126,128,130,134,135,139,141],doer:56,doesn:[12,24,25,27,33,41,46,51,55,64,65,66,67,69,71,72,80,82,84,85,86,87,89,91,92,93,94,95,98,101,102,103,106,107,109,121,124,129,130,131,134,141],doesnot:79,dollar:30,dom:94,domain:95,don:[7,15,17,18,20,23,24,25,29,30,33,35,36,37,39,40,51,55,56,57,59,62,67,68,71,72,74,75,78,80,82,85,88,89,91,92,93,94,95,96,97,98,100,101,102,103,104,106,107,108,109,111,123,124,126,128,129,130,135,137],donald:95,donat:[28,30,33,35,38,39,123],done:[0,2,8,13,14,18,27,29,31,33,34,40,41,44,48,51,52,55,56,58,62,63,65,71,72,73,77,79,81,85,92,94,95,98,103,106,107,108,109,118,121,123,124,127,128,130,131,135,139,141],donor:[28,29,30,31,32,34,35,36,37,39,40],donor_db:35,donor_model:33,dont:126,door:[24,40],dot:[82,85,87,92,98,110,124,128,131],dotsb:50,doubl:[30,50,56,59,79,97,101,103,108,110,130,131],doubler:79,doubt:123,doug:[64,93],down:[1,25,33,34,46,51,55,63,66,73,81,87,90,99,100,103,105,106,107,109,111,115,123,124,136,140],downei:23,download:[9,16,21,24,25,31,52,84,89,104,107,115,120,130,131,134,139,140],downsid:25,dp:81,draft:60,drag:[128,130],dramat:106,drastic:97,draw:[23,68,87,95],draw_a_circl:68,draw_a_polygon:68,draw_a_squar:68,draw_noth:68,draw_white_spac:135,drawn:81,drawobject:87,drill:[25,90],drink:103,driscol:81,drive:[50,71],driven:[4,24,25,30,41,55,63,147],driver:[74,89,94],drop:[66,89,115,128,139],dropdown:118,dry:[15,24,25,33,56,64,91],dtype:101,duck:[25,87,91,100,101,104],due:[25,30,62,63,66,77,84,90,106,107],dug:51,dummi:84,dump:[24,94,95,120],dunder:[58,84,101],duplex:55,duplic:[21,56,87],durat:[55,95],dure:[28,51,60,63,74,106,107,108,111,131],dusti:8,duti:82,dx:109,dxxviii:106,dylan:51,dylib:95,dynam:[50,54,55,56,57,84,87,88,89,103,106,107,134],e:[12,15,17,20,24,25,26,33,34,51,55,62,63,64,66,77,79,80,84,87,91,92,94,100,103,105,106,107,108,109,113,124,136],e_traceback:64,e_typ:64,e_val:64,each:[12,13,15,17,21,23,24,25,26,27,28,30,33,34,35,39,41,43,44,45,46,48,49,50,51,52,53,55,56,59,60,61,62,63,64,65,66,67,68,70,71,79,80,81,82,83,85,89,90,92,93,95,98,99,100,101,103,104,105,106,107,108,109,110,111,117,118,121,122,123,124,126,128,133,136],eadh:85,eafp:[24,25,104],eaftp:38,earli:[40,44,57,62,79,84,93,100,103,106,111,121,123],earlier:[25,30,33,52,53,67,79,106,124],earth:110,eas:[35,92,115,140],easi:[25,33,35,37,40,63,66,70,73,74,79,82,91,92,93,98,99,100,103,106,107,109,111,118,121,123,126,128,140],easier:[24,25,33,34,38,48,49,51,56,60,61,62,66,71,85,88,89,91,93,94,100,101,103,106,108,109,111,118,124,128,131,138],easiest:[50,59,63,92,109,130],easili:[30,33,67,74,92,106,108,109,124,130,134,139],easy_instal:[92,139],eat:56,ebook:[51,81],ebug:82,echo:[129,131,134],eclips:66,edg:[74,106],edit:[1,15,25,34,52,66,74,75,89,92,94,106,110,117,122,123,124,126,128,131,135,141],editor:[1,14,24,27,52,56,74,85,88,93,117,123,124,131,133,137,141],edu:[66,74,92,95,128],educ:[53,128],edx:124,effbot:[],effect:[12,62,85,90,92,105,118,121,136,139],effici:[15,35,37,47,48,50,55,62,68,69,74,79,85,89,95,103,111],effort:63,egg:[15,87,92,94,100],egg_info:139,egglay:87,eggs2:94,ego:25,eh:[13,54,72,80,84,101,139],ehlo:108,eight:[25,51,85,100],eighth:77,eioglterpeo:87,either:[13,15,16,20,24,25,30,31,34,35,40,50,51,54,55,56,57,61,63,66,67,69,72,79,82,90,92,95,100,106,109,110,111,118,124,128,130,134,137,139],el:25,elaps:[16,67],eleg:[35,68],element:[26,30,33,45,46,47,48,51,62,69,80,86,94,101,102,107,117,124,128,134],eli:84,elif:[18,35,56,106],els:[13,24,25,35,37,39,40,47,51,55,56,57,65,68,69,73,79,80,84,85,87,90,92,95,100,101,103,104,106,107,118,120,123,124,128,134,135,137],elsewher:[59,79,90,121],emac:[128,141],email:[28,33,40,66,77,89,105,124,126,136],embarrassingli:30,embed:[25,56,94,103],embelish:56,embrac:79,emerg:121,emphasi:87,empir:106,empti:[23,24,25,36,46,48,51,57,61,62,66,69,85,86,92,94,100,103,106,109,131,134],emptysqua:63,emul:[47,79,129],en:[6,9,16,33,34,50,55,59,60,63,72,74,81,82,84,87,90,91,92,94,96,101,104,108,109,110,111,122,127,128,129,141],enabl:[66,84,95,129],enb:66,encapsul:[33,55,64,91,98],enclos:[16,25,51,59,70,107],encod:[24,72,92,94,103,110],encodingerror:72,encount:[51,92,103,108,121],encourag:[53,60,74,88,93,98,124],end:[0,15,19,23,24,25,27,28,30,33,45,47,50,51,56,61,64,66,69,71,72,74,77,78,79,85,87,88,89,92,94,97,100,103,106,107,108,109,110,123,124,134],end_of_the_block:56,end_tim:55,endeavor:[105,136],endless:71,endswith:25,enforc:85,engag:[81,110],engin:[76,89],english:[56,72,103,111],enhanc:[24,39,41,66,70,88,110,127,129,130,131,134,137],enjoi:63,enorm:95,enough:[2,24,25,51,59,60,62,64,77,80,82,85,93,95,100,106,107,110,111,121,122,134],ensur:[12,45,63,70,74,91,95,100,106,123,129,131,135,140],ensure_futur:55,ensurepip:[129,130,131],enter:[19,59,66,74,79,85,87,92,134,135],entertain:[81,110],enthought:131,entir:[12,15,16,20,24,25,31,40,59,62,63,73,79,82,84,85,90,92,94,95,100,106,108,121,124,134,135,139,141],entri:[17,34,35,51,55,61,63,82,89,100,109,121],entry_point:[33,34,92],enumer:[78,79,88,124],env:[17,27,51,75,85,109,134,137,139],environ:[0,53,63,66,90,92,95,106,108,109,110,114,122,124,131,134,135,138,139],environment:[134,139],envrion:139,eof:19,eoferror:[19,62],eol:103,epel:129,epydoc:108,equal:[13,30,67,73,90,94,101,106,108,109,110],equat:[50,101],equiv:79,equival:[15,68,69,84,85,106,135],err:71,errand:90,errno:71,erron:94,error:[24,25,33,35,38,51,54,55,56,57,62,64,66,72,73,78,82,87,92,93,97,100,103,106,107,108,111,122,123,124,126],es:87,escap:[56,103,111,124],especi:[55,63,67,73,81,93,99,100,109,134,135],essenc:124,essenti:[20,24,55,69,84,87,88,89,92,94,97,98,104,107,108,110,121,124,131],establish:[66,73,92,93,99,104,106,109,121],etc:[1,6,12,13,17,24,25,33,39,40,55,62,67,69,71,72,75,79,80,81,82,83,84,87,89,91,92,94,95,97,100,101,102,103,104,107,111,117,123,124,126,128,130,131,140,141],etre:94,eu:[8,101],european:111,eval:[13,94,101],evalu:[16,26,48,50,54,56,57,59,62,66,78,80,83,94,100,101,103,110,122],evaluat:108,evalut:66,even:[12,23,24,25,31,33,34,40,41,46,55,56,57,59,65,66,68,71,72,74,78,79,80,83,84,86,87,90,91,92,93,94,95,98,100,103,104,106,107,108,111,121,122,124,128,134,135,139],event:[63,65,82,87],event_loop:55,event_queu:55,eventu:[29,67,82,106,108,139],ever:[24,56,59,66,100,106,109,123],everi:[2,13,24,33,40,46,51,56,59,62,63,66,68,69,73,84,86,87,89,92,98,99,100,101,103,104,106,107,108,109,111,121,124,127,129,135,139],everybodi:[87,106],everyon:[39,49,60,88,89,126,138],everyth:[10,24,25,27,33,35,40,48,51,56,57,69,71,79,86,87,88,89,92,94,98,104,105,106,111,115,118,123,124,126,128,130,134,136,138],everywher:[24,82,84,85,100],evil:95,evolv:92,evt:55,ex:[2,23,34,123,126],exact:[50,85,90,110,122],exactli:[25,33,62,66,67,68,74,89,90,98,104,106,107,108,110,122,124,128,129,130,131],examin:[60,75,84,121],exampl:[16,19,23,24,25,33,35,37,38,39,40,41,46,48,49,51,52,56,57,62,63,66,68,69,72,73,78,79,81,82,88,90,91,93,97,98,99,100,102,103,104,105,106,107,108,110,111,118,121,123,124,126,128,129,130,134,135,136,150],example1:74,example_fixtur:79,example_seq:107,exc:108,exc_tb:64,exc_typ:64,exc_val:64,exceed:99,excel:[15,81,84,92,94,106,110,115,116,135,140],except:[0,4,13,16,25,43,51,53,55,62,64,65,67,69,72,79,80,83,84,88,92,97,100,101,104,106,108,111,115,126,129,130,131,139],except_exercis:18,except_test:18,exception1:71,exception2:71,exception3:71,exception_test:111,excercis:[48,67,102],exchang:[46,90,109],exchange_first_last:46,excit:[56,106],exclud:95,exclus:[63,109,110],excus:111,execut:[17,27,28,55,56,59,66,75,78,79,95,107,108,131,139],executemani:94,executor:55,exercis:[0,12,14,15,25,27,28,30,39,43,50,51,52,53,56,57,77,84,94,99,100,103,106,107,122,124],exhaust:139,exist:[16,25,33,34,35,40,41,51,52,55,66,69,71,72,79,80,82,84,85,87,89,92,94,95,104,105,108,118,124,126,135,136,139],exit:[35,51,55,59,66,78,93,106,108,109,110,128,130,131,139],exit_program:35,exp:71,expand:[30,33,72,98,134],expans:[30,70,134],expat:94,expect:[12,16,24,25,33,35,40,41,50,56,59,60,64,66,71,72,77,79,84,92,95,98,101,106,107,108,110,111,121,124,128,130],expens:[62,63,67,95,108],expensive_funct:95,experi:[12,49,51,55,57,59,65,78,81,115,117,134,135,140,141],experienc:[95,106,131],experiment:[74,121,127,141],expir:67,explain:[6,45,60,67,70,81,110,118],explan:[2,81,101,110],explanatori:[57,70],explicit:[51,69,72,92,107,123],explicitli:[51,56,71,79,85,87,92,103,106,139],exploit:57,explor:[41,59,74,110,123,128,131],expon:[59,108],exponenti:[56,59],expr:[62,108],express:[3,23,48,50,59,62,66,67,78,79,80,95,100,103,106,108,110,111,134,143],expression_with_var_and_var2:62,expression_with_vari:62,extend:[24,25,31,40,79,92,95,104,134],extens:[24,31,33,34,49,63,75,92,109,140],extern:[33,64,74,87,94,95],extra:[15,21,24,25,26,47,48,53,55,59,62,64,69,71,80,84,92,100,101,104,110,121,127,129,131],extra_arg1:87,extra_arg2:87,extra_info:71,extran:92,extraordinair:81,extrem:66,ey:55,f197736b3bdb:103,f2:80,f:[25,26,40,45,48,50,54,56,62,64,67,71,72,73,74,79,80,84,85,92,95,100,104,106,107,109,111],face:138,facil:109,facilit:[1,121],fact:[24,25,33,45,51,52,55,56,59,65,67,69,73,78,79,82,83,84,87,90,91,92,94,98,100,101,103,104,105,106,121,122,136],facto:[31,84],factor:[24,28,30,40,98,101,104],factori:[61,73,79,84,99],fail:[13,16,24,25,39,41,51,52,66,72,82,107,108,109,121,124],failur:[16,25,51,52,66,87,106,108],fair:[13,25,33,44,55,64,89,111],fairli:[25,33,35,57,63,79,103,104,111],fake:[51,55,82,108],fall2018:72,fall2020:124,fall:[51,90,100],fals:[13,16,17,25,56,57,64,69,78,83,86,93,94,98,100,101,103,106,108,109,135],famili:[57,100,108,138],familiar:[15,24,30,48,55,60,62,65,74,84,93,98,100,107,108,117,119,121,122,124,128],fanat:15,fanci:[25,29,33,39,51,59,92,100,103,104,106,108],fancier:[33,51],fantasi:106,faq:141,far:[1,25,28,30,31,35,40,51,56,59,61,67,71,73,92,93,95,98,100,101,103,104,106,107,111,118,123,134],farther:[28,51],fashion:[63,82,94,101,106,128],fast:[2,33,51,61,63,68,69,92,95,100,106],faster:[33,49,55,63,67,69,95,101,103,131],fatal:124,favorit:[24,108],fc6f8de72dfc:59,fcobject:87,fear:15,feasibl:108,feast:15,featur:[16,23,24,25,28,30,33,35,40,51,54,55,56,57,58,65,66,69,71,73,74,77,78,81,84,89,91,92,98,101,102,103,104,106,107,108,111,119,122,123,124,126,127,128,129,130,134,135,138,139],feature_branch:118,fedoraproject:129,feed:[25,51,106],feedback:[33,105,123,124,136],feel:[1,2,30,39,47,51,60,74,100,103,111,118,128,130,131],fetch:[121,135],fetchal:94,fetchon:94,few:[2,16,20,24,25,30,33,34,50,52,55,60,64,67,71,74,79,84,85,92,93,94,97,100,103,107,108,110,115,119,122,128,134],fewer:[35,70,100],ff:[106,118],fff:25,ffff:106,ffffff:25,fib:45,fibonacci:[2,79,143],field:[30,33,35,61,84,89],fifteen:15,fifth:15,figur:[2,15,17,22,24,25,29,48,71,77,82,85,93,94,95,100,108,111,121,124,137,139],file001:48,file002:48,file010:48,file011:48,file10:48,file11:48,file1:48,file2:48,file:[0,1,11,16,17,18,19,24,25,27,31,32,33,35,40,41,42,43,44,45,48,51,52,53,55,56,59,63,64,66,69,71,73,74,82,85,89,93,95,97,103,105,106,107,108,110,111,115,120,121,122,124,126,127,128,129,130,131,134,135,136,137,139,140,141,142],file_002:48,file_cont:[25,64],file_handl:64,file_obj:64,file_out:[24,25],file_yield:64,filehandl:82,filemod:82,filenam:[20,48,51,64,66,71,82,92,94,95,107,111],filenotfounderror:71,filesgitbin:120,filesystem:[85,94],fill:[33,36,41,59,62,87,101,102],filter:[0,59,67,79,80,100,135,157],find:[6,15,18,21,25,27,33,34,39,40,42,43,44,45,48,49,51,52,56,61,62,66,68,69,71,73,74,80,81,85,87,89,90,91,92,93,95,97,100,104,106,107,108,110,111,115,116,121,124,127,128,129,130,131,135,139],find_donor:33,find_on:89,finder:128,fine:[15,16,24,25,27,28,33,34,55,56,71,77,82,107,108,109,110,111,128,130],finest:18,finger:106,finish:[2,10,45,51,63,78,94,100,109,118,124,139],fire:[51,139],firefox:94,first:[2,8,12,13,14,15,17,21,22,24,25,27,28,32,33,34,35,37,39,45,46,48,50,51,52,53,54,56,57,59,60,61,62,63,64,65,66,67,68,70,73,74,79,84,85,87,88,89,90,92,93,94,95,99,100,102,103,104,106,107,108,109,111,115,118,121,122,123,124,126,128,129,130,131,134,137,139,142],first_nam:[23,37,67,89],first_tri:18,firstnam:101,fit:[51,60,90,93,94,95,100,106,107,111,128],five:[22,28,51,81,100,103,106],fix:[13,25,51,52,71,92,93,97,106,108,115,122,124,139,140],fix_the_problem:56,fixm:106,fixtur:[79,107],fizz:[2,142],fizzbuzz:[22,124],fizzbuzztest:22,flag:[78,93,94,100,118,124],flake8lint:135,flake:109,flask:89,flat:[85,94],flavor:[108,129,134,140],flaw:108,fledg:[71,138],flexibl:[24,25,31,33,54,73,82,84,90,93,94,97,103,104,107,108,121,123,128],flickr:100,flight:115,floatcanva:79,floatingpoint:108,floor:56,florida:90,flow:[18,28,33,59,63,65,71,78,80,107,121,122,126],fluent:[67,79,81],fluff:103,fly:[13,59,60,62,74,79,80,84],fmt:82,fn:55,fname:48,focu:[30,74,103,108],focus:[90,94,118],folder:[85,92,105,123,124,128,136,139],folk:[57,62,81,91,92,100,107,110,111,128,134],follow:[7,12,13,17,23,25,27,28,30,40,48,50,51,56,59,63,64,66,67,70,71,72,73,74,76,77,79,81,82,85,87,88,89,92,93,94,95,103,106,107,108,109,110,115,123,124,128,129,130,131,134,135,137,139,140,141,159],font:[24,25,115,135],font_fac:135,font_siz:135,fontsiz:115,foo:[64,73,84,92,94,108,109,134,139],foo_bar:92,foo_config:92,foobar:134,foobarandthennotparam:134,food2:100,food:[95,100],food_copi:100,food_pref:[15,54],foolish:93,footer:51,fopen:72,forbid:103,forc:[16,25,51,56,62,66,92,93,106,107],fore_color:12,foreign:[74,89],forev:[55,78],forg:92,forget:[28,35,64,66,95,106,123],forgiv:[25,38,66,71],forgot:[66,123],fork:[45,48,56,121,123],forkin:126,form:[2,21,24,25,34,45,51,61,62,64,66,67,70,91,92,94,101,109,124,134],form_str:48,formal:[14,85,101,103],format:[3,15,24,25,28,31,37,40,55,64,66,67,78,82,84,92,95,106,108,109,128,134,137,139,145],format_str:82,formatt:48,formula:[13,45],forth:[15,63,106],fortran:[92,95,139],fortun:25,fortunatli:92,forward:[81,100],forwarn:130,found:[13,25,27,51,53,59,79,82,90,92,93,100,101,106,107,116,128,129,138,139],foundat:[28,81,110],founder:8,four:[12,17,23,24,25,27,43,51,54,82,85,100,106,110,128,135],fourteen:51,fourth:48,fr:106,frac:50,fraction:[95,101,106,108,110],frai:103,frame:[66,99],framework:[24,40,52,55,65,72,82,84,85,89,92,93,95,106,108,111,130],frank:40,frankli:[82,91,98,106],fratern:50,fred:[54,62,80,84,85,88,89,94,103],free:[2,30,39,47,51,56,60,66,74,81,84,89,103,110,116,118,121,128,130,131],frenchman:51,frequenc:[50,95],frequent:[17,25,27,45,48,51,71,95,105,124,129,130,131,136],fresh:[14,108],fri:15,friend:[25,51,67,123,135],friendli:102,frobnagl:70,from:[7,13,15,17,18,20,22,23,25,27,28,30,31,33,34,35,39,40,44,45,48,50,51,52,53,54,55,56,57,59,60,61,62,63,64,65,69,70,73,74,75,76,77,79,81,82,83,86,89,90,91,93,94,95,97,98,99,101,102,104,105,106,107,108,109,110,111,115,118,120,121,122,123,124,126,127,128,129,130,131,134,135,136,139,140,141],from_diamet:[13,102],from_iter:67,from_roman:106,fromkei:102,front3:100,front:[25,51,89,92,93,100,103,114],frown:[24,59],frozenset:[17,69],fruit:[15,17,27,35,48,54],fs:69,fstring:[48,103],fuhm:87,fulfil:128,full:[20,24,25,28,39,40,50,51,55,57,64,69,70,71,72,79,81,84,89,97,101,103,106,107,108,109,110,111,118,121,124,126,130,135,138,141],fuller:101,fulli:[10,28,32,34,41,56,69,74,81,87,92,108,110],fullstackpython:89,fun:[18,23,24,50,51,54,56,66,73,77,80,82,83,84,86,87,93,94,100,103,108],fun_with_paramet:59,func:[12,67,79,85,95,109],func_list:80,func_nam:95,func_wrapp:67,function_:90,function_build:26,function_nam:34,functionnam:93,functool:[50,83,101],fund:30,fundament:1,further:[25,33,48,51,62,66,71,79,100,134],futur:[25,28,33,40,62,93,105,107,109,110,123,136],g:[12,15,24,25,33,34,62,65,78,84,87,92,94,95,100,103,107,108,109,124],ga:51,gain:[63,104,126,128],game:44,garbag:109,garbanzo:100,gate:[28,35,126],gather:[33,49,55,103],gave:[24,25,74,93,98,123],gcc:[110,128,129,131,139],geek:[50,100,103,120,122,126],gen:79,gen_a:79,gen_b:79,gener:[15,16,19,21,23,25,28,30,33,39,40,48,50,51,55,56,63,65,66,67,72,73,74,78,80,84,85,87,88,89,90,91,92,93,94,95,98,99,104,106,107,108,110,111,115,126,130,139,159],generos:39,genexpr:[62,79],genfunct:65,genreal:61,georg:88,geospati:95,get:[1,2,8,11,12,13,14,15,16,18,20,21,23,24,25,26,28,29,30,31,32,33,34,35,37,38,39,40,44,46,47,48,50,51,52,54,55,56,57,59,61,62,63,64,65,66,67,68,71,72,73,76,77,78,79,80,82,83,84,85,86,87,88,90,91,93,94,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,114,115,116,117,118,119,121,122,123,124,125,127,128,129,133,134,135,136,137,138,139,141],get_area:104,get_color:98,get_event_loop:55,get_fullnam:67,get_input:108,get_ipython:59,get_last_pair:51,get_letter_text:40,get_news_async:55,get_news_sync:55,get_random_follow:51,get_report:40,get_scale_fun:59,get_set_attr:84,get_vers:92,get_x:96,getargvalu:66,getcwd:72,getdefaultencod:111,getpid:109,getrecursionlimit:66,getrusag:95,gettempdir:39,getter:89,getting_start:108,getvalu:72,getx:67,ghi:46,gift:28,gil:[49,109],gilectomi:63,giothub:121,gist:134,git:[1,24,27,30,45,74,105,117,118,125,133,134,135,136,140,141],git_ps1_showcolorhint:134,git_ps1_showdirtyst:134,git_ps1_showstashst:134,git_ps1_showupstream:134,gitbash:131,github:[2,8,15,18,27,44,45,48,53,66,74,77,79,84,87,89,92,95,101,103,107,116,119,120,121,122,123,128,130,131,134,135,138],gitignor:74,gitpad:120,gitub:126,give:[2,15,16,19,23,24,25,28,40,45,47,49,50,51,52,56,57,59,60,62,64,67,68,71,74,77,79,80,81,82,84,85,88,89,91,92,93,94,95,97,98,106,107,108,109,110,117,118,122,123,128,137],give_birth:87,given:[15,23,24,25,28,33,34,48,50,51,54,55,57,59,63,65,67,73,82,85,89,90,91,94,95,98,101,102,106,108,109,123,128],gjt06q0:131,glad:111,glob:64,global:[28,33,56,63,66,80,85,92,107,126,139],globalinterpreterlock:63,glossari:79,glue:94,gmail:[108,121],gnome:128,go:[1,11,12,24,25,28,30,33,34,35,38,40,45,48,51,55,56,60,62,63,64,65,66,67,69,70,73,74,79,81,82,84,85,86,87,93,98,99,100,104,105,106,108,109,110,115,117,118,120,121,122,123,124,126,129,130,131,135,136,137,139,140,142],goad:52,goal:[2,14,15,25,30,33,39,47,50,55,60,66,77,80,95,108,124],god:139,goe:[23,24,35,39,51,64,66,79,81,82,85,86,92,93,95,106,118,139,140],gohlk:[92,95],gone:[56,109,139],good:[1,2,8,12,13,14,24,25,27,29,30,32,33,34,39,40,45,48,50,51,55,56,57,59,62,66,67,70,71,72,73,74,78,79,82,85,89,90,91,92,93,94,95,98,99,101,103,104,106,107,108,110,111,116,117,121,122,123,124,128,131,134,135,137,138,139,141],goodpractic:92,goof:100,googl:[24,25,55,64,66,72,74,94,95,111,121,129,131],gori:104,gospel:104,got:[1,8,13,23,25,51,62,63,65,71,83,85,89,95,97,100,103,106,107,109,110,118,121,134,139],gotcha:[46,72,86,92,103],gotten:[25,33,51,107,124],gperftool:95,grace:[66,71],gracefulli:30,grade:[1,124],gradual:[81,110],grain:82,granddaddi:55,grant:111,graph:[89,118,126,159],graph_databas:74,graphenedb:74,graphic:[33,55,66,87,95,131],gravit:95,great:[1,8,25,31,33,34,38,49,51,55,74,80,81,83,84,92,93,94,95,103,105,106,107,108,110,115,123,128,134,135,136,139,140],greater:[55,56,66,101,106,109,117],greatest:[55,129],greek:[15,50],green:118,greenteapress:[2,3,7,81],greg:15,gregmalcolm:15,grep:[95,129],grid:[2,68,142],grid_print:103,gritti:106,grok:63,ground:55,group:[93,100,108,110,121],grow:[34,70,98,104],growl:103,grown:111,growth:[92,95],gtk:91,guarante:[91,93,94,98,106],guard:[35,74],guess:[59,95,111],gui:[33,55,80,89,130,131,141],guid:[2,8,33,55,74,82,88,91,92,93,101,115,122,124,141],guidelin:[100,101],guido:[55,69,83,92,93],gullibl:69,guppi:95,gutenberg:51,gz:139,h1:[24,25],h2:[24,25],h3:24,h:[24,54,78,82,100,103,134,139],ha:[2,7,12,13,14,16,24,25,28,30,31,33,34,35,40,41,44,45,48,49,50,51,55,56,57,59,61,62,63,64,66,67,68,69,71,72,73,74,75,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,97,98,99,100,101,103,105,106,107,108,109,111,115,118,122,123,124,126,128,129,130,134,135,136,139,141],habit:[72,124],hack:[86,92,115],hackabl:115,hackernoon:[55,103],had:[15,25,28,39,49,51,59,62,67,68,69,84,87,88,95,100,106,107,111,116],hadoop:[83,89],half:[50,59,63,108,111,134],half_lif:59,half_life_fun:59,halt:[24,55,63,66,108],ham:[15,100],hammer:[93,108],hamper:63,hand:[18,25,30,49,51,56,57,59,62,69,79,81,82,84,85,90,92,100,101,104,106,107,130,135],handi:[35,39,51,54,56,61,62,65,69,70,71,72,78,80,85,87,91,103,121,128,159],handl:[0,4,12,16,18,19,20,25,33,35,38,39,40,50,51,53,55,56,59,63,72,82,87,94,95,106,109,118,123,124,135],handle_error:64,handle_the_error:71,handle_them_al:71,handler:[18,55,66,89],hang:[52,62,80,91,98,131],happen:[12,13,25,43,51,55,56,59,63,64,65,66,67,69,71,73,79,84,85,86,91,92,93,95,98,99,104,106,108,109,110,111,118,121,122,124,131,134],happi:[30,55,121,123,124],happili:[106,135],hard:[24,25,28,32,33,35,40,51,55,59,63,64,66,73,81,82,85,89,92,93,103,107,108,109,110,116,121,128,134,139],hardcod:39,harder:[40,41,49,92,100,103,106,108],hardest:33,hardi:51,hardli:[59,98],hardwar:108,harm:[87,111,124],has_kei:94,hasattr:[24,87],hash1:122,hash2:122,hash:[110,121,122],hashabl:[51,61,69],hasn:[59,66,98,106,108],hast:63,hat:140,hatch:81,hate:89,have:[1,2,7,8,12,13,14,15,20,21,23,24,28,29,30,31,32,33,34,35,36,40,41,45,46,47,48,49,50,51,52,53,54,55,56,57,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,77,78,79,80,81,82,84,85,86,87,88,89,90,91,92,93,94,95,98,99,100,101,102,103,104,105,106,107,108,109,110,111,114,115,118,121,122,123,124,125,126,127,128,129,130,131,134,135,136,137,139,140,141],haven:[10,24,25,51,68,71,77,84,93,106,107,116,124,128,135],he:[15,51,54,55,103,124],head:[24,25,51,55,87,103,122,128,134],header:[24,51],header_s:72,heap:95,heapi:95,hear:[91,131],heard:110,heart:[63,130],heartach:126,heavi:[24,108],heavier:63,heavili:[106,109],heavyweight:[6,107],heck:[100,106],hei:[25,106],height:[24,50],held:111,hellman:93,hellmann:64,hello:[56,76,103,109,122,128],hello_unicod:111,help:[1,16,24,25,28,33,38,44,48,51,53,55,56,60,61,63,65,70,71,81,82,84,85,87,90,91,94,95,101,104,105,106,107,108,110,115,122,123,124,126,127,128,129,130,131,134,135,136,137,139,140],helper:[44,94],helpfulli:[115,120,140],henc:[25,26,44,73,80,106],her:[51,81,123],here:[2,15,16,18,24,25,26,27,28,30,31,33,35,38,40,41,42,44,48,50,51,52,55,57,62,64,65,66,67,68,69,70,71,72,73,74,77,79,82,84,85,87,88,89,90,91,92,94,95,98,99,100,101,102,103,104,105,106,108,110,111,115,117,118,120,121,122,123,124,125,126,127,128,129,131,134,135,136,137,138,139,140,141],heterogen:100,hetting:[7,87],heurist:51,hex:[15,103],hexadecim:[15,111],hexidecim:15,hg:[63,95],hh:103,hi:[51,62,63,103],hidden:[83,91,128],hide:[57,91,128],hidevcsignoredfil:115,hierarch:[82,94],hierarchi:[24,42,82,84,85,87,92,104],hifi:95,high:[2,50,51,68,89,90,94,108,123,129],higher:[18,48,66,71,72,109,110,111,126,128,137],highest:[24,106],highli:[25,69,74,82,89,90,110,115,118,121,139],highlight:[115,118,124,135,140],hijack:139,him:[71,103],himself:[55,103],hint:[13,18,25,43,46,48,50,51,81,99,105,119,136],histor:28,histori:[9,28,33,57,64,83,84,87,89,104,121,122,126],hit:[43,49,55,56,66,85,91,110,115,124,127,128,135],hitch:121,hitchhik:93,hmm:[25,65,84,106,124],hmmm:[25,51],hobbi:74,hobgoblin:93,hold:[15,24,25,28,31,33,35,41,47,51,53,55,59,61,64,65,69,71,79,81,84,89,95,103,111],holder:25,hole:66,holm:[51,106],home:[39,72,89,93,121,123,124,126,129,134,139],homebrew:[89,92,130],homework:77,homogen:[100,138],honestli:[92,116,130],honor:66,hoo:25,hood:[56,67,81,92,94,104,111],hook:[124,139,141],hoorai:106,hope:[25,55,92],hopefulli:[33,56,65],hopper:[66,71],horizont:[24,25,50,89,103],horribl:79,host:[74,109,122,126,138],hosun:131,hour:[59,95],hous:[51,104],household:89,how:[1,11,15,16,18,21,24,25,27,29,30,33,35,38,41,47,48,49,50,51,54,55,56,59,62,63,64,65,66,67,68,69,70,71,72,73,74,78,79,81,82,84,85,87,88,89,91,92,93,95,99,100,101,102,103,104,106,107,108,110,111,115,116,117,118,121,122,124,126,131,134,135,137,139,141,142],howev:[6,12,21,24,25,29,33,37,48,51,54,56,69,81,82,84,88,90,92,97,101,116,117,122,124,126,129,130,131,135,138,139,141],howto:[82,111],howzit:103,hp:95,hpy:95,hr:[24,25],href:[24,25],htat:79,htlu2dfodtg:7,htm:131,html:[2,3,4,6,7,9,12,16,27,32,33,34,41,48,51,55,57,59,61,63,64,66,67,69,72,79,81,82,84,86,87,89,92,94,95,100,101,103,104,108,109,111,116,118,126,127,139,141,155],html_basic:24,html_render:[8,24,25],htmlcov:32,http:[2,3,4,6,7,8,9,12,14,15,16,22,24,25,27,33,34,41,48,49,50,51,52,53,54,57,59,60,61,62,63,64,66,67,68,69,71,72,73,74,77,79,80,81,82,84,86,87,88,89,90,91,92,93,94,95,96,100,101,103,104,106,107,108,109,110,111,116,118,120,121,122,123,124,126,127,128,129,130,131,134,135,138,141],huge:[55,111,118],huh:[24,86,98],human:[24,31,48,49,63,84,93,94,106,108],hundr:[48,49,51,106],hunner:62,hurt:87,hyper:24,hyperdex:89,hyperlink:25,hypertext:24,hypothes:95,i:[2,16,17,20,24,25,26,32,33,34,39,40,46,50,51,52,55,56,57,59,62,63,66,67,68,69,71,72,74,75,77,78,79,80,81,82,83,84,86,87,88,89,90,91,93,94,95,96,98,100,101,103,104,105,106,107,108,109,110,111,116,118,121,124,127,129,130,135,136,137,139,140],ibm:94,ic:106,icaneatglass:111,icon:124,id:[24,25,27,33,56,62,70,75,76,84,93,110,111,137],idea:[1,14,15,19,24,25,34,39,45,47,48,51,56,59,61,62,66,71,78,79,81,84,90,91,92,93,94,98,101,103,106,107,108,111,118,121,123,124,128,139],ideal:[1,13,33,35,54,77,85,88,101,108],ident:[15,25,30,45,67,106,128],identifi:[39,93,95,106,118,121,122,124],idiom:[57,68,84,95,104],idl:[75,110],idx:78,ieee:108,ifyou:130,ignor:[24,25,34,64,87,93,103,111,118,124,134,139],ii:106,iii:[28,35,84,106],iiii:106,iimxcc:106,illuminaut:100,illustr:48,imag:[63,72,87,100],imagin:[30,44,67,87],imap:109,immed:139,immedi:[51,62,78,92,95,106,124,134],immut:[35,51,56,59,69,84,86,100,103,107],impact:[95,121],imper:67,implement:[16,25,33,35,40,45,51,55,63,64,67,69,79,84,97,101,102,108,109,111,117],impli:[90,139],implic:86,implicit:92,implicitli:[98,106],importantli:[65,93,123,141],importerror:51,importlib:[85,92],impos:139,imposs:63,impract:101,improv:[1,19,36,38,40,41,51,53,60,95,97,101,106,134],in_data:51,in_the_bodi:78,in_tupl:48,inadvert:[25,106],inamidst:111,inc:[110,128],includ:[17,25,28,30,33,35,41,44,45,51,55,56,61,62,66,69,85,89,92,94,95,100,103,105,106,113,115,124,128,135,136,139],include_package_data:34,inclus:[22,95],incompat:[92,111],incomplet:[65,67],inconsist:93,inconsolata:135,incorpor:[33,37,39,126],incorrect:25,incorrectli:106,incr:59,increas:[26,95,109],increasingli:91,incred:[28,126,134],increment:[26,59,91,109],increment_x:59,incrementalencod:111,incur:74,ind:25,inde:[16,25,30,62,63,65,84,90,92,100,106,107,117],indent:[1,25,56,85,93,106,128],indent_to_bracket:135,independ:[32,55,63,72,86,100,108,124,128,130,133,138],index:[2,25,27,32,35,45,46,47,48,51,79,80,81,87,88,89,92,97,103,106,107,108,127],index_sl:101,indexerror:[47,51,79,100,103],indic:[24,25,51,56,59,62,71,73,79,85,89,98,100,118,124,139],individu:[24,28,33,39,51,55,74,87,103,106,123,124],industri:[123,128],ineffici:103,inevet:139,infil:72,infin:111,infinit:[55,78,108,109],inflex:107,info:[27,28,40,48,51,66,71,72,82,100,103,106,107,121,128],inform:[25,33,35,39,41,45,47,51,55,56,64,66,70,71,82,89,92,94,95,97,100,101,106,107,108,110,115,127,128,129,130,131,134,135,137,139],infrastructur:30,infrequ:55,ingrain:135,inher:106,inherit:[0,7,24,25,53,84,91,98],inheritance_:104,ini:92,inifil:[25,107],init:[62,63,87,126],init_fil:92,init_someth:104,initandlisten:89,initi:[24,25,47,51,59,62,79,84,87,95,101,104,109,118,126],initial:87,inlin:[55,70,80,95],inlud:66,inner2:59,inner:[35,59,67,81,106],input:[15,19,23,24,26,27,28,33,34,35,38,40,41,45,54,55,56,57,59,62,64,65,69,71,72,73,79,80,84,85,88,90,95,96,97,99,100,102,103,108,109,111],inquisit:15,ins:[17,27,83,98,101],insensit:69,insert:[27,66,69,93,94,100,109,124],insert_on:89,inset:69,insid:[15,16,24,25,34,56,59,63,64,71,73,82,85,92,100,103,106,108,110,111,123,126],inspect:[56,66,71,95],inspir:[27,95],insstructor:48,instal:[1,32,34,42,49,52,55,74,76,85,87,88,93,95,106,108,110,113,120,125,126,129,130,131,133,134,135,137,138],install_guid:141,install_requir:92,install_test:[124,137],installed_app:73,instanc:[7,13,16,24,25,30,33,34,41,51,55,56,57,63,65,67,69,79,80,82,85,86,87,89,90,91,92,93,95,98,101,102,104,106,121,122],instanti:[29,84,90,104],instead:[15,18,22,24,25,26,33,39,46,49,50,55,57,62,68,84,87,88,92,95,97,102,103,106,108,109,111,118,129,131,134,139],instruct:[18,25,30,63,65,93,117,123,124,126,128,129,131,135,138,139],instructor:[1,14,27,33,48,77,81,105,117,123,124,126,131,136,138,141],instrument:[14,51,95,106],int_a:50,int_num_dai:88,integ:[15,23,24,38,45,48,50,56,69,71,73,79,88,94,95,97,98,99,100,101,103,106,109,110,111],integerfield:84,integr:[24,50,66,84,89,92,93,95,106,108,109,111,128,131,140],integrate_main:[95,109],integrate_numpi:109,integrate_thread:109,intel:92,intend:[25,30,36,73,99,103],intens:63,intent:[25,69,111],inter:63,interact:[28,32,33,34,35,40,55,59,76,90,94,110,112,115,122,127,128,129,130,131,134,135,137,140],interchang:[67,84,131],interest:[15,25,51,55,56,59,62,77,79,81,84,93,98,101,106,134,139,159],interfac:[14,25,29,30,32,34,40,47,55,63,64,79,82,87,94,96,104,105,109,115,122,123,128,136],interfer:84,intermedi:[66,81],intermediate_python_workshop:81,intern:[25,47,59,69,78,92,95,98,110],internation:106,internet:[69,111,121],interpret:[43,56,59,63,70,72,75,85,92,93,94,101,103,106,111,114,115,117,130,131,135,139,140],interprocess:109,interrupt:[19,51,63,78,109],intersect:[17,69],interv:[55,109],interview:22,intimid:[24,107],intro:[0,1,7,24,25,48,50,53,56,62,66,81,103,116,119,121,122,123],introduc:[1,41,53,74,79,84,101,103,107,108,109,141],introduct:[0,2,4,6,53,64,74,81,103,107,116,122,125,127,129,139,147],introductori:[2,53],intropython:74,introspect:107,intuit:[74,101,126,139],invalid:[24,56,57,72,106],invalu:106,inventor:51,invers:106,invert:57,investig:[30,66,71],invit:124,invoc:[55,63,65],invok:[39,63,72,87,92,106,108,129,130,131],involv:[1,30,33,55,64,79,84,95,98,100],io:[2,8,34,55,63,72,74,81,84,89,92,95,101,103,106,108,111,116,118,122,126,127,128,131,135],ioerror:[64,71],ionelmc:92,ip:[55,129],ipc:63,ipclust:76,ipdb:66,ipp:76,ipy_nam:59,ipyparallel:[30,76],ipython3:129,ipython:[1,10,18,22,24,27,30,54,55,56,57,59,64,65,66,70,71,72,73,75,79,84,85,96,97,99,100,102,103,110,111,114,128,157],iron:[74,110,128],ironpython:63,is_clos:108,is_dir:72,is_valid_roman_numer:106,is_weekend:52,isalnum:103,isalpha:103,isclos:50,isdigit:71,isdisjoint:69,ish:66,isinst:[24,71,104],island:106,islic:79,isn:[12,24,25,34,35,49,50,51,52,55,56,59,61,63,65,66,71,79,81,84,85,87,88,91,92,93,103,106,107,108,111],isntal:92,isnumer:103,isol:[33,92,106,109,118,122,139],isspac:51,issu:[1,25,27,35,53,55,60,63,64,66,86,92,94,106,109,122,123,124,131,135],issubclass:[98,104],issubset:69,issue18695:111,it_did_break:78,itch:81,item:[15,17,24,25,27,33,35,36,39,51,52,56,59,62,66,69,79,80,83,84,88,94,95,97,98,100,101,103,106,107,109,128,135],item_copi:69,item_view:69,item_weight:88,itemgett:35,iter:[0,7,53,55,56,62,64,71,72,83,94,95,101,102,103,106,109,159],iterable_of_numb:50,iterat:79,iterateme_1:79,iterateme_2:79,iterator_1:79,iterator_2:79,iterdir:72,iteritem:79,iterkei:79,itervalu:79,its:[13,24,25,30,47,51,54,55,59,63,64,65,67,71,76,79,82,84,85,90,91,92,94,95,98,99,104,106,107,108,109,111,119,122,124,128,134,135],itself:[1,24,25,46,51,55,56,59,62,64,67,68,73,74,79,82,84,87,91,92,99,100,103,106,108,110,121,129,130,131,133,134,140],iv:106,ivi:106,iviv:106,iwish:51,ix:106,ixiv:106,ixix:106,izip:79,j:[46,56,66,78,91,101],jack:[7,84,88],jagger:21,jail:51,jam:28,jame:87,jane:[36,85],jar:94,jason:63,java:[82,96,107,108,110,111,128],javascript:[31,55,59,69,84,94,135],jeff:[28,35,39],jeff_bezo:39,jeffknupp:73,jessica:[81,110],jetbrain:[66,128],jfine:84,jiffyclub:95,jimmi:35,jmdana:95,job:[1,18,22,25,28,33,49,63,77,95,106,109,122,125],joe:[35,103],joelonsoftwar:111,john:[62,94],join:[25,35,40,51,72,74,92,95,109,124],jointli:60,joke:18,jone:[80,89],jorg:126,jpeg:[20,72],jpg:72,jr:106,json:[89,111,135],json_respons:66,json_sav:[31,113],json_save_dec:84,json_save_meta:84,jsut:130,judg:60,judici:70,juju:111,jul:[110,131,137],jumbl:109,jump:[90,115,128,135,140,142],jun:[128,129,130,131],junit:[107,108],junk:[25,52],jupyt:110,just:[12,17,19,23,24,25,27,28,39,46,48,50,51,54,55,56,59,61,62,63,66,67,69,72,73,74,77,79,80,84,85,87,89,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,111,117,121,122,123,124,126,128,129,131,135,136,139,140,142],just_about_anyth:79,justif:103,jython:[63,110,128],k:[15,62,69,78,79,95,108],kata14:[51,79],kata:[51,79],kcachegrindcalltreeformat:95,kde:128,keep:[14,18,21,24,25,27,32,33,34,35,38,39,40,47,51,55,56,57,59,66,69,71,72,73,79,80,81,82,83,84,85,87,89,90,92,93,95,96,98,99,103,104,106,108,110,118,121,122,123,124,126,127,135,139,142],keep_go:78,keepal:109,kei:[15,17,24,25,33,34,35,37,41,46,47,51,54,55,56,57,59,61,62,63,64,65,66,74,79,80,83,84,85,87,91,92,93,94,100,102,106,108,109,111,124,127,134,135],kept:[98,99,124],kernel:141,kernprof:95,key1:[54,69],key2:[54,69],key_fun:101,keyboard:[19,115,128,135],keyboardinterrupt:19,keyerror:[67,69],keyword:[24,25,30,34,37,48,50,55,57,62,65,69,70,72,73,78,79,84,87,92,100,108,157],kib:124,kick:[2,43,92],kid:106,kill:[70,131,139],kind:[14,16,18,20,24,25,39,51,52,55,56,61,62,63,68,69,71,74,80,84,87,91,92,93,94,95,101,103,104,106,107,111,134],kinda:[54,55,72],kindl:81,kitten:139,klingon:56,kludg:108,klunki:[25,59,92],klunkier:78,knight:87,knot:103,know:[1,2,12,14,24,25,27,29,33,35,38,40,41,51,52,53,54,55,56,57,59,62,63,64,65,66,67,68,70,72,73,77,78,79,80,81,82,84,85,87,89,91,92,93,94,95,97,98,101,103,104,105,106,107,108,111,115,116,118,122,123,124,127,128,129,130,131,134,136,137,139,141],knowledg:[6,33,82,134],known:[27,30,50,51,57,62,66,67,69,71,72,78,81,85,86,92,98,100,101,103,104,106,107,108,109,110,128,134],known_valu:106,knuth:95,koan:15,kw:84,kwarg1:70,kwarg:[6,24,25,54,62,67,84,87,93,95,101,104,108,109,151],kwargument2:93,kwargument:93,kyle:59,l100:79,l1:[78,100],l2:[78,100],l3:78,l4:78,l61:84,l761:63,l:[62,78,79,80,83,95,100,106,108,121],lab:[3,4,5,6,54,81,144,146,148,151,152],label:[16,81,122],lack:96,lag:55,lai:[64,87],lamb:15,lambda:[0,35,56,59,62,76,83,157],lamda:56,lan:89,landscap:118,lang:86,languag:[1,21,24,31,48,56,57,59,62,66,68,69,70,74,78,84,89,90,91,94,100,103,104,106,110,111,115,117,122,124,128,129,138,142],laptop:121,larg:[34,47,60,62,63,64,69,70,77,85,89,92,94,104,106,108,109,110],larger:[23,49,52,94,103,106,109],largest:106,larri:63,lasagna:15,last:[18,25,27,35,46,50,51,54,56,57,59,62,64,65,66,67,69,71,72,73,77,79,80,84,85,92,96,99,100,102,103,106,108,109,111,121,124,126,127,128],last_nam:[23,37,67,89],lastli:103,lastnam:101,late:84,later:[23,24,25,37,40,55,56,62,70,72,75,78,79,80,87,95,96,97,100,106,107,110,121,122,124,135],latest:[6,16,34,55,74,81,82,84,89,92,105,106,107,108,115,118,121,129,130,131,135,136,140,141],latex:139,lather:[51,106],latin1_test:111,latitud:128,launch:[49,128,131],launcher:[27,131],launchpad:95,law:[63,101],lay_egg:87,layer:68,layout:[92,140],lazi:62,lc:106,ld:106,lead:[18,67,69,74,84,85,91,92,93,100,106,109,126,139],leanpub:81,learn:[0,2,14,17,22,25,28,30,34,35,38,47,51,55,56,60,62,64,66,68,71,72,74,75,77,82,84,91,94,97,98,100,104,105,106,107,110,116,123,124,126,127,128,131,134,135,136,139,142],learngitbranch:122,learningpython:79,learnpythonthehardwai:[2,81,110,116],least:[1,14,15,24,25,27,28,33,34,42,44,47,52,56,63,89,92,100,101,106,107,111,117],leav:[25,34,40,64,70,96,98,103,106,124,126,134],lectur:[50,74],left:[25,33,48,50,55,57,59,79,87,106,108,118,139],legal:[84,106],lemon:48,len:[15,47,51,62,71,79,84,95,100,101,102,106,108],lend:[25,30],length:[33,46,47,48,51,78,106,110,135],lenovo:35,less:[16,37,54,55,62,70,72,79,85,89,90,92,94,95,97,99,101,106,107,109,111,121,128],less_expensive_funct:95,lession:0,lesson02:[105,136],lesson03:[105,136],lesson07:[24,25],lesson1:[118,123],lesson:[0,28,31,40,56,62,97,100,105,122,124,128,136],let:[13,14,24,25,27,28,33,35,37,48,51,54,55,59,62,63,64,65,66,67,71,74,81,82,84,85,87,90,92,93,95,99,100,101,103,104,105,106,107,108,109,111,116,121,124,129,130,136,139,141],letter:[17,27,29,30,33,37,39,44,50,51,56,66,78,88,95,100,103,106,110,111],level:[2,25,42,59,63,66,67,68,72,92,93,102,109,122,123,126,129,135],levelnam:82,leverag:95,lexic:59,lexical_analysi:103,lf:103,lfd:[92,95],li:[24,59,69],lib:[49,55,72,87,92,93,95,111,129,130,131,139],libari:139,libprof:95,libprofil:95,librari:[18,41,43,48,51,55,57,59,61,63,64,66,69,71,72,74,79,81,82,84,85,87,88,91,92,93,94,95,98,100,101,102,103,106,108,109,111,130,139],licens:[53,92,110,127,128,129,130,131,137,139],lieu:30,life:[18,59,64,97,116,131],lifejacket:81,lifetim:106,lifo:66,lifoqueu:109,light:115,lightweight:[63,74,94,114,128],like:[2,13,14,15,16,17,18,21,22,23,24,25,27,28,30,31,32,33,34,35,36,39,40,41,45,46,47,48,50,51,52,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,70,71,72,73,74,78,79,80,81,82,83,84,85,87,88,89,91,92,93,94,95,96,97,98,99,100,101,103,104,106,107,108,109,110,111,117,120,121,122,123,124,128,129,130,131,134,135,137,139,140,141],liken:1,likewis:90,limit:[33,56,57,59,63,66,73,88,99,106,107,109,128],line:[1,15,17,18,20,21,23,24,25,27,28,32,34,48,50,51,52,56,59,60,62,63,64,66,67,69,70,71,72,73,74,75,85,87,88,97,100,103,106,107,108,109,110,111,117,121,123,124,126,127,129,130,131,132,134,135,137,139,141],line_count:56,line_profil:95,lineandfillmixin:87,linear:[50,103],linearli:95,linefe:103,lineno:[66,95],lineonlymixin:87,liner:15,link:[12,23,24,25,48,53,60,69,86,89,92,124,131,135,140,141],link_color:12,linspac:95,lint:[128,135,140,159],linter:[85,88,93,115,135],linux2:129,linux:[17,27,74,75,95,110,126,131,133,135,140,141],list1:[62,86],list2:[62,86],list3:86,list:[3,13,21,25,26,28,29,31,33,34,35,36,37,40,41,46,47,48,50,51,55,59,60,61,66,69,70,73,80,81,83,84,86,89,90,92,94,95,98,101,103,104,107,108,118,121,123,129,135,144],list_it:79,list_iter:79,list_lab:27,list_of_simple_object:101,list_of_str:95,list_of_stuff:103,list_of_tools_for_code_review:60,list_of_tupl:15,list_of_x:50,listdir:[72,111],listdon:88,listen:[63,89],listing1:93,listiter:79,lite:15,liter:[48,62,69,70,91,93,100,106,109,135],literal_ev:94,littl:[14,18,19,20,21,24,25,30,34,40,50,51,55,57,59,62,67,71,73,80,92,93,95,104,106,107,108,110,117,124,126,134,139],live:[67,92,109],livelock:109,ll:[15,22,23,24,25,30,33,34,37,40,43,47,49,51,52,54,55,56,59,61,62,64,65,67,69,71,72,73,74,80,81,85,87,90,91,92,93,94,95,97,98,100,103,104,106,107,108,111,121,122,123,127,128,129,130,134,138,139,140],llvm:139,lm:[48,106,124],ln:130,load:[31,33,34,66,72,84,92,94,95,108,109],loadabl:31,loadtestsfromtestcas:108,local:[8,28,30,44,48,56,66,67,74,78,80,84,89,93,95,99,100,103,105,121,123,124,126,131,136,139],local_n:84,local_vari:66,localhost:89,locat:[34,55,56,69,87,92,95,131,139],lock:55,lock_exercis:109,log:[16,25,55,66,67,89,92,95,101,121,159],logfil:82,logged_func:67,logging_add:67,logic:[28,32,33,34,35,40,41,51,57,59,60,71,95,100,106],logrecord:82,long_descript:92,longer:[51,56,73,88,95,103,108,116,120,121,126],longer_text:51,longest:[30,106],look:[1,2,13,15,17,18,21,22,24,25,27,28,30,33,34,35,36,37,39,45,46,47,48,50,51,52,54,55,56,59,61,65,66,69,71,73,74,75,78,79,80,81,84,85,87,89,90,92,95,98,99,100,101,102,103,104,106,107,108,121,122,123,124,126,128,134,135],lookup:[69,95],loop:[15,25,26,27,28,34,35,36,46,51,63,64,65,66,69,79,80,83,95,97,100,101,103,106,107,110],lose:[107,108],lost:[2,24,50,66,106,134],lot:[2,12,15,22,24,25,33,34,35,40,48,49,50,51,52,54,55,56,59,62,63,65,66,67,70,72,73,74,79,80,81,82,84,85,87,91,92,94,95,97,98,100,101,103,104,106,107,108,110,111,121,122,123,124,127,128,129,130,131,134,139],love:94,low:[33,51,63,102,109],lower:[17,69,84,101,103],lowercas:[27,93],lowest:[106,109],lprof:95,lpthw:2,lru_cach:67,ls:[63,110,124,127,131,139],lst:80,lt:95,luciano:[67,79],luck:24,luckili:134,lumberjack:15,luminari:71,lump:89,lye8w04erni:55,m:[50,51,52,55,56,66,67,68,71,85,87,88,89,93,95,103,106,107,108,113,121,123,124,129,130,131],mac:[35,92,111,126,128,130,135,140],macbook:110,machin:[14,30,63,73,94,95,99,105,108,111,121,123,124,126,128,130,134,136,138],maco:74,macport:[92,130],macroman:111,made:[27,33,39,48,51,55,56,67,84,85,89,97,100,111,123,124,126,135,140],magic:[0,13,34,53,66,84,93,101,157],magicmock:108,magnitud:108,mai:[1,2,12,15,16,21,24,25,30,31,33,34,36,38,39,40,48,49,50,51,52,54,55,56,59,61,62,63,65,66,71,74,79,80,82,84,85,87,88,89,90,91,92,94,95,98,100,101,103,104,105,106,108,109,110,111,118,121,122,123,124,126,128,129,130,131,135,136,141],mail:[28,57,60,66,69],maillroom_test:121,mailroom2:68,mailroom:[3,4,5,6,9,10,19,41,92,93,113,121,123,145,146,147,148,149,150,152,153,158,159],mailroom_fp:30,mailroom_oo:[30,33],mailroom_test:121,main:[25,28,33,34,37,39,40,42,55,63,74,79,82,90,92,106,107,108,115,123,124],maintain:[33,56,59,68,74,79,89,92,95,96,106,108,109,115,126],mainten:[95,107],major:[30,92,95,106,111,128,137,140],make:[1,12,13,14,15,17,20,21,24,25,27,28,29,31,32,33,35,38,40,41,44,45,46,47,48,50,51,53,54,55,57,59,60,61,62,63,65,66,67,68,69,70,72,73,74,75,77,78,82,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,115,118,121,122,126,127,128,129,130,133,134,135,136,137,138,139,142],make_lett:40,make_sent:51,make_word:51,maker:139,malcolm:15,malform:106,malici:94,mammal:87,man:[16,72,103],manag:[25,30,33,35,41,53,63,71,72,82,84,85,87,89,92,94,105,106,107,108,118,123,124,126,128,129,130,131,136,139,159],mang:[105,128,136],mangl:[84,93],mangledsingleton:84,mangler:84,mangler_dec:84,mango:[15,17],mani:[2,14,21,24,25,30,31,49,51,53,55,56,64,67,68,69,71,74,77,78,84,88,91,92,94,95,100,103,106,107,108,109,110,111,115,117,121,123,129,131,134,138,140],manifest:92,manipul:[4,30,33,34,40,56,59,72,74,79,91,92,103,128,141,149],manner:[115,140],manpagez:72,manual:[74,89,106,115,122,135,139],manual_adv_debugg:66,map:[0,31,51,57,59,61,63,68,69,72,79,80,84,85,89,90,94,95,101,106,109,111,157],map_async:109,mapper:74,mappingproxi:84,mappingproxytyp:84,mapreduc:94,marathon:17,march:129,mari:[62,85,123,126],maria:[66,81],mariakathryn:66,marie_curi:123,mark:[24,28,35,51,55,59,70,93,106,121],mark_zuckerberg:39,marker:[93,122],markup:[24,53],marshal:92,marta:81,martelli:[71,86],mask:73,mass:[51,90,95],massag:[34,92],massiv:87,mast:110,master:[15,53,63,73,79,84,87,121,122,123],match:[27,30,51,64,71,72,79,82,88,89,92,95,106,108,138],materi:[1,2,53,55,59,73,81,90,94],math:[13,57,62,69,85,90,92,95,98,104,108],mathemat:[73,74,90,99,101],matlab:90,matter:[51,55,66,69,85,88,101,103,106,108,124],max:[93,100,135],max_don:30,max_length:84,maxfail:66,maximum:[49,73,93,99,108,109,135],maxsiz:109,mayb:[12,13,19,20,24,25,29,32,33,36,40,50,55,65,66,67,71,78,82,84,85,86,87,92,99,101,103,104,106,110,111],mb:94,mccabe:93,mccci:106,mccxxvi:106,mcdlxxxv:106,mcdxxiv:106,mckeller:[81,110],mckinlei:66,mcmc:106,mcmlxxii:106,mcmxciii:106,mcmxlvi:106,mcs5ovhv9s4:55,mcx:106,md5:69,mdccclxxxviii:106,mdcccxxxii:106,mdccliv:106,mdclxvi:106,mdcvii:106,mdix:106,me:[24,25,40,67,71,81,82,93,95,101,121,128,134,139],mean:[8,17,20,24,25,27,33,34,35,37,39,40,45,46,48,55,56,57,59,63,66,67,69,70,73,74,80,83,84,85,87,88,90,91,92,94,96,97,100,101,103,104,105,106,107,108,109,110,111,118,121,123,124,126,128,131,134,136,139],meaning:[33,88],meant:25,meantim:130,meanwhil:55,measur:[16,108,134],meat:15,mechan:[59,63,93,94,102],medium:108,meet:[30,51,66,108,109],melia:95,member:[62,69],membership:56,memoiz:67,memori:[20,24,47,56,63,72,79,89,94,111,135],memory_profil:95,memprof:95,mention:[40,55,66],menu:[28,30,33,39,40,115,128],mercuri:126,mere:70,merg:[92,122,123],mess:[30,51,92,94,111,121,130],messag:[7,16,44,45,48,63,64,65,66,71,82,89,93,103,108,111,118,121,123,124,137],messi:60,met:108,meta:[24,25,31,84],metaclass:113,metadata:[24,92],metaprogram:159,method:[0,7,13,15,24,25,29,30,31,33,34,37,39,41,44,46,47,48,50,51,53,54,55,56,58,59,60,62,63,64,65,66,67,68,79,81,83,85,88,91,93,94,95,107,109,111,139],methodolog:40,metric:95,mexico:90,mg:95,mi:56,michael:[21,81],micheal:94,mick:21,micro:[63,110,128],micropython:[110,128],microsoft:[128,140,141],microtool:93,mid:63,middl:[24,46,90,100,106,111],middle_nam:89,middlewar:55,midwai:109,might:[1,15,21,24,25,29,30,31,33,35,40,48,50,51,53,55,56,64,66,69,70,71,81,82,84,85,87,90,91,92,95,104,105,106,107,108,109,111,121,124,135,136,159],milk:51,milkwood:[51,79],million:[22,108],mimic:109,mimick:124,min:100,min_don:30,mind:[7,25,33,38,40,59,69,77,79,84,85,87,89,90,93,95,108,110,118,123,124],mine:[25,95],mingw64:131,minhhh:[8,101],miniconda:[89,92,95],minim:[56,71,98,115,140],minimum:111,minor:[92,100,137],minu:[23,79],minut:[70,103,106,107,124],mirg:[81,110],mirror:[18,89],misconcept:89,misguid:90,mispel:66,miss:[41,54,61,71,92,97,108],mission:[32,42],misspel:41,mistak:[111,115,135,140],mix:[82,85,90,91,93,100,111],mixin:87,mkaz:[48,103],mkdir:[43,72,85,89,123,139],mmcccxliii:106,mmccxii:106,mmcdxcix:106,mmclii:106,mmcmlxxv:106,mmdcccxcii:106,mmdccxxiii:106,mmdcxlvi:106,mmdlxxiv:106,mmlxxiv:106,mmm:106,mmmcccxiii:106,mmmccl:106,mmmcdviii:106,mmmclxxxv:106,mmmcmxcix:106,mmmcmxl:106,mmmdccclxxxviii:106,mmmdcccxliv:106,mmmdccxliii:106,mmmdcx:106,mmmdi:106,mmmli:106,mmmm:106,mmmmm:106,mmmmmmmmm:106,mo:51,mock:[33,34,112,158],mock_input:108,mock_method:108,mock_object:108,mod1:85,mod2:85,mod3:85,mod:15,mode:[20,33,34,66,82,92,94,107,108,117,124,139],model:[24,31,33,34,63,73,84,99,102,107,111],modern:[24,33,55,68,85,92,94,115,128,141],modif:40,modifi:[51,63,66,84,87,103,105,109,115,124,134,136],modu:51,modul:[0,5,13,16,18,20,30,31,34,35,42,44,45,51,52,53,54,55,56,57,59,63,64,65,66,67,69,71,73,74,75,79,83,84,88,93,95,96,99,100,101,102,103,106,107,128,129,130,131,152,159],modula2:91,modular:[40,60],module1:92,module2:92,module_a:92,module_b:92,module_nam:92,module_vari:93,modulea:92,modulei:92,modulenam:85,modulenotfounderror:51,modulex:92,modulez:92,modulo:[44,56,101],modulu:56,moin:[63,69,94,95,100],moment:[33,43,57,71,87,107],monei:40,mongo_data:89,mongocli:89,mongod:89,mongodb:[83,94],mongodb_beak:89,mongolog:89,monik:[57,89],monitor:82,monokai:135,monstros:66,moral:100,more:[1,2,6,8,14,15,16,21,22,23,24,25,26,27,28,29,31,32,33,34,35,37,39,40,41,48,50,51,52,55,57,59,60,61,62,63,64,65,67,70,71,72,75,77,78,79,80,81,84,85,89,90,91,94,97,98,99,101,104,105,107,108,109,110,111,115,118,119,121,123,124,127,128,129,130,131,135,136,137,139,142,151],mortem:66,most:[6,7,18,24,25,30,33,35,40,43,53,54,55,56,57,59,60,61,62,63,64,65,66,67,69,71,72,73,74,78,79,82,84,85,86,87,91,92,93,94,95,96,97,98,99,100,101,102,103,105,106,107,108,109,110,111,117,118,124,126,128,129,135,136,139,141],mostli:[47,49,55,63,64,66,72,73,87,88,89,92,108,111],motiv:54,mous:[55,87,135],move:[25,34,54,55,59,63,66,71,83,91,92,98,106,109,120,121,122,139],movi:106,mozilla:94,mr:[18,51],mro:[9,104],ms:[92,94,106,111,128,141],msc:131,msdn:141,msft:94,msg:[93,103,108],mssql:94,much:[15,16,18,24,25,31,33,34,37,38,40,49,51,55,56,57,59,60,62,63,64,67,70,71,73,74,82,84,85,87,90,91,92,93,95,98,99,100,103,106,107,108,109,111,122,123,124,127,128,139,140,141],mul:83,mulitpl:63,multi:[28,55,56,63,66,95,101,112],multiarrai:95,multiinst:126,multipl:[0,22,24,25,33,34,46,49,51,53,54,55,59,61,63,65,70,72,73,74,79,82,85,89,91,92,98,100,101,104,105,106,108,109,110,117,118,121,122,123,124,136,139],multiplc:100,multiple_inherit:[9,87],multipli:[13,23,27,30,50,56,73,99,106,108],multiprocess:[63,159],multitask:[55,63],multithread:63,muppi:95,muscl:135,music:14,must:[51,56,61,63,64,66,69,71,73,74,78,79,84,85,87,94,99,103,106,109,111,113,134],mutabl:[6,56,69,80,84,94,151],mutat:[51,84,86,90,98,100],mutual:[63,109],mwh:86,mx:50,mxliii:106,my:[24,25,37,39,51,54,68,79,84,89,91,95,100,103,106,118,121,123,124,134,135,137],my_arrai:47,my_cool_cod:66,my_decor:67,my_for:79,my_fun:101,my_func:[85,107],my_it:79,my_list:100,my_method:84,my_mod:107,my_modul:66,my_one_func:68,my_packag:85,my_program:108,my_python_fil:88,my_quad:101,my_sequ:79,my_vari:66,my_zero_func:68,myclass:84,myexcept:66,myfunctestcas:107,mymetaclass:84,myprog:108,myproject:135,myscript:66,mysql:94,mysteri:[63,109],mytest:107,mythread:109,n1:[56,95],n2:56,n:[23,25,26,35,45,50,56,64,66,67,69,71,72,73,76,78,79,93,94,95,99,100,101,103,106,109,111,126,131,134],n_chunk:109,nail:[25,90],naiv:108,nall:56,name1:[59,85],name2:[59,85,103],name3:[59,85],name4:85,name:[5,15,16,17,18,21,23,24,25,28,29,30,33,34,35,37,39,40,44,48,50,51,52,54,56,61,62,64,66,67,69,70,71,72,73,74,75,78,80,82,84,85,87,89,92,95,98,104,106,107,108,109,110,111,118,121,122,123,124,126,135,139,150],name_1:92,name_2:92,name_3:92,name_mangl:84,name_of_remot:121,namedtupl:61,nameerror:[18,25,43,56,84,106],namesapc:84,namespac:[6,28,59,67,92,93,98,103,104,107,150],nand:25,nano:126,narrow:109,natasha:124,nativ:[4,44,55,56,89,95,101,107,141],natur:[31,45,48,55,62,69,84,90,107,121],navig:[1,66,110,118,135],nba:55,nba_stats_async:55,nba_stats_sync:55,nc:[100,104],ncall:95,nearli:111,neat:[33,98],neccesasri:40,necess:107,necessari:[66,123,126,128],necessarili:51,ned:[93,108],nedbatcheld:108,need:[7,12,13,16,17,18,20,21,23,24,25,27,28,30,31,33,34,35,38,40,41,42,48,49,50,51,52,54,55,56,57,59,60,62,63,64,65,66,68,69,70,71,72,73,74,77,79,80,82,83,84,85,86,87,88,89,90,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,115,117,118,121,122,123,124,125,126,128,129,130,131,134,135,136,137,138,139,140,142],neg:[33,95,99,100,106,110],negoti:93,neither:[68,109],neo4j:89,neo4jpw:74,neo4jus:74,neonx:74,nervou:25,nest:[15,24,25,59,82,85,94,100],net:[2,3,4,7,66,67,84,86,87,92,95,108,111,130],network:[55,63,64,74,89,94,95,109,111,118,135],networkx:74,never:[24,38,56,57,62,69,71,72,73,78,81,85,88,92,93,94,106,107,110,111,121,123],nevertheless:[79,103],new_cont:25,new_dict:[62,69],new_feature_branch_nam:118,new_fruit:35,new_funct:67,new_i:56,new_list:[36,62],new_mocked_input:108,new_set:62,new_text:51,newcircl:104,newcod:81,newcom:[55,81],newer:[103,115,139,140],newli:[59,74],newlin:[23,24,25,51,56,72,103,134],newsapi:49,next:[2,18,23,24,25,30,33,45,51,55,56,65,66,67,69,72,73,74,79,87,89,102,103,106,107,109,118,123,124],next_class:87,ng:103,nice:[13,16,24,25,28,31,32,33,48,50,51,55,61,62,67,71,73,74,78,79,80,82,84,85,87,88,89,92,93,94,95,99,100,103,106,108,110,111,115,116,118,120,123,124,128,129,130,134,135,137,139,141],nicer:[67,74,93],nicknam:[21,123],nifti:[16,22,32,45,54,57,58,64,67,69,71,72,79,81,82,84,92,100,101,103,106,107,108,111,134,135],nightmar:[68,139],nine:51,nitti:106,nix:[72,75,110,111,130,131,138],nn:95,no_error:56,noarch:129,nobodi:[87,106],node:[74,94],nois:135,noisili:106,non:[33,47,50,51,55,56,63,66,69,72,73,79,88,92,94,100,103,106,108,109,111,126,131,134,139],noncrit:95,none:[15,19,24,25,36,55,56,57,65,67,69,71,79,80,83,84,86,92,93,95,96,98,100,102,106,107,108,139,141],nonetyp:25,nonloc:56,nonstandard:94,nonzero:57,nope:[25,71,103,111],noplugin:126,noqa:93,nor:[33,56,68,88,128],normal:[55,78,84,89,103,106,126,134,139],north:90,northwest:90,northwestern:90,nose2:107,nose:[107,108],nosess:126,nosql:[74,83],notabbar:126,notabl:[69,94,111],notat:[31,48,70,84,94,98,100],note2:55,note3:126,note:[2,6,7,12,13,15,16,20,23,25,27,30,33,35,36,40,44,45,46,50,51,53,54,55,56,59,62,63,66,67,70,71,73,74,77,80,82,84,85,88,89,92,94,95,98,100,101,103,104,105,106,107,108,110,111,118,121,122,123,124,126,129,130,131,135,136,139,159],notebook:110,notepad:[126,128,131,141],noth:[24,25,52,55,56,57,62,65,68,70,72,78,80,84,93,104,106,108,124,134,135],notic:[25,41,51,54,56,59,67,69,87,92,93,100,101,106,111,122,126,134,139],notif:109,notifi:[109,124],notimplementederror:[13,25],notion:57,noun:[51,91,122],nov:121,novic:116,now:[2,10,13,17,22,23,24,25,28,30,32,33,34,35,37,38,40,48,51,52,54,55,57,59,62,64,66,67,68,69,70,71,72,73,74,79,80,84,85,87,88,89,92,93,95,96,97,98,99,100,102,103,104,105,106,107,109,110,111,118,120,121,122,123,124,126,128,129,130,131,134,135,136,139,141],np:[85,92,101],nstring:56,nt:111,nth:[26,45],nthat:103,nthi:25,nul:102,num:[15,28,48,55,66,78,100],num_in:71,num_star:56,number2:139,number:[2,13,16,17,22,23,24,25,26,27,28,31,33,37,38,44,46,48,49,50,51,54,55,56,57,59,61,62,63,65,66,67,68,69,70,71,72,74,78,79,81,83,84,85,89,93,94,95,97,99,100,101,103,104,106,107,108,109,111,114,118,121,124,128,129,131,134,135,139],numer:[13,44,45,50,57,81,82,103,109],numpi:[63,85,90,92,95,101],o1:84,o2:84,o:[55,62,63,69,78,84,100,101,103,109,134],obei:104,obj:[33,56,84],object:[0,8,9,10,16,24,25,26,28,30,31,32,35,41,48,50,53,56,57,59,61,62,63,64,65,66,67,68,69,70,72,74,79,82,83,85,87,92,95,97,99,100,102,103,107,109,111,124,127,128,129,157],object_canva:87,object_typ:68,objectid:89,objectthink:86,objgraph:95,obliqu:25,obscur:[80,101,111],observ:[45,106,123],obtain:[63,95],obviou:[33,63,66,68,87,103,106,124],obvious:[51,106],occas:[66,124],occasion:[104,126],occur:[16,25,30,55,56,64,71,82,106,108,109],occurr:[27,63,87,100],oct:121,octal:103,octav:90,oculd:79,od:102,odd:[19,25,57,71,72,87,92,100,103,138],oddli:130,off:[16,25,43,66,79,88,97,98,100,106,108,109,118,121],offer:[30,81,89,110,115,126,128,134,135,140],offic:121,offici:[48,62,69,70,74,76,81,88,92,97,101,110,122,124],offset:72,often:[22,24,35,41,44,48,55,56,57,60,62,63,66,69,70,71,79,81,82,84,85,88,89,90,91,92,94,95,100,101,102,103,104,106,107,108,109,110,118,121,122,123,128,129,130,131,135,139],oftentim:[47,92],oh:[79,86,106],ok:[15,24,25,28,36,40,49,51,56,66,71,89,100,106,128,135,139],okai:60,old:[25,27,51,55,67,68,84,88,92,94,95,104,106,111,120,121,124,128,139],old_dict:69,old_list:36,older:[55,69,89,92,95,97,103,110,123],oliv:106,omega:50,omiss:53,omit:[66,124],onc:[13,15,20,24,25,27,28,29,31,32,33,34,49,50,51,52,55,59,62,63,65,71,72,73,74,79,81,83,84,87,88,92,93,94,98,103,104,105,106,107,108,109,114,115,118,122,123,124,126,130,131,134,136,139],one:[1,2,7,8,13,15,16,17,20,21,23,24,25,26,27,30,32,33,34,35,37,41,42,43,44,45,46,48,49,50,51,53,54,55,56,57,59,60,62,63,64,65,66,67,68,69,71,72,73,74,75,77,79,80,81,82,84,85,86,88,89,90,91,92,93,94,95,98,99,100,101,103,104,105,106,107,108,109,110,111,116,118,120,121,122,123,124,126,128,129,130,131,134,135,136,139],one_fil:[105,136],one_line_comprehension_her:15,one_mor:72,onelinetag:[24,25],ones:[14,24,62,72,73,89,91,92,95,98,101,105,106,108,111,124,135,136,139],oni:[57,106],onli:[2,7,8,12,13,15,18,20,21,24,25,28,29,30,33,34,35,40,41,47,49,50,51,55,56,57,59,62,63,64,65,66,68,69,71,72,78,79,80,81,82,83,84,85,86,87,88,89,91,92,93,94,95,97,98,99,101,103,104,105,106,107,108,109,110,111,115,118,119,121,122,123,124,126,129,130,134,136,139],onlin:[24,35,51,62,74,77,81,97,110,111],onto:[66,83,123,124,134],oo:[7,8,24,33,72,102,103,104,108,153],ooo:103,oop:[6,25,99,104],op:[86,111],open:[20,24,25,30,39,40,51,52,53,55,63,64,71,74,81,82,92,94,107,109,111,115,120,122,124,128,130,135],open_tag:25,openhatch:81,oper:[1,13,15,22,23,30,33,35,39,44,46,48,51,55,57,62,63,67,71,72,79,82,83,91,95,97,100,103,108,115,128,133,138],operand:[57,100],operation:107,opinion:[63,92,93],opportun:[77,109,123,124],oppos:91,opposit:[106,124],opt:129,optim:[16,59,63,69,74,89,107,109],option:[2,12,25,28,30,33,34,39,45,49,51,54,55,61,62,66,69,70,71,73,74,75,78,82,86,87,93,95,100,103,105,106,108,110,115,116,118,120,126,128,129,130,134,135,136,139,141],oracl:94,orang:[27,35,48],orangutan:15,ord:[44,100,103,111],order:[24,25,33,34,46,48,50,51,52,53,54,55,56,59,61,64,66,67,73,82,90,93,94,95,98,100,102,103,106,107,108,113,124,126,128,134,142],order_of_oper:110,ordereddict:[61,69,102],ordin:111,ordinari:[55,71,111],oreilli:81,org:[2,6,7,9,16,24,27,33,41,48,50,51,54,55,57,59,60,61,63,64,66,67,68,69,72,74,77,79,80,81,82,84,87,88,89,90,91,92,93,94,95,96,100,101,103,104,107,108,109,110,111,116,120,128,129,130,131],organ:[33,34,85,89,108,124,126],orient:[0,1,8,9,10,25,28,30,32,41,53,68,89,94,98,104,107,129,157],oriented_program:[91,104],origin:[27,28,30,50,51,59,62,67,69,74,91,100,106,107,118,123,124,126],orm:[74,84,94],os:[17,20,27,39,63,71,75,89,95,109,110,111,121,131,133,134,135,140,141],oserror:71,oss:121,osx:130,other:[1,13,16,18,19,21,24,25,27,28,30,31,33,34,35,38,39,40,45,46,51,52,55,56,57,59,60,61,62,66,67,68,70,71,72,73,78,80,82,84,85,86,87,90,91,92,93,98,99,101,104,105,106,108,110,111,115,118,121,122,123,124,126,128,129,130,131,135,136,138,139,140],other_count:56,other_func:67,otherwis:[50,56,84,95,100,103,104,108,128,131,134],our:[24,25,29,30,33,35,41,51,52,56,67,74,81,84,87,90,94,95,96,99,100,106,107,108,118,122,126,131,139],ourselv:[25,92],out:[2,13,15,17,18,22,23,24,25,26,27,28,29,30,33,34,37,40,41,42,43,44,47,48,51,54,55,56,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,76,77,78,79,80,81,82,83,84,85,86,87,93,94,95,96,97,98,99,101,102,103,104,106,107,108,109,110,111,115,118,122,124,126,128,134,137,139,140],out_fil:[24,25],outdat:121,outer:[59,62,67],outfil:[24,25,62,72,94],outlin:[30,87,106,124],output:[15,18,23,24,25,28,32,33,35,41,51,63,67,72,82,90,93,95,106,107,108,109,111,131,134],output_fil:95,outsid:[33,56,59,63,73,85,91,92,94,106,123],over:[2,28,30,35,40,55,56,57,60,62,64,68,69,72,78,79,81,82,83,86,90,92,94,95,100,106,107],overal:106,overdo:[62,85],overflow:[86,92],overhead:[95,109],overlap:[63,93],overload:[72,91,92,122],overrid:[13,24,25,59,84,91,92,101,107,135],overridden:[25,84],overrod:25,overus:62,overview:[7,55,60,61,67,68,79,107,119,123,126,127,129,153],overwrit:[94,106],own:[1,12,22,24,25,51,53,55,59,61,63,64,65,66,67,71,79,81,82,84,85,87,89,93,94,95,98,106,107,109,110,117,122,124,126,127,128,129,140,141],p189616:15,p195669:52,p1:101,p3:98,p:[24,25,27,67,72,78,92,95,98,100,131,135],p_decor:67,pace:[81,106],packag:[28,31,32,33,49,51,53,55,63,64,65,66,72,73,74,77,84,87,91,93,94,95,98,106,107,108,111,112,113,117,128,129,130,131,135,141],package_nam:92,packagecontrol:135,packageindex:92,packagenam:[85,92],pad:48,page:[23,24,25,53,74,89,106,107,111,115,121,122,123,124,126,131,133,135,139,140],page_appl:55,pai:[106,121],pain:[24,30,51,94,95,111,129],pair:[25,33,51,62,69,79,106,109,135],palett:135,pan:87,panda:90,papa:92,paper:106,paradigm:62,paragraph:[24,25,51,67],parallel:[10,30,55,62,83,89,157],paralleliz:[63,109],param1:104,param2:104,param:[35,50,51,70,79,108,134],param_nam:108,paramet:[6,13,24,25,30,35,41,45,50,55,67,70,79,80,84,94,98,100,101,102,104,106,108,109,134],parameter:107,paramt:[54,134],paranoid:93,paren:56,parent1:87,parent2:87,parent3:87,parent:[74,87,91,92,104,108,122],parent_conn:109,parenthes:[56,62,80,97,100,103,110],parenthesi:110,parodi:66,pars:[20,25,38,66,106,134],parser:94,part:[1,28,33,35,37,40,41,51,55,56,57,59,60,65,69,71,72,73,83,84,85,87,88,90,91,92,95,96,97,99,100,101,102,103,104,106,107,110,121,124,126,128,130,135],parti:[55,63,64,70,72,92,93,94,108,139],partial:[25,124],particip:124,particulali:139,particular:[10,12,16,24,25,33,45,47,50,51,54,55,59,64,69,71,72,73,77,86,91,92,95,99,100,101,103,106,107,108,109,111,117,118,121,122,123,124,139],particularli:[23,33,57,67,70,71,82,87,92,93,100,103,104,108,111,126,128,130,138],partit:92,partli:[59,92],partygo:70,pass:[0,12,13,15,16,23,25,30,37,38,48,51,53,55,56,57,59,62,63,66,67,69,71,72,73,75,78,80,82,83,84,86,87,92,95,98,99,100,101,102,104,107,108,109,110,111],passiv:56,passportphoto:72,password:124,past:[21,25,30,33,39,89,91,100,106,124,127],pasta:[15,54],patch:[84,92],path:[34,39,63,64,66,71,81,89,101,103,106,122,131,134,135,139,140],pathlib:[20,34,64,92,101],patienc:25,patient:124,pattern:[41,62,64,67,72,84,87,92,95,99,100,104,106,107,109],paul:[28,35],paul_allen:39,paus:[55,79,85,124],payload:102,pbear:44,pbpython:141,pc:131,pcottl:122,pdb_break:66,pdf:[55,74,81],peac:[89,93],peach:27,peak:2,pear:[27,35],pedant:25,pen:87,peopl:[22,25,55,60,66,83,88,89,90,92,93,106,108,109,110,111,117,123,128],pep343:64,pep428:72,pep485:50,pep8:[33,77,88],pep8_max_line_length:135,pep:[50,54,57,68,72,77,84,85,92,94,103,108],per:[20,24,29,63,70,71,92,95,109,111,124],percal:95,percent:33,percentag:[95,108],perfect:[24,51],perform:[49,67,74,79,82,91,94,101,104,107,110,115,121,135,140,159],performancetip:95,perhap:[19,25,51,52,67,74,90,130,134],perhpa:130,period:[51,55,74],perman:[29,128],permiss:[25,38,66,71,90,121,126],permit:60,pernici:100,persist:[87,89,159],persistantdict:89,persistencetool:94,persistentlist:89,person:[34,45,51,63,84,89,92,93,106,107,117,121,135],perspect:[87,90],pformat:94,phdcomic:126,philanthropist:30,phillip:8,phoenix:87,phone:[33,89],php:[8,69,101,120],phrase:24,physic:95,pi:[13,69,85,92,98,104,110],pick:[51,52,79,106,108,139],pick_random_pair:51,picki:55,pickl:109,picklabl:94,pickleabl:63,pictur:[60,90,95],pid:25,piec:[24,25,51,56,60,103,104,106,127],pike:63,pile:[32,126],pilgrim:106,pillow:87,ping:124,ping_serv:55,pip3:129,pip:[32,34,49,52,55,74,76,87,88,89,93,95,106,107,108,113,128,139],pip_build_root:139,pipe:55,pipenv:139,pipermail:[57,69],pivot:122,pkg_name:92,pkl:94,place:[15,24,25,34,38,48,53,54,59,66,70,72,81,82,84,85,86,88,91,92,94,100,101,103,106,107,108,110,111,122,123,124,134,139],placehold:56,plai:[1,14,24,51,56,59,64,65,74,84,85,87,96,106,134],plain:[24,25,27,74,94,106,110,111],plan:[66,81,135],planningadinn:81,plate:66,platform:[25,51,52,66,72,106,107,115,117,123,126,133,138,140],platon:111,platypi:87,platypu:87,play_with_import:67,play_with_scop:59,player:55,pleas:[1,35,103,116,124,126,128],pleasant:127,plenti:121,plu:[23,108,128],plug:[94,108],pluggabl:[55,89],pluggi:[25,51,52,106,107],plugin:[93,107],plural:88,pm:66,png:95,poach:15,poet:51,point:[7,23,25,28,30,32,33,34,40,41,44,48,50,51,54,55,56,59,63,64,65,73,74,76,81,84,85,87,93,98,100,101,103,105,106,107,109,110,111,118,122,123,124,126,136,139],pointer:28,pointsobjectmixin:87,poke:100,pokerhand:104,poli:87,polic:109,polit:[90,109],poll:55,pollut:139,polygon:[68,87,95],polymorph:[25,68,91,98,104],pool:[49,55],poor:[16,93,101,128],poorli:91,pop:[25,55,61,66,100,131],popen:63,popitem:69,popul:[28,62,69,70,118],popular:[41,74,81,126,129],port:[82,89,107,108,109],portabl:94,portion:[40,122],pos1:54,pos2:54,posit:[12,25,48,50,54,59,72,73,79,80,104,110,111,121],position:73,posix:92,posixpath:[72,101],possibl:[12,24,28,32,33,40,51,56,57,66,68,71,73,74,85,87,88,91,92,94,98,104,106,107,126,131,135,141],post:[8,22,24,55,66,73,74,82,89,91,92,101,108,111,124,141],postcondit:104,poster:81,postgresql:94,postmortem:66,potenti:[24,33,34,35,51,56,63,67,84,94,104,109],pour:30,pov:95,pow:108,power:[24,25,28,48,54,59,67,69,73,80,82,89,94,97,98,100,102,103,104,106,107,108,119,122,127,128],powershel:131,pp:[66,95],ppc:92,pprint:94,pprof:95,pr:[30,105,121,124,136],practic:[1,12,20,21,40,44,51,59,65,70,79,81,82,85,86,91,92,93,98,100,101,103,128,142],practition:106,pre:[49,55,56,59,62,67,83,84,104,118,138],preced:[66,110],precis:[71,95,106],precondit:104,preemptiv:[55,63],prefer:[24,33,34,35,39,60,74,82,85,90,124,128,130,134,135],prefix:[66,92],preinstal:129,preliminari:135,prematur:95,prematureoptim:95,prepar:[87,103],prepend:[52,134],prerequisit:128,presenc:57,present:[40,47,55,56,66,81,94,100,106,115,134,139],preserv:[44,64,69,79,103,106],press:[66,118],presto:80,pretti:[12,13,22,24,25,31,33,34,40,41,50,51,55,56,57,59,60,61,62,64,66,68,72,73,74,77,80,82,84,86,87,92,93,99,100,101,103,106,107,108,109,111,121,124,128,129,135],prettier:108,prevent:[39,74,84],previou:[7,15,25,45,50,51,55,56,59,64,71,73,79,92,95,99,101,106,121,122],previous:[25,33,64,67],price:[81,94],primari:[24,54,74,92,100,106,111,124,126],primarili:55,prime:[79,84],primer:25,primit:109,principl:[6,71,91,139],print:[12,13,15,16,18,20,22,24,25,26,27,28,33,34,35,36,38,39,40,45,48,51,54,55,56,59,62,64,65,67,69,71,72,73,78,79,80,81,84,85,86,87,92,93,95,96,98,99,100,101,102,103,106,108,109,111,128,134,137],print_arg:67,print_glob:59,print_grid2:23,print_grid:[23,121],print_lett:40,print_stat:95,printabl:84,printer:[2,142],prioriti:[90,110],priority_numb:109,priorityqueu:109,privaci:130,privat:[25,59,91,93,124,139],privileg:139,pro:[66,110,122],proabli:126,prob:[15,52],probabl:[24,25,33,34,35,48,49,51,56,60,63,64,66,69,78,85,88,93,94,95,101,109,111,129,131,135,141],probali:111,problem:[23,30,33,49,50,55,57,59,61,63,66,71,73,74,79,84,90,91,99,106,108,121,122,134,142],problemat:1,problemsolv:106,proc:109,proce:[25,141],procedur:[25,51,91,98],process:[16,24,25,36,44,49,52,55,70,71,72,73,77,83,89,94,95,106,107,108,111,118,122,124,126,139,140,149],processor:[63,95],proclaim:115,produc:[25,33,37,45,48,51,56,62,79,95,103,106,109,134],produce_item:109,product:[1,55,56,60,62,81,82,83,103,106,110,115,117,118,128],prof:95,prof_dump:95,profession:[22,53,60,74,118,124,138],profil:[139,159],profilebeforeoptim:95,profilerstart:95,profilerstop:95,program:[0,1,2,8,20,21,22,24,30,31,32,34,37,38,39,40,41,47,50,51,54,56,57,59,61,62,65,66,68,70,71,72,73,74,78,81,82,84,85,88,89,92,93,94,95,96,98,100,103,104,105,106,109,112,113,119,120,122,124,125,126,128,129,130,131,136,137,138,139,141,142,159],programm:[1,22,81,89,95,102,106,117,128],programming_paradigm:91,programminginpython:[53,126],progress:[28,51,78,106,109],project:[24,25,28,30,33,51,55,60,66,74,81,91,92,105,107,108,115,118,122,123,124,126,128,130,134,136,139],project_hom:139,promis:[106,135],prompt:[27,28,35,66,110,115,116,118,124,128,130,131,134,139,140],prompt_command:134,prone:[64,72,78,111],pronounc:101,propag:[64,71],proper:[18,24,25,32,34,51,55,59,65,69,72,84,85,91,97,107,122],properli:[24,25,44,45,64,69,71,92,106,107,135],properti:[0,13,30,33,51,53,74,84,90,91,100,102,111],properties_exampl:96,property_:96,property_ugli:67,proport:[69,100],propos:88,protect:[91,109],protein:103,protocol:[8,13,55,64,74,102,111,154],prove:[66,106],provid:[2,24,25,32,33,38,44,48,51,54,55,57,59,61,64,65,66,67,69,70,71,72,73,74,79,81,82,84,87,89,92,94,95,98,100,101,103,104,105,106,107,108,109,111,115,117,118,121,122,123,124,127,128,130,134,135,136,138,139,140,141],provis:1,proxi:[69,87],ps1:134,pseudo:51,pseudonym:51,psv:103,psych:22,pth:72,publish:81,pull:[27,45,48,53,54,95,100,105,118,121,122,123,126,136],pun:[30,103],punchcard:106,punctuat:[44,51],puppi:8,purchas:94,pure:[24,25,63,91,92,95,106],purge_fruit:35,purpl:12,purpos:[30,33,50,56,66,67,69,84,100,106,108,110,118,123,124,131,139],push:[27,45,48,55,66,74,95,105,118,121,123,136],pushup:125,put:[13,18,23,24,25,28,32,34,39,40,41,42,48,52,55,56,61,62,63,64,66,68,71,74,77,80,84,85,87,89,91,92,93,94,98,99,100,103,104,106,107,108,109,110,121,123,124,126,134,135,139],puzzl:[57,106],pwd:[124,127,139],py210:124,py210a:72,py2:[64,93,139],py300:[66,108],py310:124,py3:[50,64,111,139],py:[15,16,17,18,24,25,26,27,28,30,33,34,41,42,43,44,45,48,51,52,55,59,64,66,67,72,75,77,79,82,84,85,87,89,93,94,95,96,98,101,102,105,106,107,108,109,110,111,121,123,124,127,136,137,139],pyarg:[34,92],pyc:[85,111],pycassa:89,pycharm:[66,75,110,128],pycodestyl:88,pycon:[7,63,71,81],pyconslid:7,pydanni:103,pydev:66,pyformat:[48,103],pylib:108,pylibrari:92,pylint:115,pymbook:81,pymongo:89,pymotw:[61,64,79,82],pympler:95,pypa:92,pypackag:92,pypi:[63,66,87,95,108,110,128],pyprof2calltre:95,pystat:63,pytest:[6,24,25,32,34,40,51,52,66,79,92,106],pytest_fixtur:108,python210coursemateri:[51,106,107],python2:[69,72,79,84,95,111,129,135,139],python34:129,python35:129,python37:131,python3:[2,4,7,15,17,51,52,56,66,72,75,81,83,84,85,88,92,93,103,110,111,129,130,135,137],python3_magic_method:[8,101],python:[1,3,4,6,7,8,9,12,14,15,16,17,18,21,23,24,25,27,28,30,31,32,33,35,37,41,42,44,46,48,49,50,51,52,54,55,58,59,60,61,62,64,65,67,68,69,70,71,72,73,74,77,80,82,83,84,85,86,88,90,96,99,101,102,103,105,106,107,108,109,112,113,114,116,122,123,124,125,126,127,132,133,136,139,144,150,153,159],python_101:81,python_interpret:135,python_koan:15,pythoncert:[53,123,126],pythoncertdevel:[101,107],pythonchb:[121,124],pythonclass:[24,25],pythonhost:[92,95],pythonlib:[92,95],pythonspe:95,pythonstuff:[18,72,101,107,108],pythontip:6,pythontutor:[66,81],pyvideo:[7,63],q:[56,63,109,128],qty:94,quack:[24,100],quad1:50,quadrat:[50,95,101],quadratur:50,quark:91,quarter:[77,94,123],queri:[13,27,74,89,94,100],question:[51,56,60,64,84,86,87,91,105,120,121,136,139],queue:[55,63],quick:[2,25,43,50,51,60,80,81,84,103,106,107,111,115,127,129,139],quickest:66,quicki:79,quickli:[33,48,55,63,66,76,92,106,115,138],quickref:[127,129],quickstart:[10,30,157],quit:[10,25,28,33,35,39,43,51,59,69,79,81,87,89,95,102,107,111,128,135,137,141],quot:[25,56,70,94,103,110],quotat:[51,104],quote_minim:94,qxxiii:106,r:[13,55,56,64,72,78,90,94,95,100,103,139],rabbit:66,race:63,race_condit:109,radic:84,radioact:126,radiu:[13,104],raid:51,rais:[1,13,16,18,19,24,25,30,47,56,57,59,64,65,69,72,79,80,84,100,106,107,108,111],raising_an_assert:16,ram:[73,99],ramalho:[67,79],ran:[52,59,65,95,106,107,110,139],randint:[51,55],random:[51,55,78,82,106,107,122,139],rang:[16,50,51,55,59,62,66,67,72,79,80,88,95,100,103,106,107,108,109,111],range_iter:79,rant:111,rare:[56,71,85,97,100,103,104,110],raspberri:100,rate:95,rather:[12,16,19,24,25,33,37,38,40,41,45,51,54,55,57,59,62,63,65,69,71,72,78,79,84,87,88,89,90,92,95,97,101,102,103,106,107,108,109,111,118,130,141],raw:[56,111,134,135],raymond:[7,87],rb:[20,72,94],rcfile:93,rdbm:94,rdbmss:[74,89],re:[7,19,24,25,28,31,33,34,35,37,45,48,49,51,55,62,63,64,66,67,71,79,84,87,88,89,90,91,92,93,94,101,103,104,106,107,109,118,121,124,128,134,135,137,140,142],reach:[66,93,97,99,103],read:[0,1,20,24,25,28,41,51,55,56,57,61,62,63,64,66,70,71,74,84,85,86,87,88,89,92,93,95,100,101,103,104,106,107,109,110,111,115,117,122,124,128,130,134,135,139],read_bna:95,read_data:72,read_in_data:51,readabl:[24,25,31,33,35,48,60,79,84,85,93,94,95,103,106,108],reader:[25,53,88,93,94,99],readi:[11,25,27,48,55,74,81,105,118,123,124,126,130,131,136,137],readlin:[20,64,72],readm:[92,123],readthedoc:[34,55,72,74,81,84,92,108,127],reak:66,real:[18,21,24,25,30,42,47,51,64,69,79,80,83,92,94,95,98,100,103,106,107,108,110,111,122,123,124,128],real_decor:67,realiti:[67,74,89],realiz:66,realli:[9,12,18,22,24,25,29,32,33,34,35,39,40,49,50,51,54,55,56,59,61,62,63,64,65,66,67,68,69,71,72,73,74,78,79,81,82,83,84,85,86,87,88,91,92,93,94,95,100,101,103,106,107,110,111,113,114,116,117,121,123,124,128,130,131,139],realpython:[24,41,122],reaper:18,rear:40,rearrang:50,reason:[12,16,23,25,30,33,51,52,59,62,69,70,72,79,81,82,85,88,89,92,93,101,103,106,111,121,130,135],reassembl:[63,109],rebind:[56,67],recal:[25,51,59,62,65,85,90,105,106,127,130,131,136],receiv:[55,67,87,94],recent:[18,30,51,54,56,57,59,64,65,66,69,71,72,73,79,84,85,92,96,97,99,100,101,102,103,108,111,123,129,140,141],recip:24,reciproc:106,recogn:[90,106,115,140],recognis:61,recommend:[33,57,62,63,70,75,82,85,88,91,107,110,121,128,130,131,138,140],reconcil:122,record:[30,33,35,74,82,84,94],recov:89,recover:38,rectangl:50,rectangle_area:110,rectangle_height:110,rectangle_width:110,recurs:[2,24,25,45,53,66,86,109],recursionerror:[66,99],recv:109,red:[12,24,52,98,104,115,140],redefin:101,redfin:59,redi:89,redirect:[35,106],reduc:[0,33,35,56,59,63,64,70,80,89,91,94,157],redund:[57,101],reentrant:63,refactor:[24,25,29,30,33,34,40,51,52,88,91,107],refer:[24,25,43,47,48,53,56,57,59,63,65,73,80,81,84,85,86,89,92,95,100,103,104,106,110,111,118,121,123,124,129,131],referenc:[48,56,59,73,100,109,134],reflect:[13,62,85,94,124],refresh:124,refus:51,regard:[81,101],regardless:[30,69,71,72,92,95,108,123,131,138],regist:[55,67,84,92],registri:84,regular:[12,14,25,47,61,62,63,67,80,84,95,98,101,103,106,110,111,141],regularli:[56,115],reinforc:48,reintroduc:106,rel:[34,55,63,66,72,90,95,108,109,128],rel_tol:108,relat:[45,53,63,74,77,85,88,89,90,93,98,104,107,115,122,130,135],relationship:[13,74,89],releas:[9,49,53,55,63,84,92,103,104,109,124,129,130],relev:[72,89],reli:[33,34,51,106,107],reliabl:[34,64,92],reload:[85,94],relpath:72,remain:[24,46,66,72,93,108,124],remaind:[15,56,102],rememb:[13,14,24,25,27,33,39,40,51,55,56,57,59,61,67,69,78,79,80,84,85,86,87,92,95,98,100,101,103,104,106,108,111,121,122,123,124,129,134,139],remind:[25,33,41,51,67,92,105,136],remot:[118,123,124,126,134],remov:[25,27,35,46,51,63,69,84,92,100,103,106,121,122,124,135,139],remove_fruit:35,renam:[85,88,92,106],render:[7,33,82,87,95,155],render_pag:24,render_result:25,repe:78,repeat:[15,24,25,33,48,51,56,66,71,78,87,91,95,99,100,104,106,108,124],repeatedli:[29,109],repetit:[28,64,78,91],repl:110,replac:[25,30,33,36,44,51,56,61,64,66,67,84,88,91,105,108,110,124,130,135,136],repli:103,replic:89,repo:[18,24,27,31,45,48,52,74,77,84,89,105,121,122,123,129,134,136],report:[7,25,32,33,38,39,53,66,71,95,106,107,108,115,153],repositori:[17,27,45,53,107,121,122,123,124,134],repr:[13,94,101,111],repres:[13,25,35,41,51,55,74,81,82,85,90,106,110,111,118,122],represent:[15,101,106,128],request:[27,40,45,48,53,55,63,64,79,105,108,118,121,122,123,126,136,139],requir:[1,6,13,24,25,30,33,35,39,40,45,46,50,54,55,56,59,61,63,65,69,70,71,82,87,89,91,92,94,97,99,101,103,104,105,106,107,108,111,121,122,136,139],requisit:74,research:[48,81],reserv:[24,106],reset:121,resid:[124,135],resolv:122,resolve_party_favor:70,resourc:[2,16,63,82,89,92,95,107,108,109,110,125,126,128,138,142],resource_str:92,resourcemanag:92,respect:100,respond:[40,55,124],respons:[27,33,35,55,74,82,108,128],responsibil:82,rest:[25,33,40,55,56,73,74,83,84,87,92,100,104,106,107,124,126,128,131],restart:[55,131],restat:70,restor:[93,121,124],restrict:[84,92,93,95],restructur:[25,53,70],restructuredtext:139,result:[12,13,15,16,17,23,24,25,28,32,33,36,43,45,46,48,50,51,55,56,57,59,62,63,64,65,66,67,69,70,71,72,73,79,81,85,87,90,92,95,97,98,99,100,101,103,104,106,107,108,110,124,130,131,134,137],resum:[55,63],retain:121,retriev:[74,89,94,109,124],return_valu:108,reus:[25,87,104,106,124],reusabl:98,revers:[27,35,46,56,100,101,109],revert:123,review:[12,27,33,39,43,44,48,77,81,105,106,118,121,123,128,130,136,159],reviewe:60,revis:[24,25,118,123],revisit:[15,40,70,126],rewrit:[48,67,95],rf:[94,139],rh:129,rhat:94,rhetting:87,rhscl:129,riak:89,rich:[40,89,90,94,101,106,138],richer:89,rid:[25,60,66,121],ridicul:[42,92],right:[13,25,33,34,35,40,44,48,50,51,55,56,57,59,62,64,65,66,67,74,75,82,84,87,92,93,95,97,102,103,106,108,114,118,121,122,123,126,128,129,135,139,142],rins:[51,106],risk:[63,109,141],river:59,rjust:128,rkern:95,rlock:63,rm:[94,139],ro:92,road:63,rob:63,robot:[95,108],robust:[25,33,72,89,92,99,107,111,115,135,138,140],rogerdudl:[2,122],role:74,roll:134,roman10:106,roman11:106,roman12:106,roman13:106,roman14:106,roman15:106,roman16:106,roman17:106,roman1:106,roman2:106,roman3:106,roman4:106,roman5:106,roman6:106,roman7:106,roman8:106,roman9:106,roman_numeral_map:106,room:[28,124],root:[82,95,126,129,135],rootdir:[25,51,52,106,107],rot13:[3,145],rotat:[44,82],rough:[60,100],round:[23,106,111],rout:[55,82],routin:95,row:[23,28,40,48,94,110],rpartit:128,rpm:129,rr:95,rriehl:138,rsplit:128,rst2html:139,rst2latex:139,rst2xml:139,rst:[53,92,111,123],rstpep2html:139,rstrip:128,rubi:87,rubik:90,rule:[24,25,33,56,59,62,66,88,92,93,98,101,104,107,110,126,139,157],ruler:135,run:[1,12,13,14,16,17,18,22,24,25,27,28,30,32,33,34,35,39,42,49,52,56,59,60,62,63,64,65,66,71,73,74,79,81,82,84,85,87,88,89,93,94,95,98,99,101,103,105,106,107,109,111,114,117,118,121,123,124,126,127,128,129,130,131,133,136,138,139,140,142],run_forev:55,run_html_rend:[24,25],run_in_executor:55,run_until_complet:55,runal:92,runnabl:48,runner:[51,92,107,108],runsnakerun:95,runtim:[30,84,87,93,95,108],runtimeerror:[64,66],ruthless:15,s14:74,s2:[15,17],s3:[15,17],s4:[15,17],s:[1,2,8,10,12,13,14,15,16,17,18,22,25,26,28,29,30,31,32,33,34,35,36,38,39,40,41,45,46,48,50,51,52,55,56,57,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,77,78,79,81,82,83,85,86,88,89,90,92,93,94,96,98,99,100,102,103,104,105,106,107,108,109,111,116,117,118,120,121,122,123,126,127,128,129,130,131,135,136,137,139,140,141],sa:[47,100],sad:100,safe:[25,62,63,74,92,94,109,131],safe_input:19,safer:[59,91,94,103,124],safeti:[63,100],safetli:139,sai:[17,25,27,33,40,51,68,69,79,87,89,90,92,93,95,100,103,106,107,108,110,121,123,124,128,130],said:[78,91,118,128],sake:87,salad:[15,54],salient:7,same:[6,12,13,15,16,17,24,25,29,30,33,34,39,40,44,47,48,50,51,52,54,55,56,57,59,62,63,65,66,67,69,70,71,72,73,74,79,80,82,84,85,86,87,88,89,90,91,92,93,94,95,97,98,100,101,103,104,105,106,107,108,109,110,111,115,118,121,122,123,124,126,127,128,129,130,131,133,136,139,141],sampl:[24,25,92,95,103,106,107],sample_html:24,sample_text_fil:42,sampleproject:92,sandbox:[74,89],sane:126,sat:121,satisfactori:123,satisfi:[25,35,100,106],saturdai:51,sauc:101,save:[1,16,25,31,33,34,40,59,65,66,67,72,73,74,79,84,85,93,94,95,99,103,105,110,113,118,122,123,124,128,131,135,136],saveabl:31,savepoint:89,saw:[59,67,106,109],sax:94,say_lot:55,say_someth:55,scalabl:[87,89],scalar:100,scale:[14,55,59,89],scale_factor:59,scan:[95,103],scandal:51,scatter:47,scenario:30,scene:[55,128],schedul:63,scheme:[44,108,135],school:90,scienc:[22,74,81,92,101,108,110,139],scientif:[48,51,92,131,135],scipi:[92,95],scl:129,scm:[122,130],scope:[48,63,66,67,80,85,88,90,98,100,103],score:69,scottlobdel:67,scoundrel:51,scraper:159,scratch:[30,55,98],scream:106,screen:[39,40,55,95],script1:92,script2:92,script:[1,17,21,24,27,28,31,35,42,48,55,66,71,75,77,84,95,106,109,131,134,139],script_nam:92,scriptnam:34,scroll:[124,140],sdist:92,sdk:92,se:71,search:[33,53,59,66,74,81,89,93,94,95,107,129],seat:103,seattl:[15,17],sebz:44,second:[15,16,24,25,28,35,48,51,55,57,66,67,70,77,80,81,84,87,89,94,95,106,107,108,118,134,135],secondari:128,secret:[72,73,74,101],secret_data:72,secret_head:72,secret_rest:72,section1:94,section2:94,section:[4,24,30,34,51,63,65,85,94,109,124,131],secur:[74,94,130,139],see:[13,14,15,16,18,19,24,25,26,33,35,37,39,41,48,50,51,52,55,56,57,59,61,62,63,64,65,66,67,68,69,70,71,72,73,82,84,85,87,88,90,91,92,93,95,98,100,101,102,103,104,105,106,107,108,109,110,111,118,121,122,123,124,128,129,130,131,134,135,136,137,139,140],seed:51,seek:[72,100,121],seem:[1,25,51,60,63,66,80,100,103,106,111,120,124,128],seemingli:[121,122],seemingobvi:100,seen:[24,63,64,67,71,73,78,84,88,92,95,99,100,101,102,103,106,107,121,126],select:[28,35,37,51,68,94,95,107,108,111,115,118,124,128,131,135],self:[24,25,33,56,57,59,62,64,67,70,72,79,84,87,89,93,94,96,101,102,104,107,108,109,115,121],selfclosingtag:[24,25],selfpac:124,sell:94,semant:[74,91],semaphor:63,semi:122,semver:92,send:[29,33,39,60,63,65,66,77,82,109],sens:[12,13,24,25,35,51,55,59,62,64,65,86,88,91,95,100,101,103,104,106,110,120,126,142],sensit:24,sent:82,sentenc:[51,60,70],sep:[127,130],separ:[24,25,28,30,33,34,35,40,51,54,55,56,63,73,79,80,89,90,91,92,93,94,97,99,101,103,104,106,107,108,110,118,122,124,139],separation_of_concern:33,seq:[46,67,72,100,103,107],sequenc:[0,15,36,47,51,53,56,57,62,69,78,79,83,97,101,102,103,107,111],sequenti:[63,79,95,109],seri:[2,15,17,28,51,90,106,110,123,143],serial:[31,84,159],series_templ:45,serious:85,serv:[103,126],server:[66,74,89,123],servic:[30,49,55,66,74,89,108,122,128,130,138],session01:122,session02:72,session05:121,session5:27,session:[0,24,25,29,51,52,55,66,106,107,122,127,135],set:[0,12,13,14,24,25,30,32,33,34,35,39,47,51,53,55,56,59,61,63,66,73,74,78,79,80,81,82,85,86,87,90,91,92,93,94,95,96,98,99,101,103,104,106,107,109,110,111,112,117,118,120,121,122,123,124,128,133,134,139],set__item__:101,set_result:55,set_trac:66,set_x:96,setdefault:61,setformatt:82,setlevel:82,setrecursionlimit:66,setter:[67,89],setup:[24,34,42,55,64,77,79,95,100,108,115,117,125,126,130,138,139,140],setup_cod:95,setup_requir:92,setupclass:108,setupmodul:108,setuptool:[34,139],setx:67,seven:[51,64,79,106],sever:[1,30,35,48,56,63,95,106,109,124,128,139],sgml:[24,94],sh:[108,134,139],sha:69,shadow:92,shall:109,shallow:[79,86],shame:92,shape:[51,54],shard:89,share:[24,25,60,62,63,85,89,90,91,92,98,100,105,106,107,109,136,141],sharealik:53,shaw:116,she:[27,51,81],shebang:[17,75],sheet:55,shelf:94,shell:[1,21,27,63,92,116,127,128,130,131,139,141],shelv:89,sherlock:51,sherlock_smal:51,sherm:70,sherm_the_boranga:70,shift:[121,135],shine:[49,55,62],ship:[51,66,95,106,108,115],shoot:51,shop:[18,51,81],shopkeep:18,shortcut:[62,100,106,115,120,123,128],shorten:95,shorter:51,shorthand:[50,79,121],shot:52,should:[1,2,10,12,13,15,16,17,18,19,20,22,24,25,26,27,28,29,30,31,32,33,34,35,38,39,40,41,43,44,45,46,47,48,49,50,51,52,59,60,63,64,66,67,69,71,73,75,77,81,82,84,85,89,92,93,95,99,100,101,103,104,105,106,107,109,110,111,115,118,120,121,122,123,124,126,128,129,130,131,134,135,136,137,139,140,142],should_be_remov:100,shoulder:30,shouldn:[25,33,55,64,108,124,131],show:[15,18,24,25,28,30,48,51,56,59,66,77,84,85,92,95,98,100,106,107,118,121,122,123,124,128,134,135,139],shown:[48,74,128,130],shuffl:107,shut:55,shutdown:55,shutil:[20,72],sibl:87,side:[62,63,73,85,90,109,118,139],side_effect:108,sidetrack:84,sigma:50,sign:[25,40,56,73,74,108,110,111],signal:[51,55,63,109],signatur:[13,24,25,54,62,84,104],signific:[40,48,56,95,108,109],silenc:71,silent:[71,106],silli:[12,18,25,106,115,135,140],similar:[7,16,21,23,33,34,35,45,55,57,62,64,65,69,71,77,87,89,92,94,100,103,104,107,109,130,134,135],similarli:[25,55,59,61,82,83,89,92,107],simipli:40,simmpli:69,simpl:[1,4,13,20,22,24,25,30,31,33,34,35,39,40,41,42,43,44,48,50,55,56,59,62,64,66,67,68,71,72,73,77,78,79,80,81,82,84,85,87,91,92,94,95,96,100,101,103,104,106,107,109,111,121,123,124,126,130,134,135,139,149],simple_class:98,simple_tim:109,simpler:[25,35,41,47,79,91,92,94,106],simplest:[34,48,50,56,59,65,74,82,85,94,95,98,104],simpli:[13,23,24,25,30,33,35,37,38,50,55,62,64,67,69,72,73,74,79,80,84,85,87,91,92,97,98,101,103,104,106,107,108,110,111,121,122,123,124,126,130,135,139],simplic:89,simplifi:[66,100,121],simpson:59,simul:[18,33],simultan:[49,109],sin:[50,85,92],sinc:[12,24,25,35,41,47,51,57,64,66,84,87,90,100,106,107,123,124,126,134],sincer:39,sine:50,singl:[15,24,25,30,33,34,35,39,41,46,50,51,56,59,61,62,63,67,70,71,73,77,80,82,83,84,85,87,88,90,91,92,93,94,100,101,103,110,118,124,134,135,139],singledispatch:67,singleton:57,singleton_pattern:84,singular:88,sir:[18,106],sit:[55,103,123],site:[14,49,52,62,74,89,91,92,93,95,107,110,123,129,130,131,139],situat:[51,66,80,109,121,124,134,139],six:[25,51,101],size:[20,23,24,51,54,62,69,98,109,115],skeleton:24,skill:[28,123,124],skip:[2,51,62,78,101,103,111,131],skipinitialspac:94,skit:15,sky:33,slack:[60,66],slash:[24,97,101],slave:104,sleep:[49,55,95,109],sleep_in:57,slice:[3,47,56,63,79,87,144],slick:[25,72],slight:40,slightli:[15,56,84,91,92,101,109,110],slip:[93,106],slope:50,sloth:15,slow:[55,95,103],slow_funct:55,slow_task:55,slower:[49,63,109],slowli:51,small:[2,21,24,25,28,34,50,51,55,66,69,74,83,84,89,95,100,106,108,122,150],small_task:55,smaller:[23,60,73,99,103,130],smallest:[66,107,108],smallish:60,smart:97,smart_ind:135,smarter:87,smooth:56,smtp:108,smtplib:108,snake:95,snapshot:[121,124],sneak:2,so:[2,6,12,13,15,16,17,18,20,21,23,24,25,26,27,28,29,30,31,33,34,35,36,39,40,41,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,59,60,61,62,63,64,65,66,67,68,69,71,72,73,74,75,77,78,79,80,81,82,84,85,86,87,88,89,90,91,92,93,94,95,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,115,117,118,120,121,122,123,124,126,128,129,130,131,134,135,136,138,139,140,141,142],soap:70,socket:[55,64,82,94,95],soft:33,softwar:[30,66,74,82,84,90,92,105,111,116,122,124,128,129,130,134,136,139],softwarecollect:129,solar:115,solid:[81,110,135,140],solut:[14,22,25,30,31,52,57,63,68,81,84,86,89,90,105,109,111,113,121,124,126,136],solv:[14,50,57,63,87,90,91,106,108,109],some:[2,6,13,14,16,19,21,24,25,27,28,30,31,32,33,35,39,40,41,44,45,46,47,48,50,51,52,54,55,56,57,59,61,62,63,64,69,70,71,73,78,79,81,83,84,85,88,89,90,91,92,93,94,96,98,100,101,103,104,105,106,107,108,109,110,114,115,117,118,120,121,123,125,128,131,134,135,136,138,139,140,141,159],some_argu:101,some_cod:[71,78,93,124],some_code_her:84,some_dict:12,some_express:16,some_food:100,some_initil:101,some_nam:75,some_non_existant_fil:71,some_paramet:101,some_stuff:79,some_stuff_in_her:48,some_tag:24,some_tupl:12,someenv:134,somehow:[12,51,71,82],somelist:100,someon:[24,25,28,55,66,74,106,111],someth:[2,12,13,15,16,17,18,20,21,22,23,24,25,28,33,34,39,40,44,48,51,52,54,55,56,57,62,63,64,65,66,67,68,71,72,74,77,78,79,80,82,84,85,87,89,90,91,92,93,96,97,98,101,103,104,105,106,108,110,120,121,123,124,127,128,129,130,131,134,136,137],somethign:87,something_els:[92,97],something_is_tru:62,something_or_oth:[129,130,131],something_with:79,sometim:[25,33,46,55,56,62,66,78,79,85,87,91,92,93,100,103,106,111,122],somewhat:79,somewher:[25,84,89,90,103,106,110,124],soon:[25,55,108],sorri:[72,104],sort:[13,24,25,28,33,48,49,55,56,64,67,71,79,80,82,86,95,98,103,104,106,107,138],sort_kei:[35,69,80,101],sort_ord:95,sort_stat:95,sortabl:101,sorted_dict:69,sound:[24,25,51,55,65,84,106],sourc:[20,24,30,51,53,55,56,64,72,74,81,93,94,95,103,105,106,107,108,110,111,118,122,123,128,129,130,134,136,139],sourceforg:[92,95,108,130],southeastern:90,southwest:90,sp2018:121,spa:55,space:[24,25,33,36,51,56,63,66,74,90,98,100,103,110,111],spaghetti:100,spam:[15,18,92,94,100],spam_read:94,spam_writ:94,spars:[8,154],sparsearrai:47,spcific:82,speak:[24,74,88,91],speaker:[103,111],speakerdeck:7,spec:[94,106],special:[7,8,16,25,45,50,56,59,61,64,67,69,70,84,87,91,95,96,98,100,103,104,106,107,108,110,128,154],specializ:94,specif:[15,24,33,35,36,48,51,60,69,71,73,77,84,86,87,88,91,93,95,98,101,105,106,107,109,111,118,121,124,136],specifi:[12,13,21,23,25,30,34,39,48,51,54,55,59,66,67,69,70,71,73,82,84,85,92,93,101,102,103,106,107,108,109,111,115],speed:[63,95,109,122],spell:[85,92,108],spelunk:44,spend:[51,55,63,66,95,103],spent:95,sphere:[13,102],sphinx:[53,70,108],spin:30,spini:87,spirit:[33,57],spisid:66,split:[35,40,51,72,128,135],splitext:72,splitlin:128,spoil:106,spong:70,spot:[91,93,100,106],spreadsheet:94,spring2017:108,sprinkl:25,sprint:[56,106],sprintf:103,sql:[74,159],sqlalchemi:94,sqlite3:94,sqrt:104,squar:[56,59,62,68,108,134],squish:60,stabil:90,stabl:[55,92,118,127,129],stack:[18,35,67,71,73,81,86,92,99],stackoverflow:[66,84,86,87,120],stage:[24,105,106,124,136,139],stai:[62,87,92,95],stall:63,stamp:82,stand:[30,35,92],standard:[24,43,52,55,56,59,61,63,66,69,72,81,82,84,85,86,88,89,91,92,93,94,95,98,100,102,106,108,110,111,121,123,128,134,139],stanza:92,start:[2,16,23,24,25,28,30,31,32,33,34,35,41,44,45,46,48,50,51,52,56,59,63,64,66,67,72,73,74,76,79,82,85,90,91,95,106,107,108,109,110,111,115,116,117,118,121,127,128,131,133,135,139,142],start_at:59,starter:[25,106,127],startifact:89,startswith:[25,59,84,92,108,128],startup:[89,134,139],stash:134,stat:[55,95],state:[33,55,59,64,65,66,79,85,98,107,109,121,122,124,126,134],stateless:55,statement2:95,statement:[23,33,34,35,39,45,56,57,59,62,63,64,65,66,67,68,71,78,80,85,90,95,97,103,104,106,107,108,109],staticadd:102,staticmethod:[67,102],statist:[51,55,93,95,139],statu:[105,123,124,134,136],statvf:111,stderr:[63,82],stdin:[63,69],stdlib:[72,92],stdout:[16,25,63,82,106,107,108,109],stdout_writ:109,stdtype:[57,69,79,100,103],steep:66,step:[2,12,28,33,34,40,50,51,52,61,62,63,64,66,74,76,79,100,106,109,121,122,123,124,129,131],stephan:57,stevedor:139,stick:[93,103,121],still:[1,8,19,24,25,30,33,40,51,52,54,55,56,57,62,63,64,65,66,72,74,79,81,82,85,86,87,88,92,97,98,101,102,103,104,106,107,108,110,111,118,122,124,129,131,134,138,139,141],stink:64,stipul:106,stmt:95,stock:[94,102],stone:131,stop:[51,55,57,66,71,79,84,93,95,99,103,106,107,109,135,139],stopiter:[65,79],storag:[89,94],store:[13,24,25,28,29,33,47,51,56,59,62,66,69,71,72,74,79,80,82,84,85,91,92,94,98,101,103,108,109,110,111,130],store_nam:89,stori:[51,100],str:[24,25,51,56,69,72,84,94,95,100,101,103,108,110,111,128],str_comprehens:95,str_concat:95,straight:[25,50,92,100,117,139],straightforward:[22,25,30,33,34,35,48,51,80,87,92,99,104,106,107],strang:110,strategi:[30,92,106,107,134],stream:[63,82,111],streamhandl:82,street:[33,103],strength:[91,96,106,107],strfirstnam:88,strict:[24,62,90,111],strictli:[24,88,91,101,103,128,130],strike:88,string:[0,2,12,15,24,25,27,28,31,33,34,35,37,40,44,46,51,54,57,61,62,66,67,69,70,71,72,74,78,79,82,84,88,89,92,93,94,95,98,100,101,102,106,110,121,128,134,145],string_format:48,stringifc:103,stringifi:[24,103],stringio:[24,25],strint:46,strip:[25,51,88,92,95,128],strive:[33,85,108],strnumber:88,strong:[28,88,95],strongli:131,struct:[72,98,100],structur:[6,21,24,25,28,30,32,33,37,39,42,52,55,62,64,66,71,73,74,82,89,91,94,95,102,104,106,107,150],struggl:[1,8,111],stub:106,stuck:[24,25,60,111,124,128],student:[1,21,35,81,101,105,123,124,126,128,136,138],studi:[30,84],studio:[66,114,128],studlycap:93,stuff:[24,33,55,56,62,64,71,72,73,79,84,87,92,95,98,104,106,107,108,110,111,121,122,123,127],stuff_to_join:103,stupid:71,stupidpythonidea:[6,12],style:[2,5,24,25,33,55,67,72,81,82,85,89,92,95,98,103,106,107,115,131,135,150,159],stylist:51,sub:[24,25,82,90,95,100],sub_el:24,sub_logger1:82,sub_logger2:82,subclass:[7,24,25,53,57,61,66,68,71,84,87,91,98,102,107,108],subclassi:102,subdirectori:124,subfunct:95,subject:108,subl:126,sublim:[114,126,128,141],sublime_text:126,sublimetext:[128,135],sublist:100,submiss:[123,124],submit:[1,14,15,44,45,123,126],submodul:92,subpackag:92,subpackage1:92,subpackage2:92,subprocess:55,subroutin:[55,63],subscript:[56,66],subsequ:123,subset:[2,17,69,94,108,110],substanti:55,substitut:[44,67,94],substr:[100,108],subsub:24,subsystem:[128,141],subtask:63,subtl:97,subtract:[56,106],subtyp:104,subvers:126,succe:106,success:[25,28,30,106,128],successfulli:[25,106,109,128,139],suddenli:100,sudo:[129,139],suffic:[30,134],suffix:66,sugar:[67,84],suggest:[0,14,25,33,40,51,53,88,93,104,122,124,126,128,135],suit:[25,40,55,63,84,106,135],suitabl:[63,94,111],sum2x:67,sum:[30,45,48,50,67,79,80,83,101,103,109],sum_:50,sum_seri:45,summar:95,summari:[24,28,51,89,95,106,107,123],summat:50,sun:121,sundai:77,super1:87,super2:87,super3:87,super_test:87,superclass:[24,25,84,87],supercomput:30,superset:[69,92,111],superus:139,suport:141,supplement:[1,53,126],suppli:[61,95,107],support:[1,24,25,30,34,46,47,55,56,59,61,62,64,66,69,72,83,84,89,91,92,94,97,100,101,103,104,107,108,110,111,115,122,128,129,130,131,133,139],suppos:[24,25,48,51,104],sure:[1,12,13,14,17,19,21,24,25,27,31,33,34,38,40,41,46,48,50,51,55,56,66,67,68,69,71,74,75,77,78,84,85,86,87,89,92,94,95,99,105,106,107,108,110,117,118,123,124,128,129,130,135,136,137,139,140],surfac:13,surpris:[15,25,51,85],surreal:51,surround:[24,59,66,110],sushi:100,suspend:[55,63,65],svn2github:79,swai:109,swallow:[51,66,111],swap:56,swapcas:[103,128],swarm:95,swashbuckl:51,swcarpentri:116,swear:106,sweet:[84,91,139],swift:[51,79],swig:95,switch_func_dict:68,sy:[16,35,51,66,74,85,94,109,111,137],symbol:[50,67,73,85,94,96,103,110,115,124,135,140],symlink:130,symmetr:69,symmetric_differ:69,sync:[13,69,96,103,121],synchron:[55,63,109,124],synonym:55,syntact:[67,84,122],syntax:[1,24,25,35,48,56,57,62,72,79,80,83,94,95,96,97,103,108,115,128,134,135,140],syntaxerror:[24,43,56,59,66,73,93,97,103],system:[1,11,16,25,27,31,34,39,49,52,53,55,56,63,65,66,70,71,72,82,84,87,88,92,93,94,95,104,105,106,107,108,109,110,111,115,118,121,122,123,124,126,127,129,130,131,133,135,136,138,139],t1:95,t2:95,t:[1,7,10,12,13,15,16,17,18,20,22,23,24,25,27,29,30,33,34,35,36,37,38,39,40,41,46,47,48,49,50,51,52,55,56,57,59,61,62,63,64,65,66,67,68,69,71,72,73,74,75,77,78,79,80,81,82,83,84,85,86,87,88,89,91,92,93,94,95,96,97,98,99,100,101,102,103,104,106,107,108,109,110,111,116,121,123,124,126,128,129,130,131,134,135,137,139,141],tab:[56,94,103,115,124,127,128,134,135,140],tab_siz:135,tabl:[48,51,68,69,74,89,93,94,111],tabular:[28,94],tack:104,tackl:25,tactic:51,tad:62,tag:[25,67,124,134],tag_hr:24,take:[2,16,23,24,25,30,33,35,44,46,47,48,50,51,54,56,57,59,61,62,63,64,67,69,70,72,73,77,78,79,80,81,84,85,87,90,91,92,94,95,98,99,100,101,102,103,104,106,107,109,111,123,124,130,134,135,139],taken:[2,16,66,78,115],talk:[7,24,30,55,62,63,71,74,84,86,88,90,91,94,97,108,109,121,139],tan:85,tangl:[121,139],tar:[92,139],tare:103,tarfil:102,target:[108,109,118,123],tarinfo:102,task:[12,16,17,25,27,28,30,61,63,81,106],tast:[89,100],taxonomi:7,tby:103,tcp:55,tcsh:134,tdd:[25,40,41,51,52,106,107],teach:12,team:[39,61,68,74,93,95,100,106,122,124,138],tear:107,teardown:[32,64,79],teardownclass:108,teardownmodul:108,technic:[51,52,55,62,69,85,100,103,106,141],techniqu:[59,66,84,106,129],tediou:[108,139],teh:57,televis:106,tell:[18,21,23,24,25,27,33,47,51,55,56,59,72,82,84,92,105,106,107,108,110,121,122,123,124,129,130,131,136],temp:[18,39],temp_f_to_c:66,tempfil:39,templat:[37,39,45,92],temporari:39,temporarili:[29,110],tempt:[36,88,104],ten:[24,49,106,121],tend:[25,30,90,91,95,130],term:[30,59,62,79,80,84,91,92,95,99,122,129,131,139],termin:[25,28,34,52,71,73,85,99,106,110,111,124,128,139,140],terminolog:89,ternari:57,test2:118,test:[0,6,10,13,16,20,25,26,28,30,31,32,41,42,43,44,48,53,57,59,60,62,66,68,69,72,77,79,84,93,94,95,109,111,112,117,120,121,124,125,128,134],test_:[52,106,107],test_add:108,test_address_book_mongo:89,test_almost_equ:108,test_anchor:25,test_anyth:106,test_append:25,test_append_content_in_br:25,test_article_success_context_manager_mock:108,test_article_success_decorator_mock:108,test_attribut:25,test_bodi:25,test_br:25,test_calcul:108,test_calculator_pytest:108,test_capital_mod:42,test_choic:107,test_cli:[33,34],test_content_in_br:25,test_divid:108,test_driven_develop:106,test_ehlo:108,test_element_indent1:25,test_float:108,test_float_with_integer_valu:106,test_floating_point:108,test_foo:108,test_from_roman_known_valu:106,test_gener:79,test_get_input:108,test_get_last_pair:51,test_get_letter_text:40,test_get_random_follow:51,test_get_random_follower_not_ther:51,test_hr:25,test_hr_attr:25,test_html:25,test_html_ouput:24,test_html_output2:24,test_html_output3:24,test_html_output4:24,test_html_output5:24,test_html_output6:24,test_html_output7:24,test_html_output8:24,test_html_output9:24,test_html_rend:[24,25],test_ind:25,test_indent_cont:25,test_init:25,test_invalid_charact:106,test_isclose_tini:108,test_lambda:26,test_len:108,test_make_sent:51,test_malformed_anteced:106,test_method:108,test_mock_input:108,test_model:[33,34],test_modul:108,test_module1:92,test_module2:92,test_multiple_ind:25,test_multipli:108,test_multiply_both_neg:108,test_multiply_both_posit:108,test_multiply_first_zero:108,test_multiply_one_neg:108,test_multiply_second_zero:108,test_my_func:107,test_my_mod:107,test_my_modul:108,test_name_of_tested_modul:52,test_neg:106,test_non_integ:106,test_object_canva:87,test_one_line_tag_append:25,test_p:25,test_pair:51,test_pick_random_pair:51,test_pow:108,test_random_pytest:107,test_random_unitest:107,test_rend:25,test_render_el:25,test_render_element2:25,test_repeated_pair:106,test_report:41,test_roundtrip:106,test_sampl:107,test_sample_too_larg:107,test_shuffl:107,test_shuffle_immut:107,test_someth:[52,107],test_sub_el:25,test_subel:25,test_suit:92,test_tautolog:107,test_titl:25,test_to_roman_known_valu:106,test_too_larg:106,test_too_many_repeated_numer:106,test_trigram:51,test_trigrams_following_word:51,test_trigrams_pair:51,test_val1:107,test_val2:107,test_walnut_parti:52,test_zero:106,testabl:[33,40,91,106,108],testalmostequ:108,testcalculatorfunct:108,testcas:107,testclass:[59,108],testenv:139,testload:108,testmystuff:108,tests_requir:92,testsequencefunct:107,testsuit:108,text:[4,14,20,21,24,25,31,38,39,40,44,53,55,56,66,70,74,82,83,85,88,92,94,95,103,111,114,115,117,123,124,126,133,137,140,141,149],textbook:[53,81],textedit:128,texttestrunn:108,textwrap:92,textwrapp:24,th:[78,103],than:[1,15,16,19,23,24,25,27,28,29,33,34,37,38,40,41,45,48,49,51,54,55,56,57,59,62,63,66,67,69,70,71,72,73,74,75,77,78,79,84,85,86,88,89,90,91,92,94,95,97,98,99,101,102,103,104,105,106,107,108,109,110,111,121,123,126,130,131,134,136,139,141],thank:[30,33,39,106],thank_you:39,thatth:69,the_assignment_url:124,the_diamond_problem:[9,87],the_err:71,the_error:71,the_fil:[75,105,136],the_filenam:72,the_instructors_github_handl:124,the_list:26,the_list_of_word:51,the_long_url_to_the_remote_repo:121,the_name_of_new_fil:123,the_name_of_the_assignment_repo:124,the_name_of_the_branch:121,the_name_of_the_fil:[121,127],the_name_of_the_packag:[129,130,131],the_name_of_the_script:75,the_new_tag_valu:25,the_object:68,the_packag:92,the_radiu:13,the_root_logg:82,the_shell_command:127,the_superclass:104,the_values_as_a_list:69,thegreenplac:84,thei:[7,13,16,21,24,25,28,30,33,35,36,38,41,44,47,48,51,53,54,55,56,57,59,61,62,63,65,67,69,70,71,73,74,78,79,80,81,82,84,86,87,89,90,91,92,93,94,95,97,98,99,100,101,102,103,104,106,107,108,109,110,111,118,121,122,123,124,126,128,139],thelist:24,them:[7,13,16,17,24,25,26,28,30,33,35,38,40,41,47,48,50,51,52,53,54,55,56,57,59,60,61,62,64,67,68,69,70,71,73,74,78,79,80,81,82,84,85,87,89,90,91,92,93,94,95,98,100,101,103,105,106,107,108,109,110,111,115,118,121,123,124,126,134,135,136,139,140],theme:[115,118,135],themselv:[18,24,25,30,33,51,55,81,92,103,104,109,110],theoret:[50,94,106],theori:[94,104,106],thequickbrownfoxjumpedoverthelazydog:100,therefor:[24,40,46,63,87,103,108,111],thesearesomestr:103,thi:[1,2,6,7,8,12,13,14,16,17,19,20,21,22,23,24,25,27,28,29,30,33,34,35,36,37,38,39,40,41,44,45,46,47,48,49,50,51,52,54,55,56,57,58,59,61,62,63,64,65,66,67,68,69,70,71,73,74,75,76,77,78,79,80,81,82,83,85,86,87,88,89,90,91,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,134,135,136,137,138,139,140,141,142],thig:106,thikn:108,thing:[13,16,23,24,25,33,34,35,40,49,50,51,55,56,57,59,62,63,64,66,67,68,69,70,71,72,79,80,82,84,85,87,89,90,91,92,93,94,95,96,98,100,102,103,104,107,108,109,110,111,114,117,118,122,123,126,128,129,130,131,134,135,138,139],think:[2,3,4,7,12,23,24,25,29,33,37,48,50,51,56,57,60,62,64,66,68,69,70,72,81,82,83,85,87,89,90,91,92,94,95,98,99,100,101,104,106,107,108,110,111,124,135],thinkpython008:3,thinkpython016:7,thinkpython2:[2,81],thinkpython:[3,7],third:[15,45,46,48,51,54,55,64,69,72,89,92,93,94,100,103,106,108,134,139],third_lett:100,thirsti:103,this_0:62,this_1:62,this_2:62,this_3:62,this_4:62,this_:62,this_could_be_a_filenam:100,this_func:85,this_in_utf16:111,this_is_2:56,this_is_a_symbol:56,thisthat:[23,103],thoma:51,those:[17,24,25,27,28,33,34,49,50,51,56,59,63,65,71,73,74,79,80,82,83,85,87,88,90,92,96,97,98,100,101,105,106,107,108,111,121,122,123,124,127,128,130,131,136],though:[16,24,25,40,56,57,59,62,81,82,87,92,93,94,98,100,101,108,110,111,121,123,131,135,141],thought:[40,51,53,87,91,104,106,111,139],thousand:[48,49,106,109],thread:[55,66,86,124,159],thread_count:109,threading_integr:109,three:[14,15,22,23,24,25,33,34,45,46,51,55,56,57,66,69,79,80,84,85,87,89,90,92,98,100,103,106,107,117,134],threshold:93,through:[1,13,15,17,25,26,39,46,51,53,56,62,63,64,66,72,74,79,80,82,86,87,90,92,97,106,108,111,122,123,124,128,131,135,140],thrown:[66,106,108],thu:[16,24,26,39,59,62,63,65,80,84,87,95,100,103,109,131],ti:[69,139],tight:79,tile:68,till:[18,52],time:[2,10,13,25,27,28,29,30,32,33,34,48,49,50,51,52,55,56,57,59,60,61,62,63,64,66,69,70,71,72,73,77,78,79,80,81,82,84,85,86,87,90,91,92,93,94,97,98,99,100,101,103,104,106,107,108,110,118,121,122,123,124,128,130,131,135],timecomplex:[69,95,100],timed_func:67,timelin:122,timeout:[29,109],timer:[16,95],timer_test:95,timestamp:39,tini:[24,25,55,67],tip:120,tire:[28,103],titl:[7,24,25,35,66,103,128],tmp:[95,139],tmtheme:135,to_roman:106,toast:100,todai:[91,106,126],todo:[55,70],togeth:[13,23,24,33,55,60,63,87,91,92,98,103,104,109,124,126],toggl:115,toi:87,told:[67,106,110],tom:[51,79],ton:[115,135],too:[2,24,25,32,33,34,37,50,51,55,59,62,63,71,72,78,80,82,84,85,88,89,92,93,94,100,103,106,110,111,121,122,123,128,130,138,139],took:[16,101,139],tool:[1,11,25,55,60,64,75,79,81,89,91,94,98,100,105,107,114,115,117,118,119,120,123,124,125,126,134,136,139,140,141],toolkit:[7,74,87],top:[23,24,25,28,35,42,55,67,75,85,87,92,93,95,103,106,108,109,110,111,121],topic:[33,40,74,81,94,97,106,118,128,142],tornado:55,tornadoweb:55,tortoisegit:131,total:[2,28,30,33,50,52,60,92,93,95,111,124],total_ord:101,tottim:95,touch:[77,84,121,131],toward:[30,34,90],town:103,tox:92,tprint:56,tr:103,trac:55,trace:[18,35,56,82,139],traceback:[18,51,54,57,59,64,65,66,69,71,72,73,79,84,85,96,99,100,102,103,108,111],track:[21,30,81,82,108,109,110,123,124,126,139],tradit:[55,92],tradition:[63,69],trail:[93,135],trailing_spaces_modified_lines_onli:135,train:[25,109],tran:94,tranlat:24,transact:89,transform:[84,90,100],transit:[109,111],translat:[50,51,72,99,106,128,131],translate_tabs_to_spac:135,transmit:94,transpar:89,transport:55,trap:[100,109],trapezoid:157,trapezoidal_rul:50,trapz:50,travel:55,treat:[24,51,84,100,107],tree:[8,53,81,94,115,124],trei:62,tremend:30,tresult:67,treyhunn:[62,79],tri:[24,25,51,56,95,106,109],tri_dict:51,trick:[25,54,55,57,59,65,101,124,134],tricki:[24,25,51,56,72,82,87,90,92,126],trigger:[18,66,71,89,93,124],trigram:[4,79,149],trim:87,trim_automatic_white_spac:135,trim_trailing_white_space_on_sav:135,trim_trailing_whitespace_on_sav:135,trio:55,trip:[106,111],tripl:[30,70,79,103],triplet:79,trivial:[66,69,70,85,89,106,139],troubl:[59,80,131],truli:[51,63,84,85],truncat:[101,106],trust:[24,40,71],tsepar:103,tt:93,ttab:103,tue:121,tunabl:[63,109],tune:[62,87],tupl:[33,35,37,46,48,51,54,61,69,71,78,79,80,84,86,90,94,101,103,106,107,109],tupleiter:79,turn:[16,24,34,38,48,51,55,56,59,63,64,66,79,80,84,93,94,100,103,105,111,114,121,124,128,136],tutor:60,tutori:[1,3,7,24,28,41,55,66,74,82,89,92,103,108,118,126,127,128,129,140,145,155],twelv:110,twenti:[15,17],twice:[44,51,87,105,121,136],twist:[55,100,103],twistedmatrix:55,twith:67,two:[7,13,17,18,19,24,25,27,32,34,35,45,46,51,52,54,56,57,59,62,63,64,68,69,70,72,80,83,84,85,90,93,94,95,98,100,101,103,104,105,106,108,109,110,111,113,118,121,122,124,131,134,135,136],twofold:77,txt:[21,39,42,51,64,71,72,92,95,100,111,120],type:[16,17,25,27,28,33,34,37,46,48,50,51,55,57,59,62,64,65,66,67,68,69,71,73,80,85,86,87,88,89,90,91,92,94,95,98,103,106,107,110,111,115,118,122,124,127,128,129,130,131,135,137,139,140],typeerro:107,typeerror:[24,25,43,54,56,66,69,71,97,98,100,102,103,106,107],typesseq:100,typic:[73,89,90,100,111],typo:[33,53],u221:111,u222b:111,u:[94,95,100,111,134],ubiquit:[89,93],ubuntu:[140,141],uci:[92,95],ugli:[24,64,67,79,84,89,92,94,101,111,122],ui:[32,33,112,115],uk:27,ul:24,ultim:[13,94,102,128],ultra_simpl:55,unambigu:88,unanticip:82,unari:[56,57],unauthent:94,unavail:108,unbind:56,unblock:109,unbound:[56,87,102],unboundlocalerror:[59,73],uncaught:66,unclear:[1,60,124],uncom:[24,25,55],uncomfort:1,uncondition:72,uncontamin:18,undecod:72,under:[1,30,50,51,53,56,67,79,81,85,89,94,104,107,111,115,124,128,135],underli:[69,79,102],underneath:108,underscor:[25,56,59,88,93,101,110],underspecifi:23,understand:[9,12,16,24,25,48,49,55,56,59,63,65,66,72,78,79,81,86,87,88,122,123,126,128,131],understood:107,undo:89,undon:108,unexpect:[25,66,106,109,111],unfortun:[108,120,141],unhandl:66,unhash:69,unicdo:103,unicdod:111,unichr:111,unicod:[44,56,72,94,97,103,110,159],unicode_liter:111,unicodedecodeerror:[72,111],unicodeencodeerror:111,unicodifi:111,unidata:111,unif:84,unimpl:108,uninstal:135,unintend:78,unintuit:[66,100],union:[17,69],uniqu:[39,40,51,56,78,90,98,121,122],unit:[0,6,10,13,23,25,32,33,34,41,51,53,56,77,90,92,95,107],unittest:[6,92,106],univers:[53,106],unix:[72,92,95,109,111,120,124,126,131],unknown:78,unless:[36,63,65,71,72,93,124,131],unlik:[63,102],unlock:[63,109],unnam:56,unnecessari:[62,79,83],unnecessarili:88,unord:[24,69],unpack:[72,78,139],unpickl:[84,94,109],unpython:92,unrel:[25,106],unset:134,unstag:124,unsupport:100,untest:40,until:[25,27,34,35,52,55,59,62,65,66,71,72,78,79,83,109,123,124,135],untrack:134,untrust:[94,130],unusu:70,unwieldi:[30,33],unzip:[42,120],up:[0,10,13,15,18,19,22,23,24,25,28,30,32,33,34,35,36,37,40,50,51,53,56,59,62,63,64,66,69,71,73,74,79,82,84,85,87,88,89,90,92,93,94,95,96,97,98,100,101,102,105,106,107,109,110,111,112,114,117,118,121,122,123,124,127,128,133,135,136,140,141],updat:[24,25,33,34,37,38,40,50,55,67,72,81,84,89,92,100,102,106,109,115,121,129,130,131,135,140],upgrad:[92,129,130,131],upload:92,upon:[24,59],upper:[15,17,48,51,62,74,84,103,124,128],uppercas:84,uppercase_attr:84,upriss:27,upset:55,upsid:66,upstream:[123,124],upward:51,urg:[81,139],url:[92,118,124],urlib:64,urllib:[49,64],urlopen:64,us:[1,2,4,6,7,12,13,15,16,17,18,20,21,22,23,24,25,26,27,28,30,31,32,33,34,35,36,38,39,40,41,42,43,44,45,46,48,49,50,52,53,54,56,57,60,64,65,69,70,74,75,77,78,82,83,85,86,88,90,91,92,93,94,95,97,98,99,100,102,105,106,110,112,113,114,116,117,119,120,121,122,123,124,126,127,128,134,136,138,142,148],usabl:[33,131],usag:[1,33,81,84,108,139],use_tab_stop:135,useless:[42,92,110],user:[13,19,24,25,27,28,29,32,33,34,35,37,38,39,40,47,51,52,54,55,57,63,66,71,72,74,82,84,85,91,92,95,101,106,107,108,111,124,126,128,129,131,135,139],userdict:61,userid:115,userlist:61,usernam:[74,123,124],userstr:61,usr:[17,27,51,75,85,89,95,109,129,137,139],usual:[12,18,24,25,44,47,51,52,54,55,56,57,59,62,63,64,66,67,69,71,72,79,82,84,85,87,89,92,94,96,101,102,104,106,107,108,111,121,124],utc:111,utf16:111,utf32:111,utf8:111,utf:[24,25,72,93,103,110],utf_8:111,utf_8_decod:111,util:[25,33,35,41,53,55,59,65,79,101,104,106,107,108,121,128,129,130],uuh:86,uvh:129,uw:[66,124],uwpc:[18,51,53,72,95,101,106,107,108,112,123,124,126,128,138],uxxxx:103,v1:134,v3:[74,110,127,128,130,131,137],v:[55,62,66,69,71,81,95,100,101,102,106,107,110,121,129,131,139],vacat:57,vagari:50,vagrant:138,val1:[56,107],val2:[56,107],val3:56,val:[47,69,84,101,106],valgrind:95,valid:[24,35,46,51,71,95,96,103,106,108,111],valid_char:106,validate_by_input:24,valu:[15,17,21,23,24,25,26,27,28,33,34,35,36,44,45,47,48,50,51,54,55,59,61,62,64,66,67,68,70,71,73,78,79,80,84,85,86,88,90,91,92,93,94,95,96,98,99,100,101,102,104,106,109,110,111,128,131,139],valueerr:106,valueerror:[16,38,66,71,100,106,107],vapor:51,var2:62,vari:[16,48,50,55,65,106,129],variabl:[2,33,48,59,62,65,66,80,84,85,88,91,92,93,98,99,103,108,115,134,135,139],variant:94,variat:[90,134],varieti:[94,106,107,135],variou:[12,24,33,34,51,55,69,75,79,81,89,99,101,104,106,107,121],vast:66,ve:[1,13,22,23,25,28,32,33,38,48,51,52,56,59,62,63,64,65,67,73,77,78,81,82,84,85,86,89,92,95,99,100,101,102,103,106,107,110,121,122,123,124,134,135,139,140],vector:[56,101],vendor:111,vener:[120,124],verb:122,verbos:[66,91,96,107,108],veri:[1,24,25,33,34,35,36,39,40,45,48,50,51,54,55,56,59,61,62,63,65,66,69,70,71,73,74,80,81,82,84,85,86,87,89,92,93,94,95,98,99,100,103,104,106,107,108,109,111,117,118,119,121,122,123,126,128,130,133,134,135,137,139,140,141],verifi:[12,84,106,108,118,123],versa:[78,84],version:[8,16,20,24,30,33,34,36,37,39,41,48,49,50,51,55,57,59,61,64,66,67,69,72,75,77,79,81,84,87,91,93,94,95,97,98,103,104,105,106,107,109,110,111,112,113,117,118,121,122,123,124,129,130,131,134,136,137,139,141],version_control:128,version_info:137,versu:[56,84,93,132],vi:[91,106,124,126],via:[33,44,47,53,55,62,63,64,66,74,76,77,78,92,106,108,113,115,128,133,135],vice:[78,84],victor:51,victorlin:82,video:[1,81,110,115,140],view:[35,55,61,115,123,124],view_fruit:35,viewer:[95,128],vii:106,viii:106,viiii:106,vim:[120,128,131,141],vimeo:63,violat:[88,104],virtual:[25,47,57,69,92,94,110,121,124,128,135,138,139],virtual_env:[134,139],virtualbox:138,virtualenv:[95,132,134,135],visibl:[73,93,124],visit:[57,69,100,108,124,134],visited_color:12,visual:[81,95,114,128],visualstudio:66,vital:135,vjog04:[81,110],vm:[110,128,133],voic:51,volatil:108,volum:13,vora:67,vowel:62,vrplumber:95,vs:[10,25,33,51,56,63,91,93,135,157],vscode:128,vv:106,vx:106,w3:24,w3school:24,w:[24,39,54,59,72,82,94,100,126,134],wa:[25,30,33,41,48,49,51,54,55,56,57,59,61,62,64,65,66,67,68,69,71,72,73,79,82,84,85,87,89,90,91,92,93,98,99,100,101,103,106,107,111,120,121,122,124,130,131,139],wai:[1,9,12,13,15,16,17,18,23,24,25,27,30,31,32,33,34,35,48,49,50,51,52,55,56,57,59,60,62,63,64,67,69,71,72,73,74,75,78,79,80,81,82,83,84,85,86,87,89,90,91,92,93,94,95,97,100,101,102,103,104,105,106,107,108,109,110,111,115,116,117,120,121,122,123,124,127,128,131,134,135,136,139,140,142],wait:[25,34,49,55,56,63,65,84,85,89,103,108,109,123,126,135],walk:[25,72,84,103,140],wall:[16,95,106],walnut:52,walnut_parti:52,wan:89,want:[1,2,9,12,15,16,18,23,24,25,27,29,30,31,33,34,38,39,40,44,46,47,48,49,50,51,52,54,55,56,57,59,62,63,64,65,66,67,68,69,70,71,72,74,75,78,79,80,81,82,84,85,86,87,88,89,92,93,94,95,96,97,98,100,101,102,103,104,105,106,107,108,109,110,111,115,118,120,121,123,126,127,128,129,130,131,134,136,138,139,141],war:93,warmup:[14,100],warn:[51,55,56,82,85,92,93,94,108,129,130],warner:92,washington:53,wasn:[71,87,91,92],wast:[95,106,111],watch:[1,55,63,66,71,81,98,106,110,115],water:100,wb:[20,72,94],we:[1,2,6,12,13,14,15,22,23,24,25,29,30,33,34,35,37,40,41,45,47,48,49,50,51,52,54,55,56,59,60,62,63,64,65,66,67,68,70,71,72,73,74,77,78,79,80,81,82,84,85,86,87,89,90,92,93,94,95,96,97,98,99,101,103,104,105,106,107,109,110,111,117,118,119,121,122,123,124,126,128,129,130,131,135,136,138,139],weak:91,wealthi:30,weapon:15,web:[22,24,25,31,33,52,66,74,82,84,85,89,91,111,122,128,129,138,139,159],web_connect:64,websit:[48,81,108,110,115,123,140],week:[1,2,30,61,77],weekdai:57,weekli:77,weigh:128,weight:[48,63,90],weird:[33,50,67,87,93,100,108],welcom:[30,35,115],well:[1,18,21,24,25,30,33,34,35,40,41,44,45,49,50,51,55,56,57,59,62,63,64,65,67,69,70,72,74,78,79,80,81,82,84,89,90,91,92,94,96,98,99,100,101,102,103,104,105,106,107,108,109,110,111,113,114,115,117,119,121,124,126,128,130,131,134,135,136,138,139,141],welsh:51,wendel:106,wendi:36,went:[92,107,110],were:[1,7,25,30,33,44,53,54,55,57,60,61,68,69,70,79,81,82,84,87,92,95,100,106,107,108,111,121,139],western:111,whadaya:103,what:[1,12,13,15,16,18,21,22,23,24,26,30,33,35,37,40,41,45,48,50,51,52,55,56,61,64,66,70,73,74,77,81,83,86,90,93,96,97,98,99,102,103,105,106,109,117,118,121,122,123,124,128,131,135,136],whatev:[27,33,55,72,77,84,92,100,106,108],wheel:[25,89,95],when:[0,14,15,16,17,18,19,24,25,26,27,29,30,31,33,34,35,38,39,40,41,43,44,45,46,48,49,51,52,54,55,56,57,59,61,64,65,66,67,68,69,70,71,72,73,74,78,80,82,85,86,87,88,89,90,91,92,93,95,96,97,98,99,101,102,103,104,105,106,107,108,109,110,111,115,117,120,121,122,123,124,126,128,129,130,131,134,135,136,139,140],whenev:[84,88,99,104,123,129,139],where:[13,18,25,30,33,38,44,50,51,52,55,56,59,62,66,67,69,71,72,73,74,78,79,82,85,87,88,91,94,95,97,98,101,102,103,105,106,107,108,110,111,121,122,123,124,128,129,130,131,135,136,139],wherea:[41,46,98,100],wherev:[67,139],whether:[1,17,25,69,74,82,92,95,98,106,108,109,124,128],which:[2,12,13,14,16,20,22,24,25,27,29,30,33,34,35,39,45,46,51,52,55,56,57,59,62,63,64,66,67,68,69,71,72,73,74,79,80,82,83,84,85,87,89,90,91,92,93,94,95,96,98,99,100,101,103,104,105,106,107,108,109,110,111,118,122,124,126,128,129,130,131,134,136,138,139],whichdb:94,whihc:126,whim:30,white:108,whitespac:[24,25,44,56,85,93,115,127,135],whittington:63,whl:139,who:[17,29,30,39,66,74,81,110,121,123,134],whoa:[100,139],whoaa:25,whole:[24,25,37,55,59,62,69,79,82,87,92,98,106,108,110,123],whoo:25,whoop:25,whose:[84,108],whozit:70,why:[24,25,33,35,41,45,48,51,56,59,62,63,65,66,67,68,70,71,79,83,85,86,87,90,93,95,99,103,104,106,108,111,122],wide:[47,48,50,72,88,92,107,111,121,128,130,135,139],width:[24,25,50],wiki:[9,22,33,50,55,59,60,63,69,74,80,81,82,84,87,90,91,94,95,96,100,101,104,109,110,111,128],wikidef:[66,108],wikipedia:[9,33,50,55,59,60,63,66,74,82,84,87,90,91,94,96,101,104,108,109,110,111,128],wild:103,wildcard:93,william:[28,35],william_gates_iii:39,willing:30,willnot:130,win32:131,win64:131,win:126,wind:[51,103,134],window:[17,19,27,34,72,74,75,89,94,95,103,110,111,114,124,126,133,135,138,139,140],wing:66,winnt:120,winpdb:66,winpdbtutori:66,winpti:131,wire:[55,74,139],wise:[35,101,106],wish:[51,126],within:[30,33,34,73,74,75,85,88,90,91,92,99,108,115,118,123,124,141],without:[1,13,20,21,25,33,50,51,54,55,56,62,66,67,68,71,72,79,80,82,83,84,85,89,90,93,94,98,99,103,104,106,107,108,109,118,121],wjrvveeeg9ll:74,wolframalpha:109,won:[12,24,25,46,47,51,55,56,59,82,92,94,99,100,102,106,111,126,128,131],wonder:[25,63,81,90,94],wood:51,word:[28,51,56,59,66,69,79,84,90,93,94,100,103,106,110,128],word_pair:51,word_wrap:135,wordier:62,wordpress:[87,120],work:[1,2,9,12,13,14,16,17,18,20,24,25,27,28,30,31,33,34,35,39,40,41,42,43,44,45,46,49,50,51,52,53,54,56,57,59,62,63,64,65,66,68,71,72,74,77,79,81,82,85,87,89,90,91,92,93,94,95,98,99,100,101,102,103,104,106,107,108,109,110,111,114,115,117,119,122,125,126,127,128,129,130,131,132,133,134,135,137,138,140,141],worker:[70,109],workfil:72,workflow:[0,121,139],workon_hom:139,workshop:[1,81],workstat:[123,124],world:[25,30,47,56,76,79,92,95,100,103,111,121,122,128,139],worri:[15,33,37,40,51,57,72,74,95,107,111],wors:[57,63,79,88,109],worst:[95,111],worth:[24,48,55,74,87,93,103,108,114,119,122],would:[14,23,24,25,30,33,35,38,40,48,49,50,51,56,59,60,66,67,68,69,74,79,80,81,84,85,87,88,90,95,99,100,104,106,107,108,110,111,118,121,124,134,139],wouldn:[1,24,51,69,82,88,94],wrap:[16,24,44,55,67,69,84,87,91,93,94,96],wrap_width:135,wrapper:[19,24,34,61,64,67,92],wring:51,writ:[73,111,130],writabl:[24,31,84],write:[5,12,13,14,16,18,20,21,22,23,24,25,26,28,32,33,34,35,37,40,41,42,43,45,46,48,49,50,51,52,54,55,56,57,59,60,62,64,66,67,69,71,73,74,78,79,81,82,84,85,87,89,91,92,93,94,95,98,100,101,102,103,104,107,108,109,110,111,115,123,124,128,135,140,149],writeback:94,writelin:72,writer:94,writerow:94,written:[15,16,24,25,50,51,56,60,67,68,73,79,81,82,89,92,93,94,95,106,107,108,111,120,123,124],wrong:[16,25,33,56,68,82,87,92,95,106,107,110,124,129,130,131,137,139],wrote:[25,45,50,70,71,85,106,111],wtf:70,www:[2,3,4,7,8,9,24,27,50,54,55,57,66,68,71,72,74,77,79,81,84,88,89,92,93,94,95,101,103,104,108,109,110,111,116,118,120,121,126,128,129,130,131,135],wx:87,wxpython:[79,80],wxwidget:87,x00:111,x00h:111,x00i:111,x0:109,x1:[101,109],x2:101,x86:[95,126],x86_64:129,x:[17,27,35,50,54,56,57,59,62,63,66,67,69,73,75,78,79,80,83,84,85,86,88,89,93,95,96,98,100,101,102,103,106,108,109,110,111,124,131,133,134,135,141],x_0:50,x_1:50,x_2:50,x_:50,x_i:50,x_lock:[63,109],x_n:50,xb2:111,xc:106,xcix:106,xcode:[92,130],xcx:106,xcxc:106,xfe:111,xfet:111,xff:111,xhh:103,xkcd:94,xl:106,xli:106,xlii:106,xliii:106,xliv:106,xlxl:106,xml:[24,55,111],xrang:[56,109],xs:106,xx:[57,84],xxiiiq:106,xxxi:106,xxxx:[103,106],xxyiii:106,y:[50,54,56,57,59,62,66,73,78,80,83,85,92,98,100,101,102,103,108,109,110,129,131],y_rang:79,ye:[16,25,27,34,40,51,56,66,83,84,85,95,98,100,103,105,106,107,109,122,136],yeach:111,yeah:[25,92],year:[51,86,92,103],yell:[88,103],yellow:[12,134],yet:[25,51,52,59,61,62,66,67,71,73,79,84,97,98,103,106,107,115,129,135],yet_anoth:93,yield:[55,56,64,65,101,108],yield_exampl:79,ymb0l:103,yoru:82,you:[1,2,6,7,8,9,10,12,13,14,15,16,17,19,20,21,22,23,24,26,27,29,30,31,32,33,34,35,36,37,38,39,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,59,60,61,62,63,64,65,66,67,68,69,70,73,74,75,76,77,78,80,81,83,84,85,86,87,88,89,90,92,93,94,95,96,97,98,99,101,102,103,104,105,106,107,108,111,114,115,116,117,118,120,121,122,123,124,125,126,127,128,129,130,131,134,136,137,138,139,140,141,142],your:[0,2,7,12,13,14,15,16,17,18,19,20,22,24,25,27,28,30,31,32,33,35,36,37,38,39,40,41,42,43,44,45,46,49,50,53,55,56,57,59,60,61,63,64,66,67,68,70,71,72,73,75,77,79,82,84,85,87,88,89,90,91,93,94,95,99,100,101,103,104,106,109,111,114,115,121,122,129,130,131,133,134,135,138,139,140,141],your_app:66,yourself:[15,24,25,27,33,51,56,60,67,69,71,73,78,84,87,91,92,97,100,104,105,106,107,111,121,123,124,125,135,136,139],yourusernam:131,youtu:[7,55,87],youtub:[55,71,81,110],yu:121,yum:[92,129],yup:106,z:[19,54,56,59,73,85,92,95,100,106,110,128,131],zed:116,zen:[71,85],zero:[15,17,24,27,45,47,48,55,56,57,68,69,71,102,106,108,109],zerodivisionerror:[16,56,57,66,71],zeroth:[26,80,100],zetcod:103,zfill:128,zip:[31,42,56,78,79,84,92,101,108,109],zipimport:92,zntargvp:44,zodb:94,zola:80,zone:[],zope:89,zsh:134,zuckerberg:[28,35],zzfhjytdceu:55},titles:["Python 310 Class Schedule","Lesson 1: Setting up Your Environment","Lesson 2: Basic Python and Functions","Session 3: Booleans, Sequences, Iteration, and Strings","Session 4: Dictionaries and Sets and Unit Testing","Lession 5: File Handling, Exceptions and Comprehensions","Session 6: Advanced Argument Passing and Modules","Session 7: Object Oriented Programing","Session 8: Properties and Magic methods","Session 9: Static and class methods: multiple inheritance","Session 10: Intro to Functional Programming: lambda and Map, Filter, Reduce","Orientation","args and kwargs Lab","Circle Class Exercise","Practice with CodingBat","Comprehensions Lab","A Couple Handy Context Managers","Dictionary and Set Lab","Exceptions Exercise","Exceptions Lab","File Exercise","File Processing","Fizz Buzz Exercise","Grid Printer Exercise","HTML Renderer Exercise","Tutorial for the Html Render Assignment","lambda and keyword Magic","List Lab","Mailroom","Mailroom \u2013 Decoratoring it","Mailroom - Functional","Mailroom \u2013 metaprogramming it!","Mocking Mailroom","Mailroom - Object Oriented","Mailroom \u2013 as a Python Package","Mailroom Tutorial","Mailroom With Comprehensions","Mailroom With Dicts","Mailroom With Exceptions","Mailroom With Files","Mailroom With Unit Tests","OO Intro - Report Class","A Small Example Package","Python Pushups","ROT13","Fibonacci Series Exercise","Slicing Lab","Sparse Array Exercise","String Formatting Exercise","Threaded Web Scraper","Trapezoidal Rule","Trigrams \u2013 Simple Text Manipulation","Introduction To Unit Testing","Programming in Python","Advanced Argument Passing","Asychronous Programming","Basic Python","Boolean Expressions","Callable Classes","Closures and Function Currying","Code Reviews","The Collections Module","Comprehensions","Concurrent Programming","Context Managers","Notes on Coroutines","Debugging","Decorators","Using a Dictionary to switch","Dictionaries and Sets","Documentation","Exception Handling","File Reading and Writing","More on Functions","Graph Databases","How to run a python file","IPython Parallel Quickstart","Individual Project","Iteration","Iterators and Generators","Anonymous Functions: Lambda","Useful Python Learning Resources","Logging and the logging module","Map Filter and Reduce","Metaprogramming","Code Structure, Modules, and Namespaces","A bit more on mutability (and copies)","Multiple Inheritance","Style and Naming","No SQL Databases","Object Oriented vs Functional Programming","Object Orientation Overview","Packages and Packaging","Coding Style and Linting","Persistence and Serialization","Performance and Profiling","Properties","Python 2 versus Python 3","Python Classes","Recursion","Python Sequences","Special Methods & Protocols","Static and Class Methods","Strings","Subclassing and Inheritance","Submitting your work to gitHub (old)","Test Driven Development","Testing","Advanced Testing","Threading and multiprocessing","Python Tutorial","Unicode in Python","Mailroom","<no title>","Advanced Setup","Turning Atom Into a Lightweight Python IDE","Command line basics","Introduction to your Programming Environment","Feature Branching","Git","Making your text Editor work with git on Windows","git Hints","git Overview","git Workflow","Working with gitHub Classroom","1. Setting up your Environment","Intro to Git","iPython Interpreter","Installing Python and core tools","Setting Up Linux for Python","Setting up OS-X for Python","Setting up Windows for Python","Resources","Setup Details","Shell Customizations for Python Development","Turning Sublime Text Into a Lightweight Python IDE","Submitting your work to gitHub (old)","Testing Your setup","Setting up Python via a Linux VM","Working with Virtualenv","Using Visual Studio Code as a lightweight Python IDE","Using Windows Bash for Python Development","2. Basic Python","3. Booleans and Recursion","4. Sequences and Iteration","5. Basic Text Handling","6. Exception Handling","7. Unit Testing","8. Dictionaries and Sets","9. File Handling","10. Modules and Packages","11. Advanced Argument Passing","12. Comprehensions","13. Intro to Object Oriented Programing","14. Properties and Magic Methods","15. Subclassing and Inheritance","16. Multiple Inheritance","17. Introduction to Functional Programming","18. Advanced Testing","19. Extra Topics"],titleterms:{"0":24,"1":[1,13,17,24,25,27,35,45,89,111,118,124],"10":10,"14":124,"15":124,"16":[111,124],"2":[2,13,17,23,24,25,27,35,79,89,97,111,118,124],"210":[],"3":[3,13,23,24,25,27,89,97,111,124],"310":0,"4":[4,13,24,25,27,66,124],"5":[5,13,24,25,124],"6":[6,13,24,25,124],"7":[7,13,24,25,124],"8":[8,13,24,25,88,93,111,124],"9":[9,13,24,25],"boolean":[3,57,143],"case":[68,103],"catch":[71,106],"class":[0,1,7,9,13,33,41,47,58,60,80,82,84,87,93,98,101,102,103,104],"default":[54,73,86,100],"do":[18,52,55,60,64,71,79,82,85,87,91,92,109,110,134],"final":[66,71,92,123],"float":108,"function":[2,10,23,30,33,54,55,56,59,67,68,70,73,80,83,90,98,110,111,157],"import":[67,85,92,93],"long":[93,103],"new":[30,92,103,105,123,124,136],"public":93,"return":[56,57],"static":[9,102],"super":87,"switch":[68,103],"try":57,"while":78,A:[16,23,24,25,30,35,42,47,49,50,54,62,66,67,69,70,74,79,82,86,92,95,98,106,109,110,122,135,139],And:106,At:1,But:134,By:65,For:[56,78,81,92,130],If:91,In:[56,106],Into:[115,135],Is:[79,104],It:64,No:[78,89,109,128],Not:[100,106],One:[48,55,85,100,106],The:[16,21,22,24,28,29,33,41,44,50,51,55,59,61,64,66,68,69,70,72,79,82,84,86,87,89,91,92,94,98,100,101,106,108,109,110,111,123,127,128,130,131],There:100,To:[52,92,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158],With:[36,37,38,39,40,92],__builtins__:56,__dict__:84,__exit__:64,__init__:[84,104],__new__:84,about:[27,50,53,54,62,66,70,74,79,88,108],accept:124,account:74,accuraci:50,activ:[11,125,139,142,144,145,146,147,148,149,150,151,152,157],actual:85,ad:[24,69,72,84,121,123],addit:4,advanc:[6,54,81,108,114,128,151,154,158],advantag:[63,107],advic:124,after:124,again:134,ahead:24,aiohttp:55,all:[71,89],altern:102,am:[128,134],an:[55,64,67,71,74,79,84,89,92,95,101,114,124,128],anaconda:[89,135],analysi:93,analyz:[93,95],anatomi:108,anonym:80,anywai:111,api:[55,94],app:55,approach:[30,95],appropri:37,ar:[18,57,73,84,90,96,98,100,104,111],arg:[12,54],argument:[6,12,23,50,54,56,80,86,87,93,100,151],arrai:47,articl:87,ask:124,assert:[56,108],assertalmostequ:108,assign:[25,56,69,100,118,124],asychron:55,async:55,asynchron:55,atom:115,attribut:[24,84,89,96,98,104],autocomplet:115,automat:64,await:55,b:[24,25],back:121,background:82,bad:106,bare:71,base:104,bash:141,basic:[2,24,56,82,92,98,103,111,115,116,127,135,140,142,145],basicconfig:82,battl:93,been:[121,124],beginn:81,belong:84,below:66,better:95,big:95,bin:100,binari:[72,92],bind:100,bit:[50,62,74,86,92],blame:121,block:[55,56],book:81,bool:57,boost:95,bound:109,branch:[108,118,121,124],build:[48,51,84,92,103,141],built:[71,84,95,111],builtin:[66,108],bunch:55,buzz:22,c:[91,95],call:[26,55,56,66],callabl:[58,67,101],callback:55,can:[11,54,69,73,79,91,100],cento:129,certain:103,cfg:92,chain:[57,68],challeng:111,chang:[121,123,135],charact:103,check:24,choos:[35,71,100],circl:13,classic:22,classroom:124,clean:139,clear:96,cli:128,client:55,clock:95,clone:124,close:[64,108],closur:[30,59],code:[11,26,30,34,40,51,53,55,56,60,85,93,103,106,107,108,110,115,124,140],codebas:93,codec:111,codingbat:14,collect:61,come:24,comma:100,command:[33,92,95,114,116,118,128],comment:[63,70,110],commit:[121,123,124],common:[54,72],compar:[93,101],compil:92,complet:[81,118,124],complex:[82,92,95,103],composit:104,comprehens:[5,15,36,62,79,83,152],comput:[45,50],concaten:100,concept:91,concis:96,conclus:93,concurr:63,conda:139,condit:[57,109],configpars:94,confus:124,consid:[89,95,124],consider:100,consol:66,constructor:[69,102],contain:101,context:[16,64],context_manag:64,contextmanag:64,continu:93,control:[35,55,78,90,126,128],convent:[93,100],convert:100,copi:[86,100],core:[117,128],coroutin:[55,65],count:[15,69,100],coupl:16,coverag:[32,108],cprofil:95,cpu:109,creat:[28,40,84,89,103,114,118,123,124],credit:[16,50],critic:111,csv:94,cultur:90,current:[24,92],curri:[50,59],cursor:94,custom:[100,101,134],cypher:74,data:[34,35,89,90,92,95,103],data_fil:92,databas:[74,89,94],db:[89,94],dbm:94,deactiv:139,deadlock:109,deal:92,debian:129,debug:[66,115,135],debugg:66,decis:56,decod:111,decor:[29,64,67,84,96],def:56,definit:67,delet:[56,96],design:91,detach:121,detail:[95,133],dev:1,develop:[12,33,51,52,92,106,107,108,117,124,128,134,141],diamond:[9,87],dict:[15,37,51,54,62,68,69],dictionari:[4,15,17,68,69,84,148],did:[24,110],differ:[97,109],dir:84,directori:[72,123],disadvantag:[63,107],dispatch:104,distract:95,distribut:[30,92],distro:129,dive:106,divis:97,doc:[111,141],docstr:70,doctest:108,document:[70,100],doe:[79,82,84,85,87],domin:91,done:[11,25],donor:33,donorcollect:33,doubl:15,down:24,driven:[12,33,51,52,106,107,108],duck:24,dynam:48,eafp:71,easi:108,edit:84,editor:[110,115,120,126,128,135,140],element:[24,25,100],elementtre:94,elif:68,els:[66,71,78],elsewher:85,emul:101,encod:[93,111],encrypt:44,end:1,enough:[56,108],enter:111,entri:92,environ:[1,11,117,125,128,141],equal:56,error:[16,43,71],evalu:81,even:15,event:[55,109],everyth:84,everywher:111,exampl:[26,42,50,53,55,59,64,67,71,74,80,84,85,87,89,92,94,95,101,109],except:[5,18,19,24,38,56,66,71,107,146],execut:[63,94,109],exercis:[1,2,3,4,5,6,7,8,9,10,13,18,20,22,23,24,33,45,47,48,71,95,105,108,109,111,123,136,142,143,145,146,147,148,149,150,152,153,154,155,157,158],exist:107,expect:100,experi:87,explan:118,explor:43,express:[56,57],extend:[100,115,135,140],extens:[95,118],extern:89,extra:[16,33,50,94,159],f:103,fail:106,failur:107,falsi:57,favor:104,featur:[13,109,118],fedora:129,few:[66,69,95,135,139],fibonacci:45,figur:92,file:[5,20,21,30,34,39,72,75,92,94,123,149],filter:[10,15,30,62,82,83],find:59,finish:1,fire:106,first:[80,98,110],five:48,fixtur:[32,108],fizz:22,flake8:93,floatcanva:87,flow:[35,90],fly:24,fold:66,footnot:129,fork:[118,126],format:[21,48,54,94,103],formatt:82,four:48,framework:107,from:[24,66,67,71,84,85,87,92,100,103],frozen:69,full:128,functool:[59,67],further:[67,73],futur:55,gener:[12,13,24,45,62,64,79,101,124],get:[60,69,74,81,89,92,95,130,131],getattr:84,getlogg:82,getter:96,gil:63,git:[2,119,120,121,122,123,124,126,128,129,130,131,137],github:[105,118,124,126,136],give:55,global:[59,73],go:[59,92,128],goal:[12,13,17,20,22,23,24,26,27,28,29,31,44,45,46,48],got:108,gotcha:[73,98,100,111],graph:74,graphendb:74,graphic:122,grid:23,grow:100,gui:66,guidelin:[28,33,40,88],ha:[104,121],halt:106,hand:71,handi:16,handl:[5,24,64,66,71,92,145,146,149],handler:[16,82],happen:[107,139],hash:69,hat:129,have:25,head:121,header:25,heck:111,help:[64,66,92,93,100,111],here:[59,107],hint:[16,22,23,27,44,49,66,100,121],histori:[62,79,92,111],how:[60,75,98,109],html:[24,25],http:55,human:90,hungarian:88,i:[92,128,134],id:[66,114,115,128,135,140],idea:33,ident:[56,126],idiom:[54,72],immut:90,implic:92,improv:124,includ:34,indent:24,index:[69,78,100,101],individu:77,inherit:[9,87,104,155,156],ini:94,initi:[98,123,124],input:[32,51,67,106],ins:87,instal:[11,89,92,107,115,128,139,140,141],instanc:84,instruct:[13,24],integ:57,interact:85,interchang:94,interfac:[33,74,95],interpol:103,interpret:[66,110,127,128,137],intricaci:56,intro:[10,41,126,153],introduct:[1,52,117,157],introspect:84,invok:66,ipython:[76,95,127,129,130,131,137],isclos:108,item:[46,78],iter:[3,69,78,79,97,100,144],itertool:79,itself:128,join:103,joke:103,json:[31,84,94],json_sav:84,just:110,kcachegrind:95,kei:[69,89,95,101],keyword:[12,26,54,56,59,80],know:[71,100,126],kwarg:12,lab:[12,15,17,19,27,46,67,79,84,92,94,111],lambda:[10,26,80],languag:81,larg:93,last:139,latin:111,learn:[1,19,27,81,122],length:[93,100],lession:5,lesson:[1,2],let:126,letter:40,level:[24,34,82,108],lib:94,librari:[67,107],lightweight:[115,135,140],like:[69,90],line:[33,92,93,95,116,118,128],liner:85,lint:[93,115],linux:[92,116,128,129,138],list:[15,24,27,56,62,79,97,100],liter:[56,94,103,111],littl:64,ll:135,local:[59,73],lock:[63,109],log:82,logger:82,look:60,loop:[55,56,62,78],lot:93,luca:45,made:98,magic:[8,26,92,95,154],mailroom:[28,29,30,31,32,33,34,35,36,37,38,39,40,71,112],main:[35,118],make:[11,23,30,34,49,52,56,79,120,123,124],manag:[16,64,74,90,95,109,115,135],manipul:[23,51,84,98],manylinux:92,map:[10,30,62,83],mapper:94,mark:108,master:118,match:87,math:[50,110],mean:72,measur:95,mechan:[87,109,111],member:93,membership:100,memori:95,merg:[118,121,124],messag:109,metaclass:84,metaprogram:[31,84],method:[8,9,69,72,84,87,98,100,101,102,103,104,108,154],minim:128,minimum:128,mint:129,miscellan:100,mix:[64,87],mkproject:139,mkvirtualenv:139,mock:[32,108],mode:72,model:[87,89,91,108],modul:[6,33,61,72,82,85,86,92,94,108,109,111,150],mongo:89,mongodb:89,more:[56,66,73,74,82,86,87,88,92,95,100,103,106,134],motiv:63,mro:87,multipl:[9,23,56,71,87,103,126,156],multipli:100,multiprocess:109,mutabl:[86,89,90,100],mutex:[63,109],my:[55,67,85,92,108],name:[59,88,93,100,101,103,134],namemangl:84,namespac:[78,84,85],nano:120,need:[78,91],neo4j:74,neomodel:74,nest:[62,67],next:81,nifti:[56,78],non:[89,93],nonblock:109,nonloc:59,nose2:108,nosql:[89,94],notat:[88,95],note:[24,65,69,72,79],notepad:120,now:[56,108],number:[15,45,92,110],numer:[101,106],o:95,object:[1,7,33,51,55,71,80,84,86,89,90,91,94,98,101,104,108,153],offic:141,oh:92,old:[103,105,136],onc:78,one:87,onli:[54,73,96,100,128],oo:[41,68,91],open:72,oper:[56,69,101,110],optim:95,option:[3,8,13,24,35,63,81,89,92,94,107],order:[9,69,87,101,104,110],ordin:[44,103],orient:[7,11,33,90,91,153],origin:121,os:[72,92,116,128,130],other:[15,63,69,74,79,89,94,95,97,100,103,107,109],out:[57,89,92,100,121,130,131],over:104,overal:28,overload:101,overrid:104,overview:[91,94,122],own:[52,92,123],packag:[34,42,85,92,109,115,139,150],package_data:92,page:55,paradigm:[90,91],parallel:[63,76,109],paramet:[12,23,54,56,59,64,73,93],parameter:[67,108],parametr:108,pars:21,part:[23,24,25],partial:[50,59],particular:93,pass:[6,24,50,52,54,106,151],password:74,patch:108,path:[20,72,92],pathlib:72,pdb:66,pep8:93,pep:[70,88,93],perform:[63,69,95,100,109],perhap:100,persist:94,person:60,pickl:[63,94],pictur:122,pip:[92,129,130,131],pipe:[63,109],pkg_resourc:92,place:56,placehold:103,plai:15,platform:128,pleas:106,plugin:135,point:[80,92,108],pool:109,pop:69,possibl:124,post:63,pr:[118,123],practic:14,pre:[1,107],preced:56,precis:108,prepar:[1,30,52],pretti:94,primer:24,print:[23,66,82,94,97,107,110],printer:23,problem:[9,22,51,87,100,109],procedur:[12,17,18,27,41,48],process:[20,21,30,51,63,92,109,123],profil:95,program:[7,10,28,33,35,53,55,63,83,90,91,110,117,153,157],programm:110,project:77,properti:[8,67,69,96,154],protocol:[72,79,100,101],pstat:95,pull:[89,124],purpos:87,push:124,pushup:43,put:51,puzzl:14,py2:111,py2neo:74,py:92,pycodestyl:93,pyflak:93,pygam:95,pylint:93,pypi:92,pytest:[16,107,108],python2:97,python3:[79,97],python:[0,2,11,34,43,53,56,57,63,66,75,78,79,81,87,89,91,92,93,94,95,97,98,100,104,110,111,115,117,128,129,130,131,134,135,137,138,140,141,142],qcachegrind:95,question:[95,106,124],queue:[49,109],quick:[74,121],quickstart:76,race:109,rais:[66,71],rang:[56,78],raw:103,rdbm:89,re:85,read:[2,3,4,5,6,7,8,9,10,21,67,72,73,94,96,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158],readi:[60,128],real:[55,59,87],realli:[104,108],reason:139,recommend:[4,92,93],recurs:[73,99,143],red:129,reduc:[10,30,83],reentrant:109,refactor:[60,106],refer:[55,62,82,101,127],regular:55,rel:92,relat:[94,129],reload:92,remind:88,remot:[66,121],render:[24,25],repo:[118,124,126],report:[28,40,41],repositori:126,request:[49,124],requir:[77,78,115,117,123,128,135,140],resolut:[9,87,104],resourc:[15,48,64,81,103,122,132],resubmit:124,result:[18,22,109],review:[60,124],right:24,rlock:109,roman:106,root:91,rot13:44,row:41,royal:93,rule:[50,85,87,106],run:[11,51,55,67,75,92,108,110,115,137,141],runnabl:34,s:[24,59,80,84,87,91,95,101,110,124,134],sched:109,schedul:[0,55,109],schema:89,schemaless:89,scope:[59,73],scraper:49,scratch:84,script:[34,92,115],search:92,self:98,semant:92,semaphor:109,semin:87,send:[28,40],sequenc:[3,46,100,144],seri:[27,45,81],serial:94,server:55,servic:81,session:[3,4,6,7,8,9,10],set:[1,4,11,15,17,54,62,69,84,108,115,125,126,129,130,131,135,138,140,148],setattr:84,setdefault:69,setter:96,setup:[74,82,92,114,123,124,128,133,137],setuptool:92,shallow:100,shell:[114,134],shelv:94,shortcut:57,should:[70,108],shrink:100,side:55,signatur:87,similar:78,simpl:[23,51,89,98],simplest:92,singl:[55,106,108],singleton:[56,84,93],site:[53,81],six:48,size:60,skip:93,slice:[46,100,101],small:[42,92],snakeviz:95,softwar:[91,95],solut:[50,51,92,100],solv:100,some:[7,66,72,87,95,111,124],someth:[69,100],sort:[35,69,100,101],sourc:92,space:[85,93,115,135],spars:47,special:[24,101],specif:94,specifi:24,split:103,sql:[89,94],sqlite:94,stack:[56,66],stacktrac:66,stage:50,standard:[67,101,107],start:[81,89,92,100,105,123,124,136],state:[90,92],statement:98,std:94,step:[13,24,25,45,81,95],stop:7,stopwatch:95,store:89,str:54,strategi:[63,95,109],string:[3,23,48,56,103,111],stringio:72,structur:[34,35,40,51,56,80,85,92,98,126],studio:140,stuff:[69,89],style:[88,93],subclass:[13,104,109,155],sublim:[120,135],submit:[48,105,124,136],subprocess:63,success:108,suggest:1,suit:108,summari:[93,99,101,124],superclass:104,superpow:87,supplement:[2,3,6,7,8,9],support:135,sure:11,sy:92,symbol:56,symbolog:50,symmetri:106,syntax:67,system:[89,128,141],tab:[85,93],tag:24,take:[55,108],task:[26,44,46,48,50,55],tdd:108,teardown:108,termin:[78,129,130,131],terminolog:108,test:[4,12,24,33,34,40,45,46,50,51,52,56,71,74,92,103,106,107,108,130,131,137,147,158],testcas:108,testrunn:108,text:[51,72,110,120,128,135,145],than:87,thank:[28,40],themselv:65,thi:[15,18,53,60,72,84,92],thing:[88,106,124],think:55,thought:123,thread:[49,63,109],threadpool:109,three:48,through:[50,78],thumb:85,time:[16,24,67,95,109],timeit:95,timer:109,todai:95,togeth:51,tool:[66,84,92,93,95,108,128,130,131],top:34,topic:[53,66,95,159],total:101,tour:63,trace:66,traceback:56,trapezoid:50,tree:24,trick:[56,78],trigram:51,truthi:57,tupl:[15,100],turn:[115,135],tutor:81,tutori:[2,25,35,81,110,116,121,122],two:[23,48,50,78,87],type:[24,56,60,61,63,79,84,100,101,104,109],typic:98,ubuntu:129,ui:118,under:92,understand:91,unicod:111,unit:[4,24,40,52,106,108,147],unittest:[107,108],unknown:67,unpack:15,until:106,up:[1,11,48,55,103,104,108,125,126,129,130,131,134,138,139],updat:[30,39,69,124],upstream:121,us:[37,51,55,59,61,62,63,66,67,68,71,72,73,79,80,81,84,87,89,101,103,104,107,108,109,111,115,118,129,130,131,135,139,140,141],usag:82,usual:123,utf:111,v:108,valu:[56,57,69,89,103,108],variabl:[54,56,73,110],ve:108,veri:127,version:[92,115,126,128,135,140],versu:[63,90,97],via:[95,118,138],video:7,virtualenv:[92,139],virtualenvwrapp:139,visual:[66,140],vm:138,vs:[67,73,78,84,85,90,97,100,104,111],wa:134,wai:[66,68],wait:[124,134],want:[124,135],watch:[7,100],we:[100,108],web:[49,55,81],websocket:55,what:[25,54,57,59,60,62,63,65,67,68,69,71,72,79,80,82,84,85,87,88,89,91,92,95,100,101,104,107,108,110,111,126,134,139],wheel:92,when:[11,60,62,63,79,84,100,118],where:[24,37,92,134],which:[115,135,140],whirlwind:63,white:[115,135],why:[18,55,60,82,84,89,92,100,102,118,124,126,128,139],wiki:129,wild:93,window:[92,116,120,128,131,141],within:59,without:63,word:139,work:[15,48,55,60,84,105,118,120,121,123,124,136,139],workflow:[1,118,123,124],workon:139,world:[59,87],wrap:[104,134],write:[7,39,72,106],wsl:141,wxpython:87,x:[92,116,128,130],xml:94,yield:79,you:[11,18,25,28,40,71,72,79,82,91,100,109,110,135],your:[1,11,34,48,51,52,74,81,92,105,107,108,110,117,118,120,123,124,125,126,128,136,137],yourself:64,zero:100,zip:62,zodb:89}}) \ No newline at end of file diff --git a/solutions/mailroom/mailroom_json_save/README.html b/solutions/mailroom/mailroom_json_save/README.html new file mode 100644 index 0000000..cec7bbd --- /dev/null +++ b/solutions/mailroom/mailroom_json_save/README.html @@ -0,0 +1,119 @@ + + + + + + Mailroom — Programming in Python 7.0 documentation + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/solutions/mailroom/mailroom_meta/README.html b/solutions/mailroom/mailroom_meta/README.html new file mode 100644 index 0000000..e5a565d --- /dev/null +++ b/solutions/mailroom/mailroom_meta/README.html @@ -0,0 +1,120 @@ + + + + + + <no title> — Programming in Python 7.0 documentation + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/source/conf.py b/source/conf.py deleted file mode 100644 index e9fb76c..0000000 --- a/source/conf.py +++ /dev/null @@ -1,55 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - - -# -- Project information ----------------------------------------------------- - -project = 'Python 210' -copyright = '2020, Christopher Barker' -author = 'Christopher Barker' - -# The full version, including alpha/beta/rc tags -release = '2.0' - - -# -- General configuration --------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = [] - - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'alabaster' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] \ No newline at end of file diff --git a/source/index.rst b/source/index.rst deleted file mode 100644 index 80686c5..0000000 --- a/source/index.rst +++ /dev/null @@ -1,20 +0,0 @@ -.. Python 210 documentation master file, created by - sphinx-quickstart on Sun May 31 20:06:00 2020. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to Python 210's documentation! -====================================== - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/topics/00-Template_Topic/index.html b/topics/00-Template_Topic/index.html new file mode 100644 index 0000000..36eac6f --- /dev/null +++ b/topics/00-Template_Topic/index.html @@ -0,0 +1,224 @@ + + + + + + + + + + + Topic Name — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/topics/01-setting_up/Py2vsPy3.html b/topics/01-setting_up/Py2vsPy3.html new file mode 100644 index 0000000..fe6a307 --- /dev/null +++ b/topics/01-setting_up/Py2vsPy3.html @@ -0,0 +1,315 @@ + + + + + + + + + + + 3.2.1. Python 2 versus Python 3 — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/topics/01-setting_up/advanced_setup.html b/topics/01-setting_up/advanced_setup.html new file mode 100644 index 0000000..65e8dbc --- /dev/null +++ b/topics/01-setting_up/advanced_setup.html @@ -0,0 +1,157 @@ + + + + + + Advanced Setup — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/atom_as_ide.html b/topics/01-setting_up/atom_as_ide.html new file mode 100644 index 0000000..14cc998 --- /dev/null +++ b/topics/01-setting_up/atom_as_ide.html @@ -0,0 +1,231 @@ + + + + + + Turning Atom Into a Lightweight Python IDE — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/command_line.html b/topics/01-setting_up/command_line.html new file mode 100644 index 0000000..7ea9676 --- /dev/null +++ b/topics/01-setting_up/command_line.html @@ -0,0 +1,163 @@ + + + + + + Command line basics — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/environment_overview.html b/topics/01-setting_up/environment_overview.html new file mode 100644 index 0000000..873700e --- /dev/null +++ b/topics/01-setting_up/environment_overview.html @@ -0,0 +1,166 @@ + + + + + + Introduction to your Programming Environment — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/feature_branching.html b/topics/01-setting_up/feature_branching.html new file mode 100644 index 0000000..cf2f303 --- /dev/null +++ b/topics/01-setting_up/feature_branching.html @@ -0,0 +1,305 @@ + + + + + + Feature Branching — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/git.html b/topics/01-setting_up/git.html new file mode 100644 index 0000000..ccfc2a3 --- /dev/null +++ b/topics/01-setting_up/git.html @@ -0,0 +1,146 @@ + + + + + + Git — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/git_editor_windows.html b/topics/01-setting_up/git_editor_windows.html new file mode 100644 index 0000000..a07b44d --- /dev/null +++ b/topics/01-setting_up/git_editor_windows.html @@ -0,0 +1,166 @@ + + + + + + Making your text Editor work with git on Windows — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/git_hints.html b/topics/01-setting_up/git_hints.html new file mode 100644 index 0000000..ea32968 --- /dev/null +++ b/topics/01-setting_up/git_hints.html @@ -0,0 +1,350 @@ + + + + + + git Hints — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/git_overview.html b/topics/01-setting_up/git_overview.html new file mode 100644 index 0000000..0283418 --- /dev/null +++ b/topics/01-setting_up/git_overview.html @@ -0,0 +1,259 @@ + + + + + + git Overview — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/git_workflow.html b/topics/01-setting_up/git_workflow.html new file mode 100644 index 0000000..6fdfae9 --- /dev/null +++ b/topics/01-setting_up/git_workflow.html @@ -0,0 +1,238 @@ + + + + + + git Workflow — Programming in Python 7.0 documentation + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/github_classroom.html b/topics/01-setting_up/github_classroom.html new file mode 100644 index 0000000..5c55072 --- /dev/null +++ b/topics/01-setting_up/github_classroom.html @@ -0,0 +1,514 @@ + + + + + + Working with gitHub Classroom — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/index.html b/topics/01-setting_up/index.html new file mode 100644 index 0000000..2fbd912 --- /dev/null +++ b/topics/01-setting_up/index.html @@ -0,0 +1,157 @@ + + + + + + 1. Setting up your Environment — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/intro_to_git.html b/topics/01-setting_up/intro_to_git.html new file mode 100644 index 0000000..e64df36 --- /dev/null +++ b/topics/01-setting_up/intro_to_git.html @@ -0,0 +1,222 @@ + + + + + + Intro to Git — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/ipython.html b/topics/01-setting_up/ipython.html new file mode 100644 index 0000000..58ae46f --- /dev/null +++ b/topics/01-setting_up/ipython.html @@ -0,0 +1,220 @@ + + + + + + iPython Interpreter — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/python_and_core_tools.html b/topics/01-setting_up/python_and_core_tools.html new file mode 100644 index 0000000..3018bee --- /dev/null +++ b/topics/01-setting_up/python_and_core_tools.html @@ -0,0 +1,389 @@ + + + + + + Installing Python and core tools — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/python_for_linux.html b/topics/01-setting_up/python_for_linux.html new file mode 100644 index 0000000..e5223b4 --- /dev/null +++ b/topics/01-setting_up/python_for_linux.html @@ -0,0 +1,366 @@ + + + + + + Setting Up Linux for Python — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/python_for_mac.html b/topics/01-setting_up/python_for_mac.html new file mode 100644 index 0000000..a63901d --- /dev/null +++ b/topics/01-setting_up/python_for_mac.html @@ -0,0 +1,312 @@ + + + + + + Setting up OS-X for Python — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/python_for_windows.html b/topics/01-setting_up/python_for_windows.html new file mode 100644 index 0000000..ca188c0 --- /dev/null +++ b/topics/01-setting_up/python_for_windows.html @@ -0,0 +1,301 @@ + + + + + + Setting up Windows for Python — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/resources.html b/topics/01-setting_up/resources.html new file mode 100644 index 0000000..e427ae4 --- /dev/null +++ b/topics/01-setting_up/resources.html @@ -0,0 +1,143 @@ + + + + + + Resources — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/setup_details.html b/topics/01-setting_up/setup_details.html new file mode 100644 index 0000000..6dff239 --- /dev/null +++ b/topics/01-setting_up/setup_details.html @@ -0,0 +1,146 @@ + + + + + + Setup Details — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/shell.html b/topics/01-setting_up/shell.html new file mode 100644 index 0000000..cc7bdcf --- /dev/null +++ b/topics/01-setting_up/shell.html @@ -0,0 +1,303 @@ + + + + + + Shell Customizations for Python Development — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/sublime_as_ide.html b/topics/01-setting_up/sublime_as_ide.html new file mode 100644 index 0000000..afcd848 --- /dev/null +++ b/topics/01-setting_up/sublime_as_ide.html @@ -0,0 +1,270 @@ + + + + + + Turning Sublime Text Into a Lightweight Python IDE — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/submitting_to_github.html b/topics/01-setting_up/submitting_to_github.html new file mode 100644 index 0000000..d1f33b0 --- /dev/null +++ b/topics/01-setting_up/submitting_to_github.html @@ -0,0 +1,182 @@ + + + + + + Submitting your work to gitHub (old) — Programming in Python 7.0 documentation + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/testing_your_setup.html b/topics/01-setting_up/testing_your_setup.html new file mode 100644 index 0000000..dee2fc4 --- /dev/null +++ b/topics/01-setting_up/testing_your_setup.html @@ -0,0 +1,191 @@ + + + + + + Testing Your setup — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/vagrant.html b/topics/01-setting_up/vagrant.html new file mode 100644 index 0000000..037a7ce --- /dev/null +++ b/topics/01-setting_up/vagrant.html @@ -0,0 +1,145 @@ + + + + + + Setting up Python via a Linux VM — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/virtualenv.html b/topics/01-setting_up/virtualenv.html new file mode 100644 index 0000000..95cb8ae --- /dev/null +++ b/topics/01-setting_up/virtualenv.html @@ -0,0 +1,483 @@ + + + + + + Working with Virtualenv — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/vsc_as_ide.html b/topics/01-setting_up/vsc_as_ide.html new file mode 100644 index 0000000..d179715 --- /dev/null +++ b/topics/01-setting_up/vsc_as_ide.html @@ -0,0 +1,179 @@ + + + + + + Using Visual Studio Code as a lightweight Python IDE — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/01-setting_up/windows_bash.html b/topics/01-setting_up/windows_bash.html new file mode 100644 index 0000000..43db0b0 --- /dev/null +++ b/topics/01-setting_up/windows_bash.html @@ -0,0 +1,169 @@ + + + + + + Using Windows Bash for Python Development — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/02-basic_python/index.html b/topics/02-basic_python/index.html new file mode 100644 index 0000000..abbf1e9 --- /dev/null +++ b/topics/02-basic_python/index.html @@ -0,0 +1,158 @@ + + + + + + 2. Basic Python — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/03-recursion_booleans/index.html b/topics/03-recursion_booleans/index.html new file mode 100644 index 0000000..5e0926a --- /dev/null +++ b/topics/03-recursion_booleans/index.html @@ -0,0 +1,145 @@ + + + + + + 3. Booleans and Recursion — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/04-sequences_iteration/index.html b/topics/04-sequences_iteration/index.html new file mode 100644 index 0000000..80e9580 --- /dev/null +++ b/topics/04-sequences_iteration/index.html @@ -0,0 +1,147 @@ + + + + + + 4. Sequences and Iteration — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/05-text_handling/index.html b/topics/05-text_handling/index.html new file mode 100644 index 0000000..fe81d53 --- /dev/null +++ b/topics/05-text_handling/index.html @@ -0,0 +1,152 @@ + + + + + + 5. Basic Text Handling — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/06-exceptions/index.html b/topics/06-exceptions/index.html new file mode 100644 index 0000000..9e0f5b4 --- /dev/null +++ b/topics/06-exceptions/index.html @@ -0,0 +1,150 @@ + + + + + + 6. Exception Handling — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/07-unit_testing/index.html b/topics/07-unit_testing/index.html new file mode 100644 index 0000000..513784f --- /dev/null +++ b/topics/07-unit_testing/index.html @@ -0,0 +1,150 @@ + + + + + + 7. Unit Testing — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/08-dicts_sets/index.html b/topics/08-dicts_sets/index.html new file mode 100644 index 0000000..0921fca --- /dev/null +++ b/topics/08-dicts_sets/index.html @@ -0,0 +1,150 @@ + + + + + + 8. Dictionaries and Sets — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/09-files/index.html b/topics/09-files/index.html new file mode 100644 index 0000000..0339a29 --- /dev/null +++ b/topics/09-files/index.html @@ -0,0 +1,152 @@ + + + + + + 9. File Handling — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/10-modules_packages/index.html b/topics/10-modules_packages/index.html new file mode 100644 index 0000000..d4812ec --- /dev/null +++ b/topics/10-modules_packages/index.html @@ -0,0 +1,154 @@ + + + + + + 10. Modules and Packages — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/11-argument_passing/index.html b/topics/11-argument_passing/index.html new file mode 100644 index 0000000..5c253dc --- /dev/null +++ b/topics/11-argument_passing/index.html @@ -0,0 +1,143 @@ + + + + + + 11. Advanced Argument Passing — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/12-comprehensions/index.html b/topics/12-comprehensions/index.html new file mode 100644 index 0000000..c91c687 --- /dev/null +++ b/topics/12-comprehensions/index.html @@ -0,0 +1,150 @@ + + + + + + 12. Comprehensions — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/13-intro_oo/index.html b/topics/13-intro_oo/index.html new file mode 100644 index 0000000..2c17e6a --- /dev/null +++ b/topics/13-intro_oo/index.html @@ -0,0 +1,147 @@ + + + + + + 13. Intro to Object Oriented Programing — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/14-magic_methods/index.html b/topics/14-magic_methods/index.html new file mode 100644 index 0000000..0431231 --- /dev/null +++ b/topics/14-magic_methods/index.html @@ -0,0 +1,152 @@ + + + + + + 14. Properties and Magic Methods — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/15-subclassing/index.html b/topics/15-subclassing/index.html new file mode 100644 index 0000000..87ae741 --- /dev/null +++ b/topics/15-subclassing/index.html @@ -0,0 +1,145 @@ + + + + + + 15. Subclassing and Inheritance — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/16-multiple_inheritance/index.html b/topics/16-multiple_inheritance/index.html new file mode 100644 index 0000000..ad3a50c --- /dev/null +++ b/topics/16-multiple_inheritance/index.html @@ -0,0 +1,138 @@ + + + + + + 16. Multiple Inheritance — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/17-functional_programming/index.html b/topics/17-functional_programming/index.html new file mode 100644 index 0000000..cdfe051 --- /dev/null +++ b/topics/17-functional_programming/index.html @@ -0,0 +1,156 @@ + + + + + + 17. Introduction to Functional Programming — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/18-advanced_testing/index.html b/topics/18-advanced_testing/index.html new file mode 100644 index 0000000..adf66ac --- /dev/null +++ b/topics/18-advanced_testing/index.html @@ -0,0 +1,143 @@ + + + + + + 18. Advanced Testing — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/18-advnaced_testing/index.html b/topics/18-advnaced_testing/index.html new file mode 100644 index 0000000..e023be1 --- /dev/null +++ b/topics/18-advnaced_testing/index.html @@ -0,0 +1,259 @@ + + + + + + + + + + + 17. Advanced Testing — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file diff --git a/topics/99-extras/index.html b/topics/99-extras/index.html new file mode 100644 index 0000000..4fa9703 --- /dev/null +++ b/topics/99-extras/index.html @@ -0,0 +1,174 @@ + + + + + + 19. Extra Topics — Programming in Python 7.0 documentation + + + + + + + + + + + + + + + + +
      + + +
      + +
      + +
      +
      +
      + + + + \ No newline at end of file diff --git a/topics/index.html b/topics/index.html new file mode 100644 index 0000000..139a636 --- /dev/null +++ b/topics/index.html @@ -0,0 +1,248 @@ + + + + + + + + + + + Programming in Python — Python 210 6.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + + + +
      + + +
      + +
      + +
      + + + + + + + + + + + + \ No newline at end of file